Skip to content
Open
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
8 changes: 8 additions & 0 deletions .github/workflows/mingw-build-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ jobs:
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:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/mingw-build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,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
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/mingw-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4

- 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:
Expand Down
155 changes: 112 additions & 43 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,106 @@ 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 <project>/.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_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}"
--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}" "${OPENMU_PACKETS_XML}"
COMMENT "Generating wire_sizes.generated.h from OpenMU NuGet packet 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"
$<$<BOOL:${ENABLE_EDITOR}>:${CMAKE_CURRENT_SOURCE_DIR}/MuEditor>
)

Expand Down Expand Up @@ -279,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")
Expand All @@ -327,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 <project>/.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)
Expand Down Expand Up @@ -365,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 $<CONFIG> -r ${DOTNET_RID} -p:PlatformTarget=${DOTNET_PLATFORM} -o "${DOTNET_TEMP_OUTPUT_NATIVE}" --nologo
"${_dotnet_for_aot}" publish "${DOTNET_PROJ_NATIVE}" -c $<CONFIG> -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..."
Expand Down Expand Up @@ -397,7 +466,7 @@ if (DOTNET_EXECUTABLE)

add_custom_command(
OUTPUT "${CONSTANTS_REPLACER_OUTPUT}"
COMMAND "${DOTNET_EXECUTABLE}" build "${CONSTANTS_REPLACER_PROJ_NATIVE}" -c $<CONFIG> -o "${CONSTANTS_REPLACER_OUTDIR_NATIVE}" --nologo
COMMAND "${_dotnet_for_aot}" build "${CONSTANTS_REPLACER_PROJ_NATIVE}" -c $<CONFIG> -o "${CONSTANTS_REPLACER_OUTDIR_NATIVE}" --nologo
DEPENDS "${CONSTANTS_REPLACER_PROJ}" ${CONSTANTS_REPLACER_SOURCES}
COMMENT "Building ConstantsReplacer tool..."
VERBATIM
Expand Down
25 changes: 21 additions & 4 deletions src/source/Core/Utilities/Log/muConsoleDebug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -232,6 +233,22 @@ 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);
// C99 4-arg vswprintf -- explicit buffer size, bounded write. The
// 3-arg MS-extension form is unsafe (no size param, can overflow).
_vsnwprintf(szErrorBuffer,
sizeof(szErrorBuffer) / sizeof(szErrorBuffer[0]),
pStr, pArgsForFile);
va_end(pArgsForFile);
g_ErrorReport.Write(L"[MCD_ERROR] %ls\r\n", szErrorBuffer);
}

#ifdef CONSOLE_DEBUG
if (m_bInit)
{
Expand Down
19 changes: 18 additions & 1 deletion src/source/Network/Server/WSclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,17 @@ BOOL ReceiveLogOut(const BYTE* ReceiveBuffer, BOOL bEncrypted)

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<%.64hs>: received %u bytes, expected at least %u -- packet dropped",
packet_type ? packet_type : "?",
static_cast<unsigned>(received), static_cast<unsigned>(expected));
}

BOOL ReceiveJoinMapServer(std::span<const BYTE> ReceiveBuffer)
{
MouseLButton = false;
Expand All @@ -941,7 +952,8 @@ BOOL ReceiveJoinMapServer(std::span<const BYTE> ReceiveBuffer)
CharacterAttribute->AbilityTime[1] = 0;
CharacterAttribute->AbilityTime[2] = 0;

auto const Data = safe_cast<PRECEIVE_JOIN_MAP_SERVER_EXTENDED>(ReceiveBuffer);
auto const Data = safe_cast<PRECEIVE_JOIN_MAP_SERVER_EXTENDED>(
ReceiveBuffer, "PRECEIVE_JOIN_MAP_SERVER_EXTENDED");
if (Data == nullptr)
{
assert(false);
Expand Down Expand Up @@ -13206,6 +13218,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;
Expand Down
22 changes: 19 additions & 3 deletions src/source/Network/Server/WSclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "Dotnet/Connection.h"
#include "Network/Server/CSMapServer.h"
#include <span>
#include <typeinfo>

#define WM_ASYNCSELECTMSG (WM_USER+0)

Expand Down Expand Up @@ -92,11 +93,21 @@ inline uint64_t ntoh64(uint64_t value)
((value & 0xFF00000000000000ULL) >> 56);
}

// Template to cast a span to a packet struct in a safe way.
template <typename T> T* safe_cast(const std::span<const BYTE> span)
// 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);

// 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 <typename T> T* safe_cast(const std::span<const BYTE> span, const char* packet_type = nullptr)
{
if (span.size() < sizeof(T))
{
LogSafeCastSizeMismatch(packet_type ? packet_type : typeid(T).name(),
span.size(), sizeof(T));
return nullptr;
}

Expand Down Expand Up @@ -3657,4 +3668,9 @@ typedef struct
DWORD Money;
DWORD Pause;
} PRECEIVE_MUHELPER_STATUS, * LPRECEIVE_MUHELPER_STATUS;
#pragma pack(pop)
#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"
Loading
Loading