#include "game_dialog.h"

#include <assert.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 "cycle.h"
#include "debug.h"
#include "delay.h"
#include "dialog.h"
#include "display_monitor.h"
#include "draw.h"
#include "game.h"
#include "game_mouse.h"
#include "game_sound.h"
#include "input.h"
#include "interface.h"
#include "item.h"
#include "kb.h"
#include "lips.h"
#include "memory.h"
#include "mouse.h"
#include "object.h"
#include "party_member.h"
#include "perk.h"
#include "proto.h"
#include "random.h"
#include "scripts.h"
#include "sfall_config.h"
#include "skill.h"
#include "stat.h"
#include "svga.h"
#include "text_font.h"
#include "text_object.h"
#include "tile.h"
#include "window_manager.h"

namespace fallout {

#define DIALOG_REVIEW_ENTRIES_CAPACITY 80

#define DIALOG_OPTION_ENTRIES_CAPACITY 30

#define GAME_DIALOG_WINDOW_WIDTH 640
#define GAME_DIALOG_WINDOW_HEIGHT 480

#define GAME_DIALOG_REPLY_WINDOW_X 135
#define GAME_DIALOG_REPLY_WINDOW_Y 225
#define GAME_DIALOG_REPLY_WINDOW_WIDTH 379
#define GAME_DIALOG_REPLY_WINDOW_HEIGHT 58

#define GAME_DIALOG_OPTIONS_WINDOW_X 127
#define GAME_DIALOG_OPTIONS_WINDOW_Y 335
#define GAME_DIALOG_OPTIONS_WINDOW_WIDTH 393
#define GAME_DIALOG_OPTIONS_WINDOW_HEIGHT 117

#define GAME_DIALOG_REVIEW_WINDOW_WIDTH 640
#define GAME_DIALOG_REVIEW_WINDOW_HEIGHT 480

typedef enum GameDialogReviewWindowButton {
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_SCROLL_UP,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_SCROLL_DOWN,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_DONE,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_COUNT,
} GameDialogReviewWindowButton;

typedef enum GameDialogReviewWindowButtonFrm {
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_UP_NORMAL,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_UP_PRESSED,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_DOWN_NORMAL,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_DOWN_PRESSED,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_DONE_NORMAL,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_DONE_PRESSED,
    GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_COUNT,
} GameDialogReviewWindowButtonFrm;

typedef enum GameDialogReaction {
    GAME_DIALOG_REACTION_GOOD = 49,
    GAME_DIALOG_REACTION_NEUTRAL = 50,
    GAME_DIALOG_REACTION_BAD = 51,
} GameDialogReaction;

typedef struct GameDialogReviewEntry {
    int replyMessageListId;
    int replyMessageId;
    // Can be NULL.
    char* replyText;
    int optionMessageListId;
    int optionMessageId;
    char* optionText;
} GameDialogReviewEntry;

typedef struct GameDialogOptionEntry {
    int messageListId;
    int messageId;
    int reaction;
    int proc;
    int btn;
    int field_14;
    char text[900];
    int field_39C;
} GameDialogOptionEntry;

// Provides button configuration for party member combat control and
// customization interface.
typedef struct GameDialogButtonData {
    int x;
    int y;
    int upFrmId;
    int downFrmId;
    int disabledFrmId;
    CacheEntry* upFrmHandle;
    CacheEntry* downFrmHandle;
    CacheEntry* disabledFrmHandle;
    int keyCode;
    int value;
} GameDialogButtonData;

typedef struct STRUCT_5189E4 {
    int messageId;
    int value;
} STRUCT_5189E4;

typedef enum PartyMemberCustomizationOption {
    PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE,
    PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE,
    PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON,
    PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE,
    PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO,
    PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE,
    PARTY_MEMBER_CUSTOMIZATION_OPTION_COUNT,
} PartyMemberCustomizationOption;

// 0x444D10
static int _Dogs[3] = {
    0x1000088,
    0x1000156,
    0x1000180,
};

// 0x5186D4
static int _dialog_state_fix = 0;

// 0x5186D8
static int gGameDialogOptionEntriesLength = 0;

// 0x5186DC
static int gGameDialogReviewEntriesLength = 0;

// 0x5186E0
static unsigned char* gGameDialogDisplayBuffer = nullptr;

// 0x5186E4
static int gGameDialogReplyWindow = -1;

// 0x5186E8
static int gGameDialogOptionsWindow = -1;

// 0x5186EC
static bool _gdialog_window_created = false;

// 0x5186F0
static int _boxesWereDisabled = 0;

// 0x5186F4
static int gGameDialogFidgetFid = 0;

// 0x5186F8
static CacheEntry* gGameDialogFidgetFrmHandle = nullptr;

// 0x5186FC
static Art* gGameDialogFidgetFrm = nullptr;

// 0x518700
static int gGameDialogBackground = 2;

// 0x518704
static int _lipsFID = 0;

// 0x518708
static CacheEntry* _lipsKey = nullptr;

// 0x51870C
static Art* _lipsFp = nullptr;

// 0x518710
static bool gGameDialogLipSyncStarted = false;

// 0x518714
static int _dialogue_state = 0;

// 0x518718
static int _dialogue_switch_mode = 0;

// 0x51871C
static int _gdialog_state = -1;

// 0x518720
static bool _gdDialogWentOff = false;

// 0x518724
static bool _gdDialogTurnMouseOff = false;

// 0x518728
static int _gdReenterLevel = 0;

// 0x51872C
static bool _gdReplyTooBig = false;

// 0x518730
static Object* _peon_table_obj = nullptr;

// 0x518734
static Object* _barterer_table_obj = nullptr;

// 0x518738
static Object* _barterer_temp_obj = nullptr;

// 0x51873C
static int gGameDialogBarterModifier = 0;

// dialogueBackWindow
// 0x518740
static int gGameDialogBackgroundWindow = -1;

// 0x518744
static int gGameDialogWindow = -1;

// 0x518748
static Rect _backgrndRects[8] = {
    { 126, 14, 152, 40 },
    { 488, 14, 514, 40 },
    { 126, 188, 152, 214 },
    { 488, 188, 514, 214 },
    { 152, 14, 488, 24 },
    { 152, 204, 488, 214 },
    { 126, 40, 136, 188 },
    { 504, 40, 514, 188 },
};

// 0x5187C8
static int _talk_need_to_center = 1;

// 0x5187CC
static bool _can_start_new_fidget = false;

// 0x5187D0
static int _gd_replyWin = -1;

// 0x5187D4
static int _gd_optionsWin = -1;

// 0x5187D8
static int gGameDialogOldMusicVolume = -1;

// 0x5187DC
static int gGameDialogOldCenterTile = -1;

// 0x5187E0
static int gGameDialogOldDudeTile = -1;

// 0x5187E4
static unsigned char* _light_BlendTable = nullptr;

// 0x5187E8
static unsigned char* _dark_BlendTable = nullptr;

// 0x5187EC
static int _dialogue_just_started = 0;

// 0x5187F0
static int _dialogue_seconds_since_last_input = 0;

// 0x518818
static const int gGameDialogReviewWindowButtonWidths[GAME_DIALOG_REVIEW_WINDOW_BUTTON_COUNT] = {
    35,
    35,
    82,
};

// 0x518824
static const int gGameDialogReviewWindowButtonHeights[GAME_DIALOG_REVIEW_WINDOW_BUTTON_COUNT] = {
    35,
    37,
    46,
};

// 0x518830
static int gGameDialogReviewWindowButtonFrmIds[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_COUNT] = {
    89, // di_bgdn1.frm - dialog big down arrow
    90, // di_bgdn2.frm - dialog big down arrow
    87, // di_bgup1.frm - dialog big up arrow
    88, // di_bgup2.frm - dialog big up arrow
    91, // di_done1.frm - dialog big done button up
    92, // di_done2.frm - dialog big done button down
};

// 0x518848
Object* gGameDialogSpeaker = nullptr;

// 0x51884C
bool gGameDialogSpeakerIsPartyMember = false;

// 0x518850
int gGameDialogHeadFid = 0;

// 0x518854
int gGameDialogSid = -1;

// Maps phoneme to talking head frame.
//
// 0x518858
static int _head_phoneme_lookup[PHONEME_COUNT] = {
    0,
    3,
    1,
    1,
    3,
    1,
    1,
    1,
    7,
    8,
    7,
    3,
    1,
    8,
    1,
    7,
    7,
    6,
    6,
    2,
    2,
    2,
    2,
    4,
    4,
    5,
    5,
    2,
    2,
    2,
    2,
    2,
    6,
    2,
    2,
    5,
    8,
    2,
    2,
    2,
    2,
    8,
};

// 0x518900
static int _phone_anim = 0;

// 0x518904
static int _loop_cnt = -1;

// 0x518908
static unsigned int _tocksWaiting = 10000;

// 0x51890C
static const char* _react_strs[3] = {
    "Said Good",
    "Said Neutral",
    "Said Bad",
};

// 0x518918
static int _dialogue_subwin_len = 0;

// 0x51891C
static GameDialogButtonData gGameDialogDispositionButtonsData[5] = {
    { 438, 37, 397, 395, 396, nullptr, nullptr, nullptr, 2098, 4 },
    { 438, 67, 394, 392, 393, nullptr, nullptr, nullptr, 2103, 3 },
    { 438, 96, 406, 404, 405, nullptr, nullptr, nullptr, 2102, 2 },
    { 438, 126, 400, 398, 399, nullptr, nullptr, nullptr, 2111, 1 },
    { 438, 156, 403, 401, 402, nullptr, nullptr, nullptr, 2099, 0 },
};

// 0x5189E4
static STRUCT_5189E4 _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_COUNT][6] = {
    {
        { 100, AREA_ATTACK_MODE_ALWAYS }, // Always!
        { 101, AREA_ATTACK_MODE_SOMETIMES }, // Sometimes, don't worry about hitting me
        { 102, AREA_ATTACK_MODE_BE_SURE }, // Be sure you won't hit me
        { 103, AREA_ATTACK_MODE_BE_CAREFUL }, // Be careful not to hit me
        { 104, AREA_ATTACK_MODE_BE_ABSOLUTELY_SURE }, // Be absolutely sure you won't hit me
        { -1, 0 },
    },
    {
        { 200, RUN_AWAY_MODE_COWARD - 1 }, // Abject coward
        { 201, RUN_AWAY_MODE_FINGER_HURTS - 1 }, // Your finger hurts
        { 202, RUN_AWAY_MODE_BLEEDING - 1 }, // You're bleeding a bit
        { 203, RUN_AWAY_MODE_NOT_FEELING_GOOD - 1 }, // Not feeling good
        { 204, RUN_AWAY_MODE_TOURNIQUET - 1 }, // You need a tourniquet
        { 205, RUN_AWAY_MODE_NEVER - 1 }, // Never!
    },
    {
        { 300, BEST_WEAPON_NO_PREF }, // None
        { 301, BEST_WEAPON_MELEE }, // Melee
        { 302, BEST_WEAPON_MELEE_OVER_RANGED }, // Melee then ranged
        { 303, BEST_WEAPON_RANGED_OVER_MELEE }, // Ranged then melee
        { 304, BEST_WEAPON_RANGED }, // Ranged
        { 305, BEST_WEAPON_UNARMED }, // Unarmed
    },
    {
        { 400, DISTANCE_STAY_CLOSE }, // Stay close to me
        { 401, DISTANCE_CHARGE }, // Charge!
        { 402, DISTANCE_SNIPE }, // Snipe the enemy
        { 403, DISTANCE_ON_YOUR_OWN }, // On your own
        { 404, DISTANCE_STAY }, // Say where you are
        { -1, 0 },
    },
    {
        { 500, ATTACK_WHO_WHOMEVER_ATTACKING_ME }, // Whomever is attacking me
        { 501, ATTACK_WHO_STRONGEST }, // The strongest
        { 502, ATTACK_WHO_WEAKEST }, // The weakest
        { 503, ATTACK_WHO_WHOMEVER }, // Whomever you want
        { 504, ATTACK_WHO_CLOSEST }, // Whoever is closest
        { -1, 0 },
    },
    {
        { 600, CHEM_USE_CLEAN }, // I'm clean
        { 601, CHEM_USE_STIMS_WHEN_HURT_LITTLE }, // Stimpacks when hurt a bit
        { 602, CHEM_USE_STIMS_WHEN_HURT_LOTS }, // Stimpacks when hurt a lot
        { 603, CHEM_USE_SOMETIMES }, // Any drug some of the time
        { 604, CHEM_USE_ANYTIME }, // Any drug any time
        { -1, 0 },
    },
};

// 0x518B04
static GameDialogButtonData _custom_button_info[PARTY_MEMBER_CUSTOMIZATION_OPTION_COUNT] = {
    { 95, 9, 410, 409, -1, nullptr, nullptr, nullptr, 0, 0 },
    { 96, 38, 416, 415, -1, nullptr, nullptr, nullptr, 1, 0 },
    { 96, 68, 418, 417, -1, nullptr, nullptr, nullptr, 2, 0 },
    { 96, 98, 414, 413, -1, nullptr, nullptr, nullptr, 3, 0 },
    { 96, 127, 408, 407, -1, nullptr, nullptr, nullptr, 4, 0 },
    { 96, 157, 412, 411, -1, nullptr, nullptr, nullptr, 5, 0 },
};

// 0x518BF4
static int _totalHotx = 0;

// 0x58EA80
static int _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_COUNT];

// custom.msg
//
// 0x58EA98
static MessageList gCustomMessageList;

// 0x58EAA0
static unsigned char _light_GrayTable[256];

// 0x58EBA0
static unsigned char _dark_GrayTable[256];

// 0x58ECA0
static unsigned char* _backgrndBufs[8];

// 0x58ECC0
static Rect _optionRect;

// 0x58ECD0
static Rect _replyRect;

// 0x58ECE0
static GameDialogReviewEntry gDialogReviewEntries[DIALOG_REVIEW_ENTRIES_CAPACITY];

// 0x58F460
static int _custom_buttons_start;

// 0x58F464
static int _control_buttons_start;

// 0x58F468
static int gGameDialogReviewWindowOldFont;

// 0x58F470
static int _gdialog_buttons[9];

// 0x58F4C8
static int _oldFont;

// 0x58F4CC
static unsigned int gGameDialogFidgetLastUpdateTimestamp;

// 0x58F4D0
static int gGameDialogFidgetReaction;

// 0x58F4D4
static Program* gDialogReplyProgram;

// 0x58F4D8
static int gDialogReplyMessageListId;

// 0x58F4DC
static int gDialogReplyMessageId;

// 0x58F4E0
static int dword_58F4E0;

// NOTE: The is something odd about this variable. There are 2700 bytes, which
// is 3 x 900, but anywhere in the app only 900 characters is used. The length
// of text in [DialogOptionEntry] is definitely 900 bytes. There are two
// possible explanations:
// - it's an array of 3 elements
// - there are three separate elements, two of which are not used, therefore
// they are not referenced anywhere, but they take up their space.
//
// See `_gdProcessChoice` for more info how this unreferenced range plays
// important role.
//
// 0x58F4E4
static char gDialogReplyText[900];

// 0x58FF70
static GameDialogOptionEntry gDialogOptionEntries[DIALOG_OPTION_ENTRIES_CAPACITY];

// 0x596C30
static int _talkOldFont;

// 0x596C34
static unsigned int gGameDialogFidgetUpdateDelay;

// 0x596C38
static int gGameDialogFidgetFrmCurrentFrame;

static FrmImage _reviewBackgroundFrmImage;
static FrmImage _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_COUNT];
static FrmImage _reviewButtonNormalFrmImage;
static FrmImage _reviewButtonPressedFrmImage;
static FrmImage _redButtonNormalFrmImage;
static FrmImage _redButtonPressedFrmImage;
static FrmImage _lowerHighlightFrmImage;
static FrmImage _upperHighlightFrmImage;

static int _gdialogReset();
static void gameDialogEndLips();
static int gdHide();
static int gdUnhide();
static int gameDialogAddMessageOption(int a1, int a2, int a3);
static int gameDialogAddTextOption(int a1, const char* a2, int a3);
static int gameDialogReviewWindowInit(int* win);
static int gameDialogReviewWindowFree(int* win);
static int gameDialogShowReview();
static void gameDialogReviewButtonOnMouseUp(int btn, int keyCode);
static void gameDialogReviewWindowUpdate(int win, int origin);
static void dialogReviewEntriesClear();
static int gameDialogAddReviewMessage(int messageListId, int messageId);
static int gameDialogAddReviewText(const char* text);
static int gameDialogSetReviewOptionMessage(int messageListId, int messageId);
static int gameDialogSetReviewOptionText(const char* text);
static int _gdProcessInit();
static void _gdProcessCleanup();
static int _gdProcessExit();
static void gameDialogRenderCaps();
static int _gdProcess();
static int _gdProcessChoice(int a1);
static void gameDialogOptionOnMouseEnter(int a1);
static void gameDialogOptionOnMouseExit(int a1);
static void gameDialogRenderReply();
static void _gdProcessUpdate();
static int _gdCreateHeadWindow();
static void _gdDestroyHeadWindow();
static void _gdSetupFidget(int headFid, int reaction);
static void gameDialogWaitForFidgetToComplete();
static void _gdPlayTransition(int a1);
static void _reply_arrow_up(int btn, int a2);
static void _reply_arrow_down(int btn, int a2);
static void _reply_arrow_restore(int btn, int a2);
static void _demo_copy_title(int win);
static void _demo_copy_options(int win);
static void _gDialogRefreshOptionsRect(int win, Rect* drawRect);
static void gameDialogTicker();
static void _gdialog_scroll_subwin(int a1, int a2, unsigned char* a3, unsigned char* a4, unsigned char* a5, int a6, int a7);
static int _text_num_lines(const char* a1, int a2);
static int text_to_rect_wrapped(unsigned char* buffer, Rect* rect, char* string, int* a4, int height, int pitch, int color);
static int gameDialogDrawText(unsigned char* buffer, Rect* rect, char* string, int* a4, int height, int pitch, int color, int a7);
static int _gdialog_barter_create_win();
static void _gdialog_barter_destroy_win();
static void _gdialog_barter_cleanup_tables();
static int partyMemberControlWindowInit();
static void partyMemberControlWindowFree();
static void partyMemberControlWindowUpdate();
static void gameDialogCombatControlButtonOnMouseUp(int a1, int a2);
static int _gdPickAIUpdateMsg(Object* obj);
static int _gdCanBarter();
static void partyMemberControlWindowHandleEvents();
static int partyMemberCustomizationWindowInit();
static void partyMemberCustomizationWindowFree();
static void partyMemberCustomizationWindowHandleEvents();
static void partyMemberCustomizationWindowUpdate();
static void _gdCustomSelectRedraw(unsigned char* dest, int pitch, int type, int selectedIndex);
static int _gdCustomSelect(int a1);
static void _gdCustomUpdateSetting(int option, int value);
static void gameDialogBarterButtonUpMouseUp(int btn, int a2);
static int _gdialog_window_create();
static void _gdialog_window_destroy();
static int talk_to_create_background_window();
static int gameDialogWindowRenderBackground();
static int _talkToRefreshDialogWindowRect(Rect* rect);
static void gameDialogRenderHighlight(unsigned char* src, int srcWidth, int srcHeight, int srcPitch, unsigned char* dest, int x, int y, int destPitch, unsigned char* a9, unsigned char* a10);
static void gameDialogRenderTalkingHead(Art* art, int frame);
static void gameDialogHighlightsInit();
static void gameDialogHighlightsExit();

static void gameDialogRedButtonsInit();
static void gameDialogRedButtonsExit();

static bool gGameDialogFix;
static bool gNumberOptions;

// gdialog_init
// 0x444D1C
int gameDialogInit()
{
    // SFALL: Prevents from using 0 to escape from dialogue at any time.
    gGameDialogFix = true;
    configGetBool(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_GAME_DIALOG_FIX_KEY, &gGameDialogFix);

    // SFALL: Use numbers for replies (instead of default knobs).
    gNumberOptions = false;
    configGetBool(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_NUMBERS_IS_DIALOG_KEY, &gNumberOptions);

    return 0;
}

// 0x444D20
int _gdialogReset()
{
    gameDialogEndLips();
    return 0;
}

// NOTE: Uncollapsed 0x444D20.
int gameDialogReset()
{
    return _gdialogReset();
}

// NOTE: Uncollapsed 0x444D20.
int gameDialogExit()
{
    return _gdialogReset();
}

// 0x444D2C
bool _gdialogActive()
{
    return _dialog_state_fix != 0;
}

// gdialogEnter
// 0x444D3C
void gameDialogEnter(Object* speaker, int a2)
{
    if (speaker == nullptr) {
        debugPrint("\nError: gdialogEnter: target was NULL!");
        return;
    }

    _gdDialogWentOff = false;

    if (isInCombat()) {
        return;
    }

    if (speaker->sid == -1) {
        return;
    }

    if (PID_TYPE(speaker->pid) != OBJ_TYPE_ITEM && SID_TYPE(speaker->sid) != SCRIPT_TYPE_SPATIAL) {
        MessageListItem messageListItem;

        int rc = _action_can_talk_to(gDude, speaker);
        if (rc == -1) {
            // You can't see there.
            messageListItem.num = 660;
            if (messageListGetItem(&gProtoMessageList, &messageListItem)) {
                if (a2) {
                    displayMonitorAddMessage(messageListItem.text);
                } else {
                    debugPrint(messageListItem.text);
                }
            } else {
                debugPrint("\nError: gdialog: Can't find message!");
            }
            return;
        }

        if (rc == -2) {
            // Too far away.
            messageListItem.num = 661;
            if (messageListGetItem(&gProtoMessageList, &messageListItem)) {
                if (a2) {
                    displayMonitorAddMessage(messageListItem.text);
                } else {
                    debugPrint(messageListItem.text);
                }
            } else {
                debugPrint("\nError: gdialog: Can't find message!");
            }
            return;
        }
    }

    gGameDialogOldCenterTile = gCenterTile;
    gGameDialogBarterModifier = 0;
    gGameDialogOldDudeTile = gDude->tile;
    isoDisable();

    _dialog_state_fix = 1;
    gGameDialogSpeaker = speaker;
    gGameDialogSpeakerIsPartyMember = objectIsPartyMember(speaker);

    _dialogue_just_started = 1;

    // CE: Obtain and keep SID in a separate variable. This is needed because in
    // rare circumstates the speaker can destroy itself. So after executing it's
    // script |speaker| can point to freed memory. Dereferencing such pointer
    // can lead to crash depending on the environment (confirmed on Android and
    // MSVC debug builds).
    int sid = speaker->sid;
    if (sid != -1) {
        scriptExecProc(speaker->sid, SCRIPT_PROC_TALK);
    }

    Script* script;
    if (scriptGetScript(sid, &script) == -1) {
        gameMouseObjectsShow();
        isoEnable();
        scriptsExecMapUpdateProc();
        _dialog_state_fix = 0;
        return;
    }

    if (script->scriptOverrides || _dialogue_state != 4) {
        _dialogue_just_started = 0;
        isoEnable();
        scriptsExecMapUpdateProc();
        _dialog_state_fix = 0;
        return;
    }

    gameDialogEndLips();

    if (_gdialog_state == 1) {
        // TODO: Not sure about these conditions.
        if (_dialogue_switch_mode == 2) {
            _gdialog_window_destroy();
        } else if (_dialogue_switch_mode == 8) {
            _gdialog_window_destroy();
        } else if (_dialogue_switch_mode == 11) {
            _gdialog_window_destroy();
        } else {
            if (_dialogue_switch_mode == _gdialog_state) {
                _gdialog_barter_destroy_win();
            } else if (_dialogue_state == _gdialog_state) {
                _gdialog_window_destroy();
            } else if (_dialogue_state == a2) {
                _gdialog_barter_destroy_win();
            }
        }
        _gdialogExitFromScript();
    }

    _gdialog_state = 0;
    _dialogue_state = 0;

    int tile = gDude->tile;
    if (gGameDialogOldDudeTile != tile) {
        gGameDialogOldCenterTile = tile;
    }

    if (_gdDialogWentOff) {
        _tile_scroll_to(gGameDialogOldCenterTile, 2);
    }

    isoEnable();
    scriptsExecMapUpdateProc();

    _dialog_state_fix = 0;
}

// 0x444FE4
void _gdialogSystemEnter()
{
    gameUpdateState();

    _gdDialogTurnMouseOff = true;

    soundContinueAll();
    gameDialogEnter(gGameDialogSpeaker, 0);
    soundContinueAll();

    if (gGameDialogOldDudeTile != gDude->tile) {
        gGameDialogOldCenterTile = gDude->tile;
    }

    if (_gdDialogWentOff) {
        _tile_scroll_to(gGameDialogOldCenterTile, 2);
    }

    gameRequestState(GAME_STATE_2);

    gameUpdateState();
}

// 0x445050
void gameDialogStartLips(const char* audioFileName)
{
    if (audioFileName == nullptr) {
        debugPrint("\nGDialog: Bleep!");
        soundPlayFile("censor");
        return;
    }

    char name[16];
    if (artCopyFileName(OBJ_TYPE_HEAD, gGameDialogHeadFid & 0xFFF, name) == -1) {
        return;
    }

    if (lipsLoad(audioFileName, name) == -1) {
        return;
    }

    gGameDialogLipSyncStarted = true;

    lipsStart();

    debugPrint("Starting lipsynch speech");
}

// 0x4450C4
void gameDialogEndLips()
{
    if (gGameDialogLipSyncStarted) {
        debugPrint("Ending lipsynch system");
        gGameDialogLipSyncStarted = false;

        lipsFree();
    }
}

// 0x4450EC
int gameDialogEnable()
{
    tickersAdd(gameDialogTicker);
    return 0;
}

// 0x4450FC
int gameDialogDisable()
{
    tickersRemove(gameDialogTicker);
    return 0;
}

// 0x44510C
int _gdialogInitFromScript(int headFid, int reaction)
{
    if (_dialogue_state == 1) {
        return -1;
    }

    if (_gdialog_state == 1) {
        return 0;
    }

    animationStop();

    _boxesWereDisabled = indicatorBarHide();
    gGameDialogSpeakerIsPartyMember = objectIsPartyMember(gGameDialogSpeaker);
    _oldFont = fontGetCurrent();
    fontSetCurrent(101);
    dialogSetReplyWindow(135, 225, 379, 58, nullptr);
    dialogSetReplyColor(0.3f, 0.3f, 0.3f);
    dialogSetOptionWindow(127, 335, 393, 117, nullptr);
    dialogSetOptionColor(0.2f, 0.2f, 0.2f);
    dialogSetReplyTitle(nullptr);
    _dialogRegisterWinDrawCallbacks(_demo_copy_title, _demo_copy_options);
    gameDialogHighlightsInit();
    colorCycleDisable();
    if (_gdDialogTurnMouseOff) {
        _gmouse_disable(0);
    }
    gameMouseObjectsHide();
    gameMouseSetCursor(MOUSE_CURSOR_ARROW);
    textObjectsReset();

    if (PID_TYPE(gGameDialogSpeaker->pid) != OBJ_TYPE_ITEM) {
        _tile_scroll_to(gGameDialogSpeaker->tile, 2);
    }

    _talk_need_to_center = 1;

    // CE: Fix Barter button.
    gameDialogRedButtonsInit();

    _gdCreateHeadWindow();
    tickersAdd(gameDialogTicker);
    _gdSetupFidget(headFid, reaction);
    _gdialog_state = 1;
    _gmouse_disable_scrolling();

    if (headFid == -1) {
        // SFALL: Fix the music volume when entering the dialog.
        gGameDialogOldMusicVolume = _gsound_background_volume_get_set(gMusicVolume / 2);
    } else {
        gGameDialogOldMusicVolume = -1;
        backgroundSoundDelete();
    }

    _gdDialogWentOff = true;

    GameMode::enterGameMode(GameMode::kDialog);

    return 0;
}

// 0x445298
int _gdialogExitFromScript()
{
    if (_dialogue_switch_mode == 2 || _dialogue_switch_mode == 8 || _dialogue_switch_mode == 11) {
        return -1;
    }

    if (_gdialog_state == 0) {
        return 0;
    }

    gameDialogEndLips();
    dialogReviewEntriesClear();
    tickersRemove(gameDialogTicker);

    if (PID_TYPE(gGameDialogSpeaker->pid) != OBJ_TYPE_ITEM) {
        if (gGameDialogOldDudeTile != gDude->tile) {
            gGameDialogOldCenterTile = gDude->tile;
        }
        _tile_scroll_to(gGameDialogOldCenterTile, 2);
    }

    GameMode::exitGameMode(GameMode::kDialog);

    GameMode::enterGameMode(GameMode::kSpecial);
    _gdDestroyHeadWindow();
    GameMode::exitGameMode(GameMode::kSpecial);

    // CE: Fix Barter button.
    gameDialogRedButtonsExit();

    fontSetCurrent(_oldFont);

    if (gGameDialogFidgetFrm != nullptr) {
        artUnlock(gGameDialogFidgetFrmHandle);
        gGameDialogFidgetFrm = nullptr;
    }

    if (_lipsKey != nullptr) {
        if (artUnlock(_lipsKey) == -1) {
            debugPrint("Failure unlocking lips frame!\n");
        }
        _lipsKey = nullptr;
        _lipsFp = nullptr;
        _lipsFID = 0;
    }

    // NOTE: Uninline.
    gameDialogHighlightsExit();

    _gdialog_state = 0;
    _dialogue_state = 0;

    colorCycleEnable();

    if (!gameUiIsDisabled()) {
        _gmouse_enable_scrolling();
    }

    if (gGameDialogOldMusicVolume == -1) {
        backgroundSoundRestart(11);
    } else {
        backgroundSoundSetVolume(gGameDialogOldMusicVolume);
    }

    if (_boxesWereDisabled) {
        indicatorBarShow();
    }

    _boxesWereDisabled = 0;

    if (_gdDialogTurnMouseOff) {
        if (!gameUiIsDisabled()) {
            _gmouse_enable();
        }

        _gdDialogTurnMouseOff = 0;
    }

    if (!gameUiIsDisabled()) {
        gameMouseObjectsShow();
    }

    _gdDialogWentOff = true;

    return 0;
}

// 0x445438
void gameDialogSetBackground(int a1)
{
    if (a1 != -1) {
        gGameDialogBackground = a1;
    }
}

// Renders supplementary message in reply area of the dialog.
//
// 0x445448
void gameDialogRenderSupplementaryMessage(char* msg)
{
    if (_gd_replyWin == -1) {
        debugPrint("\nError: Reply window doesn't exist!");
        return;
    }

    _replyRect.left = 5;
    _replyRect.top = 10;
    _replyRect.right = 374;
    _replyRect.bottom = 58;
    _demo_copy_title(gGameDialogReplyWindow);

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogReplyWindow);
    int lineHeight = fontGetLineHeight();

    int a4 = 0;

    // NOTE: Uninline.
    text_to_rect_wrapped(windowBuffer,
        &_replyRect,
        msg,
        &a4,
        lineHeight,
        379,
        _colorTable[992] | 0x2000000);

    windowShow(_gd_replyWin);
    windowRefresh(gGameDialogReplyWindow);
}

// 0x4454FC
int _gdialogStart()
{
    gGameDialogReviewEntriesLength = 0;
    gGameDialogOptionEntriesLength = 0;
    return 0;
}

// 0x445510
int _gdialogSayMessage()
{
    mouseShowCursor();
    _gdialogGo();

    gGameDialogOptionEntriesLength = 0;
    gDialogReplyMessageListId = -1;

    return 0;
}

// NOTE: If you look at the scripts handlers, my best guess that their intention
// was to allow scripters to specify proc names instead of proc addresses. They
// dropped this idea, probably because they've updated their own compiler, or
// maybe there was not enough time to complete it. Any way, [procedure] is the
// identifier of the procedure in the script, but it is silently ignored.
//
// 0x445538
int gameDialogAddMessageOptionWithProcIdentifier(int messageListId, int messageId, const char* proc, int reaction)
{
    gDialogOptionEntries[gGameDialogOptionEntriesLength].proc = 0;

    return gameDialogAddMessageOption(messageListId, messageId, reaction);
}

// NOTE: If you look at the script handlers, my best guess that their intention
// was to allow scripters to specify proc names instead of proc addresses. They
// dropped this idea, probably because they've updated their own compiler, or
// maybe there was not enough time to complete it. Any way, [procedure] is the
// identifier of the procedure in the script, but it is silently ignored.
//
// 0x445578
int gameDialogAddTextOptionWithProcIdentifier(int messageListId, const char* text, const char* proc, int reaction)
{
    gDialogOptionEntries[gGameDialogOptionEntriesLength].proc = 0;

    return gameDialogAddTextOption(messageListId, text, reaction);
}

// 0x4455B8
int gameDialogAddMessageOptionWithProc(int messageListId, int messageId, int proc, int reaction)
{
    gDialogOptionEntries[gGameDialogOptionEntriesLength].proc = proc;

    return gameDialogAddMessageOption(messageListId, messageId, reaction);
}

// 0x4455FC
int gameDialogAddTextOptionWithProc(int messageListId, const char* text, int proc, int reaction)
{
    gDialogOptionEntries[gGameDialogOptionEntriesLength].proc = proc;

    return gameDialogAddTextOption(messageListId, text, reaction);
}

// 0x445640
int gameDialogSetMessageReply(Program* program, int messageListId, int messageId)
{
    gameDialogAddReviewMessage(messageListId, messageId);

    gDialogReplyProgram = program;
    gDialogReplyMessageListId = messageListId;
    gDialogReplyMessageId = messageId;
    dword_58F4E0 = 0;
    gDialogReplyText[0] = '\0';
    gGameDialogOptionEntriesLength = 0;

    return 0;
}

// 0x44567C
int gameDialogSetTextReply(Program* program, int messageListId, const char* text)
{
    gameDialogAddReviewText(text);

    gDialogReplyProgram = program;
    dword_58F4E0 = 0;
    gDialogReplyMessageListId = -4;
    gDialogReplyMessageId = -4;

    strcpy(gDialogReplyText, text);

    gGameDialogOptionEntriesLength = 0;

    return 0;
}

// 0x4456D8
int _gdialogGo()
{
    if (gDialogReplyMessageListId == -1) {
        return 0;
    }

    int rc = 0;

    if (gGameDialogOptionEntriesLength < 1) {
        gDialogOptionEntries[gGameDialogOptionEntriesLength].proc = 0;

        if (gameDialogAddMessageOption(-1, -1, 50) == -1) {
            programFatalError("Error setting option.");
            rc = -1;
        }
    }

    if (rc != -1) {
        rc = _gdProcess();
    }

    gGameDialogOptionEntriesLength = 0;

    return rc;
}

// 0x445764
void _gdialogUpdatePartyStatus()
{
    if (_dialogue_state != 1) {
        return;
    }

    bool isPartyMember = objectIsPartyMember(gGameDialogSpeaker);
    if (isPartyMember == gGameDialogSpeakerIsPartyMember) {
        return;
    }

    // NOTE: Uninline.
    gdHide();

    GameMode::enterGameMode(GameMode::kSpecial);

    _gdialog_window_destroy();

    gGameDialogSpeakerIsPartyMember = isPartyMember;

    GameMode::exitGameMode(GameMode::kSpecial);

    _gdialog_window_create();

    // NOTE: Uninline.
    gdUnhide();
}

// NOTE: Inlined.
//
// 0x4457EC
static int gdHide()
{
    if (_gd_replyWin != -1) {
        windowHide(_gd_replyWin);
    }

    if (_gd_optionsWin != -1) {
        windowHide(_gd_optionsWin);
    }

    return 0;
}

// NOTE: Inlined.
//
// 0x445818
static int gdUnhide()
{
    if (_gd_replyWin != -1) {
        windowShow(_gd_replyWin);
    }

    if (_gd_optionsWin != -1) {
        windowShow(_gd_optionsWin);
    }

    return 0;
}

// 0x44585C
int gameDialogAddMessageOption(int messageListId, int messageId, int reaction)
{
    if (gGameDialogOptionEntriesLength >= DIALOG_OPTION_ENTRIES_CAPACITY) {
        debugPrint("\nError: dialog: Ran out of options!");
        return -1;
    }

    GameDialogOptionEntry* optionEntry = &(gDialogOptionEntries[gGameDialogOptionEntriesLength]);
    optionEntry->messageListId = messageListId;
    optionEntry->messageId = messageId;
    optionEntry->reaction = reaction;
    optionEntry->btn = -1;
    optionEntry->text[0] = '\0';

    gGameDialogOptionEntriesLength++;

    return 0;
}

// 0x4458BC
int gameDialogAddTextOption(int messageListId, const char* text, int reaction)
{
    if (gGameDialogOptionEntriesLength >= DIALOG_OPTION_ENTRIES_CAPACITY) {
        debugPrint("\nError: dialog: Ran out of options!");
        return -1;
    }

    GameDialogOptionEntry* optionEntry = &(gDialogOptionEntries[gGameDialogOptionEntriesLength]);
    optionEntry->messageListId = -4;
    optionEntry->messageId = -4;
    optionEntry->reaction = reaction;
    optionEntry->btn = -1;

    // SFALL
    if (gNumberOptions) {
        snprintf(optionEntry->text, sizeof(optionEntry->text), "%d. %s", gGameDialogOptionEntriesLength + 1, text);
    } else {
        snprintf(optionEntry->text, sizeof(optionEntry->text), "%c %s", '\x95', text);
    }

    gGameDialogOptionEntriesLength++;

    return 0;
}

// 0x445938
int gameDialogReviewWindowInit(int* win)
{
    if (gGameDialogLipSyncStarted) {
        if (soundIsPlaying(gLipsData.sound)) {
            gameDialogEndLips();
        }
    }

    gGameDialogReviewWindowOldFont = fontGetCurrent();

    if (win == nullptr) {
        return -1;
    }

    int reviewWindowX = (screenGetWidth() - GAME_DIALOG_REVIEW_WINDOW_WIDTH) / 2;
    int reviewWindowY = (screenGetHeight() - GAME_DIALOG_REVIEW_WINDOW_HEIGHT) / 2;
    *win = windowCreate(reviewWindowX,
        reviewWindowY,
        GAME_DIALOG_REVIEW_WINDOW_WIDTH,
        GAME_DIALOG_REVIEW_WINDOW_HEIGHT,
        256,
        WINDOW_MODAL | WINDOW_MOVE_ON_TOP);
    if (*win == -1) {
        return -1;
    }

    FrmImage backgroundFrmImage;
    int fid = buildFid(OBJ_TYPE_INTERFACE, 102, 0, 0, 0);
    if (!backgroundFrmImage.lock(fid)) {
        windowDestroy(*win);
        *win = -1;
        return -1;
    }

    unsigned char* windowBuffer = windowGetBuffer(*win);
    blitBufferToBuffer(backgroundFrmImage.getData(),
        GAME_DIALOG_REVIEW_WINDOW_WIDTH,
        GAME_DIALOG_REVIEW_WINDOW_HEIGHT,
        GAME_DIALOG_REVIEW_WINDOW_WIDTH,
        windowBuffer,
        GAME_DIALOG_REVIEW_WINDOW_WIDTH);

    backgroundFrmImage.unlock();

    int index;
    for (index = 0; index < GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_COUNT; index++) {
        int fid = buildFid(OBJ_TYPE_INTERFACE, gGameDialogReviewWindowButtonFrmIds[index], 0, 0, 0);
        if (!_reviewFrmImages[index].lock(fid)) {
            break;
        }
    }

    if (index != GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_COUNT) {
        gameDialogReviewWindowFree(win);
        return -1;
    }

    int upBtn = buttonCreate(*win,
        475,
        152,
        gGameDialogReviewWindowButtonWidths[GAME_DIALOG_REVIEW_WINDOW_BUTTON_SCROLL_UP],
        gGameDialogReviewWindowButtonHeights[GAME_DIALOG_REVIEW_WINDOW_BUTTON_SCROLL_UP],
        -1,
        -1,
        -1,
        KEY_ARROW_UP,
        _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_UP_NORMAL].getData(),
        _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_UP_PRESSED].getData(),
        nullptr,
        BUTTON_FLAG_TRANSPARENT);
    if (upBtn == -1) {
        gameDialogReviewWindowFree(win);
        return -1;
    }

    buttonSetCallbacks(upBtn, _gsound_med_butt_press, _gsound_med_butt_release);

    int downBtn = buttonCreate(*win,
        475,
        191,
        gGameDialogReviewWindowButtonWidths[GAME_DIALOG_REVIEW_WINDOW_BUTTON_SCROLL_DOWN],
        gGameDialogReviewWindowButtonHeights[GAME_DIALOG_REVIEW_WINDOW_BUTTON_SCROLL_DOWN],
        -1,
        -1,
        -1,
        KEY_ARROW_DOWN,
        _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_DOWN_NORMAL].getData(),
        _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_ARROW_DOWN_PRESSED].getData(),
        nullptr,
        BUTTON_FLAG_TRANSPARENT);
    if (downBtn == -1) {
        gameDialogReviewWindowFree(win);
        return -1;
    }

    buttonSetCallbacks(downBtn, _gsound_med_butt_press, _gsound_med_butt_release);

    int doneBtn = buttonCreate(*win,
        499,
        398,
        gGameDialogReviewWindowButtonWidths[GAME_DIALOG_REVIEW_WINDOW_BUTTON_DONE],
        gGameDialogReviewWindowButtonHeights[GAME_DIALOG_REVIEW_WINDOW_BUTTON_DONE],
        -1,
        -1,
        -1,
        KEY_ESCAPE,
        _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_DONE_NORMAL].getData(),
        _reviewFrmImages[GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_DONE_PRESSED].getData(),
        nullptr,
        BUTTON_FLAG_TRANSPARENT);
    if (doneBtn == -1) {
        gameDialogReviewWindowFree(win);
        return -1;
    }

    buttonSetCallbacks(doneBtn, _gsound_red_butt_press, _gsound_red_butt_release);

    fontSetCurrent(101);

    windowRefresh(*win);

    tickersRemove(gameDialogTicker);

    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 102, 0, 0, 0);
    if (!_reviewBackgroundFrmImage.lock(backgroundFid)) {
        gameDialogReviewWindowFree(win);
        return -1;
    }

    return 0;
}

// 0x445C18
int gameDialogReviewWindowFree(int* win)
{
    tickersAdd(gameDialogTicker);

    for (int index = 0; index < GAME_DIALOG_REVIEW_WINDOW_BUTTON_FRM_COUNT; index++) {
        _reviewFrmImages[index].unlock();
    }

    _reviewBackgroundFrmImage.unlock();

    fontSetCurrent(gGameDialogReviewWindowOldFont);

    if (win == nullptr) {
        return -1;
    }

    windowDestroy(*win);
    *win = -1;

    return 0;
}

// 0x445CA0
int gameDialogShowReview()
{
    ScopedGameMode gm(GameMode::kDialogReview);

    int win;

    if (gameDialogReviewWindowInit(&win) == -1) {
        debugPrint("\nError initializing review window!");
        return -1;
    }

    // probably current top line or something like this, which is used to scroll
    int v1 = 0;
    gameDialogReviewWindowUpdate(win, v1);

    while (true) {
        sharedFpsLimiter.mark();

        int keyCode = inputGetInput();
        if (keyCode == 17 || keyCode == 24 || keyCode == 324) {
            showQuitConfirmationDialog();
        }

        if (_game_user_wants_to_quit != 0 || keyCode == KEY_ESCAPE) {
            break;
        }

        // likely scrolling
        if (keyCode == 328) {
            v1 -= 1;
            if (v1 >= 0) {
                gameDialogReviewWindowUpdate(win, v1);
            } else {
                v1 = 0;
            }
        } else if (keyCode == 336) {
            v1 += 1;
            if (v1 <= gGameDialogReviewEntriesLength - 1) {
                gameDialogReviewWindowUpdate(win, v1);
            } else {
                v1 = gGameDialogReviewEntriesLength - 1;
            }
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }

    if (gameDialogReviewWindowFree(&win) == -1) {
        return -1;
    }

    return 0;
}

// NOTE: Uncollapsed 0x445CA0 with different signature.
void gameDialogReviewButtonOnMouseUp(int btn, int keyCode)
{
    gameDialogShowReview();
}

// 0x445D44
void gameDialogReviewWindowUpdate(int win, int origin)
{
    Rect entriesRect;
    entriesRect.left = 113;
    entriesRect.top = 76;
    entriesRect.right = 422;
    entriesRect.bottom = 418;

    int v20 = fontGetLineHeight() + 2;
    unsigned char* windowBuffer = windowGetBuffer(win);
    if (windowBuffer == nullptr) {
        debugPrint("\nError: gdialog: review: can't find buffer!");
        return;
    }

    int width = GAME_DIALOG_WINDOW_WIDTH;
    blitBufferToBuffer(
        _reviewBackgroundFrmImage.getData() + width * entriesRect.top + entriesRect.left,
        width,
        entriesRect.bottom - entriesRect.top + 15,
        width,
        windowBuffer + width * entriesRect.top + entriesRect.left,
        width);

    int y = 76;
    for (int index = origin; index < gGameDialogReviewEntriesLength; index++) {
        GameDialogReviewEntry* dialogReviewEntry = &(gDialogReviewEntries[index]);

        char name[60];
        snprintf(name, sizeof(name), "%s:", objectGetName(gGameDialogSpeaker));
        windowDrawText(win, name, 180, 88, y, _colorTable[992] | 0x2000000);
        entriesRect.top += v20;

        char* replyText;
        if (dialogReviewEntry->replyMessageListId <= -3) {
            replyText = dialogReviewEntry->replyText;
        } else {
            replyText = _scr_get_msg_str(dialogReviewEntry->replyMessageListId, dialogReviewEntry->replyMessageId);
        }

        if (replyText == nullptr) {
            showMesageBox("\nGDialog::Error Grabbing text message!");
            exit(1);
        }

        // NOTE: Uninline.
        y = text_to_rect_wrapped(windowBuffer + 113,
            &entriesRect,
            replyText,
            nullptr,
            fontGetLineHeight(),
            640,
            _colorTable[768] | 0x2000000);

        // SFALL: Cosmetic fix to the dialog review interface to prevent the
        // player name from being displayed at the bottom of the window when the
        // text is longer than one screen.
        if (y >= 407) {
            break;
        }

        if (dialogReviewEntry->optionMessageListId != -3) {
            snprintf(name, sizeof(name), "%s:", objectGetName(gDude));
            windowDrawText(win, name, 180, 88, y, _colorTable[21140] | 0x2000000);
            entriesRect.top += v20;

            char* optionText;
            if (dialogReviewEntry->optionMessageListId <= -3) {
                optionText = dialogReviewEntry->optionText;
            } else {
                optionText = _scr_get_msg_str(dialogReviewEntry->optionMessageListId, dialogReviewEntry->optionMessageId);
            }

            if (optionText == nullptr) {
                showMesageBox("\nGDialog::Error Grabbing text message!");
                exit(1);
            }

            // NOTE: Uninline.
            y = text_to_rect_wrapped(windowBuffer + 113,
                &entriesRect,
                optionText,
                nullptr,
                fontGetLineHeight(),
                640,
                _colorTable[15855] | 0x2000000);
        }

        if (y >= 407) {
            break;
        }
    }

    entriesRect.left = 88;
    entriesRect.top = 76;
    entriesRect.bottom += 14;
    entriesRect.right = 434;
    windowRefreshRect(win, &entriesRect);
}

// 0x445FDC
void dialogReviewEntriesClear()
{
    for (int index = 0; index < gGameDialogReviewEntriesLength; index++) {
        GameDialogReviewEntry* entry = &(gDialogReviewEntries[index]);
        entry->replyMessageListId = 0;
        entry->replyMessageId = 0;

        if (entry->replyText != nullptr) {
            internal_free(entry->replyText);
            entry->replyText = nullptr;
        }

        entry->optionMessageListId = 0;
        entry->optionMessageId = 0;
    }
}

// 0x446040
int gameDialogAddReviewMessage(int messageListId, int messageId)
{
    if (gGameDialogReviewEntriesLength >= DIALOG_REVIEW_ENTRIES_CAPACITY) {
        debugPrint("\nError: Ran out of review slots!");
        return -1;
    }

    GameDialogReviewEntry* entry = &(gDialogReviewEntries[gGameDialogReviewEntriesLength]);
    entry->replyMessageListId = messageListId;
    entry->replyMessageId = messageId;

    // NOTE: I'm not sure why there are two consequtive assignments.
    entry->optionMessageListId = -1;
    entry->optionMessageId = -1;

    entry->optionMessageListId = -3;
    entry->optionMessageId = -3;

    gGameDialogReviewEntriesLength++;

    return 0;
}

// 0x4460B4
int gameDialogAddReviewText(const char* string)
{
    if (gGameDialogReviewEntriesLength >= DIALOG_REVIEW_ENTRIES_CAPACITY) {
        debugPrint("\nError: Ran out of review slots!");
        return -1;
    }

    GameDialogReviewEntry* entry = &(gDialogReviewEntries[gGameDialogReviewEntriesLength]);
    entry->replyMessageListId = -4;
    entry->replyMessageId = -4;

    if (entry->replyText != nullptr) {
        internal_free(entry->replyText);
        entry->replyText = nullptr;
    }

    entry->replyText = (char*)internal_malloc(strlen(string) + 1);
    strcpy(entry->replyText, string);

    entry->optionMessageListId = -3;
    entry->optionMessageId = -3;
    entry->optionText = nullptr;

    gGameDialogReviewEntriesLength++;

    return 0;
}

// 0x4461A4
int gameDialogSetReviewOptionMessage(int messageListId, int messageId)
{
    if (gGameDialogReviewEntriesLength >= DIALOG_REVIEW_ENTRIES_CAPACITY) {
        debugPrint("\nError: Ran out of review slots!");
        return -1;
    }

    GameDialogReviewEntry* entry = &(gDialogReviewEntries[gGameDialogReviewEntriesLength - 1]);
    entry->optionMessageListId = messageListId;
    entry->optionMessageId = messageId;
    entry->optionText = nullptr;

    return 0;
}

// 0x4461F0
int gameDialogSetReviewOptionText(const char* string)
{
    if (gGameDialogReviewEntriesLength >= DIALOG_REVIEW_ENTRIES_CAPACITY) {
        debugPrint("\nError: Ran out of review slots!");
        return -1;
    }

    GameDialogReviewEntry* entry = &(gDialogReviewEntries[gGameDialogReviewEntriesLength - 1]);
    entry->optionMessageListId = -4;
    entry->optionMessageId = -4;

    entry->optionText = (char*)internal_malloc(strlen(string) + 1);
    strcpy(entry->optionText, string);

    return 0;
}

// Creates dialog interface.
//
// 0x446288
int _gdProcessInit()
{
    int upBtn;
    int downBtn;
    int optionsWindowX;
    int optionsWindowY;

    int replyWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2 + GAME_DIALOG_REPLY_WINDOW_X;
    int replyWindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2 + GAME_DIALOG_REPLY_WINDOW_Y;
    gGameDialogReplyWindow = windowCreate(replyWindowX,
        replyWindowY,
        GAME_DIALOG_REPLY_WINDOW_WIDTH,
        GAME_DIALOG_REPLY_WINDOW_HEIGHT,
        256,
        WINDOW_MOVE_ON_TOP);
    if (gGameDialogReplyWindow == -1) {
        goto err;
    }

    // Top part of the reply window - scroll up.
    upBtn = buttonCreate(gGameDialogReplyWindow, 1, 1, 377, 28, -1, -1, KEY_ARROW_UP, -1, nullptr, nullptr, nullptr, 32);
    if (upBtn == -1) {
        goto err_1;
    }

    buttonSetCallbacks(upBtn, _gsound_red_butt_press, _gsound_red_butt_release);
    buttonSetMouseCallbacks(upBtn, _reply_arrow_up, _reply_arrow_restore, nullptr, nullptr);

    // Bottom part of the reply window - scroll down.
    downBtn = buttonCreate(gGameDialogReplyWindow, 1, 29, 377, 28, -1, -1, KEY_ARROW_DOWN, -1, nullptr, nullptr, nullptr, 32);
    if (downBtn == -1) {
        goto err_1;
    }

    buttonSetCallbacks(downBtn, _gsound_red_butt_press, _gsound_red_butt_release);
    buttonSetMouseCallbacks(downBtn, _reply_arrow_down, _reply_arrow_restore, nullptr, nullptr);

    optionsWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2 + GAME_DIALOG_OPTIONS_WINDOW_X;
    optionsWindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2 + GAME_DIALOG_OPTIONS_WINDOW_Y;
    gGameDialogOptionsWindow = windowCreate(optionsWindowX, optionsWindowY, GAME_DIALOG_OPTIONS_WINDOW_WIDTH, GAME_DIALOG_OPTIONS_WINDOW_HEIGHT, 256, WINDOW_MOVE_ON_TOP);
    if (gGameDialogOptionsWindow == -1) {
        goto err_2;
    }

    // CE: Move red buttons init to `_gdialogInitFromScript`.

    _talkOldFont = fontGetCurrent();
    fontSetCurrent(101);

    return 0;

err_2:

    windowDestroy(gGameDialogOptionsWindow);
    gGameDialogOptionsWindow = -1;

err_1:

    windowDestroy(gGameDialogReplyWindow);
    gGameDialogReplyWindow = -1;

err:

    return -1;
}

// RELASE: Rename/comment.
// free dialog option buttons
// 0x446454
void _gdProcessCleanup()
{
    for (int index = 0; index < gGameDialogOptionEntriesLength; index++) {
        GameDialogOptionEntry* optionEntry = &(gDialogOptionEntries[index]);

        if (optionEntry->btn != -1) {
            buttonDestroy(optionEntry->btn);
            optionEntry->btn = -1;
        }
    }
}

// RELASE: Rename/comment.
// free dialog interface
// 0x446498
int _gdProcessExit()
{
    _gdProcessCleanup();

    // CE: Move red buttons exit to `_gdialogExitFromScript`.

    windowDestroy(gGameDialogReplyWindow);
    gGameDialogReplyWindow = -1;

    windowDestroy(gGameDialogOptionsWindow);
    gGameDialogOptionsWindow = -1;

    fontSetCurrent(_talkOldFont);

    return 0;
}

// 0x446504
void gameDialogRenderCaps()
{
    Rect rect;
    rect.left = 5;
    rect.right = 70;
    rect.top = 36;
    rect.bottom = fontGetLineHeight() + 36;

    _talkToRefreshDialogWindowRect(&rect);

    int oldFont = fontGetCurrent();
    fontSetCurrent(101);

    int caps = itemGetTotalCaps(gDude);
    char text[20];
    snprintf(text, sizeof(text), "$%d", caps);

    int width = fontGetStringWidth(text);
    if (width > 60) {
        width = 60;
    }

    windowDrawText(gGameDialogWindow, text, width, 38 - width / 2, 36, _colorTable[992] | 0x7000000);

    fontSetCurrent(oldFont);
}

// 0x4465C0
int _gdProcess()
{
    if (_gdReenterLevel == 0) {
        if (_gdProcessInit() == -1) {
            return -1;
        }
    }

    _gdReenterLevel += 1;

    _gdProcessUpdate();

    int v18 = 0;
    if (dword_58F4E0 != 0) {
        v18 = 1;
        _gdReplyTooBig = 1;
    }

    unsigned int tick = getTicks();
    int pageCount = 0;
    int pageIndex = 0;
    int pageOffsets[10];
    pageOffsets[0] = 0;
    for (;;) {
        sharedFpsLimiter.mark();

        int keyCode = inputGetInput();

        convertMouseWheelToArrowKey(&keyCode);

        if (keyCode == KEY_CTRL_Q || keyCode == KEY_CTRL_X || keyCode == KEY_F10) {
            showQuitConfirmationDialog();
        }

        if (_game_user_wants_to_quit != 0) {
            break;
        }

        if (keyCode == KEY_CTRL_B && !_mouse_click_in(135, 225, 514, 283)) {
            if (gameMouseGetCursor() != MOUSE_CURSOR_ARROW) {
                gameMouseSetCursor(MOUSE_CURSOR_ARROW);
            }
        } else {
            if (_dialogue_switch_mode == 3) {
                _dialogue_state = 4;

                GameMode::exitGameMode(GameMode::kSpecial);

                inventoryOpenTrade(gGameDialogWindow, gGameDialogSpeaker, _peon_table_obj, _barterer_table_obj, gGameDialogBarterModifier);
                _gdialog_barter_cleanup_tables();

                int v5 = _dialogue_state;
                _gdialog_barter_destroy_win();
                _dialogue_state = v5;

                if (v5 == 4) {
                    _dialogue_switch_mode = 1;
                    _dialogue_state = 1;
                }
                continue;
            } else if (_dialogue_switch_mode == 9) {
                _dialogue_state = 10;
                partyMemberControlWindowHandleEvents();
                partyMemberControlWindowFree();
                continue;
            } else if (_dialogue_switch_mode == 12) {
                _dialogue_state = 13;
                partyMemberCustomizationWindowHandleEvents();
                partyMemberCustomizationWindowFree();
                continue;
            }

            if (keyCode == KEY_LOWERCASE_B) {
                gameDialogBarterButtonUpMouseUp(-1, -1);
            }
        }

        if (_gdReplyTooBig) {
            unsigned int v6 = _get_bk_time();
            if (v18) {
                if (getTicksBetween(v6, tick) >= 10000 || keyCode == KEY_SPACE) {
                    pageCount++;
                    pageIndex++;
                    pageOffsets[pageCount] = dword_58F4E0;
                    gameDialogRenderReply();
                    tick = v6;
                    if (!dword_58F4E0) {
                        v18 = 0;
                    }
                }
            }

            if (keyCode == KEY_ARROW_UP) {
                if (pageIndex > 0) {
                    pageIndex--;
                    dword_58F4E0 = pageOffsets[pageIndex];
                    v18 = 0;
                    gameDialogRenderReply();
                }
            } else if (keyCode == KEY_ARROW_DOWN) {
                if (pageIndex < pageCount) {
                    pageIndex++;
                    dword_58F4E0 = pageOffsets[pageIndex];
                    v18 = 0;
                    gameDialogRenderReply();
                } else {
                    if (dword_58F4E0 != 0) {
                        tick = v6;
                        pageIndex++;
                        pageCount++;
                        pageOffsets[pageCount] = dword_58F4E0;
                        v18 = 0;
                        gameDialogRenderReply();
                    }
                }
            }
        }

        if (keyCode != -1) {
            if (keyCode >= 1200 && keyCode <= 1250) {
                gameDialogOptionOnMouseEnter(keyCode - 1200);
            } else if (keyCode >= 1300 && keyCode <= 1330) {
                gameDialogOptionOnMouseExit(keyCode - 1300);
            } else if (keyCode >= 48 && keyCode <= 57) {
                // SFALL: Prevents from using 0 to escape from dialogue at any time.
                if (keyCode == KEY_0 && gGameDialogFix) {
                    continue;
                }

                int v11 = keyCode - 49;
                if (v11 < gGameDialogOptionEntriesLength) {
                    pageCount = 0;
                    pageIndex = 0;
                    pageOffsets[0] = 0;
                    _gdReplyTooBig = 0;

                    if (_gdProcessChoice(v11) == -1) {
                        break;
                    }

                    tick = getTicks();

                    if (dword_58F4E0) {
                        v18 = 1;
                        _gdReplyTooBig = 1;
                    } else {
                        v18 = 0;
                    }
                }
            }
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }

    _gdReenterLevel -= 1;

    if (_gdReenterLevel == 0) {
        if (_gdProcessExit() == -1) {
            return -1;
        }
    }

    return 0;
}

// 0x4468DC
int _gdProcessChoice(int a1)
{
    // FIXME: There is a buffer underread bug when `a1` is -1 (pressing 0 on the
    // keyboard, see `_gdProcess`). When it happens the game looks into unused
    // continuation of `gDialogReplyText` (within 0x58F868-0x58FF70 range) which
    // is initialized to 0 according to C spec. I was not able to replicate the
    // same behaviour by extending gDialogReplyText to 2700 bytes or introduce
    // new 1800 bytes buffer in between, at least not in debug builds. In order
    // to preserve original behaviour this dummy dialog option entry is used.
    GameDialogOptionEntry dummy;
    memset(&dummy, 0, sizeof(dummy));

    mouseHideCursor();
    _gdProcessCleanup();

    GameDialogOptionEntry* dialogOptionEntry = a1 != -1 ? &(gDialogOptionEntries[a1]) : &dummy;
    if (dialogOptionEntry->messageListId == -4) {
        gameDialogSetReviewOptionText(dialogOptionEntry->text);
    } else {
        gameDialogSetReviewOptionMessage(dialogOptionEntry->messageListId, dialogOptionEntry->messageId);
    }

    _can_start_new_fidget = false;

    gameDialogEndLips();

    int v1 = GAME_DIALOG_REACTION_NEUTRAL;
    switch (dialogOptionEntry->reaction) {
    case GAME_DIALOG_REACTION_GOOD:
        v1 = -1;
        break;
    case GAME_DIALOG_REACTION_NEUTRAL:
        v1 = 0;
        break;
    case GAME_DIALOG_REACTION_BAD:
        v1 = 1;
        break;
    default:
        // See 0x446907 in ecx but this branch should be unreachable. Due to the
        // bug described above, this code is reachable.
        v1 = GAME_DIALOG_REACTION_NEUTRAL;
        debugPrint("\nError: dialog: Empathy Perk: invalid reaction!");
        break;
    }

    _demo_copy_title(gGameDialogReplyWindow);
    _demo_copy_options(gGameDialogOptionsWindow);
    windowRefresh(gGameDialogReplyWindow);
    windowRefresh(gGameDialogOptionsWindow);

    gameDialogOptionOnMouseEnter(a1);
    _talk_to_critter_reacts(v1);

    gGameDialogOptionEntriesLength = 0;

    if (_gdReenterLevel < 2) {
        if (dialogOptionEntry->proc != 0) {
            _executeProcedure(gDialogReplyProgram, dialogOptionEntry->proc);
        }
    }

    mouseShowCursor();

    if (gGameDialogOptionEntriesLength == 0) {
        return -1;
    }

    _gdProcessUpdate();

    return 0;
}

// 0x446A18
void gameDialogOptionOnMouseEnter(int index)
{
    // FIXME: See explanation in `_gdProcessChoice`.
    GameDialogOptionEntry dummy;
    memset(&dummy, 0, sizeof(dummy));

    GameDialogOptionEntry* dialogOptionEntry = index != -1 ? &(gDialogOptionEntries[index]) : &dummy;
    if (dialogOptionEntry->btn == 0) {
        return;
    }

    _optionRect.left = 0;
    _optionRect.top = dialogOptionEntry->field_14;
    _optionRect.right = 391;
    _optionRect.bottom = dialogOptionEntry->field_39C;
    _gDialogRefreshOptionsRect(gGameDialogOptionsWindow, &_optionRect);

    _optionRect.left = 5;
    _optionRect.right = 388;

    int color = _colorTable[32747] | 0x2000000;
    if (perkHasRank(gDude, PERK_EMPATHY)) {
        color = _colorTable[32747] | 0x2000000;
        switch (dialogOptionEntry->reaction) {
        case GAME_DIALOG_REACTION_GOOD:
            color = _colorTable[31775] | 0x2000000;
            break;
        case GAME_DIALOG_REACTION_NEUTRAL:
            break;
        case GAME_DIALOG_REACTION_BAD:
            color = _colorTable[32074] | 0x2000000;
            break;
        default:
            debugPrint("\nError: dialog: Empathy Perk: invalid reaction!");
            break;
        }
    }

    // NOTE: Uninline.
    text_to_rect_wrapped(windowGetBuffer(gGameDialogOptionsWindow),
        &_optionRect,
        dialogOptionEntry->text,
        nullptr,
        fontGetLineHeight(),
        393,
        color);

    _optionRect.left = 0;
    _optionRect.right = 391;
    _optionRect.top = dialogOptionEntry->field_14;
    windowRefreshRect(gGameDialogOptionsWindow, &_optionRect);
}

// 0x446B5C
void gameDialogOptionOnMouseExit(int index)
{
    GameDialogOptionEntry* dialogOptionEntry = &(gDialogOptionEntries[index]);

    _optionRect.left = 0;
    _optionRect.top = dialogOptionEntry->field_14;
    _optionRect.right = 391;
    _optionRect.bottom = dialogOptionEntry->field_39C;
    _gDialogRefreshOptionsRect(gGameDialogOptionsWindow, &_optionRect);

    int color = _colorTable[992] | 0x2000000;
    if (perkGetRank(gDude, PERK_EMPATHY) != 0) {
        color = _colorTable[32747] | 0x2000000;
        switch (dialogOptionEntry->reaction) {
        case GAME_DIALOG_REACTION_GOOD:
            color = _colorTable[31] | 0x2000000;
            break;
        case GAME_DIALOG_REACTION_NEUTRAL:
            color = _colorTable[992] | 0x2000000;
            break;
        case GAME_DIALOG_REACTION_BAD:
            color = _colorTable[31744] | 0x2000000;
            break;
        default:
            debugPrint("\nError: dialog: Empathy Perk: invalid reaction!");
            break;
        }
    }

    _optionRect.left = 5;
    _optionRect.right = 388;

    // NOTE: Uninline.
    text_to_rect_wrapped(windowGetBuffer(gGameDialogOptionsWindow),
        &_optionRect,
        dialogOptionEntry->text,
        nullptr,
        fontGetLineHeight(),
        393,
        color);

    _optionRect.right = 391;
    _optionRect.top = dialogOptionEntry->field_14;
    _optionRect.left = 0;
    windowRefreshRect(gGameDialogOptionsWindow, &_optionRect);
}

// 0x446C94
void gameDialogRenderReply()
{
    _replyRect.left = 5;
    _replyRect.top = 10;
    _replyRect.right = 374;
    _replyRect.bottom = 58;

    // NOTE: There is an unused if condition.
    perkGetRank(gDude, PERK_EMPATHY);

    _demo_copy_title(gGameDialogReplyWindow);

    // NOTE: Uninline.
    text_to_rect_wrapped(windowGetBuffer(gGameDialogReplyWindow),
        &_replyRect,
        gDialogReplyText,
        &dword_58F4E0,
        fontGetLineHeight(),
        379,
        _colorTable[992] | 0x2000000);
    windowRefresh(gGameDialogReplyWindow);
}

// 0x446D30
void _gdProcessUpdate()
{
    _replyRect.left = 5;
    _replyRect.top = 10;
    _replyRect.right = 374;
    _replyRect.bottom = 58;

    _optionRect.left = 5;
    _optionRect.top = 5;
    _optionRect.right = 388;
    _optionRect.bottom = 112;

    _demo_copy_title(gGameDialogReplyWindow);
    _demo_copy_options(gGameDialogOptionsWindow);

    if (gDialogReplyMessageListId > 0) {
        char* s = _scr_get_msg_str_speech(gDialogReplyMessageListId, gDialogReplyMessageId, 1);
        if (s == nullptr) {
            showMesageBox("\n'GDialog::Error Grabbing text message!");
            exit(1);
        }

        strncpy(gDialogReplyText, s, sizeof(gDialogReplyText) - 1);
        *(gDialogReplyText + sizeof(gDialogReplyText) - 1) = '\0';
    }

    gameDialogRenderReply();

    int color = _colorTable[992] | 0x2000000;

    bool hasEmpathy = perkGetRank(gDude, PERK_EMPATHY) != 0;

    int width = _optionRect.right - _optionRect.left - 4;

    MessageListItem messageListItem;

    int v21 = 0;

    for (int index = 0; index < gGameDialogOptionEntriesLength; index++) {
        GameDialogOptionEntry* dialogOptionEntry = &(gDialogOptionEntries[index]);

        if (hasEmpathy) {
            switch (dialogOptionEntry->reaction) {
            case GAME_DIALOG_REACTION_GOOD:
                color = _colorTable[31] | 0x2000000;
                break;
            case GAME_DIALOG_REACTION_NEUTRAL:
                color = _colorTable[992] | 0x2000000;
                break;
            case GAME_DIALOG_REACTION_BAD:
                color = _colorTable[31744] | 0x2000000;
                break;
            default:
                debugPrint("\nError: dialog: Empathy Perk: invalid reaction!");
                break;
            }
        }

        if (dialogOptionEntry->messageListId >= 0) {
            char* text = _scr_get_msg_str_speech(dialogOptionEntry->messageListId, dialogOptionEntry->messageId, 0);
            if (text == nullptr) {
                showMesageBox("\nGDialog::Error Grabbing text message!");
                exit(1);
            }

            // SFALL
            if (gNumberOptions) {
                snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%d. %s", index + 1, text);
            } else {
                snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%c %s", '\x95', text);
            }
        } else if (dialogOptionEntry->messageListId == -1) {
            if (index == 0) {
                // Go on
                messageListItem.num = 655;
                if (critterGetStat(gDude, STAT_INTELLIGENCE) < 4) {
                    if (messageListGetItem(&gProtoMessageList, &messageListItem)) {
                        // SFALL
                        if (gNumberOptions) {
                            snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%d. %s", index + 1, messageListItem.text);
                        } else {
                            snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%s", messageListItem.text);
                        }
                    } else {
                        debugPrint("\nError...can't find message!");
                        return;
                    }
                }
            } else {
                // TODO: Why only space?
                // SFALL
                if (gNumberOptions) {
                    snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%d. %s", index + 1, " ");
                } else {
                    strcpy(dialogOptionEntry->text, " ");
                }
            }
        } else if (dialogOptionEntry->messageListId == -2) {
            // [Done]
            messageListItem.num = 650;
            if (messageListGetItem(&gProtoMessageList, &messageListItem)) {
                // SFALL
                if (gNumberOptions) {
                    snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%d. %s", index + 1, messageListItem.text);
                } else {
                    snprintf(dialogOptionEntry->text, sizeof(dialogOptionEntry->text), "%c %s", '\x95', messageListItem.text);
                }
            } else {
                debugPrint("\nError...can't find message!");
                return;
            }
        }

        int v11 = _text_num_lines(dialogOptionEntry->text, _optionRect.right - _optionRect.left) * fontGetLineHeight() + _optionRect.top + 2;
        if (v11 < _optionRect.bottom) {
            int y = _optionRect.top;

            dialogOptionEntry->field_39C = v11;
            dialogOptionEntry->field_14 = y;

            if (index == 0) {
                y = 0;
            }

            // NOTE: Uninline.
            text_to_rect_wrapped(windowGetBuffer(gGameDialogOptionsWindow),
                &_optionRect,
                dialogOptionEntry->text,
                nullptr,
                fontGetLineHeight(),
                393,
                color);

            _optionRect.top += 2;

            dialogOptionEntry->btn = buttonCreate(gGameDialogOptionsWindow, 2, y, width, _optionRect.top - y - 4, 1200 + index, 1300 + index, -1, 49 + index, nullptr, nullptr, nullptr, 0);
            if (dialogOptionEntry->btn != -1) {
                buttonSetCallbacks(dialogOptionEntry->btn, _gsound_red_butt_press, _gsound_red_butt_release);
            } else {
                debugPrint("\nError: Can't create button!");
            }
        } else {
            if (!v21) {
                v21 = 1;
            } else {
                debugPrint("Error: couldn't make button because it went below the window.\n");
            }
        }
    }

    gameDialogRenderCaps();
    windowRefresh(gGameDialogReplyWindow);
    windowRefresh(gGameDialogOptionsWindow);
}

// 0x44715C
int _gdCreateHeadWindow()
{
    _dialogue_state = 1;

    int windowWidth = GAME_DIALOG_WINDOW_WIDTH;

    // NOTE: Uninline.
    talk_to_create_background_window();
    gameDialogWindowRenderBackground();

    unsigned char* buf = windowGetBuffer(gGameDialogBackgroundWindow);

    for (int index = 0; index < 8; index++) {
        soundContinueAll();

        Rect* rect = &(_backgrndRects[index]);
        int width = rect->right - rect->left;
        int height = rect->bottom - rect->top;
        _backgrndBufs[index] = (unsigned char*)internal_malloc(width * height);
        if (_backgrndBufs[index] == nullptr) {
            return -1;
        }

        unsigned char* src = buf;
        src += windowWidth * rect->top + rect->left;

        blitBufferToBuffer(src, width, height, windowWidth, _backgrndBufs[index], width);
    }

    _gdialog_window_create();

    gGameDialogDisplayBuffer = windowGetBuffer(gGameDialogBackgroundWindow) + windowWidth * 14 + 126;

    // TODO: jnz at 0x447275 without cmp or test, not sure what that means.
    if (false) {
        _gdDestroyHeadWindow();
        return -1;
    }

    return 0;
}

// 0x447294
void _gdDestroyHeadWindow()
{
    if (gGameDialogWindow != -1) {
        gGameDialogDisplayBuffer = nullptr;
    }

    if (_dialogue_state == 1) {
        _gdialog_window_destroy();
    } else if (_dialogue_state == 4) {
        _gdialog_barter_destroy_win();
    }

    if (gGameDialogBackgroundWindow != -1) {
        windowDestroy(gGameDialogBackgroundWindow);
        gGameDialogBackgroundWindow = -1;
    }

    for (int index = 0; index < 8; index++) {
        internal_free(_backgrndBufs[index]);
    }
}

// 0x447300
void _gdSetupFidget(int headFrmId, int reaction)
{
    gGameDialogFidgetFrmCurrentFrame = 0;

    if (headFrmId == -1) {
        gGameDialogFidgetFid = -1;
        gGameDialogFidgetFrm = nullptr;
        gGameDialogFidgetFrmHandle = INVALID_CACHE_ENTRY;
        gGameDialogFidgetReaction = -1;
        gGameDialogFidgetUpdateDelay = 0;
        gGameDialogFidgetLastUpdateTimestamp = 0;
        gameDialogRenderTalkingHead(nullptr, 0);
        _lipsFID = 0;
        _lipsKey = nullptr;
        _lipsFp = nullptr;
        return;
    }

    int anim = HEAD_ANIMATION_NEUTRAL_PHONEMES;
    switch (reaction) {
    case FIDGET_GOOD:
        anim = HEAD_ANIMATION_GOOD_PHONEMES;
        break;
    case FIDGET_BAD:
        anim = HEAD_ANIMATION_BAD_PHONEMES;
        break;
    }

    if (_lipsFID != 0) {
        if (anim != _phone_anim) {
            if (artUnlock(_lipsKey) == -1) {
                debugPrint("failure unlocking lips frame!\n");
            }
            _lipsKey = nullptr;
            _lipsFp = nullptr;
            _lipsFID = 0;
        }
    }

    if (_lipsFID == 0) {
        _phone_anim = anim;
        _lipsFID = buildFid(OBJ_TYPE_HEAD, headFrmId, anim, 0, 0);
        _lipsFp = artLock(_lipsFID, &_lipsKey);
        if (_lipsFp == nullptr) {
            debugPrint("failure!\n");

            char stats[200];
            cachePrintStats(&gArtCache, stats, sizeof(stats));
            debugPrint("%s", stats);
        }
    }

    int fid = buildFid(OBJ_TYPE_HEAD, headFrmId, reaction, 0, 0);
    int fidgetCount = artGetFidgetCount(fid);
    if (fidgetCount == -1) {
        debugPrint("\tError - No available fidgets for given frame id\n");
        return;
    }

    int chance = randomBetween(1, 100) + _dialogue_seconds_since_last_input / 2;

    int fidget = fidgetCount;
    switch (fidgetCount) {
    case 1:
        fidget = 1;
        break;
    case 2:
        if (chance < 68) {
            fidget = 1;
        } else {
            fidget = 2;
        }
        break;
    case 3:
        _dialogue_seconds_since_last_input = 0;
        if (chance < 52) {
            fidget = 1;
        } else if (chance < 77) {
            fidget = 2;
        } else {
            fidget = 3;
        }
        break;
    }

    debugPrint("Choosing fidget %d out of %d\n", fidget, fidgetCount);

    if (gGameDialogFidgetFrm != nullptr) {
        if (artUnlock(gGameDialogFidgetFrmHandle) == -1) {
            debugPrint("failure!\n");
        }
    }

    gGameDialogFidgetFid = buildFid(OBJ_TYPE_HEAD, headFrmId, reaction, fidget, 0);
    gGameDialogFidgetFrmCurrentFrame = 0;
    gGameDialogFidgetFrm = artLock(gGameDialogFidgetFid, &gGameDialogFidgetFrmHandle);
    if (gGameDialogFidgetFrm == nullptr) {
        debugPrint("failure!\n");

        char stats[200];
        cachePrintStats(&gArtCache, stats, sizeof(stats));
        debugPrint("%s", stats);
    }

    gGameDialogFidgetLastUpdateTimestamp = 0;
    gGameDialogFidgetReaction = reaction;
    gGameDialogFidgetUpdateDelay = 1000 / artGetFramesPerSecond(gGameDialogFidgetFrm);
}

// 0x447598
void gameDialogWaitForFidgetToComplete()
{
    if (gGameDialogFidgetFrm == nullptr) {
        return;
    }

    if (gGameDialogWindow == -1) {
        return;
    }

    debugPrint("Waiting for fidget to complete...\n");

    while (artGetFrameCount(gGameDialogFidgetFrm) > gGameDialogFidgetFrmCurrentFrame) {
        sharedFpsLimiter.mark();

        if (getTicksSince(gGameDialogFidgetLastUpdateTimestamp) >= gGameDialogFidgetUpdateDelay) {
            gameDialogRenderTalkingHead(gGameDialogFidgetFrm, gGameDialogFidgetFrmCurrentFrame);
            gGameDialogFidgetLastUpdateTimestamp = getTicks();
            gGameDialogFidgetFrmCurrentFrame++;
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }

    gGameDialogFidgetFrmCurrentFrame = 0;
}

// 0x447614
void _gdPlayTransition(int anim)
{
    if (gGameDialogFidgetFrm == nullptr) {
        return;
    }

    if (gGameDialogWindow == -1) {
        return;
    }

    mouseHideCursor();

    debugPrint("Starting transition...\n");

    gameDialogWaitForFidgetToComplete();

    if (gGameDialogFidgetFrm != nullptr) {
        if (artUnlock(gGameDialogFidgetFrmHandle) == -1) {
            debugPrint("\tError unlocking fidget in transition func...");
        }
        gGameDialogFidgetFrm = nullptr;
    }

    CacheEntry* headFrmHandle;
    int headFid = buildFid(OBJ_TYPE_HEAD, gGameDialogHeadFid, anim, 0, 0);
    Art* headFrm = artLock(headFid, &headFrmHandle);
    if (headFrm == nullptr) {
        debugPrint("\tError locking transition...\n");
    }

    unsigned int delay = 1000 / artGetFramesPerSecond(headFrm);

    int frame = 0;
    unsigned int time = 0;
    while (frame < artGetFrameCount(headFrm)) {
        sharedFpsLimiter.mark();

        if (getTicksSince(time) >= delay) {
            gameDialogRenderTalkingHead(headFrm, frame);
            time = getTicks();
            frame++;
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }

    if (artUnlock(headFrmHandle) == -1) {
        debugPrint("\tError unlocking transition...\n");
    }

    debugPrint("Finished transition...\n");
    mouseShowCursor();
}

// 0x447724
void _reply_arrow_up(int btn, int keyCode)
{
    if (_gdReplyTooBig) {
        gameMouseSetCursor(MOUSE_CURSOR_SMALL_ARROW_UP);
    }
}

// 0x447738
void _reply_arrow_down(int btn, int keyCode)
{
    if (_gdReplyTooBig) {
        gameMouseSetCursor(MOUSE_CURSOR_SMALL_ARROW_DOWN);
    }
}

// 0x44774C
void _reply_arrow_restore(int btn, int keyCode)
{
    gameMouseSetCursor(MOUSE_CURSOR_ARROW);
}

// demo_copy_title
// 0x447758
void _demo_copy_title(int win)
{
    _gd_replyWin = win;

    if (win == -1) {
        debugPrint("\nError: demo_copy_title: win invalid!");
        return;
    }

    int width = windowGetWidth(win);
    if (width < 1) {
        debugPrint("\nError: demo_copy_title: width invalid!");
        return;
    }

    int height = windowGetHeight(win);
    if (height < 1) {
        debugPrint("\nError: demo_copy_title: length invalid!");
        return;
    }

    if (gGameDialogBackgroundWindow == -1) {
        debugPrint("\nError: demo_copy_title: dialogueBackWindow wasn't created!");
        return;
    }

    unsigned char* src = windowGetBuffer(gGameDialogBackgroundWindow);
    if (src == nullptr) {
        debugPrint("\nError: demo_copy_title: couldn't get buffer!");
        return;
    }

    unsigned char* dest = windowGetBuffer(win);

    blitBufferToBuffer(src + 640 * 225 + 135, width, height, 640, dest, width);
}

// demo_copy_options
// 0x447818
void _demo_copy_options(int win)
{
    _gd_optionsWin = win;

    if (win == -1) {
        debugPrint("\nError: demo_copy_options: win invalid!");
        return;
    }

    int width = windowGetWidth(win);
    if (width < 1) {
        debugPrint("\nError: demo_copy_options: width invalid!");
        return;
    }

    int height = windowGetHeight(win);
    if (height < 1) {
        debugPrint("\nError: demo_copy_options: length invalid!");
        return;
    }

    if (gGameDialogBackgroundWindow == -1) {
        debugPrint("\nError: demo_copy_options: dialogueBackWindow wasn't created!");
        return;
    }

    Rect windowRect;
    windowGetRect(gGameDialogWindow, &windowRect);
    windowRect.left -= (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
    windowRect.top -= (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2;

    unsigned char* src = windowGetBuffer(gGameDialogWindow);
    if (src == nullptr) {
        debugPrint("\nError: demo_copy_options: couldn't get buffer!");
        return;
    }

    unsigned char* dest = windowGetBuffer(win);
    blitBufferToBuffer(src + 640 * (335 - windowRect.top) + 127, width, height, 640, dest, width);
}

// gDialogRefreshOptionsRect
// 0x447914
void _gDialogRefreshOptionsRect(int win, Rect* drawRect)
{
    if (drawRect == nullptr) {
        debugPrint("\nError: gDialogRefreshOptionsRect: drawRect NULL!");
        return;
    }

    if (win == -1) {
        debugPrint("\nError: gDialogRefreshOptionsRect: win invalid!");
        return;
    }

    if (gGameDialogBackgroundWindow == -1) {
        debugPrint("\nError: gDialogRefreshOptionsRect: dialogueBackWindow wasn't created!");
        return;
    }

    Rect windowRect;
    windowGetRect(gGameDialogWindow, &windowRect);
    windowRect.left -= (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
    windowRect.top -= (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2;

    unsigned char* src = windowGetBuffer(gGameDialogWindow);
    if (src == nullptr) {
        debugPrint("\nError: gDialogRefreshOptionsRect: couldn't get buffer!");
        return;
    }

    if (drawRect->top >= drawRect->bottom) {
        debugPrint("\nError: gDialogRefreshOptionsRect: Invalid Rect (too many options)!");
        return;
    }

    if (drawRect->left >= drawRect->right) {
        debugPrint("\nError: gDialogRefreshOptionsRect: Invalid Rect (too many options)!");
        return;
    }

    int destWidth = windowGetWidth(win);
    unsigned char* dest = windowGetBuffer(win);

    blitBufferToBuffer(
        src + (640 * (335 - windowRect.top) + 127) + (640 * drawRect->top + drawRect->left),
        drawRect->right - drawRect->left,
        drawRect->bottom - drawRect->top,
        640,
        dest + destWidth * drawRect->top,
        destWidth);
}

// 0x447A58
void gameDialogTicker()
{
    switch (_dialogue_switch_mode) {
    case 2:
        _loop_cnt = -1;
        _dialogue_switch_mode = 3;

        GameMode::enterGameMode(GameMode::kSpecial);

        _gdialog_window_destroy();
        _gdialog_barter_create_win();
        break;
    case 1:
        _loop_cnt = -1;
        _dialogue_switch_mode = 0;
        _gdialog_barter_destroy_win();
        _gdialog_window_create();

        // NOTE: Uninline.
        gdUnhide();

        if (_gd_optionsWin != -1) {
            // SFALL: Fix for the player's money not being displayed in the
            // dialog window after leaving the barter/combat control interface.
            gameDialogRenderCaps();
        }

        break;
    case 8:
        _loop_cnt = -1;
        _dialogue_switch_mode = 9;
        _gdialog_window_destroy();
        partyMemberControlWindowInit();
        break;
    case 11:
        _loop_cnt = -1;
        _dialogue_switch_mode = 12;
        _gdialog_window_destroy();
        partyMemberCustomizationWindowInit();
        break;
    }

    if (gGameDialogFidgetFrm == nullptr) {
        return;
    }

    if (gGameDialogLipSyncStarted) {
        lipsTicker();

        if (gLipsPhonemeChanged) {
            gameDialogRenderTalkingHead(_lipsFp, _head_phoneme_lookup[gLipsCurrentPhoneme]);
            gLipsPhonemeChanged = false;
        }

        if (!soundIsPlaying(gLipsData.sound)) {
            gameDialogEndLips();
            gameDialogRenderTalkingHead(_lipsFp, 0);
            _can_start_new_fidget = true;
            _dialogue_seconds_since_last_input = 3;
            gGameDialogFidgetFrmCurrentFrame = 0;
        }
        return;
    }

    if (_can_start_new_fidget) {
        if (getTicksSince(gGameDialogFidgetLastUpdateTimestamp) >= _tocksWaiting) {
            _can_start_new_fidget = false;
            _dialogue_seconds_since_last_input += _tocksWaiting / 1000;
            _tocksWaiting = 1000 * (randomBetween(0, 3) + 4);
            _gdSetupFidget(gGameDialogFidgetFid & 0xFFF, (gGameDialogFidgetFid & 0xFF0000) >> 16);
        }
        return;
    }

    if (getTicksSince(gGameDialogFidgetLastUpdateTimestamp) >= gGameDialogFidgetUpdateDelay) {
        if (artGetFrameCount(gGameDialogFidgetFrm) <= gGameDialogFidgetFrmCurrentFrame) {
            gameDialogRenderTalkingHead(gGameDialogFidgetFrm, 0);
            _can_start_new_fidget = true;
        } else {
            gameDialogRenderTalkingHead(gGameDialogFidgetFrm, gGameDialogFidgetFrmCurrentFrame);
            gGameDialogFidgetLastUpdateTimestamp = getTicks();
            gGameDialogFidgetFrmCurrentFrame += 1;
        }
    }
}

// FIXME: Due to the bug in `_gdProcessChoice` this function can receive invalid
// reaction value (50 instead of expected -1, 0, 1). It's handled gracefully by
// the game.
//
// 0x447CA0
void _talk_to_critter_reacts(int a1)
{
    int v1 = a1 + 1;

    debugPrint("Dialogue Reaction: ");
    if (v1 < 3) {
        debugPrint("%s\n", _react_strs[v1]);
    }

    int v3 = a1 + 50;
    _dialogue_seconds_since_last_input = 0;

    switch (v3) {
    case GAME_DIALOG_REACTION_GOOD:
        switch (gGameDialogFidgetReaction) {
        case FIDGET_GOOD:
            _gdPlayTransition(HEAD_ANIMATION_VERY_GOOD_REACTION);
            _gdSetupFidget(gGameDialogHeadFid, FIDGET_GOOD);
            break;
        case FIDGET_NEUTRAL:
            _gdPlayTransition(HEAD_ANIMATION_NEUTRAL_TO_GOOD);
            _gdSetupFidget(gGameDialogHeadFid, FIDGET_GOOD);
            break;
        case FIDGET_BAD:
            _gdPlayTransition(HEAD_ANIMATION_BAD_TO_NEUTRAL);
            _gdSetupFidget(gGameDialogHeadFid, FIDGET_NEUTRAL);
            break;
        }
        break;
    case GAME_DIALOG_REACTION_NEUTRAL:
        break;
    case GAME_DIALOG_REACTION_BAD:
        switch (gGameDialogFidgetReaction) {
        case FIDGET_GOOD:
            _gdPlayTransition(HEAD_ANIMATION_GOOD_TO_NEUTRAL);
            _gdSetupFidget(gGameDialogHeadFid, FIDGET_NEUTRAL);
            break;
        case FIDGET_NEUTRAL:
            _gdPlayTransition(HEAD_ANIMATION_NEUTRAL_TO_BAD);
            _gdSetupFidget(gGameDialogHeadFid, FIDGET_BAD);
            break;
        case FIDGET_BAD:
            _gdPlayTransition(HEAD_ANIMATION_VERY_BAD_REACTION);
            _gdSetupFidget(gGameDialogHeadFid, FIDGET_BAD);
            break;
        }
        break;
    }
}

// 0x447D98
void _gdialog_scroll_subwin(int win, int a2, unsigned char* a3, unsigned char* a4, unsigned char* a5, int a6, int a7)
{
    int v7;
    unsigned char* v9;
    Rect rect;

    v7 = a6;
    v9 = a4;

    if (a2 == 1) {
        rect.left = 0;
        rect.right = GAME_DIALOG_WINDOW_WIDTH - 1;
        rect.bottom = a6 - 1;

        int v18 = a6 / 10;
        if (a7 == -1) {
            rect.top = 10;
            v18 = 0;
        } else {
            rect.top = v18 * 10;
            v7 = a6 % 10;
            v9 += GAME_DIALOG_WINDOW_WIDTH * rect.top;
        }

        for (; v18 >= 0; v18--) {
            sharedFpsLimiter.mark();

            soundContinueAll();
            blitBufferToBuffer(a3,
                GAME_DIALOG_WINDOW_WIDTH,
                v7,
                GAME_DIALOG_WINDOW_WIDTH,
                v9,
                GAME_DIALOG_WINDOW_WIDTH);
            rect.top -= 10;
            windowRefreshRect(win, &rect);
            v7 += 10;
            v9 -= 10 * (GAME_DIALOG_WINDOW_WIDTH);

            delay_ms(33);

            renderPresent();
            sharedFpsLimiter.throttle();
        }
    } else {
        rect.right = GAME_DIALOG_WINDOW_WIDTH - 1;
        rect.bottom = a6 - 1;
        rect.left = 0;
        rect.top = 0;

        for (int index = a6 / 10; index > 0; index--) {
            sharedFpsLimiter.mark();

            soundContinueAll();

            blitBufferToBuffer(a5,
                GAME_DIALOG_WINDOW_WIDTH,
                10,
                GAME_DIALOG_WINDOW_WIDTH,
                v9,
                GAME_DIALOG_WINDOW_WIDTH);

            v9 += 10 * (GAME_DIALOG_WINDOW_WIDTH);
            v7 -= 10;
            a5 += 10 * (GAME_DIALOG_WINDOW_WIDTH);

            blitBufferToBuffer(a3,
                GAME_DIALOG_WINDOW_WIDTH,
                v7,
                GAME_DIALOG_WINDOW_WIDTH,
                v9,
                GAME_DIALOG_WINDOW_WIDTH);

            windowRefreshRect(win, &rect);

            rect.top += 10;

            delay_ms(33);

            renderPresent();
            sharedFpsLimiter.throttle();
        }
    }
}

// 0x447F64
int _text_num_lines(const char* a1, int a2)
{
    int width = fontGetStringWidth(a1);

    int v1 = 0;
    while (width > 0) {
        width -= a2;
        v1++;
    }

    return v1;
}

// NOTE: Inlined.
//
// 0x447F80
static int text_to_rect_wrapped(unsigned char* buffer, Rect* rect, char* string, int* a4, int height, int pitch, int color)
{
    return gameDialogDrawText(buffer, rect, string, a4, height, pitch, color, 1);
}

// display_msg
// 0x447FA0
int gameDialogDrawText(unsigned char* buffer, Rect* rect, char* string, int* a4, int height, int pitch, int color, int a7)
{
    char* start;
    if (a4 != nullptr) {
        start = string + *a4;
    } else {
        start = string;
    }

    int maxWidth = rect->right - rect->left;
    char* end = nullptr;
    while (start != nullptr && *start != '\0') {
        if (fontGetStringWidth(start) > maxWidth) {
            end = start + 1;
            while (*end != '\0' && *end != ' ') {
                end++;
            }

            if (*end != '\0') {
                char* lookahead = end + 1;
                while (lookahead != nullptr) {
                    while (*lookahead != '\0' && *lookahead != ' ') {
                        lookahead++;
                    }

                    if (*lookahead == '\0') {
                        lookahead = nullptr;
                    } else {
                        *lookahead = '\0';
                        if (fontGetStringWidth(start) >= maxWidth) {
                            *lookahead = ' ';
                            lookahead = nullptr;
                        } else {
                            end = lookahead;
                            *lookahead = ' ';
                            lookahead++;
                        }
                    }
                }

                if (*end == ' ') {
                    *end = '\0';
                }
            } else {
                if (rect->bottom - fontGetLineHeight() < rect->top) {
                    return rect->top;
                }

                if (a7 != 1 || start == string) {
                    fontDrawText(buffer + pitch * rect->top + 10, start, maxWidth, pitch, color);
                } else {
                    fontDrawText(buffer + pitch * rect->top, start, maxWidth, pitch, color);
                }

                if (a4 != nullptr) {
                    *a4 += static_cast<int>(strlen(start)) + 1;
                }

                rect->top += height;
                return rect->top;
            }
        }

        if (fontGetStringWidth(start) > maxWidth) {
            debugPrint("\nError: display_msg: word too long!");
            break;
        }

        if (a7 != 0) {
            if (rect->bottom - fontGetLineHeight() < rect->top) {
                if (end != nullptr && *end == '\0') {
                    *end = ' ';
                }
                return rect->top;
            }

            unsigned char* dest;
            if (a7 != 1 || start == string) {
                dest = buffer + 10;
            } else {
                dest = buffer;
            }
            fontDrawText(dest + pitch * rect->top, start, maxWidth, pitch, color);
        }

        if (a4 != nullptr && end != nullptr) {
            *a4 += static_cast<int>(strlen(start)) + 1;
        }

        rect->top += height;

        if (end != nullptr) {
            start = end + 1;
            if (*end == '\0') {
                *end = ' ';
            }
            end = nullptr;
        } else {
            start = nullptr;
        }
    }

    if (a4 != nullptr) {
        *a4 = 0;
    }

    return rect->top;
}

// 0x448214
void gameDialogSetBarterModifier(int modifier)
{
    gGameDialogBarterModifier = modifier;
}

// gdialog_barter
// 0x44821C
int gameDialogBarter(int modifier)
{
    if (!_dialog_state_fix) {
        return -1;
    }

    gGameDialogBarterModifier = modifier;
    gameDialogBarterButtonUpMouseUp(-1, -1);
    _dialogue_state = 4;
    _dialogue_switch_mode = 2;

    return 0;
}

// 0x448268
void _barter_end_to_talk_to()
{
    _dialogQuit();
    _dialogClose();
    _updatePrograms();
    _updateWindows();
    _dialogue_state = 1;
    _dialogue_switch_mode = 1;
}

// 0x448290
int _gdialog_barter_create_win()
{
    _dialogue_state = 4;

    int frmId;
    if (gGameDialogSpeakerIsPartyMember) {
        // trade.frm - party member barter/trade interface
        frmId = 420;
    } else {
        // barter.frm - barter window
        frmId = 111;
    }

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, frmId, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    unsigned char* backgroundData = backgroundFrmImage.getData();
    if (backgroundData == nullptr) {
        return -1;
    }

    _dialogue_subwin_len = backgroundFrmImage.getHeight();

    int barterWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
    int barterWindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2 + GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len;
    gGameDialogWindow = windowCreate(barterWindowX,
        barterWindowY,
        GAME_DIALOG_WINDOW_WIDTH,
        _dialogue_subwin_len,
        256,
        WINDOW_DONT_MOVE_TOP);
    if (gGameDialogWindow == -1) {
        return -1;
    }

    int width = GAME_DIALOG_WINDOW_WIDTH;

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
    unsigned char* backgroundWindowBuffer = windowGetBuffer(gGameDialogBackgroundWindow);
    blitBufferToBuffer(backgroundWindowBuffer + width * (480 - _dialogue_subwin_len), width, _dialogue_subwin_len, width, windowBuffer, width);

    _gdialog_scroll_subwin(gGameDialogWindow, 1, backgroundData, windowBuffer, nullptr, _dialogue_subwin_len, 0);

    backgroundFrmImage.unlock();

    // TRADE
    _gdialog_buttons[0] = buttonCreate(gGameDialogWindow, 41, 163, 14, 14, -1, -1, -1, KEY_LOWERCASE_M, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (_gdialog_buttons[0] != -1) {
        buttonSetCallbacks(_gdialog_buttons[0], _gsound_med_butt_press, _gsound_med_butt_release);

        // TALK
        _gdialog_buttons[1] = buttonCreate(gGameDialogWindow, 584, 162, 14, 14, -1, -1, -1, KEY_LOWERCASE_T, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
        if (_gdialog_buttons[1] != -1) {
            buttonSetCallbacks(_gdialog_buttons[1], _gsound_med_butt_press, _gsound_med_butt_release);

            if (objectCreateWithFidPid(&_peon_table_obj, -1, -1) != -1) {
                _peon_table_obj->flags |= OBJECT_HIDDEN;

                if (objectCreateWithFidPid(&_barterer_table_obj, -1, -1) != -1) {
                    _barterer_table_obj->flags |= OBJECT_HIDDEN;

                    if (objectCreateWithFidPid(&_barterer_temp_obj, gGameDialogSpeaker->fid, -1) != -1) {
                        _barterer_temp_obj->flags |= OBJECT_HIDDEN | OBJECT_NO_SAVE;
                        _barterer_temp_obj->sid = -1;
                        return 0;
                    }

                    objectDestroy(_barterer_table_obj, nullptr);
                }

                objectDestroy(_peon_table_obj, nullptr);
            }

            buttonDestroy(_gdialog_buttons[1]);
            _gdialog_buttons[1] = -1;
        }

        buttonDestroy(_gdialog_buttons[0]);
        _gdialog_buttons[0] = -1;
    }

    windowDestroy(gGameDialogWindow);
    gGameDialogWindow = -1;

    return -1;
}

// 0x44854C
void _gdialog_barter_destroy_win()
{
    if (gGameDialogWindow == -1) {
        return;
    }

    objectDestroy(_barterer_temp_obj, nullptr);
    objectDestroy(_barterer_table_obj, nullptr);
    objectDestroy(_peon_table_obj, nullptr);

    for (int index = 0; index < 9; index++) {
        buttonDestroy(_gdialog_buttons[index]);
        _gdialog_buttons[index] = -1;
    }

    unsigned char* backgroundWindowBuffer = windowGetBuffer(gGameDialogBackgroundWindow);
    backgroundWindowBuffer += (GAME_DIALOG_WINDOW_WIDTH) * (480 - _dialogue_subwin_len);

    int frmId;
    if (gGameDialogSpeakerIsPartyMember) {
        // trade.frm - party member barter/trade interface
        frmId = 420;
    } else {
        // barter.frm - barter window
        frmId = 111;
    }

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, frmId, 0, 0, 0);
    if (backgroundFrmImage.lock(backgroundFid)) {
        unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
        _gdialog_scroll_subwin(gGameDialogWindow, 0, backgroundFrmImage.getData(), windowBuffer, backgroundWindowBuffer, _dialogue_subwin_len, 0);
    }

    windowDestroy(gGameDialogWindow);
    gGameDialogWindow = -1;

    aiAttemptWeaponReload(gGameDialogSpeaker, 0);
}

// 0x448660
void _gdialog_barter_cleanup_tables()
{
    Inventory* inventory;
    int length;

    inventory = &(_peon_table_obj->data.inventory);
    length = inventory->length;
    for (int index = 0; index < length; index++) {
        Object* item = inventory->items->item;
        int quantity = itemGetQuantity(_peon_table_obj, item);
        itemMoveForce(_peon_table_obj, gDude, item, quantity);
    }

    inventory = &(_barterer_table_obj->data.inventory);
    length = inventory->length;
    for (int index = 0; index < length; index++) {
        Object* item = inventory->items->item;
        int quantity = itemGetQuantity(_barterer_table_obj, item);
        itemMoveForce(_barterer_table_obj, gGameDialogSpeaker, item, quantity);
    }

    if (_barterer_temp_obj != nullptr) {
        inventory = &(_barterer_temp_obj->data.inventory);
        length = inventory->length;
        for (int index = 0; index < length; index++) {
            Object* item = inventory->items->item;
            int quantity = itemGetQuantity(_barterer_temp_obj, item);
            itemMoveForce(_barterer_temp_obj, gGameDialogSpeaker, item, quantity);
        }
    }
}

// 0x448740
int partyMemberControlWindowInit()
{
    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 390, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    unsigned char* backgroundData = backgroundFrmImage.getData();
    if (backgroundData == nullptr) {
        partyMemberControlWindowFree();
        return -1;
    }

    _dialogue_subwin_len = backgroundFrmImage.getHeight();
    int controlWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
    int controlWindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2 + GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len;
    gGameDialogWindow = windowCreate(controlWindowX,
        controlWindowY,
        GAME_DIALOG_WINDOW_WIDTH,
        _dialogue_subwin_len,
        256,
        WINDOW_DONT_MOVE_TOP);
    if (gGameDialogWindow == -1) {
        partyMemberControlWindowFree();
        return -1;
    }

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
    unsigned char* src = windowGetBuffer(gGameDialogBackgroundWindow);
    blitBufferToBuffer(src + (GAME_DIALOG_WINDOW_WIDTH) * (GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len), GAME_DIALOG_WINDOW_WIDTH, _dialogue_subwin_len, GAME_DIALOG_WINDOW_WIDTH, windowBuffer, GAME_DIALOG_WINDOW_WIDTH);
    _gdialog_scroll_subwin(gGameDialogWindow, 1, backgroundData, windowBuffer, nullptr, _dialogue_subwin_len, 0);
    backgroundFrmImage.unlock();

    // TALK
    _gdialog_buttons[0] = buttonCreate(gGameDialogWindow, 593, 41, 14, 14, -1, -1, -1, KEY_ESCAPE, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (_gdialog_buttons[0] == -1) {
        partyMemberControlWindowFree();
        return -1;
    }
    buttonSetCallbacks(_gdialog_buttons[0], _gsound_med_butt_press, _gsound_med_butt_release);

    // TRADE
    _gdialog_buttons[1] = buttonCreate(gGameDialogWindow, 593, 97, 14, 14, -1, -1, -1, KEY_LOWERCASE_D, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (_gdialog_buttons[1] == -1) {
        partyMemberControlWindowFree();
        return -1;
    }
    buttonSetCallbacks(_gdialog_buttons[1], _gsound_med_butt_press, _gsound_med_butt_release);

    // USE BEST WEAPON
    _gdialog_buttons[2] = buttonCreate(gGameDialogWindow, 236, 15, 14, 14, -1, -1, -1, KEY_LOWERCASE_W, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (_gdialog_buttons[2] == -1) {
        partyMemberControlWindowFree();
        return -1;
    }
    buttonSetCallbacks(_gdialog_buttons[1], _gsound_med_butt_press, _gsound_med_butt_release);

    // USE BEST ARMOR
    _gdialog_buttons[3] = buttonCreate(gGameDialogWindow, 235, 46, 14, 14, -1, -1, -1, KEY_LOWERCASE_A, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (_gdialog_buttons[3] == -1) {
        partyMemberControlWindowFree();
        return -1;
    }
    buttonSetCallbacks(_gdialog_buttons[2], _gsound_med_butt_press, _gsound_med_butt_release);

    _control_buttons_start = 4;

    int v21 = 3;

    for (int index = 0; index < 5; index++) {
        GameDialogButtonData* buttonData = &(gGameDialogDispositionButtonsData[index]);
        int fid;

        fid = buildFid(OBJ_TYPE_INTERFACE, buttonData->upFrmId, 0, 0, 0);
        Art* upButtonFrm = artLock(fid, &(buttonData->upFrmHandle));
        if (upButtonFrm == nullptr) {
            partyMemberControlWindowFree();
            return -1;
        }

        int width = artGetWidth(upButtonFrm, 0, 0);
        int height = artGetHeight(upButtonFrm, 0, 0);
        unsigned char* upButtonFrmData = artGetFrameData(upButtonFrm, 0, 0);

        fid = buildFid(OBJ_TYPE_INTERFACE, buttonData->downFrmId, 0, 0, 0);
        Art* downButtonFrm = artLock(fid, &(buttonData->downFrmHandle));
        if (downButtonFrm == nullptr) {
            partyMemberControlWindowFree();
            return -1;
        }

        unsigned char* downButtonFrmData = artGetFrameData(downButtonFrm, 0, 0);

        fid = buildFid(OBJ_TYPE_INTERFACE, buttonData->disabledFrmId, 0, 0, 0);
        Art* disabledButtonFrm = artLock(fid, &(buttonData->disabledFrmHandle));
        if (disabledButtonFrm == nullptr) {
            partyMemberControlWindowFree();
            return -1;
        }

        unsigned char* disabledButtonFrmData = artGetFrameData(disabledButtonFrm, 0, 0);

        v21++;

        _gdialog_buttons[v21] = buttonCreate(gGameDialogWindow,
            buttonData->x,
            buttonData->y,
            width,
            height,
            -1,
            -1,
            buttonData->keyCode,
            -1,
            upButtonFrmData,
            downButtonFrmData,
            nullptr,
            BUTTON_FLAG_TRANSPARENT | BUTTON_FLAG_0x04 | BUTTON_FLAG_0x01);
        if (_gdialog_buttons[v21] == -1) {
            partyMemberControlWindowFree();
            return -1;
        }

        _win_register_button_disable(_gdialog_buttons[v21], disabledButtonFrmData, disabledButtonFrmData, disabledButtonFrmData);
        buttonSetCallbacks(_gdialog_buttons[v21], _gsound_med_butt_press, _gsound_med_butt_release);

        if (!partyMemberSupportsDisposition(gGameDialogSpeaker, buttonData->value)) {
            buttonDisable(_gdialog_buttons[v21]);
        }
    }

    _win_group_radio_buttons(5, &(_gdialog_buttons[_control_buttons_start]));

    int disposition = aiGetDisposition(gGameDialogSpeaker);
    _win_set_button_rest_state(_gdialog_buttons[_control_buttons_start + 4 - disposition], 1, 0);

    partyMemberControlWindowUpdate();

    _dialogue_state = 10;

    windowRefresh(gGameDialogWindow);

    return 0;
}

// 0x448C10
void partyMemberControlWindowFree()
{
    if (gGameDialogWindow == -1) {
        return;
    }

    for (int index = 0; index < 9; index++) {
        buttonDestroy(_gdialog_buttons[index]);
        _gdialog_buttons[index] = -1;
    }

    for (int index = 0; index < 5; index++) {
        GameDialogButtonData* buttonData = &(gGameDialogDispositionButtonsData[index]);

        if (buttonData->upFrmHandle) {
            artUnlock(buttonData->upFrmHandle);
            buttonData->upFrmHandle = nullptr;
        }

        if (buttonData->downFrmHandle) {
            artUnlock(buttonData->downFrmHandle);
            buttonData->downFrmHandle = nullptr;
        }

        if (buttonData->disabledFrmHandle) {
            artUnlock(buttonData->disabledFrmHandle);
            buttonData->disabledFrmHandle = nullptr;
        }
    }

    // control.frm - party member control interface
    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 390, 0, 0, 0);
    if (backgroundFrmImage.lock(backgroundFid)) {
        _gdialog_scroll_subwin(gGameDialogWindow, 0, backgroundFrmImage.getData(), windowGetBuffer(gGameDialogWindow), windowGetBuffer(gGameDialogBackgroundWindow) + (GAME_DIALOG_WINDOW_WIDTH) * (480 - _dialogue_subwin_len), _dialogue_subwin_len, 0);
    }

    windowDestroy(gGameDialogWindow);
    gGameDialogWindow = -1;
}

// 0x448D30
void partyMemberControlWindowUpdate()
{
    int oldFont = fontGetCurrent();
    fontSetCurrent(101);

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
    int windowWidth = windowGetWidth(gGameDialogWindow);

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 390, 0, 0, 0);
    if (backgroundFrmImage.lock(backgroundFid)) {
        int width = backgroundFrmImage.getWidth();
        unsigned char* buffer = backgroundFrmImage.getData();

        // Clear "Weapon Used:".
        blitBufferToBuffer(buffer + width * 20 + 112, 110, fontGetLineHeight(), width, windowBuffer + windowWidth * 20 + 112, windowWidth);

        // Clear "Armor Used:".
        blitBufferToBuffer(buffer + width * 49 + 112, 110, fontGetLineHeight(), width, windowBuffer + windowWidth * 49 + 112, windowWidth);

        // Clear character preview.
        blitBufferToBuffer(buffer + width * 84 + 8, 70, 98, width, windowBuffer + windowWidth * 84 + 8, windowWidth);

        // Clear ?
        blitBufferToBuffer(buffer + width * 80 + 232, 132, 106, width, windowBuffer + windowWidth * 80 + 232, windowWidth);

        backgroundFrmImage.unlock();
    }

    MessageListItem messageListItem;
    char* text;
    char formattedText[256];

    // Render item in right hand.
    Object* item2 = critterGetItem2(gGameDialogSpeaker);
    text = item2 != nullptr ? itemGetName(item2) : getmsg(&gProtoMessageList, &messageListItem, 10);
    snprintf(formattedText, sizeof(formattedText), "%s", text);
    fontDrawText(windowBuffer + windowWidth * 20 + 112, formattedText, 110, windowWidth, _colorTable[992]);

    // Render armor.
    Object* armor = critterGetArmor(gGameDialogSpeaker);
    text = armor != nullptr ? itemGetName(armor) : getmsg(&gProtoMessageList, &messageListItem, 10);
    snprintf(formattedText, sizeof(formattedText), "%s", text);
    fontDrawText(windowBuffer + windowWidth * 49 + 112, formattedText, 110, windowWidth, _colorTable[992]);

    // Render preview.
    CacheEntry* previewHandle;
    int previewFid = buildFid(FID_TYPE(gGameDialogSpeaker->fid), gGameDialogSpeaker->fid & 0xFFF, ANIM_STAND, (gGameDialogSpeaker->fid & 0xF000) >> 12, ROTATION_SW);
    Art* preview = artLock(previewFid, &previewHandle);
    if (preview != nullptr) {
        int width = artGetWidth(preview, 0, ROTATION_SW);
        int height = artGetHeight(preview, 0, ROTATION_SW);
        unsigned char* buffer = artGetFrameData(preview, 0, ROTATION_SW);
        blitBufferToBufferTrans(buffer, width, height, width, windowBuffer + windowWidth * (132 - height / 2) + 39 - width / 2, windowWidth);
        artUnlock(previewHandle);
    }

    // Render hit points.
    int maximumHitPoints = critterGetStat(gGameDialogSpeaker, STAT_MAXIMUM_HIT_POINTS);
    int hitPoints = critterGetStat(gGameDialogSpeaker, STAT_CURRENT_HIT_POINTS);
    snprintf(formattedText, sizeof(formattedText), "%d/%d", hitPoints, maximumHitPoints);
    fontDrawText(windowBuffer + windowWidth * 96 + 240, formattedText, 115, windowWidth, _colorTable[992]);

    // Render best skill.
    int bestSkill = partyMemberGetBestSkill(gGameDialogSpeaker);
    text = skillGetName(bestSkill);
    snprintf(formattedText, sizeof(formattedText), "%s", text);
    fontDrawText(windowBuffer + windowWidth * 113 + 240, formattedText, 115, windowWidth, _colorTable[992]);

    // Render weight summary.
    int inventoryWeight = objectGetInventoryWeight(gGameDialogSpeaker);
    int carryWeight = critterGetStat(gGameDialogSpeaker, STAT_CARRY_WEIGHT);
    snprintf(formattedText, sizeof(formattedText), "%d/%d ", inventoryWeight, carryWeight);
    fontDrawText(windowBuffer + windowWidth * 131 + 240, formattedText, 115, windowWidth, critterIsEncumbered(gGameDialogSpeaker) ? _colorTable[31744] : _colorTable[992]);

    // Render melee damage.
    int meleeDamage = critterGetStat(gGameDialogSpeaker, STAT_MELEE_DAMAGE);
    snprintf(formattedText, sizeof(formattedText), "%d", meleeDamage);
    fontDrawText(windowBuffer + windowWidth * 148 + 240, formattedText, 115, windowWidth, _colorTable[992]);

    int actionPoints;
    if (isInCombat()) {
        actionPoints = gGameDialogSpeaker->data.critter.combat.ap;
    } else {
        actionPoints = critterGetStat(gGameDialogSpeaker, STAT_MAXIMUM_ACTION_POINTS);
    }
    int maximumActionPoints = critterGetStat(gGameDialogSpeaker, STAT_MAXIMUM_ACTION_POINTS);
    snprintf(formattedText, sizeof(formattedText), "%d/%d ", actionPoints, maximumActionPoints);
    fontDrawText(windowBuffer + windowWidth * 167 + 240, formattedText, 115, windowWidth, _colorTable[992]);

    fontSetCurrent(oldFont);
    windowRefresh(gGameDialogWindow);
}

// 0x44928C
void gameDialogCombatControlButtonOnMouseUp(int btn, int keyCode)
{
    _dialogue_switch_mode = 8;
    _dialogue_state = 10;

    // NOTE: Uninline.
    gdHide();
}

// 0x4492D0
int _gdPickAIUpdateMsg(Object* critter)
{
    int pids[3];
    memcpy(pids, _Dogs, sizeof(pids));

    for (int index = 0; index < 3; index++) {
        if (critter->pid == pids[index]) {
            return 677 + randomBetween(0, 1);
        }
    }

    return 670 + randomBetween(0, 4);
}

// 0x449330
int _gdCanBarter()
{
    if (PID_TYPE(gGameDialogSpeaker->pid) != OBJ_TYPE_CRITTER) {
        return 1;
    }

    Proto* proto;
    if (protoGetProto(gGameDialogSpeaker->pid, &proto) == -1) {
        return 1;
    }

    if (proto->critter.data.flags & CRITTER_BARTER) {
        return 1;
    }

    MessageListItem messageListItem;

    // This person will not barter with you.
    messageListItem.num = 903;
    if (gGameDialogSpeakerIsPartyMember) {
        // This critter can't carry anything.
        messageListItem.num = 913;
    }

    if (!messageListGetItem(&gProtoMessageList, &messageListItem)) {
        debugPrint("\nError: gdialog: Can't find message!");
        return 0;
    }

    gameDialogRenderSupplementaryMessage(messageListItem.text);

    return 0;
}

// 0x4493B8
void partyMemberControlWindowHandleEvents()
{
    MessageListItem messageListItem;

    bool done = false;
    while (!done) {
        sharedFpsLimiter.mark();

        int keyCode = inputGetInput();
        if (keyCode != -1) {
            if (keyCode == KEY_CTRL_Q || keyCode == KEY_CTRL_X || keyCode == KEY_F10) {
                showQuitConfirmationDialog();
            }

            if (_game_user_wants_to_quit != 0) {
                break;
            }

            if (keyCode == KEY_LOWERCASE_W) {
                _inven_unwield(gGameDialogSpeaker, 1);

                Object* weapon = _ai_search_inven_weap(gGameDialogSpeaker, 0, nullptr);
                if (weapon != nullptr) {
                    _inven_wield(gGameDialogSpeaker, weapon, 1);
                    aiAttemptWeaponReload(gGameDialogSpeaker, 0);

                    int num = _gdPickAIUpdateMsg(gGameDialogSpeaker);
                    char* msg = getmsg(&gProtoMessageList, &messageListItem, num);
                    gameDialogRenderSupplementaryMessage(msg);
                    partyMemberControlWindowUpdate();
                }
            } else if (keyCode == 2098) {
                aiSetDisposition(gGameDialogSpeaker, 4);
            } else if (keyCode == 2099) {
                aiSetDisposition(gGameDialogSpeaker, 0);
                _dialogue_state = 13;
                _dialogue_switch_mode = 11;
                done = true;
            } else if (keyCode == 2102) {
                aiSetDisposition(gGameDialogSpeaker, 2);
            } else if (keyCode == 2103) {
                aiSetDisposition(gGameDialogSpeaker, 3);
            } else if (keyCode == 2111) {
                aiSetDisposition(gGameDialogSpeaker, 1);
            } else if (keyCode == KEY_ESCAPE) {
                _dialogue_switch_mode = 1;
                _dialogue_state = 1;
                return;
            } else if (keyCode == KEY_LOWERCASE_A) {
                if (gGameDialogSpeaker->pid != 0x10000A1) {
                    Object* armor = _ai_search_inven_armor(gGameDialogSpeaker);
                    if (armor != nullptr) {
                        _inven_wield(gGameDialogSpeaker, armor, 0);
                    }
                }

                int num = _gdPickAIUpdateMsg(gGameDialogSpeaker);
                char* msg = getmsg(&gProtoMessageList, &messageListItem, num);
                gameDialogRenderSupplementaryMessage(msg);
                partyMemberControlWindowUpdate();
            } else if (keyCode == KEY_LOWERCASE_D) {
                if (_gdCanBarter()) {
                    _dialogue_switch_mode = 2;
                    _dialogue_state = 4;
                    return;
                }
            } else if (keyCode == -2) {
                // CE: Minor improvement - handle on mouse up (just like other
                // buttons). Also fixed active button area (in original code
                // it's slightly smaller than the button itself).
                if ((mouseGetEvent() & MOUSE_EVENT_LEFT_BUTTON_UP) != 0) {
                    if (mouseHitTestInWindow(gGameDialogWindow, 438, 156, 438 + 109, 156 + 28)) {
                        aiSetDisposition(gGameDialogSpeaker, 0);
                        _dialogue_state = 13;
                        _dialogue_switch_mode = 11;
                        done = true;
                    }
                }
            }
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }
}

// 0x4496A0
int partyMemberCustomizationWindowInit()
{
    if (!messageListInit(&gCustomMessageList)) {
        return -1;
    }

    if (!messageListLoad(&gCustomMessageList, "game\\custom.msg")) {
        return -1;
    }

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 391, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    unsigned char* backgroundFrmData = backgroundFrmImage.getData();
    if (backgroundFrmData == nullptr) {
        // FIXME: Leaking background.
        partyMemberCustomizationWindowFree();
        return -1;
    }

    _dialogue_subwin_len = backgroundFrmImage.getHeight();

    int customizationWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
    int customizationWindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2 + GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len;
    gGameDialogWindow = windowCreate(customizationWindowX,
        customizationWindowY,
        GAME_DIALOG_WINDOW_WIDTH,
        _dialogue_subwin_len,
        256,
        WINDOW_DONT_MOVE_TOP);
    if (gGameDialogWindow == -1) {
        partyMemberCustomizationWindowFree();
        return -1;
    }

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
    unsigned char* parentWindowBuffer = windowGetBuffer(gGameDialogBackgroundWindow);
    blitBufferToBuffer(parentWindowBuffer + (GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len) * GAME_DIALOG_WINDOW_WIDTH,
        GAME_DIALOG_WINDOW_WIDTH,
        _dialogue_subwin_len,
        GAME_DIALOG_WINDOW_WIDTH,
        windowBuffer,
        GAME_DIALOG_WINDOW_WIDTH);

    _gdialog_scroll_subwin(gGameDialogWindow, 1, backgroundFrmData, windowBuffer, nullptr, _dialogue_subwin_len, 0);
    backgroundFrmImage.unlock();

    _gdialog_buttons[0] = buttonCreate(gGameDialogWindow, 593, 101, 14, 14, -1, -1, -1, 13, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (_gdialog_buttons[0] == -1) {
        partyMemberCustomizationWindowFree();
        return -1;
    }

    buttonSetCallbacks(_gdialog_buttons[0], _gsound_med_butt_press, _gsound_med_butt_release);

    int optionButton = 0;
    _custom_buttons_start = 1;

    for (int index = 0; index < PARTY_MEMBER_CUSTOMIZATION_OPTION_COUNT; index++) {
        GameDialogButtonData* buttonData = &(_custom_button_info[index]);

        int upButtonFid = buildFid(OBJ_TYPE_INTERFACE, buttonData->upFrmId, 0, 0, 0);
        Art* upButtonFrm = artLock(upButtonFid, &(buttonData->upFrmHandle));
        if (upButtonFrm == nullptr) {
            partyMemberCustomizationWindowFree();
            return -1;
        }

        int width = artGetWidth(upButtonFrm, 0, 0);
        int height = artGetHeight(upButtonFrm, 0, 0);
        unsigned char* upButtonFrmData = artGetFrameData(upButtonFrm, 0, 0);

        int downButtonFid = buildFid(OBJ_TYPE_INTERFACE, buttonData->downFrmId, 0, 0, 0);
        Art* downButtonFrm = artLock(downButtonFid, &(buttonData->downFrmHandle));
        if (downButtonFrm == nullptr) {
            partyMemberCustomizationWindowFree();
            return -1;
        }

        unsigned char* downButtonFrmData = artGetFrameData(downButtonFrm, 0, 0);

        optionButton++;
        _gdialog_buttons[optionButton] = buttonCreate(gGameDialogWindow,
            buttonData->x,
            buttonData->y,
            width,
            height,
            -1,
            -1,
            -1,
            buttonData->keyCode,
            upButtonFrmData,
            downButtonFrmData,
            nullptr,
            BUTTON_FLAG_TRANSPARENT);
        if (_gdialog_buttons[optionButton] == -1) {
            partyMemberCustomizationWindowFree();
            return -1;
        }

        buttonSetCallbacks(_gdialog_buttons[index], _gsound_med_butt_press, _gsound_med_butt_release);
    }

    _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE] = aiGetAreaAttackMode(gGameDialogSpeaker);
    _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE] = aiGetRunAwayMode(gGameDialogSpeaker);
    _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON] = aiGetBestWeapon(gGameDialogSpeaker);
    _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE] = aiGetDistance(gGameDialogSpeaker);
    _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO] = aiGetAttackWho(gGameDialogSpeaker);
    _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE] = aiGetChemUse(gGameDialogSpeaker);

    _dialogue_state = 13;

    partyMemberCustomizationWindowUpdate();

    return 0;
}

// 0x449A10
void partyMemberCustomizationWindowFree()
{
    if (gGameDialogWindow == -1) {
        return;
    }

    for (int index = 0; index < 9; index++) {
        buttonDestroy(_gdialog_buttons[index]);
        _gdialog_buttons[index] = -1;
    }

    for (int index = 0; index < PARTY_MEMBER_CUSTOMIZATION_OPTION_COUNT; index++) {
        GameDialogButtonData* buttonData = &(_custom_button_info[index]);

        if (buttonData->upFrmHandle != nullptr) {
            artUnlock(buttonData->upFrmHandle);
            buttonData->upFrmHandle = nullptr;
        }

        if (buttonData->downFrmHandle != nullptr) {
            artUnlock(buttonData->downFrmHandle);
            buttonData->downFrmHandle = nullptr;
        }

        if (buttonData->disabledFrmHandle != nullptr) {
            artUnlock(buttonData->disabledFrmHandle);
            buttonData->disabledFrmHandle = nullptr;
        }
    }

    FrmImage backgroundFrmImage;
    // custom.frm - party member control interface
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 391, 0, 0, 0);
    if (backgroundFrmImage.lock(backgroundFid)) {
        _gdialog_scroll_subwin(gGameDialogWindow, 0, backgroundFrmImage.getData(), windowGetBuffer(gGameDialogWindow), windowGetBuffer(gGameDialogBackgroundWindow) + (GAME_DIALOG_WINDOW_WIDTH) * (480 - _dialogue_subwin_len), _dialogue_subwin_len, 0);
    }

    windowDestroy(gGameDialogWindow);
    gGameDialogWindow = -1;

    messageListFree(&gCustomMessageList);
}

// 0x449B3C
void partyMemberCustomizationWindowHandleEvents()
{
    bool done = false;
    while (!done) {
        sharedFpsLimiter.mark();

        unsigned int keyCode = inputGetInput();
        if (keyCode != -1) {
            if (keyCode == KEY_CTRL_Q || keyCode == KEY_CTRL_X || keyCode == KEY_F10) {
                showQuitConfirmationDialog();
            }

            if (_game_user_wants_to_quit != 0) {
                break;
            }

            if (keyCode <= 5) {
                _gdCustomSelect(keyCode);
                partyMemberCustomizationWindowUpdate();
            } else if (keyCode == KEY_RETURN || keyCode == KEY_ESCAPE) {
                done = true;
                _dialogue_switch_mode = 8;
                _dialogue_state = 10;
            }
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }
}

// 0x449BB4
void partyMemberCustomizationWindowUpdate()
{
    int oldFont = fontGetCurrent();
    fontSetCurrent(101);

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
    int windowWidth = windowGetWidth(gGameDialogWindow);

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 391, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return;
    }

    int backgroundWidth = backgroundFrmImage.getWidth();
    int backgroundHeight = backgroundFrmImage.getHeight();
    unsigned char* backgroundData = backgroundFrmImage.getData();
    blitBufferToBuffer(backgroundData, backgroundWidth, backgroundHeight, backgroundWidth, windowBuffer, GAME_DIALOG_WINDOW_WIDTH);

    backgroundFrmImage.unlock();

    MessageListItem messageListItem;
    int num;
    char* msg;

    // BURST
    if (_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE] == -1) {
        // Not Applicable
        num = 99;
    } else {
        debugPrint("\nburst: %d", _custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE]);
        num = _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE][_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE]].messageId;
    }

    msg = getmsg(&gCustomMessageList, &messageListItem, num);
    fontDrawText(windowBuffer + windowWidth * 20 + 232, msg, 248, windowWidth, _colorTable[992]);

    // RUN AWAY
    msg = getmsg(&gCustomMessageList, &messageListItem, _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE][_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE]].messageId);
    fontDrawText(windowBuffer + windowWidth * 48 + 232, msg, 248, windowWidth, _colorTable[992]);

    // WEAPON PREF
    msg = getmsg(&gCustomMessageList, &messageListItem, _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON][_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON]].messageId);
    fontDrawText(windowBuffer + windowWidth * 78 + 232, msg, 248, windowWidth, _colorTable[992]);

    // DISTANCE
    msg = getmsg(&gCustomMessageList, &messageListItem, _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE][_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE]].messageId);
    fontDrawText(windowBuffer + windowWidth * 108 + 232, msg, 248, windowWidth, _colorTable[992]);

    // ATTACK WHO
    msg = getmsg(&gCustomMessageList, &messageListItem, _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO][_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO]].messageId);
    fontDrawText(windowBuffer + windowWidth * 137 + 232, msg, 248, windowWidth, _colorTable[992]);

    // CHEM USE
    msg = getmsg(&gCustomMessageList, &messageListItem, _custom_settings[PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE][_custom_current_selected[PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE]].messageId);
    fontDrawText(windowBuffer + windowWidth * 166 + 232, msg, 248, windowWidth, _colorTable[992]);

    windowRefresh(gGameDialogWindow);
    fontSetCurrent(oldFont);
}

// 0x449E64
void _gdCustomSelectRedraw(unsigned char* dest, int pitch, int type, int selectedIndex)
{
    MessageListItem messageListItem;

    fontSetCurrent(101);

    for (int index = 0; index < 6; index++) {
        STRUCT_5189E4* ptr = &(_custom_settings[type][index]);
        if (ptr->messageId != -1) {
            bool enabled = false;
            switch (type) {
            case PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE:
                enabled = partyMemberSupportsAreaAttackMode(gGameDialogSpeaker, ptr->value);
                break;
            case PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE:
                enabled = partyMemberSupportsRunAwayMode(gGameDialogSpeaker, ptr->value);
                break;
            case PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON:
                enabled = partyMemberSupportsBestWeapon(gGameDialogSpeaker, ptr->value);
                break;
            case PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE:
                enabled = partyMemberSupportsDistance(gGameDialogSpeaker, ptr->value);
                break;
            case PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO:
                enabled = partyMemberSupportsAttackWho(gGameDialogSpeaker, ptr->value);
                break;
            case PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE:
                enabled = partyMemberSupportsChemUse(gGameDialogSpeaker, ptr->value);
                break;
            }

            int color;
            if (enabled) {
                if (index == selectedIndex) {
                    color = _colorTable[32747];
                } else {
                    color = _colorTable[992];
                }
            } else {
                color = _colorTable[15855];
            }

            const char* msg = getmsg(&gCustomMessageList, &messageListItem, ptr->messageId);
            fontDrawText(dest + pitch * (fontGetLineHeight() * index + 42) + 42, msg, pitch - 84, pitch, color);
        }
    }
}

// 0x449FC0
int _gdCustomSelect(int a1)
{
    int oldFont = fontGetCurrent();

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 419, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    int backgroundFrmWidth = backgroundFrmImage.getWidth();
    int backgroundFrmHeight = backgroundFrmImage.getHeight();

    int selectWindowX = (screenGetWidth() - backgroundFrmWidth) / 2;
    int selectWindowY = (screenGetHeight() - backgroundFrmHeight) / 2;
    int win = windowCreate(selectWindowX, selectWindowY, backgroundFrmWidth, backgroundFrmHeight, 256, WINDOW_MODAL | WINDOW_MOVE_ON_TOP);
    if (win == -1) {
        return -1;
    }

    unsigned char* windowBuffer = windowGetBuffer(win);
    blitBufferToBuffer(backgroundFrmImage.getData(),
        backgroundFrmWidth,
        backgroundFrmHeight,
        backgroundFrmWidth,
        windowBuffer,
        backgroundFrmWidth);

    backgroundFrmImage.unlock();

    int btn1 = buttonCreate(win, 70, 164, 14, 14, -1, -1, -1, KEY_RETURN, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (btn1 == -1) {
        windowDestroy(win);
        return -1;
    }

    int btn2 = buttonCreate(win, 176, 163, 14, 14, -1, -1, -1, KEY_ESCAPE, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
    if (btn2 == -1) {
        windowDestroy(win);
        return -1;
    }

    fontSetCurrent(103);

    MessageListItem messageListItem;
    const char* msg;

    msg = getmsg(&gCustomMessageList, &messageListItem, a1);
    fontDrawText(windowBuffer + backgroundFrmWidth * 15 + 40, msg, backgroundFrmWidth, backgroundFrmWidth, _colorTable[18979]);

    msg = getmsg(&gCustomMessageList, &messageListItem, 10);
    fontDrawText(windowBuffer + backgroundFrmWidth * 163 + 88, msg, backgroundFrmWidth, backgroundFrmWidth, _colorTable[18979]);

    msg = getmsg(&gCustomMessageList, &messageListItem, 11);
    fontDrawText(windowBuffer + backgroundFrmWidth * 162 + 193, msg, backgroundFrmWidth, backgroundFrmWidth, _colorTable[18979]);

    int value = _custom_current_selected[a1];
    _gdCustomSelectRedraw(windowBuffer, backgroundFrmWidth, a1, value);
    windowRefresh(win);

    int minX = selectWindowX + 42;
    int minY = selectWindowY + 42;
    int maxX = selectWindowX + backgroundFrmWidth - 42;
    int maxY = selectWindowY + backgroundFrmHeight - 42;

    bool done = false;
    unsigned int v53 = 0;
    while (!done) {
        sharedFpsLimiter.mark();

        int keyCode = inputGetInput();
        if (keyCode != -1) {
            if (keyCode == KEY_CTRL_Q || keyCode == KEY_CTRL_X || keyCode == KEY_F10) {
                showQuitConfirmationDialog();
            }

            if (_game_user_wants_to_quit != 0) {
                break;
            }

            if (keyCode == KEY_RETURN) {
                STRUCT_5189E4* ptr = &(_custom_settings[a1][value]);
                _custom_current_selected[a1] = value;
                _gdCustomUpdateSetting(a1, ptr->value);
                done = true;
            } else if (keyCode == KEY_ESCAPE) {
                done = true;
            } else if (keyCode == -2) {
                if ((mouseGetEvent() & MOUSE_EVENT_LEFT_BUTTON_UP) != 0) {
                    // No need to use mouseHitTestInWindow as these values are already
                    // in screen coordinates.
                    if (_mouse_click_in(minX, minY, maxX, maxY)) {
                        int mouseX;
                        int mouseY;
                        mouseGetPosition(&mouseX, &mouseY);

                        int lineHeight = fontGetLineHeight();
                        int newValue = (mouseY - minY) / lineHeight;
                        if (newValue < 6) {
                            unsigned int timestamp = getTicks();
                            if (newValue == value) {
                                if (getTicksBetween(timestamp, v53) < 250) {
                                    _custom_current_selected[a1] = newValue;
                                    _gdCustomUpdateSetting(a1, newValue);
                                    done = true;
                                }
                            } else {
                                STRUCT_5189E4* ptr = &(_custom_settings[a1][newValue]);
                                if (ptr->messageId != -1) {
                                    bool enabled = false;
                                    switch (a1) {
                                    case PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE:
                                        enabled = partyMemberSupportsAreaAttackMode(gGameDialogSpeaker, ptr->value);
                                        break;
                                    case PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE:
                                        enabled = partyMemberSupportsRunAwayMode(gGameDialogSpeaker, ptr->value);
                                        break;
                                    case PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON:
                                        enabled = partyMemberSupportsBestWeapon(gGameDialogSpeaker, ptr->value);
                                        break;
                                    case PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE:
                                        enabled = partyMemberSupportsDistance(gGameDialogSpeaker, ptr->value);
                                        break;
                                    case PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO:
                                        enabled = partyMemberSupportsAttackWho(gGameDialogSpeaker, ptr->value);
                                        break;
                                    case PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE:
                                        enabled = partyMemberSupportsChemUse(gGameDialogSpeaker, ptr->value);
                                        break;
                                    }

                                    if (enabled) {
                                        value = newValue;
                                        _gdCustomSelectRedraw(windowBuffer, backgroundFrmWidth, a1, newValue);
                                        windowRefresh(win);
                                    }
                                }
                            }
                            v53 = timestamp;
                        }
                    }
                }
            }
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }

    windowDestroy(win);
    fontSetCurrent(oldFont);
    return 0;
}

// 0x44A4E0
void _gdCustomUpdateSetting(int option, int value)
{
    switch (option) {
    case PARTY_MEMBER_CUSTOMIZATION_OPTION_AREA_ATTACK_MODE:
        aiSetAreaAttackMode(gGameDialogSpeaker, value);
        break;
    case PARTY_MEMBER_CUSTOMIZATION_OPTION_RUN_AWAY_MODE:
        aiSetRunAwayMode(gGameDialogSpeaker, value);
        break;
    case PARTY_MEMBER_CUSTOMIZATION_OPTION_BEST_WEAPON:
        aiSetBestWeapon(gGameDialogSpeaker, value);
        break;
    case PARTY_MEMBER_CUSTOMIZATION_OPTION_DISTANCE:
        aiSetDistance(gGameDialogSpeaker, value);
        break;
    case PARTY_MEMBER_CUSTOMIZATION_OPTION_ATTACK_WHO:
        aiSetAttackWho(gGameDialogSpeaker, value);
        break;
    case PARTY_MEMBER_CUSTOMIZATION_OPTION_CHEM_USE:
        aiSetChemUse(gGameDialogSpeaker, value);
        break;
    }
}

// 0x44A52C
void gameDialogBarterButtonUpMouseUp(int btn, int keyCode)
{
    if (PID_TYPE(gGameDialogSpeaker->pid) != OBJ_TYPE_CRITTER) {
        return;
    }

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

    Proto* proto;
    protoGetProto(gGameDialogSpeaker->pid, &proto);
    if (proto->critter.data.flags & CRITTER_BARTER) {
        if (gGameDialogLipSyncStarted) {
            if (soundIsPlaying(gLipsData.sound)) {
                gameDialogEndLips();
            }
        }

        _dialogue_switch_mode = 2;
        _dialogue_state = 4;

        // NOTE: Uninline.
        gdHide();
    } else {
        MessageListItem messageListItem;
        // This person will not barter with you.
        messageListItem.num = 903;
        if (gGameDialogSpeakerIsPartyMember) {
            // This critter can't carry anything.
            messageListItem.num = 913;
        }

        if (messageListGetItem(&gProtoMessageList, &messageListItem)) {
            gameDialogRenderSupplementaryMessage(messageListItem.text);
        } else {
            debugPrint("\nError: gdialog: Can't find message!");
        }
    }
}

// 0x44A62C
int _gdialog_window_create()
{
    const int screenWidth = GAME_DIALOG_WINDOW_WIDTH;

    if (_gdialog_window_created) {
        return -1;
    }

    for (int index = 0; index < 9; index++) {
        _gdialog_buttons[index] = -1;
    }

    FrmImage backgroundFrmImage;
    // 389 - di_talkp.frm - dialog screen subwindow (party members)
    // 99 - di_talk.frm - dialog screen subwindow (NPC's)
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, gGameDialogSpeakerIsPartyMember ? 389 : 99, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    unsigned char* backgroundFrmData = backgroundFrmImage.getData();
    if (backgroundFrmData != nullptr) {
        _dialogue_subwin_len = backgroundFrmImage.getHeight();

        int dialogSubwindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
        int dialogSubwindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2 + GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len;
        gGameDialogWindow = windowCreate(dialogSubwindowX, dialogSubwindowY, screenWidth, _dialogue_subwin_len, 256, WINDOW_DONT_MOVE_TOP);
        if (gGameDialogWindow != -1) {

            unsigned char* v10 = windowGetBuffer(gGameDialogWindow);
            unsigned char* v14 = windowGetBuffer(gGameDialogBackgroundWindow);
            // TODO: Not sure about offsets.
            blitBufferToBuffer(v14 + screenWidth * (GAME_DIALOG_WINDOW_HEIGHT - _dialogue_subwin_len), screenWidth, _dialogue_subwin_len, screenWidth, v10, screenWidth);

            if (_dialogue_just_started) {
                windowRefresh(gGameDialogBackgroundWindow);
                _gdialog_scroll_subwin(gGameDialogWindow, 1, backgroundFrmData, v10, nullptr, _dialogue_subwin_len, -1);
                _dialogue_just_started = 0;
            } else {
                _gdialog_scroll_subwin(gGameDialogWindow, 1, backgroundFrmData, v10, nullptr, _dialogue_subwin_len, 0);
            }

            // BARTER/TRADE
            _gdialog_buttons[0] = buttonCreate(gGameDialogWindow, 593, 41, 14, 14, -1, -1, -1, -1, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
            if (_gdialog_buttons[0] != -1) {
                buttonSetMouseCallbacks(_gdialog_buttons[0], nullptr, nullptr, nullptr, gameDialogBarterButtonUpMouseUp);
                buttonSetCallbacks(_gdialog_buttons[0], _gsound_med_butt_press, _gsound_med_butt_release);

                // di_rest1.frm - dialog rest button up
                int upFid = buildFid(OBJ_TYPE_INTERFACE, 97, 0, 0, 0);
                if (_reviewButtonNormalFrmImage.lock(upFid)) {
                    // di_rest2.frm - dialog rest button down
                    int downFid = buildFid(OBJ_TYPE_INTERFACE, 98, 0, 0, 0);
                    if (_reviewButtonPressedFrmImage.lock(downFid)) {
                        // REVIEW
                        _gdialog_buttons[1] = buttonCreate(gGameDialogWindow, 13, 154, 51, 29, -1, -1, -1, -1, _reviewButtonNormalFrmImage.getData(), _reviewButtonPressedFrmImage.getData(), nullptr, 0);
                        if (_gdialog_buttons[1] != -1) {
                            buttonSetMouseCallbacks(_gdialog_buttons[1], nullptr, nullptr, nullptr, gameDialogReviewButtonOnMouseUp);
                            buttonSetCallbacks(_gdialog_buttons[1], _gsound_red_butt_press, _gsound_red_butt_release);

                            if (!gGameDialogSpeakerIsPartyMember) {
                                _gdialog_window_created = true;
                                return 0;
                            }

                            // COMBAT CONTROL
                            _gdialog_buttons[2] = buttonCreate(gGameDialogWindow, 593, 116, 14, 14, -1, -1, -1, -1, _redButtonNormalFrmImage.getData(), _redButtonPressedFrmImage.getData(), nullptr, BUTTON_FLAG_TRANSPARENT);
                            if (_gdialog_buttons[2] != -1) {
                                buttonSetMouseCallbacks(_gdialog_buttons[2], nullptr, nullptr, nullptr, gameDialogCombatControlButtonOnMouseUp);
                                buttonSetCallbacks(_gdialog_buttons[2], _gsound_med_butt_press, _gsound_med_butt_release);

                                _gdialog_window_created = true;
                                return 0;
                            }

                            buttonDestroy(_gdialog_buttons[1]);
                            _gdialog_buttons[1] = -1;
                        }

                        _reviewButtonPressedFrmImage.unlock();
                    }

                    _reviewButtonNormalFrmImage.unlock();
                }

                buttonDestroy(_gdialog_buttons[0]);
                _gdialog_buttons[0] = -1;
            }

            windowDestroy(gGameDialogWindow);
            gGameDialogWindow = -1;
        }
    }

    return -1;
}

// 0x44A9D8
void _gdialog_window_destroy()
{
    if (gGameDialogWindow == -1) {
        return;
    }

    for (int index = 0; index < 9; index++) {
        buttonDestroy(_gdialog_buttons[index]);
        _gdialog_buttons[index] = -1;
    }

    _reviewButtonNormalFrmImage.unlock();
    _reviewButtonPressedFrmImage.unlock();

    int offset = (GAME_DIALOG_WINDOW_WIDTH) * (480 - _dialogue_subwin_len);
    unsigned char* backgroundWindowBuffer = windowGetBuffer(gGameDialogBackgroundWindow) + offset;

    int frmId;
    if (gGameDialogSpeakerIsPartyMember) {
        // di_talkp.frm - dialog screen subwindow (party members)
        frmId = 389;
    } else {
        // di_talk.frm - dialog screen subwindow (NPC's)
        frmId = 99;
    }

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, frmId, 0, 0, 0);
    if (backgroundFrmImage.lock(backgroundFid)) {
        unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
        _gdialog_scroll_subwin(gGameDialogWindow, 0, backgroundFrmImage.getData(), windowBuffer, backgroundWindowBuffer, _dialogue_subwin_len, 0);
        windowDestroy(gGameDialogWindow);
        _gdialog_window_created = 0;
        gGameDialogWindow = -1;
    }
}

// NOTE: Inlined.
//
// 0x44AAD8
static int talk_to_create_background_window()
{
    int backgroundWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
    int backgroundWindowY = (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2;
    gGameDialogBackgroundWindow = windowCreate(backgroundWindowX,
        backgroundWindowY,
        GAME_DIALOG_WINDOW_WIDTH,
        GAME_DIALOG_WINDOW_HEIGHT,
        256,
        WINDOW_DONT_MOVE_TOP);

    if (gGameDialogBackgroundWindow != -1) {
        return 0;
    }

    return -1;
}

// 0x44AB18
int gameDialogWindowRenderBackground()
{
    FrmImage backgroundFrmImage;
    // alltlk.frm - dialog screen background
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 103, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    int windowWidth = GAME_DIALOG_WINDOW_WIDTH;
    unsigned char* windowBuffer = windowGetBuffer(gGameDialogBackgroundWindow);
    blitBufferToBuffer(backgroundFrmImage.getData(), windowWidth, 480, windowWidth, windowBuffer, windowWidth);

    if (!_dialogue_just_started) {
        windowRefresh(gGameDialogBackgroundWindow);
    }

    return 0;
}

// 0x44ABA8
int _talkToRefreshDialogWindowRect(Rect* rect)
{
    int frmId;
    if (gGameDialogSpeakerIsPartyMember) {
        // di_talkp.frm - dialog screen subwindow (party members)
        frmId = 389;
    } else {
        // di_talk.frm - dialog screen subwindow (NPC's)
        frmId = 99;
    }

    FrmImage backgroundFrmImage;
    int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, frmId, 0, 0, 0);
    if (!backgroundFrmImage.lock(backgroundFid)) {
        return -1;
    }

    int offset = 640 * rect->top + rect->left;

    unsigned char* windowBuffer = windowGetBuffer(gGameDialogWindow);
    blitBufferToBuffer(backgroundFrmImage.getData() + offset,
        rect->right - rect->left,
        rect->bottom - rect->top,
        GAME_DIALOG_WINDOW_WIDTH,
        windowBuffer + offset,
        GAME_DIALOG_WINDOW_WIDTH);

    windowRefreshRect(gGameDialogWindow, rect);

    return 0;
}

// 0x44AC68
void gameDialogRenderHighlight(unsigned char* src, int srcWidth, int srcHeight, int srcPitch, unsigned char* dest, int destX, int destY, int destPitch, unsigned char* a9, unsigned char* a10)
{
    int srcStep = srcPitch - srcWidth;
    int destStep = destPitch - srcWidth;

    dest += destPitch * destY + destX;

    for (int y = 0; y < srcHeight; y++) {
        for (int x = 0; x < srcWidth; x++) {
            unsigned char v1 = *src++;
            if (v1 != 0) {
                v1 = (256 - v1) >> 4;
            }

            unsigned char v15 = *dest;
            *dest++ = a9[256 * v1 + v15];
        }
        src += srcStep;
        dest += destStep;
    }
}

// 0x44ACFC
void gameDialogRenderTalkingHead(Art* headFrm, int frame)
{
    if (gGameDialogWindow == -1) {
        return;
    }

    if (headFrm != nullptr) {
        if (frame == 0) {
            _totalHotx = 0;
        }

        FrmImage backgroundFrmImage;
        int backgroundFid = buildFid(OBJ_TYPE_BACKGROUND, gGameDialogBackground, 0, 0, 0);
        if (!backgroundFrmImage.lock(backgroundFid)) {
            debugPrint("\tError locking background in display...\n");
        }

        unsigned char* backgroundFrmData = backgroundFrmImage.getData();
        if (backgroundFrmData != nullptr) {
            blitBufferToBuffer(backgroundFrmData, 388, 200, 388, gGameDialogDisplayBuffer, GAME_DIALOG_WINDOW_WIDTH);
        } else {
            debugPrint("\tError getting background data in display...\n");
        }

        int width = artGetWidth(headFrm, frame, 0);
        int height = artGetHeight(headFrm, frame, 0);
        unsigned char* data = artGetFrameData(headFrm, frame, 0);

        int a3;
        int v8;
        artGetRotationOffsets(headFrm, 0, &a3, &v8);

        int a4;
        int a5;
        artGetFrameOffsets(headFrm, frame, 0, &a4, &a5);

        _totalHotx += a4;
        a3 += _totalHotx;

        if (data != nullptr) {
            int destWidth = GAME_DIALOG_WINDOW_WIDTH;
            int destOffset = destWidth * (200 - height) + a3 + (388 - width) / 2;
            if (destOffset + width * v8 > 0) {
                destOffset += width * v8;
            }

            blitBufferToBufferTrans(
                data,
                width,
                height,
                width,
                gGameDialogDisplayBuffer + destOffset,
                destWidth);
        } else {
            debugPrint("\tError getting head data in display...\n");
        }
    } else {
        if (_talk_need_to_center == 1) {
            _talk_need_to_center = 0;
            tileWindowRefresh();
        }

        unsigned char* src = windowGetBuffer(gIsoWindow);

        // Usually rendering functions use `screenGetWidth`/`screenGetHeight` to
        // determine rendering position. However in this case `windowGetHeight`
        // is a must because isometric window's height can either include
        // interface bar or not. Offset is updated accordingly (332 -> 232, the
        // missing 100 is interface bar height, which is already accounted for
        // when we're using `windowGetHeight`). `windowGetWidth` is used for
        // consistency.
        blitBufferToBuffer(
            src + ((windowGetHeight(gIsoWindow) - 232) / 2) * windowGetWidth(gIsoWindow) + (windowGetWidth(gIsoWindow) - 388) / 2,
            388,
            200,
            windowGetWidth(gIsoWindow),
            gGameDialogDisplayBuffer,
            GAME_DIALOG_WINDOW_WIDTH);
    }

    Rect v27;
    v27.left = 126;
    v27.top = 14;
    v27.right = 514;
    v27.bottom = 214;

    unsigned char* dest = windowGetBuffer(gGameDialogBackgroundWindow);

    gameDialogRenderHighlight(_upperHighlightFrmImage.getData(),
        _upperHighlightFrmImage.getWidth(),
        _upperHighlightFrmImage.getHeight(),
        _upperHighlightFrmImage.getWidth(),
        dest,
        426,
        15,
        GAME_DIALOG_WINDOW_WIDTH,
        _light_BlendTable,
        _light_GrayTable);

    gameDialogRenderHighlight(_lowerHighlightFrmImage.getData(),
        _lowerHighlightFrmImage.getWidth(),
        _lowerHighlightFrmImage.getHeight(),
        _lowerHighlightFrmImage.getWidth(),
        dest,
        129,
        214 - _lowerHighlightFrmImage.getHeight() - 2,
        GAME_DIALOG_WINDOW_WIDTH,
        _dark_BlendTable,
        _dark_GrayTable);

    for (int index = 0; index < 8; ++index) {
        Rect* rect = &(_backgrndRects[index]);
        int width = rect->right - rect->left;

        blitBufferToBufferTrans(_backgrndBufs[index],
            width,
            rect->bottom - rect->top,
            width,
            dest + GAME_DIALOG_WINDOW_WIDTH * rect->top + rect->left,
            GAME_DIALOG_WINDOW_WIDTH);
    }

    windowRefreshRect(gGameDialogBackgroundWindow, &v27);
}

// 0x44B080
void gameDialogHighlightsInit()
{
    for (int color = 0; color < 256; color++) {
        int r = (Color2RGB(color) & 0x7C00) >> 10;
        int g = (Color2RGB(color) & 0x3E0) >> 5;
        int b = Color2RGB(color) & 0x1F;
        _light_GrayTable[color] = ((r + 2 * g + 2 * b) / 10) >> 2;
        _dark_GrayTable[color] = ((r + g + b) / 10) >> 2;
    }

    _light_GrayTable[0] = 0;
    _dark_GrayTable[0] = 0;

    _light_BlendTable = _getColorBlendTable(_colorTable[17969]);
    _dark_BlendTable = _getColorBlendTable(_colorTable[22187]);

    // hilight1.frm - dialogue upper hilight
    int upperHighlightFid = buildFid(OBJ_TYPE_INTERFACE, 115, 0, 0, 0);
    _upperHighlightFrmImage.lock(upperHighlightFid);

    // hilight2.frm - dialogue lower hilight
    int lowerHighlightFid = buildFid(OBJ_TYPE_INTERFACE, 116, 0, 0, 0);
    _lowerHighlightFrmImage.lock(lowerHighlightFid);
}

// NOTE: Inlined.
//
// 0x44B1D4
static void gameDialogHighlightsExit()
{
    _freeColorBlendTable(_colorTable[17969]);
    _freeColorBlendTable(_colorTable[22187]);

    _upperHighlightFrmImage.unlock();
    _lowerHighlightFrmImage.unlock();
}

static void gameDialogRedButtonsInit()
{
    // di_rdbt2.frm - dialog red button up
    int normalFid = buildFid(OBJ_TYPE_INTERFACE, 96, 0, 0, 0);
    if (!_redButtonNormalFrmImage.lock(normalFid)) {
        gameDialogRedButtonsExit();
    }

    // di_rdbt1.frm - dialog red button down
    int pressedFid = buildFid(OBJ_TYPE_INTERFACE, 95, 0, 0, 0);
    if (!_redButtonPressedFrmImage.lock(pressedFid)) {
        gameDialogRedButtonsExit();
    }
}

static void gameDialogRedButtonsExit()
{
    _redButtonNormalFrmImage.unlock();
    _redButtonPressedFrmImage.unlock();
}

} // namespace fallout
