'OHRRPGCE CUSTOM - Main module
'(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 "datetime.bi"  'for date serials
#include "string.bi"  'for date serials
#include "udts.bi"
#include "const.bi"
#include "allmodex.bi"
#include "cmdline.bi"
#include "common.bi"
#include "loading.bi"
#include "customsubs.bi"
#include "flexmenu.bi"
#include "slices.bi"
#include "cglobals.bi"
#include "uiconst.bi"
#include "scrconst.bi"
#include "sliceedit.bi"
#include "reloadedit.bi"
#include "editedit.bi"
#include "os.bi"
#include "distribmenu.bi"
#include "thingbrowser.bi"
#include "plankmenu.bi"
#include "editorkit.bi"
#include "custom.bi"


'''' Local function and type declarations

' Stores information about a previous or ongoing Custom editing session
TYPE SessionInfo
 workingdir as string              'The directory containing this session's files
 partial_rpg as bool               '__danger.tmp exists: was in process of unlumping or deleting lumps
 fresh_danger_tmp as bool          '__danger.tmp exists and isn't stale: may still be running
 info_file_exists as bool          'session_info.txt.tmp exists. If not, everything in this UDT below this point is unknown.
 pid as integer                    'Process ID or 0
 running as bool                   'That process is still running
 sourcerpg as string               'May be blank
 'The following are represented as native FB DateSerials, not Unix mtimes. 0.0 means N/A
 sourcerpg_old_mtime as double     'mtime of the sourcerpg when was opened/last saved by that copy of Custom
 sourcerpg_current_mtime as double 'mtime of the sourcerpg right now, as seen by us
 session_start_time as double      'When the game was last unlumped/saved (or if none, when Custom was launched)
 last_lump_mtime as double         'mtime of the most recently modified lump
END TYPE

DECLARE FUNCTION newRPGfile (templatefile as string, newrpg as string) as bool
DECLARE SUB setup_workingdir ()
DECLARE SUB check_for_crashed_workingdirs ()
DECLARE FUNCTION check_a_crashed_workingdir (sessinfo as SessionInfo) as bool
DECLARE FUNCTION empty_workingdir (workdir as string) as bool
DECLARE FUNCTION handle_dirty_workingdir (sessinfo as SessionInfo) as bool
DECLARE FUNCTION check_ok_to_open (filename as string) as bool
DECLARE FUNCTION get_previous_session_info (workdir as string) as SessionInfo

DECLARE SUB secret_menu ()
DECLARE SUB condition_test_menu ()
DECLARE SUB quad_transforms_menu ()
DECLARE SUB rotozoom_tests ()
DECLARE SUB text_test_menu ()
DECLARE SUB new_graphics_tests ()
DECLARE SUB plankmenu_cursor_move_tests
DECLARE SUB HTTP_demo()
DECLARE SUB CreateProcess_tests()
DECLARE SUB mouse_and_window_tests()

DECLARE SUB cleanup_and_terminate (show_quit_msg as bool = YES, retval as integer = 0)
DECLARE SUB import_scripts_and_terminate (scriptfile as string)
DECLARE SUB export_translations_and_terminate (translationfile as string)

DECLARE SUB prompt_for_password()
DECLARE SUB prompt_for_save_and_quit()
DECLARE SUB choose_rpg_to_create_or_load (rpg_browse_default as string)
DECLARE SUB main_editor_menu()
DECLARE SUB gfx_editor_menu()


'=================================== Globals ==================================

DIM activepalette as integer = -1
'The following are set from commandline options
DIM auto_distrib as string 'Which distribution option to package automatically
DIM option_nowait as bool  'Currently only used when importing scripts from the commandline: don't wait
DIM option_hsflags as string  'Used when importing scripts from the commandline: extra args to pass
DIM export_translations_to as string

DIM editing_a_game as bool
DIM last_active_seconds as double

DIM channel_to_Game as IPCChannel = NULL
#IFNDEF NO_TEST_GAME
DIM Game_process as ProcessHandle = 0
#ENDIF

'Should we delete workingdir when quitting normally?
'False if relumping workingdir failed.
DIM cleanup_workingdir_on_exit as bool = YES

'Affects show/fatalerror: have we started editing (ie. finished upgrades and other startup)?
'If not, we should cleanup working.tmp instead of preserving it
DIM cleanup_workingdir_on_error as bool = YES

setup_global_reload_doc


'======================== Setup directories & debug log =======================
' This is almost identical to startup code in Game; please don't unnecessarily diverge.

orig_dir = CURDIR
'Note: debug log messages go in CURDIR until log_dir set below

'Processes the -appdir commandline flag
set_app_dir

'temporarily set current directory, will be changed to game directory later if writable
'(This is where new .rpg files go by default)
'(This change in working directory is done only by Custom, not Game)
IF diriswriteable(app_dir) THEN
 'When CUSTOM is installed read-write, work in CUSTOM's folder
 CHDIR app_dir
ELSE
 'If CUSTOM is installed read-only, use your Documents dir as the default
 '(On Mac, this will also happen due to Gatekeeper Path Randomization (aka App Translocation))
 CHDIR get_documents_dir()
END IF

#IFDEF __FB_ANDROID__
 'Prevent log_dir from being changed to the .rpg directory
 '(But why? If it's on external storage, that seems like great place to put it)
 log_dir = orig_dir & SLASH
 overrode_log_dir = YES
#ELSE
 log_dir = CURDIR & SLASH
#ENDIF

'Once log_dir is set, can create debug log.
start_new_debug "Starting OHRRPGCE Custom"
debuginfo DATE & " " & TIME
debuginfo long_version & build_info
debuginfo "sysinfo: " & get_system_info()
debuginfo "exepath: " & EXEPATH & ", exe: " & COMMAND(0)
debuginfo "orig_dir: " & orig_dir
debuginfo "curdir: " & CURDIR

settings_dir = get_settings_dir()
documents_dir = get_documents_dir()  'may depend on app_dir
debuginfo "documents_dir: " & documents_dir
'FIXME: why do we use different temp dirs in game and custom?
'Plus, tmpdir is shared between all running copies of Custom, which could cause problems.
tmpdir = settings_dir & SLASH
IF NOT isdir(tmpdir) THEN fatalerror "Unable to create temp directory " & tmpdir

set_global_config_file
debuginfo "config: " & global_config_file
flush_gfx_config_settings


'========================== Process commandline flags =========================

'Read the default backend from config first so that --gfx overrides it.
'And if it's missing, default to SDL/SDL2: currently only these support non-320x200
prefer_gfx_backend "sdl"
prefer_gfx_backend "sdl2"
DIM backend as string = read_config_str("gfx.backend")
IF LEN(backend) THEN prefer_gfx_backend backend

REDIM cmdline_args() as string
' This can modify log_dir and restart the debug log
processcommandline cmdline_args(), @gamecustom_setoption, orig_dir & SLASH & "ohrrpgce_arguments.txt"


'======================= Initialise backends/graphics =========================

load_gfx_defaults  'Loads master(), uilook(), boxlook(), current_font()

set_resolution read_config_int("gfx.resolution_w", 480), read_config_int("gfx.resolution_h", 320)
IF overrode_default_zoom = NO THEN
 set_scale_factor read_config_int("gfx.zoom", 2), YES
END IF
setmodex
unlock_resolution 320, 200   'Minimum window size

setwindowtitle "O.H.R.RPG.C.E"
showmousecursor

setupmusic

'Cleanups/recovers any working.tmp for any crashed copies of Custom; requires graphics up and running
check_for_crashed_workingdirs

'This also calls write_session_info
setup_workingdir


'=============================== Select a game ================================

DIM scriptfile as string
DIM rpg_browse_default as string

FOR i as integer = 0 TO UBOUND(cmdline_args)
 DIM arg as string
 arg = simplify_path(absolute_with_orig_path(cmdline_args(i)))
 DIM extn as string = LCASE(justextension(arg))

 IF (extn = "hs" OR extn = "hss" OR extn = "txt") ANDALSO isfile(arg) THEN
  scriptfile = arg
  CONTINUE FOR
 ELSEIF extn = "rpg" ANDALSO isfile(arg) THEN
  sourcerpg = arg
 ELSEIF isdir(arg) THEN
  IF isfile(arg + SLASH + "archinym.lmp") THEN 'ok, accept it
   sourcerpg = trim_trailing_slashes(arg)
  ELSE
   rpg_browse_default = arg
  END IF
 ELSE
  visible_debug !"File not found/invalid option:\n" & cmdline_args(i)
 END IF
NEXT
IF sourcerpg = "" THEN
 scriptfile = ""
 'Show the title menu. Sets sourcerpg
 choose_rpg_to_create_or_load(rpg_browse_default)
END IF

sourcerpg = absolute_path(sourcerpg)

IF check_ok_to_open(sourcerpg) = NO THEN
 cleanup_and_terminate NO
END IF

'================= Setup game-specific directories & debug log ================

' Set up game_fname, prefsdir, and game_config_file variables
set_game_config_globals sourcerpg

flush_gfx_config_settings

write_session_info

DIM dir_to_change_into as string = trimfilename(sourcerpg)

end_debug
IF dir_to_change_into <> "" ANDALSO diriswriteable(dir_to_change_into) THEN
 CHDIR dir_to_change_into
 IF overrode_log_dir = NO THEN log_dir = dir_to_change_into & SLASH
END IF
'otherwise, keep current directory as it was (FIXME: ideally would now be the same as in Game)
'Final log_dir set, no more need to remember.
remember_debug_messages = NO

start_new_debug "Loading a game"
debuginfo DATE & " " & TIME
debuginfo "curdir: " & CURDIR
debuginfo "tmpdir: " & tmpdir
debuginfo "settings_dir: " & settings_dir
debuginfo "config: " & global_config_file


'============================= Unlump, Upgrade, Load ==========================

'Start counting edit_time from now
active_seconds = 0.
idle_time_threshold = large(read_config_int("idle_time", 30), 1)

'For getdisplayname
copylump sourcerpg, "archinym.lmp", workingdir, YES

IF nogfx_mode THEN PRINT "Loading " & sourcerpg
debuginfo "Editing game " & sourcerpg & " (" & getdisplayname(" ") & ")"
setwindowtitle "O.H.R.RPG.C.E - " + trimpath(sourcerpg)

'--set game according to the archinym
copylump sourcerpg, "archinym.lmp", workingdir, YES
copylump sourcerpg, "*.gen", workingdir, YES
DIM archinym as string
archinym = readarchinym(workingdir, sourcerpg)
game = workingdir + SLASH + archinym

copylump sourcerpg, archinym + ".gen", workingdir
xbload game + ".gen", gen(), "general data is missing: RPG file appears to be corrupt"

IF gen(genVersion) > CURRENT_RPG_VERSION THEN
 debug "genVersion = " & gen(genVersion)
 future_rpg_warning
END IF

prompt_for_password

clearpage vpage
textcolor uilook(uiText), 0
printstr "UNLUMPING DATA: please wait.", pMenuX, pMenuY, vpage
setvispage vpage, NO

touchfile workingdir + SLASH + "__danger.tmp"
IF isdir(sourcerpg) THEN
 'work on an unlumped RPG file. Don't take hidden files
 'Convert to lowercase while copying (only needed for ancient unlumped games)
 copyfiles sourcerpg, workingdir, , YES
ELSE
 unlump sourcerpg, workingdir + SLASH
END IF
safekill workingdir + SLASH + "__danger.tmp"

'Perform additional checks for future rpg files or corruption
rpg_sanity_checks

'upgrade obsolete RPG files
upgrade YES

set_music_volume 0.01 * gen(genMusicVolume)
set_global_sfx_volume 0.01 * gen(genSFXVolume)

'Unload any default graphics (from data/defaultgfx) that might have been cached, load palettes
sprite_empty_cache
palette16_reload_cache

'Load the game's palette, uicolors, font
activepalette = gen(genMasterPal)
load_master_and_uicol activepalette
setpal master()
clearpage dpage
clearpage vpage
xbload game + ".fnt", current_font(), "Font not loaded"
setfont current_font()

loadglobalstrings
getstatnames statnames()
load_special_tag_caches
load_script_triggers_and_names  'Also called in upgrade(), but be sure

IF scriptfile <> "" THEN import_scripts_and_terminate scriptfile

'Set by --export-trans
IF export_translations_to <> "" THEN export_translations_and_terminate export_translations_to

IF auto_distrib <> "" THEN
 auto_export_distribs auto_distrib
 cleanup_workingdir_on_exit = YES
 cleanup_and_terminate NO
END IF

'Reset start of session to after upgrades (to see which lumps are edited)
write_session_info

'From here on, preserve working.tmp if something goes wrong
cleanup_workingdir_on_error = NO

'debuginfo "mem usage " & memory_usage_string()

IF isdir(CURDIR & SLASH & "import") THEN set_browse_default(CURDIR & SLASH & "import")

editing_a_game = YES
main_editor_menu
'Execution ends inside main_editor_menu

'=======================================================================

SUB main_editor_menu()
 REDIM menu(20) as string
 DIM menu_display(UBOUND(menu)) as string

 menu(0) = "Edit Graphics"
 menu(1) = "Edit Maps"
 menu(2) = "Edit Heroes"
 menu(3) = "Edit Enemies"
 menu(4) = "Edit Attacks"
 menu(5) = "Edit Battle Formations"
 menu(6) = "Edit Items"
 menu(7) = "Edit Shops"
 menu(8) = "Edit Text Boxes"
 menu(9) = "Edit Tag Names"
 menu(10) = "Edit Menus"
 menu(11) = "Edit Slice Collections"
 menu(12) = "Edit Vehicles"
 menu(13) = "Import Music"
 menu(14) = "Import Sound Effects"
 menu(15) = "Edit Global Text Strings"
 menu(16) = "Edit General Game Settings"
 menu(17) = "Script Management"
 menu(18) = "Distribute Game"
 #IFDEF NO_TEST_GAME
  menu(19) = "Quit or Save"
  REDIM PRESERVE menu(19)
 #ELSE
  menu(19) = "Test Game"
  menu(20) = "Quit or Save"
 #ENDIF

 DIM selectst as SelectTypeState
 DIM state as MenuState
 state.last = UBOUND(menu)
 state.autosize = YES
 state.autosize_ignore_pixels = 36

 setkeys YES
 DO
  setwait 55
  setkeys YES

  usemenu state
  IF keyval(ccCancel) > 1 THEN
   prompt_for_save_and_quit
  END IF
  IF keyval(scF1) > 1 THEN
   show_help "main"
  END IF

  IF keyval(scF5) > 1 THEN   'Redundant, but for people with muscle memory
   reimport_previous_scripts
  END IF

  IF select_by_typing(selectst) THEN
   IF RIGHT(selectst.buffer, 4) = "spam" THEN
    select_clear selectst
    secret_menu
   ELSE
    select_on_word_boundary_excluding menu(), selectst, state, "edit"
   END IF
  END IF

  IF enter_space_click(state) THEN
   IF state.pt = 0 THEN gfx_editor_menu
   IF state.pt = 1 THEN map_picker
   IF state.pt = 2 THEN hero_editor_main
   IF state.pt = 3 THEN enemy_editor_main
   IF state.pt = 4 THEN attack_editor_main
   IF state.pt = 5 THEN formation_editor
   IF state.pt = 6 THEN item_editor
   IF state.pt = 7 THEN shop_editor_main
   IF state.pt = 8 THEN textbox_editor_main
   IF state.pt = 9 THEN tags_menu
   IF state.pt = 10 THEN menu_editor
   IF state.pt = 11 THEN slice_editor SL_COLLECT_USERDEFINED
   IF state.pt = 12 THEN vehicle_editor
   IF state.pt = 13 THEN song_editor_main
   IF state.pt = 14 THEN sfx_editor_main
   IF state.pt = 15 THEN global_text_strings_editor
   IF state.pt = 16 THEN general_data_editor
   IF state.pt = 17 THEN script_management
   IF state.pt = 18 THEN distribute_game_menu
   #IFDEF NO_TEST_GAME
    IF state.pt = 19 THEN prompt_for_save_and_quit
   #ELSE
    IF state.pt = 19 THEN spawn_game_menu(keyval(scShift) > 0, keyval(scCtrl) > 0)
    IF state.pt = 20 THEN prompt_for_save_and_quit
   #ENDIF
   '--always resave .GEN and general.reld after any menu
   '(I don't know whether saving GEN is necessary, but saving general.reld
   'is just in case we forget wherever it should have been saved)
   xbsave game + ".gen", gen(), 1000
   write_general_reld()
  END IF

  clearpage dpage
  highlight_menu_typing_selection menu(), menu_display(), selectst, state
  standardmenu menu_display(), state, , , dpage

  textcolor uilook(eduiNote), 0
  printstr version_code, pInfoX, pInfoY - 18, dpage, , fontBuiltinPlain
  printstr version_build & " In use: " & gfxbackend & "/" & musicbackend, pInfoX, pInfoY - 9, dpage, , fontBuiltinPlain
  textcolor uilook(uiText), 0
  printstr "Press F1 for help on any menu!", pInfoX, pInfoY, dpage, , fontBuiltinPlain
 
  SWAP vpage, dpage
  setvispage vpage
  dowait
 LOOP
END SUB

SUB gfx_editor_menu()
 DIM menu(16) as string
 DIM menu_display(UBOUND(menu)) as string

 menu(0) = "Return to Main Menu"
 menu(1) = "Edit Tilesets"
 menu(2) = "Import/Export Tilesets"
 menu(3) = "Draw Walkabout Graphics"
 menu(4) = "Draw Hero Battle Graphics"
 menu(5) = "Draw Small Enemy Graphics  34x34"
 menu(6) = "Draw Medium Enemy Graphics 50x50"
 menu(7) = "Draw Big Enemy Graphics    80x80"
 menu(8) = "Draw Attacks"
 menu(9) = "Draw Weapons"
 menu(10) = "Draw Box Edges"
 menu(11) = "Draw Portraits"
 menu(12) = "Import/Export Backdrops"
 menu(13) = "Change User-Interface Colors"
 menu(14) = "Change Box Styles"
 menu(15) = "Master Palettes"
 menu(16) = "Edit Font"

 DIM selectst as SelectTypeState
 DIM state as MenuState
 state.size = 24
 state.last = UBOUND(menu)

 setkeys YES
 DO
  setwait 55
  setkeys YES
  IF keyval(ccCancel) > 1 THEN
   EXIT DO
  END IF
  IF keyval(scF1) > 1 THEN
   show_help "gfxmain"
  END IF
  usemenu state

  IF select_by_typing(selectst) THEN
   select_on_word_boundary menu(), selectst, state
  END IF

  IF enter_space_click(state) THEN
   IF state.pt = 0 THEN
    EXIT DO
   END IF
   IF state.pt = 1 THEN tileset_editor
   IF state.pt = 2 THEN import_export_tilesets
   IF state.pt = 3 THEN spriteset_editor sprTypeWalkabout
   IF state.pt = 4 THEN spriteset_editor sprTypeHero
   IF state.pt = 5 THEN spriteset_editor sprTypeSmallEnemy
   IF state.pt = 6 THEN spriteset_editor sprTypeMediumEnemy
   IF state.pt = 7 THEN spriteset_editor sprTypeLargeEnemy
   IF state.pt = 8 THEN spriteset_editor sprTypeAttack
   IF state.pt = 9 THEN spriteset_editor sprTypeWeapon
   IF state.pt = 10 THEN spriteset_editor sprTypeBoxBorder
   IF state.pt = 11 THEN spriteset_editor sprTypePortrait
   IF state.pt = 12 THEN backdrop_browser
   IF state.pt = 13 THEN ui_color_editor(activepalette)
   IF state.pt = 14 THEN ui_boxstyle_editor(activepalette)
   IF state.pt = 15 THEN master_palette_menu
   IF state.pt = 16 THEN font_editor current_font()
   '--always resave the .GEN lump after any menu
   xbsave game + ".gen", gen(), 1000
  END IF
 
  clearpage dpage
  highlight_menu_typing_selection menu(), menu_display(), selectst, state
  standardmenu menu_display(), state, , , dpage
 
  SWAP vpage, dpage
  setvispage vpage
  dowait
 LOOP

END SUB

'This sub sets the sourcerpg global
SUB choose_rpg_to_create_or_load (rpg_browse_default as string)

 DIM state as MenuState
 state.pt = 1
 state.last = 2
 state.size = 20

 DIM root as Slice ptr
 root = NewSliceOfType(slContainer)
 SliceLoadFromFile root, finddatafile("choose_rpg.slice")
 
 DIM chooserpg_menu(2) as string
 chooserpg_menu(0) = "CREATE NEW GAME"
 chooserpg_menu(1) = "LOAD EXISTING GAME"
 chooserpg_menu(2) = "EXIT PROGRAM"
 DIM opts as MenuOptions
 opts.edged = YES

 setkeys
 DO
  setwait 55
  setkeys
  IF keyval(ccCancel) > 1 THEN cleanup_and_terminate
  IF keyval(scF1) > 1 THEN show_help "choose_rpg"
  IF keyval(scF6) > 1 THEN slice_editor root, SL_COLLECT_EDITOR, "choose_rpg.slice"

  DIM menusl as Slice ptr = LookupSliceSafe(SL_EDITOR_SPLASH_MENU, root)

  usemenu state
  IF enter_space_click(state) THEN
   SELECT CASE state.pt
    CASE 0
     DIM path as string
     path = inputfilename("Filename of New Game?", ".rpg", rpg_browse_default, "input_file_new_game", , NO)
     IF path <> "" THEN
       sourcerpg = path & ".rpg"
       IF NOT newRPGfile(finddatafile("ohrrpgce.new"), sourcerpg) THEN cleanup_and_terminate
       EXIT DO
     END IF
    CASE 1
     sourcerpg = browse(browseRPG, rpg_browse_default, , "custom_browse_rpg")
     IF sourcerpg <> "" THEN EXIT DO
    CASE 2
     cleanup_and_terminate
   END SELECT
  END IF
 
  clearpage dpage
  DrawSlice root, dpage
  standardmenu chooserpg_menu(), state, menusl->ScreenX, menusl->ScreenY, dpage, opts
  wrapprint short_version & " " & gfxbackend & "/" & musicbackend, 8, pBottom - 14, uilook(uiMenuItem), dpage
  edgeprint "Press F1 for help on any menu!", 8, pBottom - 4, uilook(uiText), dpage

  SWAP vpage, dpage
  setvispage vpage
  dowait
 LOOP
 DeleteSlice @root
END SUB

SUB prompt_for_save_and_quit()
 xbsave game & ".gen", gen(), 1000

 DIM quit_menu(3) as string
 quit_menu(0) = "Continue editing"
 quit_menu(1) = "Save changes and continue editing"
 quit_menu(2) = "Save changes and quit"
 quit_menu(3) = "Discard changes and quit"
 setquitflag NO  'Stop firing esc's, if the user asked to quit the program

 DIM quitnow as integer
 quitnow = multichoice("", quit_menu(), 0, 0, "quit_and_save")

 IF getquitflag() THEN '2nd quit request? Right away!
  DIM basename as string = trimextension(sourcerpg)
  DIM lumpfile as string
  DIM i as integer = 0
  DO
   lumpfile = basename & ".rpg_" & i & ".bak"
   i += 1
  LOOP WHILE isfile(lumpfile)
  clearpage vpage
  printstr "Saving as " & lumpfile, pMenuX, pMenuY, vpage
  printstr "LUMPING DATA: please wait...", pMenuX, pMenuY + 10, vpage
  setvispage vpage, NO
  write_rpg_or_rpgdir workingdir, lumpfile
  cleanup_and_terminate
  EXIT SUB
 END IF

#IFNDEF NO_TEST_GAME
 IF (quitnow = 2 OR quitnow = 3) ANDALSO channel_to_Game THEN
  'Prod the channel to see whether it's still up (send ping)
  channel_write_line(channel_to_Game, "P ")

  IF channel_to_Game THEN
   IF yesno("You are still running a copy of this game. Quitting will force " & GAMEEXE & " to quit as well. Really quit?") = NO THEN quitnow = 0
  END IF
 END IF
#ENDIF

 IF quitnow = 1 OR quitnow = 2 THEN
  save_current_game
 END IF
 IF quitnow = 3 THEN
  IF twochoice("", "I changed my mind! Don't quit!", "I am sure I don't want to save.", 0, 0) = 0 THEN quitnow = 0
  cleanup_workingdir_on_exit = YES  'This only makes a difference if a previous attempt to save failed
 END IF
 setkeys YES
 IF quitnow > 1 THEN cleanup_and_terminate

END SUB

SUB prompt_for_password()
 '--Is a password set?
 IF checkpassword("") THEN EXIT SUB
 
 '--Input password
 DIM pas as string = ""
 DIM passcomment as string = ""
 DIM tog as integer
 passcomment = "If you've forgotten your password, don't panic! It can be easily removed. " _
               "Contact the OHRRPGCE developers, or learn to compile the source code yourself."
 'Uncomment to display the/a password
 'passcomment = getpassword
 setkeys YES
 DO
  setwait 55
  setkeys YES
  tog = tog XOR 1
  IF keyval(ccCancel) > 0 THEN cleanup_and_terminate
  IF keyval(scAnyEnter) > 1 THEN
   IF checkpassword(pas) THEN
    EXIT SUB
   ELSE
    cleanup_and_terminate
   END IF
  END IF
  strgrabber pas, 17
  clearpage dpage
  wrapprint "This game requires a password to edit. Type it in and press ENTER", 10, 10, uilook(uiText), dpage
  textcolor uilook(uiSelectedItem + tog), 1
  printstr STRING(LEN(pas), "*"), 20, 40, dpage
  wrapprint passcomment, 15, pBottom - 15, uilook(uiText), dpage, rWidth - 30
  SWAP vpage, dpage
  setvispage vpage
  dowait
 LOOP
END SUB

SUB import_scripts_and_terminate (scriptfile as string)
 debuginfo "Importing scripts from " & scriptfile
 DIM success as bool
 success = compile_andor_import_scripts(absolute_with_orig_path(scriptfile), option_nowait)
 IF success THEN
  xbsave game & ".gen", gen(), 1000
  save_current_game
 END IF
 cleanup_workingdir_on_exit = YES  'Cleanup even if saving the .rpg failed: no loss
 IF success = NO AND option_nowait THEN PRINT "Compiling or importing failed"
 cleanup_and_terminate NO, IIF(success, 0, 1)
END SUB

SUB export_translations_and_terminate (translationfile as string)
 debuginfo "Importing scripts from " & translationfile
 DIM success as bool
 success = export_translations(translationfile)
 PRINT "Exporting " & translationfile & IIF(success, " succeeded", " failed")
 cleanup_and_terminate NO, IIF(success, 0, 1)
END SUB

SUB cleanup_and_terminate (show_quit_msg as bool = YES, retval as integer = 0)
 debuginfo "Cleaning up and terminating, exitcode=" & retval

 save_window_state_to_config

#IFNDEF NO_TEST_GAME
 IF channel_to_Game THEN
  channel_write_line(channel_to_Game, "Q ")
  #IFDEF __FB_WIN32__
   'On windows, can't delete workingdir until Game has closed the music. Not too serious though
   basic_textbox "Waiting for " & GAMEEXE & " to clean up...", uilook(uiText), vpage
   setvispage vpage, NO
   IF channel_wait_for_msg(channel_to_Game, "Q", "", 2000) = 0 THEN
    basic_textbox "Waiting for " & GAMEEXE & " to clean up... giving up.", uilook(uiText), vpage
    setvispage vpage, NO
    sleep 700
   END IF
  #ENDIF
  channel_close(channel_to_Game)
 END IF
 IF Game_process <> 0 THEN
  basic_textbox "Waiting for " & GAMEEXE & " to quit...", uilook(uiText), vpage
  setvispage vpage, NO
  'Under GNU/Linux this calls pclose which will block until Game has quit.
  cleanup_process @Game_process
 END IF
#ENDIF

 closemusic
 cleanup_global_reload_doc
 clear_binsize_cache
 clear_fixbits_cache
 game = ""
 sourcerpg = ""
 'Catch sprite leaks (also deletes the slice editor clipboard)
 sprite_empty_cache
 palette16_reload_cache   'Read default palettes (now that game="")
 spriteset_editor_delete_clipboard

 IF show_quit_msg ANDALSO read_config_bool("show_quit_msg", YES) ANDALSO getquitflag() = NO THEN
  clearpage vpage
  ' Don't let Spoonweaver's cat near your power cord!
  pop_warning !"Remember to keep backup copies of your work!\n\nYou never know when an unknown bug, a cat-induced hard-drive crash or a little brother might delete your files!", YES
 END IF
 IF cleanup_workingdir_on_exit THEN
  empty_workingdir workingdir
 END IF
 restoremode
 debuginfo "End."
 'Delete c_debug.txt if no errors, regardless of retval: nonzero (e.g. script
 'import failed) is not necessarily a serious error
 end_debug
 terminate_program retval
END SUB


'==========================================================================================
'                                       Global menus
'==========================================================================================


LOCAL FUNCTION volume_controls_callback(menu as MenuDef, state as MenuState, dataptr as any ptr) as bool
 ' This code is duplicated from player_menu_keys :(
 IF keyval(scF1) > 1 THEN show_help("editor_volume")
 DIM BYREF mi as MenuDefItem = *menu.items[state.pt]
 IF mi.t = mtypeSpecial AND (mi.sub_t = spMusicVolume OR mi.sub_t = spVolumeMenu) THEN
  IF keyval(ccLeft) > 1 THEN set_music_volume large(get_music_volume - 1/16, 0.0)
  IF keyval(ccRight) > 1 THEN set_music_volume small(get_music_volume + 1/16, 1.0)
 END IF
 IF mi.t = mtypeSpecial AND mi.sub_t = spSoundVolume THEN
  IF keyval(ccLeft) > 1 THEN set_global_sfx_volume large(get_global_sfx_volume - 1/16, 0.0)
  IF keyval(ccRight) > 1 THEN set_global_sfx_volume small(get_global_sfx_volume + 1/16, 1.0)
 END IF
 RETURN NO
END FUNCTION

' Allow changing the in-editor volume
SUB Custom_volume_menu
 DIM menu as MenuDef
 create_volume_menu menu
 run_MenuDef menu, @volume_controls_callback
END SUB

'Record a combined editor+player gif
SUB start_recording_combined_gif()
#IFNDEF NO_TEST_GAME
 IF channel_to_Game = NULL THEN EXIT SUB
 DIM screenfile as string = tmpdir & "screenshare" & randint(100000) & ".bmp"
 channel_write_line(channel_to_Game, "SCREEN " & screenfile)
 start_recording_gif screenfile
 debuginfo "...recording with secondscreen " & screenfile
#ENDIF
END SUB

TYPE CustomGlobalMenu
 items(any) as string
 item_codes(any) as integer
 DECLARE SUB append(code as integer, text as string)
END TYPE

SUB CustomGlobalMenu.append(code as integer, text as string)
 a_append item_codes(), code
 a_append items(), text
END SUB

' Accessible with F8 if we are editing a game
SUB Custom_global_menu
 DIM holdscreen as integer = duplicatepage(getvispage)  'For screenshots

 DIM menu as CustomGlobalMenu
 IF editing_a_game THEN
  IF inside_importscripts = NO THEN
   'Don't reallow importing if we're already in the middle of it
   'TODO: maybe this should also be disallowed from inside scriptbrowse, etc?
   menu.append 0, "Reimport scripts"
  END IF
  #IFNDEF NO_TEST_GAME
   menu.append 1, "Test Game"
  #ENDIF
  'menu.append 10, "Save Game"
 END IF
 menu.append 2, "Volume"
 menu.append 3, "Macro record/replay (Shft/Ctrl-F11)"
 menu.append 14, "Screenshot (F12)"

 menu.append 12, IIF(recording_gif(), "Stop recording", "Record") & " .gif video (Shft/Ctrl-F12)"
 IF channel_to_Game ANDALSO recording_gif() = NO THEN
  menu.append 11, "Record combined editor+player .gif"
 END IF

 'On Mac, Cmd-1/2/3/4 is handled by keycombos_logic in gfx_sdl and gfx_sdl2
 FOR zoom as integer = 1 TO 4
  #IFDEF __FB_DARWIN__
   menu.append 3 + zoom, "Zoom to " & zoom & "x (Cmd-" & zoom & ")"
  #ELSE
   menu.append 3 + zoom, "Zoom to " & zoom & "x"
  #ENDIF
 NEXT

 menu.append 8, "Engine Settings menu (Shft/Ctrl-F7)"

 menu.append 15, "View/edit ohrrpgce_config.ini"
 IF LEN(sourcerpg) THEN
  menu.append 16, "View/edit gameconfig.ini"
 END IF

 DIM note as string
 IF num_logged_errors THEN note = ": " & num_logged_errors & " errors" ELSE note = " log"
 menu.append 13, "View c_debug.txt" & note & " (Shft/Ctrl-F8)"

 DIM choice as integer
 choice = multichoice("Global Editor Options (F9)", menu.items())
 IF choice > -1 THEN choice = menu.item_codes(choice)

 IF choice = 0 THEN
  reimport_previous_scripts
#IFNDEF NO_TEST_GAME
 ELSEIF choice = 1 THEN
  spawn_game_menu(keyval(scShift) > 0, keyval(scCtrl) > 0)
#ENDIF
 ELSEIF choice = 2 THEN
  Custom_volume_menu
 ELSEIF choice = 3 THEN
  macro_controls
 ELSEIF choice = 4 THEN
  set_scale_factor 1, NO
 ELSEIF choice = 5 THEN
  set_scale_factor 2, NO
 ELSEIF choice = 6 THEN
  set_scale_factor 3, NO
 ELSEIF choice = 7 THEN
  set_scale_factor 4, NO
 ELSEIF choice = 8 THEN
  engine_settings_menu
 ELSEIF choice = 10 THEN
  'Warning: data in the current menu may not be saved! So figured it better to avoid this.
  save_current_game
 ELSEIF choice = 11 THEN
  start_recording_combined_gif
 ELSEIF choice = 12 THEN
  toggle_recording_gif
 ELSEIF choice = 13 THEN
  open_document log_dir & *app_log_filename
 ELSEIF choice = 14 THEN
  screenshot , holdscreen
 ELSEIF choice = 15 THEN
  open_document global_config_file
 ELSEIF choice = 16 THEN
  IF NOT isfile(game_config_file) THEN
   'We didn't ensure the existence of prefsdir in set_game_config_globals
   IF NOT isdir(prefsdir) THEN makedir prefsdir
   touchfile game_config_file
  END IF
  open_document game_config_file
 END IF
 freepage holdscreen
END SUB

' This is called after every setkeys unless we're already inside global_setkeys_hook
' It should be fine to call any allmodex function in here, but beware we might
' not have loaded a game yet!
SUB global_setkeys_hook
 IF keyval(scF9) > 1 THEN Custom_global_menu
 'The other keys documented in Custom_global_menu are checked in allmodex_controls
END SUB

'==========================================================================================
'                    Creating/cleaning working.tmp and creating games
'==========================================================================================


' Returns true for success
FUNCTION newRPGfile (templatefile as string, newrpg as string) as bool
 IF newrpg = "" THEN RETURN NO
 ' Error already shown if missing
 IF NOT isfile(templatefile) THEN RETURN NO
 textcolor uilook(uiSelectedDisabled), 0
 printstr "Please Wait...", pMenuX, 100, vpage
 printstr "Creating RPG File", pMenuX, 110, vpage
 setvispage vpage, NO
 writeablecopyfile templatefile, newrpg
 printstr "Unlumping", pMenuX, 120, vpage
 setvispage vpage, NO
 unlump newrpg, workingdir + SLASH

 '--create archinym information lump
 DIM fh as integer
 OPENFILE(workingdir + SLASH + "archinym.lmp", FOR_OUTPUT, fh)
 PRINT #fh, "ohrrpgce"
 PRINT #fh, short_version
 CLOSE #fh

 DIM root_node as NodePtr
 root_node = get_general_reld()

 '--Delete general.reld version info. It will then be set by upgrade()
 IF root_node = NULL THEN showerror "Couldn't load general.reld!" : RETURN NO
 DIM vernode as NodePtr
 vernode = GetChildByName(root_node, "editor_version")
 IF vernode THEN FreeNode vernode
 vernode = GetChildByName(root_node, "prev_editor_versions")
 IF vernode THEN FreeNode vernode

 '--Set creation time, wipe edit_time
 SetChildNodeDate(root_node, "edit_time", 0.)

 close_general_reld

 printstr "Finalumping", pMenuX, 130, vpage
 setvispage vpage, NO
 '--re-lump files as NEW rpg file
 RETURN write_rpg_or_rpgdir(workingdir, newrpg)
END FUNCTION

'Returns the last mtime of any file in a directory (excluding *.tmp)
FUNCTION directory_last_mtime(directory as string) as double
 DIM lasttime as double = 0
 DIM filelist() as string
 findfiles directory, ALLFILES, fileTypeFile, NO, filelist()
 FOR i as integer = 0 TO UBOUND(filelist)
  IF RIGHT(filelist(i), 4) <> ".tmp" THEN
   lasttime = large(lasttime, FILEDATETIME(directory + SLASH + filelist(i)))
  END IF
 NEXT
 RETURN lasttime
END FUNCTION

' Write workingdir/session_info.txt.tmp
' Note: we assume that whenever this is called (and sourcerpg is set) that we are
' loading or saving the game.
SUB write_session_info ()
 DIM text(11) as string
 text(0) = short_version
 text(1) = get_process_name(get_process_id())  'OS-specific meaning. Used to guard against reuse of PIDs
 text(2) = "# Custom pid:"
 text(3) = STR(get_process_id())
 text(4) = "# Editing start (load/save) time:"
 text(5) = format_date(NOW)
 text(6) = STR(NOW)
 text(7) = "# Game path:"
 'sourcerpg may be blank if we're not yet editing a game
 IF LEN(sourcerpg) THEN
  text(8) = absolute_path(sourcerpg)
  text(9) = "# Last modified time of game:"
  DIM modified as double
  IF isfile(sourcerpg) THEN
   modified = FILEDATETIME(sourcerpg)
  ELSE  'rpgdir
   modified = directory_last_mtime(sourcerpg)
  END IF
  text(10) = format_date(modified)
  text(11) = STR(modified)
 END IF
 lines_to_file text(), workingdir + SLASH + "session_info.txt.tmp", LINE_END
END SUB

' Collect data about a previous (or ongoing) editing session from a dirty working.tmp
FUNCTION get_previous_session_info (workdir as string) as SessionInfo
 DIM ret as SessionInfo
 DIM exe as string
 DIM sessionfile as string
 sessionfile = workdir + SLASH + "session_info.txt.tmp"
 ret.workingdir = workdir
 IF isfile(sessionfile) THEN
  ret.info_file_exists = YES
  DIM text() as string
  lines_from_file text(), sessionfile
  IF UBOUND(text) < 3 THEN
   'Invalid file. We've always written at least 8 lines (but many may be blank)
   debug sessionfile & " appears to contain garbage."
  ELSE
   'The metadata file's mtime should be nearly the same, but in future maybe we will want to write it
   'without saving the game.
   'ret.session_start_time = FILEDATETIME(sessionfile)
   IF UBOUND(text) >= 6 THEN
    ret.session_start_time = VAL(text(6))
   END IF
   IF UBOUND(text) >= 8 ANDALSO LEN(text(8)) > 0 THEN
    ret.sourcerpg = text(8)
    IF isfile(ret.sourcerpg) THEN
     ret.sourcerpg_current_mtime = FILEDATETIME(ret.sourcerpg)
    ELSE  'Is an .rpgdir
     ret.sourcerpg_current_mtime = directory_last_mtime(ret.sourcerpg)
    END IF
    IF UBOUND(text) >= 11 THEN ret.sourcerpg_old_mtime = VAL(text(11))
   END IF
   ret.pid = VAL(text(3))
   exe = text(1)
   ' It's possible that this copy of Custom crashed and another copy was run with the same pid,
   ' but it's incredibly unlikely
   DIM pid_current_exe as string = get_process_name(ret.pid)
   debuginfo "pid_current_exe = " & pid_current_exe
   ret.running = (LEN(exe) ANDALSO (pid_current_exe = exe ORELSE exe = "<unknown>"))
   #IFDEF __FB_ANDROID__
    'It's not possible to run two copies of the app at the same time
    ret.running = NO
   #ENDIF
  END IF
 ELSE
  'We don't know anything, except that we could work out session_start_time by looking at working.tmp mtimes.
 END IF

 ' When was a lump last modified?
 ret.last_lump_mtime = directory_last_mtime(workdir)

 ret.partial_rpg = isfile(workdir + SLASH + "__danger.tmp")
 IF ret.partial_rpg THEN
  'Check if the file is stale
  DIM daysago as double = NOW - FILEDATETIME(workdir + SLASH + "__danger.tmp")
  debuginfo "Found __danger.tmp file, " & (daysago * 24 * 60) & " minutes old"
  IF daysago * 24 * 60 < 1 THEN  'Less than 1 minute old
   ret.fresh_danger_tmp = YES
  END IF
 END IF

 debuginfo "prev_session.workingdir = " & ret.workingdir
 debuginfo "prev_session.info_file_exists = " & yesorno(ret.info_file_exists)
 debuginfo "prev_session.pid = " & ret.pid & " (exe = " & exe & ")"
 debuginfo "prev_session.running = " & yesorno(ret.running)
 debuginfo "prev_session.partial_rpg = " & yesorno(ret.partial_rpg)
 debuginfo "prev_session.fresh_danger_tmp = " & yesorno(ret.fresh_danger_tmp)
 debuginfo "prev_session.sourcerpg = " & ret.sourcerpg
 debuginfo "prev_session.sourcerpg_old_mtime = " & format_date(ret.sourcerpg_old_mtime)
 debuginfo "prev_session.sourcerpg_current_mtime = " & format_date(ret.sourcerpg_current_mtime)
 debuginfo "prev_session.session_start_time = " & format_date(ret.session_start_time)
 debuginfo "prev_session.last_lump_mtime = " & format_date(ret.last_lump_mtime)

 RETURN ret
END FUNCTION

' Try to delete everything in the given directory in a race-condition-safe order. Returns true if succeeded.
' (This is overkill now, I guess)
FUNCTION empty_workingdir (workdir as string) as bool
 touchfile workdir + SLASH + "__danger.tmp"
 DIM filelist() as string
 findfiles workdir, ALLFILES, fileTypeFile, NO, filelist()
 ' Delete these metadata files last
 a_shuffle_to_end filelist(), a_findcasei(filelist(), "__danger.tmp")
 a_shuffle_to_end filelist(), a_findcasei(filelist(), "session_info.txt.tmp")
 FOR i as integer = 0 TO UBOUND(filelist)
  DIM fname as string = workdir + SLASH + filelist(i)
  IF NOT safekill(fname) THEN
   'notification "Could not clean up " & workdir & !"\nYou may have to manually delete its contents."
   RETURN NO
  END IF
 NEXT
 killdir workdir, YES  'recursively, just in case
 RETURN YES
END FUNCTION

' Selects an unused workingdir path and creates it
SUB setup_workingdir ()
 ' This can't pick "working.tmp", so old versions of Custom won't see and clobber it.
 DIM idx as integer = 0
 DO
  workingdir = tmpdir & "working" & idx & ".tmp"
  IF NOT isdir(workingdir) THEN EXIT DO
  idx += 1
 LOOP

 debuginfo "Working in " & workingdir
 IF makedir(workingdir) <> 0 THEN
  fatalerror "Couldn't create " & workingdir & !"\nCheck c_debug.txt"
 END IF
 write_session_info
END SUB

' Check whether any other copy of Custom is already editing sourcerpg
FUNCTION check_ok_to_open (filename as string) as bool
 debuginfo "check_ok_to_open..."
 DIM olddirs() as string
 findfiles tmpdir, "working*.tmp", fileTypeDirectory, NO, olddirs()

 FOR idx as integer = 0 TO UBOUND(olddirs)
  DIM sessinfo as SessionInfo = get_previous_session_info(tmpdir & olddirs(idx))

  IF paths_equal(sessinfo.sourcerpg, filename) THEN
   IF NOT sessinfo.running THEN
    ' Apparently this crashed between when we launched, and when the .rpg was selected in the browser.
    ' Return true if we managed to delete it.
    RETURN check_a_crashed_workingdir(sessinfo)
   ELSE
    DIM msg as string
    msg = "Another copy of " CUSTOMEXE " seems to be already editing " & decode_filename(sourcerpg) & _
          !".\nYou can't open the same game twice at once! " _
          "(Make a copy first if you really want to.)"
    'IF is_windows_9x() THEN
     'sessinfo.running is not reliable on Win9x, so provide a bypass ... maybe it's not 100% reliable anyway
     IF twochoice(msg, "OK, quit", "No! I swear it's crashed! Continue") = 1 THEN
      RETURN check_a_crashed_workingdir(sessinfo)
     END IF
    'ELSE
    ' notification msg
    'END IF
   END IF
   RETURN NO
  END IF
 NEXT
 RETURN YES
END FUNCTION

SUB check_for_crashed_workingdirs ()
 'This also finds working.tmp, which belongs to old versions
 DIM olddirs() as string
 findfiles tmpdir, "working*.tmp", fileTypeDirectory, NO, olddirs()

 FOR idx as integer = 0 TO UBOUND(olddirs)
  DIM sessinfo as SessionInfo = get_previous_session_info(tmpdir & olddirs(idx))

  IF sessinfo.info_file_exists THEN
   IF sessinfo.running THEN
    ' Not crashed, so ignore
    CONTINUE FOR
   END IF
   debuginfo "Found workingtmp for crashed Custom"
  END IF

  check_a_crashed_workingdir sessinfo
 NEXT
END SUB

'Returns true if we deleted this session successfully
FUNCTION check_a_crashed_workingdir (sessinfo as SessionInfo) as bool
 IF sessinfo.info_file_exists THEN
  IF sessinfo.partial_rpg THEN
   debuginfo "...crashed while unlumping/deleting temp files, silent cleanup"
   ' In either case, safe to delete files.
   RETURN empty_workingdir(sessinfo.workingdir)
  END IF

  IF LEN(sessinfo.sourcerpg) = 0 THEN
   debuginfo "...crashed before opening a game, silent cleanup"
   RETURN empty_workingdir(sessinfo.workingdir)
  END IF
 END IF

 ' Does this look like a game, or should we just delete it?
 IF NOT sessinfo.fresh_danger_tmp THEN
  DIM filelist() as string
  findfiles sessinfo.workingdir, ALLFILES, fileTypeFileOrDir, NO, filelist()

  IF UBOUND(filelist) <= 5 THEN
   'Just some stray files that refused to delete last time,
   'or possibly an old copy of Custom running but no game opened yet no way to handle that
   debuginfo (UBOUND(filelist) + 1) & " files in working.tmp, silent cleanup"
   RETURN empty_workingdir(sessinfo.workingdir)
  END IF
 END IF

 'Auto-handling failed, ask user what to do
 RETURN handle_dirty_workingdir(sessinfo)
END FUNCTION

' When recovering an rpg from working.tmp, pick an unused destination filename.
FUNCTION pick_recovered_rpg_filename(old_sourcerpg as string) as string
 DIM destdir as string
 DIM destfile_basename as string
 IF LEN(old_sourcerpg) THEN
  ' Put next to original file
  destdir = add_trailing_slash(trimfilename(old_sourcerpg))
  IF NOT diriswriteable(destdir) THEN destdir = ""
  destfile_basename = trimpath(trimextension(old_sourcerpg)) & " crash-recovered "
 ELSE
  destfile_basename = "crash-recovered"
 END IF
 IF NOT diriswriteable(destdir) THEN destdir = documents_dir & SLASH

 DIM index as integer = 0
 DO
  DIM destfile as string = destdir & destfile_basename & index & ".rpg"
  IF NOT isfile(destfile) THEN RETURN destfile
  index += 1
 LOOP
END FUNCTION

'Returns true if we can continue, false to cleanup_and_terminate
FUNCTION recover_workingdir (sessinfo as SessionInfo) as bool
 DIM origname as string = trimpath(sessinfo.sourcerpg)  'Might be ""
 'Trim "crash-recovered"
 DIM where as integer
 where = INSTR(origname, " crash-recovered ")
 IF where THEN
  origname = LEFT(origname, where - 1) & ".rpg"
 END IF

 DIM destfile as string
 destfile = pick_recovered_rpg_filename(origname)

 printstr "Saving as " + decode_filename(destfile), pMenuX, 180, vpage
 printstr "LUMPING DATA: please wait...", pMenuX, 190, vpage
 setvispage vpage, NO
 '--re-lump recovered files as RPG filep
 IF write_rpg_or_rpgdir(sessinfo.workingdir, destfile) = NO THEN
  RETURN NO
 END IF
 clearpage vpage

 DIM msg as string
 IF LEN(origname) = 0 THEN origname = "gamename.rpg"
 msg = !"The recovered game has been saved as\n" & _
       fgtag(uilook(uiSelectedItem), decode_filename(destfile)) & !"\n" _
       "You can rename it to " & origname & ", but ALWAYS keep the previous copy " _
       !"as a backup because some data in the recovered file might be corrupt!\n" _
       "If you have questions, ask ohrrpgce-crash@HamsterRepublic.com"
 notification msg
 RETURN empty_workingdir(sessinfo.workingdir)
END FUNCTION

'Called when a partial or complete copy of a game exists
'Returns true if cleaned away, false if not cleaned up
FUNCTION handle_dirty_workingdir (sessinfo as SessionInfo) as bool
 clearpage vpage

 IF isfile(sessinfo.workingdir + SLASH + "__danger.tmp") THEN
  ' Don't provide option to recover, as this looks like garbage.
  ' If we've reached this point, then already checked whether it's a modern Custom
  ' ...but once, I saw a dirty working.tmp with __danger.tmp but no other
  ' files. Usually, __danger.tmp wouldn't appear without the session info file.
  ' However, maybe another copy of custom is busy unlumping a big game, so ask before deleting.
  DIM choice as integer
  choice = twochoice("Found a partial temporary copy of a game. " _
                     "It looks like a copy of " + CUSTOMEXE + " is or was in the process of " _
                     "either unlumping a game or deleting its temporary files. " _
                     "It might have crashed, or still be running. What do you want to do?", _
                     "Ignore", _
                     "Erase temporary files", _
                     0, 0)
  IF choice = 0 THEN
   RETURN NO
  ELSE
   RETURN empty_workingdir(sessinfo.workingdir)
  END IF
 END IF

 DIM msg as string
 DIM helpfile as string
 IF sessinfo.info_file_exists THEN
  ' We already checked Custom isn't still running

  msg = CUSTOMEXE " crashed while editing a game, but the temp unsaved modified copy of the game still exists." LINE_END
  msg &= "Original file:" LINE_END
  msg &= decode_filename(sessinfo.sourcerpg) & LINE_END

  CONST onesec as double = 1 / (24*3600)

  IF sessinfo.sourcerpg_current_mtime < sessinfo.session_start_time - onesec THEN
   ' It's a bit confusing to tell the user 4 last-mod times, so skip this one.
   msg &= "Last modified " & format_date(sessinfo.sourcerpg_old_mtime) & LINE_END
  END IF

  ' The }'s get replaced with either | or a space.
  msg &=  "}|" LINE_END _
          "}+>Loaded or last saved by Custom " LINE_END _
          "}  at:        " & format_date(sessinfo.session_start_time) & LINE_END _
          "}  Last edit: " & format_date(sessinfo.last_lump_mtime)

  IF sessinfo.sourcerpg_current_mtime > sessinfo.session_start_time + onesec THEN
   msg &= LINE_END "|" LINE_END _
          "+-> WARNING: " & decode_filename(trimpath(sessinfo.sourcerpg)) & " modified since it was loaded or saved!" _
          " Modified " & format_date(sessinfo.sourcerpg_current_mtime) ' & LINE_END

   replacestr(msg, LINE_END "}", LINE_END "|")
   helpfile = "recover_unlumped_rpg_outdated"
  ELSE
   replacestr(msg, LINE_END "}", LINE_END " ")
   helpfile = "recover_unlumped_rpg"
  END IF

 ELSE
  msg = !"An unknown game was found unlumped.\n" _
        "It appears that an old version of " + CUSTOMEXE + " is either already running, " _
        "or it has crashed."
 END IF

 DIM cleanup_menu(2) as string
 cleanup_menu(0) = "DO NOTHING (ask again later)"
 cleanup_menu(1) = "RECOVER temp files as a .rpg"
 cleanup_menu(2) = "ERASE temp files"
 DIM choice as integer
 choice = multichoice(msg, cleanup_menu(), 0, 0, helpfile)

 IF choice = 0 THEN RETURN NO
 IF choice = 1 THEN RETURN recover_workingdir(sessinfo)
 IF choice = 2 THEN RETURN empty_workingdir(sessinfo.workingdir)  'erase
END FUNCTION


'==========================================================================================
'                               Secret/testing/debug menus
'==========================================================================================

SUB backend_keyrepeat_bugtest
 notification !"Holding down the key that triggered this box should not advance it\n" _
              "waitforanykey:\n(Press any key, and hold it down)"
 basic_textbox !"Nor this box\nwaitforkeyrelease:\n(Release key to advance)", uilook(uiText), vpage
 setvispage vpage
 waitforkeyrelease
END SUB

SUB secret_menu ()
 DIM menu(...) as string = { _
     "Editor Slice Editor", _
     "Reload Editor", _
     "Editor Editor", _
     "Conditions and More Tests", _
     "Transformed Quads", _
     "plankmenu cursor move tests", _
     "Text tests", _
     "Font tests", _
     "Stat Growth Chart", _
     "Edit Status Screen (Unused)", _
     "Edit Item Screen (Unused)", _
     "Edit Spell Screen (Unused)", _
     "Edit Virtual Keyboard Screen (Unused)", _
     "(Unused)", _
     "(Unused)", _
     "(Unused)", _
     "(Unused)", _
     "Spriteset browser", _
     "New backdrop browser", _
     "RGFX tests", _
     "Backend Keyrepeat Bugtest", _
     "HTTP test", _
     "CreateProcess tests (Windows only)", _
     "Edit Translations", _
     "Rotozoom tests/benchmarks", _
     "Mouse and Window tests", _
     "Test Game under Valgrind", _
     "Test Game under GDB" _
 }
 DIM st as MenuState
 st.autosize = YES
 st.last = UBOUND(menu)
 #IFDEF NO_TEST_GAME
  st.last -= 2  'Remove the "Test Game" options
 #ENDIF

 DO
  setwait 55
  setkeys
  IF keyval(ccCancel) > 1 THEN EXIT DO
  IF enter_space_click(st) THEN
   IF st.pt = 0 THEN slice_editor SL_COLLECT_EDITOR, "<blank>", YES 'This is the best one currently for editing sourceslices/*.slice
   IF st.pt = 1 THEN reload_editor
   IF st.pt = 2 THEN editor_editor
   IF st.pt = 3 THEN condition_test_menu
   IF st.pt = 4 THEN quad_transforms_menu
   IF st.pt = 5 THEN plankmenu_cursor_move_tests
   IF st.pt = 6 THEN text_test_menu
   IF st.pt = 7 THEN font_test_menu
   IF st.pt = 8 THEN stat_growth_chart
   'These four edit a slicetree_n_n.reld lump which is currently not loaded in-game, and probably will never be loaded.
   'We will change the lump name when we feel ready to expose these special screen collections to game authors
   IF st.pt = 9 THEN slice_editor SL_COLLECT_STATUSSCREEN, , YES
   IF st.pt = 10 THEN slice_editor SL_COLLECT_ITEMSCREEN, , YES
   IF st.pt = 11 THEN slice_editor SL_COLLECT_SPELLSCREEN, , YES
   IF st.pt = 12 THEN slice_editor SL_COLLECT_VIRTUALKEYBOARDSCREEN, , YES
   '13-16 unused
   IF st.pt = 17 THEN
    'Nothing special here except that you can select Backdrop and Enemy
    DIM options(...) as string = {"Hero", "Small Enemy", "Medium Enemy", "Large Enemy", "Walkabouts", "Weapons", "Attack", "Boxborder", "Portrait", "Backdrop", "Enemy"}
    DIM sprtype as SpriteType = multichoice("Edit what?", options())
    IF sprtype > -1 THEN spriteset_editor sprtype
   END IF
   IF st.pt = 18 THEN backdrop_browser
   IF st.pt = 19 THEN new_graphics_tests
   IF st.pt = 20 THEN backend_keyrepeat_bugtest
   IF st.pt = 21 THEN HTTP_demo
   IF st.pt = 22 THEN CreateProcess_tests
   IF st.pt = 23 THEN translations_menu
   IF st.pt = 24 THEN rotozoom_tests
   IF st.pt = 25 THEN mouse_and_window_tests
   #IFNDEF NO_TEST_GAME
    IF st.pt = 26 THEN spawn_game_menu NO, YES 'With valgrind
    IF st.pt = 27 THEN spawn_game_menu YES     'With gdb
   #ENDIF
  END IF
  usemenu st
  clearpage vpage
  standardmenu menu(), st, , , vpage
  setvispage vpage
  dowait
 LOOP
 setkeys
END SUB

FUNCTION window_size_description(scale as integer) as string
 IF scale >= 11 THEN
  ' Not implemented yet.
  RETURN "maximize"
 ELSE
  RETURN "~" & (10 * scale) & "% screen width"
 END IF
END FUNCTION

SUB resolution_menu ()
 DIM menu(9) as string
 DIM st as MenuState
 st.size = 24
 st.last = UBOUND(menu)
 DIM selectable(UBOUND(menu)) as bool
 flusharray selectable(), , YES

 DIM gen_root as NodePtr = get_general_reld()
 DIM console as NodePtr = GetOrCreateChild(gen_root, "console_options")

 'FIXME: selecting a resolution other than 320x200 causes the distrib menu
 'to not package gfx_directx.dll; remove that when gfx_directx is updated

 DO
  setwait 55
  setkeys
  DIM quit as bool = (keyval(ccCancel) > 1 OR (enter_space_click(st) AND st.pt = 0))
  IF usemenu(st, selectable()) ORELSE quit THEN
   ' Reinforce limits, because we temporarily allow 0 while typing for convenience
   gen(genResolutionX) = large(MinResolutionX, gen(genResolutionX))
   gen(genResolutionY) = large(MinResolutionY, gen(genResolutionY))
  END IF
  IF quit THEN EXIT DO
  IF keyval(scF1) > 1 THEN
    show_help "window_settings"
  END IF
  SELECT CASE st.pt
   CASE 1: st.need_update OR= intgrabber(gen(genFullscreen), 0, 1)
   CASE 2: st.need_update OR= intgrabber(gen(genWindowSize), 1, 10)
   CASE 3: st.need_update OR= intgrabber(gen(genLivePreviewWindowSize), 1, 10)
   CASE 4: st.need_update OR= intgrabber(gen(genRungameFullscreenIndependent), 0, 1)
   CASE 5
    DIM margins as integer = GetChildNodeInt(console, "safe_margin", 0)
    IF (margins = 0 ANDALSO keyval(scBackspace) > 1) ORELSE keyval(scDelete) > 1 THEN
     FreeChildNode console, "safe_margin"
     st.need_update = YES
    ELSE
     IF intgrabber(margins, 0, 10) THEN
      SetChildNode console, "safe_margin", margins
      st.need_update = YES
     END IF
    END IF
   CASE 8: st.need_update OR= intgrabber(gen(genResolutionX), 0, MaxResolutionX)
   CASE 9: st.need_update OR= intgrabber(gen(genResolutionY), 0, MaxResolutionY)
  END SELECT
  IF st.need_update THEN
   xbsave game + ".gen", gen(), 1000   'Instant live previewing update
   st.need_update = NO
  END IF
  menu(0) = "Previous Menu"
  menu(1) = "Default to fullscreen: " & yesorno(gen(genFullscreen))
  menu(2) = "Default window size: " & window_size_description(gen(genWindowSize))
  menu(3) = "Test-Game window size: " & window_size_description(gen(genLivePreviewWindowSize))
  menu(4) = "rungame fullscreen state: "
  IF gen(genRungameFullscreenIndependent) THEN
   menu(4) &= "independent"
  ELSE
   menu(4) &= "shared with this game"
  END IF
  menu(5) = "Console TV safe margin %: " & GetChildNodeStr(console, "safe_margin", "Default")  'This is an integer
  selectable(6) = NO
  selectable(7) = NO
  menu(7) = fgtag(uilook(eduiHeading), " Experimental options")
  menu(8) = "Game horizontal resolution: " & gen(genResolutionX) & " pixels"
  menu(9) = "Game vertical resolution: " & gen(genResolutionY) & " pixels"
  clearpage vpage
  standardmenu menu(), st, , , vpage
  setvispage vpage
  dowait
 LOOP
 xbsave game + ".gen", gen(), 1000
 write_general_reld()
END SUB

'This menu is for testing experimental Condition UI stuff
SUB condition_test_menu ()
 DIM as Condition cond1, cond2, cond3, cond4
 DIM as AttackElementCondition atkcond
 DIM float as double
 DIM float_repr as string = "0%"
 DIM atkcond_repr as string = ": Never"
 DIM menu(8) as string
 DIM st as MenuState
 st.last = UBOUND(menu)
 st.size = 22
 DIM tmp as integer

 DO
  setwait 55
  setkeys YES
  IF keyval(ccCancel) > 1 THEN EXIT DO
  IF keyval(scF1) > 1 THEN show_help "condition_test"
  tmp = 0
  IF st.pt = 0 THEN
   IF enter_space_click(st) THEN EXIT DO
  ELSEIF st.pt = 2 THEN
   tmp = cond_grabber(cond1, YES , NO, st)
  ELSEIF st.pt = 3 THEN
   tmp = cond_grabber(cond2, NO, NO, st)
  ELSEIF st.pt = 5 THEN
   tmp = cond_grabber(cond3, YES, YES, st)
  ELSEIF st.pt = 6 THEN
   tmp = cond_grabber(cond4, NO, YES, st)
  ELSEIF st.pt = 7 THEN
   tmp = percent_cond_grabber(atkcond, atkcond_repr, ": Never", -9.99, 9.99, 5)
  ELSEIF st.pt = 8 THEN
   tmp = percent_grabber(float, float_repr, -9.99, 9.99, 5)
  END IF
  usemenu st

  clearpage vpage
  menu(0) = "Previous menu"
  menu(1) = "Enter goes to tag browser for tag conds:"
  menu(2) = " If " & condition_string(cond1, (st.pt = 2), "Always", 45)
  menu(3) = " If " & condition_string(cond2, (st.pt = 3), "Never", 45)
  menu(4) = "Enter always goes to cond editor:"
  menu(5) = " If " & condition_string(cond3, (st.pt = 5), "Always", 45)
  menu(6) = " If " & condition_string(cond4, (st.pt = 6), "Never", 45)
  menu(7) = "Fail vs damage from <fire>" & atkcond_repr
  menu(8) = "percent_grabber : " & float_repr
  standardmenu menu(), st, , , vpage
  printstr STR(tmp), 0, 190, vpage
  setvispage vpage
  dowait
 LOOP
 setkeys
END SUB

TYPE RGBAEditor EXTENDS Editorkit
 DECLARE SUB define_items()
 DECLARE SUB edit_byte(title as string, byref byt as ubyte)
 col as RGBcolor ptr
END TYPE

SUB RGBAEditor.edit_byte(title as string, byref byt as ubyte)
 defitem title
 DIM temp as integer = byt
 edit_int temp, 0, 255
 byt = temp
 finish_defitem  'Because temp falls out of scope, get rid of pointer to it
END SUB

SUB RGBAEditor.define_items()
 edit_byte "R:", col->r
 edit_byte "G:", col->g
 edit_byte "B:", col->b
 edit_byte "Alpha:", col->a
END SUB


SUB quad_transforms_menu ()
 DIM info as string = _
     !"F3: switch 8/32 bit, F4: cycle dither algo\n" _
     !"D: cycle draw type\n" _
     !"Arrows: scale X and Y\n" _
     !"<, >: change angle\n" _
     !"-, +: change spritetype\n" _
     !"[, ]: change spriteset\n" _
     !"O/P: adjust opacity\n" _
     !"T: toggle blending\n" _
     !"B: cycle blend mode\n" _
     !"1-4: set vertex colors\n" _
     !"5: set shared colormod\n" _

 DIM drawnames(2) as zstring ptr = {@"Texture", @"TextureColor", @"Color"}
 DIM blendnames(blendModeLAST) as zstring ptr = {@"Normal", @"Add", @"Multply"}

 blend_algo = gen(gen8bitBlendAlgo)
 DIM need_update as bool = YES
 DIM spritemode as SpriteType = -1 'sprTypeHero '-1 to show master palette
 DIM spriteid as integer
 DIM sprpair as GraphicPair

 DIM drawopts as DrawOptions
 drawopts.with_blending = YES
 drawopts.opacity = 0.5
 DIM angle as single = 45
 DIM scale as Float2 = (7, 7)
 DIM position as Float2 = (vpages(vpage)->w / 2, vpages(vpage)->h / 2)

 switch_to_32bit_vpages()

 DIM as SmoothedTimer normdrawtime, qdrawtime, mathtime

 DIM drawmode as integer
 DIM cols(3) as RGBcolor = {(-1), (-1), (-1), (-1)}
 DIM rgba_edit as RGBAEditor


 DO
  setwait 55

  if need_update then
   unload_sprite_and_pal sprpair

   select case spritemode
    case -1
     sprpair.sprite = frame_new(16, 16)
     FOR i as integer = 0 TO 255
      putpixel sprpair.sprite, (i MOD 16), (i \ 16), i
     NEXT
    case else
     load_sprite_and_pal sprpair, spritemode, spriteid
   end select

   need_update = NO
  end if

  setkeys
  IF keyval(ccCancel) > 1 THEN EXIT DO
  IF keyval(scF3) > 1 THEN toggle_32bit_vpages
  IF keyval(scF4) > 1 THEN loopvar blend_algo, 0, blendAlgoLAST
  IF keyval(ccLeft)  THEN scale.x -= 0.1
  IF keyval(ccRight) THEN scale.x += 0.1
  IF keyval(ccUp)    THEN scale.y -= 0.1
  IF keyval(ccDown)  THEN scale.y += 0.1
  IF keyval(scLeftCaret)  THEN angle -= 5
  IF keyval(scRightCaret) THEN angle += 5
  IF keyval(scD) > 1 THEN loopvar drawmode, 0, 2
  IF keyval(scMinus) > 1 THEN loopvar spritemode, -1, sprTypeBackdrop, -1: need_update = YES
  IF keyval(scPlus) > 1 THEN loopvar spritemode, -1, sprTypeBackdrop, 1: need_update = YES
  IF keyval(scLeftBracket) > 1 THEN loopvar spriteid, 0, sprite_sizes(spritemode).lastrec, -1: need_update = YES
  IF keyval(scRightBracket) > 1 THEN loopvar spriteid, 0, sprite_sizes(spritemode).lastrec, 1: need_update = YES
  'IF keyval(scN) > 1 THEN loopvar drawopts.scale, 0, 10, -1
  'IF keyval(scM) > 1 THEN loopvar drawopts.scale, 0, 10, 1
  IF keyval(scO) > 1 THEN drawopts.opacity -= 0.05
  IF keyval(scP) > 1 THEN drawopts.opacity += 0.05
  IF keyval(scB) > 1 THEN loopvar drawopts.blend_mode, 0, blendModeLAST
  IF keyval(scT) > 1 THEN drawopts.with_blending xor= true
  'IF keyval(scA) > 1 THEN drawopts.alpha_channel xor= true  'Does nothing without a Frame with alpha channel
  IF keyval(sc1) > 1 THEN rgba_edit.col = @cols(0) : rgba_edit.run()
  IF keyval(sc2) > 1 THEN rgba_edit.col = @cols(1) : rgba_edit.run()
  IF keyval(sc3) > 1 THEN rgba_edit.col = @cols(2) : rgba_edit.run()
  IF keyval(sc4) > 1 THEN rgba_edit.col = @cols(3) : rgba_edit.run()
  IF keyval(sc5) > 1 THEN rgba_edit.col = @drawopts.argbModifier : rgba_edit.run()

  draw_background vpages(vpage)
  textcolor uilook(uiText), 0
  wrapprint info, pMenuX, pMenuY, , vpage

  normdrawtime.start()
  frame_draw sprpair.sprite, sprpair.pal, pCentered + 30, 50, , vpages(vpage), drawopts
  normdrawtime.stop()

  mathtime.start()
  dim transf as AffineTransform
  rotozoom_transform transf, sprpair.sprite->size, , position, angle, scale
  mathtime.stop()

  qdrawtime.start()
  select case drawmode
   case 0:
    frame_draw_transformed sprpair.sprite, master(), sprpair.pal, transf, YES, vpages(vpage), drawopts
   case 1:
    frame_draw_transformed sprpair.sprite, master(), sprpair.pal, transf, YES, vpages(vpage), drawopts, @cols(0)
   case 2:
    rectangle_transformed cols(), transf, vpages(vpage), drawopts
  end select
  qdrawtime.stop()

  wrapprint strprintf(!"%s Sprite: type %d id %d; scale %.1f,%.1f angle %.0f opacity %d%% \n" _
                      !"Normal draw: %dus, quad draw: %dus, math in %.1fus\n" _
                      !"%d-bit display ditheralgo %d blending %d %s\n", _
                      drawnames(drawmode), spritemode, spriteid, scale.x, scale.y, angle, cint(drawopts.opacity * 100), _
                      cint(normdrawtime.smoothtime * 1e6), cint(qdrawtime.smoothtime * 1e6), mathtime.smoothtime * 1e6, _
                      iif(vpages_are_32bit, 32, 8), blend_algo, drawopts.with_blending, blendnames(drawopts.blend_mode)), _
            0, pBottom, , vpage

  setvispage vpage
  dowait
 LOOP
 setkeys
 unload_sprite_and_pal sprpair
 switch_to_8bit_vpages()
END SUB


'smooth is 0, 1 or 2
FUNCTION rotozoom_test_with (img as GraphicPair, rotate as double, zoomx as double, zoomy as double, smooth as integer, raster as bool = NO, trans as bool = NO, reftime as double = 0.) as double
 clearpage vpage

 'These aren't used by raster
 DIM as Surface ptr in_surf, out_surf
 IF smooth > 0 THEN
  in_surf = frame_to_surface32(img.sprite, master())
 ELSE
  IF gfx_surfaceCreateFrameView(img.sprite, @in_surf) THEN RETURN 0
 END IF

 ' First warm up the CPU, because CPU frequency toggling is the norm
 ' these days and it makes timing meaningless unless at a reliable frequency
 DIM rztime as double = TIMER, copytime as double
 WHILE TIMER - rztime < 150e-3
  out_surf = rotozoomSurface(in_surf, rotate, 1., 1., NO)
  gfx_surfaceDestroy(@out_surf)
 WEND

 ' Repeat 10 times and take the min time
 DIM rzmin as double = 1e99
 FOR repeat as integer = 1 TO 10
  ' Repeat several times until at least 3ms passed
  rztime = TIMER
  DIM cnt as integer = 0
  WHILE TIMER - rztime < 3e-3
   IF raster THEN
    DIM position as Float2 = (vpages(vpage)->w / 2, vpages(vpage)->h / 2 - 50)
    DIM transf as AffineTransform
    rotozoom_transform transf, img.sprite->size, , position, rotate, XYF(zoomx, zoomy)
    frame_draw_transformed img.sprite, master(), img.pal, transf, trans, vpages(vpage)

   ELSE
    gfx_surfaceDestroy(@out_surf)
    IF smooth = 2 THEN
     out_surf = surface_scale(in_surf, large(1, img.sprite->w * zoomx), large(1, img.sprite->h * zoomy))
    ELSE
     out_surf = rotozoomSurface(in_surf, rotate, zoomx, zoomy, smooth)  'smooth 0/1
    END IF
    BUG_IF(out_surf = NULL, "rotozoom returned NULL", 0)

   END IF
   cnt += 1
  WEND
  rzmin = small(rzmin, (TIMER - rztime) / cnt)
 NEXT

 DIM msg as string

 IF raster = NO THEN
  copytime = TIMER
  DIM spr as Frame ptr = frame_with_surface(out_surf)
  frame_draw spr, img.pal, pCentered, pCentered - 50, trans, vpage
  frame_unload @spr
  copytime = TIMER - copytime

  msg = strprintf(" (size %d*%d)", out_surf->width, out_surf->height)
 ELSE
  copytime = 0
  msg = " (RASTER)"
 END IF

 DIM totaltime as double = rzmin + copytime
 msg = strprintf("zoom %.2fx%.2f rotate %.1f smooth %d trans=%s %d-bit: %.1fus + copy %.1fus = %.1fus", _
                 zoomx, zoomy, rotate, smooth, iif(trans, @"yes", @"no"), iif(img.sprite->surf, 32, 8), _
                 (rzmin * 1e6), (copytime * 1e6), totaltime * 1e6) _
       & msg
 IF reftime > 0 THEN msg &= strprintf(", x%.3f ref time", totaltime / reftime)

 setvispage vpage
 visible_debug msg

 gfx_surfaceDestroy(@in_surf)
 gfx_surfaceDestroy(@out_surf)
 RETURN totaltime
END FUNCTION

SUB rotozoom_tests ()
 switch_to_32bit_vpages

 DIM reftime as double
 DIM img as GraphicPair
 load_sprite_and_pal img, sprTypeBackdrop, 1
 reftime = rotozoom_test_with(img, 45, 1.2, 1.2, 0)
 rotozoom_test_with(img, 45, 1.2, 1.2, 0, YES, , reftime)
 rotozoom_test_with(img, 45, 1.2, 1.2, 1, , , reftime)
 rotozoom_test_with(img, 0, 1.2, 1.2, 2, , , reftime)
 reftime = rotozoom_test_with(img, 0, 1.2, 1.2, 0, YES)
 frame_assign @img.sprite, frame_duplicate(img.sprite)
 frame_convert_to_32bit(img.sprite, master(), img.pal)
 palette16_unload @img.pal
 rotozoom_test_with(img, 0, 1.2, 1.2, 0, YES, , reftime)

 unload_sprite_and_pal img

 load_sprite_and_pal img, sprTypeLargeEnemy, 1
 FOR zoom as double = 0.5 TO 6.501 STEP 2.
  reftime = rotozoom_test_with(img, 136, zoom, zoom, 0)
  rotozoom_test_with(img, 136, zoom, zoom, 0, YES, , reftime)
  reftime = rotozoom_test_with(img, 136, zoom, zoom, 0, , YES)
  rotozoom_test_with(img, 136, zoom, zoom, 0, YES, YES, reftime)
 NEXT
 unload_sprite_and_pal img

 switch_to_8bit_vpages
END SUB


SUB text_test_menu
 DIM text as string = load_help_file("texttest")
 DIM mouse as MouseInfo
 hidemousecursor
 DO
  setwait 55
  setkeys
  mouse = readmouse
  IF keyval(ccCancel) > 1 THEN EXIT DO
  IF keyval(scF1) > 1 THEN
   show_help "texttest"
   text = load_help_file("texttest")
  END IF
  IF keyval(scF2) > 1 THEN
   pop_warning !"Extreemmmely lonngggg Extreemmmely lonngggg Extreemmmely lonngggg Extreemmmely lonngggg Extreemmmely lonngggg Extreemmmely lonngggg Extreemmmely lonngggg \n\ntext\nbox\n\nnargh\nnargh\nnargh\nndargh\nnargh\nnagrgh\nnargh\n\nmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
  END IF
  IF keyval(scF3) > 1 THEN
   text = load_help_file("texttest_stress_test")
  END IF

  DIM textpos as XYPair = XY(20, 20)

  DIM curspos as StringCharPos
  DIM pos2 as StringSize
  find_point_in_text @curspos, mouse.pos, text, 280, textpos, 0, YES, YES

  clearpage vpage
  edgeboxstyle 10, 10, 300, 185, 0, vpage
  wrapprint text, textpos.x, textpos.y, , vpage, 280, , fontPlain

  text_layout_dimensions @pos2, text, curspos.charnum, , 280, fonts(0), YES, YES

  rectangle(vpages(vpage), XY_WH(curspos.pos, curspos.size), uilook(uiHighlight))
  printstr CHR(3), mouse.x - 3, mouse.y - 3, vpage
  DIM cursor_show as string
  cursor_show = lpad(STR(curspos.charnum), , 4) & " (lineend=" & pos2.lineend & ") "
  DIM tpos as integer = curspos.charnum + 1  'curspos.charnum is 0-based!
  cursor_show &= RIGHT(LEFT(text, tpos - 1), 15)
  cursor_show &= "[" & MID(text, tpos, 1) & "]"
  cursor_show &=       MID(text, tpos + 1, 10)
  rectangle 0, pBottom, rWidth, 10, uilook(uiBackground), vpage
  edgeprint cursor_show, 0, pBottom, uilook(uiText), vpage
  setvispage vpage
  dowait
 LOOP
 setkeys
 showmousecursor
END SUB

SUB new_graphics_tests
 DIM ofile as string = tmpdir + SLASH + "backdrops.rgfx"
 convert_mxs_to_rgfx(game + ".mxs", ofile, sprTypeBackdrop)

 notification " .mxs size " & filelen(game + ".mxs") & "B, .rgfx size " & filelen(ofile) & "B"

 'Lets see how long the document takes to load
 DIM doc as DocPtr
 DIM starttime as double = timer
 doc = LoadDocument(ofile, optNoDelay)
 notification "Backdrop .rgfx completely loaded in " & CINT((timer - starttime) * 1000) & "ms"
 FreeDocument doc
 doc = NULL

 DIM fr as Frame ptr
 DIM rgfx_time as double
 FOR i as integer = 0 TO gen(genNumBackdrops) - 1
  starttime = timer
  IF doc = NULL THEN doc = rgfx_open(ofile)
  fr = rgfx_load_spriteset(doc, sprTypeBackdrop, i)
  rgfx_time += timer - starttime
  frame_draw fr, , 0, 0, NO, vpage
  setvispage vpage
  ' waitforanykey
  frame_unload @fr
 NEXT
 starttime = timer
 'Load backdrops without caching
 FOR i as integer = 0 TO gen(genNumBackdrops) - 1
  fr = frame_load_mxs(game + ".mxs", i)
  frame_unload @fr
 NEXT
 notification gen(genNumBackdrops) & " backdrops loaded from .rgfx in " & CINT(rgfx_time * 1000) & "ms; " _
     "loaded from mxs in " & CINT((timer - starttime) * 1000) & "ms"
END SUB

SUB plankmenu_tests_generate_grid(root as Slice ptr, scatter as integer, percent as integer)
 DeleteSliceChildren root
 FOR y as integer = 0 TO 4
  FOR x as integer = 0 TO 6
   IF randint(100) > percent THEN CONTINUE FOR
   DIM sl as Slice ptr = NewSliceOfType(slRectangle, root)
   sl->X = x * 40 + randint(scatter)
   sl->Y = y * 40 + randint(scatter)
   DIM sizerange as integer = small(scatter * 2, 36)
   sl->Width = 20 + randint(sizerange) - sizerange \ 2
   sl->Height = 20 + randint(sizerange) - sizerange \ 2
   'default set_plank_state makes the SELECTABLE rectanges invisible, so put it on top of another one!
   ChangeRectangleSlice sl, , uiDisabledItem * -1 - 1
   sl->Lookup = SL_PLANK_HOLDER
   DIM highlight as Slice ptr = NewSliceOfType(slRectangle, sl)
   ChangeRectangleSlice highlight, , , , borderNone
   highlight->Fill = YES
   highlight->Visible = NO
   highlight->Lookup = SL_PLANK_MENU_SELECTABLE
  NEXT
 NEXT
END SUB

SUB draw_effective_points(ps as PlankState, axis as integer, d as integer)
 REDIM planks(any) as Slice Ptr
 find_all_planks ps, ps.m, planks()

 DIM as PlankViewpoint viewpoint = PlankViewpoint(ps.cur, axis, d)

 FOR i as integer = 0 TO UBOUND(planks)
  DIM pnt as FwdSide
  IF viewpoint.plank_effective_pos(pnt, planks(i)) = NO THEN CONTINUE FOR
  DIM effpos as XYPair
  DIM byref origin as FwdSide = viewpoint.prev_center
  IF axis = 0 THEN
   effpos = XY((origin.fwd + pnt.fwd) * d, origin.side + pnt.side)
  ELSE
   effpos = XY(origin.side + pnt.side, (origin.fwd + pnt.fwd) * d)
  END IF
  rectangle effpos.x - 1, effpos.y - 1, 3, 3, findrgb(255,255,0), vpage
 NEXT i
END SUB

'Draw lines showing positions for slice centers which plank_menu_move_cursor
'would give equal preference
SUB draw_isoline(sl as Slice ptr, ps as PlankState, movex as integer, movey as integer)
 DIM sl_center as XYPair = sl->ScreenPos + sl->Size \ 2
 DIM edgelen as integer
 IF movex THEN edgelen = sl->Height ELSE edgelen = sl->Width
 DIM col as integer = findrgb(255,255,255)

 'DIM parabola_scale as double = 2. / large(8, edgelen) ^ 1.5

 FOR dist as integer = 15 TO 150 STEP 25
  DIM radius as double = dist  '(dist / 10)^0.5

  ' DIM as double semimajor = 1'large(8, ABS(sl->Width * movex) + ABS(sl->Height * movey))
  ' DIM as double semiminor = 1'large(8, ABS(sl->Width * movey) + ABS(sl->Height * movex))

  DIM as integer prev_side_width = large(16, ABS(sl->Width * movey) + ABS(sl->Height * movex))

  DIM as double directedness = 2. ^ 2

  DIM as double semimajor = 0.3333 ''large(16, ABS(sl->Width * movex) + ABS(sl->Height * movey))
  DIM as double semiminor = 1'large(16, ABS(sl->Width * movey) + ABS(sl->Height * movex))
  semimajor *= radius
  semiminor *= radius

  DIM as double angle = ATAN2(movex, movey)

  ' The center of the ellipse is 'radius' pixels in the move direction
  DIM as XYPair el_center = XY(movex * radius, movey * radius)

  el_center += sl_center

  ellipse vpages(vpage), el_center.x, el_center.y, semimajor, col, , semiminor, angle

  /'
  FOR side as integer = -100 TO 100
   DIM pnt as XYPair
   DIM as integer fwd

   fwd = dist - parabola_scale * large(0,(ABS(side) - 0)) ^ 2.5


   IF fwd <= 0 THEN CONTINUE FOR
   IF movex THEN
    pnt.x = fwd * movex
    pnt.y = side
   ELSE
    pnt.x = side
    pnt.y = fwd * movey
   END IF

   pnt += sl_center
   putpixel pnt.x, pnt.y, col, vpage
  NEXT
  '/
 NEXT

 DIM as integer axis, d
 IF movex < 0 THEN axis = 0 : d = -1
 IF movex > 0 THEN axis = 0 : d = 1
 IF movey < 0 THEN axis = 1 : d = -1
 IF movey > 0 THEN axis = 1 : d = 1
 draw_effective_points ps, axis, d
END SUB

SUB plankmenu_cursor_move_tests
 DIM as integer scatter = 14, percent = 70

 DIM root as Slice ptr = NewSliceOfType(slContainer)
 root->Fill = YES
 DIM ps as PlankState
 ps.m = root
 DIM update as bool = YES

 DIM as integer movex, movey

 setkeys
 DO
  setwait 55
  setkeys

  IF keyval(ccCancel) > 1 THEN EXIT DO
  IF keyval(scF6) > 1 THEN slice_editor root, SL_COLLECT_EDITOR
  IF keyval(scPlus) > 1 THEN scatter += 1 : update = YES
  IF keyval(scMinus) > 1 THEN scatter -= 1 : update = YES
  IF keyval(scLeftCaret) > 1 THEN percent -= 1 : update = YES
  IF keyval(scRightCaret) > 1 THEN percent += 1 : update = YES
  IF update THEN
   plankmenu_tests_generate_grid root, scatter, percent
   ps.cur = 0
   update = NO
  END IF

  IF ps.cur THEN set_plank_state ps, ps.cur, plankNORMAL

  IF keyval(scShift) > 0 THEN
   ' movex = 0
   ' movey = 0
   IF keyval(ccLeft) > 0 THEN movex = -1 : movey = 0
   IF keyval(ccRight) > 0 THEN movex = 1 : movey = 0
   IF keyval(ccUp) > 0 THEN movey = -1 : movex = 0
   IF keyval(ccDown) > 0 THEN movey = 1 : movex = 0
  ELSE
   plank_menu_arrows(ps)
  END IF
  IF keyval(scSpace) > 0 THEN movex = 0 : movey = 0
  IF ps.cur THEN set_plank_state ps, ps.cur, plankSEL

  clearpage vpage
  DrawSlice root, vpage
  IF ps.cur ANDALSO (movex OR movey) THEN draw_isoline ps.cur, ps, movex, movey
  wrapprint "Scatter: " & scatter & " (+/-)  Present: " & percent & "% (</>) " & _
            "SHIFT+arrows: isolines (SPACE clears)", pLeft, pBottom, uilook(uiText), vpage
  setvispage vpage
  dowait

  IF ps.cur = 0 THEN ps.cur = top_left_plank(ps)  'Do after first draw
 LOOP
 DeleteSlice @root
END SUB

SUB HTTP_demo()
#IFNDEF MINIMAL_OS
 DIM url as string = "http://rpg.hamsterrepublic.com/nightly-archive/"
 IF prompt_for_string(url, "URL to fetch?", 100) = NO THEN EXIT SUB
 DIM req as HTTPRequest
 HTTP_request(@req, url, "GET")
 notification "failed=" & yesorno(req.failed) & " " & req.status & " - " & *req.status_string
 pop_warning *cast(zstring ptr, req.response)
 HTTP_Request_destroy(@req)
#ENDIF
END SUB

EXTERN "C"
 'This global affects the behaviour of open_process, which is called by
 'safe_shell, run_and_get_output, and a few other places.
 EXTERN CreateProc_opts as integer
END EXTERN

SUB CreateProcess_tests()
#IF DEFINED(__FB_WIN32__) AND NOT DEFINED(MINIMAL_OS)
 DIM menu(3) as string
 menu(0) = "Get HSpeak version (run_and_get_output)"
 menu(1) = "Run madplay and get output (run_and_get_output)"
 menu(2) = "Run madplay without output (safe_shell)"
 menu(3) = "CreateProcess: "
 DIM CPopts(9) as string
 CPopts(0) = "Normal/unmodified"
 CPopts(1) = "CREATE_NO_WINDOW"
 CPopts(2) = "DETACHED_PROCESS (no window)"
 CPopts(3) = "CREATE_NEW_CONSOLE"
 CPopts(4) = "No flags"
 CPopts(5) = "No flags, inactive window"
 CPopts(6) = "No flags, inactive minimised"
 CPopts(7) = "No flags, active minimised"
 CPopts(8) = "CREATE_NO_WINDOW and no I/O"
 CPopts(9) = "DETACHED_PROCESS and no I/O"

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

 DIM madplay as string = find_helper_app("madplay")
 ERROR_IF(madplay = "", missing_helper_message("madplay" DOTEXE))
 DIM hspeak as string = find_helper_app("hspeak")
 ERROR_IF(hspeak = "", missing_helper_message("hspeak" DOTEXE))

 setkeys
 DO
  setwait 55
  setkeys
  IF keyval(ccCancel) > 1 THEN EXIT DO
  usemenu state
  IF enter_space_click(state) THEN
   SELECT CASE state.pt
    CASE 0
     notification "hspeak version '" &  get_hspeak_version(hspeak) & "'"
    CASE 1
     DIM as string outp, errp
     DIM errlvl as integer = run_and_get_output(madplay & " -V", outp, errp)
     IF errlvl = 0 THEN
      notification "Test output: " & outp & !"\n-----------------------\n" & errp
     ELSE
      notification "Running madplay failed, " & errlvl & !"\n" & outp & !"\n" & errp
     END IF
    CASE 2
     DIM handle as ProcessHandle
     handle = open_process(madplay, "-q", YES, NO)
     DIM ran as bool = handle <> 0
     VAR errlvl = wait_for_process(@handle, 2500)
     IF ran THEN
      notification iif(errlvl=2, "Success", "Unexpected error " & errlvl)
     ELSE
      notification "Couldn't start"
     END IF
   END SELECT
  END IF
  IF state.pt = 3 THEN intgrabber CreateProc_opts, 0, 8
  menu(3) = "CreateProcess: " & CPopts(CreateProc_opts)

  clearpage vpage
  standardmenu menu(), state, , , vpage
  setvispage vpage
  dowait
 LOOP
#ENDIF
END SUB

FUNCTION yn(x as bool) as string
 RETURN IIF(x, "Y", "N")
END FUNCTION

SUB mouse_and_window_tests()
 'showmousecursor
 DO
  setwait 55
  setkeys
  IF keyval(ccCancel) > 1 THEN EXIT DO

  clearpage vpage
  WITH readmouse()
   IF .dragging THEN drawline .x, .y, .clickstart.x, .clickstart.y, findrgb(255,255,0), vpage
   draw_basic_mouse_cursor vpage

   DIM infostr as string
   infostr = !"Shift-1-R to toggle resolution resizability\n\n"

   WITH *gfx_getwindowstate()
    'resolution_unlocked() reports whether asked backend for a resizable window, not actual state
    infostr &= "WINDOW: mouse-over:" & yn(.mouse_over) & " focused:" & yn(.focused) & !"\n" _
             & "zoom:" & .zoom & " windowsize:" & .windowsize.wh & " wantresizable:" & yn(resolution_unlocked) & !"\n" _
             & "maximised:" & yn(.maximised) & !"\n\n"
   END WITH

   infostr  &= "MOUSE: active:" & yn(.active) _
             & " dragging:" & .dragging & !"\n" _
             & "pos:" & .pos & " clickstart:" & .clickstart & !"\n" _
             & "clicks:" & .clicks & " buttons:" & .buttons & " release:" & .release & !"\n" _
             & "wheel_clicks:" & .wheel_clicks & " wheel_delta:" & .wheel_delta

   wrapprint infostr, pInfoX, pInfoY, uilook(uiText), vpage
  END WITH
  setvispage vpage
  dowait
 LOOP
END SUB
