Skip to content

File interception ​

Group files · 8 exports · from forks/gamexueren/scripthook.h (framework 1.0-beta7-wildm1, SH_API_VERSION 2)

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.

ExportSummary
ShFileRuleAddRegister a rule.
ShFileRuleDelTake a rule back out.
ShFileMatchCountHow many rules are registered right now.
ShFileInstalledWhether the layer's hooks are in place - 1 while at least one target is hooked, 0 after the last rule has gone.
ShFileCallCountHow many calls the layer has looked at this session: every call that reached the dispatch, whether a rule matched it or not.
ShFileOwnMark this thread's own file calls.
ShFileActionNameThe name of one action, for a log line or a menu row: "hide", "redirect", "decide", "watch", "" for anything else.
ShFileStatusOne 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

WildM is an independent community project. It is not affiliated with or endorsed by Ubisoft. Ghost Recon Wildlands is a trademark of Ubisoft Entertainment.