Repository navigation
Fix SCRIPT_FUNCTION field offsets after executeFunction for 1.16.244 - #56
Open
kiffpuppygames wants to merge 1 commit into
Open
kiffpuppygames wants to merge 1 commit into
kiffpuppygames wants to merge 1 commit into
Conversation
SCRIPT_FUNCTION is one entry of the game's console-command and script-command arrays. In Starfield 1.16.244 an entry is still 0x58 bytes, but + 0x38 is a single byte written with executeFunction (then 7 bytes of padding), compileFunction is at + 0x40, conditionFunction at + 0x48 and the two flag bytes at + 0x50 and + 0x51. The declared layout keeps Fallout 4's field order with the 8 extra bytes at the end, so the size check passed. TESConditionItem::IsTrue (71429) calls the condition function from + 0x48, the script compiler (65834) calls the compile function from + 0x40, and the function that copies the entries into the arrays (46078) copies exactly these offsets. The flag names carry over by position only. Code that tests compileFunction == nullptr reads the always-zero byte at + 0x38 today and the real compile pointer, set in every filled entry, after this.
kiffpuppygames
marked this pull request as ready for review
November 2, 2026 19:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SCRIPT_FUNCTION(include/RE/S/Script.h) describes one console command or one script function: its name, help text and parameter list, and the functions the game calls to run it, to compile it from text and to evaluate it as a condition. The game keeps two arrays of these entries: the console commands (GetConsoleCommands, id 896666; 438 are filled in 1.16.244) and the script commands (GetScriptCommands, id 896669; 947 entries, many of which are also the condition functions used on quests, dialogue and perks).In Starfield 1.16.244 an entry is still 0x58 bytes, but every field after
executeFunctionsits 8 bytes later thanScript.hdeclares. This PR moves those fields. The size check (static_assert(sizeof(SCRIPT_FUNCTION) == 0x58)) still holds.executeFunctioncompileFunctionconditionFunctioneditorFilterinvalidatesCellListunk4Ato the endWhat is in the moved fields
executeFunction. It is 0 in every entry when the tables are built, and I don't know what it means. + 0x39 to + 0x3F are padding.compileFunction(+ 0x40) is ScriptCompiler::StandardCompile (id 65846) in 944 of the 947 script-command entries (the other three are ShowMessage, EssentialDeathReload and ShowWarning) and in all 438 console-command entries.conditionFunction(+ 0x48) holds the condition function of a script command, for example id 65625 for IsInSpaceship (opcode 0x1338).Why this has gone unnoticed
Everything up to and including
executeFunctionis correct: the name, help text, parameter list and execute function. Those are the fields a plugin rewrites when it adds a console command by taking over an existing entry, and it leaves the real compile function at + 0x40 alone, so the game still compiles the typed command. Console commands added this way work.The moved fields are rarely touched, and touching them mostly has no visible effect:
editorFilterandinvalidatesCellList, as several plugins do when they take over a console command, writes the low two bytes of the real condition pointer at + 0x48. The condition readers I found (TESConditionItem::IsTrue, and ids 104364 and 104365) read only the script-command array, so for a console-command entry nothing reads that pointer.compileFunctionreturns the byte at + 0x38, which is always 0. Nothing crashes; the code just gets a wrong answer.The layout looks plausible. Fallout 4's entry is 0x50 bytes with this same field order (CommonLibF4's
include/RE/Bethesda/Script.h: compile at + 0x38, condition at + 0x40, flags at + 0x48). Starfield's entries are 0x58 bytes apart, andScript.hkeeps Fallout 4's order and adds the 8 extra bytes as unknown fields at the end. In 1.16.244 they are right afterexecuteFunction. The size check passes either way, since it checks only the total.What goes wrong with the current layout
conditionFunctionthrough the struct calls the entry's compile function (+ 0x40) with condition arguments.conditionFunctionto add a condition replaces the entry's compile function, and the game keeps calling the old condition function at + 0x48.compileFunctionwrites over the unknown byte and its padding, and the game's compile function stays unchanged.Evidence
TESConditionItem::IsTrue(id 71429, IDs.h:2445), the game's condition evaluator, loads the condition function fromGetScriptCommands()[index] + 0x48(at + 0xb18) and calls it with (ConditionCheckParams&, param1, param2, float&).[entry + 0x40]with (numParams, params, line, script) at + 0xb58; the expression compiler (id 65837) does the same.I have checked only 1.16.244, so I can't say whether the layout was already like this at launch or changed in a later update.
Other code with the same layout
SFSE 0.2.21's
GameScript.h:106-117declares the same layout, and itsHooks_Script.cpp:95-96writesbEditorFilterandbInvalidatesCellListinto BetaComment's entry. In 1.16.244 those are the low two bytes of that console command's condition pointer, which is null, so the write has no effect.Code built against the current layout
CommonLibSF is a static library, so already-built plugins are not affected; a plugin picks this change up only when it is rebuilt. One behaviour changes then: a test such as
compileFunction == nullptrreads the always-zero byte today, so it is true for every entry, and it is false for every filled entry with this PR. At least one public plugin picks a console-command slot with that test and would need a different one after rebuilding.