Skip to content

Instantly share code, notes, and snippets.

@awesie
Last active April 2, 2019 14:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save awesie/7973abc4ce714c350d5ca960537ad519 to your computer and use it in GitHub Desktop.
Save awesie/7973abc4ce714c350d5ca960537ad519 to your computer and use it in GitHub Desktop.
Port Nox patches to Unimod
From 9430904a47e10d603e4f0cca1a2900695bb2c25a Mon Sep 17 00:00:00 2001
From: Andrew Wesie <awesie@gmail.com>
Date: Mon, 1 Apr 2019 16:54:29 -0500
Subject: [PATCH] Port patches to UniMod.
---
util.cpp | 384 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 384 insertions(+)
diff --git a/util.cpp b/util.cpp
index 3b3f3d1..40d6a37 100644
--- a/util.cpp
+++ b/util.cpp
@@ -376,6 +376,18 @@ int printL(lua_State *L)
}
return 0;
}
+void InjectCallTo(DWORD Addr,void *Fn)
+{
+ BYTE *To=(BYTE *)Addr;
+ DWORD *Dw=(DWORD*)(To+1);
+ DWORD Delta=(DWORD)Fn;
+ Delta-=4+(DWORD)Dw;
+ DWORD OldProtect;
+ VirtualProtect(To,5,PAGE_EXECUTE_READWRITE,&OldProtect);
+ *Dw=Delta;
+ *(To++)=0xE8;
+ VirtualProtect(To,5,OldProtect,&OldProtect);
+};
void InjectJumpTo(DWORD Addr,void *Fn)// Пишем по данному адресу переход на нашу функцию
{
BYTE *To=(BYTE *)Addr;
@@ -1129,6 +1141,365 @@ void lua_error_(lua_State*L)
lua_error(L);
}
+// awesie: ported patches to Unimod.
+namespace
+{
+ int (__cdecl *noxCheckGameFlags) (int) = (int(__cdecl*)(int))0x0040A5C0;
+ INT_PTR(__cdecl *serverGetGameData)(int N) = (INT_PTR(__cdecl*)(int))0x00416590;
+ void (__cdecl *noxEnableTeams)() = (void(__cdecl*)())0x4185b0;
+ void (__cdecl *noxRandomizeTeams)(int) = (void(__cdecl*)(int))0x4181f0;
+ void (__cdecl *noxDisableTeams)(int) = (void(__cdecl*)(int))0x419030;
+ void (__cdecl *noxNpcInit)(unsigned char *, int) = (void(__cdecl*)(unsigned char*,int))0x49A380;
+
+ unsigned char *myNpcArray = NULL;
+ unsigned int myNpcArrayLen = 1024; // Original limit is 64.
+
+ // For your comprehension, I reimplemented the original functionality.
+ int __cdecl myCmdSetMode(int argp, char argc, const wchar_t *argv[])
+ {
+ static const struct {
+ const wchar_t *name;
+ unsigned int flag;
+ } gameModes[] = {
+ { L"Arena", 0x100 },
+ { L"Elimination", 0x400 },
+ { L"CTF", 0x20 },
+ { L"KOTR", 0x10 },
+ { L"Flagball", 0x40 },
+ { L"Individual", 0x4000 },
+ { L"Clan", 0x8000 },
+ { NULL, 0 }
+ };
+
+ // Verify that we are in a chat map.
+ if (!noxCheckGameFlags(0x80))
+ return 0;
+
+ // Verify that we have correct number of arguments.
+ if (argc != 3)
+ return 0;
+
+ // Search for matching game mode.
+ for (unsigned int i = 0; gameModes[i].name; ++i)
+ {
+ if (!_wcsicmp(gameModes[i].name, argv[argp]))
+ {
+ // Set the new game mode.
+ unsigned short *p = (unsigned short *)(serverGetGameData(1) + 52);
+ *p = (*p & 0xE80F) | gameModes[i].flag;
+ break;
+ }
+ }
+ return 1;
+ }
+
+ // Helper to return current game mode | CHAT. We use this to always allow
+ // a chat map to be loaded.
+ unsigned int myCurrentGameMode()
+ {
+ DWORD *p = (DWORD *)0x5D53A4;
+ return *p | 0x80;
+ }
+
+ void patchCmdSetMode()
+ {
+ InjectJumpTo(0x440CE0, myCmdSetMode);
+ // Hook load map command when it tries to get the current game mode.
+ // Instead, return current game mode + CHAT.
+ InjectOffs(0x4419D2 + 1, myCurrentGameMode);
+ }
+
+ int __cdecl myCmdSetTeam(int argp, char argc, const wchar_t *argv[])
+ {
+ DWORD *teamCount = (DWORD *)0x654d5c;
+
+ // Verify that we are in a chat map.
+ if (!noxCheckGameFlags(0x80))
+ return 0;
+
+ // Verify that we have correct number of arguments.
+ if (argc != 3)
+ return 0;
+
+ if (_wcsicmp(argv[argp], L"on") == 0)
+ {
+ if (*teamCount < 2)
+ {
+ noxEnableTeams();
+ noxRandomizeTeams(1); // FIXME: I used 0 before. Does it matter?
+ }
+ return 1;
+ }
+ else if (_wcsicmp(argv[argp], L"off") == 0)
+ {
+ noxDisableTeams(1);
+ return 1;
+ }
+
+ return 0;
+ }
+
+ void patchCmdSetTeam()
+ {
+ InjectJumpTo(0x440CC0, myCmdSetTeam);
+ }
+
+ void patchCmdOfficialOnly()
+ {
+ // Skip "official only" check when processing commands.
+ InjectJumpTo(0x443b48, (void *)0x443b5d);
+ }
+
+ // Thankfully Nox already does smart things here.
+ void patchDrawableLimit()
+ {
+ DWORD newLimit = 8192;
+ DWORD newArraySize = newLimit * sizeof(void *);
+ // Patch size of drawable array.
+ InjectData(0x473A41 + 1, (byte *)&newArraySize, sizeof(DWORD));
+ // Patch maximum drawable array index.
+ InjectData(0x4756B4 + 1, (byte *)&newLimit, sizeof(DWORD));
+ }
+
+ // TODO: replace static array with something more intelligent?
+ // maybe a tree for faster lookups?
+ void __cdecl myNpcArrayInit()
+ {
+ if (!myNpcArray)
+ myNpcArray = (unsigned char *)noxAlloc(1316 * myNpcArrayLen);
+ memset(myNpcArray, 0, 1316 * myNpcArrayLen);
+ }
+
+ void *__cdecl myNpcArrayNew(DWORD id)
+ {
+ for (unsigned int i = 0; i < myNpcArrayLen; ++i)
+ {
+ unsigned char *npc = myNpcArray + 1316 * i;
+ if (!npc[0])
+ {
+ noxNpcInit(npc, id);
+ return npc;
+ }
+ }
+ return NULL;
+ }
+
+ void *__cdecl myNpcArrayFind(DWORD id)
+ {
+ for (unsigned int i = 0; i < myNpcArrayLen; ++i)
+ {
+ unsigned char *npc = myNpcArray + 1316 * i;
+ if (npc[0] && *(DWORD *)&npc[4] == id)
+ return npc;
+ }
+ return NULL;
+ }
+
+ // Client-side patch.
+ void patchNpcLimit()
+ {
+ InjectJumpTo(0x49A2C0, myNpcArrayInit);
+ InjectJumpTo(0x49A300, myNpcArrayNew);
+ InjectJumpTo(0x49A340, myNpcArrayFind);
+ }
+
+ // Nox has a linked list of constructed strings that will be freed later.
+ // This list may get long if the same string is printed many times.
+ //
+ // FIXME: we could search through the list to save some memory.
+ const wchar_t *__cdecl myMissingString(const char *str)
+ {
+ struct consString {
+ consString *next;
+ wchar_t data[0];
+ };
+ consString **head = (consString **)(0x611c14);
+
+ DWORD len = MultiByteToWideChar(CP_UTF8, 0, str, -1, 0, 0);
+ consString *cs = (consString *)noxAlloc(sizeof(*cs) + (len + 1) * sizeof(wchar_t));
+ MultiByteToWideChar(CP_UTF8, 0, str, -1, cs->data, len);
+
+ cs->next = *head;
+ *head = cs;
+ return cs->data;
+ }
+
+ void patchMissingString()
+ {
+ // Replace original code with call to helper.
+ unsigned char tmp[0x35];
+ memset(tmp, 0x90, sizeof(tmp));
+ tmp[0x00] = 0x53; // push ebx
+ tmp[0x06] = 0x5B; // pop ebx
+ tmp[0x34] = 0x5F; // pop edi
+ InjectData(0x40F2C2, tmp, sizeof(tmp));
+ InjectCallTo(0x40F2C3, myMissingString);
+ }
+
+ void patchPlasmaDraw()
+ {
+ // Remove single player check.
+ unsigned char tmp[0x1e];
+ memset(tmp, 0x90, sizeof(tmp));
+ InjectData(0x4BA988, tmp, sizeof(tmp));
+ }
+
+ // FIXME
+ // I do not remember entirely how this works. The basic idea is that if
+ // the buffers are full, then flush.
+ int __cdecl myQueueUpdateData(int id, const void *inbuf, int inlen)
+ {
+ int(__cdecl *sub_40EEF0)(int,int) = (int(__cdecl*)(int,int))0x40EEF0;
+ void*(__cdecl *sub_40EFA0)(int,const void*,int) = (void*(__cdecl*)(int,const void*,int))0x40EFA0;
+ int(__cdecl *sub_420940)(void*,void*,int,int) = (int(__cdecl*)(void*,void*,int,int))0x420940;
+ void(__cdecl *sub_494E90)(int) = (void(__cdecl*)(int))0x494E90;
+ DWORD*(__cdecl *sub_417090)(int) = (DWORD*(__cdecl*)(int))0x417090;
+ void(__cdecl *sub_5528B0)(DWORD,int) = (void(__cdecl*)(DWORD,int))0x5528B0;
+ DWORD *client = ((DWORD **)0x607B08)[id];
+ void *p;
+
+ if (inlen <= 0)
+ return 1;
+
+ if (!sub_40EEF0(id, inlen) || (p = sub_40EFA0(id, inbuf, inlen)) == NULL)
+ {
+ struct buffer {
+ unsigned char data[2048];
+ unsigned int length;
+ } *buffers = (buffer *)0x5D7088;
+ struct datalist {
+ unsigned char *data;
+ unsigned int length;
+ struct datalist *prev;
+ struct datalist *next;
+ };
+ // Buffer is full. Flush and try again.
+
+ // The new update packet needs to have correct bytes at the
+ // beginning. Save the length of the first two queued datas so we
+ // can reply them.
+ datalist *dl = (datalist *)client[0];
+ unsigned int len1 = dl->length;
+ unsigned int len2 = dl->next->length;
+
+ //DebugBreak();
+
+ // Do the flush.
+ if (id == 31)
+ sub_494E90(id); // Host
+ else
+ sub_5528B0(sub_417090(id)[516] + 1, 0);
+
+ // Reset buffer length and re-queue first two updates.
+ buffers[id].length = len1 + len2;
+ sub_420940(client, buffers[id].data, len1, 1);
+ sub_420940(client, buffers[id].data + len1, len2, 1);
+
+ // Retry original request.
+ p = sub_40EFA0(id, inbuf, inlen);
+ }
+
+ // If we still are unable to queue the data, give up.
+ if (p == NULL)
+ return 0;
+
+ sub_420940(client, p, inlen, 1);
+ return 1;
+ }
+
+ // Due to packet size constraints, Nox limits the number of bytes that
+ // can be queued to send to a client. When this limit is reached, bytes
+ // will be dropped and things disappear from the client's screen.
+ //
+ // We can work around this limitation by forcing Nox to flush the pending
+ // data early. This will use more bandwidth, but should not have horrible
+ // side effects.
+ void patchQueueUpdateData()
+ {
+ InjectJumpTo(0x40EF40, myQueueUpdateData);
+ }
+
+ // See description below. This reimplements some of the Nox game logic
+ // to make it the hook easier to understand.
+ void __cdecl myPlayerDrawHook(void *a1, void *a2, void *a3, unsigned int a4)
+ {
+ DWORD*(__cdecl *sub_417040)(int) = (DWORD*(__cdecl*)(int))0x417040;
+ void(__cdecl *sub_4B8960)(void*,void*,DWORD,void*,void*,unsigned int) = (void(__cdecl*)(void*,void*,DWORD,void*,void*,unsigned int))0x4B8960;
+ void(__cdecl *sub_4B8D40)(void*,void*,DWORD,void*,void*,unsigned int) = (void(__cdecl*)(void*,void*,DWORD,void*,void*,unsigned int))0x4B8D40;
+
+ // Lookup player
+ DWORD *v3 = sub_417040(((unsigned int *)a2)[32]);
+
+ unsigned char dir = ((unsigned char *)a2)[297];
+ if (dir != 0 && dir != 1 && dir != 2 && dir != 3 && dir != 6 || ((unsigned int *)a2)[69] == 37)
+ {
+ // Draw quiver, then body, then armor / weapon.
+ sub_4B8D40(a1, a2, v3[1] & 2, v3 + 581, a3, a4);
+ sub_4B8960(a1, a2, v3[0], v3 + 743, a3, a4);
+ sub_4B8D40(a1, a2, v3[1] & ~2, v3 + 581, a3, a4);
+ }
+ else
+ {
+ // Draw armor / weapon, then body, then quiver.
+ sub_4B8D40(a1, a2, v3[1] & ~2, v3 + 581, a3, a4);
+ sub_4B8960(a1, a2, v3[0], v3 + 743, a3, a4);
+ sub_4B8D40(a1, a2, v3[1] & 2, v3 + 581, a3, a4);
+ }
+ }
+
+ // A player in Nox has a 32-bit integer field that contains bits for each
+ // type of armor / weapon. sub_4B8D40 is responsible for iterating over
+ // each type and drawing it to the screen.
+ //
+ // To add support for drawing the quiver, we must modify sub_4B8D40 to
+ // iterate from 1 instead of 2 (i.e. bit 1 indicates quiver equipped). We
+ // must also modify how sub_4B8D40 is called because of Z ordering.
+ //
+ // The Z ordering is simple to understand. Usually Nox will draw the player
+ // body, and then the armor / weapon on top (or vice versa depending on
+ // rotation). The quiver is unique because it is on the player's back, e.g.
+ // it must be drawn first, then the body, then the rest of the armor.
+ void patchPlayerDraw()
+ {
+ // Replace old code with call to hook (0x4B85D0 - 0x4B865D).
+ unsigned char tmp[0x8D];
+ memset(tmp, 0x90, sizeof(tmp));
+ tmp[0] = 0x8B; // mov edx, [esp+68h+var_58]
+ tmp[1] = 0x54;
+ tmp[2] = 0x24;
+ tmp[3] = 0x10;
+ tmp[4] = 0x8B; // mov eax, [esp+68h+var_54]
+ tmp[5] = 0x44;
+ tmp[6] = 0x24;
+ tmp[7] = 0x14;
+ tmp[8] = 0x52; // push XXXXXX (padding to match original stack)
+ tmp[9] = 0x52; // ...
+ tmp[10] = 0x52; // ...
+ tmp[11] = 0x52; // ...
+ tmp[12] = 0x52; // ...
+ tmp[13] = 0x52; // ...
+ tmp[14] = 0x52; // ...
+ tmp[15] = 0x52; // ...
+ tmp[16] = 0x52; // push edx
+ tmp[17] = 0x50; // push eax
+ tmp[18] = 0x53; // push ebx
+ tmp[19] = 0x55; // push ebp
+ InjectData(0x4B85D0, tmp, sizeof(tmp));
+ InjectCallTo(0x4B85D0 + 20, myPlayerDrawHook);
+
+ // Patch sub_4B8D40 to iterate from 1.
+ DWORD dw = 1;
+ InjectData(0x4B8D7C + 1, (byte *)&dw, sizeof(dw));
+ dw = 0xA0;
+ InjectData(0x4B8D81 + 2, (byte *)&dw, sizeof(dw));
+ }
+
+ // TODO
+ void patchHostLatency()
+ {
+ }
+};
+
extern void initSDL();
extern void windowsAllInit();
extern void unitFunctionInit();
@@ -1184,6 +1555,19 @@ int initWindowedMode(int param1, int param2, int param3)
void injectCon()
{
initSDL();
+
+ // awesie: Apply patches.
+ patchCmdSetMode();
+ patchCmdSetTeam();
+ patchCmdOfficialOnly();
+ patchDrawableLimit();
+ patchNpcLimit();
+ patchMissingString();
+ patchPlasmaDraw();
+ patchQueueUpdateData();
+ patchHostLatency();
+ patchPlayerDraw();
+
//MessageBox(0,"!",0,0);
exInit();
initModLib2();
--
2.8.1.windows.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment