Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ install(DIRECTORY "${VGUI2EXTENSION_GAMEDATA_DIR}/" DESTINATION svencoop/metahoo
option(VGUI2EXTENSION_BUILD_TESTS "Build existing language and IME regression tests" OFF)
if(VGUI2EXTENSION_BUILD_TESTS)
enable_testing()
foreach(test language_registry_tests ime_window_message_test combobox_unicode_tests career_player_name_tests teammenu_hook_tests default_server_name_tests career_popup_order_tests career_profile_storage_tests buildmode_layout_tests career_end_menu_tests)
foreach(test language_registry_tests ime_window_message_test combobox_unicode_tests career_player_name_tests teammenu_hook_tests default_server_name_tests career_popup_order_tests career_profile_storage_tests buildmode_layout_tests career_end_menu_tests border_abi_tests)
add_executable(${test} "tests/${test}.cpp")
target_compile_features(${test} PRIVATE cxx_std_17)
target_compile_options(${test} PRIVATE /W4 /WX /UNDEBUG)
add_test(NAME ${test} COMMAND ${test})
endforeach()
target_sources(career_profile_storage_tests PRIVATE src/CareerProfileStorage.cpp)
target_include_directories(border_abi_tests PRIVATE
"${METAHOOK_SOURCE_PATH}/include/Interface" "${METAHOOK_SOURCE_PATH}/include/VGUI")
endif()

# The aggregate solution owns its single LaunchGame target.
Expand Down
10 changes: 10 additions & 0 deletions memory/build_and_verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ permalink: vgui2extension/build-and-verification

# Build and verification

## Zombie Panic missing images and border ABI (2026-11-07)

- Trigger: zp 10210 constructs its scoreboard using `GetImage("ui/icons/scoreboard/icon_dono0", false)->SetSize(32, 32)`, although that image is absent from the installed resources.
- Root cause: native VGUI returns a Bitmap for the missing image, but our scheme manager deleted invalid Bitmaps and returned NULL. The client's unchecked call then dereferenced zero. Restoring the native getter in a diagnostic process bypassed this failure and exposed a separate `IBorder` virtual-slot mismatch.
- Fix: cache and return invalid Bitmaps, preserving native missing-image behavior; their existing `Paint` implementation safely skips invalid textures. Remove `ParseSideSettings` from the MetaHook public `IBorder` interface so native slot 6/7 dispatch reaches `GetName`/`SetName`; keep parsing as a private nonvirtual `Border` helper.
- Regression: `border_abi_tests` calls slots 6/7 with the native calling convention, fails on the old header, and passes on the fixed header. Aggregate Debug/Win32 build, all 11 plugin CTests, and pinned formatting checks for both components passed.
- Runtime verification: deployed the rebuilt aggregate to Zombie Panic Mod and used CDB without runtime patches. The missing image returned a VGUI2Extension Bitmap and the original client `Start()` returned successfully. Both prior crash paths were traversed without access violations or runtime stack-check failures.
- Limit: halflife-mcp startup then stopped at a different Renderer error: `GameStudioRenderer_StudioDrawModel` is missing from the matched client gamedata snapshot. No successful main-menu/map/visual regression is claimed. Test processes exited.
- Evidence: aggregate `build/x86/Debug/deployment/zp-fixed-start-verification.log` and the game's `fatalerror-20261107-192620-718-16304.log`. Applies to scheme replacements serving native GoldSrc clients, not only zp.

## Build structure

The root CMake uses MSVC x86, C++20, Debug/Release, a static CRT.
Expand Down
8 changes: 2 additions & 6 deletions src/Scheme2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1114,12 +1114,8 @@ IImage* CSchemeManager::GetImage(const char* imageName, bool hardwareFiltered)
return NULL;
}

if (imageName[0] && !pBitmap->IsValid())
{
delete pBitmap;
return NULL;
}

// Native VGUI returns a Bitmap even when the texture is missing. Clients may
// use it without a null check; invalid Bitmaps already skip painting safely.
searchBitmap.bitmap = pBitmap;
m_Bitmaps.Insert(searchBitmap);
return pBitmap;
Expand Down
66 changes: 66 additions & 0 deletions tests/border_abi_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include <vgui/IBorder.h>

#include <cassert>
#include <cstddef>
#include <cstdio>
#include <cstdlib>

class BorderProbe : public vgui::IBorder
{
public:
void Paint(vgui::VPANEL) override {}
void Paint(int, int, int, int) override {}
void Paint(int, int, int, int, int, int, int) override {}
void SetInset(int, int, int, int) override {}
void GetInset(int&, int&, int&, int&) override {}
void ApplySchemeSettings(vgui::IScheme*, KeyValues*) override {}

// If this helper becomes virtual again, native GetName dispatch must fail.
void ParseSideSettings(int, KeyValues*, vgui::IScheme*)
{
std::fputs("Native IBorder dispatch reached ParseSideSettings\n", stderr);
std::exit(EXIT_FAILURE);
}

const char* GetName() override
{
++getNameCalls;
return name;
}

void SetName(const char* value) override
{
++setNameCalls;
name = value;
}

backgroundtype_e GetBackgroundType() override { return BACKGROUND_FILLED; }
bool PaintFirst() override { return false; }

const char* name = nullptr;
int getNameCalls = 0;
int setNameCalls = 0;
};

int main()
{
// GoldSrc clients call these slots using the native IBorder ABI.
constexpr std::size_t NativeGetNameSlot = 6;
constexpr std::size_t NativeSetNameSlot = 7;
using GetNameFn = const char*(__fastcall*)(void*, int);
using SetNameFn = void(__fastcall*)(void*, int, const char*);

BorderProbe border;
auto vtable = *reinterpret_cast<void***>(&border);
auto getName = reinterpret_cast<GetNameFn>(vtable[NativeGetNameSlot]);
auto setName = reinterpret_cast<SetNameFn>(vtable[NativeSetNameSlot]);

assert(nullptr == getName(&border, 0));
assert(1 == border.getNameCalls);

const char name[] = "ScoreboardBorder";
setName(&border, 0, name);
assert(1 == border.setNameCalls);
assert(name == getName(&border, 0));
assert(2 == border.getNameCalls);
}
Loading