#include "90CN_seria_minimap.h" #include "90CN_seria_room_policy.h" #include namespace dnf90_seria { static Logger LogLine; static HMODULE g_image; static volatile LONG g_ready; static SRWLOCK g_lock = SRWLOCK_INIT; static Lifecycle g_owner = {}; static void* g_original[8]; typedef uintptr_t (__thiscall* TownResourceLookup)(void*, int, int); static TownResourceLookup g_titleTownLookup; enum Kind { Setter, Bind, TownGet, Region, Draw, Marker, Rect }; static uintptr_t Va(uintptr_t address) { return reinterpret_cast(g_image) + address - 0x400000; } // PUSHAD layout. Native argument words, register/flag/FPU/SSE state, return // values and RET n cleanup are forwarded verbatim; no guessed C++ native ABI. struct Registers { DWORD edi, esi, ebp, esp, ebx, edx, ecx, eax, flags; }; struct Display { Owner owner; uintptr_t ui, entry, marker; int region; }; struct Frame { uintptr_t nativeStack, caller, self, esi; Kind kind; int town, area; bool active; Display display; ULONGLONG entered; }; struct PacketFrame { unsigned ticket; unsigned opcode; Notice notice; }; struct ThreadState { Frame hooks[128]; unsigned depth; PacketFrame packets[8]; unsigned packetDepth, nextTicket; }; static __declspec(thread) ThreadState g_thread = {}; static bool ReadScene(Scene* scene) { *scene = Scene(); __try { uintptr_t root = *reinterpret_cast(Va(0x051b1290)); if (!root) return false; scene->world = *reinterpret_cast(root + 0xb4); if (!scene->world) return false; scene->town = *reinterpret_cast(scene->world + 0x1e4); uintptr_t area = *reinterpret_cast(scene->world + 0x1e8); scene->area = area ? *reinterpret_cast(area) : -1; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { *scene = Scene(); return false; } } static uintptr_t ReadUi() { __try { uintptr_t manager = *reinterpret_cast(Va(0x051b2764)); return manager ? *reinterpret_cast(manager + 0xb7f0) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { return 0; } } static bool ReadDisplay(Display* value, bool binding) { *value = Display(); Scene scene; if (!ReadScene(&scene) || !Shared(scene)) return false; AcquireSRWLockShared(&g_lock); value->owner = g_owner.context; ReleaseSRWLockShared(&g_lock); if (value->owner.world != scene.world || !FindTown(value->owner.town)) return false; value->ui = ReadUi(); if (!value->ui) return false; __try { value->entry = *reinterpret_cast(value->ui + 0x2c); if (!value->entry) return binding; value->region = *reinterpret_cast(value->entry + 0x68); value->marker = *reinterpret_cast(value->entry + 0x64); bool supported = value->marker && *reinterpret_cast(value->marker) == Va(0x045a6dd8) && *reinterpret_cast(value->marker + 4) == 2 && *reinterpret_cast(value->marker + 0x5c) != 0xffff; // Binding precedes native marker construction. A previous region's // marker must not veto the FIRST resource selection on cold login. return binding || supported; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } static bool SameDisplay(const Display& a, const Display& b) { return a.owner.world == b.owner.world && a.owner.serial == b.owner.serial && a.owner.town == b.owner.town && a.entry == b.entry && a.marker == b.marker && b.region == b.owner.region; } static Frame* Current(Kind kind) { for (unsigned n = g_thread.depth; n; --n) { Frame* frame = g_thread.hooks + n - 1; if (frame->kind != kind) continue; // The innermost frame owns this call, even when it is ineligible. return frame->active && GetTickCount64() - frame->entered < 1500 ? frame : 0; } return 0; } static bool ValidFrame(Frame* frame, Display* display) { return frame && ReadDisplay(display, false) && SameDisplay(frame->display, *display); } static void BindPending() { if (!g_thread.packetDepth) return; PacketFrame* packet = g_thread.packets + g_thread.packetDepth - 1; if (packet->opcode != 24) return; Scene scene; if (!ReadScene(&scene)) return; AcquireSRWLockExclusive(&g_lock); bool changed = g_owner.Bind(packet->notice, scene); Owner owner = g_owner.context; ReleaseSRWLockExclusive(&g_lock); if (changed) LogLine("Seria minimap owner bound town=%d region=%d serial=%u (UI only)", owner.town, owner.region, owner.serial); } unsigned BeginPacket(int packet) { if (!g_ready || !packet || g_thread.packetDepth >= 8) return 0; unsigned opcode; __try { opcode = *reinterpret_cast(packet + 1); } __except (EXCEPTION_EXECUTE_HANDLER) { return 0; } PacketFrame value = {}; value.opcode = opcode; if (opcode == 24) ReadScene(&value.notice.before); value.ticket = ++g_thread.nextTicket; if (!value.ticket) value.ticket = ++g_thread.nextTicket; g_thread.packets[g_thread.packetDepth++] = value; return value.ticket; } void EndPacket(unsigned ticket) { if (!ticket || !g_thread.packetDepth) return; PacketFrame* value = g_thread.packets + g_thread.packetDepth - 1; if (value->ticket != ticket) return; if (value->opcode == 24) { Scene after; if (ReadScene(&after)) { AcquireSRWLockExclusive(&g_lock); g_owner.Finish(value->notice, after); ReleaseSRWLockExclusive(&g_lock); } } --g_thread.packetDepth; } static void ReturnThunk(); static void __cdecl Before(unsigned id, Registers* registers) { if (!g_ready || id > Rect) return; uintptr_t* stack = reinterpret_cast(registers + 1); // A native SEH unwind can bypass a return thunk. Discard only frames whose // original stack was actually unwound; never reuse a stale marker scope. while (g_thread.depth && *stack != reinterpret_cast(&ReturnThunk) && reinterpret_cast(stack) >= g_thread.hooks[g_thread.depth-1].nativeStack) --g_thread.depth; if (g_thread.depth == 128) return; // no return-address rewrite on overflow Frame value = {}; value.kind = static_cast(id); value.nativeStack = reinterpret_cast(stack); value.caller = *stack; value.self = registers->ecx; value.esi = registers->esi; value.entered = GetTickCount64(); __try { if (id == Setter || id == Region || id == Rect) value.town = static_cast(stack[1]); if (id == Rect) value.area = static_cast(stack[2]); } __except (EXCEPTION_EXECUTE_HANDLER) { return; } if (id == Setter && value.caller == Va(0x01d82152) && g_thread.packetDepth && g_thread.packets[g_thread.packetDepth-1].opcode == 1375) { Scene scene; if (ReadScene(&scene) && scene.world == value.self) { AcquireSRWLockExclusive(&g_lock); g_owner.Seed(scene.world, value.town); ReleaseSRWLockExclusive(&g_lock); LogLine("Seria minimap explicit initial owner town=%d (native cache untouched)", value.town); } } else if (id == Bind) { value.active = value.self && value.self == ReadUi(); if (value.active) BindPending(); // before the binder's FIRST town getter } else if (id == Draw) { value.active = ReadDisplay(&value.display, false) && value.self == value.display.entry && value.display.region == value.display.owner.region; } else if (id == Marker) { Frame* draw = Current(Draw); value.active = ValidFrame(draw, &value.display) && value.self == value.display.marker; } g_thread.hooks[g_thread.depth++] = value; *stack = reinterpret_cast(&ReturnThunk); } // Synthetic descriptors are DLL-owned UI data, not a native scene/PVF object. struct Descriptor { int town, area; int* geometry; int rect[4]; }; static Descriptor g_rects[sizeof(kGlyphs)/sizeof(kGlyphs[0])]; static uintptr_t DescriptorFor(const GlyphRow* glyph) { return glyph ? reinterpret_cast(g_rects + (glyph - kGlyphs)) : 0; } static uintptr_t ProjectRect(const Frame& frame, uintptr_t original) { Display state; if (frame.caller != Va(0x0224e083) || frame.town != 38 || frame.area != 1 || !ValidFrame(Current(Marker), &state) || frame.self != state.entry) return original; const GlyphRow* glyph = 0; __try { const int* geometry = original ? *reinterpret_cast(original + 8) : 0; glyph = BorrowGlyph(state.owner.town, original == 0, geometry); } __except (EXCEPTION_EXECUTE_HANDLER) { return original; } return glyph ? DescriptorFor(glyph) : original; } static void __cdecl After(Registers* registers) { // Return target resides in the reserved word ABOVE saved flags. Popping // this word with RET preserves every native output register and ESP. uintptr_t* target = reinterpret_cast(registers + 1); if (!g_thread.depth) { // A missing target is an invariant violation, not a recoverable UI // state. It cannot be produced by disabled/overflow hooks (no rewrite). RaiseFailFastException(0, 0, 0); return; } Frame frame = g_thread.hooks[--g_thread.depth]; *target = frame.caller; if (!g_ready) return; Display state; if (frame.kind == TownGet && registers->eax == 38 && ReadDisplay(&state, frame.caller == Va(0x01c987f6) || frame.caller == Va(0x01c9bc5b)) && frame.self == state.owner.world) { Frame* bind = Current(Bind); bool allowed = (frame.caller == Va(0x01c987f6) && bind && bind->self == state.ui) || frame.caller == Va(0x01c9bc5b) || (frame.caller == Va(0x022478bd) && frame.esi == state.marker && ValidFrame(Current(Draw), &state)); if (allowed) registers->eax = state.owner.town; } else if (frame.kind == Region && frame.town == 38 && registers->eax == 18 && frame.caller == Va(0x0223c4fd) && ValidFrame(Current(Marker), &state)) { registers->eax = state.owner.region; } else if (frame.kind == Rect) { registers->eax = static_cast(ProjectRect(frame, registers->eax)); } } // sub_3411790 binds the minimap, then independently copies the REAL town // resource's name at 3412282. Its room38 name bypasses scene_town_get entirely. // Redirect only that CALL's string source; native copy/destruction still owns // the value, and the scene, town resource and global string copier stay intact. static void __cdecl BeforeTitleCopy(Registers* registers) { if (!g_ready || !g_titleTownLookup || !g_thread.packetDepth || g_thread.packets[g_thread.packetDepth-1].opcode != 24) return; uintptr_t* stack = reinterpret_cast(registers + 1); if (*stack != Va(0x03412287)) return; Display state; if (!ReadDisplay(&state, true) || state.region != state.owner.region || registers->edi != state.owner.world || state.owner.town == 38) return; __try { // The guarded caller takes this exact source from its scene resource. uintptr_t realTown = *reinterpret_cast(registers->ebp - 0x24); if (!realTown || stack[1] != realTown + 4) return; uintptr_t loader = *reinterpret_cast(Va(0x051b26d8)); // This singleton is constructed with town resource type 12 at // 22938d1..22938df; the generic lookup must not parse another type. if (!loader || *reinterpret_cast(loader + 0x22c) != 12) return; // Same native cache-enabled resource lookup used by scene entry. uintptr_t town = g_titleTownLookup(reinterpret_cast(loader), state.owner.town, 1); if (!town) return; uintptr_t name = town + 4; unsigned length = *reinterpret_cast(name + 0x14); unsigned capacity = *reinterpret_cast(name + 0x18); if (!length || length > capacity) return; Display current; if (!ReadDisplay(¤t, true) || !SameDisplay(state, current)) return; stack[1] = name; LogLine("Seria minimap title projected town=%d region=%d serial=%u (native resource name)", state.owner.town, state.owner.region, state.owner.serial); } __except (EXCEPTION_EXECUTE_HANDLER) { return; } } // Preserve flags and the complete x87/MMX/SSE state around our observer, not // merely callee-saved integer registers. FX area is 16-byte aligned even when // the game's incoming stack is not. The native function sees its exact stack. #define SAVE_OBSERVER __asm { pushfd } __asm { pushad } __asm { sub esp, 528 } \ __asm { lea eax, [esp+15] } __asm { and eax, -16 } __asm { fxsave [eax] } #define RESTORE_OBSERVER __asm { lea eax, [esp+15] } __asm { and eax, -16 } \ __asm { fxrstor [eax] } __asm { add esp, 528 } __asm { popad } __asm { popfd } #define ENTRY(name, id, offset) static void __declspec(naked) name() { \ SAVE_OBSERVER __asm { lea eax, [esp+528] } __asm { push eax } __asm { push id } \ __asm { call Before } __asm { add esp, 8 } RESTORE_OBSERVER \ __asm { jmp dword ptr [g_original+offset] } } ENTRY(SetterEntry, 0, 0) ENTRY(BindEntry, 1, 4) ENTRY(TownEntry, 2, 8) ENTRY(RegionEntry, 3, 12) ENTRY(DrawEntry, 4, 16) ENTRY(MarkerEntry, 5, 20) ENTRY(RectEntry, 6, 24) static void __declspec(naked) TitleCopyEntry() { SAVE_OBSERVER __asm { lea eax, [esp+528] } __asm { push eax } __asm { call BeforeTitleCopy } __asm { add esp, 4 } RESTORE_OBSERVER __asm { jmp dword ptr [g_original+28] } } static void __declspec(naked) ReturnThunk() { __asm { push 0 } SAVE_OBSERVER __asm { lea eax, [esp+528] } __asm { push eax } __asm { call After } __asm { add esp, 4 } RESTORE_OBSERVER __asm { ret } } #undef ENTRY #undef SAVE_OBSERVER #undef RESTORE_OBSERVER struct Hook { const char* name; uintptr_t address; const char* bytes; unsigned size; void* entry; uintptr_t callTarget; BYTE saved[16]; }; static Hook g_hooks[] = { {"world_town_set",0x03406800,"558bec568b7508",7,SetterEntry}, {"minimap_bind",0x01c987c0,"568bf1ff156c261a04",9,BindEntry}, {"scene_town_get",0x03406320,"8b81e4010000",6,TownEntry}, {"town_region_resolve",0x0392cc90,"558bec83ec18",6,RegionEntry}, {"minimap_draw_layers",0x022476c0,"558bec6aff",5,DrawEntry}, {"marker_update",0x0223c460,"558bec83ec0c",6,MarkerEntry}, {"map_rect_lookup",0x0224d960,"558bec83ec18",6,RectEntry}, {"minimap_title_copy",0x03412282,"e849a732fd",5,TitleCopyEntry,0x0073c9d0} }; static const unsigned kHookCount = sizeof(g_hooks)/sizeof(g_hooks[0]); struct Guard { uintptr_t address; const char* bytes; }; static const Guard g_guards[] = { {0x03406818,"85c0740980b8f0000000017443"}, {0x01d8214d,"e8ae4668018be55dc3"}, {0x01c987f1,"e82adb760150e89444c901"}, {0x01c9bc56,"e8c5a6760150e82f10c901"}, {0x022478b8,"e863ea1b0150e8cd536e0183c4043b43687568"}, {0x02247884,"8b34b985f6744d837e0402753a"}, {0x0223c4f8,"e893076f018bf883c404"}, {0x0224e07e,"e8ddf8ffff8b4d0c"}, {0x03680720,"8b412cc3"}, {0x034e4b90,"8b4164c3"}, {0x01c43fa3,"e8e80b8a0183c008"}, {0x01c44239,"e8420b0500"}, {0x0224821e,"8b55e4897a64"}, {0x02240c56,"895e08895e0c"}, {0x022479a2,"c23800"}, {0x03411950,"6a01899fe40100008b0dd8261b0553e84c8704008945dc"}, {0x03412273,"8b45dc83ec2083c0048bcc8965d050e849a732fdc745fc03000000c745fcffffffff8bcee8142183fe"}, {0x0345a0b0,"558bec5356578bf1e873f9ffff85c074118b5d08538bc8e8c4aa19008bf885ff75095f5e33c05b5dc208008b450c508bce899e34020000e804ad37ff578bcee8acfc13005fc78634020000ffffffff5e5b5dc20800"}, {0x0073c9d0,"558bec6aff6868f6f90364a1000000005083ec1456a13c670e05"} }; static int Hex(char c) { return c >= '0' && c <= '9' ? c-'0' : c >= 'a' && c <= 'f' ? c-'a'+10 : -1; } static bool Match(uintptr_t address, const char* bytes) { __try { size_t length = strlen(bytes); if (length % 2) return false; for (size_t i = 0; i < length/2; ++i) if (*reinterpret_cast(Va(address)+i) != (Hex(bytes[2*i])*16+Hex(bytes[2*i+1]))) return false; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } static bool WriteCode(void* destination, const void* source, unsigned size) { DWORD protection = 0; if (!VirtualProtect(destination, size, PAGE_EXECUTE_READWRITE, &protection)) return false; memcpy(destination, source, size); FlushInstructionCache(GetCurrentProcess(), destination, size); DWORD ignored; return VirtualProtect(destination, size, protection, &ignored) != FALSE; } static void Jump(BYTE* destination, uintptr_t source, uintptr_t target) { destination[0] = 0xe9; *reinterpret_cast(destination+1) = static_cast(target - source - 5); } bool Install(HMODULE image, Logger logger) { if (g_ready) return true; g_image = image; LogLine = logger; if (!image || !logger) return false; // Verify ALL entry/caller/layout guards before allocation or any hook. for (unsigned n = 0; n < sizeof(g_hooks)/sizeof(g_hooks[0]); ++n) if (!Match(g_hooks[n].address, g_hooks[n].bytes)) { LogLine("Seria minimap disabled: native entry mismatch %s", g_hooks[n].name); return false; } for (unsigned n = 0; n < sizeof(g_guards)/sizeof(g_guards[0]); ++n) if (!Match(g_guards[n].address, g_guards[n].bytes)) { LogLine("Seria minimap disabled: native guard mismatch %08X", static_cast(g_guards[n].address)); return false; } g_titleTownLookup = reinterpret_cast(Va(0x0345a0b0)); for (unsigned n = 0; n < sizeof(g_rects)/sizeof(g_rects[0]); ++n) { g_rects[n].town = 38; g_rects[n].area = 1; g_rects[n].geometry = g_rects[n].rect; memcpy(g_rects[n].rect, kGlyphs[n].rect, sizeof(g_rects[n].rect)); } unsigned allocated = 0, written = 0; bool prepared = true, patched = true; for (; allocated != kHookCount; ++allocated) { Hook& hook = g_hooks[allocated]; memcpy(hook.saved, reinterpret_cast(Va(hook.address)), hook.size); if (hook.callTarget) { g_original[allocated] = reinterpret_cast(Va(hook.callTarget)); continue; } BYTE* trampoline = static_cast(VirtualAlloc(0, hook.size+5, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)); if (!trampoline) { prepared = false; break; } g_original[allocated] = trampoline; memcpy(trampoline, hook.saved, hook.size); Jump(trampoline+hook.size, reinterpret_cast(trampoline)+hook.size, Va(hook.address)+hook.size); DWORD previous; if (!VirtualProtect(trampoline, hook.size+5, PAGE_EXECUTE_READ, &previous)) { prepared = false; ++allocated; break; } FlushInstructionCache(GetCurrentProcess(), trampoline, hook.size+5); } if (prepared && allocated == kHookCount) { for (; written != kHookCount; ++written) { Hook& hook = g_hooks[written]; BYTE replacement[16]; memset(replacement, 0x90, hook.size); Jump(replacement, Va(hook.address), reinterpret_cast(hook.entry)); if (hook.callTarget) replacement[0] = 0xe8; // keep the native CALL/RET 4 ABI if (!WriteCode(reinterpret_cast(Va(hook.address)), replacement, hook.size)) { patched = false; ++written; break; } } if (patched && written == kHookCount) { InterlockedExchange(&g_ready, 1); LogLine("Seria minimap ready: owning-town title hooks=8 guards=19 generated-rooms=16, recording independent"); return true; } } // Roll back entry bytes BEFORE freeing executable trampolines. bool restored = true; for (unsigned n = 0; n != written; ++n) restored = WriteCode(reinterpret_cast(Va(g_hooks[n].address)), g_hooks[n].saved, g_hooks[n].size) && restored; if (restored) for (unsigned n = 0; n != allocated; ++n) { if (g_original[n] && !g_hooks[n].callTarget) VirtualFree(g_original[n], 0, MEM_RELEASE); g_original[n] = 0; } LogLine("Seria minimap disabled: install failed; entry rollback=%d", restored ? 1 : 0); return false; } }