#include "interpreter_extra.h"

#include <limits.h>
#include <stdio.h>
#include <string.h>

#include "actions.h"
#include "animation.h"
#include "art.h"
#include "color.h"
#include "combat.h"
#include "combat_ai.h"
#include "critter.h"
#include "debug.h"
#include "dialog.h"
#include "display_monitor.h"
#include "endgame.h"
#include "game.h"
#include "game_dialog.h"
#include "game_movie.h"
#include "game_sound.h"
#include "geometry.h"
#include "interface.h"
#include "item.h"
#include "light.h"
#include "loadsave.h"
#include "map.h"
#include "object.h"
#include "palette.h"
#include "party_member.h"
#include "perk.h"
#include "proto.h"
#include "proto_instance.h"
#include "queue.h"
#include "random.h"
#include "reaction.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_opcodes.h"
#include "skill.h"
#include "stat.h"
#include "svga.h"
#include "text_object.h"
#include "tile.h"
#include "trait.h"
#include "vcr.h"
#include "worldmap.h"

namespace fallout {

typedef enum ScriptError {
    SCRIPT_ERROR_NOT_IMPLEMENTED,
    SCRIPT_ERROR_OBJECT_IS_NULL,
    SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID,
    SCRIPT_ERROR_FOLLOWS,
    SCRIPT_ERROR_COUNT,
} ScriptError;

typedef enum Metarule {
    METARULE_SIGNAL_END_GAME = 13,
    METARULE_FIRST_RUN = 14,
    METARULE_ELEVATOR = 15,
    METARULE_PARTY_COUNT = 16,
    METARULE_AREA_KNOWN = 17,
    METARULE_WHO_ON_DRUGS = 18,
    METARULE_MAP_KNOWN = 19,
    METARULE_IS_LOADGAME = 22,
    METARULE_CAR_CURRENT_TOWN = 30,
    METARULE_GIVE_CAR_TO_PARTY = 31,
    METARULE_GIVE_CAR_GAS = 32,
    METARULE_SKILL_CHECK_TAG = 40,
    METARULE_DROP_ALL_INVEN = 42,
    METARULE_INVEN_UNWIELD_WHO = 43,
    METARULE_GET_WORLDMAP_XPOS = 44,
    METARULE_GET_WORLDMAP_YPOS = 45,
    METARULE_CURRENT_TOWN = 46,
    METARULE_LANGUAGE_FILTER = 47,
    METARULE_VIOLENCE_FILTER = 48,
    METARULE_WEAPON_DAMAGE_TYPE = 49,
    METARULE_CRITTER_BARTERS = 50,
    METARULE_CRITTER_KILL_TYPE = 51,
    METARULE_SET_CAR_CARRY_AMOUNT = 52,
    METARULE_GET_CAR_CARRY_AMOUNT = 53,
} Metarule;

typedef enum Metarule3 {
    METARULE3_CLR_FIXED_TIMED_EVENTS = 100,
    METARULE3_MARK_SUBTILE = 101,
    METARULE3_SET_WM_MUSIC = 102,
    METARULE3_GET_KILL_COUNT = 103,
    METARULE3_MARK_MAP_ENTRANCE = 104,
    METARULE3_WM_SUBTILE_STATE = 105,
    METARULE3_TILE_GET_NEXT_CRITTER = 106,
    METARULE3_ART_SET_BASE_FID_NUM = 107,
    METARULE3_TILE_SET_CENTER = 108,
    // chem use preference
    METARULE3_109 = 109,
    // probably true if car is out of fuel
    METARULE3_110 = 110,
    // probably returns city index
    METARULE3_111 = 111,
} Metarule3;

typedef enum CritterTrait {
    CRITTER_TRAIT_PERK = 0,
    CRITTER_TRAIT_OBJECT = 1,
    CRITTER_TRAIT_TRAIT = 2,
} CritterTrait;

typedef enum CritterTraitObject {
    CRITTER_TRAIT_OBJECT_AI_PACKET = 5,
    CRITTER_TRAIT_OBJECT_TEAM = 6,
    CRITTER_TRAIT_OBJECT_ROTATION = 10,
    CRITTER_TRAIT_OBJECT_IS_INVISIBLE = 666,
    CRITTER_TRAIT_OBJECT_GET_INVENTORY_WEIGHT = 669,
} CritterTraitObject;

// See [opGetCritterState].
typedef enum CritterState {
    CRITTER_STATE_NORMAL = 0x00,
    CRITTER_STATE_DEAD = 0x01,
    CRITTER_STATE_PRONE = 0x02,
} CritterState;

enum {
    INVEN_TYPE_WORN = 0,
    INVEN_TYPE_RIGHT_HAND = 1,
    INVEN_TYPE_LEFT_HAND = 2,
    INVEN_TYPE_INV_COUNT = -2,
};

typedef enum FloatingMessageType {
    FLOATING_MESSAGE_TYPE_WARNING = -2,
    FLOATING_MESSAGE_TYPE_COLOR_SEQUENCE = -1,
    FLOATING_MESSAGE_TYPE_NORMAL = 0,
    FLOATING_MESSAGE_TYPE_BLACK,
    FLOATING_MESSAGE_TYPE_RED,
    FLOATING_MESSAGE_TYPE_GREEN,
    FLOATING_MESSAGE_TYPE_BLUE,
    FLOATING_MESSAGE_TYPE_PURPLE,
    FLOATING_MESSAGE_TYPE_NEAR_WHITE,
    FLOATING_MESSAGE_TYPE_LIGHT_RED,
    FLOATING_MESSAGE_TYPE_YELLOW,
    FLOATING_MESSAGE_TYPE_WHITE,
    FLOATING_MESSAGE_TYPE_GREY,
    FLOATING_MESSAGE_TYPE_DARK_GREY,
    FLOATING_MESSAGE_TYPE_LIGHT_GREY,
    FLOATING_MESSAGE_TYPE_COUNT,
} FloatingMessageType;

typedef enum OpRegAnimFunc {
    OP_REG_ANIM_FUNC_BEGIN = 1,
    OP_REG_ANIM_FUNC_CLEAR = 2,
    OP_REG_ANIM_FUNC_END = 3,
} OpRegAnimFunc;

static void scriptPredefinedError(Program* program, const char* name, int error);
static void scriptError(const char* format, ...);
static int tileIsVisible(int tile);
static int _correctFidForRemovedItem(Object* a1, Object* a2, int a3);
static void opGiveExpPoints(Program* program);
static void opScrReturn(Program* program);
static void opPlaySfx(Program* program);
static void opSetMapStart(Program* program);
static void opOverrideMapStart(Program* program);
static void opHasSkill(Program* program);
static void opUsingSkill(Program* program);
static void opRollVsSkill(Program* program);
static void opSkillContest(Program* program);
static void opDoCheck(Program* program);
static void opSuccess(Program* program);
static void opCritical(Program* program);
static void opHowMuch(Program* program);
static void opMarkAreaKnown(Program* program);
static void opReactionInfluence(Program* program);
static void opRandom(Program* program);
static void opRollDice(Program* program);
static void opMoveTo(Program* program);
static void opCreateObject(Program* program);
static void opDestroyObject(Program* program);
static void opDisplayMsg(Program* program);
static void opScriptOverrides(Program* program);
static void opObjectIsCarryingObjectWithPid(Program* program);
static void opTileContainsObjectWithPid(Program* program);
static void opGetSelf(Program* program);
static void opGetSource(Program* program);
static void opGetTarget(Program* program);
static void opGetDude(Program* program);
static void opGetObjectBeingUsed(Program* program);
static void opGetLocalVar(Program* program);
static void opSetLocalVar(Program* program);
static void opGetMapVar(Program* program);
static void opSetMapVar(Program* program);
static void opGetGlobalVar(Program* program);
static void opSetGlobalVar(Program* program);
static void opGetScriptAction(Program* program);
static void opGetObjectType(Program* program);
static void opGetItemType(Program* program);
static void opGetCritterStat(Program* program);
static void opSetCritterStat(Program* program);
static void opAnimateStand(Program* program);
static void opAnimateStandReverse(Program* program);
static void opAnimateMoveObjectToTile(Program* program);
static void opTileInTileRect(Program* program);
static void opMakeDayTime(Program* program);
static void opTileDistanceBetween(Program* program);
static void opTileDistanceBetweenObjects(Program* program);
static void opGetObjectTile(Program* program);
static void opGetTileInDirection(Program* program);
static void opPickup(Program* program);
static void opDrop(Program* program);
static void opAddObjectToInventory(Program* program);
static void opRemoveObjectFromInventory(Program* program);
static void opWieldItem(Program* program);
static void opUseObject(Program* program);
static void opObjectCanSeeObject(Program* program);
static void opAttackComplex(Program* program);
static void opStartGameDialog(Program* program);
static void opEndGameDialog(Program* program);
static void opGameDialogReaction(Program* program);
static void opMetarule3(Program* program);
static void opSetMapMusic(Program* program);
static void opSetObjectVisibility(Program* program);
static void opLoadMap(Program* program);
static void opWorldmapCitySetPos(Program* program);
static void opSetExitGrids(Program* program);
static void opAnimBusy(Program* program);
static void opCritterHeal(Program* program);
static void opSetLightLevel(Program* program);
static void opGetGameTime(Program* program);
static void opGetGameTimeInSeconds(Program* program);
static void opGetObjectElevation(Program* program);
static void opKillCritter(Program* program);
static int _correctDeath(Object* critter, int anim, bool a3);
static void opKillCritterType(Program* program);
static void opCritterDamage(Program* program);
static void opAddTimerEvent(Program* program);
static void opRemoveTimerEvent(Program* program);
static void opGameTicks(Program* program);
static void opHasTrait(Program* program);
static void opObjectCanHearObject(Program* program);
static void opGameTimeHour(Program* program);
static void opGetFixedParam(Program* program);
static void opTileIsVisible(Program* program);
static void opGameDialogSystemEnter(Program* program);
static void opGetActionBeingUsed(Program* program);
static void opGetCritterState(Program* program);
static void opGameTimeAdvance(Program* program);
static void opRadiationIncrease(Program* program);
static void opRadiationDecrease(Program* program);
static void opCritterAttemptPlacement(Program* program);
static void opGetObjectPid(Program* program);
static void opGetCurrentMap(Program* program);
static void opCritterAddTrait(Program* program);
static void opCritterRemoveTrait(Program* program);
static void opGetProtoData(Program* program);
static void opGetMessageString(Program* program);
static void opCritterGetInventoryObject(Program* program);
static void opSetObjectLightLevel(Program* program);
static void opWorldmap(Program* program);
static void _op_inven_cmds(Program* program);
static void opFloatMessage(Program* program);
static void opMetarule(Program* program);
static void opAnim(Program* program);
static void opObjectCarryingObjectByPid(Program* program);
static void opRegAnimFunc(Program* program);
static void opRegAnimAnimate(Program* program);
static void opRegAnimAnimateReverse(Program* program);
static void opRegAnimObjectMoveToObject(Program* program);
static void opRegAnimObjectRunToObject(Program* program);
static void opRegAnimObjectMoveToTile(Program* program);
static void opRegAnimObjectRunToTile(Program* program);
static void opPlayGameMovie(Program* program);
static void opAddMultipleObjectsToInventory(Program* program);
static void opRemoveMultipleObjectsFromInventory(Program* program);
static void opGetMonth(Program* program);
static void opGetDay(Program* program);
static void opExplosion(Program* program);
static void opGetDaysSinceLastVisit(Program* program);
static void _op_gsay_start(Program* program);
static void _op_gsay_end(Program* program);
static void _op_gsay_reply(Program* program);
static void _op_gsay_option(Program* program);
static void _op_gsay_message(Program* program);
static void _op_giq_option(Program* program);
static void opPoison(Program* program);
static void opGetPoison(Program* program);
static void opPartyAdd(Program* program);
static void opPartyRemove(Program* program);
static void opRegAnimAnimateForever(Program* program);
static void opCritterInjure(Program* program);
static void opCombatIsInitialized(Program* program);
static void _op_gdialog_barter(Program* program);
static void opGetGameDifficulty(Program* program);
static void opGetRunningBurningGuy(Program* program);
static void _op_inven_unwield(Program* program);
static void opObjectIsLocked(Program* program);
static void opObjectLock(Program* program);
static void opObjectUnlock(Program* program);
static void opObjectIsOpen(Program* program);
static void opObjectOpen(Program* program);
static void opObjectClose(Program* program);
static void opGameUiDisable(Program* program);
static void opGameUiEnable(Program* program);
static void opGameUiIsDisabled(Program* program);
static void opGameFadeOut(Program* program);
static void opGameFadeIn(Program* program);
static void opItemCapsTotal(Program* program);
static void opItemCapsAdjust(Program* program);
static void _op_anim_action_frame(Program* program);
static void opRegAnimPlaySfx(Program* program);
static void opCritterModifySkill(Program* program);
static void opSfxBuildCharName(Program* program);
static void opSfxBuildAmbientName(Program* program);
static void opSfxBuildInterfaceName(Program* program);
static void opSfxBuildItemName(Program* program);
static void opSfxBuildWeaponName(Program* program);
static void opSfxBuildSceneryName(Program* program);
static void opSfxBuildOpenName(Program* program);
static void opAttackSetup(Program* program);
static void opDestroyMultipleObjects(Program* program);
static void opUseObjectOnObject(Program* program);
static void opEndgameSlideshow(Program* program);
static void opMoveObjectInventoryToObject(Program* program);
static void opEndgameMovie(Program* program);
static void opGetObjectFid(Program* program);
static void opGetFidAnim(Program* program);
static void opGetPartyMember(Program* program);
static void opGetRotationToTile(Program* program);
static void opJamLock(Program* program);
static void opGameDialogSetBarterMod(Program* program);
static void opGetCombatDifficulty(Program* program);
static void opObjectOnScreen(Program* program);
static void opCritterIsFleeing(Program* program);
static void opCritterSetFleeState(Program* program);
static void opTerminateCombat(Program* program);
static void opDebugMessage(Program* program);
static void opCritterStopAttacking(Program* program);
static void opTileGetObjectWithPid(Program* program);
static void opGetObjectName(Program* program);
static void opGetPcStat(Program* program);

// 0x504B0C
static char _aCritter[] = "<Critter>";

// 0x453FC0
static Rect stru_453FC0 = { 0, 0, 640, 480 };

// 0x518EC0
static const char* _dbg_error_strs[SCRIPT_ERROR_COUNT] = {
    "unimped",
    "obj is NULL",
    "can't match program to sid",
    "follows",
};

// Last message type during op_float_msg sequential.
//
// 0x518F00
static int _last_color = 1;

// 0x518F04
static char* _strName = _aCritter;

// NOTE: This value is a little bit odd. It's used to handle 2 operations:
// [opStartGameDialog] and [opGameDialogReaction]. It's not used outside those
// functions.
//
// When used inside [opStartGameDialog] this value stores [Fidget] constant
// (1 - Good, 4 - Neutral, 7 - Bad).
//
// When used inside [opGameDialogReaction] this value contains specified
// reaction (-1 - Good, 0 - Neutral, 1 - Bad).
//
// 0x5970D0
static int gGameDialogReactionOrFidget;

// 0x453FD0
static void scriptPredefinedError(Program* program, const char* name, int error)
{
    char string[260];

    snprintf(string, sizeof(string), "Script Error: %s: op_%s: %s", program->name, name, _dbg_error_strs[error]);

    debugPrint(string);
}

// 0x45400C
static void scriptError(const char* format, ...)
{
    char string[260];

    va_list argptr;
    va_start(argptr, format);
    vsnprintf(string, sizeof(string), format, argptr);
    va_end(argptr);

    debugPrint(string);
}

// 0x45404C
static int tileIsVisible(int tile)
{
    if (abs(gCenterTile - tile) % 200 < 5) {
        return 1;
    }

    if (abs(gCenterTile - tile) / 200 < 5) {
        return 1;
    }

    return 0;
}

// 0x45409C
static int _correctFidForRemovedItem(Object* a1, Object* a2, int flags)
{
    if (a1 == gDude) {
        bool animated = !gameUiIsDisabled();
        interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
    }

    int fid = a1->fid;
    int v8 = (fid & 0xF000) >> 12;
    int newFid = -1;

    if ((flags & 0x03000000) != 0) {
        if (a1 == gDude) {
            if (interfaceGetCurrentHand()) {
                if ((flags & 0x02000000) != 0) {
                    v8 = 0;
                }
            } else {
                if ((flags & 0x01000000) != 0) {
                    v8 = 0;
                }
            }
        } else {
            if ((flags & 0x02000000) != 0) {
                v8 = 0;
            }
        }

        if (v8 == 0) {
            newFid = buildFid(FID_TYPE(fid), fid & 0xFFF, FID_ANIM_TYPE(fid), 0, (fid & 0x70000000) >> 28);
        }
    } else {
        if (a1 == gDude) {
            newFid = buildFid(FID_TYPE(fid), _art_vault_guy_num, FID_ANIM_TYPE(fid), v8, (fid & 0x70000000) >> 28);
        }

        _adjust_ac(a1, a2, nullptr);
    }

    if (newFid != -1) {
        Rect rect;
        objectSetFid(a1, newFid, &rect);
        tileWindowRefreshRect(&rect, gElevation);
    }

    return 0;
}

// give_exp_points
// 0x4541C8
static void opGiveExpPoints(Program* program)
{
    int xp = programStackPopInteger(program);

    if (pcAddExperience(xp) != 0) {
        scriptError("\nScript Error: %s: op_give_exp_points: stat_pc_set failed");
    }
}

// scr_return
// 0x454238
static void opScrReturn(Program* program)
{
    int data = programStackPopInteger(program);

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        script->field_28 = data;
    }
}

// play_sfx
// 0x4542AC
static void opPlaySfx(Program* program)
{
    char* name = programStackPopString(program);

    soundPlayFile(name);
}

// set_map_start
// 0x454314
static void opSetMapStart(Program* program)
{
    int rotation = programStackPopInteger(program);
    int elevation = programStackPopInteger(program);
    int y = programStackPopInteger(program);
    int x = programStackPopInteger(program);

    if (mapSetElevation(elevation) != 0) {
        scriptError("\nScript Error: %s: op_set_map_start: map_set_elevation failed", program->name);
        return;
    }

    int tile = 200 * y + x;
    if (tileSetCenter(tile, TILE_SET_CENTER_REFRESH_WINDOW | TILE_SET_CENTER_FLAG_IGNORE_SCROLL_RESTRICTIONS) != 0) {
        scriptError("\nScript Error: %s: op_set_map_start: tile_set_center failed", program->name);
        return;
    }

    mapSetStart(tile, elevation, rotation);
}

// override_map_start
// 0x4543F4
static void opOverrideMapStart(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    int rotation = programStackPopInteger(program);
    int elevation = programStackPopInteger(program);
    int y = programStackPopInteger(program);
    int x = programStackPopInteger(program);

    char text[60];
    snprintf(text, sizeof(text), "OVERRIDE_MAP_START: x: %d, y: %d", x, y);
    debugPrint(text);

    int tile = 200 * y + x;
    int previousTile = gCenterTile;
    if (tile != -1) {
        if (objectSetRotation(gDude, rotation, nullptr) != 0) {
            scriptError("\nError: %s: obj_set_rotation failed in override_map_start!", program->name);
        }

        if (objectSetLocation(gDude, tile, elevation, nullptr) != 0) {
            scriptError("\nError: %s: obj_move_to_tile failed in override_map_start!", program->name);

            if (objectSetLocation(gDude, previousTile, elevation, nullptr) != 0) {
                scriptError("\nError: %s: obj_move_to_tile RECOVERY Also failed!");
                exit(1);
            }
        }

        tileSetCenter(tile, TILE_SET_CENTER_REFRESH_WINDOW);
        tileWindowRefresh();
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// has_skill
// 0x454568
static void opHasSkill(Program* program)
{
    int skill = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int result = 0;
    if (object != nullptr) {
        if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
            result = skillGetValue(object, skill);
        }
    } else {
        scriptPredefinedError(program, "has_skill", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, result);
}

// using_skill
// 0x454634
static void opUsingSkill(Program* program)
{
    int skill = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    // NOTE: In the original source code this value is left uninitialized, that
    // explains why garbage is returned when using something else than dude and
    // SKILL_SNEAK as arguments.
    int result = 0;

    if (skill == SKILL_SNEAK && object == gDude) {
        result = dudeHasState(DUDE_STATE_SNEAKING);
    }

    programStackPushInteger(program, result);
}

// roll_vs_skill
// 0x4546E8
static void opRollVsSkill(Program* program)
{
    int modifier = programStackPopInteger(program);
    int skill = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int roll = ROLL_CRITICAL_FAILURE;
    if (object != nullptr) {
        if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
            int sid = scriptGetSid(program);

            Script* script;
            if (scriptGetScript(sid, &script) != -1) {
                roll = skillRoll(object, skill, modifier, &(script->howMuch));
            }
        }
    } else {
        scriptPredefinedError(program, "roll_vs_skill", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, roll);
}

// skill_contest
// 0x4547D4
static void opSkillContest(Program* program)
{
    int data[3];

    for (int arg = 0; arg < 3; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    scriptPredefinedError(program, "skill_contest", SCRIPT_ERROR_NOT_IMPLEMENTED);
    programStackPushInteger(program, 0);
}

// do_check
// 0x454890
static void opDoCheck(Program* program)
{
    int mod = programStackPopInteger(program);
    int stat = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int roll = 0;
    if (object != nullptr) {
        int sid = scriptGetSid(program);

        Script* script;
        if (scriptGetScript(sid, &script) != -1) {
            switch (stat) {
            case STAT_STRENGTH:
            case STAT_PERCEPTION:
            case STAT_ENDURANCE:
            case STAT_CHARISMA:
            case STAT_INTELLIGENCE:
            case STAT_AGILITY:
            case STAT_LUCK:
                roll = statRoll(object, stat, mod, &(script->howMuch));
                break;
            default:
                scriptError("\nScript Error: %s: op_do_check: Stat out of range", program->name);
                break;
            }
        }
    } else {
        scriptPredefinedError(program, "do_check", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, roll);
}

// success
// 0x4549A8
static void opSuccess(Program* program)
{
    int data = programStackPopInteger(program);

    int result = -1;

    switch (data) {
    case ROLL_CRITICAL_FAILURE:
    case ROLL_FAILURE:
        result = 0;
        break;
    case ROLL_SUCCESS:
    case ROLL_CRITICAL_SUCCESS:
        result = 1;
        break;
    }

    programStackPushInteger(program, result);
}

// critical
// 0x454A44
static void opCritical(Program* program)
{
    int data = programStackPopInteger(program);

    int result = -1;

    switch (data) {
    case ROLL_CRITICAL_FAILURE:
    case ROLL_CRITICAL_SUCCESS:
        result = 1;
        break;
    case ROLL_FAILURE:
    case ROLL_SUCCESS:
        result = 0;
        break;
    }

    programStackPushInteger(program, result);
}

// how_much
// 0x454AD0
static void opHowMuch(Program* program)
{
    int data = programStackPopInteger(program);

    int result = 0;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        result = script->howMuch;
    } else {
        scriptPredefinedError(program, "how_much", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushInteger(program, result);
}

// mark_area_known
// 0x454B6C
static void opMarkAreaKnown(Program* program)
{
    int data[3];

    for (int arg = 0; arg < 3; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    // TODO: Provide meaningful names.
    if (data[2] == 0) {
        if (data[0] == CITY_STATE_INVISIBLE) {
            wmAreaSetVisibleState(data[1], 0, 1);
        } else {
            wmAreaSetVisibleState(data[1], 1, 1);
            wmAreaMarkVisitedState(data[1], data[0]);
        }
    } else if (data[2] == 1) {
        wmMapMarkVisited(data[1]);
    }
}

// reaction_influence
// 0x454C34
static void opReactionInfluence(Program* program)
{
    int data[3];

    for (int arg = 0; arg < 3; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    int result = _reaction_influence_();
    programStackPushInteger(program, result);
}

// random
// 0x454CD4
static void opRandom(Program* program)
{
    int data[2];

    for (int arg = 0; arg < 2; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    int result;
    if (vcrGetState() == VCR_STATE_TURNED_OFF) {
        result = randomBetween(data[1], data[0]);
    } else {
        result = (data[0] - data[1]) / 2;
    }

    programStackPushInteger(program, result);
}

// roll_dice
// 0x454D88
static void opRollDice(Program* program)
{
    int data[2];

    for (int arg = 0; arg < 2; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    scriptPredefinedError(program, "roll_dice", SCRIPT_ERROR_NOT_IMPLEMENTED);

    programStackPushInteger(program, 0);
}

// move_to
// 0x454E28
static void opMoveTo(Program* program)
{
    int elevation = programStackPopInteger(program);
    int tile = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int newTile;

    if (object != nullptr) {
        if (object == gDude) {
            bool tileLimitingEnabled = tileScrollLimitingIsEnabled();
            bool tileBlockingEnabled = tileScrollBlockingIsEnabled();

            if (tileLimitingEnabled) {
                tileScrollLimitingDisable();
            }

            if (tileBlockingEnabled) {
                tileScrollBlockingDisable();
            }

            Rect rect;
            newTile = objectSetLocation(object, tile, elevation, &rect);
            if (newTile != -1) {
                tileSetCenter(object->tile, TILE_SET_CENTER_REFRESH_WINDOW);
            }

            if (tileLimitingEnabled) {
                tileScrollLimitingEnable();
            }

            if (tileBlockingEnabled) {
                tileScrollBlockingEnable();
            }
        } else {
            Rect before;
            objectGetRect(object, &before);

            if (object->elevation != elevation && PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
                _combat_delete_critter(object);
            }

            Rect after;
            newTile = objectSetLocation(object, tile, elevation, &after);
            if (newTile != -1) {
                rectUnion(&before, &after, &before);
                tileWindowRefreshRect(&before, gElevation);
            }
        }
    } else {
        scriptPredefinedError(program, "move_to", SCRIPT_ERROR_OBJECT_IS_NULL);
        newTile = -1;
    }

    programStackPushInteger(program, newTile);
}

// create_object_sid
// 0x454FA8
static void opCreateObject(Program* program)
{
    int data[4];

    for (int arg = 0; arg < 4; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    int pid = data[3];
    int tile = data[2];
    int elevation = data[1];
    int sid = data[0];

    Object* object = nullptr;

    if (_isLoadingGame() != 0) {
        debugPrint("\nError: attempt to Create critter in load/save-game: %s!", program->name);
        goto out;
    }

    if (pid == 0) {
        debugPrint("\nError: attempt to Create critter With PID of 0: %s!", program->name);
        goto out;
    }

    Proto* proto;
    if (protoGetProto(pid, &proto) != -1) {
        if (objectCreateWithFidPid(&object, proto->fid, pid) != -1) {
            if (tile == -1) {
                tile = 0;
            }

            Rect rect;
            if (objectSetLocation(object, tile, elevation, &rect) != -1) {
                tileWindowRefreshRect(&rect, object->elevation);
            }
        }
    } else {
        // SFALL: Prevent a crash when the proto is missing.
        goto out;
    }

    if (sid != -1) {
        int scriptType = 0;
        switch (PID_TYPE(object->pid)) {
        case OBJ_TYPE_CRITTER:
            scriptType = SCRIPT_TYPE_CRITTER;
            break;
        case OBJ_TYPE_ITEM:
        case OBJ_TYPE_SCENERY:
            scriptType = SCRIPT_TYPE_ITEM;
            break;
        }

        if (object->sid != -1) {
            scriptRemove(object->sid);
            object->sid = -1;
        }

        if (scriptAdd(&(object->sid), scriptType) == -1) {
            goto out;
        }

        Script* script;
        if (scriptGetScript(object->sid, &script) == -1) {
            goto out;
        }

        script->field_14 = sid - 1;

        if (scriptType == SCRIPT_TYPE_SPATIAL) {
            script->sp.built_tile = builtTileCreate(object->tile, object->elevation);
            script->sp.radius = 3;
        }

        object->id = scriptsNewObjectId();
        script->field_1C = object->id;
        script->owner = object;
        _scr_find_str_run_info(sid - 1, &(script->field_50), object->sid);
    };

out:

    programStackPushPointer(program, object);
}

// destroy_object
// 0x4551E4
static void opDestroyObject(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "destroy_object", SCRIPT_ERROR_OBJECT_IS_NULL);
        program->flags &= ~PROGRAM_FLAG_0x20;
        return;
    }

    if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
        if (_isLoadingGame()) {
            debugPrint("\nError: attempt to destroy critter in load/save-game: %s!", program->name);
            program->flags &= ~PROGRAM_FLAG_0x20;
            return;
        }
    }

    bool isSelf = object == scriptGetSelf(program);

    if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
        _combat_delete_critter(object);
    }

    Object* owner = objectGetOwner(object);
    if (owner != nullptr) {
        int quantity = itemGetQuantity(owner, object);
        itemRemove(owner, object, quantity);

        if (owner == gDude) {
            bool animated = !gameUiIsDisabled();
            interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
        }

        _obj_connect(object, 1, 0, nullptr);

        if (isSelf) {
            object->sid = -1;
            object->flags |= (OBJECT_HIDDEN | OBJECT_NO_SAVE);
        } else {
            reg_anim_clear(object);
            objectDestroy(object, nullptr);
        }
    } else {
        reg_anim_clear(object);

        Rect rect;
        objectDestroy(object, &rect);
        tileWindowRefreshRect(&rect, gElevation);
    }

    program->flags &= ~PROGRAM_FLAG_0x20;

    if (isSelf) {
        program->flags |= PROGRAM_FLAG_0x0100;
    }
}

// display_msg
// 0x455388
static void opDisplayMsg(Program* program)
{
    char* string = programStackPopString(program);
    displayMonitorAddMessage(string);

    if (settings.debug.show_script_messages) {
        debugPrint("\n");
        debugPrint(string);
    }
}

// script_overrides
// 0x455430
static void opScriptOverrides(Program* program)
{
    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        script->scriptOverrides = 1;
    } else {
        scriptPredefinedError(program, "script_overrides", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }
}

// obj_is_carrying_obj_pid
// 0x455470
static void opObjectIsCarryingObjectWithPid(Program* program)
{
    int pid = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    int result = 0;
    if (obj != nullptr) {
        result = objectGetCarriedQuantityByPid(obj, pid);
    } else {
        scriptPredefinedError(program, "obj_is_carrying_obj_pid", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, result);
}

// tile_contains_obj_pid
// 0x455534
static void opTileContainsObjectWithPid(Program* program)
{
    int pid = programStackPopInteger(program);
    int elevation = programStackPopInteger(program);
    int tile = programStackPopInteger(program);

    int result = 0;

    Object* object = objectFindFirstAtLocation(elevation, tile);
    while (object) {
        if (object->pid == pid) {
            result = 1;
            break;
        }
        object = objectFindNextAtLocation();
    }

    programStackPushInteger(program, result);
}

// self_obj
// 0x455600
static void opGetSelf(Program* program)
{
    Object* object = scriptGetSelf(program);
    programStackPushPointer(program, object);
}

// source_obj
// 0x455624
static void opGetSource(Program* program)
{
    Object* object = nullptr;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        object = script->source;
    } else {
        scriptPredefinedError(program, "source_obj", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushPointer(program, object);
}

// target_obj
// 0x455678
static void opGetTarget(Program* program)
{
    Object* object = nullptr;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        object = script->target;
    } else {
        scriptPredefinedError(program, "target_obj", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushPointer(program, object);
}

// dude_obj
// 0x4556CC
static void opGetDude(Program* program)
{
    programStackPushPointer(program, gDude);
}

// NOTE: The implementation is the same as in [opGetTarget].
//
// obj_being_used_with
// 0x4556EC
static void opGetObjectBeingUsed(Program* program)
{
    Object* object = nullptr;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        object = script->target;
    } else {
        scriptPredefinedError(program, "obj_being_used_with", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushPointer(program, object);
}

// local_var
// 0x455740
static void opGetLocalVar(Program* program)
{
    int data = programStackPopInteger(program);

    ProgramValue value;
    value.opcode = VALUE_TYPE_INT;
    value.integerValue = -1;

    int sid = scriptGetSid(program);
    scriptGetLocalVar(sid, data, value);

    programStackPushValue(program, value);
}

// set_local_var
// 0x4557C8
static void opSetLocalVar(Program* program)
{
    ProgramValue value = programStackPopValue(program);
    int variable = programStackPopInteger(program);

    int sid = scriptGetSid(program);
    scriptSetLocalVar(sid, variable, value);
}

// map_var
// 0x455858
static void opGetMapVar(Program* program)
{
    int data = programStackPopInteger(program);

    ProgramValue value;
    if (mapGetGlobalVar(data, value) == -1) {
        value.opcode = VALUE_TYPE_INT;
        value.integerValue = -1;
    }

    programStackPushValue(program, value);
}

// set_map_var
// 0x4558C8
static void opSetMapVar(Program* program)
{
    ProgramValue value = programStackPopValue(program);
    int variable = programStackPopInteger(program);

    mapSetGlobalVar(variable, value);
}

// global_var
// 0x455950
static void opGetGlobalVar(Program* program)
{
    int variable = programStackPopInteger(program);

    if (gGameGlobalVarsLength != 0) {
        void* ptr = gameGetGlobalPointer(variable);
        if (ptr != nullptr) {
            programStackPushPointer(program, ptr);
        } else {
            programStackPushInteger(program, gameGetGlobalVar(variable));
        }
    } else {
        scriptError("\nScript Error: %s: op_global_var: no global vars found!", program->name);
        programStackPushInteger(program, -1);
    }
}

// set_global_var
// 0x4559EC
// 0x80C6
static void opSetGlobalVar(Program* program)
{
    ProgramValue value = programStackPopValue(program);
    int variable = programStackPopInteger(program);

    if (gGameGlobalVarsLength != 0) {
        if (value.opcode == VALUE_TYPE_PTR) {
            gameSetGlobalPointer(variable, value.pointerValue);
            gameSetGlobalVar(variable, 0);
        } else {
            gameSetGlobalPointer(variable, nullptr);
            gameSetGlobalVar(variable, value.integerValue);
        }
    } else {
        scriptError("\nScript Error: %s: op_set_global_var: no global vars found!", program->name);
    }
}

// script_action
// 0x455A90
static void opGetScriptAction(Program* program)
{
    int action = 0;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        action = script->action;
    } else {
        scriptPredefinedError(program, "script_action", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushInteger(program, action);
}

// obj_type
// 0x455AE4
static void opGetObjectType(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int objectType = -1;
    if (object != nullptr) {
        objectType = FID_TYPE(object->fid);
    }

    programStackPushInteger(program, objectType);
}

// obj_item_subtype
// 0x455B6C
static void opGetItemType(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    int itemType = -1;
    if (obj != nullptr) {
        if (PID_TYPE(obj->pid) == OBJ_TYPE_ITEM) {
            Proto* proto;
            if (protoGetProto(obj->pid, &proto) != -1) {
                itemType = itemGetType(obj);
            }
        }
    }

    programStackPushInteger(program, itemType);
}

// get_critter_stat
// 0x455C10
static void opGetCritterStat(Program* program)
{
    int stat = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int value = -1;
    if (object != nullptr) {
        value = critterGetStat(object, stat);
    } else {
        scriptPredefinedError(program, "get_critter_stat", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, value);
}

// NOTE: Despite it's name it does not actually "set" stat, but "adjust". So
// it's last argument is amount of adjustment, not it's final value.
//
// set_critter_stat
// 0x455CCC
static void opSetCritterStat(Program* program)
{
    int value = programStackPopInteger(program);
    int stat = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int result = 0;
    if (object != nullptr) {
        if (object == gDude) {
            int currentValue = critterGetBaseStatWithTraitModifier(object, stat);
            critterSetBaseStat(object, stat, currentValue + value);
        } else {
            scriptPredefinedError(program, "set_critter_stat", SCRIPT_ERROR_FOLLOWS);
            debugPrint(" Can't modify anyone except obj_dude!");
            result = -1;
        }
    } else {
        scriptPredefinedError(program, "set_critter_stat", SCRIPT_ERROR_OBJECT_IS_NULL);
        result = -1;
    }

    programStackPushInteger(program, result);
}

// animate_stand_obj
// 0x455DC8
static void opAnimateStand(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));
    if (object == nullptr) {
        int sid = scriptGetSid(program);

        Script* script;
        if (scriptGetScript(sid, &script) == -1) {
            scriptPredefinedError(program, "animate_stand_obj", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
            return;
        }

        object = scriptGetSelf(program);
    }

    if (!isInCombat()) {
        reg_anim_begin(ANIMATION_REQUEST_UNRESERVED);
        animationRegisterAnimate(object, ANIM_STAND, 0);
        reg_anim_end();
    }
}

// animate_stand_reverse_obj
// 0x455E7C
static void opAnimateStandReverse(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));
    if (object == nullptr) {
        int sid = scriptGetSid(program);

        Script* script;
        if (scriptGetScript(sid, &script) == -1) {
            scriptPredefinedError(program, "animate_stand_reverse_obj", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
            return;
        }

        object = scriptGetSelf(program);
    }

    if (!isInCombat()) {
        reg_anim_begin(ANIMATION_REQUEST_UNRESERVED);
        animationRegisterAnimateReversed(object, ANIM_STAND, 0);
        reg_anim_end();
    }
}

// animate_move_obj_to_tile
// 0x455F30
static void opAnimateMoveObjectToTile(Program* program)
{
    int flags = programStackPopInteger(program);
    int tile = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "animate_move_obj_to_tile", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (tile <= -1) {
        return;
    }

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) == -1) {
        scriptPredefinedError(program, "animate_move_obj_to_tile", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
        return;
    }

    if (!critterIsActive(object)) {
        return;
    }

    if (isInCombat()) {
        return;
    }

    if ((flags & 0x10) != 0) {
        reg_anim_clear(object);
        flags &= ~0x10;
    }

    reg_anim_begin(ANIMATION_REQUEST_UNRESERVED);

    if (flags == 0) {
        animationRegisterMoveToTile(object, tile, object->elevation, -1, 0);
    } else {
        animationRegisterRunToTile(object, tile, object->elevation, -1, 0);
    }

    reg_anim_end();
}

// tile_in_tile_rect
// 0x45607C
static void opTileInTileRect(Program* program)
{
    Point points[5];

    for (int arg = 0; arg < 5; arg++) {
        int value = programStackPopInteger(program);

        points[arg].x = value % 200;
        points[arg].y = value / 200;
    }

    int x = points[0].x;
    int y = points[0].y;

    int minX = points[1].x;
    int maxX = points[4].x;

    int minY = points[4].y;
    int maxY = points[1].y;

    int result = 0;
    if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
        result = 1;
    }

    programStackPushInteger(program, result);
}

// make_daytime
// 0x456170
static void opMakeDayTime(Program* program)
{
}

// tile_distance
// 0x456174
static void opTileDistanceBetween(Program* program)
{
    int tile2 = programStackPopInteger(program);
    int tile1 = programStackPopInteger(program);

    int distance;

    if (tile1 != -1 && tile2 != -1) {
        distance = tileDistanceBetween(tile1, tile2);
    } else {
        distance = 9999;
    }

    programStackPushInteger(program, distance);
}

// tile_distance_objs
// 0x456228
static void opTileDistanceBetweenObjects(Program* program)
{
    Object* object2 = static_cast<Object*>(programStackPopPointer(program));
    Object* object1 = static_cast<Object*>(programStackPopPointer(program));

    int distance = 9999;
    if (object1 != nullptr && object2 != nullptr) {
        if ((uintptr_t)object2 >= HEX_GRID_SIZE && (uintptr_t)object1 >= HEX_GRID_SIZE) {
            if (object1->elevation == object2->elevation) {
                if (object1->tile != -1 && object2->tile != -1) {
                    distance = tileDistanceBetween(object1->tile, object2->tile);
                }
            }
        } else {
            scriptPredefinedError(program, "tile_distance_objs", SCRIPT_ERROR_FOLLOWS);
            debugPrint(" Passed a tile # instead of an object!!!BADBADBAD!");
        }
    }

    programStackPushInteger(program, distance);
}

// tile_num
// 0x456324
static void opGetObjectTile(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    int tile = -1;
    if (obj != nullptr) {
        tile = obj->tile;
    } else {
        scriptPredefinedError(program, "tile_num", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, tile);
}

// tile_num_in_direction
// 0x4563B4
static void opGetTileInDirection(Program* program)
{
    int distance = programStackPopInteger(program);
    int rotation = programStackPopInteger(program);
    int origin = programStackPopInteger(program);

    int tile = -1;

    if (origin != -1) {
        if (rotation < ROTATION_COUNT) {
            if (distance != 0) {
                tile = tileGetTileInDirection(origin, rotation, distance);
                if (tile < -1) {
                    debugPrint("\nError: %s: op_tile_num_in_direction got #: %d", program->name, tile);
                    tile = -1;
                }
            }
        } else {
            scriptPredefinedError(program, "tile_num_in_direction", SCRIPT_ERROR_FOLLOWS);
            debugPrint(" rotation out of Range!");
        }
    } else {
        scriptPredefinedError(program, "tile_num_in_direction", SCRIPT_ERROR_FOLLOWS);
        debugPrint(" tileNum is -1!");
    }

    programStackPushInteger(program, tile);
}

// pickup_obj
// 0x4564D4
static void opPickup(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        return;
    }

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) == 1) {
        scriptPredefinedError(program, "pickup_obj", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
        return;
    }

    Object* self = script->target;

    // SFALL: Override `self` via `op_set_self`.
    // CE: Implementation is different. Sfall integrates via `scriptGetSid` by
    // returning fake script with overridden `self` (and `target` in this case).
    if (script->overriddenSelf != nullptr) {
        self = script->overriddenSelf;
        script->overriddenSelf = nullptr;
    }

    if (self == nullptr) {
        scriptPredefinedError(program, "pickup_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    actionPickUp(self, object);
}

// drop_obj
// 0x456580
static void opDrop(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        return;
    }

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) == -1) {
        // FIXME: Should be SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID.
        scriptPredefinedError(program, "drop_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (script->target == nullptr) {
        // FIXME: Should be SCRIPT_ERROR_OBJECT_IS_NULL.
        scriptPredefinedError(program, "drop_obj", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
        return;
    }

    _obj_drop(script->target, object);
}

// add_obj_to_inven
// 0x45662C
static void opAddObjectToInventory(Program* program)
{
    Object* item = static_cast<Object*>(programStackPopPointer(program));
    Object* owner = static_cast<Object*>(programStackPopPointer(program));

    if (owner == nullptr || item == nullptr) {
        return;
    }

    if (item->owner == nullptr) {
        if (itemAdd(owner, item, 1) == 0) {
            Rect rect;
            _obj_disconnect(item, &rect);
            tileWindowRefreshRect(&rect, item->elevation);
        }
    } else {
        scriptPredefinedError(program, "add_obj_to_inven", SCRIPT_ERROR_FOLLOWS);
        debugPrint(" Item was already attached to something else!");
    }
}

// rm_obj_from_inven
// 0x456708
static void opRemoveObjectFromInventory(Program* program)
{
    Object* item = static_cast<Object*>(programStackPopPointer(program));
    Object* owner = static_cast<Object*>(programStackPopPointer(program));

    if (owner == nullptr || item == nullptr) {
        return;
    }

    bool updateFlags = false;
    int flags = 0;

    if ((item->flags & OBJECT_EQUIPPED) != 0) {
        if ((item->flags & OBJECT_IN_LEFT_HAND) != 0) {
            flags |= OBJECT_IN_LEFT_HAND;
        }

        if ((item->flags & OBJECT_IN_RIGHT_HAND) != 0) {
            flags |= OBJECT_IN_RIGHT_HAND;
        }

        if ((item->flags & OBJECT_WORN) != 0) {
            flags |= OBJECT_WORN;
        }

        updateFlags = true;
    }

    if (itemRemove(owner, item, 1) == 0) {
        Rect rect;
        _obj_connect(item, 1, 0, &rect);
        tileWindowRefreshRect(&rect, item->elevation);

        if (updateFlags) {
            _correctFidForRemovedItem(owner, item, flags);
        }
    }
}

// wield_obj_critter
// 0x45681C
static void opWieldItem(Program* program)
{
    Object* item = static_cast<Object*>(programStackPopPointer(program));
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    if (critter == nullptr) {
        scriptPredefinedError(program, "wield_obj_critter", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (item == nullptr) {
        scriptPredefinedError(program, "wield_obj_critter", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (PID_TYPE(critter->pid) != OBJ_TYPE_CRITTER) {
        scriptPredefinedError(program, "wield_obj_critter", SCRIPT_ERROR_FOLLOWS);
        debugPrint(" Only works for critters!  ERROR ERROR ERROR!");
        return;
    }

    int hand = HAND_RIGHT;

    bool shouldAdjustArmorClass = false;
    Object* oldArmor = nullptr;
    Object* newArmor = nullptr;
    if (critter == gDude) {
        if (interfaceGetCurrentHand() == HAND_LEFT) {
            hand = HAND_LEFT;
        }

        if (itemGetType(item) == ITEM_TYPE_ARMOR) {
            oldArmor = critterGetArmor(gDude);

            // SFALL
            shouldAdjustArmorClass = true;
            newArmor = item;
        }
    }

    if (_inven_wield(critter, item, hand) == -1) {
        scriptPredefinedError(program, "wield_obj_critter", SCRIPT_ERROR_FOLLOWS);
        debugPrint(" inven_wield failed!  ERROR ERROR ERROR!");
        return;
    }

    if (critter == gDude) {
        if (shouldAdjustArmorClass) {
            _adjust_ac(critter, oldArmor, newArmor);

            // SFALL
            interfaceRenderArmorClass(false);
        }

        bool animated = !gameUiIsDisabled();
        interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
    }
}

// use_obj
// 0x4569D0
static void opUseObject(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "use_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) == -1) {
        // FIXME: Should be SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID.
        scriptPredefinedError(program, "use_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (script->target == nullptr) {
        scriptPredefinedError(program, "use_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    Object* self = scriptGetSelf(program);
    if (PID_TYPE(self->pid) == OBJ_TYPE_CRITTER) {
        _action_use_an_object(script->target, object);
    } else {
        _obj_use(self, object);
    }
}

// obj_can_see_obj
// 0x456AC4
static void opObjectCanSeeObject(Program* program)
{
    Object* object2 = static_cast<Object*>(programStackPopPointer(program));
    Object* object1 = static_cast<Object*>(programStackPopPointer(program));

    bool canSee = false;

    if (object2 != nullptr && object1 != nullptr) {
        // SFALL: Check objects are on the same elevation.
        // CE: These checks are on par with |opObjectCanHearObject|.
        if (object2->elevation == object1->elevation) {
            if (object2->tile != -1 && object1->tile != -1) {
                if (isWithinPerception(object1, object2)) {
                    Object* obstacle = nullptr;
                    _make_straight_path(object1, object1->tile, object2->tile, nullptr, &obstacle, 16);
                    if (obstacle == object2) {
                        canSee = true;
                    }
                }
            }
        }
    } else {
        scriptPredefinedError(program, "obj_can_see_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, canSee);
}

// attack_complex
// 0x456C00
static void opAttackComplex(Program* program)
{
    int data[8];

    for (int arg = 0; arg < 7; arg++) {
        data[arg] = programStackPopInteger(program);
    }

    Object* target = static_cast<Object*>(programStackPopPointer(program));
    if (target == nullptr) {
        scriptPredefinedError(program, "attack", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    program->flags |= PROGRAM_FLAG_0x20;

    Object* self = scriptGetSelf(program);
    if (self == nullptr) {
        program->flags &= ~PROGRAM_FLAG_0x20;
        return;
    }

    if (!critterIsActive(self) || (self->flags & OBJECT_HIDDEN) != 0) {
        debugPrint("\n   But is already Inactive (Dead/Stunned/Invisible)");
        program->flags &= ~PROGRAM_FLAG_0x20;
        return;
    }

    if (!critterIsActive(target) || (target->flags & OBJECT_HIDDEN) != 0) {
        debugPrint("\n   But target is already dead or invisible");
        program->flags &= ~PROGRAM_FLAG_0x20;
        return;
    }

    if ((target->data.critter.combat.maneuver & CRITTER_MANUEVER_FLEEING) != 0) {
        debugPrint("\n   But target is AFRAID");
        program->flags &= ~PROGRAM_FLAG_0x20;
        return;
    }

    if (_gdialogActive()) {
        // TODO: Might be an error, program flag is not removed.
        return;
    }

    if (isInCombat()) {
        CritterCombatData* combatData = &(self->data.critter.combat);
        if ((combatData->maneuver & CRITTER_MANEUVER_ENGAGING) == 0) {
            combatData->maneuver |= CRITTER_MANEUVER_ENGAGING;
            combatData->whoHitMe = target;
        }
    } else {
        CombatStartData combat;
        combat.attacker = self;
        combat.defender = target;
        combat.actionPointsBonus = 0;
        combat.accuracyBonus = data[4];
        combat.damageBonus = 0;
        combat.minDamage = data[3];
        combat.maxDamage = data[2];

        // TODO: Something is probably broken here, why it wants
        // flags to be the same? Maybe because both of them
        // are applied to defender because of the bug in 0x422F3C?
        if (data[1] == data[0]) {
            combat.overrideAttackResults = 1;
            combat.targetResults = data[0];
            combat.attackerResults = data[1];
        } else {
            combat.overrideAttackResults = 0;
        }

        scriptsRequestCombat(&combat);
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// start_gdialog
// 0x456DF0
static void opStartGameDialog(Program* program)
{
    int backgroundId = programStackPopInteger(program);
    int headId = programStackPopInteger(program);
    int reactionLevel = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));
    programStackPopInteger(program);

    if (isInCombat()) {
        return;
    }

    if (obj == nullptr) {
        scriptPredefinedError(program, "start_gdialog", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    gGameDialogHeadFid = -1;
    if (PID_TYPE(obj->pid) == OBJ_TYPE_CRITTER) {
        Proto* proto;
        if (protoGetProto(obj->pid, &proto) == -1) {
            return;
        }
    }

    if (headId != -1) {
        gGameDialogHeadFid = buildFid(OBJ_TYPE_HEAD, headId, 0, 0, 0);
    }

    gameDialogSetBackground(backgroundId);
    gGameDialogReactionOrFidget = reactionLevel;

    if (gGameDialogHeadFid != -1) {
        int npcReactionValue = reactionGetValue(gGameDialogSpeaker);
        int npcReactionType = reactionTranslateValue(npcReactionValue);
        switch (npcReactionType) {
        case NPC_REACTION_BAD:
            gGameDialogReactionOrFidget = FIDGET_BAD;
            break;
        case NPC_REACTION_NEUTRAL:
            gGameDialogReactionOrFidget = FIDGET_NEUTRAL;
            break;
        case NPC_REACTION_GOOD:
            gGameDialogReactionOrFidget = FIDGET_GOOD;
            break;
        }
    }

    gGameDialogSid = scriptGetSid(program);
    gGameDialogSpeaker = scriptGetSelf(program);
    _gdialogInitFromScript(gGameDialogHeadFid, gGameDialogReactionOrFidget);
}

// end_dialogue
// 0x456F80
static void opEndGameDialog(Program* program)
{
    if (_gdialogExitFromScript() != -1) {
        gGameDialogSpeaker = nullptr;
        gGameDialogSid = -1;
    }
}

// dialogue_reaction
// 0x456FA4
static void opGameDialogReaction(Program* program)
{
    int value = programStackPopInteger(program);

    gGameDialogReactionOrFidget = value;
    _talk_to_critter_reacts(value);
}

// metarule3
// 0x457110
static void opMetarule3(Program* program)
{
    ProgramValue param3 = programStackPopValue(program);
    ProgramValue param2 = programStackPopValue(program);
    ProgramValue param1 = programStackPopValue(program);
    int rule = programStackPopInteger(program);

    ProgramValue result;
    result.opcode = VALUE_TYPE_INT;
    result.integerValue = 0;

    switch (rule) {
    case METARULE3_CLR_FIXED_TIMED_EVENTS:
        if (1) {
            _scrSetQueueTestVals(static_cast<Object*>(param1.pointerValue), param2.integerValue);
            _queue_clear_type(EVENT_TYPE_SCRIPT, _scrQueueRemoveFixed);
        }
        break;
    case METARULE3_MARK_SUBTILE:
        result.integerValue = wmSubTileMarkRadiusVisited(param1.integerValue, param2.integerValue, param3.integerValue);
        break;
    case METARULE3_GET_KILL_COUNT:
        result.integerValue = killsGetByType(param1.integerValue);
        break;
    case METARULE3_MARK_MAP_ENTRANCE:
        result.integerValue = wmMapMarkMapEntranceState(param1.integerValue, param2.integerValue, param3.integerValue);
        break;
    case METARULE3_WM_SUBTILE_STATE:
        if (1) {
            int state;
            if (wmSubTileGetVisitedState(param1.integerValue, param2.integerValue, &state) == 0) {
                result.integerValue = state;
            }
        }
        break;
    case METARULE3_TILE_GET_NEXT_CRITTER:
        if (1) {
            int tile = param1.integerValue;
            int elevation = param2.integerValue;
            Object* previousCritter = static_cast<Object*>(param3.pointerValue);

            bool critterFound = previousCritter == nullptr;

            Object* object = objectFindFirstAtLocation(elevation, tile);
            while (object != nullptr) {
                if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
                    if (critterFound) {
                        result.opcode = VALUE_TYPE_PTR;
                        result.pointerValue = object;
                        break;
                    }
                }

                if (object == previousCritter) {
                    critterFound = true;
                }

                object = objectFindNextAtLocation();
            }
        }
        break;
    case METARULE3_ART_SET_BASE_FID_NUM:
        if (1) {
            Object* obj = static_cast<Object*>(param1.pointerValue);
            int frmId = param2.integerValue;

            int fid = buildFid(FID_TYPE(obj->fid),
                frmId,
                FID_ANIM_TYPE(obj->fid),
                (obj->fid & 0xF000) >> 12,
                (obj->fid & 0x70000000) >> 28);

            Rect updatedRect;
            objectSetFid(obj, fid, &updatedRect);
            tileWindowRefreshRect(&updatedRect, gElevation);
        }
        break;
    case METARULE3_TILE_SET_CENTER:
        result.integerValue = tileSetCenter(param1.integerValue, TILE_SET_CENTER_REFRESH_WINDOW);
        break;
    case METARULE3_109:
        result.integerValue = aiGetChemUse(static_cast<Object*>(param1.pointerValue));
        break;
    case METARULE3_110:
        result.integerValue = wmCarIsOutOfGas() ? 1 : 0;
        break;
    case METARULE3_111:
        result.integerValue = _map_target_load_area();
        break;
    }

    programStackPushValue(program, result);
}

// set_map_music
// 0x45734C
static void opSetMapMusic(Program* program)
{
    char* string = programStackPopString(program);
    int mapIndex = programStackPopInteger(program);

    debugPrint("\nset_map_music: %d, %s", mapIndex, string);
    wmSetMapMusic(mapIndex, string);
}

// NOTE: Function name is a bit misleading. Last parameter is a boolean value
// where 1 or true makes object invisible, and value 0 (false) makes it visible
// again. So a better name for this function is opSetObjectInvisible.
//
//
// set_obj_visibility
// 0x45741C
static void opSetObjectVisibility(Program* program)
{
    int invisible = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    if (obj == nullptr) {
        scriptPredefinedError(program, "set_obj_visibility", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (_isLoadingGame()) {
        debugPrint("Error: attempt to set_obj_visibility in load/save-game: %s!", program->name);
        return;
    }

    if (invisible != 0) {
        if ((obj->flags & OBJECT_HIDDEN) == 0) {
            if (isInCombat()) {
                objectDisableOutline(obj, nullptr);
                objectClearOutline(obj, nullptr);
            }

            Rect rect;
            if (objectHide(obj, &rect) != -1) {
                if (PID_TYPE(obj->pid) == OBJ_TYPE_CRITTER) {
                    obj->flags |= OBJECT_NO_BLOCK;
                }

                tileWindowRefreshRect(&rect, obj->elevation);
            }
        }
    } else {
        if ((obj->flags & OBJECT_HIDDEN) != 0) {
            if (PID_TYPE(obj->pid) == OBJ_TYPE_CRITTER) {
                obj->flags &= ~OBJECT_NO_BLOCK;
            }

            Rect rect;
            if (objectShow(obj, &rect) != -1) {
                tileWindowRefreshRect(&rect, obj->elevation);
            }
        }
    }
}

// load_map
// 0x45755C
static void opLoadMap(Program* program)
{
    int param = programStackPopInteger(program);
    ProgramValue mapIndexOrName = programStackPopValue(program);

    char* mapName = nullptr;

    if ((mapIndexOrName.opcode & VALUE_TYPE_MASK) != VALUE_TYPE_INT) {
        if ((mapIndexOrName.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
            mapName = programGetString(program, mapIndexOrName.opcode, mapIndexOrName.integerValue);
        } else {
            programFatalError("script error: %s: invalid arg 1 to load_map", program->name);
        }
    }

    int mapIndex = -1;

    if (mapName != nullptr) {
        gGameGlobalVars[GVAR_LOAD_MAP_INDEX] = param;
        mapIndex = wmMapMatchNameToIdx(mapName);
    } else {
        if (mapIndexOrName.integerValue >= 0) {
            gGameGlobalVars[GVAR_LOAD_MAP_INDEX] = param;
            mapIndex = mapIndexOrName.integerValue;
        }
    }

    if (mapIndex != -1) {
        MapTransition transition;
        transition.map = mapIndex;
        transition.elevation = -1;
        transition.tile = -1;
        transition.rotation = -1;
        mapSetTransition(&transition);
    }
}

// wm_area_set_pos
// 0x457680
static void opWorldmapCitySetPos(Program* program)
{
    int y = programStackPopInteger(program);
    int x = programStackPopInteger(program);
    int city = programStackPopInteger(program);

    if (wmAreaSetWorldPos(city, x, y) == -1) {
        scriptPredefinedError(program, "wm_area_set_pos", SCRIPT_ERROR_FOLLOWS);
        debugPrint("Invalid Parameter!");
    }
}

// set_exit_grids
// 0x457730
static void opSetExitGrids(Program* program)
{
    int destinationRotation = programStackPopInteger(program);
    int destinationTile = programStackPopInteger(program);
    int destinationElevation = programStackPopInteger(program);
    int destinationMap = programStackPopInteger(program);
    int elevation = programStackPopInteger(program);

    Object* object = objectFindFirstAtElevation(elevation);
    while (object != nullptr) {
        if (object->pid >= FIRST_EXIT_GRID_PID && object->pid <= LAST_EXIT_GRID_PID) {
            object->data.misc.map = destinationMap;
            object->data.misc.tile = destinationTile;
            object->data.misc.elevation = destinationElevation;
        }
        object = objectFindNextAtElevation();
    }
}

// anim_busy
// 0x4577EC
static void opAnimBusy(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int rc = 0;
    if (object != nullptr) {
        rc = animationIsBusy(object);
    } else {
        scriptPredefinedError(program, "anim_busy", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, rc);
}

// critter_heal
// 0x457880
static void opCritterHeal(Program* program)
{
    int amount = programStackPopInteger(program);
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    int rc = critterAdjustHitPoints(critter, amount);

    if (critter == gDude) {
        interfaceRenderHitPoints(true);
    }

    programStackPushInteger(program, rc);
}

// set_light_level
// 0x457934
static void opSetLightLevel(Program* program)
{
    // Maps light level to light intensity.
    //
    // Middle value is mapped one-to-one which corresponds to 50% light level
    // (cavern lighting). Light levels above (51-100%) and below (0-49%) is
    // calculated as percentage from two adjacent light values.
    //
    // 0x453F90
    static const int intensities[3] = {
        LIGHT_INTENSITY_MIN,
        (LIGHT_INTENSITY_MIN + LIGHT_INTENSITY_MAX) / 2,
        LIGHT_INTENSITY_MAX,
    };

    int data = programStackPopInteger(program);

    int lightLevel = data;

    if (data == 50) {
        lightSetAmbientIntensity(intensities[1], true);
        return;
    }

    int lightIntensity;
    if (data > 50) {
        lightIntensity = intensities[1] + data * (intensities[2] - intensities[1]) / 100;
    } else {
        lightIntensity = intensities[0] + data * (intensities[1] - intensities[0]) / 100;
    }

    lightSetAmbientIntensity(lightIntensity, true);
}

// game_time
// 0x4579F4
static void opGetGameTime(Program* program)
{
    int time = gameTimeGetTime();
    programStackPushInteger(program, time);
}

// game_time_in_seconds
// 0x457A18
static void opGetGameTimeInSeconds(Program* program)
{
    int time = gameTimeGetTime();
    programStackPushInteger(program, time / 10);
}

// elevation
// 0x457A44
static void opGetObjectElevation(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int elevation = 0;
    if (object != nullptr) {
        elevation = object->elevation;
    } else {
        scriptPredefinedError(program, "elevation", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, elevation);
}

// kill_critter
// 0x457AD4
static void opKillCritter(Program* program)
{
    int deathFrame = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "kill_critter", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (_isLoadingGame()) {
        debugPrint("\nError: attempt to destroy critter in load/save-game: %s!", program->name);
    }

    program->flags |= PROGRAM_FLAG_0x20;

    Object* self = scriptGetSelf(program);
    bool isSelf = self == object;

    reg_anim_clear(object);
    _combat_delete_critter(object);
    critterKill(object, deathFrame, 1);

    program->flags &= ~PROGRAM_FLAG_0x20;

    if (isSelf) {
        program->flags |= PROGRAM_FLAG_0x0100;
    }
}

// [forceBack] is to force fall back animation, otherwise it's fall front if it's present
static int _correctDeath(Object* critter, int anim, bool forceBack)
{
    if (anim >= ANIM_BIG_HOLE_SF && anim <= ANIM_FALL_FRONT_BLOOD_SF) {
        bool useStandardDeath = false;
        if (settings.preferences.violence_level < VIOLENCE_LEVEL_MAXIMUM_BLOOD) {
            useStandardDeath = true;
        } else {
            int fid = buildFid(OBJ_TYPE_CRITTER, critter->fid & 0xFFF, anim, (critter->fid & 0xF000) >> 12, critter->rotation + 1);
            if (!artExists(fid)) {
                useStandardDeath = true;
            }
        }

        if (useStandardDeath) {
            if (forceBack) {
                anim = ANIM_FALL_BACK;
            } else {
                int fid = buildFid(OBJ_TYPE_CRITTER, critter->fid & 0xFFF, ANIM_FALL_FRONT, (critter->fid & 0xF000) >> 12, critter->rotation + 1);
                if (artExists(fid)) {
                    anim = ANIM_FALL_FRONT;
                } else {
                    anim = ANIM_FALL_BACK;
                }
            }
        }
    }

    return anim;
}

// kill_critter_type
// 0x457CB4
static void opKillCritterType(Program* program)
{
    // 0x518ED0
    static const int ftList[] = {
        ANIM_FALL_BACK_BLOOD_SF,
        ANIM_BIG_HOLE_SF,
        ANIM_CHARRED_BODY_SF,
        ANIM_CHUNKS_OF_FLESH_SF,
        ANIM_FALL_FRONT_BLOOD_SF,
        ANIM_FALL_BACK_BLOOD_SF,
        ANIM_DANCING_AUTOFIRE_SF,
        ANIM_SLICED_IN_HALF_SF,
        ANIM_EXPLODED_TO_NOTHING_SF,
        ANIM_FALL_BACK_BLOOD_SF,
        ANIM_FALL_FRONT_BLOOD_SF,
    };

    int deathFrame = programStackPopInteger(program);
    int pid = programStackPopInteger(program);

    if (_isLoadingGame()) {
        debugPrint("\nError: attempt to destroy critter in load/save-game: %s!", program->name);
        return;
    }

    program->flags |= PROGRAM_FLAG_0x20;

    Object* previousObj = nullptr;
    int count = 0;
    int ftIndex = 0;

    Object* obj = objectFindFirst();
    while (obj != nullptr) {
        if (FID_ANIM_TYPE(obj->fid) < ANIM_FALL_BACK_SF) {
            if ((obj->flags & OBJECT_HIDDEN) == 0 && obj->pid == pid && !critterIsDead(obj)) {
                if (obj == previousObj || count > 200) {
                    scriptPredefinedError(program, "kill_critter_type", SCRIPT_ERROR_FOLLOWS);
                    debugPrint(" Infinite loop destroying critters!");
                    program->flags &= ~PROGRAM_FLAG_0x20;
                    return;
                }

                reg_anim_clear(obj);

                if (deathFrame != 0) {
                    _combat_delete_critter(obj);
                    if (deathFrame == 1) {
                        // Pick next animation from the |ftList|.
                        int anim = _correctDeath(obj, ftList[ftIndex], true);

                        // SFALL: Fix for incorrect death animation.
                        // CE: The fix is slightly different. Sfall passes
                        // |false| to |correctDeath| to disambiguate usage
                        // between this function and |opAnim|. On |false| it
                        // simply returns |ANIM_FALL_BACK_SF|. Instead of doing
                        // the same, check for standard death animations
                        // returned from |correctDeath| and convert them to
                        // appropariate single frame cousins.
                        switch (anim) {
                        case ANIM_FALL_BACK:
                            anim = ANIM_FALL_BACK_SF;
                            break;
                        case ANIM_FALL_FRONT:
                            anim = ANIM_FALL_FRONT_SF;
                            break;
                        }

                        critterKill(obj, anim, true);

                        ftIndex += 1;
                        if (ftIndex >= (sizeof(ftList) / sizeof(ftList[0]))) {
                            ftIndex = 0;
                        }
                    } else if (deathFrame >= FIRST_SF_DEATH_ANIM && deathFrame <= LAST_SF_DEATH_ANIM) {
                        // CE: In some cases user-space scripts randomize death
                        // frame between front/back animations but technically
                        // speaking a scripter can provide any single frame
                        // death animation which original code simply ignores.
                        critterKill(obj, deathFrame, true);
                    } else {
                        critterKill(obj, ANIM_FALL_BACK_SF, true);
                    }
                } else {
                    reg_anim_clear(obj);

                    Rect rect;
                    objectDestroy(obj, &rect);
                    tileWindowRefreshRect(&rect, gElevation);
                }

                previousObj = obj;
                count += 1;

                objectFindFirst();

                gMapHeader.lastVisitTime = gameTimeGetTime();
            }
        }

        obj = objectFindNext();
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// critter_dmg
// 0x457EB4
static void opCritterDamage(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    int damageTypeWithFlags = programStackPopInteger(program);
    int amount = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "critter_damage", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (PID_TYPE(object->pid) != OBJ_TYPE_CRITTER) {
        scriptPredefinedError(program, "critter_damage", SCRIPT_ERROR_FOLLOWS);
        debugPrint(" Can't call on non-critters!");
        return;
    }

    Object* self = scriptGetSelf(program);
    if (object->data.critter.combat.whoHitMeCid == -1) {
        object->data.critter.combat.whoHitMe = nullptr;
    }

    bool animate = (damageTypeWithFlags & 0x200) == 0;
    bool bypassArmor = (damageTypeWithFlags & 0x100) != 0;
    int damageType = damageTypeWithFlags & ~(0x100 | 0x200);
    actionDamage(object->tile, object->elevation, amount, amount, damageType, animate, bypassArmor);

    program->flags &= ~PROGRAM_FLAG_0x20;

    if (self == object) {
        program->flags |= PROGRAM_FLAG_0x0100;
    }
}

// add_timer_event
// 0x457FF0
static void opAddTimerEvent(Program* program)
{
    int param = programStackPopInteger(program);
    int delay = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptError("\nScript Error: %s: op_add_timer_event: pobj is NULL!", program->name);
        return;
    }

    scriptAddTimerEvent(object->sid, delay, param);
}

// rm_timer_event
// 0x458094
static void opRemoveTimerEvent(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        // FIXME: Should be op_rm_timer_event.
        scriptError("\nScript Error: %s: op_add_timer_event: pobj is NULL!");
        return;
    }

    queueRemoveEvents(object);
}

// Converts seconds into game ticks.
//
// game_ticks
// 0x458108
static void opGameTicks(Program* program)
{
    int ticks = programStackPopInteger(program);

    if (ticks < 0) {
        ticks = 0;
    }

    programStackPushInteger(program, ticks * 10);
}

// NOTE: The name of this function is misleading. It has (almost) nothing to do
// with player's "Traits" as a feature. Instead it's used to query many
// information of the critters using passed parameters. It's like "metarule" but
// for critters.
//
// 0x458180
// has_trait
static void opHasTrait(Program* program)
{
    int param = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));
    int type = programStackPopInteger(program);

    int result = 0;

    if (object != nullptr) {
        switch (type) {
        case CRITTER_TRAIT_PERK:
            if (param < PERK_COUNT) {
                result = perkGetRank(object, param);
            } else {
                scriptError("\nScript Error: %s: op_has_trait: Perk out of range", program->name);
            }
            break;
        case CRITTER_TRAIT_OBJECT:
            switch (param) {
            case CRITTER_TRAIT_OBJECT_AI_PACKET:
                if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
                    result = object->data.critter.combat.aiPacket;
                }
                break;
            case CRITTER_TRAIT_OBJECT_TEAM:
                if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
                    result = object->data.critter.combat.team;
                }
                break;
            case CRITTER_TRAIT_OBJECT_ROTATION:
                result = object->rotation;
                break;
            case CRITTER_TRAIT_OBJECT_IS_INVISIBLE:
                result = (object->flags & OBJECT_HIDDEN) == 0;
                break;
            case CRITTER_TRAIT_OBJECT_GET_INVENTORY_WEIGHT:
                result = objectGetInventoryWeight(object);
                break;
            }
            break;
        case CRITTER_TRAIT_TRAIT:
            if (param < TRAIT_COUNT) {
                result = traitIsSelected(param);
            } else {
                scriptError("\nScript Error: %s: op_has_trait: Trait out of range", program->name);
            }
            break;
        default:
            scriptError("\nScript Error: %s: op_has_trait: Trait out of range", program->name);
            break;
        }
    } else {
        scriptPredefinedError(program, "has_trait", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, result);
}

// obj_can_hear_obj
// 0x45835C
static void opObjectCanHearObject(Program* program)
{
    Object* object2 = static_cast<Object*>(programStackPopPointer(program));
    Object* object1 = static_cast<Object*>(programStackPopPointer(program));

    bool canHear = false;

    // SFALL: Fix broken implementation.
    // CE: In Sfall this fix is available under "ObjCanHearObjFix" switch and
    // it's not enabled by default. Probably needs testing.
    if (object2 != nullptr && object1 != nullptr) {
        if (object2->elevation == object1->elevation) {
            if (object2->tile != -1 && object1->tile != -1) {
                if (isWithinPerception(object1, object2)) {
                    canHear = true;
                }
            }
        }
    }

    programStackPushInteger(program, canHear);
}

// game_time_hour
// 0x458438
static void opGameTimeHour(Program* program)
{
    int value = gameTimeGetHour();
    programStackPushInteger(program, value);
}

// fixed_param
// 0x45845C
static void opGetFixedParam(Program* program)
{
    int fixedParam = 0;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        fixedParam = script->fixedParam;
    } else {
        scriptPredefinedError(program, "fixed_param", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushInteger(program, fixedParam);
}

// tile_is_visible
// 0x4584B0
static void opTileIsVisible(Program* program)
{
    int data = programStackPopInteger(program);

    int isVisible = 0;
    if (tileIsVisible(data)) {
        isVisible = 1;
    }

    programStackPushInteger(program, isVisible);
}

// dialogue_system_enter
// 0x458534
static void opGameDialogSystemEnter(Program* program)
{
    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) == -1) {
        return;
    }

    Object* self = scriptGetSelf(program);
    if (PID_TYPE(self->pid) == OBJ_TYPE_CRITTER) {
        if (!critterIsActive(self)) {
            return;
        }
    }

    if (isInCombat()) {
        return;
    }

    if (gameRequestState(GAME_STATE_4) == -1) {
        return;
    }

    gGameDialogSpeaker = scriptGetSelf(program);
}

// action_being_used
// 0x458594
static void opGetActionBeingUsed(Program* program)
{
    int action = -1;

    int sid = scriptGetSid(program);

    Script* script;
    if (scriptGetScript(sid, &script) != -1) {
        action = script->actionBeingUsed;
    } else {
        scriptPredefinedError(program, "action_being_used", SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID);
    }

    programStackPushInteger(program, action);
}

// critter_state
// 0x4585E8
static void opGetCritterState(Program* program)
{
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    int state = CRITTER_STATE_DEAD;
    if (critter != nullptr && PID_TYPE(critter->pid) == OBJ_TYPE_CRITTER) {
        if (critterIsActive(critter)) {
            state = CRITTER_STATE_NORMAL;

            int anim = FID_ANIM_TYPE(critter->fid);
            if (anim >= ANIM_FALL_BACK_SF && anim <= ANIM_FALL_FRONT_SF) {
                state = CRITTER_STATE_PRONE;
            }

            state |= (critter->data.critter.combat.results & DAM_CRIP);
        } else {
            if (!critterIsDead(critter)) {
                state = CRITTER_STATE_PRONE;
            }
        }
    } else {
        scriptPredefinedError(program, "critter_state", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, state);
}

// game_time_advance
// 0x4586C8
static void opGameTimeAdvance(Program* program)
{
    int data = programStackPopInteger(program);

    int days = data / GAME_TIME_TICKS_PER_DAY;
    int remainder = data % GAME_TIME_TICKS_PER_DAY;

    for (int day = 0; day < days; day++) {
        gameTimeAddTicks(GAME_TIME_TICKS_PER_DAY);
        queueProcessEvents();
    }

    gameTimeAddTicks(remainder);
    queueProcessEvents();
}

// radiation_inc
// 0x458760
static void opRadiationIncrease(Program* program)
{
    int amount = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "radiation_inc", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    critterAdjustRadiation(object, amount);
}

// radiation_dec
// 0x458800
static void opRadiationDecrease(Program* program)
{
    int amount = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "radiation_dec", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    int radiation = critterGetRadiation(object);
    int adjustment = radiation >= 0 ? -amount : 0;

    critterAdjustRadiation(object, adjustment);
}

// critter_attempt_placement
// 0x4588B4
static void opCritterAttemptPlacement(Program* program)
{
    int elevation = programStackPopInteger(program);
    int tile = programStackPopInteger(program);
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    if (critter == nullptr) {
        scriptPredefinedError(program, "critter_attempt_placement", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (elevation != critter->elevation && PID_TYPE(critter->pid) == OBJ_TYPE_CRITTER) {
        _combat_delete_critter(critter);
    }

    objectSetLocation(critter, 0, elevation, nullptr);

    int rc = _obj_attempt_placement(critter, tile, elevation, 1);
    programStackPushInteger(program, rc);
}

// obj_pid
// 0x4589A0
static void opGetObjectPid(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    int pid = -1;
    if (obj) {
        pid = obj->pid;
    } else {
        scriptPredefinedError(program, "obj_pid", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, pid);
}

// cur_map_index
// 0x458A30
static void opGetCurrentMap(Program* program)
{
    int mapIndex = mapGetCurrentMap();
    programStackPushInteger(program, mapIndex);
}

// critter_add_trait
// 0x458A54
static void opCritterAddTrait(Program* program)
{
    int value = programStackPopInteger(program);
    int param = programStackPopInteger(program);
    int kind = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
            switch (kind) {
            case CRITTER_TRAIT_PERK:
                if (1) {
                    char* critterName = critterGetName(object);
                    char* perkName = perkGetName(param);
                    debugPrint("\nintextra::critter_add_trait: Adding Perk %s to %s", perkName, critterName);

                    if (value > 0) {
                        if (perkAddForce(object, param) != 0) {
                            scriptError("\nScript Error: %s: op_critter_add_trait: perk_add_force failed", program->name);
                            debugPrint("Perk: %d", param);
                        }
                    } else {
                        if (perkRemove(object, param) != 0) {
                            // FIXME: typo in debug message, should be perk_sub
                            scriptError("\nScript Error: %s: op_critter_add_trait: per_sub failed", program->name);
                            debugPrint("Perk: %d", param);
                        }
                    }

                    if (object == gDude) {
                        interfaceRenderHitPoints(true);
                    }
                }
                break;
            case CRITTER_TRAIT_OBJECT:
                switch (param) {
                case CRITTER_TRAIT_OBJECT_AI_PACKET:
                    critterSetAiPacket(object, value);
                    break;
                case CRITTER_TRAIT_OBJECT_TEAM:
                    if (objectIsPartyMember(object)) {
                        break;
                    }

                    if (object->data.critter.combat.team == value) {
                        break;
                    }

                    if (_isLoadingGame()) {
                        break;
                    }

                    critterSetTeam(object, value);
                    break;
                }
                break;
            default:
                scriptError("\nScript Error: %s: op_critter_add_trait: Trait out of range", program->name);
                break;
            }
        }
    } else {
        scriptPredefinedError(program, "critter_add_trait", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, -1);
}

// critter_rm_trait
// 0x458C2C
static void opCritterRemoveTrait(Program* program)
{
    int value = programStackPopInteger(program);
    int param = programStackPopInteger(program);
    int kind = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "critter_rm_trait", SCRIPT_ERROR_OBJECT_IS_NULL);
        // FIXME: Ruins stack.
        return;
    }

    if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
        switch (kind) {
        case CRITTER_TRAIT_PERK:
            while (perkGetRank(object, param) > 0) {
                if (perkRemove(object, param) != 0) {
                    scriptError("\nScript Error: op_critter_rm_trait: perk_sub failed");
                }
            }
            break;
        default:
            scriptError("\nScript Error: %s: op_critter_rm_trait: Trait out of range", program->name);
            break;
        }
    }

    programStackPushInteger(program, -1);
}

// proto_data
// 0x458D38
static void opGetProtoData(Program* program)
{
    int member = programStackPopInteger(program);
    int pid = programStackPopInteger(program);

    ProtoDataMemberValue value;
    value.integerValue = 0;
    int valueType = protoGetDataMember(pid, member, &value);
    switch (valueType) {
    case PROTO_DATA_MEMBER_TYPE_INT:
        programStackPushInteger(program, value.integerValue);
        break;
    case PROTO_DATA_MEMBER_TYPE_STRING:
        programStackPushString(program, value.stringValue);
        break;
    default:
        programStackPushInteger(program, 0);
        break;
    }
}

// message_str
// 0x458E10
static void opGetMessageString(Program* program)
{
    // 0x518EFC
    static char errStr[] = "Error";

    int messageIndex = programStackPopInteger(program);
    int messageListIndex = programStackPopInteger(program);

    char* string;
    if (messageIndex >= 0) {
        string = _scr_get_msg_str_speech(messageListIndex, messageIndex, 1);
        if (string == nullptr) {
            debugPrint("\nError: No message file EXISTS!: index %d, line %d", messageListIndex, messageIndex);
            string = errStr;
        }
    } else {
        string = errStr;
    }

    programStackPushString(program, string);
}

// critter_inven_obj
// 0x458F00
static void opCritterGetInventoryObject(Program* program)
{
    int type = programStackPopInteger(program);
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    if (PID_TYPE(critter->pid) == OBJ_TYPE_CRITTER) {
        switch (type) {
        case INVEN_TYPE_WORN:
            programStackPushPointer(program, critterGetArmor(critter));
            break;
        case INVEN_TYPE_RIGHT_HAND:
            if (critter == gDude) {
                if (interfaceGetCurrentHand() != HAND_LEFT) {
                    programStackPushPointer(program, critterGetItem2(critter));
                } else {
                    programStackPushPointer(program, nullptr);
                }
            } else {
                programStackPushPointer(program, critterGetItem2(critter));
            }
            break;
        case INVEN_TYPE_LEFT_HAND:
            if (critter == gDude) {
                if (interfaceGetCurrentHand() == HAND_LEFT) {
                    programStackPushPointer(program, critterGetItem1(critter));
                } else {
                    programStackPushPointer(program, nullptr);
                }
            } else {
                programStackPushPointer(program, critterGetItem1(critter));
            }
            break;
        case INVEN_TYPE_INV_COUNT:
            programStackPushInteger(program, critter->data.inventory.length);
            break;
        default:
            scriptError("script error: %s: Error in critter_inven_obj -- wrong type!", program->name);
            programStackPushInteger(program, 0);
            break;
        }
    } else {
        scriptPredefinedError(program, "critter_inven_obj", SCRIPT_ERROR_FOLLOWS);
        debugPrint("  Not a critter!");
        programStackPushInteger(program, 0);
    }
}

// obj_set_light_level
// 0x459088
static void opSetObjectLightLevel(Program* program)
{
    int lightDistance = programStackPopInteger(program);
    int lightIntensity = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr) {
        scriptPredefinedError(program, "obj_set_light_level", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    Rect rect;
    if (lightIntensity != 0) {
        if (objectSetLight(object, lightDistance, (lightIntensity * 65636) / 100, &rect) == -1) {
            return;
        }
    } else {
        if (objectSetLight(object, lightDistance, 0, &rect) == -1) {
            return;
        }
    }
    tileWindowRefreshRect(&rect, object->elevation);
}

// 0x459170
static void opWorldmap(Program* program)
{
    scriptsRequestWorldMap();
}

// inven_cmds
// 0x459178
static void _op_inven_cmds(Program* program)
{
    int index = programStackPopInteger(program);
    int cmd = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    Object* item = nullptr;

    if (obj != nullptr) {
        switch (cmd) {
        case 13:
            item = _inven_index_ptr(obj, index);
            break;
        }
    } else {
        scriptPredefinedError(program, "inven_cmds", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushPointer(program, item);
}

// float_msg
// 0x459280
static void opFloatMessage(Program* program)
{
    int floatingMessageType = programStackPopInteger(program);
    ProgramValue stringValue = programStackPopValue(program);
    char* string = nullptr;
    if ((stringValue.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
        string = programGetString(program, stringValue.opcode, stringValue.integerValue);
    }
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    int color = _colorTable[32747];
    int a5 = _colorTable[0];
    int font = 101;

    if (obj == nullptr) {
        scriptPredefinedError(program, "float_msg", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (string == nullptr || *string == '\0') {
        textObjectsRemoveByOwner(obj);
        tileWindowRefresh();
        return;
    }

    if (obj->elevation != gElevation) {
        return;
    }

    if (floatingMessageType == FLOATING_MESSAGE_TYPE_COLOR_SEQUENCE) {
        floatingMessageType = _last_color + 1;
        if (floatingMessageType >= FLOATING_MESSAGE_TYPE_COUNT) {
            floatingMessageType = FLOATING_MESSAGE_TYPE_BLACK;
        }
        _last_color = floatingMessageType;
    }

    switch (floatingMessageType) {
    case FLOATING_MESSAGE_TYPE_WARNING:
        color = _colorTable[31744];
        a5 = _colorTable[0];
        font = 103;
        tileSetCenter(gDude->tile, TILE_SET_CENTER_REFRESH_WINDOW);
        break;
    case FLOATING_MESSAGE_TYPE_NORMAL:
    case FLOATING_MESSAGE_TYPE_YELLOW:
        color = _colorTable[32747];
        break;
    case FLOATING_MESSAGE_TYPE_BLACK:
    case FLOATING_MESSAGE_TYPE_PURPLE:
    case FLOATING_MESSAGE_TYPE_GREY:
        color = _colorTable[10570];
        break;
    case FLOATING_MESSAGE_TYPE_RED:
        color = _colorTable[31744];
        break;
    case FLOATING_MESSAGE_TYPE_GREEN:
        color = _colorTable[992];
        break;
    case FLOATING_MESSAGE_TYPE_BLUE:
        color = _colorTable[31];
        break;
    case FLOATING_MESSAGE_TYPE_NEAR_WHITE:
        color = _colorTable[21140];
        break;
    case FLOATING_MESSAGE_TYPE_LIGHT_RED:
        color = _colorTable[32074];
        break;
    case FLOATING_MESSAGE_TYPE_WHITE:
        color = _colorTable[32767];
        break;
    case FLOATING_MESSAGE_TYPE_DARK_GREY:
        color = _colorTable[8456];
        break;
    case FLOATING_MESSAGE_TYPE_LIGHT_GREY:
        color = _colorTable[15855];
        break;
    }

    Rect rect;
    if (textObjectAdd(obj, string, font, color, a5, &rect) != -1) {
        tileWindowRefreshRect(&rect, obj->elevation);
    }
}

// metarule
// 0x4594A0
static void opMetarule(Program* program)
{
    ProgramValue param = programStackPopValue(program);
    int rule = programStackPopInteger(program);

    int result = 0;

    switch (rule) {
    case METARULE_SIGNAL_END_GAME:
        result = 0;
        _game_user_wants_to_quit = 2;
        break;
    case METARULE_FIRST_RUN:
        result = (gMapHeader.flags & MAP_SAVED) == 0;
        break;
    case METARULE_ELEVATOR:
        scriptsRequestElevator(scriptGetSelf(program), param.integerValue);
        result = 0;
        break;
    case METARULE_PARTY_COUNT:
        result = _getPartyMemberCount();
        break;
    case METARULE_AREA_KNOWN:
        result = wmAreaVisitedState(param.integerValue);
        break;
    case METARULE_WHO_ON_DRUGS:
        result = queueHasEvent(static_cast<Object*>(param.pointerValue), EVENT_TYPE_DRUG);
        break;
    case METARULE_MAP_KNOWN:
        result = wmMapIsKnown(param.integerValue);
        break;
    case METARULE_IS_LOADGAME:
        result = _isLoadingGame();
        break;
    case METARULE_CAR_CURRENT_TOWN:
        result = wmCarCurrentArea();
        break;
    case METARULE_GIVE_CAR_TO_PARTY:
        result = wmCarGiveToParty();
        break;
    case METARULE_GIVE_CAR_GAS:
        result = wmCarFillGas(param.integerValue);
        break;
    case METARULE_SKILL_CHECK_TAG:
        result = skillIsTagged(param.integerValue);
        break;
    case METARULE_DROP_ALL_INVEN:
        if (1) {
            Object* object = static_cast<Object*>(param.pointerValue);
            result = itemDropAll(object, object->tile);
            if (gDude == object) {
                interfaceUpdateItems(false, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
                interfaceRenderArmorClass(false);
            }
        }
        break;
    case METARULE_INVEN_UNWIELD_WHO:
        if (1) {
            Object* object = static_cast<Object*>(param.pointerValue);

            int hand = HAND_RIGHT;
            if (object == gDude) {
                if (interfaceGetCurrentHand() == HAND_LEFT) {
                    hand = HAND_LEFT;
                }
            }

            result = _invenUnwieldFunc(object, hand, 0);

            if (object == gDude) {
                bool animated = !gameUiIsDisabled();
                interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
            } else {
                Object* item = critterGetItem1(object);
                if (itemGetType(item) == ITEM_TYPE_WEAPON) {
                    item->flags &= ~OBJECT_IN_LEFT_HAND;
                }
            }
        }
        break;
    case METARULE_GET_WORLDMAP_XPOS:
        wmGetPartyWorldPos(&result, nullptr);
        break;
    case METARULE_GET_WORLDMAP_YPOS:
        wmGetPartyWorldPos(nullptr, &result);
        break;
    case METARULE_CURRENT_TOWN:
        if (wmGetPartyCurArea(&result) == -1) {
            debugPrint("\nIntextra: Error: metarule: current_town");
        }
        break;
    case METARULE_LANGUAGE_FILTER:
        result = static_cast<int>(settings.preferences.language_filter);
        break;
    case METARULE_VIOLENCE_FILTER:
        result = settings.preferences.violence_level;
        break;
    case METARULE_WEAPON_DAMAGE_TYPE:
        if (1) {
            Object* object = static_cast<Object*>(param.pointerValue);
            if (PID_TYPE(object->pid) == OBJ_TYPE_ITEM) {
                if (itemGetType(object) == ITEM_TYPE_WEAPON) {
                    result = weaponGetDamageType(nullptr, object);
                    break;
                }
            } else {
                if (buildFid(OBJ_TYPE_MISC, 10, 0, 0, 0) == object->fid) {
                    result = DAMAGE_TYPE_EXPLOSION;
                    break;
                }
            }

            scriptPredefinedError(program, "metarule:w_damage_type", SCRIPT_ERROR_FOLLOWS);
            debugPrint("Not a weapon!");
        }
        break;
    case METARULE_CRITTER_BARTERS:
        if (1) {
            Object* object = static_cast<Object*>(param.pointerValue);
            if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
                Proto* proto;
                protoGetProto(object->pid, &proto);
                if ((proto->critter.data.flags & CRITTER_BARTER) != 0) {
                    result = 1;
                }
            }
        }
        break;
    case METARULE_CRITTER_KILL_TYPE:
        result = critterGetKillType(static_cast<Object*>(param.pointerValue));
        break;
    case METARULE_SET_CAR_CARRY_AMOUNT:
        if (1) {
            Proto* proto;
            if (protoGetProto(PROTO_ID_CAR_TRUNK, &proto) != -1) {
                proto->item.data.container.maxSize = param.integerValue;
                result = 1;
            }
        }
        break;
    case METARULE_GET_CAR_CARRY_AMOUNT:
        if (1) {
            Proto* proto;
            if (protoGetProto(PROTO_ID_CAR_TRUNK, &proto) != -1) {
                result = proto->item.data.container.maxSize;
            }
        }
        break;
    }

    programStackPushInteger(program, result);
}

// anim
// 0x4598BC
static void opAnim(Program* program)
{
    ProgramValue frameValue = programStackPopValue(program);
    int anim = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    // CE: There is a bug in the `animate_rotation` macro in the user-space
    // sсripts - instead of passing direction, it passes object. The direction
    // argument is thrown away by preprocessor. Original code ignores this bug
    // since there is no distiction between integers and pointers. In addition
    // there is a guard in the code path below which simply ignores any value
    // greater than 6 (so rotation does not change when pointer is passed).
    int frame;
    if (frameValue.opcode == VALUE_TYPE_INT) {
        frame = frameValue.integerValue;
    } else if (anim == 1000 && frameValue.opcode == VALUE_TYPE_PTR) {
        // Force code path below to skip setting rotation.
        frame = ROTATION_COUNT;
    } else {
        programFatalError("script error: %s: invalid arg 2 to anim", program->name);
    }

    if (obj == nullptr) {
        scriptPredefinedError(program, "anim", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (anim < ANIM_COUNT) {
        CritterCombatData* combatData = nullptr;
        if (PID_TYPE(obj->pid) == OBJ_TYPE_CRITTER) {
            combatData = &(obj->data.critter.combat);
        }

        anim = _correctDeath(obj, anim, true);

        reg_anim_begin(ANIMATION_REQUEST_UNRESERVED);

        // TODO: Not sure about the purpose, why it handles knock down flag?
        if (frame == 0) {
            animationRegisterAnimate(obj, anim, 0);
            if (anim >= ANIM_FALL_BACK && anim <= ANIM_FALL_FRONT_BLOOD) {
                int fid = buildFid(OBJ_TYPE_CRITTER, obj->fid & 0xFFF, anim + 28, (obj->fid & 0xF000) >> 12, (obj->fid & 0x70000000) >> 28);
                animationRegisterSetFid(obj, fid, -1);
            }

            if (combatData != nullptr) {
                combatData->results &= DAM_KNOCKED_DOWN;
            }
        } else {
            int fid = buildFid(FID_TYPE(obj->fid), obj->fid & 0xFFF, anim, (obj->fid & 0xF000) >> 12, (obj->fid & 0x70000000) >> 24);
            animationRegisterAnimateReversed(obj, anim, 0);

            if (anim == ANIM_PRONE_TO_STANDING) {
                fid = buildFid(FID_TYPE(obj->fid), obj->fid & 0xFFF, ANIM_FALL_FRONT_SF, (obj->fid & 0xF000) >> 12, (obj->fid & 0x70000000) >> 24);
            } else if (anim == ANIM_BACK_TO_STANDING) {
                fid = buildFid(FID_TYPE(obj->fid), obj->fid & 0xFFF, ANIM_FALL_BACK_SF, (obj->fid & 0xF000) >> 12, (obj->fid & 0x70000000) >> 24);
            }

            if (combatData != nullptr) {
                combatData->results |= DAM_KNOCKED_DOWN;
            }

            animationRegisterSetFid(obj, fid, -1);
        }

        reg_anim_end();
    } else if (anim == 1000) {
        if (frame < ROTATION_COUNT) {
            Rect rect;
            objectSetRotation(obj, frame, &rect);
            tileWindowRefreshRect(&rect, gElevation);
        }
    } else if (anim == 1010) {
        Rect rect;
        objectSetFrame(obj, frame, &rect);
        tileWindowRefreshRect(&rect, gElevation);
    } else {
        scriptError("\nScript Error: %s: op_anim: anim out of range", program->name);
    }
}

// obj_carrying_pid_obj
// 0x459B5C
static void opObjectCarryingObjectByPid(Program* program)
{
    int pid = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    Object* result = nullptr;
    if (object != nullptr) {
        result = objectGetCarriedObjectByPid(object, pid);
    } else {
        scriptPredefinedError(program, "obj_carrying_pid_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushPointer(program, result);
}

// reg_anim_func
// 0x459C20
static void opRegAnimFunc(Program* program)
{
    ProgramValue param = programStackPopValue(program);
    int cmd = programStackPopInteger(program);

    if (!isInCombat()) {
        switch (cmd) {
        case OP_REG_ANIM_FUNC_BEGIN:
            reg_anim_begin(param.integerValue);
            break;
        case OP_REG_ANIM_FUNC_CLEAR:
            reg_anim_clear(static_cast<Object*>(param.pointerValue));
            break;
        case OP_REG_ANIM_FUNC_END:
            reg_anim_end();
            break;
        }
    }
}

// reg_anim_animate
// 0x459CD4
static void opRegAnimAnimate(Program* program)
{
    int delay = programStackPopInteger(program);
    int anim = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (anim != 20 || object == nullptr || object->pid != 0x100002F || (settings.preferences.violence_level >= 2)) {
            if (object != nullptr) {
                animationRegisterAnimate(object, anim, delay);
            } else {
                scriptPredefinedError(program, "reg_anim_animate", SCRIPT_ERROR_OBJECT_IS_NULL);
            }
        }
    }
}

// reg_anim_animate_reverse
// 0x459DC4
static void opRegAnimAnimateReverse(Program* program)
{
    int delay = programStackPopInteger(program);
    int anim = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (object != nullptr) {
            animationRegisterAnimateReversed(object, anim, delay);
        } else {
            scriptPredefinedError(program, "reg_anim_animate_reverse", SCRIPT_ERROR_OBJECT_IS_NULL);
        }
    }
}

// reg_anim_obj_move_to_obj
// 0x459E74
static void opRegAnimObjectMoveToObject(Program* program)
{
    int delay = programStackPopInteger(program);
    Object* dest = static_cast<Object*>(programStackPopPointer(program));
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (object != nullptr) {
            animationRegisterMoveToObject(object, dest, -1, delay);
        } else {
            scriptPredefinedError(program, "reg_anim_obj_move_to_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        }
    }
}

// reg_anim_obj_run_to_obj
// 0x459F28
static void opRegAnimObjectRunToObject(Program* program)
{
    int delay = programStackPopInteger(program);
    Object* dest = static_cast<Object*>(programStackPopPointer(program));
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (object != nullptr) {
            animationRegisterRunToObject(object, dest, -1, delay);
        } else {
            scriptPredefinedError(program, "reg_anim_obj_run_to_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        }
    }
}

// reg_anim_obj_move_to_tile
// 0x459FDC
static void opRegAnimObjectMoveToTile(Program* program)
{
    int delay = programStackPopInteger(program);
    int tile = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (object != nullptr) {
            animationRegisterMoveToTile(object, tile, object->elevation, -1, delay);
        } else {
            scriptPredefinedError(program, "reg_anim_obj_move_to_tile", SCRIPT_ERROR_OBJECT_IS_NULL);
        }
    }
}

// reg_anim_obj_run_to_tile
// 0x45A094
static void opRegAnimObjectRunToTile(Program* program)
{
    int delay = programStackPopInteger(program);
    int tile = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (object != nullptr) {
            animationRegisterRunToTile(object, tile, object->elevation, -1, delay);
        } else {
            scriptPredefinedError(program, "reg_anim_obj_run_to_tile", SCRIPT_ERROR_OBJECT_IS_NULL);
        }
    }
}

// play_gmovie
// 0x45A14C
static void opPlayGameMovie(Program* program)
{
    // 0x453F9C
    static const unsigned short flags[MOVIE_COUNT] = {
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
        GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC,
    };

    program->flags |= PROGRAM_FLAG_0x20;

    int movie = programStackPopInteger(program);

    // CE: Disable map updates. Needed to stop animation of objects (dude in
    // particular) when playing movies (the problem can be seen as visual
    // artifacts when playing endgame oilrig explosion).
    bool isoWasDisabled = isoDisable();

    gameDialogDisable();

    if (gameMoviePlay(movie, flags[movie]) == -1) {
        debugPrint("\nError playing movie %d!", movie);
    }

    gameDialogEnable();

    if (isoWasDisabled) {
        isoEnable();
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// add_mult_objs_to_inven
// 0x45A200
static void opAddMultipleObjectsToInventory(Program* program)
{
    int quantity = programStackPopInteger(program);
    Object* item = static_cast<Object*>(programStackPopPointer(program));
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object == nullptr || item == nullptr) {
        return;
    }

    if (quantity < 0) {
        quantity = 1;
    } else if (quantity > 99999) {
        // SFALL
        quantity = 99999;
    }

    if (itemAdd(object, item, quantity) == 0) {
        Rect rect;
        _obj_disconnect(item, &rect);
        tileWindowRefreshRect(&rect, item->elevation);
    }
}

// rm_mult_objs_from_inven
// 0x45A2D4
static void opRemoveMultipleObjectsFromInventory(Program* program)
{
    int quantityToRemove = programStackPopInteger(program);
    Object* item = static_cast<Object*>(programStackPopPointer(program));
    Object* owner = static_cast<Object*>(programStackPopPointer(program));

    if (owner == nullptr || item == nullptr) {
        scriptPredefinedError(program, "rm_mult_objs_from_inven", SCRIPT_ERROR_OBJECT_IS_NULL);
        programStackPushInteger(program, 0);
        return;
    }

    bool itemWasEquipped = (item->flags & OBJECT_EQUIPPED) != 0;

    int quantity = itemGetQuantity(owner, item);
    if (quantity > quantityToRemove) {
        quantity = quantityToRemove;
    }

    if (quantity != 0) {
        if (itemRemove(owner, item, quantity) == 0) {
            Rect updatedRect;
            _obj_connect(item, 1, 0, &updatedRect);
            if (itemWasEquipped) {
                if (owner == gDude) {
                    bool animated = !gameUiIsDisabled();
                    interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
                }
            }
        }
    }

    programStackPushInteger(program, quantity);
}

// get_month
// 0x45A40C
static void opGetMonth(Program* program)
{
    int month;
    gameTimeGetDate(&month, nullptr, nullptr);

    programStackPushInteger(program, month);
}

// get_day
// 0x45A43C
static void opGetDay(Program* program)
{
    int day;
    gameTimeGetDate(nullptr, &day, nullptr);

    programStackPushInteger(program, day);
}

// explosion
// 0x45A46C
static void opExplosion(Program* program)
{
    int maxDamage = programStackPopInteger(program);
    int elevation = programStackPopInteger(program);
    int tile = programStackPopInteger(program);

    if (tile == -1) {
        debugPrint("\nError: explosion: bad tile_num!");
        return;
    }

    int minDamage = 1;
    if (maxDamage == 0) {
        minDamage = 0;
    }

    scriptsRequestExplosion(tile, elevation, minDamage, maxDamage);
}

// days_since_visited
// 0x45A528
static void opGetDaysSinceLastVisit(Program* program)
{
    int days;

    if (gMapHeader.lastVisitTime != 0) {
        days = (gameTimeGetTime() - gMapHeader.lastVisitTime) / GAME_TIME_TICKS_PER_DAY;
    } else {
        days = -1;
    }

    programStackPushInteger(program, days);
}

// gsay_start
// 0x45A56C
static void _op_gsay_start(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    if (_gdialogStart() != 0) {
        program->flags &= ~PROGRAM_FLAG_0x20;
        programFatalError("Error starting dialog.");
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// gsay_end
// 0x45A5B0
static void _op_gsay_end(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;
    _gdialogGo();
    program->flags &= ~PROGRAM_FLAG_0x20;
}

// gsay_reply
// 0x45A5D4
static void _op_gsay_reply(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    ProgramValue msg = programStackPopValue(program);
    int messageListId = programStackPopInteger(program);

    if ((msg.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
        char* string = programGetString(program, msg.opcode, msg.integerValue);
        gameDialogSetTextReply(program, messageListId, string);
    } else if (msg.opcode == VALUE_TYPE_INT) {
        gameDialogSetMessageReply(program, messageListId, msg.integerValue);
    } else {
        programFatalError("script error: %s: invalid arg %d to gsay_reply", program->name, 0);
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// gsay_option
// 0x45A6C4
static void _op_gsay_option(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    int reaction = programStackPopInteger(program);
    ProgramValue proc = programStackPopValue(program);
    ProgramValue msg = programStackPopValue(program);
    int messageListId = programStackPopInteger(program);

    if ((proc.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
        char* procName = programGetString(program, proc.opcode, proc.integerValue);
        if ((msg.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
            const char* string = programGetString(program, msg.opcode, msg.integerValue);
            gameDialogAddTextOptionWithProcIdentifier(messageListId, string, procName, reaction);
        } else if (msg.opcode == VALUE_TYPE_INT) {
            gameDialogAddMessageOptionWithProcIdentifier(messageListId, msg.integerValue, procName, reaction);
        } else {
            programFatalError("script error: %s: invalid arg %d to gsay_option", program->name, 1);
        }
    } else if ((proc.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_INT) {
        if ((msg.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
            const char* string = programGetString(program, msg.opcode, msg.integerValue);
            gameDialogAddTextOptionWithProc(messageListId, string, proc.integerValue, reaction);
        } else if (msg.opcode == VALUE_TYPE_INT) {
            gameDialogAddMessageOptionWithProc(messageListId, msg.integerValue, proc.integerValue, reaction);
        } else {
            programFatalError("script error: %s: invalid arg %d to gsay_option", program->name, 1);
        }
    } else {
        programFatalError("Invalid arg 3 to sayOption");
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// gsay_message
// 0x45A8AC
static void _op_gsay_message(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    int reaction = programStackPopInteger(program);
    ProgramValue msg = programStackPopValue(program);
    int messageListId = programStackPopInteger(program);

    if ((msg.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
        char* string = programGetString(program, msg.opcode, msg.integerValue);
        gameDialogSetTextReply(program, messageListId, string);
    } else if (msg.opcode == VALUE_TYPE_INT) {
        gameDialogSetMessageReply(program, messageListId, msg.integerValue);
    } else {
        programFatalError("script error: %s: invalid arg %d to gsay_message", program->name, 1);
    }

    gameDialogAddMessageOptionWithProcIdentifier(-2, -2, nullptr, 50);
    _gdialogSayMessage();

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// giq_option
// 0x45A9B4
static void _op_giq_option(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    int reaction = programStackPopInteger(program);
    ProgramValue proc = programStackPopValue(program);
    ProgramValue msg = programStackPopValue(program);
    int messageListId = programStackPopInteger(program);
    int iq = programStackPopInteger(program);

    int intelligence = critterGetStat(gDude, STAT_INTELLIGENCE);
    intelligence += perkGetRank(gDude, PERK_SMOOTH_TALKER);

    if (iq < 0) {
        if (-intelligence < iq) {
            program->flags &= ~PROGRAM_FLAG_0x20;
            return;
        }
    } else {
        if (intelligence < iq) {
            program->flags &= ~PROGRAM_FLAG_0x20;
            return;
        }
    }

    if ((proc.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
        char* procName = programGetString(program, proc.opcode, proc.integerValue);
        if ((msg.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
            char* string = programGetString(program, msg.opcode, msg.integerValue);
            gameDialogAddTextOptionWithProcIdentifier(messageListId, string, procName, reaction);
        } else if (msg.opcode == VALUE_TYPE_INT) {
            gameDialogAddMessageOptionWithProcIdentifier(messageListId, msg.integerValue, procName, reaction);
        } else {
            programFatalError("script error: %s: invalid arg %d to giq_option", program->name, 1);
        }
    } else if (proc.opcode == VALUE_TYPE_INT) {
        if ((msg.opcode & VALUE_TYPE_MASK) == VALUE_TYPE_STRING) {
            char* string = programGetString(program, msg.opcode, msg.integerValue);
            gameDialogAddTextOptionWithProc(messageListId, string, proc.integerValue, reaction);
        } else if (msg.opcode == VALUE_TYPE_INT) {
            gameDialogAddMessageOptionWithProc(messageListId, msg.integerValue, proc.integerValue, reaction);
        } else {
            programFatalError("script error: %s: invalid arg %d to giq_option", program->name, 1);
        }
    } else {
        programFatalError("script error: %s: invalid arg %d to giq_option", program->name, 3);
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// poison
// 0x45AB90
static void opPoison(Program* program)
{
    int amount = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    if (obj == nullptr) {
        scriptPredefinedError(program, "poison", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (critterAdjustPoison(obj, amount) != 0) {
        debugPrint("\nScript Error: poison: adjust failed!");
    }
}

// get_poison
// 0x45AC44
static void opGetPoison(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    int poison = 0;
    if (obj != nullptr) {
        if (PID_TYPE(obj->pid) == OBJ_TYPE_CRITTER) {
            poison = critterGetPoison(obj);
        } else {
            debugPrint("\nScript Error: get_poison: who is not a critter!");
        }
    } else {
        scriptPredefinedError(program, "get_poison", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, poison);
}

// party_add
// 0x45ACF4
static void opPartyAdd(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));
    if (object == nullptr) {
        scriptPredefinedError(program, "party_add", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    partyMemberAdd(object);
}

// party_remove
// 0x45AD68
static void opPartyRemove(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));
    if (object == nullptr) {
        scriptPredefinedError(program, "party_remove", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    partyMemberRemove(object);
}

// reg_anim_animate_forever
// 0x45ADDC
static void opRegAnimAnimateForever(Program* program)
{
    int anim = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    if (!isInCombat()) {
        if (obj != nullptr) {
            animationRegisterAnimateForever(obj, anim, -1);
        } else {
            scriptPredefinedError(program, "reg_anim_animate_forever", SCRIPT_ERROR_OBJECT_IS_NULL);
        }
    }
}

// critter_injure
// 0x45AE8C
static void opCritterInjure(Program* program)
{
    int flags = programStackPopInteger(program);
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    if (critter == nullptr) {
        scriptPredefinedError(program, "critter_injure", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    bool reverse = (flags & DAM_PERFORM_REVERSE) != 0;

    flags &= DAM_CRIP;

    if (reverse) {
        critter->data.critter.combat.results &= ~flags;
    } else {
        critter->data.critter.combat.results |= flags;
    }

    if (critter == gDude) {
        if ((flags & DAM_CRIP_ARM_ANY) != 0) {
            int leftItemAction;
            int rightItemAction;
            interfaceGetItemActions(&leftItemAction, &rightItemAction);
            interfaceUpdateItems(true, leftItemAction, rightItemAction);
        }
    }
}

// combat_is_initialized
// 0x45AF7C
static void opCombatIsInitialized(Program* program)
{
    programStackPushInteger(program, isInCombat() ? 1 : 0);
}

// gdialog_barter
// 0x45AFA0
static void _op_gdialog_barter(Program* program)
{
    int data = programStackPopInteger(program);

    if (gameDialogBarter(data) == -1) {
        debugPrint("\nScript Error: gdialog_barter: failed");
    }
}

// difficulty_level
// 0x45B010
static void opGetGameDifficulty(Program* program)
{
    programStackPushInteger(program, settings.preferences.game_difficulty);
}

// running_burning_guy
// 0x45B05C
static void opGetRunningBurningGuy(Program* program)
{
    programStackPushInteger(program, static_cast<int>(settings.preferences.running_burning_guy));
}

// inven_unwield
static void _op_inven_unwield(Program* program)
{
    Object* obj;
    int v1;

    obj = scriptGetSelf(program);
    v1 = 1;

    if (obj == gDude && !interfaceGetCurrentHand()) {
        v1 = 0;
    }

    _inven_unwield(obj, v1);
}

// obj_is_locked
// 0x45B0D8
static void opObjectIsLocked(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    bool locked = false;
    if (object != nullptr) {
        locked = objectIsLocked(object);
    } else {
        scriptPredefinedError(program, "obj_is_locked", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, locked ? 1 : 0);
}

// obj_lock
// 0x45B16C
static void opObjectLock(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        objectLock(object);
    } else {
        scriptPredefinedError(program, "obj_lock", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// obj_unlock
// 0x45B1E0
static void opObjectUnlock(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        objectUnlock(object);
    } else {
        scriptPredefinedError(program, "obj_unlock", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// obj_is_open
// 0x45B254
static void opObjectIsOpen(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    bool isOpen = false;
    if (object != nullptr) {
        isOpen = objectIsOpen(object);
    } else {
        scriptPredefinedError(program, "obj_is_open", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, isOpen ? 1 : 0);
}

// obj_open
// 0x45B2E8
static void opObjectOpen(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        objectOpen(object);
    } else {
        scriptPredefinedError(program, "obj_open", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// obj_close
// 0x45B35C
static void opObjectClose(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        objectClose(object);
    } else {
        scriptPredefinedError(program, "obj_close", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// game_ui_disable
// 0x45B3D0
static void opGameUiDisable(Program* program)
{
    gameUiDisable(0);
}

// game_ui_enable
// 0x45B3D8
static void opGameUiEnable(Program* program)
{
    gameUiEnable();
}

// game_ui_is_disabled
// 0x45B3E0
static void opGameUiIsDisabled(Program* program)
{
    programStackPushInteger(program, gameUiIsDisabled() ? 1 : 0);
}

// gfade_out
// 0x45B404
static void opGameFadeOut(Program* program)
{
    int data = programStackPopInteger(program);

    if (data != 0) {
        paletteFadeTo(gPaletteBlack);
    } else {
        scriptPredefinedError(program, "gfade_out", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// gfade_in
// 0x45B47C
static void opGameFadeIn(Program* program)
{
    int data = programStackPopInteger(program);

    if (data != 0) {
        paletteFadeTo(_cmap);
    } else {
        scriptPredefinedError(program, "gfade_in", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// item_caps_total
// 0x45B4F4
static void opItemCapsTotal(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int amount = 0;
    if (object != nullptr) {
        amount = itemGetTotalCaps(object);
    } else {
        scriptPredefinedError(program, "item_caps_total", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, amount);
}

// item_caps_adjust
// 0x45B588
static void opItemCapsAdjust(Program* program)
{
    int amount = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int rc = -1;

    if (object != nullptr) {
        rc = itemCapsAdjust(object, amount);
    } else {
        scriptPredefinedError(program, "item_caps_adjust", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, rc);
}

// anim_action_frame
static void _op_anim_action_frame(Program* program)
{
    int anim = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int actionFrame = 0;

    if (object != nullptr) {
        int fid = buildFid(FID_TYPE(object->fid), object->fid & 0xFFF, anim, 0, object->rotation);
        CacheEntry* frmHandle;
        Art* frm = artLock(fid, &frmHandle);
        if (frm != nullptr) {
            actionFrame = artGetActionFrame(frm);
            artUnlock(frmHandle);
        }
    } else {
        scriptPredefinedError(program, "anim_action_frame", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, actionFrame);
}

// reg_anim_play_sfx
// 0x45B740
static void opRegAnimPlaySfx(Program* program)
{
    int delay = programStackPopInteger(program);
    char* soundEffectName = programStackPopString(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    if (soundEffectName == nullptr) {
        scriptPredefinedError(program, "reg_anim_play_sfx", SCRIPT_ERROR_FOLLOWS);
        debugPrint(" Can't match string!");
    }

    if (obj != nullptr) {
        animationRegisterPlaySoundEffect(obj, soundEffectName, delay);
    } else {
        scriptPredefinedError(program, "reg_anim_play_sfx", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// critter_mod_skill
// 0x45B840
static void opCritterModifySkill(Program* program)
{
    int points = programStackPopInteger(program);
    int skill = programStackPopInteger(program);
    Object* critter = static_cast<Object*>(programStackPopPointer(program));

    if (critter != nullptr && points != 0) {
        if (PID_TYPE(critter->pid) == OBJ_TYPE_CRITTER) {
            if (critter == gDude) {
                int normalizedPoints = abs(points);
                if (skillIsTagged(skill)) {
                    // Halve number of skill points. Increment/decrement skill
                    // points routines handle that.
                    normalizedPoints /= 2;
                }

                if (points > 0) {
                    // Increment skill points one by one.
                    for (int it = 0; it < normalizedPoints; it++) {
                        skillAddForce(gDude, skill);
                    }
                } else {
                    // Decrement skill points one by one.
                    for (int it = 0; it < normalizedPoints; it++) {
                        skillSubForce(gDude, skill);
                    }
                }

                // TODO: Checking for critter is dude twice probably means this
                // is inlined function.
                if (critter == gDude) {
                    int leftItemAction;
                    int rightItemAction;
                    interfaceGetItemActions(&leftItemAction, &rightItemAction);
                    interfaceUpdateItems(false, leftItemAction, rightItemAction);
                }
            } else {
                scriptPredefinedError(program, "critter_mod_skill", SCRIPT_ERROR_FOLLOWS);
                debugPrint(" Can't modify anyone except obj_dude!");
            }
        }
    } else {
        scriptPredefinedError(program, "critter_mod_skill", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, 0);
}

// sfx_build_char_name
// 0x45B9C4
static void opSfxBuildCharName(Program* program)
{
    int extra = programStackPopInteger(program);
    int anim = programStackPopInteger(program);
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    if (obj != nullptr) {
        char soundEffectName[16];
        strcpy(soundEffectName, sfxBuildCharName(obj, anim, extra));
        programStackPushString(program, soundEffectName);
    } else {
        scriptPredefinedError(program, "sfx_build_char_name", SCRIPT_ERROR_OBJECT_IS_NULL);
        programStackPushString(program, nullptr);
    }
}

// sfx_build_ambient_name
// 0x45BAA8
static void opSfxBuildAmbientName(Program* program)
{
    char* baseName = programStackPopString(program);

    char soundEffectName[16];
    strcpy(soundEffectName, gameSoundBuildAmbientSoundEffectName(baseName));
    programStackPushString(program, soundEffectName);
}

// sfx_build_interface_name
// 0x45BB54
static void opSfxBuildInterfaceName(Program* program)
{
    char* baseName = programStackPopString(program);

    char soundEffectName[16];
    strcpy(soundEffectName, gameSoundBuildInterfaceName(baseName));
    programStackPushString(program, soundEffectName);
}

// sfx_build_item_name
// 0x45BC00
static void opSfxBuildItemName(Program* program)
{
    const char* baseName = programStackPopString(program);

    char soundEffectName[16];
    strcpy(soundEffectName, gameSoundBuildInterfaceName(baseName));
    programStackPushString(program, soundEffectName);
}

// sfx_build_weapon_name
// 0x45BCAC
static void opSfxBuildWeaponName(Program* program)
{
    Object* target = static_cast<Object*>(programStackPopPointer(program));
    int hitMode = programStackPopInteger(program);
    Object* weapon = static_cast<Object*>(programStackPopPointer(program));
    int weaponSfxType = programStackPopInteger(program);

    char soundEffectName[16];
    strcpy(soundEffectName, sfxBuildWeaponName(weaponSfxType, weapon, hitMode, target));
    programStackPushString(program, soundEffectName);
}

// sfx_build_scenery_name
// 0x45BD7C
static void opSfxBuildSceneryName(Program* program)
{
    int actionType = programStackPopInteger(program);
    int action = programStackPopInteger(program);
    char* baseName = programStackPopString(program);

    char soundEffectName[16];
    strcpy(soundEffectName, sfxBuildSceneryName(actionType, action, baseName));
    programStackPushString(program, soundEffectName);
}

// sfx_build_open_name
// 0x45BE58
static void opSfxBuildOpenName(Program* program)
{
    int action = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        char soundEffectName[16];
        strcpy(soundEffectName, sfxBuildOpenName(object, action));
        programStackPushString(program, soundEffectName);
    } else {
        scriptPredefinedError(program, "sfx_build_open_name", SCRIPT_ERROR_OBJECT_IS_NULL);
        programStackPushString(program, nullptr);
    }
}

// attack_setup
// 0x45BF38
static void opAttackSetup(Program* program)
{
    Object* defender = static_cast<Object*>(programStackPopPointer(program));
    Object* attacker = static_cast<Object*>(programStackPopPointer(program));

    program->flags |= PROGRAM_FLAG_0x20;

    if (attacker != nullptr) {
        if (!critterIsActive(attacker) || (attacker->flags & OBJECT_HIDDEN) != 0) {
            debugPrint("\n   But is already dead or invisible");
            program->flags &= ~PROGRAM_FLAG_0x20;
            return;
        }

        if (!critterIsActive(defender) || (defender->flags & OBJECT_HIDDEN) != 0) {
            debugPrint("\n   But target is already dead or invisible");
            program->flags &= ~PROGRAM_FLAG_0x20;
            return;
        }

        if ((defender->data.critter.combat.maneuver & CRITTER_MANUEVER_FLEEING) != 0) {
            debugPrint("\n   But target is AFRAID");
            program->flags &= ~PROGRAM_FLAG_0x20;
            return;
        }

        if (isInCombat()) {
            if ((attacker->data.critter.combat.maneuver & CRITTER_MANEUVER_ENGAGING) == 0) {
                attacker->data.critter.combat.maneuver |= CRITTER_MANEUVER_ENGAGING;
                attacker->data.critter.combat.whoHitMe = defender;
            }
        } else {
            CombatStartData combat;
            combat.attacker = attacker;
            combat.defender = defender;
            combat.actionPointsBonus = 0;
            combat.accuracyBonus = 0;
            combat.damageBonus = 0;
            combat.minDamage = 0;
            combat.maxDamage = INT_MAX;
            combat.overrideAttackResults = 0;

            scriptsRequestCombat(&combat);
        }
    }

    program->flags &= ~PROGRAM_FLAG_0x20;
}

// destroy_mult_objs
// 0x45C0E8
static void opDestroyMultipleObjects(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;

    int quantity = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    Object* self = scriptGetSelf(program);
    bool isSelf = self == object;

    int result = 0;

    if (PID_TYPE(object->pid) == OBJ_TYPE_CRITTER) {
        _combat_delete_critter(object);
    }

    Object* owner = objectGetOwner(object);
    if (owner != nullptr) {
        int quantityToDestroy = itemGetQuantity(owner, object);
        if (quantityToDestroy > quantity) {
            quantityToDestroy = quantity;
        }

        itemRemove(owner, object, quantityToDestroy);

        if (owner == gDude) {
            bool animated = !gameUiIsDisabled();
            interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
        }

        _obj_connect(object, 1, 0, nullptr);

        if (isSelf) {
            object->sid = -1;
            object->flags |= (OBJECT_HIDDEN | OBJECT_NO_SAVE);
        } else {
            reg_anim_clear(object);
            objectDestroy(object, nullptr);
        }

        result = quantityToDestroy;
    } else {
        reg_anim_clear(object);

        Rect rect;
        objectDestroy(object, &rect);
        tileWindowRefreshRect(&rect, gElevation);
    }

    programStackPushInteger(program, result);

    program->flags &= ~PROGRAM_FLAG_0x20;

    if (isSelf) {
        program->flags |= PROGRAM_FLAG_0x0100;
    }
}

// use_obj_on_obj
// 0x45C290
static void opUseObjectOnObject(Program* program)
{
    Object* target = static_cast<Object*>(programStackPopPointer(program));
    Object* item = static_cast<Object*>(programStackPopPointer(program));

    if (item == nullptr) {
        scriptPredefinedError(program, "use_obj_on_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (target == nullptr) {
        scriptPredefinedError(program, "use_obj_on_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    Script* script;
    int sid = scriptGetSid(program);
    if (scriptGetScript(sid, &script) == -1) {
        // FIXME: Should be SCRIPT_ERROR_CANT_MATCH_PROGRAM_TO_SID.
        scriptPredefinedError(program, "use_obj_on_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    Object* self = scriptGetSelf(program);

    // SFALL: Override `self` via `op_set_self`.
    // CE: Implementation is different. Sfall integrates via `scriptGetSid` by
    // returning fake script with overridden `self`.
    if (script->overriddenSelf != nullptr) {
        self = script->overriddenSelf;
        script->overriddenSelf = nullptr;
    }

    if (PID_TYPE(self->pid) == OBJ_TYPE_CRITTER) {
        _action_use_an_item_on_object(self, target, item);
    } else {
        _obj_use_item_on(self, target, item);
    }
}

// endgame_slideshow
// 0x45C3B0
static void opEndgameSlideshow(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;
    scriptsRequestEndgame();
    program->flags &= ~PROGRAM_FLAG_0x20;
}

// move_obj_inven_to_obj
// 0x45C3D0
static void opMoveObjectInventoryToObject(Program* program)
{
    Object* object2 = static_cast<Object*>(programStackPopPointer(program));
    Object* object1 = static_cast<Object*>(programStackPopPointer(program));

    if (object1 == nullptr) {
        scriptPredefinedError(program, "move_obj_inven_to_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    if (object2 == nullptr) {
        scriptPredefinedError(program, "move_obj_inven_to_obj", SCRIPT_ERROR_OBJECT_IS_NULL);
        return;
    }

    Object* oldArmor = nullptr;
    Object* item2 = nullptr;
    if (object1 == gDude) {
        oldArmor = critterGetArmor(object1);
    } else {
        item2 = critterGetItem2(object1);
    }

    if (object1 != gDude && item2 != nullptr) {
        int flags = 0;
        if ((item2->flags & 0x01000000) != 0) {
            flags |= 0x01000000;
        }

        if ((item2->flags & 0x02000000) != 0) {
            flags |= 0x02000000;
        }

        _correctFidForRemovedItem(object1, item2, flags);
    }

    itemMoveAll(object1, object2);

    if (object1 == gDude) {
        if (oldArmor != nullptr) {
            _adjust_ac(gDude, oldArmor, nullptr);
        }

        _proto_dude_update_gender();

        bool animated = !gameUiIsDisabled();
        interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
    }
}

// endgame_movie
// 0x45C54C
static void opEndgameMovie(Program* program)
{
    program->flags |= PROGRAM_FLAG_0x20;
    endgamePlayMovie();
    program->flags &= ~PROGRAM_FLAG_0x20;
}

// obj_art_fid
// 0x45C56C
static void opGetObjectFid(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int fid = 0;
    if (object != nullptr) {
        fid = object->fid;
    } else {
        scriptPredefinedError(program, "obj_art_fid", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, fid);
}

// art_anim
// 0x45C5F8
static void opGetFidAnim(Program* program)
{
    int data = programStackPopInteger(program);
    programStackPushInteger(program, (data & 0xFF0000) >> 16);
}

// party_member_obj
// 0x45C66C
static void opGetPartyMember(Program* program)
{
    int data = programStackPopInteger(program);

    Object* object = partyMemberFindByPid(data);
    programStackPushPointer(program, object);
}

// rotation_to_tile
// 0x45C6DC
static void opGetRotationToTile(Program* program)
{
    int tile2 = programStackPopInteger(program);
    int tile1 = programStackPopInteger(program);

    int rotation = tileGetRotationTo(tile1, tile2);
    programStackPushInteger(program, rotation);
}

// jam_lock
// 0x45C778
static void opJamLock(Program* program)
{
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    objectJamLock(object);
}

// gdialog_set_barter_mod
// 0x45C7D4
static void opGameDialogSetBarterMod(Program* program)
{
    int data = programStackPopInteger(program);

    gameDialogSetBarterModifier(data);
}

// combat_difficulty
// 0x45C830
static void opGetCombatDifficulty(Program* program)
{
    programStackPushInteger(program, settings.preferences.combat_difficulty);
}

// obj_on_screen
// 0x45C878
static void opObjectOnScreen(Program* program)
{
    Rect rect;
    rectCopy(&rect, &stru_453FC0);

    Object* object = static_cast<Object*>(programStackPopPointer(program));

    int result = 0;

    if (object != nullptr) {
        if (gElevation == object->elevation) {
            Rect objectRect;
            objectGetRect(object, &objectRect);

            if (rectIntersection(&objectRect, &rect, &objectRect) == 0) {
                result = 1;
            }
        }
    } else {
        scriptPredefinedError(program, "obj_on_screen", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, result);
}

// critter_is_fleeing
// 0x45C93C
static void opCritterIsFleeing(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    bool fleeing = false;
    if (obj != nullptr) {
        fleeing = (obj->data.critter.combat.maneuver & CRITTER_MANUEVER_FLEEING) != 0;
    } else {
        scriptPredefinedError(program, "critter_is_fleeing", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushInteger(program, fleeing ? 1 : 0);
}

// critter_set_flee_state
// 0x45C9DC
static void opCritterSetFleeState(Program* program)
{
    int fleeing = programStackPopInteger(program);
    Object* object = static_cast<Object*>(programStackPopPointer(program));

    if (object != nullptr) {
        if (fleeing != 0) {
            object->data.critter.combat.maneuver |= CRITTER_MANUEVER_FLEEING;
        } else {
            object->data.critter.combat.maneuver &= ~CRITTER_MANUEVER_FLEEING;
        }
    } else {
        scriptPredefinedError(program, "critter_set_flee_state", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// terminate_combat
// 0x45CA84
static void opTerminateCombat(Program* program)
{
    if (isInCombat()) {
        _game_user_wants_to_quit = 1;
        Object* self = scriptGetSelf(program);
        if (self != nullptr) {
            if (PID_TYPE(self->pid) == OBJ_TYPE_CRITTER) {
                self->data.critter.combat.maneuver |= CRITTER_MANEUVER_DISENGAGING;
                self->data.critter.combat.whoHitMe = nullptr;
                aiInfoSetLastTarget(self, nullptr);
            }
        }
    }
}

// debug_msg
// 0x45CAC8
static void opDebugMessage(Program* program)
{
    char* string = programStackPopString(program);

    if (string != nullptr) {
        if (settings.debug.show_script_messages) {
            debugPrint("\n");
            debugPrint(string);
        }
    }
}

// critter_stop_attacking
// 0x45CB70
static void opCritterStopAttacking(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));

    if (obj != nullptr) {
        obj->data.critter.combat.maneuver |= CRITTER_MANEUVER_DISENGAGING;
        obj->data.critter.combat.whoHitMe = nullptr;
        aiInfoSetLastTarget(obj, nullptr);
    } else {
        scriptPredefinedError(program, "critter_stop_attacking", SCRIPT_ERROR_OBJECT_IS_NULL);
    }
}

// tile_contains_pid_obj
// 0x45CBF8
static void opTileGetObjectWithPid(Program* program)
{
    int pid = programStackPopInteger(program);
    int elevation = programStackPopInteger(program);
    int tile = programStackPopInteger(program);
    Object* found = nullptr;

    if (tile != -1) {
        Object* object = objectFindFirstAtLocation(elevation, tile);
        while (object != nullptr) {
            if (object->pid == pid) {
                found = object;
                break;
            }
            object = objectFindNextAtLocation();
        }
    }

    programStackPushPointer(program, found);
}

// obj_name
// 0x45CCC8
static void opGetObjectName(Program* program)
{
    Object* obj = static_cast<Object*>(programStackPopPointer(program));
    if (obj != nullptr) {
        _strName = objectGetName(obj);
    } else {
        scriptPredefinedError(program, "obj_name", SCRIPT_ERROR_OBJECT_IS_NULL);
    }

    programStackPushString(program, _strName);
}

// get_pc_stat
// 0x45CD64
static void opGetPcStat(Program* program)
{
    int data = programStackPopInteger(program);

    int value = pcGetStat(data);
    programStackPushInteger(program, value);
}

// 0x45CDD4
void _intExtraClose_()
{
    sfallOpcodesExit();
}

// 0x45CDD8
void _initIntExtra()
{
    interpreterRegisterOpcode(0x80A1, opGiveExpPoints); // op_give_exp_points
    interpreterRegisterOpcode(0x80A2, opScrReturn); // op_scr_return
    interpreterRegisterOpcode(0x80A3, opPlaySfx); // op_play_sfx
    interpreterRegisterOpcode(0x80A4, opGetObjectName); // op_obj_name
    interpreterRegisterOpcode(0x80A5, opSfxBuildOpenName); // op_sfx_build_open_name
    interpreterRegisterOpcode(0x80A6, opGetPcStat); // op_get_pc_stat
    interpreterRegisterOpcode(0x80A7, opTileGetObjectWithPid); // op_tile_contains_pid_obj
    interpreterRegisterOpcode(0x80A8, opSetMapStart); // op_set_map_start
    interpreterRegisterOpcode(0x80A9, opOverrideMapStart); // op_override_map_start
    interpreterRegisterOpcode(0x80AA, opHasSkill); // op_has_skill
    interpreterRegisterOpcode(0x80AB, opUsingSkill); // op_using_skill
    interpreterRegisterOpcode(0x80AC, opRollVsSkill); // op_roll_vs_skill
    interpreterRegisterOpcode(0x80AD, opSkillContest); // op_skill_contest
    interpreterRegisterOpcode(0x80AE, opDoCheck); // op_do_check
    interpreterRegisterOpcode(0x80AF, opSuccess); // op_success
    interpreterRegisterOpcode(0x80B0, opCritical); // op_critical
    interpreterRegisterOpcode(0x80B1, opHowMuch); // op_how_much
    interpreterRegisterOpcode(0x80B2, opMarkAreaKnown); // op_mark_area_known
    interpreterRegisterOpcode(0x80B3, opReactionInfluence); // op_reaction_influence
    interpreterRegisterOpcode(0x80B4, opRandom); // op_random
    interpreterRegisterOpcode(0x80B5, opRollDice); // op_roll_dice
    interpreterRegisterOpcode(0x80B6, opMoveTo); // op_move_to
    interpreterRegisterOpcode(0x80B7, opCreateObject); // op_create_object
    interpreterRegisterOpcode(0x80B8, opDisplayMsg); // op_display_msg
    interpreterRegisterOpcode(0x80B9, opScriptOverrides); // op_script_overrides
    interpreterRegisterOpcode(0x80BA, opObjectIsCarryingObjectWithPid); // op_obj_is_carrying_obj
    interpreterRegisterOpcode(0x80BB, opTileContainsObjectWithPid); // op_tile_contains_obj_pid
    interpreterRegisterOpcode(0x80BC, opGetSelf); // op_self_obj
    interpreterRegisterOpcode(0x80BD, opGetSource); // op_source_obj
    interpreterRegisterOpcode(0x80BE, opGetTarget); // op_target_obj
    interpreterRegisterOpcode(0x80BF, opGetDude);
    interpreterRegisterOpcode(0x80C0, opGetObjectBeingUsed); // op_obj_being_used_with
    interpreterRegisterOpcode(0x80C1, opGetLocalVar); // op_get_local_var
    interpreterRegisterOpcode(0x80C2, opSetLocalVar); // op_set_local_var
    interpreterRegisterOpcode(0x80C3, opGetMapVar); // op_get_map_var
    interpreterRegisterOpcode(0x80C4, opSetMapVar); // op_set_map_var
    interpreterRegisterOpcode(0x80C5, opGetGlobalVar); // op_get_global_var
    interpreterRegisterOpcode(0x80C6, opSetGlobalVar); // op_set_global_var
    interpreterRegisterOpcode(0x80C7, opGetScriptAction); // op_script_action
    interpreterRegisterOpcode(0x80C8, opGetObjectType); // op_obj_type
    interpreterRegisterOpcode(0x80C9, opGetItemType); // op_item_subtype
    interpreterRegisterOpcode(0x80CA, opGetCritterStat); // op_get_critter_stat
    interpreterRegisterOpcode(0x80CB, opSetCritterStat); // op_set_critter_stat
    interpreterRegisterOpcode(0x80CC, opAnimateStand); // op_animate_stand_obj
    interpreterRegisterOpcode(0x80CD, opAnimateStandReverse); // animate_stand_reverse_obj
    interpreterRegisterOpcode(0x80CE, opAnimateMoveObjectToTile); // animate_move_obj_to_tile
    interpreterRegisterOpcode(0x80CF, opTileInTileRect); // tile_in_tile_rect
    interpreterRegisterOpcode(0x80D0, opAttackComplex); // op_attack
    interpreterRegisterOpcode(0x80D1, opMakeDayTime); // op_make_daytime
    interpreterRegisterOpcode(0x80D2, opTileDistanceBetween); // op_tile_distance
    interpreterRegisterOpcode(0x80D3, opTileDistanceBetweenObjects); // op_tile_distance_objs
    interpreterRegisterOpcode(0x80D4, opGetObjectTile); // op_tile_num
    interpreterRegisterOpcode(0x80D5, opGetTileInDirection); // op_tile_num_in_direction
    interpreterRegisterOpcode(0x80D6, opPickup); // op_pickup_obj
    interpreterRegisterOpcode(0x80D7, opDrop); // op_drop_obj
    interpreterRegisterOpcode(0x80D8, opAddObjectToInventory); // op_add_obj_to_inven
    interpreterRegisterOpcode(0x80D9, opRemoveObjectFromInventory); // op_rm_obj_from_inven
    interpreterRegisterOpcode(0x80DA, opWieldItem); // op_wield_obj_critter
    interpreterRegisterOpcode(0x80DB, opUseObject); // op_use_obj
    interpreterRegisterOpcode(0x80DC, opObjectCanSeeObject); // op_obj_can_see_obj
    interpreterRegisterOpcode(0x80DD, opAttackComplex); // op_attack
    interpreterRegisterOpcode(0x80DE, opStartGameDialog); // op_start_gdialog
    interpreterRegisterOpcode(0x80DF, opEndGameDialog); // op_end_gdialog
    interpreterRegisterOpcode(0x80E0, opGameDialogReaction); // op_dialogue_reaction
    interpreterRegisterOpcode(0x80E1, opMetarule3); // op_metarule3
    interpreterRegisterOpcode(0x80E2, opSetMapMusic); // op_set_map_music
    interpreterRegisterOpcode(0x80E3, opSetObjectVisibility); // op_set_obj_visibility
    interpreterRegisterOpcode(0x80E4, opLoadMap); // op_load_map
    interpreterRegisterOpcode(0x80E5, opWorldmapCitySetPos); // op_wm_area_set_pos
    interpreterRegisterOpcode(0x80E6, opSetExitGrids); // op_set_exit_grids
    interpreterRegisterOpcode(0x80E7, opAnimBusy); // op_anim_busy
    interpreterRegisterOpcode(0x80E8, opCritterHeal); // op_critter_heal
    interpreterRegisterOpcode(0x80E9, opSetLightLevel); // op_set_light_level
    interpreterRegisterOpcode(0x80EA, opGetGameTime); // op_game_time
    interpreterRegisterOpcode(0x80EB, opGetGameTimeInSeconds); // op_game_time / 10
    interpreterRegisterOpcode(0x80EC, opGetObjectElevation); // op_elevation
    interpreterRegisterOpcode(0x80ED, opKillCritter); // op_kill_critter
    interpreterRegisterOpcode(0x80EE, opKillCritterType); // op_kill_critter_type
    interpreterRegisterOpcode(0x80EF, opCritterDamage); // op_critter_damage
    interpreterRegisterOpcode(0x80F0, opAddTimerEvent); // op_add_timer_event
    interpreterRegisterOpcode(0x80F1, opRemoveTimerEvent); // op_rm_timer_event
    interpreterRegisterOpcode(0x80F2, opGameTicks); // op_game_ticks
    interpreterRegisterOpcode(0x80F3, opHasTrait); // op_has_trait
    interpreterRegisterOpcode(0x80F4, opDestroyObject); // op_destroy_object
    interpreterRegisterOpcode(0x80F5, opObjectCanHearObject); // op_obj_can_hear_obj
    interpreterRegisterOpcode(0x80F6, opGameTimeHour); // op_game_time_hour
    interpreterRegisterOpcode(0x80F7, opGetFixedParam); // op_fixed_param
    interpreterRegisterOpcode(0x80F8, opTileIsVisible); // op_tile_is_visible
    interpreterRegisterOpcode(0x80F9, opGameDialogSystemEnter); // op_dialogue_system_enter
    interpreterRegisterOpcode(0x80FA, opGetActionBeingUsed); // op_action_being_used
    interpreterRegisterOpcode(0x80FB, opGetCritterState); // critter_state
    interpreterRegisterOpcode(0x80FC, opGameTimeAdvance); // op_game_time_advance
    interpreterRegisterOpcode(0x80FD, opRadiationIncrease); // op_radiation_inc
    interpreterRegisterOpcode(0x80FE, opRadiationDecrease); // op_radiation_dec
    interpreterRegisterOpcode(0x80FF, opCritterAttemptPlacement); // critter_attempt_placement
    interpreterRegisterOpcode(0x8100, opGetObjectPid); // op_obj_pid
    interpreterRegisterOpcode(0x8101, opGetCurrentMap); // op_cur_map_index
    interpreterRegisterOpcode(0x8102, opCritterAddTrait); // op_critter_add_trait
    interpreterRegisterOpcode(0x8103, opCritterRemoveTrait); // critter_rm_trait
    interpreterRegisterOpcode(0x8104, opGetProtoData); // op_proto_data
    interpreterRegisterOpcode(0x8105, opGetMessageString); // op_message_str
    interpreterRegisterOpcode(0x8106, opCritterGetInventoryObject); // op_critter_inven_obj
    interpreterRegisterOpcode(0x8107, opSetObjectLightLevel); // op_obj_set_light_level
    interpreterRegisterOpcode(0x8108, opWorldmap); // op_scripts_request_world_map
    interpreterRegisterOpcode(0x8109, _op_inven_cmds); // op_inven_cmds
    interpreterRegisterOpcode(0x810A, opFloatMessage); // op_float_msg
    interpreterRegisterOpcode(0x810B, opMetarule); // op_metarule
    interpreterRegisterOpcode(0x810C, opAnim); // op_anim
    interpreterRegisterOpcode(0x810D, opObjectCarryingObjectByPid); // op_obj_carrying_pid_obj
    interpreterRegisterOpcode(0x810E, opRegAnimFunc); // op_reg_anim_func
    interpreterRegisterOpcode(0x810F, opRegAnimAnimate); // op_reg_anim_animate
    interpreterRegisterOpcode(0x8110, opRegAnimAnimateReverse); // op_reg_anim_animate_reverse
    interpreterRegisterOpcode(0x8111, opRegAnimObjectMoveToObject); // op_reg_anim_obj_move_to_obj
    interpreterRegisterOpcode(0x8112, opRegAnimObjectRunToObject); // op_reg_anim_obj_run_to_obj
    interpreterRegisterOpcode(0x8113, opRegAnimObjectMoveToTile); // op_reg_anim_obj_move_to_tile
    interpreterRegisterOpcode(0x8114, opRegAnimObjectRunToTile); // op_reg_anim_obj_run_to_tile
    interpreterRegisterOpcode(0x8115, opPlayGameMovie); // op_play_gmovie
    interpreterRegisterOpcode(0x8116, opAddMultipleObjectsToInventory); // op_add_mult_objs_to_inven
    interpreterRegisterOpcode(0x8117, opRemoveMultipleObjectsFromInventory); // rm_mult_objs_from_inven
    interpreterRegisterOpcode(0x8118, opGetMonth); // op_month
    interpreterRegisterOpcode(0x8119, opGetDay); // op_day
    interpreterRegisterOpcode(0x811A, opExplosion); // op_explosion
    interpreterRegisterOpcode(0x811B, opGetDaysSinceLastVisit); // op_days_since_visited
    interpreterRegisterOpcode(0x811C, _op_gsay_start);
    interpreterRegisterOpcode(0x811D, _op_gsay_end);
    interpreterRegisterOpcode(0x811E, _op_gsay_reply); // op_gsay_reply
    interpreterRegisterOpcode(0x811F, _op_gsay_option); // op_gsay_option
    interpreterRegisterOpcode(0x8120, _op_gsay_message); // op_gsay_message
    interpreterRegisterOpcode(0x8121, _op_giq_option); // op_giq_option
    interpreterRegisterOpcode(0x8122, opPoison); // op_poison
    interpreterRegisterOpcode(0x8123, opGetPoison); // op_get_poison
    interpreterRegisterOpcode(0x8124, opPartyAdd); // op_party_add
    interpreterRegisterOpcode(0x8125, opPartyRemove); // op_party_remove
    interpreterRegisterOpcode(0x8126, opRegAnimAnimateForever); // op_reg_anim_animate_forever
    interpreterRegisterOpcode(0x8127, opCritterInjure); // op_critter_injure
    interpreterRegisterOpcode(0x8128, opCombatIsInitialized); // op_is_in_combat
    interpreterRegisterOpcode(0x8129, _op_gdialog_barter); // op_gdialog_barter
    interpreterRegisterOpcode(0x812A, opGetGameDifficulty); // op_game_difficulty
    interpreterRegisterOpcode(0x812B, opGetRunningBurningGuy); // op_running_burning_guy
    interpreterRegisterOpcode(0x812C, _op_inven_unwield); // op_inven_unwield
    interpreterRegisterOpcode(0x812D, opObjectIsLocked); // op_obj_is_locked
    interpreterRegisterOpcode(0x812E, opObjectLock); // op_obj_lock
    interpreterRegisterOpcode(0x812F, opObjectUnlock); // op_obj_unlock
    interpreterRegisterOpcode(0x8131, opObjectOpen); // op_obj_open
    interpreterRegisterOpcode(0x8130, opObjectIsOpen); // op_obj_is_open
    interpreterRegisterOpcode(0x8132, opObjectClose); // op_obj_close
    interpreterRegisterOpcode(0x8133, opGameUiDisable); // op_game_ui_disable
    interpreterRegisterOpcode(0x8134, opGameUiEnable); // op_game_ui_enable
    interpreterRegisterOpcode(0x8135, opGameUiIsDisabled); // op_game_ui_is_disabled
    interpreterRegisterOpcode(0x8136, opGameFadeOut); // op_gfade_out
    interpreterRegisterOpcode(0x8137, opGameFadeIn); // op_gfade_in
    interpreterRegisterOpcode(0x8138, opItemCapsTotal); // op_item_caps_total
    interpreterRegisterOpcode(0x8139, opItemCapsAdjust); // op_item_caps_adjust
    interpreterRegisterOpcode(0x813A, _op_anim_action_frame); // op_anim_action_frame
    interpreterRegisterOpcode(0x813B, opRegAnimPlaySfx); // op_reg_anim_play_sfx
    interpreterRegisterOpcode(0x813C, opCritterModifySkill); // op_critter_mod_skill
    interpreterRegisterOpcode(0x813D, opSfxBuildCharName); // op_sfx_build_char_name
    interpreterRegisterOpcode(0x813E, opSfxBuildAmbientName); // op_sfx_build_ambient_name
    interpreterRegisterOpcode(0x813F, opSfxBuildInterfaceName); // op_sfx_build_interface_name
    interpreterRegisterOpcode(0x8140, opSfxBuildItemName); // op_sfx_build_item_name
    interpreterRegisterOpcode(0x8141, opSfxBuildWeaponName); // op_sfx_build_weapon_name
    interpreterRegisterOpcode(0x8142, opSfxBuildSceneryName); // op_sfx_build_scenery_name
    interpreterRegisterOpcode(0x8143, opAttackSetup); // op_attack_setup
    interpreterRegisterOpcode(0x8144, opDestroyMultipleObjects); // op_destroy_mult_objs
    interpreterRegisterOpcode(0x8145, opUseObjectOnObject); // op_use_obj_on_obj
    interpreterRegisterOpcode(0x8146, opEndgameSlideshow); // op_endgame_slideshow
    interpreterRegisterOpcode(0x8147, opMoveObjectInventoryToObject); // op_move_obj_inven_to_obj
    interpreterRegisterOpcode(0x8148, opEndgameMovie); // op_endgame_movie
    interpreterRegisterOpcode(0x8149, opGetObjectFid); // op_obj_art_fid
    interpreterRegisterOpcode(0x814A, opGetFidAnim); // op_art_anim
    interpreterRegisterOpcode(0x814B, opGetPartyMember); // op_party_member_obj
    interpreterRegisterOpcode(0x814C, opGetRotationToTile); // op_rotation_to_tile
    interpreterRegisterOpcode(0x814D, opJamLock); // op_jam_lock
    interpreterRegisterOpcode(0x814E, opGameDialogSetBarterMod); // op_gdialog_set_barter_mod
    interpreterRegisterOpcode(0x814F, opGetCombatDifficulty); // op_combat_difficulty
    interpreterRegisterOpcode(0x8150, opObjectOnScreen); // op_obj_on_screen
    interpreterRegisterOpcode(0x8151, opCritterIsFleeing); // op_critter_is_fleeing
    interpreterRegisterOpcode(0x8152, opCritterSetFleeState); // op_critter_set_flee_state
    interpreterRegisterOpcode(0x8153, opTerminateCombat); // op_terminate_combat
    interpreterRegisterOpcode(0x8154, opDebugMessage); // op_debug_msg
    interpreterRegisterOpcode(0x8155, opCritterStopAttacking); // op_critter_stop_attacking

    sfallOpcodesInit();
}

// NOTE: Uncollapsed 0x45D878.
//
// 0x45D878
void intExtraUpdate()
{
}

// NOTE: Uncollapsed 0x45D878.
//
// 0x45D878
void intExtraRemoveProgramReferences(Program* program)
{
}

} // namespace fallout
