From cf0354eb8e043fcd9c6c17756701972f948a16f1 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 22 Aug 2024 00:02:48 +0100 Subject: Rewrite the gamedata and entprops systems entirely This removes the horrible janky old KeyValues parser and replaces it with a couple of trivial ad-hoc text parsers. In doing so, make the format of the actual gamedata files more human-friendly too. We also gain support for nested SendTables in mkentprops, which are required to get at various things like player velocity. And, the actual string matching is made more efficient (or, at least, more scalable) by way of a cool radix tree thing which generates a bunch of switch cases on distinct characters. --- compile | 12 +- compile.bat | 14 +- gamedata/engine.kv | 181 ------------------ gamedata/engine.txt | 162 ++++++++++++++++ gamedata/entprops.kv | 9 - gamedata/entprops.txt | 8 + gamedata/gamelib.kv | 57 ------ gamedata/gamelib.txt | 46 +++++ gamedata/inputsystem.kv | 17 -- gamedata/inputsystem.txt | 12 ++ gamedata/matchmaking.kv | 11 -- gamedata/matchmaking.txt | 7 + gamedata/vgui2.kv | 7 - gamedata/vgui2.txt | 7 + gamedata/vguimatsurface.kv | 69 ------- gamedata/vguimatsurface.txt | 49 +++++ src/build/mkentprops.c | 447 ++++++++++++++++++++++++++++++-------------- src/build/mkgamedata.c | 345 ++++++++++++++++++---------------- src/build/vec.h | 2 +- src/engineapi.c | 3 +- src/engineapi.h | 11 +- src/kv.c | 290 ---------------------------- src/kv.h | 95 ---------- src/l4dwarp.c | 4 +- src/mem.h | 2 +- test/kv.test.c | 55 ------ 26 files changed, 807 insertions(+), 1115 deletions(-) delete mode 100644 gamedata/engine.kv create mode 100644 gamedata/engine.txt delete mode 100644 gamedata/entprops.kv create mode 100644 gamedata/entprops.txt delete mode 100644 gamedata/gamelib.kv create mode 100644 gamedata/gamelib.txt delete mode 100644 gamedata/inputsystem.kv create mode 100644 gamedata/inputsystem.txt delete mode 100644 gamedata/matchmaking.kv create mode 100644 gamedata/matchmaking.txt delete mode 100644 gamedata/vgui2.kv create mode 100644 gamedata/vgui2.txt delete mode 100644 gamedata/vguimatsurface.kv create mode 100644 gamedata/vguimatsurface.txt delete mode 100644 src/kv.c delete mode 100644 src/kv.h delete mode 100644 test/kv.test.c diff --git a/compile b/compile index d9ab4f6..17a1ba4 100755 --- a/compile +++ b/compile @@ -89,15 +89,15 @@ if [ "$dbg" = 1 ]; then src="$src \ fi $HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \ - -o .build/codegen src/build/codegen.c src/build/cmeta.c + -o .build/codegen src/build/codegen.c src/build/cmeta.c src/os.c $HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \ - -o .build/mkgamedata src/build/mkgamedata.c src/kv.c + -o .build/mkgamedata src/build/mkgamedata.c src/os.c $HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \ - -o .build/mkentprops src/build/mkentprops.c src/kv.c + -o .build/mkentprops src/build/mkentprops.c src/os.c .build/codegen `for s in $src; do echo "src/$s"; done` -.build/mkgamedata gamedata/engine.kv gamedata/gamelib.kv gamedata/inputsystem.kv \ -gamedata/matchmaking.kv gamedata/vgui2.kv gamedata/vguimatsurface.kv -.build/mkentprops gamedata/entprops.kv +.build/mkgamedata gamedata/engine.txt gamedata/gamelib.txt gamedata/inputsystem.txt \ +gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt +.build/mkentprops gamedata/entprops.txt for s in $src; do cc "$s"; done $CC -shared -fpic -fuse-ld=lld -O0 -w -o .build/libtier0.so src/stubs/tier0.c $CC -shared -fpic -fuse-ld=lld -O0 -w -o .build/libvstdlib.so src/stubs/vstdlib.c diff --git a/compile.bat b/compile.bat index f36d17d..ca632ed 100644 --- a/compile.bat +++ b/compile.bat @@ -115,13 +115,13 @@ if %host64%==1 ( %HOSTCC% -fuse-ld=lld -municode -O2 %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^ -L.build %lbcryptprimitives_host% -o .build/codegen.exe src/build/codegen.c src/build/cmeta.c src/os.c || goto :end %HOSTCC% -fuse-ld=lld -municode -O2 %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^ --L.build %lbcryptprimitives_host% -o .build/mkgamedata.exe src/build/mkgamedata.c src/kv.c src/os.c || goto :end -%HOSTCC% -fuse-ld=lld -municode -O2 %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^ --L.build %lbcryptprimitives_host% -o .build/mkentprops.exe src/build/mkentprops.c src/kv.c src/os.c || goto :end +-L.build %lbcryptprimitives_host% -o .build/mkgamedata.exe src/build/mkgamedata.c src/os.c || goto :end +%HOSTCC% -fuse-ld=lld -municode -O2 -g %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^ +-L.build %lbcryptprimitives_host% -o .build/mkentprops.exe src/build/mkentprops.c src/os.c || goto :end .build\codegen.exe%src% || goto :end -.build\mkgamedata.exe gamedata/engine.kv gamedata/gamelib.kv gamedata/inputsystem.kv ^ -gamedata/matchmaking.kv gamedata/vgui2.kv gamedata/vguimatsurface.kv || goto :end -.build\mkentprops.exe gamedata/entprops.kv || goto :end +.build\mkgamedata.exe gamedata/engine.txt gamedata/gamelib.txt gamedata/inputsystem.txt ^ +gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt || goto :end +.build\mkentprops.exe gamedata/entprops.txt || goto :end llvm-rc /FO .build\dll.res src\dll.rc || goto :end for %%b in (%src%) do ( call :cc %%b || goto :end ) :: we need different library names for debugging because Microsoft... @@ -143,8 +143,6 @@ del .build\sst.lib :: special case: test must be 32-bit %HOSTCC% -fuse-ld=lld -m32 -O2 -g -L.build -lbcryptprimitives -include test/test.h -o .build/hook.test.exe test/hook.test.c || goto :end .build\hook.test.exe || goto :end -%HOSTCC% -fuse-ld=lld -O2 -g -include test/test.h -o .build/kv.test.exe test/kv.test.c || goto :end -.build\kv.test.exe || goto :end %HOSTCC% -fuse-ld=lld -O2 -g -include test/test.h -o .build/x86.test.exe test/x86.test.c || goto :end .build\x86.test.exe || goto :end diff --git a/gamedata/engine.kv b/gamedata/engine.kv deleted file mode 100644 index 304b218..0000000 --- a/gamedata/engine.kv +++ /dev/null @@ -1,181 +0,0 @@ -// = engine library = - -// ICvar -// XXX: const and non-const entries might be flipped here on Windows, not 100% -// sure. kind of just choosing not to care thusfar, as it still works the same! -vtidx_AllocateDLLIdentifier { default 5 Portal2 8 } -vtidx_RegisterConCommand { default 6 Portal2 9 } -vtidx_UnregisterConCommands { default 8 Portal2 11 } -//vtidx_FindComandBase { default 10 Portal2 13 } // unused -vtidx_FindVar { default 12 Portal2 15 } -//vtidx_FindVar_const { default 13 Portal2 16 } // e.g. this might be wrong(???) -vtidx_FindCommand { default 14 Portal2 17 } -vtidx_CallGlobalChangeCallbacks { default 20 L4Dx 18 Portal2 21 } -vtidx_ConsoleColorPrintf { OrangeBoxbased 23 L4Dx 21 Portal2 24 } - -// CDemoRecorder -vtidx_StartRecording 2 -vtidx_SetSignonState 3 -vtidx_StopRecording 7 -vtidx_RecordPacket 11 - -// VEngineClient -vtidx_IsInGame { - Client015 26 - Client014 { - L4D2 28 - 2013 26 - } - Client013 { - L4D1 27 - default 26 - } - // TODO(compat): unconfirmed, and OE support isn't really a thing yet anyway - //Client012 28 -} -vtidx_GetGameDirectory { - Client015 35 // current portal 2 - Client014 { - L4D2 73 // YES IT'S SEVENTY THREE ALL OF A SUDDEN. - 2013 35 - } - Client013 { - L4Dx 36 // AND THEN THEY CHANGED IT BACK LATER! (on 2.0.4.1) - default 35 // <- most things have this! - } - Client012 37 // dmomm, ep1, ... -} -vtidx_GetEngineBuildNumber { - Client013 { - L4D2 99 - Portal2 100 - } - Client014 { - L4D2 31 - 2013 98 - } - Portal1_3420 91 - // TODO(compat): we'll need these... but need to detect first - //Portal1_5135 102 - //L4D1_1005 99 - //L4D1_Steam 97 -} - -// IGameUIFuncs -vtidx_GetDesktopResolution 5 - -// IGame/CGame -vtidx_DispatchAllStoredGameMessages 16 - -// VEngineServer -vtidx_PEntityOfEntIndex { OrangeBox 19 } // probably OE too but??? -vtidx_ServerCommand { OrangeBoxbased 36 } - -sz_edict { - default 20 - L4Dbased 16 // see engineapi.h comment -} - -// vgui::Panel -vtidx_SetPaintEnabled { - default 67 - Client013 { - L4D1 68 - L4D2 { - default 71 - L4D2_2147plus 72 - } - } - Client014 { - L4D2 70 - } -} -vtidx_Paint { - default 123 - Client014 { L4D2 126 } // 2000 - Client013 { - L4D2 { - default 127 // 2045 - L4D2_2147plus 128 - } - } -} - -// SendProp -sz_SendProp { - // wrapping all these in 005 for right now. - // will need at least 009 as well at some point! - SrvDLL005 { - OrangeBox 76 - L4D1 80 - L4D2 84 - Portal2 84 - } - //2013 80 // TODO(compat): not sure about 2013/009 yet -} -off_SP_varname { - SrvDLL005 { - OrangeBox 44 - //L4Dbased 48 // TODO(compat): haven't tested Survivors - // for now do this instead: - L4D 48 - Portal2 48 - } - //2013 48 // TODO(compat): not sure about 2013/009 yet pt2 -} -off_SP_offset { - SrvDLL005 { - OrangeBox 68 - L4D1 72 - L4D2 76 - Portal2 76 - } - //2013 72 // TODO(compat): not sure about 2013/009 yet pt3 -} - -// CBaseServer/CGameServer -vtidx_GetSpawnCount { - //OrangeBox "13 + NVDTOR" // not used right now anyway - L4D1 "13 + NVDTOR" - L4D2 "14 + NVDTOR" // GetTimescale() added, pushed it down - // rest untested, add later if/when actually needed for something -} - -// IEngineVGuiInternal/CEngineVGui -vtidx_GetPanel NVDTOR -vtidx_VGuiConnect { // note: the actual name is Connect() but that's too generic - default "3 + NVDTOR" - L4Dbased { - default "4 + NVDTOR" // ActivateGameUI added - L4DS "5 + NVDTOR" // some other crap added, god knows - } -} -vtidx_VGuiIsInitialized { // likewise, function is just called IsInitialized() - default "6 + NVDTOR" - L4Dbased { - default "7 + NVDTOR" - L4DS "8 + NVDTOR" - } -} - -// CDedicatedServerAPI -vtidx_RunFrame 7 - -// IEngine -vtidx_Frame "4 + NVDTOR" - -// CEngineTool -vtidx_GetRealTime { - default 34 // HL2, P1, L4D1, BMS - // OE, DMoMM 24 - L4D2 35 - Portal2 36 -} -vtidx_HostFrameTime { - default 35 - // OE, DMoMM 25 - L4D2 38 - Portal2 39 -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 ft=text diff --git a/gamedata/engine.txt b/gamedata/engine.txt new file mode 100644 index 0000000..b5186a6 --- /dev/null +++ b/gamedata/engine.txt @@ -0,0 +1,162 @@ +# ICvar +# XXX: const and non-const entries might be flipped here on Windows, not 100% +# sure. kind of just choosing not to care thusfar, as it still works the same! +vtidx_AllocateDLLIdentifier 5 + Portal2 8 +vtidx_RegisterConCommand 6 + Portal2 9 +vtidx_UnregisterConCommands 8 + Portal2 11 +# unused: +#vtidx_FindCommandBase 10 +# Portal2 13 +vtidx_FindVar 12 + Portal2 15 +vtidx_FindCommand 14 + Portal2 17 +vtidx_CallGlobalChangeCallbacks 20 + L4Dx 18 + Portal2 21 +vtidx_ConsoleColorPrintf + OrangeBoxbased 23 + L4Dx 21 + Portal2 24 + +# CDemoRecorder +vtidx_StartRecording 2 +vtidx_SetSignonState 3 +vtidx_StopRecording 7 +vtidx_RecordPacket 11 + +# VEngineClient +vtidx_IsInGame + Client015 26 + Client014 + L4D2 28 + 2013 26 + Client013 26 + L4D1 27 + # TODO(compat): unconfirmed, and OE support isn't really a thing yet anyway + #Client012 28 +vtidx_GetGameDirectory + Client015 35 # current Portal 2 + Client014 + L4D2 73 # YES IT'S SEVENTY THREE SOMEHOW ALL OF A SUDDEN. + 2013 35 + Client013 35 + L4Dx 36 # AND THEY ACTUALLY CHANGED IT BACK LATER! (on 2.0.4.1) + Client012 37 # dmomm, ep1, ... +vtidx_GetEngineBuildNumber + Client013 + L4D2 99 + Portal2 100 + Client014 + L4D2 31 + 2013 98 + Portal1_3420 91 + # TODO(compat): we'll need these... but need to detect first + #Portal1_5135 102 + #L4D1_1005 99 + #L4D1_Steam 97 + +# IGameUIFuncs +vtidx_GetDesktopResolution 5 + +# IGame/CGame +vtidx_DispatchAllStoredGameMessages 16 + +# VEngineServer +vtidx_PEntityOfEntIndex + OrangeBox 19 # probably OE too but ?? +vtidx_ServerCommand + OrangeBoxbased 36 + +sz_edict 20 + L4Dbased 16 + +# vgui::Panel +vtidx_SetPaintEnabled 67 + Client013 + L4D1 68 + L4D2 71 + L4D2_2147plus 72 + Client014 + L4D2 70 +vtidx_Paint 123 + Client014 + L4D2 126 # 2000 + Client013 + L4D2 127 # 2045 + L4D2_2147plus 128 + +# SendProp +sz_SendProp + # wrapping all these in 005 for right now. + # will need at least 009 as well at some point! + SrvDLL005 + OrangeBox 76 + L4D1 80 + L4D2 84 + Portal2 84 + #2013 80 # TODO(compat): not sure about 2013/009 yet +off_SP_type 8 +off_SP_varname + SrvDLL005 + OrangeBox 44 + #L4Dbased 48 # TODO(compat): haven't tested Survivors + # for now do this instead: + L4D 48 + Portal2 48 + #2013 48 # TODO(compat): not sure about 2013/009 yet pt2 +off_SP_subtable + SrvDLL005 + OrangeBox 64 + L4D1 68 + L4D2 72 + Portal2 72 + #2013 68 # TODO(compat): not sure about 2013/009 yet pt3 +off_SP_offset + SrvDLL005 + OrangeBox 68 + L4D1 72 + L4D2 76 + Portal2 76 + #2013 72 # TODO(compat): not sure about 2013/009 yet pt4 + +DT_DataTable 5 # constant value from SendPropType enum (XXX: name okay???) + L4Dbased 6 + +# CBaseServer/CGameServer +vtidx_GetSpawnCount + #OrangeBox 13 + NVDTOR # not used right now anyway + L4D1 13 + NVDTOR + L4D2 14 + NVDTOR # GetTimescale() added, pushed it down + # rest untested, add later if/when actually needed for something + +# IEngineVGuiInternal/CEngineVGui +vtidx_GetPanel NVDTOR +# note: actual name of this function is Connect() but that's too generic +vtidx_VGuiConnect 3 + NVDTOR + L4Dbased 4 + NVDTOR # ActivateGameUI added + L4DS 5 + NVDTOR # some other crap added, god knows +vtidx_VGuiIsInitialized 6 + NVDTOR # this is also just called IsInitialized() + L4Dbased 7 + NVDTOR + L4DS 8 + NVDTOR + +# CDedicatedServerAPI +vtidx_RunFrame 7 + +# IEngine +vtidx_Frame 4 + NVDTOR + +# CEngineTool +vtidx_GetRealTime 34 # HL2, P1, L4D1, BMS + # OE, DMoMM 24 + L4D2 35 + Portal2 36 +vtidx_HostFrameTime 35 + # OE, DMoMM 25 + L4D2 38 + Portal2 39 + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/gamedata/entprops.kv b/gamedata/entprops.kv deleted file mode 100644 index 942d9c9..0000000 --- a/gamedata/entprops.kv +++ /dev/null @@ -1,9 +0,0 @@ -// This follows a different format to the other gamedata files. -// It simply assigns variable names to network table property name strings. -// Network names are classname/propname, e.g. CBasePlayer/m_fWhatever - -off_entpos CBaseEntity/m_vecOrigin -// look angles, currently just for L4D1/2, can add other games as needed -off_eyeang "CCSPlayer/m_angEyeAngles[0]" - -// vi: sw=4 ts=4 noet tw=80 cc=80 ft=text diff --git a/gamedata/entprops.txt b/gamedata/entprops.txt new file mode 100644 index 0000000..5d6bd7c --- /dev/null +++ b/gamedata/entprops.txt @@ -0,0 +1,8 @@ +# Format: variable sendtable[/subtable/...]/property +# Example: off_coolvar CClassName/m_pSubTableThing/m_fCoolVariable + +off_entpos CBaseEntity/m_vecOrigin +# look angles, currently just for L4D1/2, can add other games as needed +off_eyeang CCSPlayer/m_angEyeAngles[0] + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/gamedata/gamelib.kv b/gamedata/gamelib.kv deleted file mode 100644 index 7f53383..0000000 --- a/gamedata/gamelib.kv +++ /dev/null @@ -1,57 +0,0 @@ -// = client and server libraries = - -// CGameMovement -vtidx_CheckJumpButton { - Portal1_3420 "22 + NVDTOR" - 2013 "28 + NVDTOR" - L4D "32 + NVDTOR" - L4DS "33 + NVDTOR" - Portal2 "35 + NVDTOR" -} -off_mv { - default 8 - Portal1_3420 4 -} - -// IServerGameDLL -vtidx_GetAllServerClasses { - default 10 - 2013 11 - // TODO(compat): BMS 11 -} - -// I(Server|Client)Unknown -vtidx_GetBaseEntity "4 + NVDTOR" - -// CBaseEntity or CBasePlayer or something -off_netprop_statechanged { L4D 88 } -off_simtime { L4D 128 } -vtidx_Spawn { - L4D2 { - default "22 + NVDTOR" - TheLastStand "23 + NVDTOR" - } -} -vtidx_Teleport { - L4D "104 + NVDTOR" - L4D2 { - default "116 + NVDTOR" // TODO(linux): might actually be 119!?!? - TheLastStand "117 + NVDTOR" // I dunno why JAiZ changed this - } -} - -// CGlobalVars -off_curtime 12 -off_edicts { L4D 88 } - -// IServerGameDLL -vtidx_GameFrame 4 -vtidx_GameShutdown 7 - -// CDirector -vtidx_OnGameplayStart { - L4D2 11 // note: just happens the same on linux! - L4D1 11 // TODO(linux): unknown whether or not this is the same -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 ft=text diff --git a/gamedata/gamelib.txt b/gamedata/gamelib.txt new file mode 100644 index 0000000..35d5551 --- /dev/null +++ b/gamedata/gamelib.txt @@ -0,0 +1,46 @@ +# CGameMovement +vtidx_CheckJumpButton + Portal1_3420 22 + NVDTOR + 2013 28 + NVDTOR + L4D 32 + NVDTOR + L4DS 33 + NVDTOR + Portal2 35 + NVDTOR +off_mv 8 + Portal1_3420 4 + +# IServerGameDLL +vtidx_GetAllServerClasses 10 + 2013 11 + # TODO(compat): BMS 11 + +# I(Server|Client)Unknown +vtidx_GetBaseEntity 4 + NVDTOR + +# CBaseEntity or CBasePlayer or something +off_netprop_statechanged + L4D 88 +off_simtime + L4D 128 +vtidx_Spawn + L4D2 22 + NVDTOR + TheLastStand 23 + NVDTOR +vtidx_Teleport + L4D 104 + NVDTOR + L4D2 116 + NVDTOR # TODO(linux): might actually be 119!?!? + TheLastStand 117 + NVDTOR # I dunno why JAiZ changed this + +# CGlobalVars +off_curtime 12 +off_edicts + L4D 88 + +# IServerGameDLL +vtidx_GameFrame 4 +vtidx_GameShutdown 7 + +# CDirector +vtidx_OnGameplayStart + L4D2 11 # note: just happens the same on linux! + L4D1 11 + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/gamedata/inputsystem.kv b/gamedata/inputsystem.kv deleted file mode 100644 index 3c13e09..0000000 --- a/gamedata/inputsystem.kv +++ /dev/null @@ -1,17 +0,0 @@ -// = inputsystem library = - -vtidx_SleepUntilInput { - default 31 // TODO(compat): don't really know yet if this causes any issues - Portal2 34 // IAppSystem changes -} - -// XXX: This function won't always exist even when has_ is true. -// It's only used by rinput.c after checking that m_rawinput exists. -vtidx_GetRawMouseAccumulators { - L4D1 39 - L4D2 37 - 2013 39 - Portal2 50 -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 ft=text diff --git a/gamedata/inputsystem.txt b/gamedata/inputsystem.txt new file mode 100644 index 0000000..65bca21 --- /dev/null +++ b/gamedata/inputsystem.txt @@ -0,0 +1,12 @@ +vtidx_SleepUntilInput 31 # TODO(compat): unsure if this causes any issues yet + Portal2 34 # IAppSystem changes + +# XXX: This function won't always exist even when has_ is true. +# It's only used by rinput.c after checking that m_rawinput exists. +vtidx_GetRawMouseAccumulators + L4D1 39 + L4D2 37 + 2013 39 + Portal2 50 + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/gamedata/matchmaking.kv b/gamedata/matchmaking.kv deleted file mode 100644 index 8ee618d..0000000 --- a/gamedata/matchmaking.kv +++ /dev/null @@ -1,11 +0,0 @@ -// = matchmaking library = - -// IMatchFramework -vtidx_GetMatchNetworkMsgController { - L4D 10 // NOTE: probably same for aswarm or p2 except with IAppSystem shift -} - -// IMatchNetworkMsgController -vtidx_GetActiveGameServerDetails { L4D 1 } - -// vi: sw=4 ts=4 noet tw=80 cc=80 ft=text diff --git a/gamedata/matchmaking.txt b/gamedata/matchmaking.txt new file mode 100644 index 0000000..2c93120 --- /dev/null +++ b/gamedata/matchmaking.txt @@ -0,0 +1,7 @@ +# IMatchFramework +vtidx_GetMatchNetworkMsgController + L4D 10 # NOTE: probably same for aswarm or p2 except with IAppSystem shift +vtidx_GetActiveGameServerDetails + L4D 1 + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/gamedata/vgui2.kv b/gamedata/vgui2.kv deleted file mode 100644 index 6fd55a3..0000000 --- a/gamedata/vgui2.kv +++ /dev/null @@ -1,7 +0,0 @@ -// = vgui2 library = - -// ISchemeManager -vtidx_GetIScheme 8 - -// IScheme -vtidx_GetFont 3 diff --git a/gamedata/vgui2.txt b/gamedata/vgui2.txt new file mode 100644 index 0000000..91aba01 --- /dev/null +++ b/gamedata/vgui2.txt @@ -0,0 +1,7 @@ +# ISchemeManager +vtidx_GetIScheme 8 + +# IScheme +vtidx_GetFont 3 + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/gamedata/vguimatsurface.kv b/gamedata/vguimatsurface.kv deleted file mode 100644 index 19857be..0000000 --- a/gamedata/vguimatsurface.kv +++ /dev/null @@ -1,69 +0,0 @@ -// = vguimatsurface library = - -// ISurface -vtidx_DrawSetColor { - OrangeBoxbased 10 - L4D 10 -} -vtidx_DrawFilledRect { - OrangeBoxbased 12 - L4D 12 -} -vtidx_DrawOutlinedRect { - OrangeBoxbased 14 - L4D 14 -} -vtidx_DrawLine { - OrangeBoxbased 15 - L4D 15 -} -vtidx_DrawPolyLine { - OrangeBoxbased 16 - L4D 16 -} -vtidx_DrawSetTextFont { - OrangeBoxbased 17 - L4D 17 -} -vtidx_DrawSetTextColor { - OrangeBoxbased 18 - L4D 18 -} -vtidx_DrawSetTextPos { - OrangeBoxbased 20 - L4D 20 -} -vtidx_DrawPrintText { - OrangeBoxbased 22 - L4D 22 -} -vtidx_GetScreenSize { - OrangeBoxbased 37 - L4D { - default 37 - L4D2_2147plus 35 - } -} -// Unused: currently no good way to create custom fonts without leaking them -//vtidx_CreateFont { -// OrangeBoxbased 64 -// L4D { -// default 64 -// L4D2_2147plus 63 -// } -//} -//vtidx_SetFontGlyphSet { -// OrangeBoxbased 65 -// L4D { -// default 65 -// L4D2_2147plus 64 -// } -//} -vtidx_GetFontTall { - OrangeBoxbased 67 - L4D 67 -} -vtidx_GetCharacterWidth { - OrangeBoxbased 71 - L4D 71 -} diff --git a/gamedata/vguimatsurface.txt b/gamedata/vguimatsurface.txt new file mode 100644 index 0000000..a9dc3f2 --- /dev/null +++ b/gamedata/vguimatsurface.txt @@ -0,0 +1,49 @@ +# ISurface +vtidx_DrawSetColor + OrangeBoxbased 10 + L4D 10 +vtidx_DrawFilledRect + OrangeBoxbased 12 + L4D 12 +vtidx_DrawOutlinedRect + OrangeBoxbased 14 + L4D 14 +vtidx_DrawLine + OrangeBoxbased 15 + L4D 15 +vtidx_DrawPolyLine + OrangeBoxbased 16 + L4D 16 +vtidx_DrawSetTextFont + OrangeBoxbased 17 + L4D 17 +vtidx_DrawSetTextColor + OrangeBoxbased 18 + L4D 18 +vtidx_DrawSetTextPos + OrangeBoxbased 20 + L4D 20 +vtidx_DrawPrintText + OrangeBoxbased 22 + L4D 22 +vtidx_GetScreenSize + OrangeBoxbased 37 + L4D 37 + L4D2_2147plus 35 +# Unused: currently no good way to create custom fonts without leaking them +#vtidx_CreateFont +# OrangeBoxbased 64 +# L4D 64 +# L4D2_2147plus 63 +#vtidx_SetFontGlyphSet +# OrangeBoxbased 65 +# L4D 65 +# L4D2_2147plus 64 +vtidx_GetFontTall + OrangeBoxbased 67 + L4D 67 +vtidx_GetCharacterWidth + OrangeBoxbased 71 + L4D 71 + +# vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/build/mkentprops.c b/src/build/mkentprops.c index fdbb8af..4806996 100644 --- a/src/build/mkentprops.c +++ b/src/build/mkentprops.c @@ -17,13 +17,11 @@ #include #include #include +#include #include "../intdefs.h" -#include "../kv.h" #include "../langext.h" #include "../os.h" -#include "skiplist.h" -#include "vec.h" #ifdef _WIN32 #define fS "S" @@ -31,176 +29,349 @@ #define fS "s" #endif -static noreturn die(const char *s) { +static noreturn die(int status, const char *s) { fprintf(stderr, "mkentprops: %s\n", s); - exit(100); + exit(status); } - -struct prop { - const char *varname; /* the C global name */ - const char *propname; /* the entity property name */ - struct prop *next; -}; -struct vec_prop VEC(struct prop *); - -DECL_SKIPLIST(static, class, struct class, const char *, 4) -struct class { - const char *name; /* the entity class name */ - struct vec_prop props; - struct skiplist_hdr_class hdr; -}; -static inline int cmp_class(struct class *c, const char *s) { - return strcmp(c->name, s); +static noreturn dieparse(const os_char *file, int line, const char *s) { + fprintf(stderr, "mkentprops: %" fS ":%d: %s\n", file, line, s); + exit(2); } -static inline struct skiplist_hdr_class *hdr_class(struct class *c) { - return &c->hdr; + +static char *sbase; // input file contents - string values are indices off this + +// custom data structure of the day: zero-alloc half-SoA adaptive radix trees(!) + +#define ART_MAXNODES 16384 +static uchar art_firstbytes[ART_MAXNODES]; +static struct art_core { + int soff; // string offset to entire key chunk (including firstbyte) + u16 slen; // number of bytes in key chunk (including firstbyte, >=1) + u16 next; // sibling node (same prefix, different firstbyte) +} art_cores[ART_MAXNODES]; +// if node doesn't end in \0: child node (i.e. key appends more bytes) +// if node DOES end in \0: offset into art_leaves (below) +static u16 art_children[ART_MAXNODES]; +static int art_nnodes = 0; + +#define ART_NULL ((u16)-1) + +static inline int art_newnode(void) { + if (art_nnodes == ART_MAXNODES) die(2, "out of tree nodes"); + return art_nnodes++; } -DEF_SKIPLIST(static, class, cmp_class, hdr_class) -static struct skiplist_hdr_class classes = {0}; -static int nclasses = 0; - -struct parsestate { - const os_char *filename; - struct kv_parser *parser; - char *lastvar; -}; - -static noreturn badparse(struct parsestate *state, const char *e) { - fprintf(stderr, "mkentprops: %" fS ":%d:%d: parse error: %s", - state->filename, state->parser->line, state->parser->col, e); - exit(1); + +#define ART_MAXLEAVES 8192 +static struct art_leaf { + int varstr; // offset of string (generated variable), if any, or -1 if none + u16 subtree; // art index of subtree (nested SendTable), or -1 if none + u16 nsubs; // number of subtrees (used to short-circuit the runtime search) +} art_leaves[ART_MAXLEAVES]; +static int art_nleaves = 0; +static u16 art_root = ART_NULL; // ServerClasses (which point at SendTables) +static u16 nclasses = 0; // similar short circuit for ServerClasses + +#define VAR_NONE -1 + +// for quick iteration over var names to generate decls header without ART faff +#define MAXDECLS 4096 +static int decls[MAXDECLS]; +static int ndecls = 0; + +static inline int art_newleaf(void) { + if (art_nleaves == ART_MAXLEAVES) die(2, "out of leaf nodes"); + return art_nleaves++; } -static void kv_cb(enum kv_token type, const char *p, uint len, void *ctxt) { - struct parsestate *state = ctxt; - switch_exhaust_enum (kv_token, type) { - case KV_IDENT: case KV_IDENT_QUOTED: - state->lastvar = malloc(len + 1); - if (!state->lastvar) die("couldn't allocate memory"); - memcpy(state->lastvar, p, len); state->lastvar[len] = '\0'; - break; - case KV_NEST_START: badparse(state, "unexpected nested block"); - case KV_NEST_END: badparse(state, "unexpected closing brace"); - case KV_VAL: case KV_VAL_QUOTED:; - struct prop *prop = malloc(sizeof(*prop)); - if (!prop) die("couldn't allocate memory"); - prop->varname = state->lastvar; - char *classname = malloc(len + 1); - if (!classname) die("couldn't allocate memory"); - memcpy(classname, p, len); classname[len] = '\0'; - char *propname = strchr(classname, '/'); - if (!propname) { - badparse(state, "network name not in class/prop format"); +static struct art_lookup_ret { + bool isnew; // true if node created, false if found + u16 leafidx; // index of value (leaf) node +} art_lookup(u16 *art, int soff, int len) { + assume(len > 0); // everything must be null-terminated! + for (;;) { + const uchar *p = (const uchar *)sbase + soff; + u16 cur = *art; + if (cur == ART_NULL) { // append + int new = art_newnode(); + *art = new; + art_firstbytes[new] = *p; + art_cores[new].soff = soff; + art_cores[new].slen = len; + art_cores[new].next = ART_NULL; + int leaf = art_newleaf(); // N.B. firstbyte is already 0 here + art_children[new] = leaf; + return (struct art_lookup_ret){true, leaf}; + } + while (art_firstbytes[cur] == *p) { + int nodelen = art_cores[cur].slen; + int matchlen = 1; + const uchar *q = (const uchar *)sbase + art_cores[cur].soff; + for (; matchlen < nodelen; ++matchlen) { + if (p[matchlen] != q[matchlen]) { + // node and key diverge: split into child nodes + // (left is new part of key string, right is existing tail) + art_cores[cur].slen = matchlen; + int l = art_newnode(), r = art_newnode(); + art_firstbytes[l] = p[matchlen]; + art_cores[l].soff = soff + matchlen; + art_cores[l].slen = len - matchlen; + art_cores[l].next = r; + art_firstbytes[r] = q[matchlen]; + art_cores[r].soff = art_cores[cur].soff + matchlen; + art_cores[r].slen = nodelen - matchlen; + art_cores[r].next = ART_NULL; + art_children[r] = art_children[cur]; + art_children[cur] = l; + int leaf = art_newleaf(); + art_children[l] = leaf; + return (struct art_lookup_ret){true, leaf}; + } } - *propname = '\0'; ++propname; // split! - prop->propname = propname; - struct class *class = skiplist_get_class(&classes, classname); - if (!class) { - class = malloc(sizeof(*class)); - if (!class) die("couldn't allocate memory"); - *class = (struct class){.name = classname}; - skiplist_insert_class(&classes, classname, class); - ++nclasses; + if (matchlen == len) { + // node matches entire key: we have matched an existing entry + return (struct art_lookup_ret){false, art_children[cur]}; } - // (if class is already there just leak classname, no point freeing) - if (!vec_push(&class->props, prop)) die("couldn't append to array"); + // node is substring of key: descend into child nodes + soff += matchlen; + len -= matchlen; + cur = art_children[cur]; // note: this can't be ART_NULL (thus loop) + p = (const uchar *)sbase + soff; // XXX: kinda silly dupe + } + // if we didn't match this node, try the next sibling node. + // if sibling is null, we'll hit the append case above. + art = &art_cores[cur].next; + } +} + +static struct art_leaf *helpgetleaf(u16 *art, const char *s, int len, + const os_char *parsefile, int parseline, u16 *countvar) { + struct art_lookup_ret leaf = art_lookup(art, s - sbase, len); + if (leaf.isnew) { + art_leaves[leaf.leafidx].varstr = VAR_NONE; + art_leaves[leaf.leafidx].subtree = ART_NULL; + ++*countvar; + } + else if (parsefile) { // it's null if an existing entry is allowed + dieparse(parsefile, parseline, "duplicate property name"); + } + return art_leaves + leaf.leafidx; +} + +static inline void handleentry(char *k, char *v, int vlen, + const os_char *file, int line) { + if (ndecls == MAXDECLS) die(2, "out of declaration entries"); + decls[ndecls++] = k - sbase; + char *propname = memchr(v, '/', vlen); + if (!propname) { + dieparse(file, line, "network name not in class/property format"); + } + *propname++ = '\0'; + int sublen = propname - v; + if (sublen > 65535) { + dieparse(file, line, "network class name is far too long"); + } + vlen -= sublen; + struct art_leaf *leaf = helpgetleaf(&art_root, v, sublen, 0, 0, &nclasses); + u16 *subtree = &leaf->subtree; + for (;;) { + if (vlen > 65535) { + dieparse(file, line, "property (SendTable) name is far too long"); + } + char *nextpart = memchr(propname, '/', vlen); + if (!nextpart) { + leaf = helpgetleaf(subtree, propname, vlen, file, line, + &leaf->nsubs); + leaf->varstr = k - sbase; break; - case KV_COND_PREFIX: case KV_COND_SUFFIX: - badparse(state, "unexpected conditional"); + } + *nextpart++ = '\0'; + sublen = nextpart - propname; + leaf = helpgetleaf(subtree, propname, sublen, file, line, &leaf->nsubs); + subtree = &leaf->subtree; + vlen -= sublen; + propname = nextpart; } } -static inline noreturn diewrite(void) { die("couldn't write to file"); } +static void parse(const os_char *file, int len) { + char *s = sbase; // for convenience + if (s[len - 1] != '\n') dieparse(file, 0, "invalid text file (missing EOL)"); + enum { BOL = 0, KEY = 4, KWS = 8, VAL = 12, COM = 16, ERR = -1 }; + static const s8 statetrans[] = { + // layout: any, space|tab, #, \n + [BOL + 0] = KEY, [BOL + 1] = ERR, [BOL + 2] = COM, [BOL + 3] = BOL, + [KEY + 0] = KEY, [KEY + 1] = KWS, [KEY + 2] = ERR, [KEY + 3] = ERR, + [KWS + 0] = VAL, [KWS + 1] = KWS, [KWS + 2] = ERR, [KWS + 3] = ERR, + [VAL + 0] = VAL, [VAL + 1] = VAL, [VAL + 2] = COM, [VAL + 3] = BOL, + [COM + 0] = COM, [COM + 1] = COM, [COM + 2] = COM, [COM + 3] = BOL + }; + char *key, *val; + for (int state = BOL, i = 0, line = 1; i < len; ++i) { + int transidx = state; + switch (s[i]) { + case '\0': dieparse(file, line, "unexpected null byte"); + case ' ': case '\t': transidx += 1; break; + case '#': transidx += 2; break; + case '\n': transidx += 3; + } + int newstate = statetrans[transidx]; + if_cold (newstate == ERR) { + if (state == BOL) dieparse(file, line, "unexpected indentation"); + if (s[i] == '\n') dieparse(file, line, "unexpected end of line"); + dieparse(file, line, "unexpected comment"); + } + switch_exhaust (newstate) { + case KEY: if_cold (state != KEY) key = s + i; break; + case KWS: if_cold (state != KWS) s[i] = '\0'; break; + case VAL: if_cold (state == KWS) val = s + i; break; + case COM: case BOL: + if (state == VAL) { + int j = i; + while (s[j - 1] == ' ' || s[j - 1] == '\t') --j; + s[j] = '\0'; + int vallen = j - (val - s) + 1; + handleentry(key, val, vallen, file, line); + } + } + line += state == BOL; + state = newstate; + } +} -#define _(x) \ - if (fprintf(out, "%s\n", x) < 0) diewrite(); -#define F(f, ...) \ - if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +static inline noreturn diewrite(void) { die(100, "couldn't write to file"); } +#define _(x) if (fprintf(out, "%s\n", x) < 0) diewrite(); +#define _i(x) for (int i = 0; i < indent; ++i) fputc('\t', out); _(x) +#define F(f, ...) if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +#define Fi(...) for (int i = 0; i < indent; ++i) fputc('\t', out); F(__VA_ARGS__) #define H() \ _( "/* This file is autogenerated by src/build/mkentprops.c. DO NOT EDIT! */") \ _( "") -static void decls(FILE *out) { - for (struct class *c = classes.x[0]; c; c = c->hdr.x[0]) { - for (struct prop **pp = c->props.data; - pp - c->props.data < c->props.sz; ++pp) { -F( "extern bool has_%s;", (*pp)->varname) -F( "extern int %s;", (*pp)->varname) +static void dosendtables(FILE *out, u16 art, int indent) { +_i("switch (*p) {") + while (art != ART_NULL) { + if (art_cores[art].slen != 1) { + const char *tail = sbase + art_cores[art].soff + 1; + int len = art_cores[art].slen - 1; +Fi(" case '%c': if (!strncmp(p + 1, \"%.*s\", %d)) {", +art_firstbytes[art], len, tail, len) + } + else { +Fi(" case '%c': {", art_firstbytes[art]) } + int idx = art_children[art]; + // XXX: kind of a dumb and bad way to distinguish these. okay for now... + if (sbase[art_cores[art].soff + art_cores[art].slen - 1] != '\0') { + dosendtables(out, idx, indent + 1); + } + else { + if (art_leaves[idx].varstr != VAR_NONE) { +_i(" if (mem_loads32(mem_offset(sp, off_SP_type)) != DT_DataTable) {") +Fi(" %s = mem_loads32(mem_offset(sp, off_SP_offset));", +sbase + art_leaves[idx].varstr); +Fi(" --need;") +_i(" }") + } + if (art_leaves[idx].subtree != ART_NULL) { +_i(" if (mem_loads32(mem_offset(sp, off_SP_type)) == DT_DataTable) {") +_i(" const struct SendTable *st = mem_loadptr(mem_offset(sp, off_SP_subtable));") +_i(" // BEGIN SUBTABLE") +Fi(" for (int i = 0, need = %d; i < st->nprops && need; ++i) {", +art_leaves[idx].nsubs) +_i(" const struct SendProp *sp = mem_offset(st->props, sz_SendProp * i);") +_i(" const char *p = mem_loadptr(mem_offset(sp, off_SP_varname));") + dosendtables(out, art_leaves[idx].subtree, indent + 4); +_i(" }") +_i(" // END SUBTABLE") +_i(" }") + } + } +_i(" } break;") + art = art_cores[art].next; } +_i("}") } -static void defs(FILE *out) { - for (struct class *c = classes.x[0]; c; c = c->hdr.x[0]) { - for (struct prop **pp = c->props.data; - pp - c->props.data < c->props.sz; ++pp) { -F( "bool has_%s = false;", (*pp)->varname) -F( "int %s;", (*pp)->varname) +static void doclasses(FILE *out, u16 art, int indent) { +_i("switch (*p) {") + for (; art != ART_NULL; art = art_cores[art].next) { + if (art_cores[art].slen != 1) { + const char *tail = sbase + art_cores[art].soff + 1; + int len = art_cores[art].slen - 1; +Fi(" case '%c': if (!strncmp(p + 1, \"%.*s\", %d)) {", +art_firstbytes[art], len, tail, len) } - } -_( "") -_( "static void initentprops(struct ServerClass *class) {") -F( " for (int needclasses = %d; class; class = class->next) {", nclasses) - char *else1 = ""; - for (struct class *c = classes.x[0]; c; c = c->hdr.x[0]) { - // TODO(opt): some sort of PHF or trie instead of chained strcmp, if we - // ever have more than a few classes/properties? -F( " %sif (!strcmp(class->name, \"%s\")) {", else1, c->name) -_( " struct SendTable *st = class->table;") -F( " int needprops = %d;", c->props.sz) -_( " for (struct SendProp *p = st->props;") -_( " mem_diff(p, st->props) < st->nprops * sz_SendProp;") -_( " p = mem_offset(p, sz_SendProp)) {") -_( " const char *varname = mem_loadptr(mem_offset(p, off_SP_varname));") - char *else2 = ""; - for (struct prop **pp = c->props.data; - pp - c->props.data < c->props.sz; ++pp) { -F( " %sif (!strcmp(varname, \"%s\")) {", else2, (*pp)->propname) -F( " has_%s = true;", (*pp)->varname) -F( " %s = mem_loads32(mem_offset(p, off_SP_offset));", - (*pp)->varname) -_( " if (!--needprops) break;") -_( " }") - else2 = "else "; + else { +Fi(" case '%c': {", art_firstbytes[art]) } -_( " }") -_( " if (!--needclasses) break;") -_( " }") - else1 = "else "; + int idx = art_children[art]; + // XXX: same dumb-and-bad-ness as above. there must be a better way! + if (sbase[art_cores[art].soff + art_cores[art].slen - 1] != '\0') { +Fi(" p += %d;", art_cores[art].slen) + doclasses(out, art_children[art], indent + 2); + } + else { + assume(art_leaves[idx].varstr == VAR_NONE); + assume(art_leaves[idx].subtree != ART_NULL); +_i(" const struct SendTable *st = class->table;") +Fi(" for (int i = 0, need = %d; i < st->nprops && need; ++i) {", +art_leaves[idx].nsubs) + // note: annoyingly long line here, but the generated code gets + // super nested anyway, so there's no point in caring really + // XXX: basically a dupe of dosendtables() - fold into above? +_i(" const struct SendProp *sp = mem_offset(st->props, sz_SendProp * i);") +_i(" const char *p = mem_loadptr(mem_offset(sp, off_SP_varname));") + dosendtables(out, art_leaves[idx].subtree, indent + 3); +_i(" }") + } +_i(" } break;") + } +_i("}") +} + +static void dodecls(FILE *out) { + for (int i = 0; i < ndecls; ++i) { + const char *s = sbase + decls[i]; +F( "extern int %s;", s); +F( "#define has_%s (!!%s)", s, s); // offsets will NEVER be 0, due to vtable! + } +} + +static void doinit(FILE *out) { + for (int i = 0; i < ndecls; ++i) { + const char *s = sbase + decls[i]; +F( "int %s = 0;", s); } +_( "") +_( "static inline void initentprops(const struct ServerClass *class) {") +_( " const char *p = class->name;") +F( " for (int need = %d; need && class; class = class->next) {", nclasses) + doclasses(out, art_root, 2); _( " }") _( "}") } int OS_MAIN(int argc, os_char *argv[]) { - for (++argv; *argv; ++argv) { - int f = os_open_read(*argv); - if (f == -1) die("couldn't open file"); - struct kv_parser kv = {0}; - struct parsestate state = {*argv, &kv}; - char buf[1024]; - int nread; - while (nread = os_read(f, buf, sizeof(buf))) { - if (nread == -1) die("couldn't read file"); - if (!kv_parser_feed(&kv, buf, nread, &kv_cb, &state)) goto ep; - } - if (!kv_parser_done(&kv)) { -ep: fprintf(stderr, "mkentprops: %" fS ":%d:%d: bad syntax: %s\n", - *argv, kv.line, kv.col, kv.errmsg); - exit(1); - } - os_close(f); - } + if (argc != 2) die(1, "wrong number of arguments"); + int f = os_open_read(argv[1]); + if (f == -1) die(100, "couldn't open file"); + vlong len = os_fsize(f); + if (len > 1u << 30 - 1) die(2, "input file is far too large"); + sbase = malloc(len); + if (!sbase) die(100, "couldn't allocate memory"); + if (os_read(f, sbase, len) != len) die(100, "couldn't read file"); + os_close(f); + parse(argv[1], len); FILE *out = fopen(".build/include/entprops.gen.h", "wb"); - if (!out) die("couldn't open entprops.gen.h"); + if (!out) die(100, "couldn't open entprops.gen.h"); H(); - decls(out); + dodecls(out); out = fopen(".build/include/entpropsinit.gen.h", "wb"); - if (!out) die("couldn't open entpropsinit.gen.h"); + if (!out) die(100, "couldn't open entpropsinit.gen.h"); H(); - defs(out); + doinit(out); return 0; } diff --git a/src/build/mkgamedata.c b/src/build/mkgamedata.c index 266a411..1fce1cf 100644 --- a/src/build/mkgamedata.c +++ b/src/build/mkgamedata.c @@ -19,10 +19,8 @@ #include #include "../intdefs.h" -#include "../kv.h" #include "../langext.h" #include "../os.h" -#include "vec.h" #ifdef _WIN32 #define fS "S" @@ -30,214 +28,247 @@ #define fS "s" #endif -static noreturn die(const char *s) { - fprintf(stderr, "mkgamedata: %s\n", s); - exit(100); +static noreturn die(int status, const char *s) { + fprintf(stderr, "mkentprops: %s\n", s); + exit(status); +} + +// concatenated input file contents - string values are indices off this +static char *sbase = 0; + +static const os_char *const *srcnames; +static noreturn dieparse(int file, int line, const char *s) { + fprintf(stderr, "mkentprops: %" fS ":%d: %s\n", srcnames[file], line, s); + exit(2); +} + +#define MAXENTS 32768 +static int tags[MAXENTS]; // varname/gametype +static int exprs[MAXENTS]; +static uchar indents[MAXENTS]; // nesting level +static schar srcfiles[MAXENTS]; +static int srclines[MAXENTS]; +static int nents = 0; + +static inline void handleentry(char *k, char *v, int indent, + int file, int line) { + int previndent = nents ? indents[nents - 1] : -1; // meh + if_cold (indent > previndent + 1) { + dieparse(file, line, "excessive indentation"); + } + if_cold (indent == previndent && !exprs[nents - 1]) { + dieparse(file, line - 1, "missing a value and/or conditional(s)"); + } + if_cold (nents == MAXENTS) die(2, "out of array indices"); + tags[nents] = k - sbase; + exprs[nents] = v - sbase; // will produce garbage for null v. this is fine! + indents[nents] = indent; + srcfiles[nents] = file; + srclines[nents++] = line; } /* - * We keep the gamedata KV format as simple as possible. Default values are + * -- Quick file format documentation! -- + * + * We keep the gamedata format as simple as possible. Default values are * specified as direct key-value pairs: * - * + * * - * Game- or engine-specific values are set using blocks: + * Game- or engine-specific values are set using indented blocks: * - * { ... [default ] } + * + * + * # you can write EOL comments too! + * * * The most complicated it can get is if conditionals are nested, which - * basically translates directly into nested ifs: - * { { } } - * [however many entries...] + * basically translates directly into nested ifs. * - * If that doesn't make sense, just look at one of the existing data files and - * then it should be obvious. :^) - * - * Note: if `default` isn't given in a conditional block, that piece of gamedata - * is considered unavailable and modules that use it won't get initialised/used - * unless all the conditions are met. + * Just be aware that whitespace is significant, and you have to use tabs. + * Any and all future complaints about that decision SHOULD - and MUST - be + * directed to the Python Software Foundation and the authors of the POSIX + * Makefile specification. In that order. */ -struct vec_ent VEC(struct ent *); -struct ent { - const char *name; // (or condition tag, in a child node) - const char *defexpr; - struct vec_ent subents; - struct ent *parent; // to back up a level during parse -}; -// root only contains subents list but it's easier to use the same struct -static struct ent root = {0}; - -struct parsestate { - const os_char *filename; - struct kv_parser *parser; - struct ent *curent; // current ent lol - bool haddefault; // blegh; -}; - -static noreturn badparse(struct parsestate *state, const char *e) { - fprintf(stderr, "mkgamedata: %" fS ":%d:%d: parse error: %s", - state->filename, state->parser->line, state->parser->col, e); - exit(1); -} -static void kv_cb(enum kv_token type, const char *p, uint len, void *ctxt) { - struct parsestate *state = ctxt; - switch (type) { - case KV_IDENT: case KV_IDENT_QUOTED:; - if (len == 7 && !memcmp(p, "default", 7)) { // special case! - if (state->curent == &root) { - badparse(state, "unexpected default keyword at top level"); +static void parse(int file, char *s, int len) { + if (s[len - 1] != '\n') dieparse(file, 0, "invalid text file (missing EOL)"); + enum { BOL = 0, KEY = 4, KWS = 8, VAL = 12, COM = 16, ERR = -1 }; + static const s8 statetrans[] = { + // layout: any, space|tab, #, \n + [BOL + 0] = KEY, [BOL + 1] = BOL, [BOL + 2] = COM, [BOL + 3] = BOL, + [KEY + 0] = KEY, [KEY + 1] = KWS, [KEY + 2] = COM, [KEY + 3] = BOL, + [KWS + 0] = VAL, [KWS + 1] = KWS, [KWS + 2] = COM, [KWS + 3] = BOL, + [VAL + 0] = VAL, [VAL + 1] = VAL, [VAL + 2] = COM, [VAL + 3] = BOL, + [COM + 0] = COM, [COM + 1] = COM, [COM + 2] = COM, [COM + 3] = BOL + }; + char *key, *val = sbase; // 0 index by default (invalid value works as null) + for (int state = BOL, i = 0, line = 1, indent = 0; i < len; ++i) { + int transidx = state; + char c = s[i]; + switch (c) { + case '\0': dieparse(file, line, "unexpected null byte"); + case ' ': + if_cold (state == BOL) { + dieparse(file, line, "unexpected space at start of line"); } - struct ent *e = state->curent; - if (e->defexpr) { - badparse(state, "multiple default keywords"); - } - state->haddefault = true; + case '\t': + transidx += 1; break; - } - state->haddefault = false; - char *k = malloc(len + 1); - if (!k) die("couldn't allocate key string"); - // FIXME(?): should check and prevent duplicate keys probably! - // need table.h or something to avoid O(n^2) :) - memcpy(k, p, len); k[len] = '\0'; - struct ent *e = malloc(sizeof(*e)); - if (!e) die("couldn't allocate memory"); - e->name = k; - e->defexpr = 0; - e->subents = (struct vec_ent){0}; - if (!vec_push(&state->curent->subents, e)) { - die("couldn't append to array"); - } - e->parent = state->curent; - state->curent = e; - break; - case KV_NEST_START: - if (state->haddefault) badparse(state, "default cannot be a block"); - break; - case KV_NEST_END: - if (!state->curent->parent) { - badparse(state, "unexpected closing brace"); - } - state->curent = state->curent->parent; - break; - case KV_VAL: case KV_VAL_QUOTED:; - char *s = malloc(len + 1); - if (!s) die("couldn't allocate value string"); - memcpy(s, p, len); s[len] = '\0'; - state->curent->defexpr = s; - if (!state->haddefault) { - // a non-default value is just a node that itself only has a - // default value. - state->curent = state->curent->parent; - } - break; - case KV_COND_PREFIX: case KV_COND_SUFFIX: - badparse(state, "unexpected conditional"); + case '#': transidx += 2; break; + case '\n': transidx += 3; + } + int newstate = statetrans[transidx]; + switch_exhaust (newstate) { + case KEY: if_cold (state != KEY) key = s + i; break; + case KWS: if_cold (state != KWS) s[i] = '\0'; break; + case VAL: if_cold (state == KWS) val = s + i; break; + case BOL: + indent += state == BOL; + if_cold (indent > 255) { // this shouldn't happen if we're sober + dieparse(file, line, "exceeded max nesting level (255)"); + } + case COM: + if_hot (state != BOL) { + if (state != COM) { // blegh! + int j = i; + while (s[j - 1] == ' ' || s[j - 1] == '\t') --j; + s[j] = '\0'; + handleentry(key, val, indent, file, line); + } + val = sbase; // reset this again + } + } + if_cold (c == '\n') { // ugh, so much for state transitions. + indent = 0; + ++line; + } + state = newstate; } } -static inline noreturn diewrite(void) { die("couldn't write to file"); } - -#define _doindent \ - for (int _indent = 0; _indent < indent; ++_indent) { \ - if (fputs("\t", out) == -1) diewrite(); \ - } -#define _(x) \ - if (fprintf(out, "%s\n", x) < 0) diewrite(); -#define _i(x) _doindent _(x) -#define F(f, ...) \ - if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); -#define Fi(...) _doindent F(__VA_ARGS__) +static inline noreturn diewrite(void) { die(100, "couldn't write to file"); } +#define _(x) if (fprintf(out, "%s\n", x) < 0) diewrite(); +#define _i(x) for (int i = 0; i < indent; ++i) fputc('\t', out); _(x) +#define F(f, ...) if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +#define Fi(...) for (int i = 0; i < indent; ++i) fputc('\t', out); F(__VA_ARGS__) #define H() \ _( "/* This file is autogenerated by src/build/mkgamedata.c. DO NOT EDIT! */") \ _( "") static void decls(FILE *out) { - for (struct ent *const *pp = root.subents.data; - pp - root.subents.data < root.subents.sz; ++pp) { - if ((*pp)->defexpr) { -F( "#define has_%s true", (*pp)->name) - if ((*pp)->subents.sz) { -F( "extern int %s;", (*pp)->name) - } - else { -F( "enum { %s = %s };", (*pp)->name, (*pp)->defexpr) - } + for (int i = 0; i < nents; ++i) { + if (indents[i] != 0) continue; +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if (exprs[i]) { // default value is specified - entry always exists + // *technically* this case is redundant - the other has_ macro would + // still work. however, having a distinct case makes the generated + // header a little easier to read at a glance. +F( "#define has_%s 1", sbase + tags[i]) } - else { -F( "extern bool has_%s;", (*pp)->name) -F( "extern int %s;", (*pp)->name) + else { // entry is missing unless a tag is matched + // implementation detail: INT_MIN is reserved for missing gamedata! + // XXX: for max robustness, should probably check for this in input? +F( "#define has_%s (%s != -2147483648)", sbase + tags[i], sbase + tags[i]) + } +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if_cold (i == nents - 1 || !indents[i + 1]) { // no tags - it's constant +F( "enum { %s = (%s) };", sbase + tags[i], sbase + exprs[i]) + } + else { // global variable intialised by gamedata_init() call +F( "extern int %s;", sbase + tags[i]); } } } -static void inits(FILE *out, const char *var, struct vec_ent *v, bool needhas, - int indent) { - for (struct ent *const *pp = v->data; pp - v->data < v->sz; ++pp) { -Fi("if (GAMETYPE_MATCHES(%s)) {", (*pp)->name) - if ((*pp)->defexpr) { - if (needhas) { -Fi(" has_%s = true;", var); +static void defs(FILE *out) { + for (int i = 0; i < nents; ++i) { + if (indents[i] != 0) continue; + if_hot (i < nents - 1 && indents[i + 1]) { +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if (exprs[i]) { +F( "int %s = (%s);", sbase + tags[i], sbase + exprs[i]) + } + else { +F( "int %s = -2147483648;", sbase + tags[i]) } -Fi(" %s = %s;", var, (*pp)->defexpr); } - inits(out, var, &(*pp)->subents, needhas && !(*pp)->defexpr, indent + 1); -_i("}") } } -static void defs(FILE *out) { - for (struct ent *const *pp = root.subents.data; - pp - root.subents.data < root.subents.sz; ++pp) { - if ((*pp)->defexpr) { - if ((*pp)->subents.sz) { -F( "int %s = %s;", (*pp)->name, (*pp)->defexpr); +static void init(FILE *out) { +_( "void gamedata_init(void) {") + int varidx; + int indent = 0; + for (int i = 0; i < nents; ++i) { + if (indents[i] < indents[i - 1]) { + for (; indent != indents[i]; --indent) { +_i("}") } } + if (indents[i] == 0) { + varidx = i; + continue; + } +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if (indents[i] > indents[i - 1]) { +Fi(" if (GAMETYPE_MATCHES(%s)) {", sbase + tags[i]) + ++indent; + } else { -F( "int %s;", (*pp)->name); -F( "bool has_%s = false;", (*pp)->name); +_i("}") +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) +Fi("else if (GAMETYPE_MATCHES(%s)) {", sbase + tags[i]) + } + if (exprs[i]) { +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) +Fi(" %s = (%s);", sbase + tags[varidx], sbase + exprs[i]) } } -_( "") -_( "void gamedata_init(void) {") - for (struct ent *const *pp = root.subents.data; - pp - root.subents.data < root.subents.sz; ++pp) { - inits(out, (*pp)->name, &(*pp)->subents, !(*pp)->defexpr, 1); + for (; indent != 0; --indent) { +_i("}") } _( "}") } int OS_MAIN(int argc, os_char *argv[]) { - for (++argv; *argv; ++argv) { - int fd = os_open_read(*argv); - if (fd == -1) die("couldn't open file"); - struct kv_parser kv = {0}; - struct parsestate state = {*argv, &kv, &root}; - char buf[1024]; - int nread; - while (nread = os_read(fd, buf, sizeof(buf))) { - if (nread == -1) die("couldn't read file"); - if (!kv_parser_feed(&kv, buf, nread, &kv_cb, &state)) goto ep; + srcnames = (const os_char *const *)argv; + int sbase_len = 0, sbase_max = 65536; + sbase = malloc(sbase_max); + if (!sbase) die(100, "couldn't allocate memory"); + int n = 1; + for (++argv; *argv; ++argv, ++n) { + int f = os_open_read(*argv); + if (f == -1) die(100, "couldn't open file"); + vlong len = os_fsize(f); + if (sbase_len + len > 1u << 29) { + die(2, "combined input files are far too large"); } - if (!kv_parser_done(&kv)) { -ep: fprintf(stderr, "mkgamedata: %" fS ":%d:%d: bad syntax: %s\n", - *argv, kv.line, kv.col, kv.errmsg); - exit(1); + if (sbase_len + len > sbase_max) { + fprintf(stderr, "mkgamedata: warning: need to resize string. " + "increase sbase_max to avoid this extra work!\n"); + sbase_max *= 4; + sbase = realloc(sbase, sbase_max); + if (!sbase) die(100, "couldn't grow memory allocation"); } - os_close(fd); + char *s = sbase + sbase_len; + if (os_read(f, s, len) != len) die(100, "couldn't read file"); + os_close(f); + parse(n, s, len); + sbase_len += len; } FILE *out = fopen(".build/include/gamedata.gen.h", "wb"); - if (!out) die("couldn't open gamedata.gen.h"); + if (!out) die(100, "couldn't open gamedata.gen.h"); H(); decls(out); out = fopen(".build/include/gamedatainit.gen.h", "wb"); - if (!out) die("couldn't open gamedatainit.gen.h"); + if (!out) die(100, "couldn't open gamedatainit.gen.h"); H(); defs(out); + _("") + init(out); return 0; } diff --git a/src/build/vec.h b/src/build/vec.h index 6b4dffb..6dfa645 100644 --- a/src/build/vec.h +++ b/src/build/vec.h @@ -62,7 +62,7 @@ static bool _vec_make_room(struct _vec *v, uint tsize, uint addcnt) { // internal: for reuse by vec0 #define _vec_push(v, val, slack) ( \ ((v)->sz + (slack) < (v)->max || \ - _vec_make_room((struct _vec *)(v), sizeof(val), 1)) && \ + _vec_make_room((struct _vec *)(v), sizeof(val), 1)) && /*NOLINT*/ \ ((v)->data[(v)->sz++ - slack] = (val), true) \ ) diff --git a/src/engineapi.c b/src/engineapi.c index 04e6a8c..b92de4d 100644 --- a/src/engineapi.c +++ b/src/engineapi.c @@ -115,7 +115,8 @@ void engineapi_lateinit(void) { // > can detect that and set the SPROP_IS_VECTOR_ELEM flag. // by doing this at the deferred stage, we avoid having to abs() everything if (srvdll && has_vtidx_GetAllServerClasses && has_sz_SendProp && - has_off_SP_varname && has_off_SP_offset) { + has_off_SP_varname && has_off_SP_type && has_off_SP_offset && + has_DT_DataTable) { initentprops(GetAllServerClasses(srvdll)); } } diff --git a/src/engineapi.h b/src/engineapi.h index 4489e21..4f96b73 100644 --- a/src/engineapi.h +++ b/src/engineapi.h @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith + * Copyright © 2024 Michael Smith * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -96,15 +96,6 @@ struct CMoveData { struct vec3f origin; }; -#define SENDPROP_INT 0 -#define SENDPROP_FLOAT 1 -#define SENDPROP_VEC 2 -#define SENDPROP_VECXY 3 -#define SENDPROP_STR 4 -#define SENDPROP_ARRAY 5 -#define SENDPROP_DTABLE 6 -#define SENDPROP_INT64 7 - // these have to be opaque because, naturally, they're unstable between // branches - access stuff using gamedata offsets as usual struct RecvProp; diff --git a/src/kv.c b/src/kv.c deleted file mode 100644 index 698aa8e..0000000 --- a/src/kv.c +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright © 2024 Michael Smith - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ - -#include "intdefs.h" -#include "kv.h" -#include "langext.h" - -#define EOF -1 - -// parser states, implemented by STATE() macros in kv_parser_feed() below. -// needs to be kept in sync! -enum { - ok, ok_slash, - ident, ident_slash, identq, - sep, sep_slash, condsep, condsep_slash, - cond_prefix, - val, val_slash, valq, afterval, afterval_slash, - cond_suffix -}; - -bool kv_parser_feed(struct kv_parser *this, const char *in, uint sz, - kv_parser_cb cb, void *ctxt) { - const char *p = in; - short c; - - // slight hack, makes init more convenient (just {0}) - if (!this->line) this->line = 1; - if (!this->outp) this->outp = this->tokbuf; - - // this is a big ol' blob of ugly state machine macro spaghetti - too bad! - #define INCCOL() (*p == '\n' ? (++this->line, this->col = 0) : ++this->col) - #define READ() (p == in + sz ? EOF : (INCCOL(), *p++)) - #define ERROR(s) do { \ - this->errmsg = s; \ - return false; \ - } while (0) - #define OUT(c) do { \ - if (this->outp - this->tokbuf == KV_TOKEN_MAX) { \ - ERROR("token unreasonably large!"); \ - } \ - *this->outp++ = (c); \ - } while (0) - #define CASE_WS case ' ': case '\t': case '\n': case '\r' - // note: multi-eval - #define IS_WS(c) ((c) == ' ' || (c) == '\t' || (c) == '\n' || (c) == '\r') - #define STATE(s) case s: s - #define HANDLE_EOF() do { case EOF: return true; } while (0) - #define SKIP_COMMENT(next) do { \ - this->state = next; \ - this->incomment = true; \ - goto start; \ - } while (0) - #define GOTO(s) do { this->state = s; goto s; } while (0) - #define CB(type) do { \ - cb(type, this->tokbuf, this->outp - this->tokbuf, ctxt); \ - this->outp = this->tokbuf; \ - } while (0) - // prefix and suffix conditions are more or less the same, just in different - // contexts, because very good syntax yes. - #define CONDSTATE(name, type, next) do { \ - STATE(name): \ - switch (c = READ()) { \ - HANDLE_EOF(); \ - CASE_WS: ERROR("unexpected whitespace in conditional"); \ - case '[': ERROR("unexpected opening bracket in conditional"); \ - case '{': case '}': ERROR("unexpected brace in conditional"); \ - case '/': ERROR("unexpected slash in conditional"); \ - case ']': CB(type); GOTO(next); \ - default: OUT(c); goto name; \ - } \ - } while (0) - -start: // special spaghetti so we don't have a million different comment states - if (this->incomment) while ((c = READ()) != '\n') if (c == EOF) return true; - this->incomment = false; - -switch (this->state) { - -STATE(ok): - c = READ(); -ident_postread: - switch (c) { - HANDLE_EOF(); - CASE_WS: goto ok; - case '#': ERROR("kv macros not supported"); - case '{': ERROR("unexpected control character"); - case '}': - if (!this->nestlvl) ERROR("too many closing braces"); - --this->nestlvl; - char c_ = c; - cb(KV_NEST_END, &c_, 1, ctxt); - goto ok; - case '"': GOTO(identq); - case '/': GOTO(ok_slash); - case '[': case ']': ERROR("unexpected conditional bracket"); - default: GOTO(ident); - } - -STATE(ok_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(ok); - default: GOTO(ident); - } - -ident: - OUT(c); -case ident: // continue here - switch (c = READ()) { - HANDLE_EOF(); - case '{': - CB(KV_IDENT); - ++this->nestlvl; - char c_ = c; - cb(KV_NEST_START, &c_, 1, ctxt); - GOTO(ok); - // XXX: assuming [ is a token break; haven't checked Valve's code - case '[': CB(KV_IDENT); GOTO(cond_prefix); - case '}': ERROR("unexpected closing brace"); - case ']': ERROR("unexpected closing bracket"); - case '"': ERROR("unexpected quote mark"); - CASE_WS: CB(KV_IDENT); GOTO(sep); - case '/': GOTO(ident_slash); - default: goto ident; - } - -STATE(ident_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': CB(KV_IDENT); SKIP_COMMENT(sep); - default: OUT('/'); GOTO(ident); - } - -STATE(identq): - switch (c = READ()) { - HANDLE_EOF(); - case '"': CB(KV_IDENT_QUOTED); GOTO(sep); - default: OUT(c); goto identq; - } - -STATE(sep): - do c = READ(); while (IS_WS(c)); - switch (c) { - HANDLE_EOF(); - case '{':; - char c_ = c; - ++this->nestlvl; - cb(KV_NEST_START, &c_, 1, ctxt); - GOTO(ok); - case '[': GOTO(cond_prefix); - case '"': GOTO(valq); - case '}': ERROR("unexpected closing brace"); - case ']': ERROR("unexpected closing bracket"); - case '/': GOTO(sep_slash); - default: GOTO(val); - } - -STATE(sep_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(sep); - default: GOTO(val); - } - -CONDSTATE(cond_prefix, KV_COND_PREFIX, condsep); - -STATE(condsep): - do c = READ(); while (IS_WS(c)); - switch (c) { - HANDLE_EOF(); - case '{':; - char c_ = c; - ++this->nestlvl; - cb(KV_NEST_START, &c_, 1, ctxt); - GOTO(ok); - case '}': ERROR("unexpected closing brace"); - case '[': ERROR("unexpected opening bracket"); - case ']': ERROR("unexpected closing bracket"); - case '/': GOTO(condsep_slash); - // these conditions only go before braces because very good syntax - default: ERROR("unexpected string value after prefix condition"); - } - -STATE(condsep_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(condsep); - default: ERROR("unexpected string value after prefix condition"); - } - -val: - OUT(c); -case val: // continue here - switch (c = READ()) { - HANDLE_EOF(); - case '{': ERROR("unexpected opening brace"); - case ']': ERROR("unexpected closing bracket"); - case '"': ERROR("unexpected quotation mark"); - // might get [ or } with no whitespace - case '}': - CB(KV_VAL); - --this->nestlvl; - char c_ = c; - cb(KV_NEST_END, &c_, 1, ctxt); - GOTO(afterval); - case '[': CB(KV_VAL); GOTO(cond_suffix); - CASE_WS: CB(KV_VAL); GOTO(afterval); - case '/': GOTO(val_slash); - default: goto val; - } - -STATE(val_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': CB(KV_VAL); SKIP_COMMENT(afterval); - default: OUT('/'); GOTO(val); - } - -STATE(valq): - switch (c = READ()) { - HANDLE_EOF(); - case '"': CB(KV_VAL_QUOTED); GOTO(afterval); - default: OUT(c); goto valq; - } - -STATE(afterval): - switch (c = READ()) { - HANDLE_EOF(); - CASE_WS: goto afterval; - case '[': GOTO(cond_suffix); - case '/': GOTO(afterval_slash); - // mildly dumb hack: if no conditional, we can just use the regular - // starting state handler to get next transition correct - just avoid - // double-reading the character - default: goto ident_postread; - } - -STATE(afterval_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(afterval); - default: GOTO(ident); - } - -CONDSTATE(cond_suffix, KV_COND_SUFFIX, ok); - -} - - #undef CONDSTATE - #undef CB - #undef GOTO - #undef SKIP_COMMENT - #undef HANDLE_EOF - #undef STATE - #undef IS_WS - #undef CASE_WS - #undef OUT - #undef ERROR - #undef READ - #undef INCCOL - - unreachable; // pretty sure! -} - -bool kv_parser_done(struct kv_parser *this) { - if (this->state != ok && this->state != afterval) { - this->errmsg = "unexpected end of input"; - return false; - } - if (this->nestlvl != 0) { - this->errmsg = "unterminated object (unbalanced braces)"; - return false; - } - return true; -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/kv.h b/src/kv.h deleted file mode 100644 index 63a9146..0000000 --- a/src/kv.h +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright © 2022 Michael Smith - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ - -#ifndef INC_KV_H -#define INC_KV_H - -#include "intdefs.h" - -/* - * Maximum length of a single token. Since this code is trying to avoid dynamic - * memory allocations, this arbitrary limit is chosen to accomodate all known - * "reasonable" tokens likely to come in any real files, probably. - */ -#define KV_TOKEN_MAX 512 - -/* - * Contains all the state associated with parsing (lexing?) a KeyValues file. - * Should be zeroed out prior to the first call (initialise with `= {0};`). - */ -struct kv_parser { - ushort line, col; /* the current line and column in the text */ - char state : 7; /* internal, shouldn't usually be touched directly */ - bool incomment : 1; /* internal */ - ushort nestlvl; /* internal */ - const char *errmsg; /* the error message, *IF* parsing just failed */ - - // trying to avoid dynamic allocations - valve's own parser seems to have - // a similar limit as well and our use case doesn't really need to worry - // about stupid massive values, so it's fine - char *outp; - char tokbuf[KV_TOKEN_MAX]; -}; - -/* - * These are the tokens that can be received by a kv_parser_cb (below). - * The x-macro and string descriptions are given to allow for easy debug - * stringification. Note that this "parser" is really just lexing out these - * tokens - handling the actual structure of the file should be done in the - * callback. This is so that data can be streamed rather than all read into - * memory at once. - */ -#define KV_TOKENS(X) \ - X(KV_IDENT, "ident") \ - X(KV_IDENT_QUOTED, "quoted-ident") \ - X(KV_VAL, "value") \ - X(KV_VAL_QUOTED, "quoted-value") \ - X(KV_COND_PREFIX, "cond-prefix") \ - X(KV_COND_SUFFIX, "cond-suffix") \ - X(KV_NEST_START, "object-start") \ - X(KV_NEST_END, "object-end") - -#define _ENUM(s, ignore) s, -enum kv_token { KV_TOKENS(_ENUM) }; -#undef _ENUM - -typedef void (*kv_parser_cb)(enum kv_token type, const char *p, uint len, - void *ctxt); - -/* - * Feed a block of text into the lexer. This would usually be a block of data - * read in from a file. - * - * The lexer is reentrant and can be fed arbitrarily sized blocks of data at a - * time. The function may return early in the event of an error; a return value - * of false indicates thaat this has happened, otherwise true is returned. - * - * In the event of an error, the errmsg, line and col fields of the parser - * struct can be used for diagnostics. - */ -bool kv_parser_feed(struct kv_parser *this, const char *in, uint sz, - kv_parser_cb cb, void *ctxt); - -/* - * This indicates that parsing is done; if this is called at an unexpected time, - * a parsing error will result; this is indicated in the return value as with - * kv_parser_feed. - */ -bool kv_parser_done(struct kv_parser *this); - -#endif - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/l4dwarp.c b/src/l4dwarp.c index d24e39c..c17c7b8 100644 --- a/src/l4dwarp.c +++ b/src/l4dwarp.c @@ -44,8 +44,8 @@ DEF_CCMD_HERE_UNREG(sst_l4d_testwarp, "Simulate a bot warping to you", struct edict *ed = ent_getedict(con_cmdclient + 1); if_cold (!ed) { errmsg_errorx("couldn't access player entity"); return; } void *e = GetBaseEntity(ed->ent_unknown); // is this call required? - struct vec3f *org = mem_offset(e, off_entpos); - struct vec3f *ang = mem_offset(e, off_eyeang); + const struct vec3f *org = mem_offset(e, off_entpos); + const struct vec3f *ang = mem_offset(e, off_eyeang); // L4D idle warps go up to 10 units behind yaw, lessening based on pitch. float pitch = ang->x * M_PI / 180, yaw = ang->y * M_PI / 180; float shift = -10 * cos(pitch); diff --git a/src/mem.h b/src/mem.h index 7b1a0b0..6acf018 100644 --- a/src/mem.h +++ b/src/mem.h @@ -67,7 +67,7 @@ static inline usize mem_loadusize(const void *p) { } /* Adds a byte count to a pointer and returns a freely-assignable pointer. */ -static inline void *mem_offset(void *p, int off) { return (char *)p + off; } +static inline void *mem_offset(const void *p, int off) { return (char *)p + off; } /* Returns the offset in bytes from one pointer to another (p - q). */ static inline ssize mem_diff(const void *p, const void *q) { diff --git a/test/kv.test.c b/test/kv.test.c deleted file mode 100644 index 1bd1784..0000000 --- a/test/kv.test.c +++ /dev/null @@ -1,55 +0,0 @@ -/* This file is dedicated to the public domain. */ - -{.desc = "the KeyValues parser"}; - -// undef conflicting macros -#undef ERROR // windows.h -#undef OUT // " -#undef EOF // stdio.h -#include "../src/kv.c" - -#include "../src/intdefs.h" -#include "../src/langext.h" - -static noreturn die(const struct kv_parser *kvp) { - fprintf(stderr, "parse error: %d:%d: %s\n", kvp->line, kvp->col, - kvp->errmsg); - exit(1); -} - -static void tokcb(enum kv_token type, const char *p, uint len, - void *ctxt) { - // nop - we're just testing the tokeniser -} - -static const char data[] = -"KeyValues {\n\ - Key/1 Val1![conditional]\n\ - Key2\n\ -Val2// comment\n\ - \"String Key\" // also comment\n\ - Val3 Key4 [conditional!]{ Key5 \"Value Five\" } // one more\n\ -} \n\ -"; -static const int sz = sizeof(data) - 1; - -TEST("parsing should work with any buffer size") { - for (int chunksz = 3; chunksz <= sz; ++chunksz) { - struct kv_parser kvp = {0}; - // sending data in chunks to test reentrancy - for (int chunk = 0; chunk * chunksz < sz; ++chunk) { - int thischunk = chunksz; - if (chunk * chunksz + thischunk > sz) { - thischunk = sz - chunk * chunksz; - } - if (!kv_parser_feed(&kvp, data + chunk * chunksz, thischunk, - tokcb, 0)) { - die(&kvp); - } - } - if (!kv_parser_done(&kvp)) die(&kvp); - } - return true; -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 -- cgit v1.2.3