pokeemmo/include/script.h
Martin Griffin bb781f21a1
Arbitrary trainer scripts + map script/trigger softlock prevention (#5033)
Script_RunImmediatelyUntilEffect runs a script until either a specified
effect may occur or it reaches an end.

All existing script commands and natives, and some specials, call
Script_RequestEffects which allows us to analyze them.

Any downstream script commands/natives/specials will be statically known
not to call Script_RequestEffects and treated as if they have all
effects. Manually tagging them with requests_effects=1 and calling
Script_RequestEffects will make them analyzable.

Using these, we're able to execute scripts until they either exit with
no effect, or would possibly have an effect. This allows us to:
1. Not run on frame map scripts or triggers if they would have no
   effect.
2. Immediately run triggers if they only affect flags/vars. This removes
   the lag frames when biking into the Cycling Road, for example.
3. Migrate on load/on transition/on resume/on return to field/on dive
   warp scripts onto the global script context if they would block
   (approximated via SCREFF_HARDWARE).
4. Support arbitrary control flow in trainer scripts. The trainer does
   not see the player if the script has no effect, and the trainer will
   use whichever trainerbattle command is branched to.
5. Support arbitrary scripts in trainer scripts. cant_see and
   cant_see_if_* commands have been introduced so that scripts are able
   to do something when the player interacts with the trainer even if
   that trainer wouldn't see them.
2025-01-08 10:27:00 +00:00

178 lines
6.3 KiB
C

#ifndef GUARD_SCRIPT_H
#define GUARD_SCRIPT_H
#include <setjmp.h>
struct ScriptContext;
typedef bool8 (*ScrCmdFunc)(struct ScriptContext *);
typedef u8 Script[];
struct ScriptContext
{
u8 stackDepth;
u8 mode;
u8 comparisonResult;
bool8 breakOnTrainerBattle;
u8 (*nativePtr)(void);
const u8 *scriptPtr;
const u8 *stack[20];
ScrCmdFunc *cmdTable;
ScrCmdFunc *cmdTableEnd;
u32 data[4];
};
#define ScriptReadByte(ctx) (*(ctx->scriptPtr++))
void InitScriptContext(struct ScriptContext *ctx, void *cmdTable, void *cmdTableEnd);
u8 SetupBytecodeScript(struct ScriptContext *ctx, const u8 *ptr);
void SetupNativeScript(struct ScriptContext *ctx, bool8 (*ptr)(void));
void StopScript(struct ScriptContext *ctx);
bool8 RunScriptCommand(struct ScriptContext *ctx);
void ScriptJump(struct ScriptContext *ctx, const u8 *ptr);
void ScriptCall(struct ScriptContext *ctx, const u8 *ptr);
void ScriptReturn(struct ScriptContext *ctx);
u16 ScriptReadHalfword(struct ScriptContext *ctx);
u32 ScriptReadWord(struct ScriptContext *ctx);
u32 ScriptPeekWord(struct ScriptContext *ctx);
void LockPlayerFieldControls(void);
void UnlockPlayerFieldControls(void);
bool8 ArePlayerFieldControlsLocked(void);
void ScriptContext_Init(void);
bool8 ScriptContext_IsEnabled(void);
bool8 ScriptContext_RunScript(void);
void ScriptContext_SetupScript(const u8 *ptr);
void ScriptContext_ContinueScript(struct ScriptContext *ctx);
void ScriptContext_Stop(void);
void ScriptContext_Enable(void);
void RunScriptImmediately(const u8 *ptr);
const u8 *MapHeaderGetScriptTable(u8 tag);
void MapHeaderRunScriptType(u8 tag);
const u8 *MapHeaderCheckScriptTable(u8 tag);
void RunOnLoadMapScript(void);
void RunOnTransitionMapScript(void);
void RunOnResumeMapScript(void);
void RunOnReturnToFieldMapScript(void);
void RunOnDiveWarpMapScript(void);
bool8 TryRunOnFrameMapScript(void);
void TryRunOnWarpIntoMapScript(void);
u32 CalculateRamScriptChecksum(void);
void ClearRamScript(void);
bool8 InitRamScript(const u8 *script, u16 scriptSize, u8 mapGroup, u8 mapNum, u8 objectId);
const u8 *GetRamScript(u8 objectId, const u8 *script);
bool32 ValidateSavedRamScript(void);
u8 *GetSavedRamScriptIfValid(void);
void InitRamScript_NoObjectEvent(u8 *script, u16 scriptSize);
// srccmd.h
void SetMovingNpcId(u16 npcId);
extern u8 gMsgIsSignPost;
extern u8 gMsgBoxIsCancelable;
/* Script effects analysis.
*
* 'RunScriptImmediatelyUntilEffect' executes a script until it reaches
* the first command which calls 'Script_RequestEffects' with an
* effect in 'effects' in which case it returns 'TRUE' and stores the
* current state in 'ctx'; or until it reaches an 'end'/'return' in
* which case it returns 'FALSE'.
*
* 'Script_HasNoEffect' wraps 'RunScriptImmediatelyUntilEffect' and
* returns 'TRUE' if the script exits without an effect on the save or
* the hardware, or 'FALSE' if it would have an effect (the effect is
* not performed).
*
* Commands, natives, and specials which call 'Script_RequestEffects'
* must be explicitly tagged with 'requests_effects=1', and must call
* the function before any of those effects occur. An untagged function
* could cause any effect, so execution is stopped to be safe. If the
* code has no effects it must call 'Script_RequestEffects(SCREFF_V1)'
* to note that explicitly.
*
* Regular variables are in the save (so should use 'SCREFF_SAVE'), but
* special variables are not in the save, so 'Script_RequestWriteVar' is
* provided to only request the 'SCREFF_SAVE' effect for a non-special
* variable.
*
* The 'effects' parameter to 'RunScriptImmediatelyUntilEffect' and
* 'Script_RequestEffects' must be the bitwise or of an effects version
* (currently 'SCREFF_V1') and any number of effects. For example
* 'Script_RequestEffects(SCREFF_V1 | SCREFF_SAVE)'. */
enum // effects
{
SCREFF_SAVE = 1 << 0, // writes to the save.
SCREFF_HARDWARE = 1 << 1, // writes to a hardware register.
SCREFF_TRAINERBATTLE = 1 << 2, // 'trainerbattle' command.
};
enum // effects versions
{
SCREFF_V1 = ~7,
};
extern struct ScriptEffectContext *gScriptEffectContext;
bool32 RunScriptImmediatelyUntilEffect_Internal(u32 effects, const u8 *ptr, struct ScriptContext *ctx);
bool32 Script_HasNoEffect(const u8 *ptr);
void Script_GotoBreak_Internal(void);
void Script_RequestEffects_Internal(u32 effects);
void Script_RequestWriteVar_Internal(u32 varId);
static inline bool32 Script_IsAnalyzingEffects(void)
{
return gScriptEffectContext != NULL;
}
#define RunScriptImmediatelyUntilEffect(effects, ptr, ctx) \
({ \
_Static_assert((effects) & 0x80000000, "RunScriptImmediatelyUntilEffect requires an effects version"); \
RunScriptImmediatelyUntilEffect_Internal(effects, ptr, ctx); \
})
/* Optimize 'Script_RequestEffects' to a no-op if it would have no
* effect. 'Script_RequestEffects' must be called in all commands and
* natives/specials with 'request_effects=TRUE' even if it would have
* no effect to future-proof against new effects. */
#define Script_RequestEffects(effects) \
({ \
_Static_assert((effects) & 0x80000000, "Script_RequestEffects requires an effects version"); \
if ((effects) != SCREFF_V1) \
if (Script_IsAnalyzingEffects()) \
Script_RequestEffects_Internal(effects); \
})
/* Optimize 'Script_RequestWriteVar' to a no-op if it would have no
* effect. */
#define Script_RequestWriteVar(varId) \
({ \
if (Script_IsAnalyzingEffects()) \
Script_RequestWriteVar_Internal(varId); \
})
static inline void Script_CheckEffectInstrumentedSpecial(u32 specialId)
{
typedef u16 (*SpecialFunc)(void);
extern const SpecialFunc gSpecials[];
// In ROM mirror 1.
if (Script_IsAnalyzingEffects() && (((uintptr_t)gSpecials[specialId]) & 0xE000000) != 0xA000000)
Script_GotoBreak_Internal();
}
static inline void Script_CheckEffectInstrumentedGotoNative(bool8 (*func)(void))
{
// In ROM mirror 1.
if (Script_IsAnalyzingEffects() && (((uintptr_t)func) & 0xE000000) != 0xA000000)
Script_GotoBreak_Internal();
}
static inline void Script_CheckEffectInstrumentedCallNative(void (*func)(struct ScriptContext *))
{
// In ROM mirror 1.
if (Script_IsAnalyzingEffects() && (((uintptr_t)func) & 0xE000000) != 0xA000000)
Script_GotoBreak_Internal();
}
#endif // GUARD_SCRIPT_H