From 4e8e5983e51bb141408f50b5f653787ee50b8b81 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Fri, 22 May 2026 12:08:44 +0200 Subject: [PATCH 01/10] Guard server-packet structs against silent size drift Adds two defenses for the bug class behind PR #402 / fixed by #404: 1. static_assert(sizeof == N) on PRECEIVE_CREATE_CHARACTER (19), PRECEIVE_JOIN_MAP_SERVER_EXTENDED (92), and PRECEIVE_REVIVAL_EXTENDED (36). If the #pragma pack(push, 1) ever gets dropped or a field is added/reordered, the build now breaks at the offending struct with a pointer to check the pragma. Wire sizes match OpenMU's XML schema (CharacterInformationExtended, RespawnAfterDeathExtended). 2. Named overload safe_cast(span, "TYPE_NAME") that logs an MCD_ERROR with packet type + received vs expected size on failure. Used at the join-map receive site. The dispatcher also logs the user-visible symptom ("loading screen will appear frozen") so a future drift turns a silent freeze into a one-line console diagnostic. --- src/source/Network/Server/WSclient.cpp | 15 ++++++++++++++- src/source/Network/Server/WSclient.h | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/source/Network/Server/WSclient.cpp b/src/source/Network/Server/WSclient.cpp index e2ea2e584..79655d5ca 100644 --- a/src/source/Network/Server/WSclient.cpp +++ b/src/source/Network/Server/WSclient.cpp @@ -930,6 +930,13 @@ BOOL ReceiveLogOut(const BYTE* ReceiveBuffer, BOOL bEncrypted) int HeroIndex; +void LogSafeCastSizeMismatch(const char* packet_type, std::size_t received, std::size_t expected) +{ + g_ConsoleDebug->Write(MCD_ERROR, + L"safe_cast<%hs>: received %zu bytes, expected at least %zu -- packet dropped", + packet_type ? packet_type : "?", received, expected); +} + BOOL ReceiveJoinMapServer(std::span ReceiveBuffer) { MouseLButton = false; @@ -941,7 +948,8 @@ BOOL ReceiveJoinMapServer(std::span ReceiveBuffer) CharacterAttribute->AbilityTime[1] = 0; CharacterAttribute->AbilityTime[2] = 0; - auto const Data = safe_cast(ReceiveBuffer); + auto const Data = safe_cast( + ReceiveBuffer, "PRECEIVE_JOIN_MAP_SERVER_EXTENDED"); if (Data == nullptr) { assert(false); @@ -13206,6 +13214,11 @@ static void ProcessPacket(const BYTE* ReceiveBuffer, int32_t Size) case 0x03: //receive join map server if (!ReceiveJoinMapServer(received_span)) { + // safe_cast logged the size mismatch; reiterate the user-visible + // symptom so the cause is obvious in the console. + g_ConsoleDebug->Write(MCD_ERROR, + L"[ReceiveJoinMapServer] dropped -- protocol state stays REQUEST_JOIN_MAP_SERVER, " + L"main render will not be enabled (loading screen will appear frozen)."); //return ( FALSE); } break; diff --git a/src/source/Network/Server/WSclient.h b/src/source/Network/Server/WSclient.h index 211acec5c..6cdcf553d 100644 --- a/src/source/Network/Server/WSclient.h +++ b/src/source/Network/Server/WSclient.h @@ -103,6 +103,24 @@ template T* safe_cast(const std::span span) return reinterpret_cast(const_cast(span.data())); } +// Logs a size-mismatch when a typed safe_cast fails. Defined in WSclient.cpp +// so the header stays free of g_ConsoleDebug includes. +void LogSafeCastSizeMismatch(const char* packet_type, std::size_t received, std::size_t expected); + +// Named overload: logs the packet type and observed vs expected size when the +// cast fails. Use this for any packet whose silent failure would be hard to +// diagnose (e.g. server packets that, on size mismatch, would freeze the +// client at a protocol-state transition). +template T* safe_cast(const std::span span, const char* packet_type) +{ + if (auto p = safe_cast(span)) + { + return p; + } + LogSafeCastSizeMismatch(packet_type, span.size(), sizeof(T)); + return nullptr; +} + typedef struct { BYTE Code; @@ -342,6 +360,8 @@ typedef struct //BYTE Equipment[24]; } PRECEIVE_CREATE_CHARACTER, * LPPRECEIVE_CREATE_CHARACTER; #pragma pack(pop) +static_assert(sizeof(PRECEIVE_CREATE_CHARACTER) == 19, + "PRECEIVE_CREATE_CHARACTER wire size drift -- check #pragma pack(push, 1)"); //receive join map server #pragma pack(push, 1) @@ -384,6 +404,9 @@ typedef struct WORD Resets; } PRECEIVE_JOIN_MAP_SERVER_EXTENDED, * LPPRECEIVE_JOIN_MAP_SERVER_EXTENDED; #pragma pack(pop) +static_assert(sizeof(PRECEIVE_JOIN_MAP_SERVER_EXTENDED) == 92, + "PRECEIVE_JOIN_MAP_SERVER_EXTENDED wire size drift -- check #pragma pack(push, 1). " + "OpenMU CharacterInformationExtended is 92 bytes."); #pragma pack(push, 1) typedef struct @@ -403,6 +426,9 @@ typedef struct DWORD Gold; } PRECEIVE_REVIVAL_EXTENDED, * LPPRECEIVE_REVIVAL_EXTENDED; #pragma pack(pop) +static_assert(sizeof(PRECEIVE_REVIVAL_EXTENDED) == 36, + "PRECEIVE_REVIVAL_EXTENDED wire size drift -- check #pragma pack(push, 1). " + "OpenMU RespawnAfterDeathExtended is 36 bytes."); //inventory typedef struct { From 33730c5d78dede8b36fdfb0eccc319f1e5d8b19c Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Fri, 22 May 2026 12:52:12 +0200 Subject: [PATCH 02/10] LogSafeCastSizeMismatch: use %u + cast for older-msvcrt portability Older Windows CRTs (msvcrt.dll pre-VS2015) routed via vswprintf do not recognise the C99 %zu length modifier. Cast the size_t arguments to unsigned and use %u; packet sizes always fit in 32 bits on this i686 target. Per Gemini review on PR #66. --- src/source/Network/Server/WSclient.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/source/Network/Server/WSclient.cpp b/src/source/Network/Server/WSclient.cpp index 79655d5ca..ff97ee74e 100644 --- a/src/source/Network/Server/WSclient.cpp +++ b/src/source/Network/Server/WSclient.cpp @@ -932,9 +932,13 @@ int HeroIndex; void LogSafeCastSizeMismatch(const char* packet_type, std::size_t received, std::size_t expected) { + // %u + cast (instead of %zu) keeps the format compatible with older msvcrt + // builds where vswprintf does not recognise C99 length modifiers. Packet + // sizes always fit in 32 bits. g_ConsoleDebug->Write(MCD_ERROR, - L"safe_cast<%hs>: received %zu bytes, expected at least %zu -- packet dropped", - packet_type ? packet_type : "?", received, expected); + L"safe_cast<%hs>: received %u bytes, expected at least %u -- packet dropped", + packet_type ? packet_type : "?", + static_cast(received), static_cast(expected)); } BOOL ReceiveJoinMapServer(std::span ReceiveBuffer) From ba7f68743024b80c229fa31776e8a87da43d44f1 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Fri, 22 May 2026 13:01:09 +0200 Subject: [PATCH 03/10] safe_cast: log on every failure, not just named-overload calls Merge the two safe_cast overloads into a single template with an optional packet_type. Now every safe_cast(span) call site -- not just the ones updated to pass a name -- emits an MCD_ERROR on size mismatch. When no name is supplied, typeid(T).name() (mangled but readable) is used as a fallback. Keeps g_ConsoleDebug rather than stderr (stderr is invisible in the GUI client). Addresses follow-up review feedback. --- src/source/Network/Server/WSclient.h | 33 +++++++++++----------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/source/Network/Server/WSclient.h b/src/source/Network/Server/WSclient.h index 6cdcf553d..17e082e46 100644 --- a/src/source/Network/Server/WSclient.h +++ b/src/source/Network/Server/WSclient.h @@ -6,6 +6,7 @@ #include "Dotnet/Connection.h" #include "Network/Server/CSMapServer.h" #include +#include #define WM_ASYNCSELECTMSG (WM_USER+0) @@ -92,33 +93,25 @@ inline uint64_t ntoh64(uint64_t value) ((value & 0xFF00000000000000ULL) >> 56); } -// Template to cast a span to a packet struct in a safe way. -template T* safe_cast(const std::span span) -{ - if (span.size() < sizeof(T)) - { - return nullptr; - } - - return reinterpret_cast(const_cast(span.data())); -} - // Logs a size-mismatch when a typed safe_cast fails. Defined in WSclient.cpp // so the header stays free of g_ConsoleDebug includes. void LogSafeCastSizeMismatch(const char* packet_type, std::size_t received, std::size_t expected); -// Named overload: logs the packet type and observed vs expected size when the -// cast fails. Use this for any packet whose silent failure would be hard to -// diagnose (e.g. server packets that, on size mismatch, would freeze the -// client at a protocol-state transition). -template T* safe_cast(const std::span span, const char* packet_type) +// Casts a span to a packet struct, returning nullptr if the buffer is smaller +// than sizeof(T). On failure, logs an MCD_ERROR with the packet type and the +// observed vs expected size so the failure is never silent. Callers SHOULD +// pass a human-readable packet_type; if omitted, typeid(T).name() is used as +// a fallback (the compiler-mangled name, still better than nothing). +template T* safe_cast(const std::span span, const char* packet_type = nullptr) { - if (auto p = safe_cast(span)) + if (span.size() < sizeof(T)) { - return p; + LogSafeCastSizeMismatch(packet_type ? packet_type : typeid(T).name(), + span.size(), sizeof(T)); + return nullptr; } - LogSafeCastSizeMismatch(packet_type, span.size(), sizeof(T)); - return nullptr; + + return reinterpret_cast(const_cast(span.data())); } typedef struct From aa179a24d0ab209c67d855b7359e459d4ecd871d Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Fri, 22 May 2026 17:09:17 +0200 Subject: [PATCH 04/10] MCD_ERROR: log to MuError.log unconditionally; fix GetInstance UB Two coupled fixes that make MCD_ERROR diagnostics actually visible in Release builds: 1. CmuConsoleDebug::GetInstance() now always returns a valid static instance. The previous behaviour (return nullptr unless CSK_LH_DEBUG_CONSOLE was defined) made every g_ConsoleDebug->Write call site a null-pointer member-function call. It "worked" only because the Write body was empty without CONSOLE_DEBUG; any code added there would have crashed in Release. 2. Write() now routes MCD_ERROR to g_ErrorReport.Write (file-based, always compiled) before the existing CONSOLE_DEBUG-gated path. MCD_SEND / MCD_RECEIVE / MCD_NORMAL stay debug-only so production logs don't get flooded. Net effect: any MCD_ERROR call site -- including the safe_cast size mismatch diagnostics added in PR #66 -- now lands in MuError.log even in Release builds. --- .../Core/Utilities/Log/muConsoleDebug.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/source/Core/Utilities/Log/muConsoleDebug.cpp b/src/source/Core/Utilities/Log/muConsoleDebug.cpp index 92655c9b2..b2bd2cf40 100644 --- a/src/source/Core/Utilities/Log/muConsoleDebug.cpp +++ b/src/source/Core/Utilities/Log/muConsoleDebug.cpp @@ -53,12 +53,13 @@ CmuConsoleDebug::~CmuConsoleDebug() CmuConsoleDebug* CmuConsoleDebug::GetInstance() { -#ifdef CSK_LH_DEBUG_CONSOLE + // Always return a valid instance. Previously returned nullptr in builds + // without CSK_LH_DEBUG_CONSOLE, which made every g_ConsoleDebug->Write + // call site a null-deref in disguise (it "worked" only because the Write + // body was empty when CONSOLE_DEBUG was undefined). Returning a real + // instance is required for the always-on MCD_ERROR path below to be safe. static CmuConsoleDebug sInstance; return &sInstance; -#else - return 0; -#endif } void CmuConsoleDebug::UpdateMainScene() @@ -232,6 +233,18 @@ bool CmuConsoleDebug::CheckCommand(const std::wstring& strCommand) void CmuConsoleDebug::Write(int iType, const wchar_t* pStr, ...) { + // MCD_ERROR is always logged to MuError.log, regardless of CONSOLE_DEBUG. + // Other log levels remain debug-only so they don't spam production logs. + if (iType == MCD_ERROR) + { + wchar_t szErrorBuffer[256] = L""; + va_list pArgsForFile; + va_start(pArgsForFile, pStr); + vswprintf(szErrorBuffer, pStr, pArgsForFile); + va_end(pArgsForFile); + g_ErrorReport.Write(L"[MCD_ERROR] %ls\r\n", szErrorBuffer); + } + #ifdef CONSOLE_DEBUG if (m_bInit) { From 6fa8f1c9c4adc5e6b741590d87c180bb07e1c99c Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Fri, 22 May 2026 22:56:14 +0200 Subject: [PATCH 05/10] wire_sizes.generated.h: generate size guards from OpenMU XML Replace the three hand-typed static_asserts with a generated header sourced from OpenMU's authoritative packet schema. Same protection against the PR #402 class of bug, but maintained automatically. Pieces: - third_party/OpenMU: git submodule pinned to 1a56f6dd5. - tools/gen_wire_sizes.py: parses ServerToClientPackets.xml, joins against a hand-maintained packet-name mapping, emits wire_sizes.generated.h with one static_assert(sizeof(X) <= N) per mapped packet. The mapping is currently the three structs PR #66 covered by hand -- adding more is a one-line append. - src/CMakeLists.txt: add_custom_command runs the generator when the XML changes; output goes to ${CMAKE_BINARY_DIR}/generated/, which is added to the include path. Main depends on GenWireSizes. - src/source/Network/Server/WSclient.h: drop the three hand-written asserts; #include the generated header at end-of-file. We assert `<=` rather than `==` because some client structs intentionally don't decode all trailing fields (e.g. PRECEIVE_CREATE_ CHARACTER doesn't decode PreviewData, so its sizeof is 19 vs the wire length 42). The actual safety property is "client struct must fit in wire packet so safe_cast succeeds," which `<=` expresses correctly. To add coverage for another packet: append a tuple to PACKET_MAPPING in tools/gen_wire_sizes.py and rebuild. --- .gitmodules | 4 + src/CMakeLists.txt | 23 ++++++ src/source/Network/Server/WSclient.h | 15 ++-- third_party/OpenMU | 1 + tools/gen_wire_sizes.py | 118 +++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 9 deletions(-) create mode 160000 third_party/OpenMU create mode 100644 tools/gen_wire_sizes.py diff --git a/.gitmodules b/.gitmodules index 6c0b732c8..9f9901c5b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,7 @@ [submodule "src/ThirdParty/SDL_mixer"] path = src/ThirdParty/SDL_mixer url = https://github.com/libsdl-org/SDL_mixer.git +[submodule "third_party/OpenMU"] + path = third_party/OpenMU + url = https://github.com/MUnique/OpenMU.git + branch = master diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8b758ad62..203950e9a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -198,10 +198,33 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") ) endif() +# --- Generated wire-size guards ---------------------------------------------- +# tools/gen_wire_sizes.py reads OpenMU's authoritative packet XML and emits +# wire_sizes.generated.h with static_asserts that lock each client packet +# struct's sizeof <= the wire length declared by the server. Guards against +# the PR #402 class of bug (silent #pragma pack drift that freezes the client). +set(WIRE_SIZES_GEN_SCRIPT "${CMAKE_SOURCE_DIR}/tools/gen_wire_sizes.py") +set(WIRE_SIZES_OPENMU_ROOT "${CMAKE_SOURCE_DIR}/third_party/OpenMU") +set(WIRE_SIZES_XML "${WIRE_SIZES_OPENMU_ROOT}/src/Network/Packets/ServerToClient/ServerToClientPackets.xml") +set(WIRE_SIZES_GEN_HEADER "${CMAKE_BINARY_DIR}/generated/Network/Server/wire_sizes.generated.h") +find_package(Python3 COMPONENTS Interpreter REQUIRED) +add_custom_command( + OUTPUT "${WIRE_SIZES_GEN_HEADER}" + COMMAND "${Python3_EXECUTABLE}" "${WIRE_SIZES_GEN_SCRIPT}" + --openmu-root "${WIRE_SIZES_OPENMU_ROOT}" + --output "${WIRE_SIZES_GEN_HEADER}" + DEPENDS "${WIRE_SIZES_GEN_SCRIPT}" "${WIRE_SIZES_XML}" + COMMENT "Generating wire_sizes.generated.h from OpenMU XML..." + VERBATIM +) +add_custom_target(GenWireSizes DEPENDS "${WIRE_SIZES_GEN_HEADER}") +add_dependencies(Main GenWireSizes) + target_include_directories(Main PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/source" "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/include" "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/netcore/includes" + "${CMAKE_BINARY_DIR}/generated" $<$:${CMAKE_CURRENT_SOURCE_DIR}/MuEditor> ) diff --git a/src/source/Network/Server/WSclient.h b/src/source/Network/Server/WSclient.h index 17e082e46..29c1b8126 100644 --- a/src/source/Network/Server/WSclient.h +++ b/src/source/Network/Server/WSclient.h @@ -353,8 +353,6 @@ typedef struct //BYTE Equipment[24]; } PRECEIVE_CREATE_CHARACTER, * LPPRECEIVE_CREATE_CHARACTER; #pragma pack(pop) -static_assert(sizeof(PRECEIVE_CREATE_CHARACTER) == 19, - "PRECEIVE_CREATE_CHARACTER wire size drift -- check #pragma pack(push, 1)"); //receive join map server #pragma pack(push, 1) @@ -397,9 +395,6 @@ typedef struct WORD Resets; } PRECEIVE_JOIN_MAP_SERVER_EXTENDED, * LPPRECEIVE_JOIN_MAP_SERVER_EXTENDED; #pragma pack(pop) -static_assert(sizeof(PRECEIVE_JOIN_MAP_SERVER_EXTENDED) == 92, - "PRECEIVE_JOIN_MAP_SERVER_EXTENDED wire size drift -- check #pragma pack(push, 1). " - "OpenMU CharacterInformationExtended is 92 bytes."); #pragma pack(push, 1) typedef struct @@ -419,9 +414,6 @@ typedef struct DWORD Gold; } PRECEIVE_REVIVAL_EXTENDED, * LPPRECEIVE_REVIVAL_EXTENDED; #pragma pack(pop) -static_assert(sizeof(PRECEIVE_REVIVAL_EXTENDED) == 36, - "PRECEIVE_REVIVAL_EXTENDED wire size drift -- check #pragma pack(push, 1). " - "OpenMU RespawnAfterDeathExtended is 36 bytes."); //inventory typedef struct { @@ -3676,4 +3668,9 @@ typedef struct DWORD Money; DWORD Pause; } PRECEIVE_MUHELPER_STATUS, * LPRECEIVE_MUHELPER_STATUS; -#pragma pack(pop) \ No newline at end of file +#pragma pack(pop) + +// Generated static_assert size guards. Sourced from OpenMU's authoritative +// packet XML via tools/gen_wire_sizes.py. Must appear after all packet struct +// declarations so the asserts can see them. +#include "Network/Server/wire_sizes.generated.h" \ No newline at end of file diff --git a/third_party/OpenMU b/third_party/OpenMU new file mode 160000 index 000000000..d6689f04c --- /dev/null +++ b/third_party/OpenMU @@ -0,0 +1 @@ +Subproject commit d6689f04c9e561115d91de61a8d1049f9092aa06 diff --git a/tools/gen_wire_sizes.py b/tools/gen_wire_sizes.py new file mode 100644 index 000000000..5508dfad4 --- /dev/null +++ b/tools/gen_wire_sizes.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Generate wire_sizes.generated.h: static_assert size guards for client +packet structs, derived from OpenMU's authoritative packet XML. + +For each entry in PACKET_MAPPING, look up the wire in OpenMU's +ServerToClientPackets.xml and emit: + + static_assert(sizeof() <= , + "wire size drift -- generated from ( bytes)"); + +We use <= rather than == because some client structs intentionally do not +decode all trailing fields (e.g. PreviewData). The bug class we are guarding +against (PR #402) is "client struct grew larger than the wire packet, so +safe_cast rejects it" -- <= catches that exactly. + +To add a packet: append to PACKET_MAPPING. There is no naming convention +linking XML names to C++ struct names, so the mapping must be hand-maintained. +""" + +import argparse +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +# XML packet name (without subcode/code prefix) -> client C++ struct name. +# Seeded with the structs PR #66 covered by hand. +PACKET_MAPPING = [ + ("CharacterCreationSuccessful", "PRECEIVE_CREATE_CHARACTER"), + ("CharacterInformationExtended", "PRECEIVE_JOIN_MAP_SERVER_EXTENDED"), + ("RespawnAfterDeathExtended", "PRECEIVE_REVIVAL_EXTENDED"), +] + + +def load_lengths(xml_path: Path) -> dict[str, int]: + """Return {packet_name: wire_length_bytes} from a Packets XML file.""" + tree = ET.parse(xml_path) + root = tree.getroot() + # Default namespace handling: strip any xmlns prefix so we can match + # element names directly. + def localname(tag: str) -> str: + return tag.rsplit("}", 1)[-1] + + lengths: dict[str, int] = {} + for packet in root.iter(): + if localname(packet.tag) != "Packet": + continue + name = None + length = None + for child in packet: + tag = localname(child.tag) + if tag == "Name": + name = (child.text or "").strip() + elif tag == "Length": + try: + length = int((child.text or "").strip()) + except ValueError: + length = None + if name and length is not None: + lengths[name] = length + return lengths + + +def emit_header(lengths: dict[str, int], out: Path, source_rel: str) -> int: + missing = [xml for xml, _ in PACKET_MAPPING if xml not in lengths] + if missing: + print(f"error: XML packets not found in {source_rel}: {missing}", + file=sys.stderr) + return 1 + + lines = [ + "// THIS FILE IS GENERATED. DO NOT EDIT.", + f"// Source: {source_rel}", + "// Generator: tools/gen_wire_sizes.py", + "//", + "// Static-assert that each client packet struct fits within the wire packet", + "// length declared by OpenMU. Guards against the PR #402 class of bug where a", + "// dropped #pragma pack(1) silently inflates the client struct above the wire", + "// size, causing safe_cast to reject every packet (freezes the loading screen).", + "", + "#pragma once", + "", + ] + for xml_name, cpp_name in PACKET_MAPPING: + n = lengths[xml_name] + lines += [ + f"static_assert(sizeof({cpp_name}) <= {n},", + f" \"wire size drift -- generated from {xml_name} ({n} bytes)\");", + "", + ] + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text("\n".join(lines), encoding="utf-8") + print(f"wrote {out} ({len(PACKET_MAPPING)} asserts)", file=sys.stderr) + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--openmu-root", required=True, type=Path, + help="Path to the OpenMU repo root (e.g. third_party/OpenMU)") + ap.add_argument("--output", required=True, type=Path, + help="Output header path") + args = ap.parse_args() + + xml_path = (args.openmu_root + / "src" / "Network" / "Packets" + / "ServerToClient" / "ServerToClientPackets.xml") + if not xml_path.exists(): + print(f"error: not found: {xml_path}", file=sys.stderr) + return 1 + + lengths = load_lengths(xml_path) + return emit_header(lengths, args.output, + source_rel=str(xml_path.relative_to(args.openmu_root))) + + +if __name__ == "__main__": + raise SystemExit(main()) From bb4130997062288550cb06cd231f5f3681aa6f96 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 23 May 2026 06:23:58 +0200 Subject: [PATCH 06/10] ci: recurse submodules on checkout The codegen step in src/CMakeLists.txt depends on the OpenMU packet XML, which lives in the third_party/OpenMU submodule added by the previous commit. CI was running actions/checkout@v4 without `submodules: recursive` so the submodule directory was empty and the build broke at the codegen step (cannot find ServerToClientPackets.xml). --- .github/workflows/mingw-build-dev.yml | 2 ++ .github/workflows/mingw-build-pr.yml | 2 ++ .github/workflows/mingw-build.yml | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mingw-build-dev.yml b/.github/workflows/mingw-build-dev.yml index c77e7e0e9..80b1c09de 100644 --- a/.github/workflows/mingw-build-dev.yml +++ b/.github/workflows/mingw-build-dev.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Install MinGW-w64 toolchain run: | diff --git a/.github/workflows/mingw-build-pr.yml b/.github/workflows/mingw-build-pr.yml index 935647a9d..623e3a1ac 100644 --- a/.github/workflows/mingw-build-pr.yml +++ b/.github/workflows/mingw-build-pr.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Install MinGW-w64 toolchain run: | diff --git a/.github/workflows/mingw-build.yml b/.github/workflows/mingw-build.yml index 22f49c378..8addbb58f 100644 --- a/.github/workflows/mingw-build.yml +++ b/.github/workflows/mingw-build.yml @@ -18,7 +18,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + with: + submodules: recursive + - name: Install MinGW-w64 toolchain run: | sudo apt-get update From fa80f5ab5d2fc35eef9ebaad1ef081aa13a6ec0f Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 23 May 2026 07:11:12 +0200 Subject: [PATCH 07/10] Pin third_party/OpenMU submodule to 1a56f6dd5 The previous codegen commit recorded the submodule pointer at master HEAD at the time of `git submodule add` (d6689f04), not the commit I actually checked out and built against locally (1a56f6dd5). Local builds worked because the working tree was at the right SHA; CI failed because it clones the recorded pointer, which pointed at a tree where ServerToClientPackets.xml is at a different path/state. Pinning to 1a56f6dd5 -- the OpenMU master HEAD on 2026-05-21 -- which matches the wire sizes the static_asserts encode (CharacterInformation Extended = 92, RespawnAfterDeathExtended = 36, CharacterCreation Successful = 42). --- third_party/OpenMU | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/OpenMU b/third_party/OpenMU index d6689f04c..1a56f6dd5 160000 --- a/third_party/OpenMU +++ b/third_party/OpenMU @@ -1 +1 @@ -Subproject commit d6689f04c9e561115d91de61a8d1049f9092aa06 +Subproject commit 1a56f6dd5ba1229f9bbb3fc9fd5ec01b4864d929 From 093c15b03fe0dd93188f4c5c8cced3182f30b0f3 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 23 May 2026 07:35:39 +0200 Subject: [PATCH 08/10] MCD_ERROR: use C99 4-arg vswprintf; bound type-name field width Per Gemini review on PR #66 (high-priority security finding). The 3-arg vswprintf(buf, fmt, args) is a Microsoft extension with no buffer-size parameter -- a sufficiently long format expansion can overflow the fixed 256-wchar buffer. Switch the new MCD_ERROR path in CmuConsoleDebug::Write to the C99 4-arg form vswprintf(buf, n, fmt, args), matching the convention already used by CErrorReport::Write. Also cap the type-name field in LogSafeCastSizeMismatch with %.64hs so even a pathological typeid(T).name() expansion cannot bloat the format output. Defense in depth at the format-string level on top of the buffer-size fix. The pre-existing 3-arg vswprintf in the legacy CONSOLE_DEBUG-gated console output is left alone (out of scope for this PR). --- src/source/Core/Utilities/Log/muConsoleDebug.cpp | 6 +++++- src/source/Network/Server/WSclient.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/source/Core/Utilities/Log/muConsoleDebug.cpp b/src/source/Core/Utilities/Log/muConsoleDebug.cpp index b2bd2cf40..512ab0243 100644 --- a/src/source/Core/Utilities/Log/muConsoleDebug.cpp +++ b/src/source/Core/Utilities/Log/muConsoleDebug.cpp @@ -240,7 +240,11 @@ void CmuConsoleDebug::Write(int iType, const wchar_t* pStr, ...) wchar_t szErrorBuffer[256] = L""; va_list pArgsForFile; va_start(pArgsForFile, pStr); - vswprintf(szErrorBuffer, pStr, pArgsForFile); + // C99 4-arg vswprintf -- explicit buffer size, bounded write. The + // 3-arg MS-extension form is unsafe (no size param, can overflow). + vswprintf(szErrorBuffer, + sizeof(szErrorBuffer) / sizeof(szErrorBuffer[0]), + pStr, pArgsForFile); va_end(pArgsForFile); g_ErrorReport.Write(L"[MCD_ERROR] %ls\r\n", szErrorBuffer); } diff --git a/src/source/Network/Server/WSclient.cpp b/src/source/Network/Server/WSclient.cpp index ff97ee74e..94bfdf703 100644 --- a/src/source/Network/Server/WSclient.cpp +++ b/src/source/Network/Server/WSclient.cpp @@ -936,7 +936,7 @@ void LogSafeCastSizeMismatch(const char* packet_type, std::size_t received, std: // builds where vswprintf does not recognise C99 length modifiers. Packet // sizes always fit in 32 bits. g_ConsoleDebug->Write(MCD_ERROR, - L"safe_cast<%hs>: received %u bytes, expected at least %u -- packet dropped", + L"safe_cast<%.64hs>: received %u bytes, expected at least %u -- packet dropped", packet_type ? packet_type : "?", static_cast(received), static_cast(expected)); } From a5ffd4a1275156a6bcc0afcf5657d0d01916b350 Mon Sep 17 00:00:00 2001 From: Mosch0512 <48485443+Mosch0512@users.noreply.github.com> Date: Sat, 23 May 2026 08:04:40 +0200 Subject: [PATCH 09/10] Update src/source/Core/Utilities/Log/muConsoleDebug.cpp Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/source/Core/Utilities/Log/muConsoleDebug.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/Core/Utilities/Log/muConsoleDebug.cpp b/src/source/Core/Utilities/Log/muConsoleDebug.cpp index 512ab0243..ccb68b18f 100644 --- a/src/source/Core/Utilities/Log/muConsoleDebug.cpp +++ b/src/source/Core/Utilities/Log/muConsoleDebug.cpp @@ -242,7 +242,7 @@ void CmuConsoleDebug::Write(int iType, const wchar_t* pStr, ...) va_start(pArgsForFile, pStr); // C99 4-arg vswprintf -- explicit buffer size, bounded write. The // 3-arg MS-extension form is unsafe (no size param, can overflow). - vswprintf(szErrorBuffer, + _vsnwprintf(szErrorBuffer, sizeof(szErrorBuffer) / sizeof(szErrorBuffer[0]), pStr, pArgsForFile); va_end(pArgsForFile); From 6adc72994186d6a1eed9e4cec0ee83ac05bb32d2 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 23 May 2026 22:02:19 +0200 Subject: [PATCH 10/10] Source OpenMU packet XML from NuGet instead of git submodule The wire-size codegen needs OpenMU's authoritative ServerToClientPackets.xml. The previous setup vendored the entire OpenMU repository as a git submodule (third_party/OpenMU) -- hundreds of MB of C# server, tooling, and tests just to read one XML file. The .NET ClientLibrary already pulls MUnique.OpenMU.Network.Packets via NuGet (see MUnique.Client.Library.csproj). That package ships all four packet XMLs (ClientToServer, ServerToClient, ChatServer, ConnectServer) under contentFiles/. Sourcing from there means a single versioned dependency (0.9.9) instead of vendoring the entire OpenMU repo, and removes the "MuMain's wire definitions are coupled to a specific OpenMU server commit" framing in favour of a normal package upgrade flow. Changes: - src/CMakeLists.txt: hoist DOTNET_EXECUTABLE detection, mu_native_path(), and MU_NUGET_CACHE_DIR above the wire-size block so codegen can resolve the package path. The wire-size block then builds OPENMU_PACKETS_XML against the cache and, if missing, triggers a one-shot `dotnet restore` of MUnique.Client.Library.csproj at configure time. dotnet restore is fast on cache-hit so re-configuring stays cheap. WSLENV=NUGET_PACKAGES/w is set so the cache override propagates across the WSL->Windows interop boundary when DOTNET_EXECUTABLE is dotnet.exe. - src/CMakeLists.txt: the Native-AOT cross-OS guard now clears a local _dotnet_for_aot variable instead of DOTNET_EXECUTABLE, so a Linux dotnet targeting Windows can still restore the NuGet package for codegen even though it cannot publish the AOT DLL. - tools/gen_wire_sizes.py: take --xml directly (and an optional --source-label for the generated header's provenance comment) instead of --openmu-root + an internal layout assumption. Cleaner decoupling from any specific source layout. - .gitmodules / third_party/OpenMU: removed. The other submodules (imgui, SDL, SDL_mixer) are unaffected -- they auto-init via mu_ensure_submodule() and don't need `submodules: recursive`. - .github/workflows/mingw-build*.yml: drop `submodules: recursive` (only #408 added it, only for the OpenMU submodule). Add actions/setup-dotnet@v4 step so the wire-size codegen's restore-on-demand has a dotnet to invoke. Manually verified locally: configure restores the package to .nuget/munique.openmu.network.packets/0.9.9/, wire_sizes.generated.h emits the same three static_asserts (lengths 42, 96, 36), and the mingw-i686 build links cleanly with the editor-enabled target. --- .github/workflows/mingw-build-dev.yml | 10 +- .github/workflows/mingw-build-pr.yml | 10 +- .github/workflows/mingw-build.yml | 10 +- .gitmodules | 4 - src/CMakeLists.txt | 142 +++++++++++++++++--------- third_party/OpenMU | 1 - tools/gen_wire_sizes.py | 36 ++++--- 7 files changed, 139 insertions(+), 74 deletions(-) delete mode 160000 third_party/OpenMU diff --git a/.github/workflows/mingw-build-dev.yml b/.github/workflows/mingw-build-dev.yml index 80b1c09de..197dbd820 100644 --- a/.github/workflows/mingw-build-dev.yml +++ b/.github/workflows/mingw-build-dev.yml @@ -17,14 +17,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - submodules: recursive - name: Install MinGW-w64 toolchain run: | sudo apt-get update sudo apt-get install -y mingw-w64 g++-mingw-w64-i686 cmake ninja-build + - name: Install .NET SDK 10 + # Required at CMake configure time to restore the + # MUnique.OpenMU.Network.Packets NuGet package, which the wire-size + # codegen reads ServerToClientPackets.xml from. + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Cache libjpeg-turbo (MinGW i686) uses: actions/cache@v4 with: diff --git a/.github/workflows/mingw-build-pr.yml b/.github/workflows/mingw-build-pr.yml index 623e3a1ac..bdb39b789 100644 --- a/.github/workflows/mingw-build-pr.yml +++ b/.github/workflows/mingw-build-pr.yml @@ -20,8 +20,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - submodules: recursive - name: Install MinGW-w64 toolchain run: | @@ -29,6 +27,14 @@ jobs: sudo apt-get update sudo apt-get install -y mingw-w64 g++-mingw-w64-i686 cmake ninja-build wine wine32:i386 + - name: Install .NET SDK 10 + # Required at CMake configure time to restore the + # MUnique.OpenMU.Network.Packets NuGet package, which the wire-size + # codegen reads ServerToClientPackets.xml from. + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Initialise wine prefix run: | wineboot --init >/dev/null 2>&1 || true diff --git a/.github/workflows/mingw-build.yml b/.github/workflows/mingw-build.yml index 8addbb58f..932e2cab1 100644 --- a/.github/workflows/mingw-build.yml +++ b/.github/workflows/mingw-build.yml @@ -18,14 +18,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - submodules: recursive - name: Install MinGW-w64 toolchain run: | sudo apt-get update sudo apt-get install -y mingw-w64 g++-mingw-w64-i686 cmake ninja-build + - name: Install .NET SDK 10 + # Required at CMake configure time to restore the + # MUnique.OpenMU.Network.Packets NuGet package, which the wire-size + # codegen reads ServerToClientPackets.xml from. + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Cache libjpeg-turbo (MinGW i686) uses: actions/cache@v4 with: diff --git a/.gitmodules b/.gitmodules index 9f9901c5b..6c0b732c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,7 +7,3 @@ [submodule "src/ThirdParty/SDL_mixer"] path = src/ThirdParty/SDL_mixer url = https://github.com/libsdl-org/SDL_mixer.git -[submodule "third_party/OpenMU"] - path = third_party/OpenMU - url = https://github.com/MUnique/OpenMU.git - branch = master diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 203950e9a..654a94df7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -198,23 +198,96 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") ) endif() +# --- .NET / NuGet infrastructure (shared by wire-sizes codegen and ClientLibrary) +# Lifted up here so the wire-sizes block below can resolve the OpenMU packet +# XML from the same NuGet cache the ClientLibrary uses. +find_program(DOTNET_EXECUTABLE dotnet.exe) +if (NOT DOTNET_EXECUTABLE) + find_program(DOTNET_EXECUTABLE dotnet) +endif() + +# Helper macro: convert a path to Windows-native format when using +# Windows dotnet.exe from WSL. On all other platforms this is a no-op. +function(mu_native_path input_path output_var) + if (DOTNET_EXECUTABLE MATCHES "\\.exe$" AND CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + execute_process( + COMMAND wslpath -w "${input_path}" + OUTPUT_VARIABLE native_path + RESULT_VARIABLE rc + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if (rc EQUAL 0) + set(${output_var} "${native_path}" PARENT_SCOPE) + else() + message(FATAL_ERROR "wslpath failed for '${input_path}'. Ensure wslpath is available.") + endif() + else() + set(${output_var} "${input_path}" PARENT_SCOPE) + endif() +endfunction() + +# NuGet cache: defaults to /.nuget, override with -DMU_NUGET_CACHE_DIR=... +set(MU_NUGET_CACHE_DIR "${CMAKE_SOURCE_DIR}/.nuget" CACHE PATH "NuGet package cache directory") +file(MAKE_DIRECTORY "${MU_NUGET_CACHE_DIR}") + # --- Generated wire-size guards ---------------------------------------------- # tools/gen_wire_sizes.py reads OpenMU's authoritative packet XML and emits # wire_sizes.generated.h with static_asserts that lock each client packet # struct's sizeof <= the wire length declared by the server. Guards against # the PR #402 class of bug (silent #pragma pack drift that freezes the client). +# +# The XML lives in the MUnique.OpenMU.Network.Packets NuGet package -- the +# same package the .NET ClientLibrary consumes (see MUnique.Client.Library.csproj). +# Sourcing from NuGet means a single versioned dependency instead of vendoring +# the entire OpenMU repository as a submodule. +set(OPENMU_PACKETS_VERSION "0.9.9" CACHE STRING + "Version of MUnique.OpenMU.Network.Packets to source packet XML from") +set(OPENMU_PACKETS_XML + "${MU_NUGET_CACHE_DIR}/munique.openmu.network.packets/${OPENMU_PACKETS_VERSION}/contentFiles/any/net10.0/ServerToClient/ServerToClientPackets.xml") + +# Restore the package on demand at configure time if it isn't cached yet. +# dotnet restore is fast on cache-hit, so re-configuring stays cheap. +if (NOT EXISTS "${OPENMU_PACKETS_XML}") + if (NOT DOTNET_EXECUTABLE) + message(FATAL_ERROR + "wire-size codegen needs MUnique.OpenMU.Network.Packets v${OPENMU_PACKETS_VERSION} " + "but it is not cached at ${MU_NUGET_CACHE_DIR} and no .NET SDK was found to restore " + "it. Install dotnet (Linux or Windows) or pre-populate the NuGet cache.") + endif() + message(STATUS "Restoring MUnique.OpenMU.Network.Packets v${OPENMU_PACKETS_VERSION} for wire-size codegen...") + set(_packets_csproj "${CMAKE_CURRENT_SOURCE_DIR}/../ClientLibrary/MUnique.Client.Library.csproj") + mu_native_path("${_packets_csproj}" _packets_csproj_native) + mu_native_path("${MU_NUGET_CACHE_DIR}" _packets_nuget_native) + # WSLENV=NUGET_PACKAGES/w is needed so the env var crosses the WSL->Windows + # interop boundary when DOTNET_EXECUTABLE is dotnet.exe; harmless otherwise. + execute_process( + COMMAND ${CMAKE_COMMAND} -E env + "WSLENV=NUGET_PACKAGES/w" + "NUGET_PACKAGES=${_packets_nuget_native}" + "${DOTNET_EXECUTABLE}" restore "${_packets_csproj_native}" --nologo + RESULT_VARIABLE _restore_rc + ) + if (NOT _restore_rc EQUAL 0) + message(FATAL_ERROR "dotnet restore failed (rc=${_restore_rc}) -- see output above.") + endif() + if (NOT EXISTS "${OPENMU_PACKETS_XML}") + message(FATAL_ERROR + "dotnet restore completed but ServerToClientPackets.xml is still missing at " + "${OPENMU_PACKETS_XML}. Did the NuGet package layout change?") + endif() +endif() + set(WIRE_SIZES_GEN_SCRIPT "${CMAKE_SOURCE_DIR}/tools/gen_wire_sizes.py") -set(WIRE_SIZES_OPENMU_ROOT "${CMAKE_SOURCE_DIR}/third_party/OpenMU") -set(WIRE_SIZES_XML "${WIRE_SIZES_OPENMU_ROOT}/src/Network/Packets/ServerToClient/ServerToClientPackets.xml") set(WIRE_SIZES_GEN_HEADER "${CMAKE_BINARY_DIR}/generated/Network/Server/wire_sizes.generated.h") find_package(Python3 COMPONENTS Interpreter REQUIRED) add_custom_command( OUTPUT "${WIRE_SIZES_GEN_HEADER}" COMMAND "${Python3_EXECUTABLE}" "${WIRE_SIZES_GEN_SCRIPT}" - --openmu-root "${WIRE_SIZES_OPENMU_ROOT}" + --xml "${OPENMU_PACKETS_XML}" + --source-label "MUnique.OpenMU.Network.Packets v${OPENMU_PACKETS_VERSION} (NuGet)" --output "${WIRE_SIZES_GEN_HEADER}" - DEPENDS "${WIRE_SIZES_GEN_SCRIPT}" "${WIRE_SIZES_XML}" - COMMENT "Generating wire_sizes.generated.h from OpenMU XML..." + DEPENDS "${WIRE_SIZES_GEN_SCRIPT}" "${OPENMU_PACKETS_XML}" + COMMENT "Generating wire_sizes.generated.h from OpenMU NuGet packet XML..." VERBATIM ) add_custom_target(GenWireSizes DEPENDS "${WIRE_SIZES_GEN_HEADER}") @@ -302,46 +375,23 @@ if (MSVC) endif() # .NET Client Library and tools (platform-agnostic, requires dotnet SDK) +# DOTNET_EXECUTABLE, mu_native_path() and MU_NUGET_CACHE_DIR are already +# resolved above (shared with the wire-size codegen block). +# # Native AOT can only target the OS it runs on, so when cross-compiling from # WSL we need the Windows dotnet.exe (available via WSL interop) rather than -# a Linux dotnet. -find_program(DOTNET_EXECUTABLE dotnet.exe) -if (NOT DOTNET_EXECUTABLE) - find_program(DOTNET_EXECUTABLE dotnet) -endif() -if (DOTNET_EXECUTABLE) - # Native AOT cannot cross-compile across OS boundaries. A Linux dotnet - # targeting win-x86/win-x64 will fail. Only proceed when we have a Windows - # dotnet.exe (native or via WSL interop). - if (NOT DOTNET_EXECUTABLE MATCHES "\\.exe$" AND CMAKE_SYSTEM_NAME STREQUAL "Windows") - message(WARNING "Found Linux dotnet but target is Windows. " - "Cross-OS Native AOT is not supported. MUnique.Client.Library.dll will NOT be built. " - "Install the Windows .NET SDK or use WSL interop (dotnet.exe) to enable.") - set(DOTNET_EXECUTABLE "") - endif() +# a Linux dotnet. The AOT publish below is gated on _dotnet_for_aot so that a +# cross-OS scenario only disables the AOT step, not the package restore that +# the wire-sizes block needs. +set(_dotnet_for_aot "${DOTNET_EXECUTABLE}") +if (_dotnet_for_aot AND NOT _dotnet_for_aot MATCHES "\\.exe$" AND CMAKE_SYSTEM_NAME STREQUAL "Windows") + message(WARNING "Found Linux dotnet but target is Windows. " + "Cross-OS Native AOT is not supported. MUnique.Client.Library.dll will NOT be built. " + "Install the Windows .NET SDK or use WSL interop (dotnet.exe) to enable.") + set(_dotnet_for_aot "") endif() -if (DOTNET_EXECUTABLE) - message(STATUS "Found .NET SDK: ${DOTNET_EXECUTABLE}") - - # Helper macro: convert a path to Windows-native format when using - # Windows dotnet.exe from WSL. On all other platforms this is a no-op. - function(mu_native_path input_path output_var) - if (DOTNET_EXECUTABLE MATCHES "\\.exe$" AND CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - execute_process( - COMMAND wslpath -w "${input_path}" - OUTPUT_VARIABLE native_path - RESULT_VARIABLE rc - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if (rc EQUAL 0) - set(${output_var} "${native_path}" PARENT_SCOPE) - else() - message(FATAL_ERROR "wslpath failed for '${input_path}'. Ensure wslpath is available.") - endif() - else() - set(${output_var} "${input_path}" PARENT_SCOPE) - endif() - endfunction() +if (_dotnet_for_aot) + message(STATUS "Found .NET SDK: ${_dotnet_for_aot}") # 1. Define the output path using a variable CMake can understand early set(DOTNET_DLL_PATH "${CMAKE_CURRENT_BINARY_DIR}/MUnique.Client.Library.dll") @@ -350,13 +400,9 @@ if (DOTNET_EXECUTABLE) # Create temp directories for Native AOT build. set(DOTNET_TEMP_OUTPUT "${CMAKE_BINARY_DIR}/dotnet_out") set(DOTNET_TEMP_DIR "${CMAKE_BINARY_DIR}/.dotnet_temp") - - # NuGet cache: defaults to /.nuget, override with -DMU_NUGET_CACHE_DIR=... - set(MU_NUGET_CACHE_DIR "${CMAKE_SOURCE_DIR}/.nuget" CACHE PATH "NuGet package cache directory") set(DOTNET_NUGET_DIR "${MU_NUGET_CACHE_DIR}") file(MAKE_DIRECTORY "${DOTNET_TEMP_OUTPUT}") file(MAKE_DIRECTORY "${DOTNET_TEMP_DIR}") - file(MAKE_DIRECTORY "${DOTNET_NUGET_DIR}") # Convert paths to Windows-native format for MSBuild (no-op outside WSL). mu_native_path("${DOTNET_PROJ}" DOTNET_PROJ_NATIVE) @@ -388,7 +434,7 @@ if (DOTNET_EXECUTABLE) "DOTNET_CLI_HOME=${DOTNET_TEMP_DIR_NATIVE}" "TEMP=${DOTNET_TEMP_DIR_NATIVE}" "TMP=${DOTNET_TEMP_DIR_NATIVE}" - "${DOTNET_EXECUTABLE}" publish "${DOTNET_PROJ_NATIVE}" -c $ -r ${DOTNET_RID} -p:PlatformTarget=${DOTNET_PLATFORM} -o "${DOTNET_TEMP_OUTPUT_NATIVE}" --nologo + "${_dotnet_for_aot}" publish "${DOTNET_PROJ_NATIVE}" -c $ -r ${DOTNET_RID} -p:PlatformTarget=${DOTNET_PLATFORM} -o "${DOTNET_TEMP_OUTPUT_NATIVE}" --nologo COMMAND ${CMAKE_COMMAND} -E copy_if_different "${DOTNET_TEMP_OUTPUT}/MUnique.Client.Library.dll" "${DOTNET_DLL_PATH}" DEPENDS "${DOTNET_PROJ}" ${DOTNET_SOURCES} COMMENT "Checking for .NET Client Library updates..." @@ -420,7 +466,7 @@ if (DOTNET_EXECUTABLE) add_custom_command( OUTPUT "${CONSTANTS_REPLACER_OUTPUT}" - COMMAND "${DOTNET_EXECUTABLE}" build "${CONSTANTS_REPLACER_PROJ_NATIVE}" -c $ -o "${CONSTANTS_REPLACER_OUTDIR_NATIVE}" --nologo + COMMAND "${_dotnet_for_aot}" build "${CONSTANTS_REPLACER_PROJ_NATIVE}" -c $ -o "${CONSTANTS_REPLACER_OUTDIR_NATIVE}" --nologo DEPENDS "${CONSTANTS_REPLACER_PROJ}" ${CONSTANTS_REPLACER_SOURCES} COMMENT "Building ConstantsReplacer tool..." VERBATIM diff --git a/third_party/OpenMU b/third_party/OpenMU deleted file mode 160000 index 1a56f6dd5..000000000 --- a/third_party/OpenMU +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1a56f6dd5ba1229f9bbb3fc9fd5ec01b4864d929 diff --git a/tools/gen_wire_sizes.py b/tools/gen_wire_sizes.py index 5508dfad4..1233202a3 100644 --- a/tools/gen_wire_sizes.py +++ b/tools/gen_wire_sizes.py @@ -2,8 +2,11 @@ """Generate wire_sizes.generated.h: static_assert size guards for client packet structs, derived from OpenMU's authoritative packet XML. -For each entry in PACKET_MAPPING, look up the wire in OpenMU's -ServerToClientPackets.xml and emit: +The XML lives in the MUnique.OpenMU.Network.Packets NuGet package (the same +package the .NET ClientLibrary consumes). CMake resolves the path and passes +it via --xml. + +For each entry in PACKET_MAPPING, look up the wire and emit: static_assert(sizeof() <= , "wire size drift -- generated from ( bytes)"); @@ -61,16 +64,16 @@ def localname(tag: str) -> str: return lengths -def emit_header(lengths: dict[str, int], out: Path, source_rel: str) -> int: +def emit_header(lengths: dict[str, int], out: Path, source_label: str) -> int: missing = [xml for xml, _ in PACKET_MAPPING if xml not in lengths] if missing: - print(f"error: XML packets not found in {source_rel}: {missing}", + print(f"error: XML packets not found in {source_label}: {missing}", file=sys.stderr) return 1 lines = [ "// THIS FILE IS GENERATED. DO NOT EDIT.", - f"// Source: {source_rel}", + f"// Source: {source_label}", "// Generator: tools/gen_wire_sizes.py", "//", "// Static-assert that each client packet struct fits within the wire packet", @@ -96,22 +99,25 @@ def emit_header(lengths: dict[str, int], out: Path, source_rel: str) -> int: def main() -> int: ap = argparse.ArgumentParser() - ap.add_argument("--openmu-root", required=True, type=Path, - help="Path to the OpenMU repo root (e.g. third_party/OpenMU)") + ap.add_argument("--xml", required=True, type=Path, + help="Path to ServerToClientPackets.xml " + "(from the MUnique.OpenMU.Network.Packets NuGet " + "package contentFiles)") + ap.add_argument("--source-label", default=None, + help="Optional label written into the generated header's " + "'Source:' comment (e.g. the NuGet package " + "version). Defaults to the XML basename.") ap.add_argument("--output", required=True, type=Path, help="Output header path") args = ap.parse_args() - xml_path = (args.openmu_root - / "src" / "Network" / "Packets" - / "ServerToClient" / "ServerToClientPackets.xml") - if not xml_path.exists(): - print(f"error: not found: {xml_path}", file=sys.stderr) + if not args.xml.exists(): + print(f"error: not found: {args.xml}", file=sys.stderr) return 1 - lengths = load_lengths(xml_path) - return emit_header(lengths, args.output, - source_rel=str(xml_path.relative_to(args.openmu_root))) + lengths = load_lengths(args.xml) + label = args.source_label or args.xml.name + return emit_header(lengths, args.output, source_label=label) if __name__ == "__main__":