'OHRRPGCE GAME - Various unsorted routines
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "udts.bi"
#include "allmodex.bi"
#include "common.bi"
#include "gglobals.bi"
#include "const.bi"
#include "scrconst.bi"
#include "uiconst.bi"
#include "loading.bi"
#include "slices.bi"
#include "savegame.bi"

#include "game.bi"
#include "game_udts.bi"
#include "walkabouts.bi"
#include "scriptcommands.bi"
#include "yetmore2.bi"
#include "moresubs.bi"
#include "menustuf.bi"
#include "bmodsubs.bi"
#include "purchase.bi"
#include "scripting.bi"
#include "achievements_runtime.bi"

'--Local subs and functions
DECLARE SUB teleporttool_load_map (map as integer, maptiles2() as TileMap, pass2 as TileMap, tilesets2() as TilesetData ptr)
DECLARE SUB teleporttool_generate_minimap(byref mini as Frame Ptr, maptilesX() as TileMap, passX as TileMap, tilesetsX() as TilesetData ptr, byref zoom as integer, byref maxzoom as integer, byref mapsize as XYPair, byref minisize as XYPair, byref offset as XYPair, byref camera as XYPair, dest as XYPair)
DECLARE SUB inventory_overflow_handler(byval item_id as integer, byval numitems as integer)
DECLARE SUB hero_swap_menu_init(st as OrderTeamState)
DECLARE SUB hero_swap_menu_display (st as OrderTeamState)
DECLARE SUB hero_swap_menu_mouse_control (st as OrderTeamState)



'who is the hero id
SUB addhero (who as integer, slot as integer, forcelevel as integer = -1, allow_rename as bool = YES, loading as bool = NO)
DIM her as HeroDef

IF who > gen(genMaxHero) THEN
 showerror "Can't add hero " & who & ", no such hero!"
 IF party_size() > 0 THEN EXIT SUB
 'Adding an initial hero to the party
 '(upgrade() normally ensures gen(genStartHero) is valid, but if live-previewing
 'things can became invalid, and upgrade() isn't called anyway)
 who = 0
END IF

'--load hero's data
loadherodata her, who

'--do level forcing
IF forcelevel >= 0 THEN her.def_level = forcelevel

'--do average level enforcement
IF her.def_level < 0 THEN her.def_level = averagelev

WITH gam.hero(slot)
 '--formally add hero
 .id = who

 '---MUST SET DEFAULT EQUIP---
 FOR i as integer = 0 TO UBOUND(.equip)
  .equip(i).id = -1
 NEXT i
 .equip(0).id = her.def_weapon
END WITH

'--fill in stats
WITH gam.hero(slot).stat
 FOR statnum as integer = 0 TO statLast
  .base.sta(statnum) = atlevel(her.def_level, her.Lev0.sta(statnum), her.LevMax.sta(statnum))
 NEXT
 recompute_hero_max_stats slot
 FOR statnum as integer = 0 TO statLast
  .cur.sta(statnum) = .max.sta(statnum)
 NEXT
END WITH

'--put spells in spell list
FOR i as integer = 0 TO 3
 FOR o as integer = 0 TO 23
  gam.hero(slot).spells(i, o) = 0
  WITH her.spell_lists(i, o)
   IF .attack > 0 AND .learned - 1 <= her.def_level AND .learned > 0 THEN
    gam.hero(slot).spells(i, o) = .attack
   END IF
  END WITH
 NEXT o
NEXT i

'--damage from elements
FOR i as integer = 0 TO gen(genNumElements) - 1
 gam.hero(slot).elementals(i) = her.elementals(i)
NEXT

'--setup experience
gam.hero(slot).exp_mult = her.exp_mult
gam.hero(slot).lev = her.def_level
gam.hero(slot).lev_gain = 0
gam.hero(slot).exp_cur = 0
gam.hero(slot).exp_next = exptolevel(her.def_level + 1, her.exp_mult)

'--reset levelmp
reset_levelmp gam.hero(slot)

'--clear learnmask slots (just to be really thorough)
flusharray gam.hero(slot).learnmask()

'--heros are added unlocked
gam.hero(slot).locked = NO

'--appearance settings
' udts are self documenting
WITH gam.hero(slot)
 .battle_pic = her.sprite
 .battle_pal = her.sprite_pal
 .pic = her.walk_sprite
 .pal = her.walk_sprite_pal
 .def_wep = her.def_weapon + 1 'default weapon
 FOR i as integer = 0 to 1
  .hand_pos(i) = her.hand_pos(i)
 NEXT i
 .hand_pos_overridden = NO
 .portrait_pic = her.portrait
 .portrait_pal = her.portrait_pal
 .sl = create_hero_slices(slot)
END WITH

'--auto battle
gam.hero(slot).auto_battle = her.default_auto_battle

'--name
gam.hero(slot).name = her.name
gam.hero(slot).rename_on_status = xreadbit(her.bits(), 25)

'--if renaming is permitted, do it
IF allow_rename ANDALSO readbit(her.bits(), 0, 24) THEN
 '--add-hero rename is allowed
 renamehero slot, NO
END IF

'--update hero auto-set tags and herow
party_change_updates

IF gen(genAddHeroScript) > 0 THEN
 'Prioritise to ensure it runs before the newgame script
 trigger_script gen(genAddHeroScript), 3, NO, "add hero", context_string, mainFibreGroup, 100
 trigger_script_arg 0, slot, "slot"
 trigger_script_arg 1, who, "hero ID"
 trigger_script_arg 2, IIF(loading, 1, 0), "loading"
END IF

END SUB

'Deletes a hero, and then (unless resetting) calls forceparty.
'Pass resetting_game=YES only when cleaning up while resetting/quitting.
'In that case, the caller should wipe Slice ptrs in herow()!
SUB deletehero(slot as integer, resetting_game as bool = NO)
 IF resetting_game = NO ANDALSO gen(genRemoveHeroScript) > 0 THEN
  trigger_script gen(genRemoveHeroScript), 2, NO, "delete hero", context_string, mainFibreGroup, 100
  trigger_script_arg 0, slot, "slot"
  trigger_script_arg 1, gam.hero(slot).id, "hero ID"
 END IF

 gam.hero(slot).id = -1
 gam.hero(slot).name &= "(deleted)"
 DeleteSlice @gam.hero(slot).sl
 IF resetting_game = NO THEN
  IF active_party_size() = 0 THEN forceparty
  party_change_updates
 END IF
END SUB

FUNCTION averagelev () as integer
 DIM average as integer = 0
 DIM count as integer = 0
 FOR i as integer = 0 TO 3
  IF gam.hero(i).id >= 0 THEN average += gam.hero(i).lev: count += 1
 NEXT i
 IF count > 0 THEN average = average / count
 RETURN average
END FUNCTION

'Subtracts one of an item in an inventory slot.
'Returns true if the item slot is depleted, else false.
FUNCTION consumeitem (byval invslot as integer) as bool
 consumeitem = 0
 inventory(invslot).num -= 1
 IF inventory(invslot).num <= 0 THEN
  inventory(invslot).used = NO
  consumeitem = -1
 END IF
 update_inventory_caption invslot
END FUNCTION

FUNCTION countitem (byval item_id as integer) as integer
 DIM total as integer = 0
 FOR o as integer = 0 TO last_inv_slot()
  IF inventory(o).used AND item_id = inventory(o).id THEN
   total += inventory(o).num
  END IF
 NEXT o
 RETURN total
END FUNCTION

FUNCTION count_equipped_item(byval item_id as integer) as integer
 DIM count as integer = 0
 FOR slot as integer = 0 to 3
  WITH gam.hero(slot)
   IF .id < 0 THEN CONTINUE FOR
   FOR eqslot as integer = 0 TO UBOUND(.equip)
    IF .equip(eqslot).id = item_id THEN count += 1
   NEXT eqslot
  END WITH
 NEXT slot
 RETURN count
END FUNCTION

'Add to inventory. item_id is not offset.
'Returns true if all items fitted in inventory (may fit just some in and return false!)
'ok_to_fail is intended for when the caller wants to handle overflow itself.
FUNCTION getitem (byval item_id as integer, byval num as integer=1, byval ok_to_fail as bool=NO) as bool

 DIM numitems as integer = num
 DIM room as integer
 DIM stacksize as integer = get_item_stack_size(item_id)

 FOR i as integer = 0 TO last_inv_slot()
  ' Loop through all inventory slots looking for a slot that already
  ' contains the item we are adding. If found increment that slot
  room = stacksize - inventory(i).num
  IF inventory(i).used AND item_id = inventory(i).id AND room > 0 THEN
   IF room < numitems THEN
    inventory(i).num = stacksize
    update_inventory_caption i
    numitems -= room
   ELSE
    inventory(i).num += numitems
    update_inventory_caption i
    RETURN YES
   END IF
  END IF
 NEXT i
 
 FOR i as integer = 0 TO last_inv_slot()
  'loop through each inventory slot looking for an empty slot to populate 
  IF inventory(i).used = 0 THEN
   inventory(i).used = -1
   inventory(i).id = item_id
   inventory(i).num = small(numitems, stacksize)
   numitems -= inventory(i).num
   update_inventory_caption i
   IF numitems = 0 THEN RETURN YES
  END IF
 NEXT
 
 'No slot was found to put this item into!
 IF ok_to_fail = NO THEN
  inventory_overflow_handler item_id, numitems
 END IF
 RETURN NO
END FUNCTION

SUB inventory_overflow_handler(byval item_id as integer, byval numitems as integer)
 debug "Didn't have room for " & readitemname(item_id) & "x" & numitems
END SUB

FUNCTION room_for_item (byval item_id as integer, byval num as integer = 1) as bool
 DIM room as integer
 DIM stacksize as integer = get_item_stack_size(item_id)

 FOR i as integer = 0 TO last_inv_slot()
  ' Loop through all inventory slots looking for a slot that already contains the item
  room = stacksize - inventory(i).num
  IF inventory(i).used AND item_id = inventory(i).id AND room > 0 THEN
   IF room >= num THEN
    RETURN YES
   END IF
   num -= room
  END IF
 NEXT
 FOR i as integer = 0 TO last_inv_slot()
  'loop through each inventory slot looking for an empty slot to populate 
  IF inventory(i).used = NO THEN
   IF num <= stacksize THEN
    RETURN YES
   END IF
   num -= stacksize
  END IF
 NEXT
 RETURN NO
END FUNCTION

SUB delitem (byval item_id as integer, byval amount as integer=1)
 FOR o as integer = 0 TO last_inv_slot()
  IF inventory(o).used AND item_id = inventory(o).id THEN
   IF inventory(o).num <= amount THEN
    amount -= inventory(o).num
    inventory(o).num = 0
    inventory(o).id = 0
    inventory(o).used = NO
   ELSE
    inventory(o).num -= amount
    amount = 0
   END IF
   update_inventory_caption o
   IF amount = 0 THEN EXIT FOR
  END IF
 NEXT o
END SUB

'This only needs to be called from doswap
LOCAL SUB update_HeroSliceContext(byval party_slot as integer)
 WITH gam.hero(party_slot)
  IF .sl ANDALSO *.sl->Context IS HeroSliceContext THEN
   CAST(HeroSliceContext ptr, .sl->Context)->slot = party_slot
  END IF
 END WITH
END SUB

'Swap two party slots, occupied or not. There's no significance to the order of the args.
SUB doswap (byval s as integer, byval d as integer)
 IF s = d THEN EXIT SUB

 'Because this sub can swap between the active party and the reserve party,
 'we also need to prepare to swap the parentage of the hero slice between
 'the hero layer and the reserve layer
 DIM s_par as Slice Ptr = NULL
 DIM d_par as Slice Ptr = NULL
 IF gam.hero(s).sl THEN s_par = gam.hero(s).sl->Parent
 IF gam.hero(d).sl THEN d_par = gam.hero(d).sl->Parent

 '---hero state and stats
 SWAP gam.hero(s), gam.hero(d)

 'Restore the correct parentage of the hero slice, active to the hero layer
 'and reserve to the reserve layer 
 IF gam.hero(s).sl THEN SetSliceParent gam.hero(s).sl, s_par
 IF gam.hero(d).sl THEN SetSliceParent gam.hero(d).sl, d_par

 '--The hero slice tells which slot the hero is in, needs updating
 update_HeroSliceContext s
 update_HeroSliceContext d

 '--set tags, reload hero pictures and palettes, update herow(), etc
 party_change_updates

 '--Trigger scripts
 IF gen(genMoveHeroScript) > 0 THEN
  FOR i as integer = 0 TO 1
   IF gam.hero(s).id >= 0 THEN
    trigger_script gen(genMoveHeroScript), 2, NO, "move hero", context_string, mainFibreGroup, 100
    trigger_script_arg 0, s, "slot"
    trigger_script_arg 1, d, "previous slot"
   END IF
   SWAP s, d
  NEXT i
 END IF
END SUB

SUB update_textbox ()
STATIC tog as integer
tog = tog XOR 1

'On the first tick, show_lines is -1
IF txt.fully_shown = NO THEN
 txt.show_lines += 1
 '--play sounds for non-blank lines
 IF TRIM(txt.box.text(txt.show_lines)) <> "" THEN
  IF txt.box.line_sound > 0 THEN
   ' Don't subtract 1, since menusound takes sfx id + 1
   menusound txt.box.line_sound
  ELSEIF txt.box.line_sound = 0 THEN
   ' Default
   menusound gen(genTextboxLine)
  ELSEIF txt.box.line_sound < 0 THEN
   ' Silence
  END IF
 END IF
 '--note when the display of lines is done
 IF txt.show_lines >= UBOUND(txt.box.text) THEN
  txt.fully_shown = YES
 END IF
 '--update the slice to show the right number of lines
 DIM text_sl As Slice Ptr
 text_sl = LookupSlice(SL_TEXTBOX_TEXT, txt.sl)
 IF text_sl THEN
  DIM dat as TextSliceData Ptr
  dat = text_sl->SliceData
  IF dat THEN
   dat->line_limit = txt.show_lines
   IF txt.fully_shown THEN
    dat->line_limit = -1  'Show all
   END IF
  END IF
 END IF
END IF

IF txt.box.choice_enabled THEN
 '--Make the selected choice flash
 DIM choice_sl(1) as Slice Ptr
 choice_sl(0) = LookupSlice(SL_TEXTBOX_CHOICE0, SliceTable.Root)
 choice_sl(1) = LookupSlice(SL_TEXTBOX_CHOICE1, SliceTable.Root)
 DIM col as integer
 IF choice_sl(0) <> 0 AND choice_sl(1) <> 0 THEN
  FOR i as integer = 0 TO 1
   col = uilook(uiMenuItem)
   IF txt.choicestate.hover = i THEN col = uilook(uiMouseHoverItem)
   IF txt.choicestate.pt = i THEN col = uilook(uiSelectedItem + tog)
   ChangeTextSlice choice_sl(i), ,col
  NEXT i
 END IF
END IF
END SUB

SUB choicebox_controls()
 usemenusounds
 'Keyboard controls
 usemenu txt.choicestate
 'Mouse controls
 IF get_gen_bool("/mouse/mouse_menus") THEN
  DIM choice_sl(1) as Slice Ptr
  choice_sl(0) = LookupSlice(SL_TEXTBOX_CHOICE0, SliceTable.Root)
  choice_sl(1) = LookupSlice(SL_TEXTBOX_CHOICE1, SliceTable.Root)
  txt.choicestate.hover = -1
  FOR i as integer = 0 TO UBOUND(choice_sl)
   IF SliceCollidePoint(choice_sl(i), readmouse.pos) THEN
    txt.choicestate.hover = i
   END IF
  NEXT i
  IF (readmouse.buttons AND mouseLeft) ANDALSO txt.choicestate.hover >= 0 THEN
   txt.choicestate.pt = txt.choicestate.hover
  END IF
 END IF
END SUB

FUNCTION use_touch_textboxes() as bool
 'Also applies to mouse-click textboxes
 IF running_on_mobile() THEN
  IF get_gen_bool("/mobile_options/touch_textboxes/enabled") THEN RETURN YES
 END IF
 IF get_gen_bool("/mouse/click_textboxes") THEN
  RETURN YES
 END IF
 RETURN NO
END FUNCTION

FUNCTION user_textbox_advance() as bool
 'Return YES if the player wants to advance the textbox using built-in controls
 IF use_touch_textboxes() THEN
  'Mouse and touch controls
  IF readmouse().release AND mouseLeft THEN
   IF NOT txt.box.choice_enabled THEN
    'No choicebox, so accept clicks anywhere on the whole screen
    RETURN YES
   END IF
   'Only accept clicks on the choicebox itself
   DIM box_sl as slice ptr = LookupSlice(SL_TEXTBOX_CHOICE_BOX, SliceTable.Root)
   IF box_sl = 0 THEN 
    'No choicebox was found at all (which might be a sign that a script messed with it?)
    'Go ahead and advance the textbox to avoid getting stuck.
    RETURN YES
   END IF
   IF SliceCollidePoint(box_sl, readmouse.pos) THEN RETURN YES
  END IF
 END IF

 'Keyboard/joystick textbox advance
 IF game_check_use_key() THEN RETURN YES

 RETURN NO
END FUNCTION

SUB evalherotags ()
 'Note: this funciton can be called in a middle of changing a party, with no heroes in the active party
 DIM leaderid as integer = herobyrank(0)

 FOR i as integer = 0 TO small(gen(genMaxHero), UBOUND(herotags)) '--for each available hero
  'unset all tags, including ones used on heroes not in the party 
  WITH herotags(i)
   settag .have_tag, NO
   settag .alive_tag, NO
   settag .leader_tag, NO
   settag .active_tag, NO
   FOR j as integer = 0 TO v_len(.checks) - 1
    settag .checks[j].tag, NO
   NEXT j
  END WITH
 NEXT i
 'scan party
 FOR i as integer = 0 TO UBOUND(gam.hero)
  DIM byref hero as HeroState = gam.hero(i)
  'Don't crash if the hero definition has been deleted since!
  IF hero.id >= 0 ANDALSO hero.id <= UBOUND(herotags) THEN
   WITH herotags(hero.id)
    settag .have_tag, YES
    IF hero.stat.cur.hp > 0 THEN settag .alive_tag, YES
    IF hero.id = leaderid THEN settag .leader_tag, YES
    IF i < active_party_slots THEN settag .active_tag, YES
    FOR j as integer = 0 TO v_len(.checks) - 1
     WITH .checks[j]
      SELECT CASE .kind
       CASE TagRangeCheckKind.level
        IF hero.lev >= .min ANDALSO hero.lev <= .max THEN
         settag .tag, YES
        END IF
       CASE ELSE
      END SELECT
     END WITH
    NEXT j
   END WITH
  END IF
 NEXT i
END SUB

'Copy data from party list (gam.hero()) to caterpillar (herow())
LOCAL SUB update_herow_on_party_change()
 DIM as integer rank, slot
 FOR rank = 0 TO UBOUND(herow)
  herow(rank).sl = NULL
  herow(rank).party_slot = -1
 NEXT
 rank = 0
 FOR slot = 0 TO active_party_slots - 1
  IF gam.hero(slot).id >= 0 THEN
   herow(rank).sl = gam.hero(slot).sl
   herow(rank).party_slot = slot
   rank += 1
  END IF
 NEXT
END SUB

'Call this after a change to the party
SUB party_change_updates
 update_herow_on_party_change
 evalherotags
 evalitemtags  'Because of items with 'actively equipped' tags
 vishero
 tag_updates
END SUB

SUB evalitemtags
 DIM as integer id

 FOR i as integer = 0 TO small(gen(genMaxItem), UBOUND(itemtags))
  WITH itemtags(i)
   'clear all four tags
   settag .have_tag, NO
   settag .in_inventory_tag, NO
   settag .is_equipped_tag, NO
   settag .is_actively_equipped_tag, NO
  END WITH
 NEXT i

 'search inventory slots
 FOR j as integer = 0 TO last_inv_slot()
  'get item ID
  id = inventory(j).id
  IF inventory(j).used ANDALSO id <= UBOUND(itemtags) THEN 'there is an item in this slot
   WITH itemtags(id)
    settag .have_tag, YES
    settag .in_inventory_tag, YES
   END WITH
  END IF
 NEXT j

 FOR j as integer = 0 TO UBOUND(gam.hero) 'search hero list
  FOR k as integer = 0 TO UBOUND(gam.hero(j).equip) 'search equipment slots
   id = gam.hero(j).equip(k).id
   IF id >= 0 ANDALSO id <= UBOUND(itemtags) THEN ' there is an item equipped in this slot
    WITH itemtags(id)
     settag .have_tag, YES
     settag .is_equipped_tag, YES
     IF j < active_party_slots THEN settag .is_actively_equipped_tag, YES
    END WITH
   END IF
  NEXT k
 NEXT j
END SUB

SUB hero_swap_menu (byval reserve_too as bool)

DIM st as OrderTeamState
st.show_reserve = reserve_too
st.reserve.pt = -1
st.reserve.last = -1
st.swapme = -1
st.party.need_update = YES

'--Preserve background for display beneath the hero swapper
DIM holdscreen as integer
st.page = vpage
holdscreen = allocatepage
copypage st.page, holdscreen

hero_swap_menu_init st

show_virtual_gamepad()

MenuSound gen(genAcceptSFX)
setkeys
DO
 setwait speedcontrol
 setkeys
 st.party.tog XOR= 1
 playtimer
 
 IF get_gen_bool("/mouse/mouse_menus") THEN
  hero_swap_menu_mouse_control st
 END IF
 
 IF game_check_cancel_key() THEN
  IF st.swapme >= 0 THEN
   MenuSound gen(genCancelSFX)
   st.swapme = -1
  ELSE
   st.do_quit = YES
  END IF
 END IF
 IF st.show_reserve THEN
  IF carray(ccUp) > 1 THEN
   MenuSound gen(genCursorSFX)
   IF st.reserve.pt < 0 THEN
    st.reserve.pt = st.reserve.last
    st.reserve.need_update = YES
   ELSE
    loopvar st.reserve.pt, -1, st.reserve.last, -1
    st.reserve.need_update = YES
   END IF
  END IF
  IF carray(ccDown) > 1 THEN
   MenuSound gen(genCursorSFX)
   IF st.reserve.pt < 0 THEN
    st.reserve.pt = 0
    st.reserve.need_update = YES
   ELSE
    loopvar st.reserve.pt, -1, st.reserve.last, 1
    st.reserve.need_update = YES
   END IF
  END IF
 END IF

 IF st.reserve.need_update THEN
  st.reserve.need_update = NO
  IF st.reserve.pt < st.reserve.top THEN st.reserve.top = large(st.reserve.pt, 0)
  IF st.reserve.pt > st.reserve.top + 7 THEN st.reserve.top = st.reserve.pt - 7
  st.party.need_update = YES
 END IF
 
 IF carray(ccLeft) > 1 AND st.reserve.pt < 0 THEN
  MenuSound gen(genCursorSFX)
  loopvar st.party.pt, 0, 3, -1
  st.party.need_update = YES
 END IF
 IF carray(ccRight) > 1 AND st.reserve.pt < 0 THEN
  MenuSound gen(genCursorSFX)
  loopvar st.party.pt, 0, 3, 1
  st.party.need_update = YES
 END IF
 
 IF game_check_use_key() THEN st.do_pick = YES
 
 IF st.do_quit THEN EXIT DO
 
 IF st.do_pick THEN
  st.do_pick = NO
  DO
  IF prefbit(20) THEN  '"Locked heroes can't be re-ordered"
   IF (gam.hero(st.party.pt).locked ANDALSO gam.hero(st.party.pt).id >= 0) ORELSE (st.swapme >= 0 ANDALSO gam.hero(st.swapme).locked ANDALSO gam.hero(st.swapme).id >= 0) THEN
    MenuSound gen(genCancelSFX)
    EXIT DO
   END IF
  END IF
  IF st.swapme = -1 THEN
   MenuSound gen(genAcceptSFX)
   IF st.reserve.pt < 0 THEN
    st.swapme = st.party.pt
   ELSE
    st.swapme = 4 + st.reserve.pt
   END IF
  ELSE
   MenuSound gen(genAcceptSFX)
   DIM swap1 as integer
   DIM swap2 as integer
   DO
    IF st.swapme < 4 THEN
     IF active_party_size() <= 1 ANDALSO st.reserve.pt = st.reserve.last THEN EXIT DO
     IF gam.hero(st.swapme).locked ANDALSO gam.hero(st.swapme).id >= 0 ANDALSO st.reserve.pt > -1 THEN EXIT DO
    ELSE
     IF st.swapme - 4 = st.reserve.last AND st.reserve.pt = -1 AND active_party_size() <= 1 THEN EXIT DO
     IF gam.hero(st.party.pt).locked ANDALSO gam.hero(st.party.pt).id >= 0 ANDALSO st.reserve.pt = -1 THEN EXIT DO
    END IF
    '---IDENTIFY DESTINATION---
    IF st.reserve.pt < 0 THEN
     swap1 = st.party.pt
    ELSE
     swap1 = st.swindex(st.reserve.pt)
    END IF
    '---IDENTIFY SOURCE---
    IF st.swapme < 4 THEN
     swap2 = st.swapme
    ELSE
     swap2 = st.swindex(st.swapme - 4)
    END IF
    doswap swap1, swap2
    st.swapme = -1
    hero_swap_menu_init st
    EXIT DO
   LOOP '--this loop just exists for convenient breaking with EXIT DO
  END IF
  EXIT DO
  LOOP '--this loop just exists for convenient breaking with EXIT DO
 END IF
 IF st.party.need_update THEN
  st.party.need_update = NO
  IF gam.hero(st.party.pt).id >= 0 AND st.reserve.pt < 0 THEN st.info = gam.hero(st.party.pt).name ELSE st.info = ""
 END IF

 copypage holdscreen, st.page
 hero_swap_menu_display st
 setvispage st.page
 dowait
LOOP
clearkeys
MenuSound gen(genCancelSFX)
freepage holdscreen

party_change_updates

END SUB

SUB hero_swap_menu_mouse_control (st as OrderTeamState)
 DIM old_party_pt as integer = st.party.pt
 DIM old_reserve_pt as integer = st.reserve.pt
 DIM old_swapme as integer = st.swapme
 DIM centerx as integer = vpages(st.page)->w \ 2
 DIM centery as integer = vpages(st.page)->h \ 2
 DIM order_box as RectType = XYWH(centerx - 130\2, centery - 38\2 - 34, 130, 38)
 DIM reserve_w as integer = st.charsize.x * 8 + 16
 DIM reserve_h as integer = small(st.charsize.y, 8) * 10 + 10
 DIM reserve_box as RectType = XYWH(centerx - reserve_w\2, centery - reserve_h\2 + small(st.charsize.y, 8) * 5, reserve_w, reserve_h)
 IF rect_collide_point(order_box, readmouse.pos) THEN
  'In the order box
  DIM hover as integer = -1
  FOR i as integer = 0 TO 3
   DIM as integer xpos = centerx - 55 + (30 * i), ypos = centery - 40
   IF rect_collide_point(XYWH(xpos, ypos, 30, 30), readmouse.pos) THEN hover = i
  NEXT i
  IF hover >= 0 THEN
   IF (readmouse.release AND mouseLeft) THEN
    'Clicked on a slot in the active party
    IF st.party.pt = hover THEN
     'Pick selected
     st.do_pick = YES
     st.reserve.pt = -1
    ELSE
     'Select hovered
     st.party.pt = hover
     st.reserve.pt = -1
     IF st.swapme >= 0 THEN st.do_pick = YES
    END IF
   END IF
  END IF
 ELSEIF st.show_reserve ANDALSO rect_collide_point(reserve_box, readmouse.pos) THEN
  'In the reserve box
  DIM r_hover as integer = -1
  FOR i as integer = st.reserve.top TO small(st.reserve.top + 7, st.reserve.last)
   IF rect_collide_point(XYWH(centerx - reserve_w\2, centery + (i - st.reserve.top) * 10, reserve_w, 10), readmouse.pos) THEN r_hover = i
  NEXT i
  IF r_hover >= 0 THEN
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
    IF st.reserve.pt = r_hover THEN
     'Pick selected
     st.do_pick = YES
    ELSE
     'Select hovered
     st.reserve.pt = r_hover
     IF st.swapme >= 0 THEN st.do_pick = YES
    END IF
   END IF
  END IF
  mouse_scroll_menu(st.reserve)
  mouse_drag_menu(st.reserve)
 ELSE
  'Anywhere outside the boxes
  IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
   st.do_quit = YES
  END IF
 END IF
 IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
  IF st.swapme >= 0 THEN
   MenuSound gen(genCancelSFX)
   st.swapme = -1
  ELSE
   st.do_quit = YES
  END IF
 END IF
 
 IF old_party_pt <> st.party.pt ORELSE old_reserve_pt <> st.reserve.pt ORELSE old_swapme <> st.swapme THEN
  MenuSound gen(genCursorSFX)
  st.party.need_update = YES
  st.reserve.need_update = YES
 END IF
END SUB

SUB hero_swap_menu_display (st as OrderTeamState)
 '--DRAWS SWAP MENU AND CURRENT SELECTION
 DIM centerx as integer = vpages(st.page)->w \ 2
 DIM centery as integer = vpages(st.page)->h \ 2
 centerbox centerx, centery - 34, 130, 38, 1, st.page
 FOR i as integer = 0 TO active_party_slots - 1
  DIM as integer xpos = centerx - 55 + (30 * i), ypos = centery - 40
  IF i = st.swapme OR gam.hero(i).id >= 0 THEN rectangle xpos, ypos, 20, 20, boxlook(0).bgcol, st.page
  IF gam.hero(i).id >= 0 THEN
   set_walkabout_frame gam.hero(i).sl, dirDown, 0
   IF i = st.swapme THEN ypos -= 6
   DrawSliceAt LookupSliceSafe(SL_WALKABOUT_SPRITE_COMPONENT, gam.hero(i).sl), xpos, ypos, 20, 20, st.page, YES
  END IF
 NEXT i
 IF st.reserve.pt < 0 THEN edgeprint CHR(24), centerx - 49 + 30 * st.party.pt, centery - 48, uilook(uiSelectedItem + st.party.tog), st.page
 IF st.show_reserve THEN
  DIM reserve_box as RectType
  WITH reserve_box
   .wide = st.charsize.x * 8 + 16
   .high = small(st.charsize.y, 8) * 10 + 10
   .x = centerx - .wide \ 2
   .y = centery + small(st.charsize.y, 8) * 5 - .high \ 2
  END WITH
  edgeboxstyle reserve_box, 0, st.page
  draw_scrollbar st.reserve, reserve_box, , st.page
  FOR i as integer = st.reserve.top TO small(st.reserve.top + 7, st.reserve.last)
   'Some of the colours are a bit bizarre, here, especially the time bar stuff below
   DIM menu_color as integer = uilook(uiMenuItem)
   IF st.swapme = i + 4 THEN menu_color = uilook(uiSelectedDisabled)
   IF st.reserve.pt = i THEN
    menu_color = uilook(uiSelectedItem + st.party.tog)
    IF st.swapme = i + 4 THEN menu_color = uilook(uiSelectedDisabled + st.party.tog)
   END IF
   IF st.swapme > -1 AND st.swapme < 4 THEN
    IF (active_party_size() <= 1 ANDALSO i = st.reserve.last) ORELSE (gam.hero(st.party.pt).locked ANDALSO gam.hero(st.party.pt).id >= 0) THEN
     menu_color = uilook(uiTimeBar + ((st.reserve.pt = i) * st.party.tog))
    END IF
   END IF
   edgeprint st.swname(i), pCentered, centery + (i - st.reserve.top) * 10, menu_color, st.page
  NEXT i
 END IF
 IF LEN(st.info) THEN
  centerbox centerx, centery - 56, (LEN(st.info) + 2) * 8, 14, 1, st.page
  edgeprint st.info, pCentered, centery - 61, uilook(uiText), st.page
 END IF
END SUB

SUB hero_swap_menu_init(st as OrderTeamState)
 '--MAPS OUT ONLY VALID SWAPABLE HEROS PLUS A BLANK
 st.reserve.last = -1
 st.charsize.x = 0
 FOR i as integer = 4 TO 40
  IF gam.hero(i).locked = NO ANDALSO gam.hero(i).id >= 0 THEN
   st.reserve.last = st.reserve.last + 1
   st.swindex(st.reserve.last) = i
   st.swname(st.reserve.last) = gam.hero(i).name
   st.charsize.x = large(st.charsize.x, LEN(st.swname(st.reserve.last)))
  END IF
 NEXT i
 st.reserve.last += 1
 FOR i as integer = 40 TO 4 STEP -1
  IF gam.hero(i).id = -1 THEN
   st.swindex(st.reserve.last) = i
   st.swname(st.reserve.last) = readglobalstring(48, "REMOVE", 10)
   st.charsize.x = large(st.charsize.x, 7)
  END IF
 NEXT i
 st.reserve.size = small(7, st.reserve.last)
 st.reserve.spacing = 10
 st.charsize.y = small(8, st.reserve.last + 1)
 st.party.need_update = YES
END SUB

/' Not used yet
FUNCTION check_condition(cond as Condition) as bool
 IF cond.comp = compTag THEN
  RETURN istag(tag(), cond.tag, , NO)
 ELSE
  IF cond.varnum < 0 ORELSE cond.varnum > UBOUND(global) THEN RETURN NO
  DIM globalvar as integer = global(cond.varnum)
  SELECT CASE cond.comp
   CASE compEq :    RETURN globalvar = cond.value
   CASE compNe :    RETURN globalvar <> cond.value
   CASE compLt :    RETURN globalvar < cond.value
   CASE compLe :    RETURN globalvar <= cond.value
   CASE compGt :    RETURN globalvar > cond.value
   CASE compGe :    RETURN globalvar >= cond.value
   CASE ELSE   :    debugerror strprintf("Invalid Condition (%d,%d,%d)", cond.comp, cond.varnum, cond.value)
  END SELECT
 END IF
END FUNCTION
'/

'Either pass a tag number and specify YES/NO, or pass just a tag number; +ve/-ve indicates value
SUB settag (byval tagnum as integer, byval value as integer = 4444)
 IF ABS(tagnum) <= max_tag() THEN
  settag tag(), tagnum, value
 ELSE
  settag onetime(), tagnum - (max_tag()+1) * SGN(tagnum), value
 END IF
END SUB

FUNCTION istag (num as integer, zero as bool=NO) as bool
 IF ABS(num) <= max_tag() THEN
  RETURN istag(tag(), num, zero)
 ELSE
  RETURN istag(onetime(), num - (max_tag()+1) * SGN(num), zero)
 END IF
END FUNCTION

'Either pass a tag number and specify YES/NO, or pass just a tag number; +ve/-ve indicates value
SUB settag (tagbits() as integer, byval tagnum as integer, byval value as integer = 4444)
 IF value <> 4444 THEN
  IF ABS(tagnum) > 1 THEN setbit tagbits(), 0, ABS(tagnum), value
 ELSEIF tagnum < -1 THEN
  setbit tagbits(), 0, ABS(tagnum), NO
 ELSEIF tagnum > 1 THEN
  setbit tagbits(), 0, tagnum, YES
 END IF
END SUB

FUNCTION istag (tagbits() as integer, num as integer, zero as bool=NO) as bool
 IF num = 0 THEN RETURN zero 'why go through all that just to return defaults?
 IF num = 1 THEN RETURN NO
 IF num = -1 THEN RETURN YES
 IF ABS(num) >= UBOUND(tagbits) * 16 + 16 THEN RETURN zero ' use default in case of an invalid tag

 DIM ret as integer = readbit(tagbits(), 0, ABS(num)) 'raw bit: 0 or 1

 IF num > 0 AND ret <> 0 THEN RETURN YES
 IF num < 0 AND ret = 0 THEN RETURN YES
 RETURN NO
END FUNCTION

'The minimap, indicating hero position
'playerpos is in pixels
SUB minimap (playerpos as XYPair)
 IF gen(genMinimapAlgorithm) = minimapScaled THEN switch_to_32bit_vpages

 DIM mini as Frame Ptr
 DIM zoom as integer = -1
 mini = createminimap(maptiles(), tilesets(), @pass, zoom, CAST(MinimapAlgorithmEnum, gen(genMinimapAlgorithm)))

 DIM offset as XYPair
 offset.x = rCenter - mini->w / 2
 offset.y = rCenter - mini->h / 2

 edgeboxstyle offset.x - 2, offset.y - 2, mini->w + 4, mini->h + 4, 0, vpage
 frame_draw mini, NULL, offset.x, offset.y, NO, vpage
 frame_unload @mini

 MenuSound gen(genAcceptSFX)

 DIM indicator as XYPair = offset + (playerpos / 20) * zoom
 DIM tog as integer

 show_virtual_gamepad()

 setkeys
 DO
  setwait speedcontrol
  setkeys
  tog = tog XOR 1
  playtimer
  rectangle indicator.x, indicator.y, zoom, zoom, uilook(uiSelectedItem) * tog, vpage
  setvispage vpage
  dowait
  'Exit on keypress after setvispage, because it does F12 screenshot check
 LOOP UNTIL anykeypressed(YES, YES)  'inc. joystick and mouse
 setkeys
 MenuSound gen(genCancelSFX)
 IF gen(gen32bitMode) THEN switch_to_32bit_vpages ELSE switch_to_8bit_vpages
END SUB

'The map-teleport debug menu.
'Sets gam.map.id and hero position and returns true if the player wants to teleport.
FUNCTION teleporttool () as bool
 'The maptiles, passmap and tilesets for displayed map, if not the same as the current in-game map
 REDIM maptiles2(0) as TileMap
 DIM pass2 as TileMap
 DIM tilesets2(maplayerMax) as TilesetData ptr

 DIM mini as Frame Ptr
 DIM zoom as integer = -1 'pixels per tile
 DIM maxzoom as integer

 DIM mapsize as XYPair  'In tiles
 DIM minisize as XYPair 'Minimap size in pixels
 DIM offset as XYPair   'Top-left position where the on-screen map viewport starts
 DIM camera as XYPair   'In pixels
 DIM dest as XYPair = herotpos(0)

 'Switch unconditionally, in-case live-previewing and genMinimapAlgorithm is changed
 switch_to_32bit_vpages

 ensure_normal_palette

 'Preserve background for display beneath
 DIM holdscreen as integer
 holdscreen = duplicatepage(vpage)

 'Initially use the real map's maptiles() and tilesets(), which has
 'the advantage of making the map look more correct.
 teleporttool_generate_minimap mini, maptiles(), pass, tilesets(), zoom, maxzoom, mapsize, minisize, offset, camera, dest

 DIM state as MenuState
 state.pt = 0
 state.top = 0
 state.size = 22
 state.first = 0
 state.last = 1
 state.need_update = YES
 DIM menuopts as MenuOptions
 menuopts.edged = YES

 DIM preview_delay as integer = 0  'Ticks before loading new map
 DIM pickpoint as bool = NO
 DIM destmap as integer = gam.map.id
 DIM currentmap as integer = gam.map.id
 DIM toggle as integer

 DIM menu(1) as string
 menu(0) = CHR(27) & "Map " & destmap & CHR(26) & " " & getmapname(destmap)
 menu(1) = "Position X = " & dest.x & " Y = " & dest.y

 teleporttool = NO

 MenuSound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys
  toggle XOR= 1


  'Cap to a maximum zoom because createminimap draws the whole map and is really slow
  IF keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 THEN
   zoom = large(1, zoom - 1)
   state.need_update = YES
  END IF
  IF keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1 THEN
   zoom = small(maxzoom, zoom + 1)
   state.need_update = YES
  END IF

  IF preview_delay > 0 THEN
   preview_delay -= 1
   IF preview_delay = 0 THEN
    teleporttool_load_map destmap, maptiles2(), pass2, tilesets2()
    state.need_update = YES
    zoom = -1  'Back to default
   END IF
  END IF

  IF state.need_update THEN
   state.need_update = NO
   IF destmap = currentmap THEN
    teleporttool_generate_minimap mini, maptiles(), pass, tilesets(), zoom, maxzoom, mapsize, minisize, offset, camera, dest
   ELSE
    teleporttool_generate_minimap mini, maptiles2(), pass2, tilesets2(), zoom, maxzoom, mapsize, minisize, offset, camera, dest
   END IF
  END IF

  IF pickpoint = NO THEN
   IF game_check_cancel_key() THEN
    EXIT DO
   END IF
   IF intgrabber(destmap, 0, gen(genMaxMap)) THEN
    preview_delay = 8
    menu(0) = CHR(27) & "Map " & destmap & CHR(26) & " " & getmapname(destmap)
   END IF
   IF enter_or_space() THEN pickpoint = YES
  ELSE
   IF game_check_use_key() THEN 'confirm and teleport
    IF gam.map.id <> destmap THEN teleporttool = YES
    gam.map.id = destmap
    (heropos(0)) = dest * 20
    EXIT DO
   END IF
   IF game_check_cancel_key() THEN pickpoint = NO

   DIM xrate as integer  'Movement rate
   DIM yrate as integer
   IF keyval(scShift) > 0 THEN
    xrate = 8
    yrate = 5
   ELSE
    xrate = 1
    yrate = 1
   END IF

   IF slowkey(ccUp, 45) THEN dest.y = large(dest.y - yrate, 0)
   IF slowkey(ccDown, 45) THEN dest.y = small(dest.y + yrate, mapsize.y - 1)
   IF slowkey(ccLeft, 45) THEN dest.x = large(dest.x - xrate, 0)
   IF slowkey(ccRight, 45) THEN dest.x = small(dest.x + xrate, mapsize.x - 1)

   DIM temp as XYPair = camera
   camera.x = bound(camera.x, (dest.x + 1) * zoom + 40 - minisize.x, dest.x * zoom - 40)  'follow dest
   camera.y = bound(camera.y, (dest.y + 1) * zoom + 30 - minisize.y, dest.y * zoom - 30)
   camera.x = bound(camera.x, 0, mapsize.x * zoom - minisize.x)  'bound to map edges
   camera.y = bound(camera.y, 0, mapsize.y * zoom - minisize.y)
  END IF

  menu(1) = "Position X = " & dest.x & " Y = " & dest.y

  ' Draw screen
  copypage holdscreen, vpage
  edgeboxstyle offset.x - 2, offset.y - 2, minisize.x + 4, minisize.y + 4, 0, vpage
  frame_draw mini, NULL, offset.x - camera.x, offset.y - camera.y, NO, vpage
  'Hero cursor
  DIM cursor as XYPair = offset + dest * zoom - camera
  rectangle cursor.x, cursor.y, zoom, zoom, uilook(uiSelectedItem) * toggle, vpage
  state.pt = IIF(pickpoint, 1, 0)
  standardmenu menu(), state, 0, vpages(vpage)->h - 29, vpage, menuopts
  DIM caption as string = IIF(pickpoint, "Arrows/SHIFT move; ENTER to teleport", "Select map; ENTER to select position")
  edgeprint caption, 0, pBottom, uilook(uiText), vpage
  edgeprint "+/- to zoom", pRight - 4, pBottom - 9, uilook(uiText), vpage
  setvispage vpage
  dowait
 LOOP
 frame_unload @mini
 freepage holdscreen
 unloadtilemaps maptiles2()
 unloadtilemap pass2
 unloadmaptilesets tilesets2()
 restore_previous_palette
 setkeys
 MenuSound gen(genCancelSFX)
 IF gen(gen32bitMode) THEN switch_to_32bit_vpages ELSE switch_to_8bit_vpages
END FUNCTION

'Load enough of a map's data to display it
SUB teleporttool_load_map (map as integer, maptiles2() as TileMap, pass2 as TileMap, tilesets2() as TilesetData ptr)
 DIM gmap2(dimbinsize(binMAP)) as integer
 loadrecord gmap2(), game + ".map", getbinsize(binMAP) \ 2, map
 loadtilemaps maptiles2(), maplumpname(map, "t")
 loadmaptilesets tilesets2(), gmap2()
 loadtilemap pass2, maplumpname(map, "p")
END SUB

'Generate the minimap given a zoom factor, and determinate viewport and camera positions
SUB teleporttool_generate_minimap(byref mini as Frame Ptr, maptilesX() as TileMap, passX as TileMap, tilesetsX() as TilesetData ptr, byref zoom as integer, byref maxzoom as integer, byref mapsize as XYPair, byref minisize as XYPair, byref offset as XYPair, byref camera as XYPair, dest as XYPair)
 mapsize = maptilesX(0).size
 DIM screenw as integer = vpages(vpage)->w
 DIM screenh as integer = vpages(vpage)->h
 DIM screen_ratio as double = small(screenw / maptilesX(0).wide, screenh / maptilesX(0).high)
 IF zoom = -1 THEN
  'minimum zoom level to make tiles easy to pick
  zoom = bound(INT(screen_ratio), 4, 20)
 END IF
 maxzoom = bound(INT(2.5 * screen_ratio), 4, 20)

 frame_unload @mini
 mini = createminimap(maptilesX(), tilesetsX(), @passX, zoom, CAST(MinimapAlgorithmEnum, gen(genMinimapAlgorithm)))
 offset.x = large(screenw \ 2 - mini->w \ 2, 0)
 offset.y = large(screenh \ 2 - mini->h \ 2, 0)
 minisize.x = screenw - offset.x * 2
 minisize.y = screenh - offset.y * 2
 camera.x = bound(dest.x * zoom - minisize.x \ 2, 0, mapsize.x * zoom - minisize.x)
 camera.y = bound(dest.y * zoom - minisize.y \ 2, 0, mapsize.y * zoom - minisize.y)
END SUB

' This is the hero picker (for the active party only).
' Returns party slot or -1 if cancelled.
' If there is only one hero, return immediately if skip_if_alone.
FUNCTION onwho (caption as string, skip_if_alone as bool = YES) as integer

'-- pre-select the first hero
DIM who as integer = rank_to_party_slot(0)

IF skip_if_alone ANDALSO active_party_size() <= 1 THEN
 setkeys
 RETURN who
END IF

menusound gen(genAcceptSFX)
copypage vpage, dpage
DIM page as integer = compatpage
DIM tog as integer
DIM wtog as integer

show_virtual_gamepad()

setkeys
DO
 setwait speedcontrol
 setkeys
 tog = tog XOR 1
 playtimer
 loopvar wtog, 0, max_wtog()
 DIM do_exit as bool = NO
 IF game_check_cancel_key() THEN do_exit = YES
 IF carray(ccLeft) > 1 THEN
  who = loop_active_party_slot(who, -1)
  menusound gen(genCursorSFX)
 END IF
 IF carray(ccRight) > 1 THEN
  who = loop_active_party_slot(who, 1)
  menusound gen(genCursorSFX)
 END IF
 IF game_check_use_key() THEN EXIT DO

 IF get_gen_bool("/mouse/mouse_menus") THEN
  DIM old_who as integer = who
  DIM hover as integer = -1
  FOR i as integer = 0 TO 3
   IF rect_collide_point(XYWH(100 + i * 30 - 5, 100 - 5, 30, 30), readmouse.pos) THEN
    hover = i
   END IF
  NEXT i
  IF (readmouse.release AND mouseRight) THEN do_exit = YES
  IF (readmouse.buttons AND mouseLeft) THEN
   IF hover >= 0 ANDALSO gam.hero(hover).id >= 0 THEN
    who = hover
   END IF
  END IF
  IF (readmouse.release AND mouseLeft) THEN
   IF hover >= 0 THEN
    IF gam.hero(hover).id >= 0 THEN
     IF who = hover THEN
      'Already selected, confirm
      EXIT DO
     ELSE
      who = hover
     END IF
    ELSE
     'Can't pick an empty slot
     menusound gen(genCancelSFX)
    END IF
   ELSE
    IF NOT rect_collide_point(Type(90, 74, 140, 52), readmouse.pos) THEN
     'left clicked outside the rectangle
     do_exit = YES
    END IF
   END IF
  END IF
  IF who <> old_who THEN menusound gen(genCursorSFX)
 END IF

 IF do_exit THEN
  who = -1
  menusound gen(genCancelSFX)
  EXIT DO
 END IF

 centerbox 160, 100, 140, 52, 1, page
 FOR party_slot as integer = 0 TO active_party_slots - 1
  IF gam.hero(party_slot).id >= 0 THEN
   DIM walkframe as integer
   IF who = party_slot THEN walkframe = wtog_to_frame(wtog) ELSE walkframe = 0
   set_walkabout_frame gam.hero(party_slot).sl, dirDown, walkframe
   DrawSliceAt LookupSliceSafe(SL_WALKABOUT_SPRITE_COMPONENT, gam.hero(party_slot).sl), 100 + party_slot * 30, 100, 20, 20, page, YES
  END IF
 NEXT
 edgeprint CHR(25), 106 + who * 30, 90, uilook(uiSelectedItem + tog), page
 wrapprint caption, pCentered, 80, uilook(uiText), page
 setvispage vpage
 dowait
LOOP
freepage page
setkeys
RETURN who
END FUNCTION

FUNCTION renamehero (byval who as integer, byval escapable as bool) as bool
'If escapable is true, then ESC exits, otherwise it resets the entered name
'Returns true if the player didn't cancel.
DIM ret as bool

DIM her as HeroDef
loadherodata her, gam.hero(who).id

DIM limit as integer = her.max_name_len
IF limit = 0 THEN limit = 16

DIM prompt as string = readglobalstring(137, "Name the Hero", 20)
DIM entry_box_width as integer = large(limit, LEN(gam.hero(who).name)) * 8
DIM remember as string = gam.hero(who).name

DIM need_fade_out as bool
IF faded_in = NO THEN
 setvispage vpage
 fadein
 need_fade_out = YES
END IF

DIM holdscreen as integer
holdscreen = allocatepage
copypage vpage, holdscreen

IF running_on_console() THEN
 'native virtual keyboard does not work on OUYA right now, so do this instead.
 gam.hero(who).name = gamepad_virtual_keyboard(gam.hero(who).name, limit)
 ret = YES
ELSEIF running_on_mobile() THEN
 hide_virtual_gamepad()
 gam.pad.being_shown = NO
 gam.hero(who).name = touch_virtual_keyboard(gam.hero(who).name, limit, prompt)
 ret = YES
ELSE
 'On platforms that need it (<s>currently</s> just Android) show the virtual keyboard.
 'This does nothing on platforms that don't need it.
 '
 'FIXME: this is obsoleted by touch_virtual_keyboard() but we may want it again
 '       in the future when the Android virtual keyboard is working again
 show_virtual_keyboard

 '--Wait while the player types a new name
 setkeys YES
 DO
  setwait speedcontrol
  setkeys YES
  playtimer
  IF game_check_use_key() AND keyval(scSpace) = 0 THEN
   IF LEN(gam.hero(who).name) > 0 THEN
    ret = YES
    EXIT DO
   ELSE
    menusound gen(genCancelSFX)
   END IF
  END IF
  IF game_check_cancel_key() THEN
   gam.hero(who).name = remember
   menusound gen(genCancelSFX)
   IF escapable THEN EXIT DO
  END IF
  strgrabber gam.hero(who).name, limit

  copypage holdscreen, vpage
  centerbox rCenter, rCenter, 168, 32, 1, vpage
  edgeprint prompt, pCentered, rCenter - 10, uilook(uiText), vpage
  rectangle pCentered, rCenter + 1, entry_box_width, 10, uilook(uiHighlight), vpage
  edgeprint gam.hero(who).name, pCentered, rCenter, uilook(uiMenuItem), vpage
 
  setvispage vpage
  dowait
 LOOP

 hide_virtual_keyboard
END IF

IF ret THEN menusound gen(genAcceptSFX)

IF need_fade_out THEN
 fadeout uilook(uiFadeoutNewGame)  'Usually happens if the starting hero is renameable
END IF
freepage holdscreen
RETURN ret
END FUNCTION

SUB resetgame ()
'Warning: don't put necessary initialisation in here,
'as this is ONLY called AFTER playing a game, on gameover/resetgame/loadgame/quit.
'initgamedefaults is probably a better place for that.
'In order words, be suspicious of anything in here that resets
'to a value other than zero/empty string/whatever the UDT's default value is.

gam.map.id = 0
(herox(0)) = 0
(heroy(0)) = 0
(heroz(0)) = 0
(herodir(0)) = 0
gam.getinputtext_enabled = NO
script_log_resetgame
'leader = 0
mapx = 0
mapy = 0
gold = 0
gam.showstring = ""
gam.debug_camera_pan = NO
gam.paused = NO
'--return gen to defaults
xbload game + ".gen", gen(), "General data is missing from " + sourcerpg

CleanNPCL npc()
flusharray tag()
flusharray onetime()
FOR i as integer = 0 TO 40
 'TODO: use HeroState.Constructor instead
 deletehero i, YES   'Don't forceparty or run triggers
 gam.hero(i).locked = NO
 FOR j as integer = 0 TO statLast
  gam.hero(i).stat.cur.sta(j) = 0
  gam.hero(i).stat.max.sta(j) = 0
  gam.hero(i).stat.base.sta(j) = 0
 NEXT j
 gam.hero(i).lev = 0
 gam.hero(i).lev_gain = 0
 gam.hero(i).battle_pic = 0
 gam.hero(i).battle_pal = 0
 gam.hero(i).pic = 0
 gam.hero(i).pal = 0
 gam.hero(i).def_wep = 0
 gam.hero(i).rename_on_status = NO
 'Hero elementals are actually initialised (to 1 = 100% damage) in addhero.
 FOR j as integer = 0 TO maxElements - 1
  gam.hero(i).elementals(j) = .0f
 NEXT
 FOR j as integer = 0 to 1
  gam.hero(i).hand_pos(j).x = 0
  gam.hero(i).hand_pos(j).y = 0
 NEXT j
 gam.hero(i).hand_pos_overridden = NO
 FOR o as integer = 0 TO 3
  FOR j as integer = 0 TO 23
   gam.hero(i).spells(o, j) = 0
  NEXT j
 NEXT o
 FOR o as integer = 0 TO maxMPLevel
  gam.hero(i).levelmp(o) = 0
 NEXT o
 gam.hero(i).exp_cur = 0
 gam.hero(i).exp_next = 0
 gam.hero(i).name = ""
 FOR o as integer = 0 TO UBOUND(gam.hero(i).equip)
  gam.hero(i).equip(o).id = -1
 NEXT o
NEXT i
CleanInventory(inventory())

flusharray global(), maxScriptGlobals, 0
reset_vehicle vstate
cleanup_text_box

FOR i as integer = 0 TO UBOUND(plotstr)
 WITH plotstr(i)
  .s = ""
  .col = -1  'default to uilook(uiText)
  .BGCol = 0
  .X = 0
  .Y = 0
  .Bits = 0
 END WITH
NEXT i

ERASE menus
ERASE mstates
topmenu = -1

'delete temp files that are part of the game state
deletetemps

'doesn't unload scripts: not needed
killallscripts

'This only happens when using Test Game
IF lump_reloading.hsp.changed THEN reload_scripts

REDIM timers(numInitialTimers - 1)

close_persist_reld()

cleanup_game_slices()
'plotslices() should now be all zeroed out (the .handles aren't). Rather than
'checking, check slicedebug() (if enabled)
#IFDEF ENABLE_SLICE_DEBUG
 SliceDebugDump YES
#ENDIF
SetupGameSlices
reset_game_state()

END SUB

FUNCTION gamepadmap_from_reload(gamepad as NodePtr, byval use_dpad as bool=NO) as GamePadMap
 DIM gp as GamePadMap
 IF use_dpad THEN
  gp.Ud = gamepad."UP".integer
  gp.Rd = gamepad."RIGHT".integer
  gp.Dd = gamepad."DOWN".integer
  gp.Ld = gamepad."LEFT".integer
 END IF
 gp.A = gamepad."A".integer
 gp.B = gamepad."B".integer
 gp.X = gamepad."X".integer
 gp.Y = gamepad."Y".integer
 gp.L1 = gamepad."L1".integer
 gp.R1 = gamepad."R1".integer
 gp.L2 = gamepad."L2".integer
 gp.R2 = gamepad."R2".integer
 RETURN gp
END FUNCTION

DESTRUCTOR HeroWalkabout ()
 v_free curzones
END DESTRUCTOR

'Unlike resetgame(), this is called before a game first is played,
'in addition to being called after playing (resetgame/gameover/loadgame/quit).
SUB reset_game_state ()
 reset_map_state(gam.map)
 gam.wonbattle = NO
 gam.remembermusic = -1
 gam.random_battle_countdown = range(100, 60)
 gam.mouse_enabled = NO
 defaultmousecursor  'init mouse state
 gam.pad.being_shown = NO

 'Resize these after we have loaded gen()
 REDIM gam.stock(gen(genMaxShop), 49)
 REDIM gam.original_stock(gen(genMaxShop), 49)
 
 DIM root as NodePtr = get_general_reld()

 DIM gamepad as NodePtr = root."gamepad".ptr
 remap_android_gamepad 0, gamepadmap_from_reload(gamepad, NO)
 FOR i as integer = 1 TO 3
  'Default the other gamepads to be the same as the first one
  '(this might be overridden in a moment)
  remap_android_gamepad i, gamepadmap_from_reload(gamepad, NO)
 NEXT i

 DIM player as integer
 IF NOT root."multiplayer_gamepads".exists THEN
  SetChildNode root, "multiplayer_gamepads"
 END IF
 READNODE root."multiplayer_gamepads" as multi
  WITHNODE multi."player" as playernode
   player = GetInteger(playernode)
   SELECT CASE player
    CASE 1 TO 3:
     remap_android_gamepad player, gamepadmap_from_reload(playernode, YES)
    CASE ELSE:
     debug "Ignore invalid multiplayer_gamepads.player id of " & player
   END SELECT
  END WITHNODE
 END READNODE

 remap_virtual_gamepad("virtual_gamepad")

 Achievements.runtime_reset

END SUB

SUB remap_virtual_gamepad(nodename as string)
 DIM root as NodePtr = get_general_reld()
 DIM mobile as NodePtr = root."mobile_options".ptr
 IF mobile = 0 THEN mobile = SetChildNode(root, "mobile_options")
 DIM vgp as NodePtr = GetChildByName(mobile, nodename)
 IF vgp = 0 THEN
  'node does not exist, create defaults
  vgp = SetChildNode(mobile, nodename)
  AppendChildNode(vgp, "button", scEnter)
  AppendChildNode(vgp, "button", scESC)
 END IF
 DIM button_id as integer = 0
 READNODE vgp, ignoreall
  WITHNODE vgp."button" as but
   remap_touchscreen_button button_id, GetInteger(but)
   button_id += 1
  END WITHNODE
 END READNODE
 FOR i as integer = button_id TO 5
  remap_touchscreen_button i, 0
 NEXT i
 update_virtual_gamepad_display()
END SUB

FUNCTION should_disable_virtual_gamepad() as bool
 IF NOT running_on_mobile() THEN RETURN NO
 DIM root as NodePtr = get_general_reld()
 DIM mobile as NodePtr = root."mobile_options".ptr
 IF mobile = 0 THEN mobile = SetChildNode(root, "mobile_options")
 RETURN mobile."disable_virtual_gamepad".exists
END FUNCTION

FUNCTION should_hide_virtual_gamepad_when_suspendplayer() as bool
 IF NOT running_on_mobile() THEN RETURN NO
 DIM root as NodePtr = get_general_reld()
 RETURN root."mobile_options"."hide_virtual_gamepad_when_suspendplayer".integer.default(NO) <> 0
END FUNCTION

SUB reset_map_state (map as MapModeState)
 map.id = gen(genStartMap)
 map.lastmap = -1
 map.same = NO
 map.name = ""
END SUB

'Calculate the maximum (full) amount of level MP points
'UBOUND(ret) should be maxMPLevel
SUB get_max_levelmp (ret() as integer, byval hero_level as integer)
 FOR mplevel as integer = 0 TO maxMPLevel
  ret(mplevel) = 0
 NEXT
 DIM nextlevel as integer = 0
 DIM turn_point as integer = 0
 FOR mplevel as integer = 0 TO hero_level
  ret(nextlevel) += 1
  nextlevel += 1
  IF nextlevel > turn_point THEN nextlevel = 0: turn_point += 1
  IF turn_point > maxMPLevel THEN turn_point = 0
 NEXT
END SUB

'Reset a hero's level mp to maximum amounts
SUB reset_levelmp (byref hero as HeroState)
 get_max_levelmp(hero.levelmp(), hero.lev)
END SUB

FUNCTION max_levelmp_for_hero(byref hero as HeroState, lmp_slot as integer) as integer
 'Currently this is the same for all heros based on level,
 'but it would be lovely to have a way to customize that in the future
 DIM maxes(UBOUND(hero.levelmp)) as integer
 get_max_levelmp maxes(), hero.lev
 RETURN maxes(lmp_slot)
END FUNCTION 

SUB shop (byval id as integer)

DIM storebuf(40) as integer
DIM menu(10) as string
DIM menuid(10) as integer
DIM sn as string
DIM autopick as bool
DIM w as integer
DIM tog as integer
DIM inn as integer
DIM rsr as integer
DIM c as integer
DIM t as integer
DIM o as integer
DIM page as integer
DIM holdscreen as integer
DIM st as MenuState

FOR i as integer = 0 TO 7
 menuid(i) = i
NEXT i

menu(0) = readglobalstring(70, "Buy", 10)
menu(1) = readglobalstring(71, "Sell", 10)
menu(2) = readglobalstring(73, "Hire", 10)
menu(3) = readglobalstring(72, "Inn", 10)
menu(4) = readglobalstring(63, "Equip", 10)
menu(5) = readglobalstring(66, "Save", 10)
menu(6) = readglobalstring(68, "Map", 10)
menu(7) = readglobalstring(65, "Team", 10)

st.size = 22
st.pt = 0
st.last = -1

'--initshop
loadrecord storebuf(), game + ".sho", 40 \ 2, id
sn = readbadbinstring(storebuf(), 0, 15, 0)
o = 0
FOR i as integer = 0 TO 7
 IF readbit(storebuf(), 17, i) THEN
  SWAP menu(i), menu(o)
  SWAP menuid(i), menuid(o)
  st.last = o
  o = o + 1
 END IF
NEXT i

IF st.last = -1 THEN EXIT SUB
IF st.last = 0 THEN autopick = YES
st.last += 1
menu(st.last) = readglobalstring(74, "Exit", 10)
menuid(st.last) = 8

DIM box as RectType
WITH box
 .wide = 96
 .high = (st.last + 2) * 10
 .x = 160 - .wide \ 2
 .y = 104
END WITH

menusound gen(genAcceptSFX)

'--Preserve background for display beneath top-level shop menu
holdscreen = duplicatepage(vpage)

page = compatpage

show_virtual_gamepad()

setkeys
DO
 setwait speedcontrol
 setkeys
 tog = tog XOR 1
 playtimer
 usemenu st
 usemenusounds

 DIM do_quit as bool = NO
 DIM do_pick as bool = NO

 IF game_check_cancel_key() THEN do_quit = YES
 IF game_check_use_key() THEN do_pick = YES

 IF get_gen_bool("/mouse/mouse_menus") ANDALSO autopick = NO THEN
  IF rect_collide_point(box, readmouse.pos) THEN
   DIM hover as integer = -1
   FOR i as integer = 0 TO st.last
    IF rect_collide_point(XYWH(box.x, box.y + 5 + i * 10, box.wide, 10), readmouse.pos) THEN hover = i
   NEXT i
   IF hover >= 0 THEN
    IF (readmouse.buttons AND mouseLeft) THEN
     st.pt = hover
    END IF
    IF (readmouse.release AND mouseLeft) THEN
     do_pick = YES
    END IF
   END IF
  ELSE
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
    do_quit = YES
   END IF
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   do_quit = YES
  END IF
 END IF
 
 IF do_quit THEN menusound gen(genCancelSFX) : EXIT DO
 IF do_pick ORELSE autopick THEN
  IF menuid(st.pt) = 8 THEN '--EXIT
   menusound gen(genCancelSFX)
   EXIT DO
  END IF
  IF menuid(st.pt) = 0 THEN '--BUY
   buystuff id, 0, storebuf()
  END IF
  IF menuid(st.pt) = 1 THEN '--SELL
   sellstuff id, storebuf()
  END IF
  IF menuid(st.pt) = 2 THEN '--HIRE
   buystuff id, 1, storebuf()
  END IF
  IF menuid(st.pt) = 6 THEN '--MAP
   minimap heropos(0)
  END IF
  IF menuid(st.pt) = 7 THEN '--TEAM
   hero_swap_menu 1
  END IF
  IF menuid(st.pt) = 4 THEN '--EQUIP
   w = onwho(readglobalstring(108, "Equip Who?", 20))
   IF w >= 0 THEN
    equip_menu w
   END IF
  END IF
  IF menuid(st.pt) = 5 THEN '--SAVE
   DIM slot as integer = picksave()
   IF slot >= 0 THEN savegame slot
  END IF
  IF menuid(st.pt) = 3 THEN '--INN
   IF useinn(storebuf(18), holdscreen) THEN
    innRestore
    IF storebuf(19) > 0 THEN
     '--Run animation for Inn
     trigger_script storebuf(19), 0, NO, "inn", "ID " & id, mainFibreGroup
     EXIT DO
    ELSE
     '--Inn has no script, do simple fade
     fadeout uilook(uiFadeoutInn)
     queue_fade_in
    END IF
   END IF
   copypage holdscreen, vpage
  END IF
  IF autopick THEN EXIT DO
  'Re-show the virtual gamepad, in case a special screen was triggered that hid it
  show_virtual_gamepad()
 END IF
 copypage holdscreen, vpage
 edgeboxstyle box, 0, page
 centerbox 160, 90, LEN(sn) * 8 + 8, 16, 1, page
 edgeprint sn, pCentered, 85, uilook(uiText), page
 FOR i as integer = 0 TO st.last
  c = uilook(uiMenuItem): IF st.pt = i THEN c = uilook(uiSelectedItem + tog)
  edgeprint menu(i), pCentered, 109 + i * 10, c, page
 NEXT i
 setvispage vpage
 check_for_queued_fade_in
 dowait
LOOP
clearkeys
freepage page
freepage holdscreen

evalitemtags
party_change_updates

END SUB

FUNCTION inn_screen (byval cost as integer, byval skip_fade as bool=NO) as bool
 'This is a simple wrapper for a basic Inn that is not associated with a shop.
 'It is used for textbox Inns, and scripted inns, but not Shop Inns
 '
 'Return value is YES if the player slept in the inn, or NO if they cancelled
 DIM result as bool = NO
 DIM holdscreen as integer = duplicatepage(vpage)
 IF useinn(cost, holdscreen) THEN
  innRestore
  IF NOT skip_fade THEN
   fadeout uilook(uiFadeoutInn)
   queue_fade_in 1, YES
  END IF
  result = YES
 END IF
 freepage holdscreen
 RETURN result
END FUNCTION

'Returns true if the player decided to sleep at the inn
'holdscreen is a copy of vpage (not a compatpage)
FUNCTION useinn (byval price as integer, byval holdscreen as integer) as bool
DIM menu(1) as string
DIM page as integer
page = compatpage
DIM tog as integer

DIM state as MenuState
state.last = UBOUND(menu)
state.size = 2

useinn = NO 'default return value

menu(0) = readglobalstring(49, "Pay", 10)
menu(1) = readglobalstring(50, "Cancel", 10)
DIM inncost as string = readglobalstring(143, "THE INN COSTS", 20)
DIM youhave as string = readglobalstring(145, "You have", 20)
menusound gen(genAcceptSFX)

show_virtual_gamepad()

setkeys
DO
 setwait speedcontrol
 setkeys
 tog = tog XOR 1
 playtimer
 DIM do_pick as bool = NO
 DIM do_quit as bool = NO
 IF game_check_cancel_key() THEN do_quit = YES
 usemenusounds
 usemenu state
 'alternatively
 IF carray(ccLeft) > 1 OR carray(ccRight) > 1 THEN
  menusound gen(genCursorSFX)
  state.pt = state.pt XOR 1
 END IF
 IF game_check_use_key() THEN do_pick = YES

 IF get_gen_bool("/mouse/mouse_menus") THEN
  IF rect_collide_point(Type(60, 60, 200, 60), readmouse.pos) THEN
   DIM hover as integer = -1
   FOR i as integer = 0 TO 1
    IF rect_collide_point(XYWH(160 - 60\2, 94 + i * 8, 60, 8), readmouse.pos) THEN hover = i
   NEXT i
   IF hover >= 0 THEN
    IF (readmouse.buttons AND mouseLeft) THEN state.pt = hover
    IF (readmouse.release AND mouseLeft) THEN do_pick = YES
   END IF
  ELSE
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
 END IF

 IF do_quit THEN
  menusound gen(genCancelSFX)
  EXIT DO
 END IF

 IF do_pick THEN
  IF state.pt = 0 AND gold >= price THEN  'Pay
   gold -= price
   useinn = YES
   menusound gen(genAcceptSFX)
   EXIT DO
  ELSE
   menusound gen(genCancelSFX)
  END IF
  IF state.pt = 1 THEN EXIT DO  'Cancel
 END IF

 'Draw screen
 copypage holdscreen, vpage
 edgeboxstyle 0, 3, 218, active_party_size() * 10 + 4, 0, page
 DIM y as integer = 0
 FOR i as integer = 0 TO active_party_slots - 1
  WITH gam.hero(i)
   IF .id >= 0 THEN
    DIM col as integer = uilook(uiText)
    edgeprint .name, 128 + ancRight, 5 + y * 10, col, page
    edgeprint .stat.cur.hp & "/" & .stat.max.hp, 136, 5 + y * 10, col, page
    y += 1
   END IF
  END WITH
 NEXT i
 centerfuz 160, 90, 200, 60, 1, page
 DIM menuwidth as integer = large(textwidth(menu(0)), textwidth(menu(1))) + 8
 rectangle 160 + ancCenter, 92, menuwidth, 22, uilook(uiHighlight), page 'orig colour 20
 edgeprint inncost & " " & price_string(price), pCentered, 70, uilook(uiText), page
 edgeprint youhave & " " & price_string(gold), pCentered, 80, uilook(uiText), page
 FOR i as integer = 0 TO 1
  DIM col as integer
  col = uilook(uiMenuItem)
  IF state.pt = i THEN col = uilook(uiSelectedItem + tog)
  edgeprint menu(i), pCentered, 94 + i * 8, col, page
 NEXT i

 setvispage vpage
 check_for_queued_fade_in
 dowait
LOOP
freepage page
party_change_updates
END FUNCTION

'Tag debug menu
SUB tagdisplay (page as integer)
STATIC st as MenuState
DIM lineh as integer = lineheight()
'It's possible to set other tags with "set tag", so don't limit to named tags
st.last = max_tag() 'gen(genMaxTagName)
IF gam.debug_showtags = 2 THEN
 st.size = small(st.last, get_resolution().h \ lineh - 1)
ELSE
 st.size = 6
END IF

'Controls
IF keyval(scCtrl) > 0 ORELSE keyval(scShift) > 0 THEN
 IF keyval(scNumpadMinus) > 1 OR keyval(scMinus) > 1 THEN
  settag st.pt, NO
  tag_updates
 END IF
 IF keyval(scNumpadPlus) > 1 OR keyval(scPlus) > 1 THEN
  settag st.pt, YES
  tag_updates
 END IF
ELSE
 IF keyval(scEnd) > 1 THEN
  'Go to last named tag instead of tag 15999
  st.pt = gen(genMaxTagName)
  correct_menu_state st
 ELSEIF usemenu(st, scMinus, scPlus) = NO THEN
  'little bit hacky...
  usemenu(st, scNumpadMinus, scNumpadPlus)
 END IF
END IF
IF keyval(scTab) > 1 THEN
 settag st.pt, NOT istag(st.pt, 0)
 tag_updates
END IF

''Draw the menu
'Help message, and a fuzzy box behind it
DIM helpmsg as string = !"To scroll:\n+/-/pgup/down\nTAB to toggle\nF4 expand/quit"
DIM helpbox as XYPair = textsize(helpmsg, , fontEdged)
fuzzyrect pRight, pTop, helpbox.w, helpbox.h, uilook(uiOutline), page
wrapprint helpmsg, pRight, 0, uilook(uiMenuItem), page
'This fuzzy box is exactly wide enough for the max tag name length
fuzzyrect 0, 0, 208, (st.size + 1) * lineh, uilook(uiOutline), page
DIM as integer col0 = findrgb(140,140,140), col1 = findrgb(230,230,230)
FOR i as integer = st.top TO st.top + st.size
 DIM temp as string = i & IIF(istag(i, 0), " +", " -")
 SELECT CASE i
  CASE 0, 1
   temp += "Reserved Tag"
  CASE IS > 1
   temp += load_tag_name(i)
   IF tag_is_autoset(i) THEN temp &= " [AUTOSET]"
   IF i = gen(genMaxTagName) + 1 THEN temp &= "[NO MORE NAMED TAGS]"
 END SELECT
 DIM col as integer
 IF istag(i, 0) THEN col = col1 ELSE col = col0
 DIM y as integer = (i - st.top) * lineh
 edgeprint temp, 8, y, col, page
 IF i = st.pt THEN edgeprint CHR(26), 0, y, uilook(uiText), page
NEXT i

END SUB

FUNCTION default_margin() as integer
 IF running_on_console() THEN
  'On OUYA, default to a safe 8% margin
  RETURN 8
 END IF
 ' on all other platforms, default to no margin
 RETURN 0
END FUNCTION

FUNCTION default_margin_for_game() as integer
 'Only for use when a game is currently loaded
 DIM root as NodePtr
 root = get_general_reld()
 RETURN root."console_options"."safe_margin".default(default_margin())
END FUNCTION


'==========================================================================================
'                                      Playing time
'==========================================================================================


FUNCTION playtime (byval d as integer, byval h as integer, byval m as integer) as string
 DIM s as string = ""

 SELECT CASE d
  CASE 1
   s = s & d & " " & readglobalstring(154, "day", 10) & " "
  CASE IS > 1
   s = s & d & " " & readglobalstring(155, "days", 10) & " "
 END SELECT

 SELECT CASE h
  CASE 1
   s = s & h & " " & readglobalstring(156, "hour", 10) & " "
  CASE IS > 1
   s = s & h & " " & readglobalstring(157, "hours", 10) & " "
 END SELECT

 SELECT CASE m
  CASE 1
   s = s & m & " " & readglobalstring(158, "minute", 10) & " "
  CASE IS > 1
   s = s & m & " " & readglobalstring(159, "minutes", 10) & " "
 END SELECT

 RETURN s

END FUNCTION

SUB playtimer
 STATIC n as double
 
 IF TIMER >= n + 1 OR n - TIMER > 3600 THEN
  n = INT(TIMER)
  gen(genSeconds) = gen(genSeconds) + 1
  WHILE gen(genSeconds) >= 60
   gen(genSeconds) = gen(genSeconds) - 60
   gen(genMinutes) = gen(genMinutes) + 1
  WEND
  WHILE gen(genMinutes) >= 60
   gen(genMinutes) = gen(genMinutes) - 60
   gen(genHours) = gen(genHours) + 1
   refresh_keepalive_file
  WEND
  WHILE gen(genHours) >= 24
   gen(genHours) = gen(genHours) - 24
   IF gen(genDays) < 32767 THEN gen(genDays) = gen(genDays) + 1
  WEND
 END IF
 IF autosnap > 0 ANDALSO (get_tickcount MOD autosnap) = 0 THEN
  write_checkpoint
 END IF
END SUB