Theme
File interception
One owner for the file APIs, and rules instead of hooks.
Five parts of this repository used to intercept the same kernel32 file calls with five private arrangements: skipintro patched the main module's import table by hand, while GhostNoWipe, GhostWipeProbe, forgeprobe and scripthook_forge_io each built their own MinHook set. MinHook keeps one hook per target per module, so those arrangements have been shaping the code around them - forge_io asks a handle where its file is with GetFinalPathNameByHandleW because CreateFileW was taken, and skipintro wrote a PE parser because a slot was easier to own than a target.
This layer is that owner. It hooks the file APIs once, in the framework DLL, and everything else - framework modules and plugins alike - registers a rule. A rule says which file it is about, which calls it covers, and what to do when one matches:
- SH_FILE_HIDE answer "no such file" without calling anything;
- SH_FILE_REDIRECT run the call against another path;
- SH_FILE_DECIDE hand the call to desc->before, which either answers it or lets it through.
plus an optional desc->after, which sees the call once it has run (or been answered) and may change what the caller gets - ReadFile's buffer among it. A watcher is a rule with only an after callback: it looks at everything and changes nothing.
More than one rule can match one call. They are weighed, not raced: HIDE beats REDIRECT beats DECIDE, and within one action the rule registered first wins. Hiding first is deliberate - "the file is not there" is the most conservative answer available, and the only one that cannot hand a caller something wrong.
The hooks are inline, so they answer for the whole process: the game, every plugin and the framework's own modules go through this one place, and a pointer taken with GetProcAddress is intercepted exactly like a static import. That is the point - and it is why the layer also has to know when NOT to look: its own pass is marked, a thread can mark its own I/O with ShFileOwn, and a callback's own file calls pass straight through untouched.
Nothing is installed until a rule is registered, and the last unregistration takes every hook and trampoline back out: a session that registers nothing runs with not one intercepted call. That is what lets a one-shot user keep its promise - skipintro releases its rule when it is done, and the layer uninstalls itself if nobody else is left.
The callbacks run on the calling thread, inside the file call they are about, with the caller's stack below them: keep them short, never block, and never wait for another thread that needs the same thread to make a file call. A callback may call any file API it likes - the layer recognises its own threads and passes those calls through - but it should not expect its own rules to apply to them: they do not, on purpose, which is what keeps "look at the real state of the disk" possible from inside a rule.
Not here, on purpose: no priority field (three actions and registration order are the whole ordering rule), no directory virtualization, no way for a plugin to hold or call the real functions, and no global switch. A rule is a registration, and two writers of one answer is the thing this layer exists to prevent.
| Export | Summary |
|---|---|
ShFileRuleAdd | Register a rule. |
ShFileRuleDel | Take a rule back out. |
ShFileMatchCount | How many rules are registered right now. |
ShFileInstalled | Whether the layer's hooks are in place - 1 while at least one target is hooked, 0 after the last rule has gone. |
ShFileCallCount | How many calls the layer has looked at this session: every call that reached the dispatch, whether a rule matched it or not. |
ShFileOwn | Mark this thread's own file calls. |
ShFileActionName | The name of one action, for a log line or a menu row: "hide", "redirect", "decide", "watch", "" for anything else. |
ShFileStatus | One line about what the layer is doing, for a status row or a log: how many rules, which of them are watching or deciding, whether the hooks are in place and how many calls have been looked at. |
ShFileRuleAdd
c
SH_API ShFileRule *ShFileRuleAdd(const ShFileRuleDesc *desc);Register a rule. 1 = installed, and the layer's hooks go up with it. 0 = refused, with the reason in logs\scripthook_files.log (no slot left, a group with no bit in it, SH_FILE_REDIRECT with no to, SH_FILE_DECIDE with no before and no after, or a name longer than the table holds). The caller's module is the identity the log and ShFileRuleOwner report, so a plugin does not name itself.
scripthook.h, line 2614
ShFileRuleDel
c
SH_API int ShFileRuleDel(ShFileRule *rule);Take a rule back out. 1 = it was live and is not any more; 0 = it was already gone (never fails otherwise). The hooks come down with the last rule, so this is also how a one-shot user puts the process back the way it found it.
scripthook.h, line 2620
ShFileMatchCount
c
SH_API int ShFileMatchCount(void);How many rules are registered right now.
scripthook.h, line 2623
ShFileInstalled
c
SH_API int ShFileInstalled(void);Whether the layer's hooks are in place - 1 while at least one target is hooked, 0 after the last rule has gone.
scripthook.h, line 2627
ShFileCallCount
c
SH_API uint32_t ShFileCallCount(void);How many calls the layer has looked at this session: every call that reached the dispatch, whether a rule matched it or not.
scripthook.h, line 2631
ShFileOwn
c
SH_API void ShFileOwn(int on);Mark this thread's own file calls. While on, the layer does not look at anything this thread does: no rule is applied and no observer sees it. For a module that reads files as part of serving one (the forge loader's own reads are the reason this exists), which would otherwise be fed back through its own rules. Every turn on must be matched by one off, and it does not nest.
scripthook.h, line 2639
ShFileActionName
c
SH_API const char *ShFileActionName(int action);The name of one action, for a log line or a menu row: "hide", "redirect", "decide", "watch", "" for anything else. A translation key: pass it through ShLang before showing it.
scripthook.h, line 2644
ShFileStatus
c
SH_API int ShFileStatus(char *buf, int n);One line about what the layer is doing, for a status row or a log: how many rules, which of them are watching or deciding, whether the hooks are in place and how many calls have been looked at. Returns the length written, 0 when buf is too small.
scripthook.h, line 2650