'OHRRPGCE - Some Custom/Game common code ' 'Please read LICENSE.txt for GPL License details and disclaimer of liability 'See README.txt for code docs and apologies for crappyness of this code ;) ' ' This file is for code that is shared between GAME and CUSTOM. ' Code that is not OHRRPGCE-specific that would be of general use ' to any FreeBasic program belongs in util.bas instead. ' Also, a few of these functions (those declared in common_base.bi) ' have alternative simpler implementations in common_base.bas ' and can used in commandline utilities. #include "config.bi" #include "ver.txt" #include "const.bi" #include "allmodex.bi" #include "audiofile.bi" #include "os.bi" #include "cutil.bi" #include "string.bi" #include "gfx.bi" #include "udts.bi" #include "scrconst.bi" #include "uiconst.bi" #include "common.bi" #include "slices.bi" #include "reload.bi" #include "sliceedit.bi" #include "music.bi" #include "loading.bi" 'Subs and functions only used here DECLARE SUB setup_sprite_sizes () DECLARE SUB check_map_count () DECLARE SUB fatal_error_shutdown () DECLARE SUB zero_out_pre_july_8_1999_general_map_data_garbage () DECLARE SUB append_to_logfile(s as string) DECLARE SUB archive_log_file(logfile as string, archivefile as string) #IFDEF IS_GAME #include "game_udts.bi" #include "gglobals.bi" #include "moresubs.bi" #include "scripting.bi" #include "yetmore2.bi" #ENDIF #IFDEF IS_CUSTOM #include "cglobals.bi" #include "customsubs.bi" #ENDIF DEFINE_VECTOR_OF_TYPE(TagRangeCheck, TagRangeCheck) ''''' Global variables (anything else in common.bi missing here will be in game.bas or custom.bas) 'Used for search_string_cache. Should change every time the current game changes; changing 'it on reloading some lumps while live previewing also useful, to clear certain caches '(Always blank in Custom) DIM game_unique_id as string 'Allocate sprite size table REDIM sprite_sizes(sprTypeFirst TO sprTypeLast) as SpriteSize setup_sprite_sizes 'Used as a description of the context of what we were doing when an error occurs, 'useful if bound_arg or reporterr is called from outside the script interpreter '(it's always printed by reporterr) 'NOTE: you must remember to reset this to "" when leaving a context! DIM context_string as string 'The directory to write logs like g/c_debug[_archive].txt '(Should have a slash. For very short time during initialisation equal to "" to write to CWD) DIM log_dir as string 'While true, debug messages also get appended to log_buffer. DIM remember_debug_messages as bool = YES 'Used on Mac to point to the app bundle Resources directory DIM app_resources_dir as string 'This is for application-wide settings, and prefsdir for games will be a subdirectory of it. DIM settings_dir as string 'ohrrpgce_config.ini. This is stored in either EXEPATH or settings_dir. 'Contains global settings. Custom could also put game-specific settings here, using a different 'key for each, which I think is better than loads of ini files, but there are no such settings yet. 'On the other hand, Game uses a different ini file for each game (that's the game_config global). DIM global_config_file as string 'The prefix used by read_config_int/etc functions for reading ohrrpgce_config.ini. 'Defines the current context. E.g. "edit.game_(gamename)." DIM config_prefix as string 'Used by intgrabber, reset by usemenu DIM negative_zero as integer = NO 'The global reload document acts as a parent to any misc reload data structures (e.g. hero data) kept in memory. DIM global_reload_doc as reload.DocPtr global_reload_doc = reload.CreateDocument() reload.SetRootNode(global_reload_doc, reload.CreateNode(global_reload_doc, "")) ''''' Module-local variables 'a primitive system for printing messages that scroll TYPE ConsoleData as integer x = 0, y = 0, top = 0, h = 200, c = 0, margin = 4 END TYPE DIM SHARED console as ConsoleData DIM SHARED show_upgrade_messages as bool 'Whether any upgrade messages have been shown yet DIM SHARED got_upgrade_messages as bool 'upgrading timing stuff DIM SHARED time_rpg_upgrade as bool = NO 'Change to YES, or pass --time-upgrade DIM SHARED last_upgrade_time as double DIM SHARED upgrade_overhead_time as double DIM SHARED upgrade_start_time as double 'Upgrade all game data (required for writing), or only enough to allow reading? #IFDEF IS_CUSTOM DIM SHARED full_upgrade as bool = YES #ELSE DIM SHARED full_upgrade as bool = NO #ENDIF ' Debug log stuff DIM SHARED debug_to_console as bool = NO 'don't delete the debug file at end of play DIM SHARED importantdebug as bool = NO 'Stores debug messages printed during initialisation, or in-game between playing different games. DIM SHARED log_buffer as string 'When restarting the log, the previous path if significant DIM SHARED lastlogfile as string 'Prevent two threads from writing at once, probably an unnecessary precaution DIM SHARED debug_log_mutex as any ptr debug_log_mutex = mutexcreate '.stt lump read into memory DIM SHARED global_strings_buffer as string 'Used by ensure_normal_palette & restore_previous_palette DIM SHARED remember_master(255) as RGBcolor DIM SHARED master_remembered as bool = NO 'binsize.bin cache DIM SHARED binsize_cache(binLASTENTRY) as integer DIM SHARED binsize_cache_loaded as bool = NO FUNCTION common_setoption(opt as string, arg as string) as integer IF opt = "time-upgrade" THEN time_rpg_upgrade = YES RETURN 1 'arg not used ELSEIF opt = "full-upgrade" THEN full_upgrade = YES RETURN 1 'arg not used END IF END FUNCTION 'fade in and out not actually used in custom SUB fadein () fadestate = YES fadetopal master() END SUB SUB fadeout (byval red as integer, byval green as integer, byval blue as integer) fadestate = NO fadeto red, green, blue END SUB 'Call before showing debug screens and messages in Game, in case the screen is faded out or 'the palette has been modified (e.g. for a scripted screen fade) 'Call restore_previous_palette() before resuming (mandatory) SUB ensure_normal_palette () #IFDEF IS_GAME 'IF fadestate = NO THEN setpal master() 'Simpler solution IF master_remembered THEN EXIT SUB master_remembered = YES FOR i as integer = 0 TO 255 remember_master(i) = master(i) NEXT 'Skip we haven't started a game yet IF LEN(game) > 0 THEN loadpalette master(), gam.current_master_palette setpal master() #ENDIF END SUB SUB restore_previous_palette () #IFDEF IS_GAME master_remembered = NO FOR i as integer = 0 TO 255 master(i) = remember_master(i) NEXT IF fadestate THEN setpal master() ELSE ' It's possible the screen was faded out to some other colour, but unusual REDIM black(255) as RGBcolor setpal black() END IF #ENDIF END SUB FUNCTION filesize (file as string) as string 'returns size of a file in formatted string DIM as integer size, spl DIM as string fsize, units IF isfile(file) THEN size = FILELEN(file) units = " B" spl = 0 IF size > 1024 THEN spl = 1 : units = " KB" IF size > 1048576 THEN spl = 1 : size = size / 1024 : units = " MB" fsize = STR(size) IF spl <> 0 THEN size = size / 102.4 fsize = STR(size \ 10) IF size < 1000 THEN fsize = fsize + "." + STR(size MOD 10) END IF RETURN fsize + units ELSE RETURN "N/A" END IF END FUNCTION ' If c/g_debug.txt already exists, archive it and create a new one. ' title is what message to write at the top SUB start_new_debug(title as string) DIM as string logfile, archivefile #IFDEF IS_GAME logfile = log_dir & "g_debug.txt" archivefile = log_dir & "g_debug_archive.txt" #ELSE logfile = log_dir & "c_debug.txt" archivefile = log_dir & "c_debug_archive.txt" #ENDIF ' Don't archive if we're already writing to this logfile IF lastlogfile <> absolute_path(logfile) THEN IF isfile(logfile) THEN archive_log_file logfile, archivefile END IF IF LEN(log_buffer) THEN ' Write internal buffer, which contains a lot of useful info printed during startup. append_to_logfile log_buffer END IF END IF debuginfo " ----" & title & "----" END SUB ' Append logfile to archivefile and delete it. PRIVATE SUB archive_log_file(logfile as string, archivefile as string) DIM dlog as integer = FREEFILE OPEN logfile FOR BINARY as dlog DIM archive as integer = FREEFILE OPEN archivefile FOR BINARY as archive CONST buflen = 128 * 1024 DIM as ubyte ptr buf = ALLOCATE(buflen) DIM copyamount as size_t = bound(buflen - LOF(dlog), 0, LOF(archive)) IF copyamount THEN SEEK #archive, LOF(archive) - copyamount + 1 'don't cut the file in the middle of a line DO GET #archive, , buf[0] LOOP UNTIL buf[0] = 10 GET #archive, , buf[0], buflen, copyamount END IF CLOSE #archive archive = FREEFILE OPEN archivefile FOR BINARY ACCESS WRITE as archive PUT #archive, , *buf, copyamount IF LOF(dlog) > buflen THEN SEEK #dlog, LOF(dlog) - buflen + 1 DO GET #dlog, , buf[0] LOOP UNTIL buf[0] = 10 END IF GET #dlog, , buf[0], buflen, copyamount PUT #archive, , LINE_END " +++|=========> APPLY SWORD, CUT HERE (Please send whole sections)" LINE_END LINE_END PUT #archive, , *buf, copyamount CLOSE #dlog CLOSE #archive DEALLOCATE(buf) safekill logfile END SUB ' Delete the log file if there were no important error messages. SUB end_debug DIM filename as string #IFDEF IS_GAME filename = "g_debug.txt" #ELSE filename = "c_debug.txt" #ENDIF IF NOT importantdebug THEN safekill log_dir & filename lastlogfile = "" log_buffer &= "end_debug: no debug/error messages during startup (skipping rest of startup)" LINE_END ELSE 'Remember not to archive the log if we restart the log in the same directory lastlogfile = absolute_path(log_dir & filename) END IF importantdebug = NO END SUB SUB debug (s as string) importantdebug = YES debuginfo "! " & s END SUB SUB debugc CDECL (byval errorlevel as errorLevelEnum, byval s as zstring ptr) 'This both provides a C interface to 'debug', and error levels to distinguish the significance of 'different messages 'See enum in const.bi for error level details 'Unimplemented: the idea is that 'Bug' error levels could prompt the user to send a bug report somehow SELECT CASE errorlevel CASE errInfo debuginfo *s CASE errDebug debug *s CASE errError debug "Error: " & *s CASE errPromptError debug "Error: " & *s notification "Error: " & *s & !"\nPress any key..." CASE errBug debug "(BUG) " & *s CASE errPrompt showerror *s CASE errPromptBug showerror *s, NO, YES CASE errFatal showerror *s, YES, NO CASE errFatalBug showerror *s, YES, YES CASE errDie 'TODO: Maybe print a stacktrace PRINT "DIE: " & *s debug "DIE: " & *s fatal_error_shutdown END SELECT END SUB SUB debuginfo (s as string) 'Use for throwaway messages like upgrading '(Also this is internally the implementation of debug()) ' It's likely that holding a mutex while writing isn't necessary, since ' all FB routines grab a mutex before changing any state anyway. mutexlock debug_log_mutex 'For now, on Android log everything to the kernel log for convenience external_log s IF debug_to_console THEN PRINT s IF remember_debug_messages THEN 'Don't keep growing the buffer or the program will grind to a halt IF LEN(log_buffer) < 10000 THEN log_buffer &= s & LINE_END END IF append_to_logfile s mutexunlock debug_log_mutex END SUB PRIVATE SUB append_to_logfile(s as string) STATIC sizeerror as integer = 0 DIM filename as string #IFDEF IS_GAME filename = log_dir & "g_debug.txt" #ELSE filename = log_dir & "c_debug.txt" #ENDIF DIM fh as integer = FREEFILE IF OPEN(filename FOR APPEND as #fh) THEN external_log "Couldn't open debug log " & filename PRINT "Couldn't open debug log " & filename EXIT SUB END IF IF LOF(fh) > 2 * 1024 * 1024 THEN IF sizeerror = 0 THEN PRINT #fh, "too much debug() output, not printing any more messages" sizeerror = -1 CLOSE #fh EXIT SUB END IF sizeerror = 0 PRINT #fh, normalize_newlines(s, LINE_END) CLOSE #fh END SUB 'Wrap a string in colour tags. Default to flashing. FUNCTION hilite(what as string, col as integer = -1) as string IF col = -1 THEN col = uilook(uiSelectedItem + global_tog) 'IF col = -1 THEN col = uilook(IIF(global_tog, uiSelectedItem, uiHighlight2)) RETURN "${K" & col & "}" & what & "${K-1}" END FUNCTION ' Takes "a string with `certain` parts marked to be `High`lighted" with colour 'col' ' and returns a marked-up string. FUNCTION ticklite(what as string, col as integer = -1) as string IF col = -1 THEN col = uilook(uiSelectedItem + global_tog) DIM ret as string DIM index as integer DIM lastindex as integer = 1 DIM inside_hilite as bool = NO ' Already opened a highlight tag DO index = INSTR(lastindex, what, "`") IF index = 0 THEN RETURN ret & MID(what, lastindex) 'Add the final part ret &= MID(what, lastindex, index - lastindex) IF inside_hilite THEN ret &= "${K-1}" ELSE ret &= "${K" & col & "}" inside_hilite XOR= YES lastindex = index + 1 LOOP END FUNCTION 'IF ticklite("`Test`f`x``") <> "${K2}Test${K-1}f${K2}x${K-1}${K2}" THEN fail 'Return the markup tag to switch foreground color. -1 resets to initial. 'Optionally, if you pass a string, wraps it with start/reset color tags. FUNCTION fgtag(col as integer, text as string = "") as string IF LEN(text) THEN RETURN "${K" & col & "}" & text & "${K-1}" ELSE RETURN "${K" & col & "}" END IF END FUNCTION 'Return the markup tag to switch background color (0 is transparent). -1 resets to initial. 'Optionally, if you pass a string, wraps it with start/reset color tags. FUNCTION bgtag(col as integer, text as string = "") as string IF LEN(text) THEN RETURN "${KB" & col & "}" & text & "${KB-1}" ELSE RETURN "${KB" & col & "}" END IF END FUNCTION 'Draw a wrapped string in a box, by default at the middle of the page. 'You can optionally specify a maximum width (default whole screen) 'If shrink=NO then the textbox extends to maximum width, otherwise its width is minimal. SUB basic_textbox (msg as string, col as integer = -1, page as integer, ypos as RelPos = pCentered, width as RelPos = -1, shrink as bool = NO, suppress_borders as bool = NO) IF col = -1 THEN col = uilook(uiText) IF width = -1 THEN width = small(vpages(page)->w - 20, 450) DIM size as XYPair = textsize(msg, width, fontEdged) ypos = relative_pos(ypos, vpages(page)->h, size.h) 'This line is necessary only to support anchor points DIM text_xpos as RelPos = pCentered IF shrink = NO THEN size.w = relative_pos(width, vpages(page)->w) text_xpos = rCenter - size.w \ 2 END IF edgeboxstyle pCentered, ypos - 5, size.w + 10, size.h + 10, 2, page, , suppress_borders wrapprint msg, text_xpos, ypos, col, page, width '- rCenter + size.w \ 2 END SUB ' Show a message in a box and wait for the user to advance with a keypress. ' Returns the scancode of the keypress (it is cleared!) FUNCTION notification (msg as string) as integer ' For safety disallow reentering, though I'm not currently aware of a way this might happen STATIC entered as bool = NO IF entered THEN debug "BUG: notfication reentered" RETURN 0 END IF entered = YES ensure_normal_palette DIM prev_mouse_vis as CursorVisibility = getcursorvisibility() defaultmousecursor DIM holdpage as integer = allocatepage copypage vpage, holdpage basic_textbox msg, uilook(uiText), holdpage setvispage holdpage DIM scancode as integer = waitforanykey() freepage holdpage restore_previous_palette setcursorvisibility(prev_mouse_vis) entered = NO RETURN scancode END FUNCTION 'Same as debugc, but always show a message to the user regardless of errlvl SUB visible_debug (msg as string, errlvl as errorLevelEnum = errDebug) debugc errlvl, msg IF errlvl < errPromptError OR errlvl = errBug THEN 'If it's not one of these error levels, then a message has already been shown notification msg + !"\nPress any key..." 'pop_warning msg END IF END SUB FUNCTION confirmed_copy (srcfile as string, destfile as string) as bool 'Copy a file, check to make sure it really was copied, and show an error message if not. '(FIXME: The real error message is printed to debug by copy_file_replacing()) ' Returns true if the copy was okay, false if it failed IF NOT isfile(srcfile) THEN visible_debug "ERROR: file " & destfile & " was missing" RETURN NO END IF ' I really hope that checking isfile is redundant, but who knows. IF NOT copyfile(srcfile, destfile) THEN visible_debug "ERROR: couldn't copy file to " & destfile RETURN NO END IF IF NOT isfile(destfile) THEN visible_debug "BUG: couldn't copy file to " & destfile & ", although copyfile() succeeded" RETURN NO END IF RETURN YES END FUNCTION FUNCTION confirmed_copydirectory(src as string, dest as string) as bool 'Copy a directory, but if it fails, clean up any partial copy and show a visible warning 'Returns YES for success and NO for failure DIM result as string result = copydirectory(src, dest, YES) IF result <> "" THEN visible_debug result IF isdir(dest) THEN killdir dest, YES END IF RETURN NO END IF RETURN YES END FUNCTION FUNCTION os_shell_move(src as string, dest as string) as bool 'When used to move a directory on unixes, this should preserve bits and symlinks. 'When used to move a directory on Windows this should just be a dang ol' move... except it doesn't work at all :( 'Returns YES for success or NO for failure 'Warning: doesn't send lump modification messages! So don't use to move lumps! 'Use local_file_move() if possible to send those message, and for more robust error reporting. IF isfile(dest) THEN safekill dest ELSEIF isdir(dest) THEN killdir dest, YES END IF DIM src_is_file as bool = isfile(src) IF NOT src_is_file ANDALSO NOT isdir(src) THEN debug "os_shell_move: src file " & src & " does not exist" RETURN NO END IF DIM stderr_s as string DIM mv as string DIM args as string #IFDEF __FB_UNIX__ mv = "mv" args = " -f" ' Overwrite without confirmation #ENDIF #IFDEF __FB_WIN32__ mv = "move" args = " /Y" ' Overwrite without confirmation #ENDIF args &= " " & escape_filename(src) & " " & escape_filename(dest) 'SHELL mv & args IF run_and_get_output(mv & args, "", stderr_s) THEN visible_debug "os_shell_move: move failed: " & stderr_s : RETURN NO END IF DIM success as bool = NO IF src_is_file THEN success = isfile(dest) ELSE success = isdir(dest) IF NOT success THEN debug "os_shell_move: dest file not created: " & dest RETURN success END FUNCTION FUNCTION getfixbit(byval bitnum as integer) as integer DIM f as string f = workingdir + SLASH + "fixbits.bin" IF NOT isfile(f) THEN RETURN 0 DIM fh as integer IF OPENFILE(f, FOR_BINARY + ACCESS_READ, fh) THEN debug "Could not read " & f : RETURN 0 DIM ub as UBYTE GET #fh, (bitnum \ 8) + 1, ub CLOSE #fh RETURN BIT(ub, bitnum MOD 8) 'BIT is a standard macro END FUNCTION SUB setfixbit(byval bitnum as integer, byval bitval as integer) IF bitnum >= sizeFixBits THEN fatalerror "setfixbit(" & bitnum & "): You forgot sizefixbits!" DIM f as string f = workingdir + SLASH + "fixbits.bin" DIM fh as integer IF OPENFILE(f, FOR_BINARY + ACCESS_READ_WRITE, fh) THEN fatalerror "Impossible to upgrade game: Could not write " & f 'Really bad! extendfile fh, (bitnum \ 8) + 1 'Prevent writing garbage DIM ub as UBYTE GET #fh, (bitnum \ 8) + 1, ub IF bitval THEN ub = BITSET(ub, bitnum MOD 8) ELSE ub = BITRESET(ub, bitnum MOD 8) PUT #fh, (bitnum \ 8) + 1, ub CLOSE #fh END SUB '----- Styled boxes 'All of the following require a box style rather than color, border, etc. 'Backwards compatibility wrapper 'x and y default to center of the screen 'This is equivalent to calling edgeboxstyle with ancCenter added to x/y SUB centerbox (x as RelPos=rCenter, y as RelPos=rCenter, w as RelPos, h as RelPos, boxstyle_plus1 as integer, page as integer) center_edgeboxstyle x, y, w, h, boxstyle_plus1 - 1, page END SUB SUB centerfuz (x as RelPos, y as RelPos, w as RelPos, h as RelPos, boxstyle_plus1 as integer, page as integer) center_edgeboxstyle x, y, w, h, boxstyle_plus1 - 1, page, YES END SUB 'boxstyle must be >= 0 SUB center_edgeboxstyle (x as RelPos=rCenter, y as RelPos=rCenter, w as RelPos, h as RelPos, boxstyle as integer, page as integer, fuzzy as bool=NO, suppress_borders as bool=NO) w = relative_pos(w, vpages(page)->w) h = relative_pos(h, vpages(page)->h) edgeboxstyle x - w \ 2, y - h \ 2, w, h, boxstyle, page, fuzzy, suppress_borders END SUB SUB edgeboxstyle (rect as RectType, boxstyle as integer, page as integer, fuzzy as bool=NO, suppress_borders as bool=NO) edgeboxstyle rect.x, rect.y, rect.wide, rect.high, boxstyle, page, fuzzy, suppress_borders END SUB SUB edgeboxstyle (x as RelPos, y as RelPos, w as RelPos, h as RelPos, boxstyle as integer, page as integer, fuzzy as bool=NO, suppress_borders as bool=NO) IF boxstyle < 0 OR boxstyle > UBOUND(boxlook) THEN showerror "edgeboxstyle: invalid boxstyle " & boxstyle EXIT SUB END IF DIM col as integer = boxlook(boxstyle).bgcol DIM bordercol as integer = boxlook(boxstyle).edgecol DIM border as RectBorderTypes = boxstyle DIM trans as RectTransTypes = transOpaque IF suppress_borders THEN border = borderLine IF fuzzy THEN trans = transFuzzy edgebox x, y, w, h, col, bordercol, page, trans, border END SUB '----- Unstyled boxes 'The following take col/bgcol/border instead of a box style number SUB edgebox (x as RelPos, y as RelPos, w as RelPos, h as RelPos, col as integer, bordercol as integer, page as integer, trans as RectTransTypes=transOpaque, border as RectBorderTypes=borderLine, fuzzfactor as integer=50, relcoords as bool=YES) edgebox x, y, w, h, col, bordercol, vpages(page), trans, border, fuzzfactor, relcoords END SUB SUB edgebox (x as RelPos, y as RelPos, w as RelPos, h as RelPos, col as integer, bordercol as integer, fr as Frame Ptr, trans as RectTransTypes=transOpaque, border as RectBorderTypes=borderLine, fuzzfactor as integer=50, relcoords as bool=YES) '--border: -2 is none, -1 is simple line, 0+ is styled box edge DIM borderindex as RectBorderTypes IF border >= 0 THEN IF border <= UBOUND(boxlook) THEN borderindex = boxlook(border).border - 1 ELSE borderindex = border END IF edgebox_rawborder x, y, w, h, col, bordercol, fr, trans, borderindex, fuzzfactor, relcoords END SUB SUB edgebox_rawborder (x as RelPos, y as RelPos, w as RelPos, h as RelPos, col as integer, bordercol as integer, fr as Frame Ptr, trans as RectTransTypes=transOpaque, borderindex as RectBorderTypes=borderLine, fuzzfactor as integer=50, relcoords as bool=YES) '--borderindex: -2 is none -1 is simple line, 0+ is box border sprite record number IF relcoords THEN w = relative_pos(w, fr->w) h = relative_pos(h, fr->h) x = relative_pos(x, fr->w, w) y = relative_pos(y, fr->h, h) END IF IF trans = transFuzzy THEN fuzzyrect fr, x, y, w, h, col, fuzzfactor ELSEIF trans = transOpaque THEN rectangle fr, x, y, w, h, col END IF IF borderindex = borderLine THEN '--Simple line border drawbox fr, x, y, w, h, bordercol ELSEIF borderindex >= 0 AND borderindex <= gen(genMaxBoxBorder) THEN '--Normal Border IF trans <> transHollow THEN drawbox fr, x, y, w, h, bordercol DIM border_gfx as GraphicPair load_sprite_and_pal border_gfx, sprTypeBoxBorder, borderindex WITH border_gfx IF .sprite THEN ' Only proceed if a sprite is actually selected 'Draw edges 'ensure we are clipping the correct page (there are many ways of doing this) setclip , , , , fr '--Top and bottom edges FOR i as integer = x + 8 TO x + w - 24 STEP 16 setclip , , , y + h - 1 frame_draw .sprite + 2, .pal, i, y - 8, 1, YES, fr setclip , y, , frame_draw .sprite + 13, .pal, i, y + h - 8, 1, YES, fr NEXT i '--Left and right edges FOR i as integer = y + 8 TO y + h - 24 STEP 16 setclip , , x + w - 1, frame_draw .sprite + 7, .pal, x - 8, i, 1, YES, fr setclip x, , , frame_draw .sprite + 8, .pal, x + w - 8, i, 1, YES, fr NEXT i 'Draw end-pieces IF w > 26 THEN '--Top end pieces setclip , , , y + h - 1 frame_draw .sprite + 3, .pal, x + w - 24, y - 8, 1, YES, fr frame_draw .sprite + 1, .pal, x + 8, y - 8, 1, YES, fr '--Bottom end pieces setclip , y, , frame_draw .sprite + 14, .pal, x + w - 24, y + h - 8, 1, YES, fr frame_draw .sprite + 12, .pal, x + 8, y + h - 8, 1, YES, fr ELSEIF w > 16 THEN '--Not enough space for the end pieces, have to draw part of the edge after all '--Top and bottom edges setclip x + 8, , x + w - 9, y + h - 1 frame_draw .sprite + 2, .pal, x + 8, y - 8, 1, YES, fr setclip x + 8, y, x + w - 9, frame_draw .sprite + 13, .pal, x + 8, y + h - 8, 1, YES, fr END IF IF h > 26 THEN '--Left side end pieces setclip , , x + w - 1, frame_draw .sprite + 9, .pal, x - 8, y + h - 24, 1, YES, fr frame_draw .sprite + 5, .pal, x - 8, y + 8, 1, YES, fr '--Right side end pieces setclip x, , , frame_draw .sprite + 10, .pal, x + w - 8, y + h - 24, 1, YES, fr frame_draw .sprite + 6, .pal, x + w - 8, y + 8, 1, YES, fr ELSEIF h > 16 THEN '--Not enough space for the end pieces, have to draw part of the edge after all '--Left and right edges setclip , y + 8, x + w - 1, y + h - 9 frame_draw .sprite + 7, .pal, x - 8, y + 8, 1, YES, fr setclip x, y + 8, , y + h - 9 frame_draw .sprite + 8, .pal, x + w - 8, y + 8, 1, YES, fr END IF 'Draw corners 'If the box is really tiny, we need to only draw part of each corner setclip , , x + w - 1, y + h - 1 frame_draw .sprite, .pal, x - 8, y - 8, 1, YES, fr setclip x, , , y + h - 1 frame_draw .sprite + 4, .pal, x + w - 8, y - 8, 1, YES, fr setclip , y, x + w - 1, frame_draw .sprite + 11, .pal, x - 8, y + h - 8, 1, YES, fr setclip x, y, , frame_draw .sprite + 15, .pal, x + w - 8, y + h - 8, 1, YES, fr setclip END IF END WITH unload_sprite_and_pal border_gfx END IF END SUB '------------------------------------------------------------------------------ FUNCTION read32bitstring (stringptr as integer ptr) as string DIM as string result = SPACE(stringptr[0]) memcpy(STRPTR(result), @stringptr[1], stringptr[0]) return result END FUNCTION FUNCTION readbadgenericname (byval index as integer, filename as string, byval recsize as integer, byval offset as integer, byval size as integer, byval skip as integer = 0, byval expectexists as bool=YES) as string 'recsize is in BYTES! 'FIXME: there isn't any good reason to load the whole record ' just to get the string field IF index >= 0 THEN DIM buf(recsize \ 2 - 1) as integer IF loadrecord(buf(), filename, recsize \ 2, index, expectexists) THEN RETURN readbadbinstring(buf(), offset, size, skip) END IF END IF RETURN "" 'failure END FUNCTION ' Given a trigger or script ID, return a script ID. If the script is missing, ' show an error (Game only), and return 0. FUNCTION decodetrigger (byval trigger as integer) as integer 'debug "decoding " + STR(trigger) IF trigger >= 16384 AND trigger - 16384 <= UBOUND(lookup1_bin_cache) THEN WITH lookup1_bin_cache(trigger - 16384) 'debug " id " & .id & " name " & .name #ifdef IS_GAME IF .id = 0 THEN ' Not an error in Custom as we're not trying to actually run it scripterr "Script " + .name + " is used but is missing (no script with that name)", serrError END IF #endif RETURN .id END WITH ELSE RETURN trigger END IF END FUNCTION 'A script trigger which has a default value, such the onkeypress trigger FUNCTION trigger_or_default(trigger as integer, default as integer) as integer IF trigger < 0 THEN RETURN 0 IF trigger = 0 THEN RETURN default RETURN trigger END FUNCTION ' Find the name of a script given ID number or trigger ID. ' If called with an invalid script id, the result will be "[id ###]" FUNCTION scriptname (id_or_trigger as integer) as string IF id_or_trigger = 0 THEN RETURN "[none]" ELSE DIM idx as integer = intstr_array_find(script_names(), id_or_trigger) IF idx > -1 THEN RETURN script_names(idx).s RETURN "[id " & id_or_trigger & "]" END IF END FUNCTION ' 0 means use the default default, -1 means none FUNCTION scriptname_default(id_or_trigger as integer, default_trigger as integer) as string IF id_or_trigger = -1 THEN RETURN "[none]" IF id_or_trigger = 0 THEN IF default_trigger = 0 THEN RETURN "[default: none]" RETURN "[default] " & scriptname(default_trigger) END IF RETURN scriptname(id_or_trigger) END FUNCTION Function seconds2str(byval sec as integer, f as string = " %m: %S") as string dim ret as string dim as integer s, m, h s = sec mod 60 m = (sec \ 60) mod 60 h = (sec \ 3600) dim as integer i FOR i as integer = 0 to len(f) - 1 if f[i] = asc("%") then i+=1 select case as const f[i] case asc("s") ret = ret & sec case asc("S") if s < 10 then ret = ret & "0" ret = ret & s case asc("m") ret = ret & (sec \ 60) case asc("M") if m < 10 then ret = ret & "0" ret = ret & m case asc("h") ret = ret & h case asc("H") if h < 10 then ret = ret & "0" ret = ret & h case asc("%") ret = ret & "%" end select else ret = ret & chr(f[i]) end if next return ret end function FUNCTION getdefaultpal(byval fileset as SpriteType, byval index as integer) as integer IF fileset < 0 OR fileset > sprTypeLastPT THEN debugc errBug, "getdefaultpal: bad fileset " & fileset RETURN -1 END IF DIM as string lumpsdir, fname ' Use a default if no game has been loaded lumpsdir = IIF(LEN(game), workingdir, finddatadir("defaultgfx")) fname = lumpsdir & SLASH & "defpal" & fileset & ".bin" DIM fh as integer IF OPENFILE(fname, FOR_BINARY + ACCESS_READ, fh) = 0 THEN DIM v as short GET #fh, 1 + index * 2, v CLOSE #fh RETURN v ELSE 'currently extended NPCs palettes are initialised to -1, which means lots of debug spam in old games 'debug "Default palette file " & fname & " does not exist" RETURN -1 END IF END FUNCTION FUNCTION abs_pal_num(byval num as integer, byval sprtype as SpriteType, byval spr as integer) as integer IF num >= 0 THEN RETURN num IF num = -1 THEN RETURN getdefaultpal(sprtype, spr) debug "decode_default_pal: invalid palette " & num RETURN 0 END FUNCTION SUB loaddefaultpals(byval fileset as SpriteType, poffset() as integer, byval sets as integer) DIM v as short DIM f as string = workingdir & SLASH & "defpal" & fileset & ".bin" DIM fh as integer IF OPENFILE(f, FOR_BINARY + ACCESS_READ, fh) = 0 THEN FOR i as integer = 0 to sets GET #fh, 1 + i * 2, v poffset(i) = v NEXT i CLOSE #fh ELSE guessdefaultpals fileset, poffset(), sets END IF END SUB SUB savedefaultpals(byval fileset as SpriteType, poffset() as integer, byval sets as integer) DIM v as short DIM f as string = workingdir & SLASH & "defpal" & fileset & ".bin" DIM fh as integer OPENFILE(f, FOR_BINARY + ACCESS_WRITE, fh) FOR i as integer = 0 to sets v = poffset(i) PUT #fh, 1 + i * 2, v NEXT i CLOSE #fh END SUB SUB guessdefaultpals(byval fileset as SpriteType, poffset() as integer, byval sets as integer) DIM her as herodef DIM found as integer flusharray poffset(), sets, 0 SELECT CASE fileset CASE 0 'Heroes FOR j as integer = 0 TO gen(genMaxHero) loadherodata her, j IF her.sprite >= 0 AND her.sprite <= sets THEN poffset(her.sprite) = her.sprite_pal NEXT CASE 1 TO 3 'Enemies 'Inefficient DIM enemy as EnemyDef FOR j as integer = 0 TO gen(genMaxEnemy) loadenemydata enemy, j IF enemy.size + 1 = fileset THEN IF enemy.pic >= 0 AND enemy.pic <= sets THEN poffset(enemy.pic) = enemy.pal END IF NEXT j CASE 4 'Walkabouts FOR j as integer = 0 TO gen(genMaxHero) loadherodata her, j IF her.walk_sprite >= 0 AND her.walk_sprite <= sets THEN poffset(her.walk_sprite) = her.walk_sprite_pal END IF NEXT j REDIM npcbuf(0) as NPCType FOR mapi as integer = 0 TO gen(genMaxMap) LoadNPCD maplumpname(mapi, "n"), npcbuf() FOR j as integer = 0 to UBOUND(npcbuf) IF npcbuf(j).picture >= 0 AND npcbuf(j).picture <= sets THEN poffset(npcbuf(j).picture) = npcbuf(j).palette END IF NEXT j NEXT mapi CASE 5 'Weapons REDIM buf(dimbinsize(binITM)) as integer FOR j as integer = 0 TO gen(genMaxItem) loaditemdata buf(), j IF buf(49) = 1 THEN IF buf(52) >= 0 AND buf(52) <= sets THEN poffset(buf(52)) = buf(53) END IF NEXT CASE 6 'Attacks REDIM buf(40 + dimbinsize(binATTACK)) as integer FOR j as integer = 0 TO gen(genMaxAttack) loadattackdata buf(), j IF buf(0) >= 0 AND buf(0) <= sets THEN poffset(buf(0)) = buf(1) NEXT CASE ELSE 'Portraits and later 'Default palettes were implemented before portraits, so this can only be called 'the first time you ever open the portrait editor in an old game -- no point 'implementing this END SELECT END SUB ' Returns the default record size in BYTES to use for getbinsize() when binsize.bin is missing FUNCTION defbinsize (byval id as integer) as integer IF id = binATTACK THEN RETURN 0 'attack.bin IF id = binSTF THEN RETURN 64 '.stf IF id = binSONGDATA THEN RETURN 0 'songdata.bin IF id = binSFXDATA THEN RETURN 0 'sfxdata.bin IF id = binMAP THEN RETURN 40 '.map IF id = binMENUS THEN RETURN 0 'menus.bin IF id = binMENUITEM THEN RETURN 0 'menuitem.bin IF id = binUICOLORS THEN RETURN 0 'uicolors.bin IF id = binSAY THEN RETURN 400 '.say IF id = binN THEN RETURN 30 '.n## IF id = binDT0 THEN RETURN 636 '.dt0 IF id = binDT1 THEN RETURN 320 '.dt1 IF id = binITM THEN RETURN 200 '.itm RETURN 0 END FUNCTION ' Returns the native size in BYTES of the records for the version you are running FUNCTION curbinsize (byval id as integer) as integer IF id = binATTACK THEN RETURN 596 'attack.bin '(size of combined attack data as on DT6 page minus 80) IF id = binSTF THEN RETURN 84 '.stf IF id = binSONGDATA THEN RETURN 32 'songdata.bin IF id = binSFXDATA THEN RETURN 34 'sfxdata.bin IF id = binMAP THEN RETURN 758 '.map IF id = binMENUS THEN RETURN 54 'menus.bin IF id = binMENUITEM THEN RETURN 64 'menuitem.bin IF id = binUICOLORS THEN RETURN 126 'uicolors.bin IF id = binSAY THEN RETURN 422 '.say IF id = binN THEN RETURN 38 '.n## IF id = binDT0 THEN RETURN 858 '.dt0 IF id = binDT1 THEN RETURN 742 '.dt1 IF id = binITM THEN RETURN 478 '.itm RETURN 0 END FUNCTION PRIVATE SUB load_binsize_cache () FOR i as integer = 0 TO binLASTENTRY binsize_cache(i) = defbinsize(i) NEXT DIM fh as integer = FREEFILE ' No error if missing IF OPENFILE(workingdir + SLASH + "binsize.bin", FOR_BINARY + ACCESS_READ, fh) = fberrOK THEN DIM as short recordsize ' If binsize.bin is larger than expected that is reported in rpg_sanity_checks DIM as integer recs = LOF(fh) \ 2 FOR id as integer = 0 TO small(binLASTENTRY, recs - 1) GET #fh, 1 + id * 2, recordsize binsize_cache(id) = recordsize NEXT CLOSE #fh END IF binsize_cache_loaded = YES END SUB SUB clear_binsize_cache () binsize_cache_loaded = NO END SUB FUNCTION getbinsize (byval id as integer) as integer 'returns the current size in BYTES of the records in the specific binary file you are working with IF id < 0 OR id > binLASTENTRY THEN showerror "Request for unknown binsize entry " & id RETURN 0 ELSE IF binsize_cache_loaded = NO THEN load_binsize_cache RETURN binsize_cache(id) END IF END FUNCTION 'INTS, not bytes! FUNCTION dimbinsize (byval id as integer) as integer 'curbinsize is size supported by current version of engine 'getbinsize is size of records in RPG file dimbinsize = large(curbinsize(id), getbinsize(id)) \ 2 - 1 END FUNCTION SUB setbinsize (byval id as integer, byval size as integer) IF id < 0 OR id > binLASTENTRY THEN showerror "Tried to set unknown binsize entry " & id RETURN END IF DIM fh as integer IF OPENFILE(workingdir & SLASH & "binsize.bin", FOR_BINARY + ACCESS_READ_WRITE, fh) THEN showerror "Failed to write binsize.bin(" & id & "), will cause data corruption!" RETURN END IF PUT #fh, 1 + id * 2, CAST(short, size) CLOSE #fh binsize_cache(id) = size END SUB 'Normally gamedir will be workingdir, and sourcefile will be sourcerpg FUNCTION readarchinym (gamedir as string, sourcefile as string) as string DIM iname as string DIM fh as integer IF isfile(gamedir + SLASH + "archinym.lmp") THEN OPENFILE(gamedir + SLASH + "archinym.lmp", FOR_INPUT, fh) LINE INPUT #fh, iname CLOSE #fh iname = LCASE(iname) 'IF isfile(gamedir + SLASH + iname + ".gen") THEN RETURN iname 'ELSE ' debug gamedir + SLASH + "archinym.lmp" + " invalid, ignored" 'END IF ELSE debuginfo gamedir + SLASH + "archinym.lmp" + " unreadable" END IF ' for backwards compatibility with ancient games that lack archinym.lmp 'iname = LCASE(trimextension(trimpath(sourcefile))) 'IF isfile(gamedir + SLASH + iname + ".gen") THEN RETURN iname ' Otherwise just scan the directory for a .GEN lump ' (findfiles is case-insensitive) DIM listing() as string findfiles gamedir, "*.gen", , , listing() IF UBOUND(listing) = 0 THEN debuginfo "But found " & listing(0) RETURN LCASE(trimextension(listing(0))) END IF fatalerror sourcefile & " doesn't appear to be an OHRRPGCE game: doesn't contain crucial data files" END FUNCTION FUNCTION maplumpname (byval map_id as integer, oldext as string) as string IF map_id < 100 THEN return game & "." & oldext & RIGHT("0" & map_id, 2) ELSE return workingdir & SLASH & map_id & "." & oldext END IF END FUNCTION SUB fatal_error_shutdown #IFDEF IS_CUSTOM IF cleanup_workingdir_on_error ANDALSO LEN(workingdir) ANDALSO isdir(workingdir) THEN touchfile workingdir & SLASH & "__danger.tmp" killdir workingdir END IF closemusic restoremode 'no need for end_debug SYSTEM 1 #ELSE exitprogram NO, 1 #ENDIF END SUB SUB fatalerror (msg as string) showerror msg, YES END SUB 'Show an error. If isfatal, doesn't return. 'If multiple errors occur, give the user the option to quit instead of continuing. SUB showerror (msg as string, byval isfatal as bool = NO, isbug as bool = NO) DIM printmsg as string printmsg = IIF(isbug, "(BUG) ", "") + IIF(isfatal, "FATAL: ", "ERROR: ") + msg PRINT printmsg STATIC num_errors as integer = 0 num_errors += 1 STATIC last_error as string, ignore_error as string IF msg = ignore_error AND isfatal = NO THEN EXIT SUB STATIC entered as integer = 0 'don't allow reentry IF entered THEN IF isfatal AND entered = 1 THEN entered = 2 fatal_error_shutdown END IF EXIT SUB END IF entered = 1 debug printmsg IF modex_initialised = NO THEN PRINT "showerror: modex_initialised false" debug "showerror: modex_initialised false" fatal_error_shutdown END IF DIM prompt_to_quit as bool = NO DIM quitmsg as string quitmsg = !"\n\n" '"${K" & uilook(uiMenuItem) & "}" IF isfatal THEN quitmsg += "Press any key to quit." prompt_to_quit = YES ELSE IF msg = last_error THEN quitmsg += !"(Error repeated)\n" ignore_error = msg END IF IF num_errors >= 3 THEN quitmsg += "Press ESC to cleanly quit, or any other key to ignore and try to continue." prompt_to_quit = YES ELSE quitmsg += "Press any key to try to continue." END IF END IF #IFDEF IS_CUSTOM IF prompt_to_quit ANDALSO cleanup_workingdir_on_error = NO THEN quitmsg += !"\nThe editing state of the game will be preserved" IF isfatal = NO THEN quitmsg += " if you quit immediately" quitmsg += "; run " + CUSTOMEXE + " again and you will be asked whether you want to recover it." END IF #ENDIF IF isbug THEN quitmsg += !"\nPlease report this engine bug by sending an e-mail to ohrrpgce-crash@HamsterRepublic.com" ELSE quitmsg += !"\nIf this error is unexpected, please send an e-mail to ohrrpgce-crash@HamsterRepublic.com" END IF last_error = msg IF isfatal THEN msg = !" -- FATAL ERROR --\n" + msg ELSEIF isbug THEN msg = !" -- OHRRPGCE BUG --\n" + msg END IF 'Reset palette (in case the error happened in a fade-to-black or due to 'corrupt/missing palette or UI colours) REDIM default_palette(255) as RGBcolor load_default_master_palette default_palette() setpal default_palette() DefaultUIColors uilook(), boxlook() clearpage vpage basic_textbox msg + quitmsg, uilook(uiText), vpage setvispage vpage, NO DIM key as integer = waitforanykey IF isfatal ORELSE (num_errors >= 3 ANDALSO key = scEsc) THEN fatal_error_shutdown END IF debug "showerror: user continued" #IFDEF IS_CUSTOM 'Continuing to edit IF editing_a_game THEN clearpage vpage basic_textbox "Warning! If you had unsaved changes to your game you should backup the old .RPG file " _ "BEFORE attempting to save, because there is a chance that saving will produce a corrupt file.", _ uilook(uiText), vpage setvispage vpage waitforanykey END IF IF activepalette > -1 THEN loadpalette master(), activepalette LoadUIColors uilook(), boxlook(), activepalette END IF #ELSE 'Restore game's master palette and ui colors IF gam.current_master_palette > -1 THEN loadpalette master(), gam.current_master_palette LoadUIColors uilook(), boxlook(), gam.current_master_palette END IF #ENDIF setpal master() entered = 0 END SUB 'Return the right end of a string, trimming and prepending '...' if longer than wide pixels '(Might sometimes want to handle this using text slices instead) FUNCTION shorten_to_left (text as string, byval wide as integer) as string DIM w as integer = textwidth(text) IF w <= wide THEN RETURN text wide -= textwidth("...") DIM curspos as StringCharPos find_point_in_text @curspos, wide, 0, text, , , , fontPlain RETURN "..." + RIGHT(text, curspos.charnum) END FUNCTION 'Return the left end of a string, trimming and appending '...' if longer than wide pixels '(Might sometimes want to handle this using text slices instead) FUNCTION shorten_to_right (text as string, byval wide as integer) as string DIM w as integer = textwidth(text) IF w <= wide THEN RETURN text wide -= textwidth("...") DIM curspos as StringCharPos find_point_in_text @curspos, wide, 0, text, , , , fontPlain RETURN LEFT(text, curspos.charnum) + "..." END FUNCTION SUB poke8bit (array16() as integer, byval index as integer, byval val8 as integer) IF index \ 2 > UBOUND(array16) THEN debug "Dang rotten poke8bit(array(" & UBOUND(array16) & ")," & index & "," & val8 & ") out of range" EXIT SUB END IF IF val8 <> (val8 AND &hFF) THEN debug "Warning: " & val8 & " is not an 8-bit number. Discarding bits: " & (val8 XOR &hFF) val8 = val8 AND &hFF END IF DIM element as integer = array16(index \ 2) DIM lb as integer = element AND &hFF DIM hb as integer = (element AND &hFF00) SHR 8 IF index AND 1 THEN hb = val8 ELSE lb = val8 END IF element = lb OR (hb SHL 8) array16(index \ 2) = element END SUB FUNCTION peek8bit (array16() as integer, byval index as integer) as integer IF index \ 2 > UBOUND(array16) THEN debug "Dang rotten peek8bit(array(" & UBOUND(array16) & ")," & index & ") out of range" RETURN 0 END IF DIM element as integer = array16(index \ 2) IF index AND 1 THEN RETURN (element AND &hFF00) SHR 8 ELSE RETURN element AND &hFF END IF END FUNCTION SUB loadpalette(pal() as RGBcolor, byval palnum as integer) IF palnum < 0 THEN debug "loadpalette: invalid palnum " & palnum palnum = 0 END IF DIM fh as integer IF OPENFILE(workingdir + SLASH + "palettes.bin", FOR_BINARY + ACCESS_READ, fh) <> fberrOK THEN '.MAS fallback, palnum ignored because it doesn't matter DIM oldpalbuf(767) as integer xbload game + ".mas", oldpalbuf(), "master palette missing from " + sourcerpg convertpalette oldpalbuf(), pal() ELSE DIM as short headsize, recsize DIM palbuf(767) as ubyte GET #fh, , headsize GET #fh, , recsize GET #fh, recsize * palnum + headsize + 1, palbuf() CLOSE #fh FOR i as integer = 0 TO 255 pal(i).r = palbuf(i * 3) pal(i).g = palbuf(i * 3 + 1) pal(i).b = palbuf(i * 3 + 2) NEXT END IF 'Uncomment the line below if you want the palette in text format for updating load_default_master_palette 'dump_master_palette_as_hex pal() END SUB SUB savepalette(pal() as RGBcolor, byval palnum as integer) DIM as short headsize = 4, recsize = 768 'Defaults DIM fh as integer OPENFILE(workingdir + SLASH + "palettes.bin", FOR_BINARY + ACCESS_READ_WRITE, fh) IF LOF(fh) >= 4 THEN GET #fh, 1, headsize GET #fh, 3, recsize ELSE PUT #fh, 1, headsize PUT #fh, 3, recsize END IF DIM palbuf(recsize - 1) as UBYTE FOR i as integer = 0 TO 255 palbuf(i * 3) = pal(i).r palbuf(i * 3 + 1) = pal(i).g palbuf(i * 3 + 2) = pal(i).b NEXT PUT #fh, recsize * palnum + headsize + 1, palbuf() CLOSE #fh 'This is not necessary in the slightest, but we copy the default master palette 'back to the .MAS lump, to give old graphics utilities some chance of working IF palnum = gen(genMasterPal) THEN unconvertpalette() END SUB SUB convertpalette(oldpal() as integer, newpal() as RGBcolor) 'takes a old QB style palette (as 768 ints), translates it to '8 bits per component and writes it to the provided RGBcolor array DIM r as integer DIM g as integer DIM b as integer FOR i as integer = 0 TO 255 r = oldpal(i * 3) g = oldpal(i * 3 + 1) b = oldpal(i * 3 + 2) 'newpal(i).r = r shl 2 or r shr 4 'newpal(i).g = g shl 2 or g shr 4 'newpal(i).b = b shl 2 or b shr 4 newpal(i).r = iif(r, r shl 2 + 3, 0) 'Mapping as Neo suggested newpal(i).g = iif(g, g shl 2 + 3, 0) newpal(i).b = iif(b, b shl 2 + 3, 0) NEXT END SUB SUB unconvertpalette() 'Takes the default new format palette and saves it in the old QB style palette 'format. This is only here to help out old graphics tools DIM newpal(255) as RGBcolor, oldpal(767) as integer loadpalette newpal(), gen(genMasterPal) FOR i as integer = 0 TO 255 oldpal(i * 3) = newpal(i).r \ 4 oldpal(i * 3 + 1) = newpal(i).g \ 4 oldpal(i * 3 + 2) = newpal(i).b \ 4 NEXT xbsave game + ".mas", oldpal(), 1536 END SUB FUNCTION rgb_to_string(col as RGBcolor) as string RETURN "rgb(" & col.r & "," & col.g & "," & col.b & ")" END FUNCTION 'Decode a string like "rgb(0,1,2)". Returns true on success FUNCTION string_to_rgb(text as string, byref col as RGBcolor) as bool IF starts_with(LCASE(text), "rgb(") = NO THEN RETURN NO DIM pos as integer = 5 'one-based 'Use VALINT because it ignores trailing garbage '(This is very lenient) col.r = VALINT(MID(text, pos)) pos = INSTR(pos, text, ",") IF pos = 0 THEN RETURN NO pos += 1 col.g = VALINT(MID(text, pos)) pos = INSTR(pos + 1, text, ",") IF pos = 0 THEN RETURN NO pos += 1 col.b = VALINT(MID(text, pos)) pos = INSTR(pos, text, ")") IF pos = 0 THEN RETURN NO RETURN YES END FUNCTION 'Takes either a string like "rgb(0,1,2)", and looks it up in the master 'palette (never maps to color 0) or a string containing a color index, eg "14". FUNCTION string_to_color(text as string, default as integer = -1) as integer DIM col as RGBcolor IF string_to_rgb(text, col) THEN RETURN nearcolor(master(), col.r, col.g, col.b, 1) END IF DIM ret as integer = str2int(text, -1) IF ret < 0 OR ret > 255 THEN debuginfo "invalid color string " & text : RETURN default RETURN ret END FUNCTION /' Don't compile any testcases from this module yet #IFDEF __FB_MAIN__ startTest(rgb) DIM col as RGBcolor IF rgb_to_string(col) <> "rgb(0,0,0)" THEN fail IF string_to_rgb("rgb(16,0,255)", col) <> YES THEN fail IF col.col <> &h1000ff THEN fail 'BGRA IF string_to_rgb("rgb(16,0,255", col) THEN fail endTest #ENDIF '/ '---------------------------------------------------------------------- FUNCTION getmapname (byval mapnum as integer) as string DIM nameread(39) as integer ' Ignore a missing .mn file, but allow partially missing records... IF loadrecord(nameread(), game + ".mn", 40, mapnum, NO, YES) THEN ' ...because the size of the record is twice the max size of the map name, which is a mistake. DIM a as string = STRING(small((nameread(0) AND 255), 39), " ") array2str nameread(), 1, a RETURN a END IF END FUNCTION FUNCTION createminimap (tiles() as TileMap, tilesets() as TilesetData ptr, pmapptr as TileMap ptr = NULL, byref zoom as integer = -1) as Frame ptr 'zoom = 1 is unzoomed, zoom = 20 is 1 pixel per tile. 'If zoom is -1, calculate and store it 'DIM drawtime as double = TIMER IF zoom = -1 THEN 'auto-detect best zoom zoom = bound(small(vpages(vpage)->w \ tiles(0).wide, vpages(vpage)->h \ tiles(0).high), 1, 20) END IF DIM mini as Frame Ptr mini = frame_new(zoom * tiles(0).wide, zoom * tiles(0).high) DIM composed_tile as Frame Ptr composed_tile = frame_new(20, 20) DIM fraction as single fraction = 20 / zoom DIM pixel as integer DIM prng_state as uinteger = 0 FOR ty as integer = 0 TO tiles(0).high - 1 FOR tx as integer = 0 TO tiles(0).wide - 1 'Clearing is only necessary if map layer 0 isn't in tiles(), so the maplayer stack may be transparent IF tiles(0).layernum <> 0 THEN frame_clear(composed_tile, uilook(uiBackground)) END IF draw_layers_at_tile(composed_tile, tiles(), tilesets(), tx, ty, pmapptr) FOR x as integer = 0 TO zoom - 1 FOR y as integer = 0 TO zoom - 1 DIM as integer i, j i = INT((x + simple_rand(prng_state)) * fraction) j = INT((y + simple_rand(prng_state)) * fraction) pixel = composed_tile->image[j * 20 + i] mini->image[(tx * zoom + x) + (ty * zoom + y) * mini->w] = pixel NEXT NEXT NEXT NEXT frame_unload @composed_tile ' drawtime = (TIMER - drawtime) ' debug "createminimap in " & drawtime & " -- " & (1e6 * drawtime / mini->w / mini->h) & "us/pix" RETURN mini END FUNCTION FUNCTION createminimap (layer as TileMap, tileset as TilesetData ptr, byref zoom as integer = -1) as Frame ptr DIM layers(0) as TileMap DIM tilesetsdata(0) as TilesetData ptr layers(0) = layer tilesetsdata(0) = tileset RETURN createminimap(layers(), tilesetsdata(), NULL, zoom) END FUNCTION FUNCTION readattackname (byval index as integer) as string RETURN readbadgenericname(index, game + ".dt6", 80, 24, 10, 1) END FUNCTION FUNCTION readattackcaption (byval index as integer) as string DIM buf(40 + dimbinsize(binATTACK)) as integer loadattackdata buf(), index RETURN readbinstring(buf(), 73, 38) END FUNCTION FUNCTION readenemyname (byval index as integer) as string RETURN readbadgenericname(index, game + ".dt1", getbinsize(binDT1), 0, 16, 0) END FUNCTION FUNCTION readitemname (byval index as integer) as string RETURN readbadgenericname(index, game + ".itm", getbinsize(binITM), 0, 8, 0) END FUNCTION FUNCTION readitemdescription (byval index as integer) as string RETURN readbadgenericname(index, game + ".itm", getbinsize(binITM), 9, 36, 0) END FUNCTION FUNCTION readshopname (byval shopnum as integer) as string RETURN readbadgenericname(shopnum, game + ".sho", 40, 0, 15, 0) END FUNCTION FUNCTION getsongname (byval num as integer, byval prefixnum as integer = 0) as string DIM songd(dimbinsize(binSONGDATA)) as integer DIM s as string IF num = -1 THEN RETURN "-none-" s = "" IF prefixnum THEN s = num & " " loadrecord songd(), workingdir + SLASH + "songdata.bin", curbinsize(binSONGDATA) \ 2, num s = s & readbinstring(songd(), 0, 30) RETURN s END FUNCTION FUNCTION getsfxname (byval num as integer) as string DIM sfxd(dimbinsize(binSFXDATA)) as integer loadrecord sfxd(), workingdir & SLASH & "sfxdata.bin", curbinsize(binSFXDATA) \ 2, num RETURN readbinstring (sfxd(), 0, 30) END FUNCTION 'keygrabber is like intgrabber (or usemenu) except it only watches the two keys passed as arguments, not 'number keys, page up/down, copy/paste, minus, etc. FUNCTION keygrabber (byref n as integer, min as integer, max as integer, less as integer=scLeft, more as integer=scRight) as bool IF keyval(more) > 1 THEN n = loopvar(n, min, max, 1) RETURN YES ELSEIF keyval(less) > 1 THEN n = loopvar(n, min, max, -1) RETURN YES END IF RETURN NO END FUNCTION 'Modify an integer according to key input (less and more are scancodes for decrementing and incrementing) 'If returninput is true, returns whether the user tried to modify the int, 'otherwise returns true only if the int actually changed. 'If autoclamp is false, n is clamped within allowable range only if a key is pressed FUNCTION intgrabber (byref n as integer, min as integer, max as integer, less as integer=scLeft, more as integer=scRight, returninput as bool=NO, use_clipboard as bool=YES, autoclamp as bool=YES, scrollwheel as WheelHandlingEnum=wheelRightButton) as bool DIM as longint temp = n intgrabber = intgrabber(temp, cast(longint, min), cast(longint, max), less, more, returninput, use_clipboard, autoclamp, scrollwheel) n = temp END FUNCTION 'See above for documentation FUNCTION intgrabber (byref n as longint, min as longint, max as longint, less as integer=scLeft, more as integer=scRight, returninput as bool=NO, use_clipboard as bool=YES, autoclamp as bool=YES, scrollwheel as WheelHandlingEnum=wheelRightButton) as bool STATIC clip as longint DIM old as longint = n DIM typed as bool = NO ' Mapping from scNumpad7 and up to scNumpad0 to values STATIC numpad_nums(...) as integer = {7, 8, 9, -1, 4, 5, 6, -1, 1, 2, 3, 0} DIM updown as integer = 0 IF scrollwheel = wheelRightButton THEN IF readmouse().buttons AND mouseright THEN updown = readmouse().wheel_clicks ELSEIF scrollwheel = wheelAlways THEN updown = readmouse().wheel_clicks END IF IF more <> 0 ANDALSO keyval(more) > 1 THEN updown += 1 IF less <> 0 ANDALSO keyval(less) > 1 THEN updown -= 1 IF updown THEN n = bound(n, min, max) n = loopvar(n, min, max, updown) typed = YES ELSE DIM sign as integer = SGN(n) n = ABS(n) IF keyval(scBackspace) > 1 THEN n \= 10: typed = YES ' Make sure you're not typing something else with shift IF keyval(scShift) = 0 THEN FOR i as integer = 1 TO 9 IF keyval(sc1 - 1 + i) > 1 THEN n = n * 10 + i typed = YES END IF NEXT IF keyval(sc0) > 1 THEN n = n * 10 typed = YES END IF END IF ' However, allow Shift while using numpad keys: under gfx_fb on X11, ' numpad keys aren't converted to Left, End, etc IF shift XOR numlock FOR i as integer = 0 TO UBOUND(numpad_nums) IF keyval(scNumpad7 + i) > 1 THEN IF numpad_nums(i) > -1 THEN n = n * 10 + numpad_nums(i) typed = YES END IF END IF NEXT ' Handle negatives IF old = 0 ANDALSO n <> 0 ANDALSO negative_zero THEN sign = -1 IF min < 0 AND max > 0 THEN IF keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 THEN IF n = 0 THEN negative_zero = YES ELSE sign = sign * -1 typed = YES END IF END IF IF (keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1) AND sign < 0 THEN sign = sign * -1 typed = YES END IF END IF IF sign < 0 OR (min < 0 AND max = 0) THEN n = -n 'CLIPBOARD IF use_clipboard THEN IF copy_keychord() THEN clip = n IF paste_keychord() THEN n = clip typed = YES END IF END IF n = bound(n, min, max) END IF IF typed = NO AND autoclamp = NO THEN n = old IF typed = YES THEN negative_zero = NO IF returninput THEN RETURN typed ELSE RETURN (old <> n) END IF END FUNCTION FUNCTION zintgrabber (byref n as integer, min as integer, max as integer, less as integer=scLeft, more as integer=scRight) as bool '--adjust for entries that are offset by +1 '--what a hack! '--all entries <= 0 are special options not meant to be enumerated '--supply the min & max as visible, not actual range for n '--eg a menu with 'A' = -2, 'B' = -1, 'C' = 0, 'item 0 - item 99' = 1 - 100 would have min = -3, max = 99 DIM old as integer = n DIM temp as integer = n - 1 '--must adjust to always be able to type in a number IF temp < 0 THEN FOR i as integer = 2 TO 11 IF keyval(i) > 1 THEN temp = 0 NEXT i END IF intgrabber temp, min, max, less, more n = temp + 1 IF old = 1 AND keyval(scBackspace) > 1 THEN n = 0 RETURN (old <> n) END FUNCTION FUNCTION xintgrabber (byref n as integer, pmin as integer, pmax as integer, nmin as integer=1, nmax as integer=1, less as integer=scLeft, more as integer=scRight) as integer '--quite a bit of documentation required: '--like zintgrabber, but for cases where positive values mean one thing, negatives '--another, and 0 means none. 'Requirements: nmax <= nmin <= 0 <= pmin <= pmax 'Omit nmax and nmin for no negative range 'nmin to nmax is the visible range of negative values 'eg. nmin = -1 nmax = -100: negatives indicate a number between 1 and 100 'pmin to pmax is positive range 'eg. 2 - 50 means n==1 is '2', n==49 is '50', and 0 - 1 means n==1 is '0' and n==2 is '1' DIM old as integer = n 'calculate the range of n DIM as integer valmin, valmax IF nmin <> 1 THEN valmin = -1 + (nmax - nmin) END IF valmax = 1 + (pmax - pmin) 'calculate the visible value DIM as integer visval, oldvisval IF n > 0 THEN visval = n + pmin - 1 ELSEIF n < 0 THEN visval = n + nmin + 1 ELSE visval = 0 END IF oldvisval = visval IF more <> 0 ANDALSO keyval(more) > 1 THEN 'easy case n = loopvar(n, valmin, valmax, 1) ELSEIF less <> 0 ANDALSO keyval(less) > 1 THEN 'easy case n = loopvar(n, valmin, valmax, -1) /'--Why on earth do we want to support negation anyway? ELSEIF nmin < 0 AND pmax > 0 AND _ (keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 OR _ ((keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1) AND s < 0)) THEN 'nasty case: negate n based on *displayed* value visval = bound(-visval, nmax, pmax) n = ... '/ ELSE 'horrible case: change n based on *displayed* value visval = ABS(visval) IF keyval(scBackspace) > 1 THEN visval \= 10 'Special case for when backspace changes to None. Isolate this case to allow 'some sanity in the rest of the logic IF (oldvisval = 0) OR (n > 0 AND visval < pmin) OR (n < 0 AND -visval > nmin) THEN n = 0 RETURN YES END IF ELSE FOR i as integer = 1 TO 9 IF keyval(i - 1 + sc1) > 1 THEN visval = visval * 10 + i NEXT i IF keyval(sc0) > 1 THEN visval *= 10 END IF 'convert absolute visval back to n 'None can become positive, but positive remains positive and negative remains negative IF old = 0 THEN IF visval <> oldvisval THEN visval = bound(visval, pmin, pmax) n = visval - pmin + 1 END IF ELSEIF old > 0 THEN visval = bound(visval, pmin, pmax) n = visval - pmin + 1 ELSE visval = bound(-visval, nmax, nmin) n = visval - nmin - 1 END IF END IF RETURN (old <> n) END FUNCTION ' Handle all key combinations for deleting a character or word forward or backwards: ' Backspace, Delete, Alt/Ctrl-Backspace/Delete ' On Mac, Option- (which is Alt) Backspace/Delete are used instead of Ctrl-Backspace/Delete ' so we support that too. (Note Backspace and Delete are called Delete and Forward Delete on Mac). ' pre is the part of the text before the insert point, and post is after. PRIVATE SUB stredit_delete_keys(pre as string, post as string) IF keyval(scCtrl) > 0 OR keyval(scAlt) > 0 THEN DIM spacepos as integer ' Delete prev word IF keyval(scBackspace) > 1 THEN ' First remove any whitespace at the end pre = RTRIM(pre, ANY !" \n") ' Find whitespace character before the last word, but don't delete it spacepos = INSTRREV(pre, ANY !" \n", -1) '-1: search from end pre = LEFT(pre, spacepos) END IF ' Delete next word IF keyval(scDelete) > 1 THEN ' First remove any whitespace post = LTRIM(post, ANY !" \n") ' Find whitespace character after the next word, but don't delete it spacepos = INSTR(post, ANY !" \n") post = MID(post, spacepos) END IF ELSE IF keyval(scBackspace) > 1 THEN pre = LEFT(pre, LEN(pre) - 1) END IF IF keyval(scDelete) > 1 THEN post = RIGHT(post, LEN(post) - 1) END IF END IF END SUB 'Handle copy/paste edits to 'text'. Attempt to use the OS clipboard if possible, 'otherwise use 'clip' as the clipboard. SUB handle_text_copy_paste (byref text as string, byref clip as string) IF copy_keychord() THEN clip = text io_set_clipboard_text(text) 'Don't need ohr_to_utf8 END IF IF paste_keychord() THEN DIM osclip as zstring ptr 'ustring osclip = io_get_clipboard_text() 'Intentionally using utf8_to_latin1 instead of utf8_to_ohr here, so that 'round trips of text from OHR -> clipboard/other programs -> OHR don't lose 'icon characters. IF LEN(*osclip) THEN clip = utf8_to_latin1(*osclip) DEALLOCATE osclip text = clip END IF END SUB 'Returns true if the string has changed FUNCTION strgrabber (text as string, maxl as integer) as bool STATIC clip as string DIM original as string = text stredit_delete_keys text, "" #IFDEF IS_CUSTOM '--copy+paste support handle_text_copy_paste text, clip text = LEFT(text, maxl) #ENDIF '--adding chars IF LEN(text) < maxl THEN #IFDEF IS_CUSTOM IF keyval(scSpace) > 1 AND keyval(scCtrl) > 0 THEN '--charlist support text &= charpicker() END IF #ENDIF 'Note: never returns newlines; and we don't check either text = LEFT(text + getinputtext, maxl) END IF RETURN (text <> original) END FUNCTION FUNCTION stredit (s as string, byref insert as integer, maxl as integer, numlines as integer = 1, line_length as integer = 1) as integer 'Return value is the line that the cursor is on, or 0 if numlines=1 'insert is the 0-based position of the cursor (range 0..LEN(s)-1), and is modified byref. Set to -1 to move automatically to end of string 'maxl is maximum length in characters. 'numlines is number of lines visible. If > 1 then wrapping and newlines are enabled. 'line_length is number of characters on a line (for wrapping) stredit = 0 STATIC clip as string '--copy+paste support handle_text_copy_paste s, clip s = LEFT(s, maxl) IF insert < 0 THEN insert = LEN(s) insert = bound(insert, 0, LEN(s)) '--insert cursor movement ' (Mac uses Option (ie ALT) instead of CTRL for moving by word, so accept that too. ' Doesn't use Option-Home/End though.) IF keyval(scCtrl) = 0 AND keyval(scAlt) = 0 THEN IF keyval(scLeft) > 1 THEN insert = large(0, insert - 1) IF keyval(scRight) > 1 THEN insert = small(LEN(s), insert + 1) ELSE 'CTRL IF keyval(scLeft) > 1 THEN 'move by word IF insert > 0 THEN 'searching from position -1 searches from the end insert = INSTRREV(s, ANY !" \n", insert - 1) 'different argument order: the FB devs, they are so funny END IF END IF IF keyval(scRight) > 1 THEN insert = INSTR(insert + 1, s, ANY !" \n") IF insert = 0 THEN insert = LEN(s) END IF IF keyval(scHome) > 1 THEN insert = 0 IF keyval(scEnd) > 1 THEN insert = LEN(s) END IF '--up and down arrow keys IF numlines > 1 THEN DIM wrapped as string wrapped = wordwrap(s, large(1, line_length)) DIM lines() as string split(wrapped, lines()) DIM line_starts() as integer split_line_positions s, lines(), line_starts() DIM insert_line as integer = -1 DIM offset_in_line as integer '0-based DIM move_lines as integer = 0 FOR linenum as integer = 0 TO UBOUND(lines) DIM next_line as integer = IIF(linenum = UBOUND(lines), len(s) + 1, line_starts(linenum + 1)) IF next_line > insert THEN insert_line = linenum offset_in_line = insert - line_starts(linenum) EXIT FOR END IF NEXT linenum IF insert_line = -1 THEN debugc errPromptBug, "Ignorable engine bug: stredit couldn't find line" END IF IF keyval(scUp) > 1 THEN move_lines = -1 IF keyval(scDown) > 1 THEN move_lines = 1 IF keyval(scPageUp) > 1 THEN move_lines = -(numlines - 2) IF keyval(scPageDown) > 1 THEN move_lines = numlines - 2 move_lines += readmouse.wheel_clicks * 3 IF move_lines THEN insert_line = bound(insert_line + move_lines, 0, UBOUND(lines) + 1) IF insert_line = UBOUND(lines) + 1 THEN 'Trying to move past last line: move to its end instead insert = LEN(s) ELSE insert = line_starts(insert_line) + small(offset_in_line, LEN(lines(insert_line))) END IF END IF '--set return value stredit = insert_line '--end of special handling for up and down motion '--Home and end keys: go to previous/next newline, '--unless Ctrl is pressed, which is handled above IF keyval(scCtrl) = 0 THEN IF keyval(scHome) > 1 THEN IF insert > 0 THEN 'searching from position -1 searches from the end insert = INSTRREV(s, CHR(10), insert - 1) END IF END IF IF keyval(scEnd) > 1 THEN insert = INSTR(insert + 1, s, CHR(10)) IF insert = 0 THEN insert = LEN(s) ELSE insert -= 1 END IF END IF '--end of special keys that only work in multiline mode END IF DIM pre as string = LEFT(s, insert) DIM post as string = RIGHT(s, LEN(s) - insert) '--BACKSPACE, DELETE, ALT/CTRL+BACKSPACE/DELETE stredit_delete_keys pre, post '--adding chars IF LEN(pre) + LEN(post) < maxl THEN IF keyval(scSpace) > 1 AND keyval(scCtrl) > 0 THEN #IFDEF IS_CUSTOM '--charlist support pre &= charpicker() #ENDIF ELSEIF keyval(scAnyEnter) > 1 THEN IF numlines > 1 THEN pre &= CHR(10) END IF ELSE pre &= getinputtext END IF END IF insert = LEN(pre) s = LEFT(pre & post, maxl) END FUNCTION SUB pop_warning(s as string, byval autoquit as integer = NO) 'autoquit is specifically for cleanup_and_terminate and has no other use '--Construct the warning UI (This will be hella easier later when the Slice Editor can save/load) DIM root as Slice Ptr root = NewSliceOfType(slContainer) WITH *root .Fill = YES END WITH DIM outer_box as Slice Ptr outer_box = NewSliceOfType(slContainer, root) WITH *outer_box ' .paddingTop = 20 ' .paddingBottom = 20 ' .paddingLeft = 20 ' .paddingRight = 20 ' .Fill = YES .Y = 200 .AlignHoriz = alignCenter .AlignVert = alignCenter .AnchorHoriz = alignCenter .AnchorVert = alignCenter .Width = small(320 - 40, get_resolution().w) .Height = small(200 - 40, get_resolution().h) END WITH DIM inner_box as Slice Ptr inner_box = NewSliceOfType(slRectangle, outer_box) WITH *inner_box .paddingTop = 8 .paddingBottom = 8 .paddingLeft = 8 .paddingRight = 8 .Fill = YES ChangeRectangleSlice inner_box, 2 END WITH DIM text_area as Slice Ptr text_area = NewSliceOfType(slText, inner_box) WITH *text_area .Fill = YES ChangeTextSlice text_area, s, , , YES END WITH DIM animate as Slice Ptr animate = outer_box '--Preserve whatever screen was already showing as a background DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen copypage vpage, dpage DIM dat as TextSliceData Ptr dat = text_area->SliceData dat->line_limit = 15 DIM deadkeys as integer = 15 DIM scrollbar_state as MenuState scrollbar_state.size = 16 '--Now loop displaying text setkeys DO setwait 17 setkeys IF autoquit THEN DIM winstate as WindowState ptr = gfx_getwindowstate() IF winstate andalso winstate->focused = NO THEN EXIT DO END IF IF deadkeys = 0 THEN IF keyval(scESC) > 1 OR enter_or_space() OR click_dismiss() THEN EXIT DO IF keyval(scUp) > 1 THEN dat->first_line -= 1 IF keyval(scDown) > 1 THEN dat->first_line += 1 dat->first_line = bound(dat->first_line, 0, large(0, dat->line_count - dat->line_limit)) END IF deadkeys = large(deadkeys - 1, 0) 'Animate the arrival of the pop-up animate->Y = large(animate->Y - vpages(vpage)->h \ 15, 0) DrawSlice root, dpage WITH scrollbar_state .top = dat->first_line .last = dat->line_count - 1 END WITH draw_fullscreen_scrollbar scrollbar_state, , dpage SWAP vpage, dpage setvispage vpage copypage holdscreen, dpage dowait LOOP '--Animate the removal of the screen DO setkeys setwait 17 animate->Y = animate->Y + vpages(vpage)->h \ 15 IF animate->Y > 200 THEN EXIT DO DrawSlice root, dpage SWAP vpage, dpage setvispage vpage copypage holdscreen, dpage dowait LOOP freepage holdscreen DeleteSlice @root END SUB FUNCTION prompt_for_string (byref s as string, caption as string, byval limit as integer=NO) as integer '--Construct the prompt UI. FIXME: redo this when the Slice Editor can save/load) DIM root as Slice Ptr root = NewSliceOfType(slContainer) root->Fill = YES DIM outer_box as Slice Ptr outer_box = NewSliceOfType(slRectangle, root) WITH *outer_box .AnchorHoriz = 1 .AnchorVert = 1 .AlignHoriz = 1 .AlignVert = 1 .paddingTop = 16 .paddingBottom = 16 .paddingLeft = 16 .paddingRight = 16 .Width = 300 .Height = 64 END WITH ChangeRectangleSlice outer_box, 1 DIM caption_area as Slice Ptr caption_area = NewSliceOfType(slText, outer_box) ChangeTextSlice caption_area, caption, uilook(uiText) DIM inner_box as Slice Ptr inner_box = NewSliceOfType(slContainer, outer_box) WITH *inner_box .paddingTop = 16 .Fill = YES END WITH DIM text_border_box As Slice Ptr text_border_box = NewSliceOfType(slRectangle, inner_box) WITH *text_border_box .paddingTop = 2 .paddingBottom = 2 .paddingLeft = 2 .paddingRight = 2 .Fill = YES END WITH ChangeRectangleSlice text_border_box, , uilook(uiOutline), uilook(uiText) DIM text_area as Slice Ptr text_area = NewSliceOfType(slText, text_border_box) WITH *text_area .Fill = YES END WITH ChangeTextSlice text_area, s, uilook(uiMenuItem), , , uilook(uiOutline) '--Preserve whatever screen was already showing as a background DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen DIM dat as TextSliceData Ptr dat = text_area->SliceData IF limit = NO THEN limit = 40 '--Now loop while editing string setkeys YES DO setwait 40 setkeys YES IF keyval(scESC) > 1 THEN prompt_for_string = NO EXIT DO END IF IF keyval(scAnyEnter) > 1 THEN prompt_for_string = YES s = dat->s EXIT DO END IF strgrabber dat->s, limit copypage holdscreen, dpage DrawSlice root, dpage SWAP vpage, dpage setvispage vpage dowait LOOP setkeys freepage holdscreen DeleteSlice @root END FUNCTION PRIVATE FUNCTION _highlight_text(original as string, arg as any ptr) as string DIM col as integer = *CAST(integer ptr, arg) RETURN bgtag(col, original) END FUNCTION 'Returns text with highlighting applied to all matches of searchstring FUNCTION highlight_for_incremental_search(text as string, searchstring as string, charnum as integer, byref search_num_matches as integer) as string IF LEN(searchstring) = 0 THEN RETURN text DIM ret as string = text DIM col as integer = findrgb(150, 150, 0) DIM at_cursor as string = MID(ret, charnum, LEN(searchstring)) IF LCASE(at_cursor) = LCASE(searchstring) THEN 'Highlight the selected match, so if you press Ctrl+S you can see it advance ret = MID(ret, 1, charnum - 1) + fgtag(uilook(uiSelectedItem), at_cursor) _ + MID(ret, charnum + LEN(at_cursor)) END IF search_num_matches = replacestr(ret, searchstring, @_highlight_text, @col, , YES) RETURN ret END FUNCTION 'Advances charnum (the cursor position) to a match of searchstring, and sets scroll_to_y. 'advance_dir is -1 or 1 to search backwards/forwards, or 0 to find next match 'but not advancing if already at match SUB advance_incremental_search(text as string, searchstring as string, byref charnum as integer, advance_dir as integer, byref scroll_to_y as integer, sl as Slice ptr) DIM match as integer IF advance_dir = -1 THEN 'First look backwards match = INSTRREV(LCASE(text), LCASE(searchstring), charnum + advance_dir) IF match = 0 THEN 'Otherwise from end match = INSTRREV(LCASE(text), LCASE(searchstring)) END IF ELSE 'First look forwards match = INSTR(charnum + advance_dir, LCASE(text), LCASE(searchstring)) IF match = 0 THEN 'Otherwise from start match = INSTR(LCASE(text), LCASE(searchstring)) END IF END IF IF match = 0 THEN EXIT SUB charnum = match 'DIM charpos as XYPair = TextSliceCharPos(sl, charnum) DIM charpos as StringCharPos find_text_char_position(@charpos, text, charnum, sl->Width, fontPlain) scroll_to_y = charpos.pos.y END SUB SUB show_help(helpkey as string) DIM original_text as string original_text = load_help_file(helpkey) DIM help_str as string = original_text 'The edited version '--Construct the help UI (This will be hella easier later when the Slice Editor can save/load) DIM help_root as Slice Ptr help_root = NewSliceOfType(slContainer) WITH *help_root .Y = vpages(vpage)->h .Fill = NO END WITH DIM help_outer_box as Slice Ptr help_outer_box = NewSliceOfType(slContainer, help_root) WITH *help_outer_box .paddingTop = 4 .paddingBottom = 4 .paddingLeft = 4 .paddingRight = 4 .Fill = Yes END WITH DIM help_box as Slice Ptr help_box = NewSliceOfType(slRectangle, help_outer_box) WITH *help_box .paddingTop = 8 .paddingBottom = 8 .paddingLeft = 8 '.paddingRight = 8 .Fill = YES .Clip = YES ChangeRectangleSlice help_box, 1 END WITH DIM footer_text2 as Slice Ptr footer_text2 = NewSliceOfType(slText, help_outer_box) WITH *footer_text2 .AlignVert = alignBottom .AlignHoriz = alignRight .AnchorHoriz = alignRight .Y = -4 .X = -3 END WITH ChangeTextSlice footer_text2, , uilook(uiMenuItem), NO, , boxlook(1).bgcol 'outline=NO DIM footer_text as Slice Ptr footer_text = NewSliceOfType(slText, help_outer_box) WITH *footer_text .AlignVert = alignBottom .X = 3 .Y = -4 END WITH ChangeTextSlice footer_text, , uilook(uiMenuItem), NO, , boxlook(1).bgcol 'outline=NO CAST(TextSliceData ptr, footer_text->SliceData)->use_render_text = YES 'Fills the clipping area DIM clip_box as Slice Ptr clip_box = NewSliceOfType(slContainer, help_box) WITH *clip_box .paddingRight = 4 .Fill = YES END WITH DIM help_draw_area as Slice Ptr help_draw_area = NewSliceOfType(slScroll, clip_box) WITH *help_draw_area .paddingRight = 4 .Clip = NO .Fill = YES END WITH DIM help_text as Slice Ptr help_text = NewSliceOfType(slText, help_draw_area) WITH *help_text .Fill = YES .FillMode = sliceFillHoriz CAST(TextSliceData ptr, .SliceData)->use_render_text = YES ChangeTextSlice help_text, help_str, , , YES 'wrap=YES END WITH DIM animate as Slice Ptr animate = help_root '--Preserve whatever screen was already showing as a background DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen copypage vpage, dpage DIM dat as TextSliceData Ptr dat = help_text->SliceData dat->insert = 0 DIM editing as bool = NO DIM deadkeys as integer = 25 DIM cursor_line as integer = 0 DIM searchstring as string DIM default_footer as string = "Type to search" DIM footerstring as string = default_footer DIM footerstring2 as string DIM search_num_matches as integer '--Now loop displaying help DIM prev_mouse_vis as CursorVisibility = getcursorvisibility() showmousecursor ensure_normal_palette setkeys YES DO setwait 17 setkeys YES 'The necessity of this would be eliminated by using a root slice set to fill (filling the screen slice) UpdateRootSliceSize(help_root) DIM scroll_to_y as integer = -1 IF editing THEN DIM numlines as integer = help_draw_area->Height \ 10 + 1 cursor_line = stredit(help_str, dat->insert, 32767, numlines, help_text->Width \ 8) 'The limit of 32767 chars is totally arbitrary and maybe not a good limit ChangeTextSlice help_text, help_str END IF IF deadkeys = 0 THEN IF keyval(scESC) > 1 AND (editing OR searchstring = "") THEN '--If there are any changes to the help screen, offer to save them IF original_text = help_str THEN EXIT DO ELSE 'Prevent attempt to quit the program, stop and wait for response first DIM quitting as bool = getquitflag() setquitflag NO DIM choice as integer = twochoice("Save changes to help for """ & helpkey & """?", "Yes", "No", 0, -1) IF getquitflag() THEN choice = 1 'Second attempt to quit: discard IF choice <> -1 THEN IF quitting THEN setquitflag IF choice = 0 THEN save_help_file helpkey, dat->s EXIT DO END IF END IF END IF IF editing THEN IF keyval(scCtrl) > 0 AND keyval(scS) > 1 THEN 'Because while editing we don't use_render_text so can't do text highlighting, 'so use this old search implementation IF prompt_for_string(searchstring, "Search") THEN DIM idx as integer = INSTR(dat->insert + 2, LCASE(dat->s), LCASE(searchstring)) IF idx = 0 THEN 'wrap idx = INSTR(dat->s, searchstring) END IF IF idx THEN dat->insert = idx - 1 END IF END IF ELSE 'Fancy new incremental search DIM advance_dir as integer = -2 DIM update as bool = NO IF keyval(scESC) > 1 THEN searchstring = "" : advance_dir = 0 'Forward/backward search DIM temp as integer = find_next_or_prev_keychord() IF temp <> 0 THEN advance_dir = temp IF keyval(scCtrl) = 0 AND (keyval(scLeftCommand) OR keyval(scRightCommand)) = 0 THEN 'On input, advance to next match if not already at it IF strgrabber(searchstring, 60) THEN advance_dir = 0 END IF IF advance_dir <> -2 THEN 'ChangeTextSlice help_text, help_str 'So that TextSliceCharPos() returns the correct result advance_incremental_search(help_str, searchstring, dat->insert, advance_dir, scroll_to_y, help_text) DIM highlighted as string highlighted = highlight_for_incremental_search(help_str, searchstring, dat->insert, search_num_matches) ChangeTextSlice help_text, highlighted IF LEN(searchstring) THEN footerstring = "Search: " & fgtag(uilook(uiText), bgtag(findrgb(150, 150, 0), searchstring)) footerstring2 = "Ctrl+S: next ("& search_num_matches & " matches)" ELSE footerstring = default_footer footerstring2 = "" END IF END IF END IF IF keyval(scCtrl) > 0 AND keyval(scE) > 1 THEN IF diriswriteable(get_help_dir()) THEN editing = YES 'Displaying the cursor doesn't work properly with use_render_text :( CAST(TextSliceData ptr, help_text->SliceData)->use_render_text = NO ChangeTextSlice help_text, help_str 'Hide footer help_outer_box->paddingBottom = 4 footer_text->Visible = NO footer_text2->Visible = NO 'Kludge: insert is actually 0-based offset, but incremental search use 1-based offset IF dat->insert THEN dat->insert -= 1 dat->show_insert = YES ChangeRectangleSlice help_box, , uilook(uiBackground), , 0 ELSE ' It would be possible to just automatically makedir($documents_dir/ohrhelp), and warn the user that it happened pop_warning "Your """ & get_help_dir() & """ folder is not writable. Try making a copy of it at """ & documents_dir & SLASH & "ohrhelp""" END IF END IF IF keyval(scF1) and helpkey <> "share_helphelp" THEN show_help "share_helphelp" END IF 'Update scroll position (help_text->Y) 'Decreasing Y to scroll down is confusing... DIM top as integer = -help_text->Y DIM maxy as integer = large(0, (help_text->Height - help_draw_area->Height)) IF editing THEN 'Scroll to cursor_line 'TODO: use text_layout_dimensions to correctly calculate the Y position of a line. 'However stredit does its own word wrapping assuming a 8x8 font, no markup, etc, 'to calculate the text position, so will need rewriting in future :( scroll_to_y = cursor_line * 10 /' Move cursor to location of mouse click. Doesn't work; find_point_in_text is broken. IF readmouse.buttons AND mouseLeft THEN DIM curspos as StringCharPos DIM mpoint as XYPair = readmouse.pos - help_text->ScreenPos find_point_in_text @curspos, mpoint.x, mpoint.y, dat->s, help_text->Width, help_text->ScreenX, help_text->ScreenY, 0, YES, YES dat->insert = curspos.charnum END IF '/ ELSE '--not editing, just browsing IF slowkey(scUp, 30) THEN top -= 8 IF slowkey(scDown, 30) THEN top += 8 IF keyval(scPageUp) > 1 THEN top -= help_draw_area->Height - 12 IF keyval(scPageDown) > 1 THEN top += help_draw_area->Height - 12 IF keyval(scHome) > 1 THEN top = 0 IF keyval(scEnd) > 1 THEN top = maxy top += readmouse.wheel_delta \ 4 END IF IF scroll_to_y <> -1 THEN top = small(top, scroll_to_y - 20) top = large(top, scroll_to_y + 30 - help_draw_area->Height) END IF help_text->Y = -bound(top, 0, maxy) END IF deadkeys = large(deadkeys - 1, 0) 'Animate the arrival of the help screen animate->Y = large(animate->Y - vpages(vpage)->h \ 15, 0) copypage holdscreen, vpage ChangeTextSlice footer_text, footerstring ChangeTextSlice footer_text2, footerstring2 DrawSlice help_root, vpage setvispage vpage dowait LOOP '--Animate the removal of the help screen DO setkeys setwait 17 animate->Y = animate->Y + vpages(vpage)->h \ 15 IF animate->Y > vpages(vpage)->h THEN EXIT DO copypage holdscreen, vpage DrawSlice help_root, vpage setvispage vpage dowait LOOP setcursorvisibility(prev_mouse_vis) restore_previous_palette freepage holdscreen DeleteSlice @help_root END SUB FUNCTION multiline_string_editor(s as string, helpkey as string="", prompt_to_save as bool = YES) as string 'probably contains more code duplication than is apropriate when comared to the help_editor '--Construct the UI (loading a slice collection might be better here? but not from the RPG file!) DIM root as Slice Ptr root = NewSliceOfType(slContainer) WITH *root 'TODO: this doesn't handle changes to window size. Have a look at 'how pop_warning handles this instead. .Y = 200 .Fill = NO .Width = get_resolution().w .Height = get_resolution().h END WITH DIM outer_box as Slice Ptr outer_box = NewSliceOfType(slContainer, root) WITH *outer_box .paddingTop = 4 .paddingBottom = 4 .paddingLeft = 4 .paddingRight = 4 .Fill = YES END WITH DIM box as Slice Ptr box = NewSliceOfType(slRectangle, outer_box) WITH *box .paddingTop = 8 .paddingBottom = 8 .paddingLeft = 8 .paddingRight = 8 .Fill = YES ChangeRectangleSlice box, , uilook(uiBackground), , 0 END WITH DIM text as Slice Ptr text = NewSliceOfType(slText, box) WITH *text .Fill = YES ChangeTextSlice text, s, , , YES END WITH DIM animate as Slice Ptr animate = root '--Preserve whatever screen was already showing as a background DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen copypage vpage, dpage DIM dat as TextSliceData Ptr dat = text->SliceData dat->line_limit = 18 dat->insert = 0 dat->show_insert = YES DIM deadkeys as integer = 25 DIM cursor_line as integer = 0 DIM scrollbar_state as MenuState scrollbar_state.size = 17 '--Now loop displaying help setkeys YES DO setwait 30 setkeys YES DIM textstring as string = dat->s cursor_line = stredit(textstring, dat->insert, 32767, dat->line_limit, text->Width \ 8) 'The limit of 32767 chars is totally arbitrary and maybe not a good limit ChangeTextSlice text, textstring IF keyval(scESC) > 1 THEN '--If there are any changes to the help screen, offer to save them IF s = dat->s THEN EXIT DO ELSEIF prompt_to_save THEN DIM choice as integer = twochoice("Keep changes to this text?", "Yes", "No", 0, -1) IF choice = 1 THEN dat->s = s '--don't use changes! IF choice >= 0 THEN EXIT DO ELSE EXIT DO END IF END IF IF deadkeys = 0 THEN IF keyval(scF1) ANDALSO helpkey <> "" THEN show_help helpkey IF keyval(scF2) THEN export_string_to_file dat->s IF keyval(scF3) THEN import_string_from_file dat->s dat->first_line = small(dat->first_line, cursor_line - 1) dat->first_line = large(dat->first_line, cursor_line - (dat->line_limit - 2)) dat->first_line = bound(dat->first_line, 0, large(0, dat->line_count - dat->line_limit)) END IF deadkeys = large(deadkeys -1, 0) 'Animate the arrival of the help screen animate->Y = large(animate->Y - 20, 0) copypage holdscreen, vpage DrawSlice root, vpage WITH scrollbar_state .top = dat->first_line .last = dat->line_count - 1 END WITH draw_fullscreen_scrollbar scrollbar_state, , vpage setvispage vpage dowait LOOP '--Animate the removal of the multiline text editor DO setkeys setwait 17 animate->Y = animate->Y + 20 IF animate->Y > 200 THEN EXIT DO copypage holdscreen, vpage DrawSlice root, vpage setvispage vpage dowait LOOP DIM result as string result = dat->s freepage holdscreen DeleteSlice @root RETURN result END FUNCTION SUB export_string_to_file(s as string) DIM filename as string filename = inputfilename("Name of text file to export to?", ".txt", "", "export_string_to_file") IF filename = "" THEN RETURN filename &= ".txt" IF isfile(filename) THEN IF yesno("""" & decode_filename(filename) & """ already exists. Do you want to overwrite it?", NO, NO) = NO THEN RETURN END IF IF NOT fileiswriteable(filename) THEN visible_debug "File """ & decode_filename(filename) & """ is not writeable" : RETURN string_to_file s, filename visible_debug "Successfully wrote the text to """ & decode_filename(filename) & """" END SUB SUB import_string_from_file(s as string) DIM prefix as string = "" IF s <> "" THEN SELECT CASE twochoice("Do you want to append to the text or replace it?", "Append", "Replace", 0, -1, "import_string_from_file_append") CASE 0: 'append prefix = s & !"\n" CASE 1: 'replace CASE ELSE 'cancel RETURN END SELECT END IF DIM filename as string filename = browse(0, "", "*.txt", "import_string_from_file") IF filename = "" THEN RETURN s = prefix & string_from_file(filename) END SUB 'Show a message box and a multichoice menu below it, return option number. 'Preserves the contents of the screen. 'centerlines: whether to center-justify everything, otherwise left-justified. 'extra_message: shown at the bottom of the screen. FUNCTION multichoice(capt as string, choices() as string, defaultval as integer=0, escval as integer=-1, helpkey as string="", centerlines as bool=YES, extra_message as string = "") as integer DIM state as MenuState DIM menu as MenuDef ClearMenuData menu DIM result as integer DIM captlines() as string DIM maxwidth as integer 'max possible line length, in characters DIM wide as integer 'max line length, in pixels 'TODO: convert to text slices. This is ugly and still can't handle window resizing maxwidth = (vpages(vpage)->w - 24) \ 8 split(wordwrap(capt, maxwidth), captlines()) FOR i as integer = 0 TO UBOUND(captlines) wide = large(wide, textwidth(captlines(i))) NEXT FOR i as integer = 0 TO UBOUND(choices) append_menu_item menu, choices(i) NEXT DIM boxy as integer 'Y of box top, relative to screen centre DIM boxheight as integer = 10 * (UBOUND(captlines) + 1) boxy = large(-20 - boxheight \ 2 - 5 * UBOUND(choices), -vpages(vpage)->h \ 2 + 10) menu.offset.Y = boxy + boxheight + 5 menu.anchorvert = alignTop menu.maxrows = (vpages(vpage)->h \ 2 - menu.offset.Y - 20) \ 10 menu.withtags = YES state.active = YES init_menu_state state, menu state.pt = defaultval DIM prev_mouse_vis as CursorVisibility = getcursorvisibility() showmousecursor force_use_mouse += 1 ensure_normal_palette 'Keep whatever was on the screen already as a background (NOTE: this doesn't always work (not necessarily vpage)) DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen setkeys DO setwait 55 setkeys IF keyval(scEsc) > 1 THEN result = escval state.active = NO END IF IF keyval(scF1) > 1 ANDALSO LEN(helpkey) > 0 THEN show_help helpkey END IF IF enter_space_click(state) THEN result = state.pt state.active = NO END IF IF state.active = NO THEN EXIT DO usemenu state copypage holdscreen, vpage centerbox rCenter, rCenter + boxy + boxheight \ 2, 16 + wide, 6 + boxheight, 2, vpage FOR i as integer = 0 TO UBOUND(captlines) DIM linex as RelPos IF centerlines THEN linex = pCentered ELSE linex = rCenter - wide \ 2 END IF edgeprint captlines(i), linex, rCenter + boxy + i * 10, uilook(uiMenuItem), vpage NEXT draw_menu menu, state, vpage IF LEN(helpkey) > 0 THEN edgeprint "F1 Help", pRight, pBottom, uilook(uiMenuItem), vpage END IF edgeprint extra_message, pLeft, pBottom, uilook(uiMenuItem), vpage, YES, YES setvispage vpage dowait LOOP setkeys restore_previous_palette setcursorvisibility(prev_mouse_vis) force_use_mouse -= 1 copypage holdscreen, vpage freepage holdscreen ClearMenuData menu RETURN result END FUNCTION FUNCTION twochoice(capt as string, strA as string="Yes", strB as string="No", byval defaultval as integer=0, byval escval as integer=-1, helpkey as string="") as integer DIM choices(1) as string = {strA, strB} RETURN multichoice(capt, choices(), defaultval, escval, helpkey) END FUNCTION 'Asks a yes-or-no pop-up question. Less flexible than twochoice, since it only returns a bool. '(Not to be confused with yesorno(), which returns a yes/no string) FUNCTION yesno(capt as string, byval defaultval as bool=YES, byval escval as bool=NO) as bool IF defaultval THEN defaultval = 0 ELSE defaultval = 1 IF escval THEN escval = 0 ELSE escval = 1 DIM result as integer result = twochoice(capt, "Yes", "No", defaultval, escval) IF result = 0 THEN RETURN YES IF result = 1 THEN RETURN NO END FUNCTION SUB playsongnum (byval songnum as integer) DIM songfile as string = find_music_lump(songnum) IF songfile = "" THEN EXIT SUB loadsong songfile END SUB ' Open a URL using default system handler, which must start with a protocol like ' http:// or https://. ' Would return NO to indicate failure (no web browser installed), except we can't ' detect failure on most platforms. Definitely no error for a bad URL. FUNCTION open_url (url as string) as bool DIM cmd as string DIM result as string #IFDEF __FB_DARWIN__ safe_shell "open '" & url & "'" 'Don't have a way of detecting if that worked RETURN YES #ENDIF #IFDEF __FB_WIN32__ ' safe_shell "START """ & url & """" ' 'Well, Dang, I don't know how to check this for success either! ' RETURN YES 'Alternative way to open an HTML using winapi call, which doesn't involve 'running cmd.exe, and returns whether it succeeds, but is untested result = open_document(url) IF LEN(result) = 0 THEN RETURN YES visible_debug "Couldn't open " & url & ": " & result RETURN NO #ENDIF '--Try the unix way safe_shell "xdg-open '" & url & "'" '-Unfortunately we can't test if it worked, so just pretend that it did. RETURN YES END FUNCTION FUNCTION spawn_and_wait (app as string, args as string) as string 'Run a commandline program in a terminal emulator and wait for it to finish. 'On Windows the program is run asynchronously and users are offered the option to kill it. 'On other platforms the program just freezes. 'You can of course also kill the program on all platforms with Ctrl+C 'app should be a path to an executable (not escaped), while args should be escaped as needed. 'Returns an error message, or "" if no apparent failure 'NOTE: you should display the error with visible_debug, because this function doesn't write 'it to the debug log (TODO: change that) 'It may be better to pass arguments in an array (the Unix way), so that 'we can do all the necessary quoting required for Windows here. debuginfo "spawn_and_wait " & escape_filename(app) + " " + args IF gfxbackend = "console" THEN CLS safe_shell escape_filename(app) & " " & args 'Sync actual and backend-internal screens CLS clearpage vpage setvispage vpage, NO RETURN "" END IF #IF defined(__FB_DARWIN__) basic_textbox "Please wait, running " & trimpath(app), uilook(uiText), vpage setvispage vpage, NO 'Wow that's a crazy amount of indirection! 'Running Terminal.app is the only way to get a terminal, but 'open' is for opening a file with an application only, 'so we use an AppleScript script embedded in Terminal_wrapper.sh to run HSpeak DIM term_wrap as string = find_helper_app("Terminal_wrapper.sh") IF term_wrap = "" THEN RETURN missing_helper_message("Terminal_wrapper.sh") waitforkeyrelease DIM fh as integer DIM dummyscript as string = tmpdir + "dummyscript" & randint(10000) & ".sh" fh = FREEFILE OPEN dummyscript FOR OUTPUT as #fh PRINT #fh, "#!/bin/sh" PRINT #fh, "cd " & escape_filename(curdir()) PRINT #fh, "clear" PRINT #fh, escape_filename(app) + " " + args CLOSE #fh safe_shell "chmod +x " + escape_filename(dummyscript) safe_shell escape_filename(term_wrap) + " " + escape_filename(dummyscript) safekill dummyscript RETURN "" #ELSEIF defined(__FB_WIN32__) waitforkeyrelease DIM handle as ProcessHandle handle = open_console_process(app, args) IF handle = 0 THEN RETURN "Could not run " & app END IF DIM dots as integer = 0 DIM exitcode as integer DO setwait 400 setkeys IF process_running(handle, @exitcode) = NO THEN cleanup_process @handle IF exitcode THEN 'Error, or the user might have killed the program some other way RETURN trimpath(app) + " reported failure, errorcode " & exitcode END IF RETURN "" END IF IF keyval(scEsc) > 1 THEN kill_process handle cleanup_process @handle setkeys RETURN "User cancelled." END IF dots = (dots + 1) MOD 5 DIM msg as string msg = "Please wait, running " & trimpath(app) & rpad(STRING(dots, "."), , 5) msg += fgcol_text(!"\nPress ESC to cancel", uilook(uiMenuItem)) basic_textbox msg, uilook(uiText), vpage setvispage vpage dowait LOOP #ELSE 'Generic UNIX: xterm is nearly everywhere (but not installed by default in some linux distros) 'os_* process handling functions only currently implemented on Windows 'We run run_and_get_output which runs sh (via system()), which runs xterm, which runs app. 'That's a lot that can go wrong! IF LEN(find_helper_app("xterm")) = 0 THEN RETURN "Could not continue because xterm is not installed, please install it. For example run 'yum install xterm' or 'apt-get install xterm'." END IF waitforkeyrelease DIM cmd as string, outbuffer as string, stderr_s as string cmd = "xterm -bg black -fg gray90 -e """ & escape_string(escape_filename(app) & " " & args, """\") & """" ' Capture stdout and stderr DIM res as integer = run_and_get_output(cmd, outbuffer, stderr_s) IF res THEN IF LEN(outbuffer) = 0 THEN outbuffer = "Error code " & res RETURN "Could not run " & app & !":\n" & outbuffer & " " & stderr_s END IF RETURN "" 'Success #ENDIF END FUNCTION FUNCTION find_support_dir () as string 'Returns, creating if needed, the support directory path. 'The support dir is only normally used on Windows and Mac (on Mac, hspeak lives there). 'On GNU/Linux or Android it (currently) won't pre-exist unless the OHR is compiled from source, 'and it is only used to hold utilities downloaded for distributing games (currently just rcedit.exe). '(Downloaded versions of Game itself are stored in $settings_dir/_gameplayer instead) DIM suppdir as string suppdir = exepath & SLASH "support" IF isdir(suppdir) THEN RETURN suppdir IF makedir(suppdir) = 0 THEN RETURN suppdir suppdir = settings_dir & SLASH "support" IF isdir(suppdir) THEN RETURN suppdir IF makedir(suppdir) = 0 THEN RETURN suppdir showerror "Could not find or create a directory for support utilities" RETURN "" ' not found. booo! :( END FUNCTION FUNCTION find_helper_app (appname as string, try_install as integer=NO, download_url as string="") as string 'Returns an empty string if the app is not found, or the full path if it is found 'try_install currently only works on Windows, and not for all possible apps 'download_url: only used if try_install is true, to override the default 'Look in the same folder as CUSTOM/GAME IF isfile(exepath & SLASH & appname & DOTEXE) THEN RETURN exepath & SLASH & appname & DOTEXE #IFDEF __FB_DARWIN__ IF isfile(exepath & "/support/" & appname) THEN RETURN exepath & "/support/" & appname #ENDIF #IFDEF __FB_UNIX__ 'Use the standard util "which" to search the path DIM where as string run_and_get_output("which " & appname, where, , NO) 'disable debuginfo spam 'where is empty on failure RETURN RTRIM(where, !"\n") #ENDIF #IFDEF __FB_WIN32__ RETURN find_windows_helper_app(appname, try_install, download_url) #ENDIF END FUNCTION FUNCTION find_windows_helper_app (appname as string, try_install as integer=NO, download_url as string="") as string 'Gets the windows version of the helper app, even when run on Linux or Mac (for utils to be run via wine) 'Returns an empty string if the app is not found, or the full path if it is found 'try_install: download it if not installed 'download_url: only used if try_install is true, to override the default 'For windows, first look in the support subdirectory DIM support as string = find_support_dir() IF appname = "zip" THEN 'A little special magic to make that if we are looking for zip.exe we find version 3.0 and not 2.3 IF NOT isfile(support & SLASH & appname & "-version-3.0.txt") THEN safekill support & SLASH & "zip.exe" DIM fh as integer = FREEFILE OPEN support & SLASH & appname & "-version-3.0.txt" FOR OUTPUT AS #fh PRINT #fh, "This file is just here to let the OHRRPGCE know that we have already upgraded zip.exe from version 2.3 to 3.0" CLOSE #fh END IF END IF IF isfile(support & SLASH & appname & ".exe") THEN RETURN support & SLASH & appname & ".exe" #IFDEF __FB_WIN32__ ' Check whether appname.exe is in the $PATH DIM ret as string ' Prepend a _ because if its args are blank echo prints "ECHO ON." run_and_get_output "for %X in (" & appname & ".exe) do echo _%~$PATH:X", ret, , NO 'disable debuginfo spam ret = RTRIM(ret, ANY !"\n\r") IF LEN(ret) > 1 THEN RETURN MID(ret, 2) ' Trim the _. #ENDIF IF try_install THEN DIM choice as integer choice = twochoice(appname & ".exe was not found. Would you like to automatically download it from HamsterRepublic.com?", , , , , "download_win_support_util") IF choice = 0 THEN DIM extension as string = "zip" IF appname = "unzip" THEN extension = "exe" IF LEN(download_url) THEN download_file download_url, support ELSE download_file "http://HamsterRepublic.com/ohrrpgce/support/" & appname & "." & extension, support END IF IF NOT isfile(support & SLASH & appname & "." & extension) THEN visible_debug "Unable to download " & appname & "." & extension RETURN "" END IF IF appname <> "unzip" THEN DIM unzip as string = find_helper_app("unzip") IF unzip = "" THEN visible_debug "Can't find unzip tool" RETURN "" END IF ' -q quiet -o overwrite -C case-insenstive -L make lowercase -j junk directories DIM arglist as string arglist = " -qoCLj " & escape_filename(support & SLASH & appname & ".zip") & " -d " & escape_filename(support) DIM spawn_ret as string spawn_ret = spawn_and_wait(unzip, arglist) IF NOT isfile(support & SLASH & appname & ".exe") THEN visible_debug "Unable to unzip " & appname & ".exe from " & appname & ".zip" RETURN "" END IF END IF RETURN support & SLASH & appname & ".exe" END IF END IF RETURN "" END FUNCTION FUNCTION download_file (url as string, destdir as string, forcefilename as string="") as integer 'Returns True on success, false on failure. ' 'wget is the first choice because its -N option can prevent unneeded redundant 'downloads when the file has not changed. curl is an important fallback because 'it is installed by default on Mac OS X ' 'If you specify forcefilename, the -N option will do nothing, ' and the file will be re-downloaded even if it has not changed ' since the last time it was downloaded. DIM spawn_ret as string DIM args as string '--Find the wget to to do the downloading DIM wget as string = find_helper_app("wget") IF wget <> "" THEN '--prepare the command line IF forcefilename = "" THEN args = "-N -P " & escape_filename(destdir) ELSE args = "-O " & escape_filename(destdir & SLASH & forcefilename) END IF args &= " " & escape_filename(url) '--Do the download spawn_ret = spawn_and_wait(wget, args) '--Check to see if the download worked IF LEN(spawn_ret) = 0 THEN RETURN YES visible_debug "ERROR: wget download failed: " & spawn_ret END IF '--find curl to do the downloading DIM curl as string = find_helper_app("curl") IF curl <> "" THEN DIM destfile as string = forcefilename IF destfile = "" THEN destfile = trimpath(url) END IF args = "-o " & escape_filename(destdir & SLASH & destfile) & " " & escape_filename(url) '--Do the download spawn_ret = spawn_and_wait(curl, args) '--Check to see if the download worked IF LEN(spawn_ret) = 0 THEN RETURN YES visible_debug "ERROR: curl download failed: " & spawn_ret END IF visible_debug "Download failed (tried both wget and curl)" RETURN NO END FUNCTION 'There is way too much stuff in this function, would probably be cleaner to remove it FUNCTION missing_helper_message (appname as string) as string DIM ret as string DIM mult as integer = INSTR(appname, " ") ret = appname + DOTEXE + IIF(mult, " are both missing (only one required).", " is missing.") #IFDEF __FB_WIN32__ IF appname = "hspeak" THEN 'support/hspeak.exe WILL work, but that's not where we package it ret += " Check that it is in the same folder as custom.exe." ELSE ret += " Check that it is in the support folder." END IF #ELSEIF DEFINED(__FB_DARWIN__) ret += " This ought to be included inside OHRRPGCE-Custom! Please report this." #ELSE ret += " You must install it on your system." #ENDIF 'Linux nightly builds are full distributions, while on Windows they are missing much. #IF DEFINED(__FB_WIN32__) IF version_branch = "wip" THEN ret += CHR(10) + "You are using a nightly build. Did you unzip the nightly on top of a full install of a stable release, as you are meant to?" IF INSTR(appname, "oggenc") OR INSTR(appname, "madplay") THEN ret += " Alternatively, download oggenc+madplay.zip from the nightly ""alternative backends"" folder." END IF END IF #ENDIF RETURN ret END FUNCTION SUB upgrade_message (s as string) ' Don't black out the screen to show upgrade messages if there aren't any IF show_upgrade_messages AND got_upgrade_messages = NO THEN got_upgrade_messages = YES reset_console 20, vpages(vpage)->h - 20 show_message("Auto-Updating obsolete RPG file") END IF DIM temptime as double IF time_rpg_upgrade THEN temptime = TIMER upgrade_overhead_time -= temptime IF last_upgrade_time <> 0.0 THEN debuginfo "...done in " & FORMAT((temptime - last_upgrade_time) * 1000, ".#") & "ms" END IF END IF debuginfo "rpgfix:" & s IF show_upgrade_messages THEN show_message(s) IF time_rpg_upgrade THEN temptime = TIMER last_upgrade_time = temptime upgrade_overhead_time += temptime END IF END SUB 'admittedly, these 'console' functions suck SUB reset_console (byval top as integer = 0, byval h as integer = 200, byval c as integer = -1) IF c = -1 THEN c = uilook(uiBackground) WITH console .margin = 4 .top = top .h = h .x = .margin .y = top + .margin .c = c DIM tempfr as Frame ptr tempfr = frame_new_view(vpages(vpage), 0, .top, vpages(vpage)->w, .h) frame_clear tempfr, c frame_unload @tempfr END WITH END SUB SUB show_message (s as string) WITH console IF .x > .margin THEN .x = .margin : .y += 8 append_message s END WITH END SUB SUB append_message (s as string) DIM as integer display = YES IF RIGHT(TRIM(s), 1) = "," THEN display = NO WITH console IF .x > .margin AND textwidth(s) + .x > vpages(vpage)->w - .margin THEN .x = .margin .y += 8 display = YES END IF IF .y >= .top + .h - 8 - .margin THEN 'scroll page up 2 lines DIM as Frame ptr tempfr, copied tempfr = frame_new_view(vpages(vpage), 0, .top + 16, vpages(vpage)->w, .h - 16) copied = frame_duplicate(tempfr) frame_clear tempfr, .c frame_draw copied, , 0, .top, , NO, vpage .y -= 16 frame_unload @copied frame_unload @tempfr END IF printstr s, .x, .y, vpage .x += textwidth(s) IF display THEN setvispage vpage END WITH END SUB SUB animatetilesets (tilesets() as TilesetData ptr) FOR i as integer = 0 TO UBOUND(tilesets) 'Animate each tileset... FOR j as integer = 0 TO i - 1 '--unless of course we already animated it for this frame ' because the same tileset can be used on more than one layer... IF tilesets(i) = tilesets(j) THEN CONTINUE FOR, FOR NEXT cycletile tilesets(i)->anim(), tilesets(i)->tastuf() NEXT END SUB SUB cycletile (tanim_state() as TileAnimState, tastuf() as integer) DIM notstuck as integer FOR i as integer = 0 TO 1 #IFDEF IS_GAME IF istag(tastuf(1 + 20 * i), 0) THEN CONTINUE FOR #ENDIF WITH tanim_state(i) .skip = large(.skip - 1, 0) IF .skip = 0 THEN notstuck = 10 DO SELECT CASE tastuf(2 + 20 * i + .pt) CASE 0 'End of animation IF .pt <> 0 THEN .cycle = 0 'this is done for the tile animation plotscript commands .pt = 0 CASE 1 'Left .cycle = .cycle - tastuf(11 + 20 * i + .pt) * 16 .pt = loopvar(.pt, 0, 8, 1) CASE 2 'Right .cycle = .cycle + tastuf(11 + 20 * i + .pt) * 16 .pt = loopvar(.pt, 0, 8, 1) CASE 3 'Down .cycle = .cycle + tastuf(11 + 20 * i + .pt) .pt = loopvar(.pt, 0, 8, 1) CASE 4 'Up .cycle = .cycle - tastuf(11 + 20 * i + .pt) .pt = loopvar(.pt, 0, 8, 1) CASE 5 'Wait .skip = tastuf(11 + 20 * i + .pt) .pt = loopvar(.pt, 0, 8, 1) #IFDEF IS_GAME CASE 6 'If tag then stop IF istag(tastuf(11 + 20 * i + .pt), 0) THEN .pt = loopvar(.pt, 0, 8, 1) ELSE .pt = 0 .cycle = 0 END IF #ENDIF CASE ELSE 'Invalid .pt = loopvar(.pt, 0, 8, 1) END SELECT notstuck = large(notstuck - 1, 0) LOOP WHILE notstuck AND .skip = 0 END IF END WITH NEXT i END SUB #MACRO try_data_path(path) temp = path IF get_file_type(temp) = searchtype THEN RETURN temp #ENDMACRO PRIVATE FUNCTION finddatafile_internal(filename as string, searchtype as FileTypeEnum) as string DIM temp as string 'Current dir try_data_path(filename) try_data_path("data" & SLASH & filename) 'platform-specific relative data files path (Mac OS X bundles) try_data_path(app_resources_dir & SLASH & filename) 'same folder as executable try_data_path(exepath & SLASH & filename) try_data_path(exepath & SLASH & "data" & SLASH & filename) #IFDEF __FB_UNIX__ '~/.ohrrpgce/ try_data_path(settings_dir & filename) #ENDIF IF LEN(DATAFILES) THEN try_data_path(DATAFILES & SLASH & filename) ' In this case the packaging script could avoid put anything in a subdirectory try_data_path(DATAFILES & SLASH & "data" & SLASH & filename) END IF RETURN "" END FUNCTION PRIVATE FUNCTION finddatafile_internal2(filename as string, searchtype as FileTypeEnum, error_if_missing as bool) as string DIM ret as string = finddatafile_internal(filename, searchtype) IF LEN(ret) THEN RETURN ret IF error_if_missing THEN showerror !"Error: data " & IIF(searchtype = fileTypeFile, "file", "directory") & " data/" & _ filename & " not found. The OHRRPGCE apparently isn't installed properly. Try reinstalling." END IF END FUNCTION ' Given a filename, search all the locations that data files for Game or Custom might ' be packages, and return its path if found. FUNCTION finddatafile(filename as string, error_if_missing as bool = YES) as string RETURN finddatafile_internal2(filename, fileTypeFile, error_if_missing) END FUNCTION ' Like finddatafile, for subdirectories. Has a trailing slash only if dirname has one. FUNCTION finddatadir(dirname as string, error_if_missing as bool = YES) as string RETURN finddatafile_internal2(dirname, fileTypeDirectory, error_if_missing) END FUNCTION ' Return the location of the data/ dir, in case you want to create a new file there ' or browse for files. Otherwise, always use finddatafile or finddatadir instead! FUNCTION get_data_dir() as string DIM temp as string DIM searchtype as FileTypeEnum = fileTypeDirectory 'Unlike finddatafile, try exepath first try_data_path(exepath & SLASH & "data") try_data_path("data") 'Contents of data/ are copied into Resources dir in Mac OS X app bundles IF LEN(app_resources_dir) THEN try_data_path(app_resources_dir) 'Don't bother with DATAFILES, probably read-only IF makedir("data") = 0 THEN RETURN "data" END FUNCTION ' Given a path to a file or subdir inside the data/ directory, return a portable ' relative path to it that can be passed to finddatafile, or "" if not possible. ' This function is only really intended for use for engine development, ' so we assume exepath & "/data/" specifically. ' (Also converts to Unix / path separators) FUNCTION filename_relative_to_datadir(filename as string) as string DIM ret as string ret = simplify_path_further(filename, get_data_dir()) replacestr(ret, "\", "/") IF is_absolute_path(ret) OR LEFT(ret, 2) = ".." THEN notification "Not a valid asset file: not inside " & get_data_dir() RETURN "" END IF RETURN ret END FUNCTION SUB updaterecordlength (lumpf as string, byval bindex as integer, byval headersize as integer = 0, byval repeating as integer = NO) 'If the length of records in this lump has changed (increased) according to binsize.bin, stretch it, padding records with zeroes. 'Note: does not create a lump if it doesn't exist. 'Pass 'repeating' as true when more than one lump with this bindex exists. ''headersize' is the number of bytes before the first record. IF getbinsize(bindex) < curbinsize(bindex) THEN DIM oldsize as integer = getbinsize(bindex) DIM newsize as integer = curbinsize(bindex) upgrade_message trimpath(lumpf) & " record size = " & newsize 'Only bother to do this for records of nonzero size (this implies the file doesn't exist, right?) IF oldsize > 0 ANDALSO isfile(lumpf) THEN DIM tempf as string = lumpf & ".resize.tmp" 'This tends to break (it's a C/unix system call), hence all the paranoia IF local_file_move(lumpf, tempf) = NO THEN fatalerror "Impossible to upgrade game: Could not move " & lumpf END IF DIM inputf as integer OPENFILE(tempf, FOR_BINARY, inputf) DIM outputf as integer OPENFILE(lumpf, FOR_BINARY, outputf) 'Round up record count. Due to various bugs a record might be short. Will be zero-extended. '(Note that if a partial record is not extended here, then fix_record_count will do so) DIM total_bytes as integer = LOF(inputf) - headersize DIM records as integer = total_bytes \ oldsize IF total_bytes MOD oldsize THEN debuginfo "updaterecordslength: last record of " & trimpath(lumpf) & " is " & (total_bytes MOD oldsize) & " bytes, expected " & oldsize & ". Extending." records += 1 END IF IF headersize > 0 THEN DIM headerbuf(headersize - 1) as BYTE GET #inputf, , headerbuf() PUT #outputf, , headerbuf() END IF DIM buf(newsize \ 2 - 1) as integer FOR i as integer = 0 TO records - 1 loadrecord buf(), inputf, oldsize \ 2 storerecord buf(), outputf, newsize \ 2 NEXT CLOSE inputf CLOSE outputf killfile tempf END IF 'If we are repeating, we need to keep the old binsize intact IF repeating = NO THEN setbinsize bindex, newsize END IF END SUB 'Clamp a value to within a range with warning SUB clamp_value (byref value as integer, byval min as integer, byval max as integer, argname as string) DIM oldval as integer = value IF value < min THEN value = min IF value > max THEN value = max IF value <> oldval THEN debug "Clamped invalid " + argname + " value " & oldval & " to " & value END SUB FUNCTION passwordhash (p as string) as ushort 'Just a simple stupid 9-bit hash. 'The idea is just to make the password unretrieveable, without using a cryptographic hash. IF p = "" THEN RETURN 0 DIM hash as ushort FOR i as integer = 0 TO LEN(p) - 1 hash = hash * 3 + p[i] * 31 NEXT RETURN (hash AND 511) OR 512 'Never return 0 END FUNCTION 'If someone forgets their password, call this function to generate a new one FUNCTION generatepassword(byval hash as integer) as string IF hash = 0 THEN RETURN "" IF hash - 512 < 0 OR hash - 512 > 511 THEN RETURN "" DO DIM p as string = "" FOR i as integer = 0 TO 3 p += CHR(ASC("a") + randint(25)) NEXT IF passwordhash(p) = hash THEN RETURN p LOOP END FUNCTION SUB writepassword (pass as string) gen(genPassVersion) = 257 gen(genPW4Hash) = passwordhash(pass) '--Provide limited back-compat for PW3 (not possible to open a passworded '--file with an older version of Custom even if you know the password) DIM dummypw as string IF pass = "" THEN '--Write empty 3rd-style password dummypw = STRING(17, 0) ELSE '--Write unguessable garbage 3rd-style password FOR i as integer = 1 TO 17 dummypw += CHR(randint(254)) NEXT i END IF gen(genPW3Rot) = 0 str2array dummypw, gen(), 14 END SUB 'Read old-old-old password (very similar to PW3) FUNCTION read_PW1_password () as string DIM rpas as string FOR i as integer = 1 TO gen(genPW1Length) IF gen(4 + i) >= 0 AND gen(4 + i) <= 255 THEN rpas = rpas + CHR(loopvar(gen(4 + i), 0, 255, gen(genPW1Offset) * -1)) NEXT i RETURN rpas END FUNCTION 'Read old-old scattertable password format FUNCTION read_PW2_password () as string DIM stray(10) as integer DIM pass as string = STRING(20, "!") FOR i as integer = 0 TO gen(genPW2Length) setbit stray(), 0, i, readbit(gen(), 200 - 1, gen(200 + i)) NEXT i array2str stray(), 0, pass pass = LEFT(pass, INT((gen(genPW2Length) + 1) / 8)) RETURN rotascii(pass, gen(genPW2Offset) * -1) END FUNCTION FUNCTION read_PW3_password () as string '--read a 17-byte string from GEN at word offset 7 '--(Note that array2str uses the byte offset not the word offset) DIM pass as string pass = STRING(17, 0) array2str gen(), 14, pass '--reverse ascii rotation / weak obfuscation pass = rotascii(pass, gen(genPW3Rot) * -1) '-- discard ascii chars lower than 32 DIM pass2 as string = "" FOR i as integer = 1 TO 17 DIM c as string = MID(pass, i, 1) IF ASC(c) >= 32 THEN pass2 += c NEXT i RETURN pass2 END FUNCTION 'Return true if it passes. 'Supports all password formats, because this is called before upgrade FUNCTION checkpassword (pass as string) as integer IF gen(genPassVersion) > 257 THEN 'Please let this never happen RETURN NO ELSEIF gen(genPassVersion) = 257 THEN RETURN (passwordhash(pass) = gen(genPW4Hash)) ELSEIF gen(genPassVersion) = 256 THEN '--new format password RETURN (pass = read_PW3_password) ELSEIF gen(genVersion) >= 3 THEN '--old scattertable format RETURN (pass = read_PW2_password) ELSE '--ancient format RETURN (pass = read_PW1_password) END IF END FUNCTION 'Used for forgotten password retrieval. Move along. FUNCTION getpassword () as string IF gen(genPassVersion) = 257 THEN RETURN "Random password: " & generatepassword(gen(genPW4Hash)) ELSEIF gen(genPassVersion) = 256 THEN RETURN read_PW3_password ELSEIF gen(genVersion) >= 3 THEN RETURN read_PW2_password ELSE RETURN read_PW1_password END IF END FUNCTION '========================================================================================== ' Upgrade old games '========================================================================================== /' 'If you used r4109 (Zenzizenzic, 2011) or later to open a game last edited before 'r1651 (Ubersetzung, 2007), all "weak to/strong to/absorbs/is enemytype" bits were 'discarded instead of being upgraded to "takes X% damage from ..." 'This routine imports them from a 2011-or-earlier .dt1 lump. SUB import_enemy_elementals_from_dt1 (copy_enemytypes_from as string) 'r4109 was also the same commit that switched DT1 to binsize IF FILELEN(copy_enemytypes_from) MOD 320 THEN visible_debug copy_enemytypes_from & " is the wrong length. Probably not from before the corruption" EXIT SUB END IF REDIM old_dat(320) as integer DIM enemy as EnemyDef FOR enemynum as integer = 0 TO gen(genMaxEnemy) loadrecord old_dat(), copy_enemytypes_from, 160, enemynum loadenemydata enemy, enemynum FOR j as integer = 0 TO 63 DIM currentval as single = enemy.elementals(j) DIM oldval as single = loadoldenemyresist(old_dat(), j) IF currentval <> 1.0 AND (j < 16 OR currentval <> 0.) THEN visible_debug "Enemy " & enemynum & " element " & j & " is already set to " & _ format_percent(currentval) & ", skipping enemy" EXIT FOR ELSE enemy.elementals(j) = oldval END IF NEXT saveenemydata enemy, enemynum NEXT END SUB '/ 'Wandering Hamster demo from 1997 stored map tilesets in GEN, as it had no MAP lump SUB upgrade_1997 () DIM gmap(dimbinsize(binMAP)) as integer FOR mapnum as integer = 0 TO gen(genMaxMap) gmap(0) = gen(1 + mapnum) gen(1 + mapnum) = 0 storerecord gmap(), game & ".map", getbinsize(binMAP) \ 2, mapnum 'NPCs didn't have facing direction stored, and just defaulted to south DIM npcl(299) as NPCInst LoadNPCL maplumpname(mapnum, "l"), npcl() FOR i as integer = 0 TO UBOUND(npcl) npcl(i).dir = dirSouth NEXT SaveNPCL maplumpname(mapnum, "l"), npcl() NEXT 'Also, had no title screen, so set "Skip title screen" setbit gen(), genBits, 11, 1 'Other stuff: textbox to add a hero has .hero_swap = 99 and .hero_lock = 99, 'global text strings diff END SUB SUB upgrade (show_messages as bool) DIM fh as integer DIM master_temp(255) as RGBcolor show_upgrade_messages = show_messages got_upgrade_messages = NO last_upgrade_time = 0.0 upgrade_start_time = TIMER upgrade_overhead_time = 0.0 DIM original_genVersion as integer = gen(genVersion) 'Custom and Game should both have provided a writeable workingdir. Double check. '(This is partially in vain, as we could crash if any of the lumps are unwriteable) IF NOT diriswriteable(workingdir) THEN fatalerror "Upgrade failure: " + workingdir + " not writeable" IF full_upgrade THEN debuginfo "Full game data upgrade..." ELSE debuginfo "Partial game data upgrade..." END IF DIM general_reld as Node ptr = get_general_reld() DIM last_editor_version as EngineVersion = read_last_editor_version() DIM auto_upgrader_version as EngineVersion = read_engine_version_node(general_reld."automatic_upgrader_version".ptr) ' Note that creating a new .rpg file updates the archinym creation info, but not immediately last_editor_version IF last_editor_version.recorded THEN debuginfo "Last edited by: [[" & last_editor_version.long_version & "]]" ELSE debuginfo "No detailed editor version info." END IF IF auto_upgrader_version.recorded THEN debuginfo "Upgraded by: " & auto_upgrader_version.long_version END IF debuginfo "archinym creation info: " & read_archinym_version() DIM has_sfx as bool = isfile(workingdir & SLASH & "sfxdata.bin") '---------------------------------------- 'Update record-count for all fixed-length lumps. 'WARNING: we haven't done fixWipeGEN yet, so don't initialise anything in gen(199) and up! IF time_rpg_upgrade THEN upgrade_message "Updating record counts" FOR i as integer = 0 TO 8 fix_sprite_record_count i NEXT i fix_record_count gen(genMaxTile), 320 * 200, game & ".til", "Tilesets" fix_record_count gen(genNumBackdrops), 320 * 200, game & ".mxs", "Backdrops", , -1 IF isfile(workingdir & SLASH & "heroes.reld") THEN 'TODO: genMaxHero fix-up for heroes.reld is unimplemented ELSE 'In old versions, games start with 60 heroes fix_record_count gen(genMaxHero), getbinsize(binDT0), game & ".dt0", "Heroes" END IF fix_record_count gen(genMaxEnemy), getbinsize(binDT1), game & ".dt1", "Enemies" fix_record_count gen(genMaxFormation), 80, game & ".for", "Battle Formations" IF gen(genVersion) >= 5 THEN ' Note: there is a second record count in the header, which is respected by Palette16_load, ' and may not match gen(genMaxPal)! TODO: should probably ignore it. fix_record_count gen(genMaxPal), 16, game & ".pal", "16-color Palettes", 16 ELSE ' The .pal file format changed in Format 5 (genMaxPal is computed below) gen(genMaxPal) = 99 END IF fix_record_count gen(genMaxTextbox), getbinsize(binSAY), game & ".say", "Text Boxes" '.veh may not exist yet fix_record_count gen(genMaxVehicle), 80, game & ".veh", "Vehicles" fix_record_count gen(genMaxTagname), 42, game & ".tmn", "Tag names", -84 'Note: no records for tags 0 and 1, so we handle that with a negative header size. 'In older versions menus.bin is always 2 bytes short... except when it's zero length or missing. 'In fact, it might be zero length even if the menu has been modified (menuitem.bin contains the modified items)!! fix_record_count gen(genMaxMenu), getbinsize(binMENUS), workingdir & SLASH & "menus.bin", "Menus" fix_record_count gen(genMaxMenuItem), getbinsize(binMENUITEM), workingdir & SLASH & "menuitem.bin", "Menu Items" fix_record_count gen(genMaxItem), getbinsize(binITM), game & ".itm", "Items" 'palettes.bin may not exist yet fix_record_count gen(genMaxMasterPal), 256 * 3, workingdir & SLASH & "palettes.bin", "Master Palettes", 4 fix_record_count gen(genMaxSong), getbinsize(binSONGDATA), workingdir & SLASH "songdata.bin", "Songs" 'Warning: don't deduce number of maps from length of .MAP or .MN: may be appended with garbage 'FIXME: Attack data is split over two lumps. Must handle mismatch. In the meantime, a quick fix (increase only) DIM dt6_records as integer = (filelen(game & ".dt6") + 79) \ 80 IF dt6_records > gen(genMaxAttack) + 1 THEN upgrade_message "Increasing number of attacks from " & (gen(genMaxAttack) + 1) & " to " & dt6_records gen(genMaxAttack) = dt6_records - 1 END IF IF gen(genVersion) = 0 ANDALSO isfile(game & ".map") = NO THEN upgrade_message "Wow! Upgrading .RPG file from 1997" upgrade_1997 END IF 'Check the number of maps, updating gen(genMaxMap), and check all lumps are present check_map_count '---------------------------------------- '--make sure binsize.bin is full. why are we doing this? Otherwise as lumps are upgraded '--and binsize.bin is extended, records in binsize which are meant to default '--because they don't exist would become undefined instead FOR i as integer = 0 TO binLASTENTRY setbinsize i, getbinsize(i) NEXT IF getfixbit(fixNumElements) = 0 THEN setfixbit(fixNumElements, 1) 'This has to be set before we start loading and saving heroes, enemies, and items gen(genNumElements) = 16 END IF IF gen(genNumElements) < 1 THEN visible_debug "genNumElements was invalid: " & gen(genNumElements) & ". Resetting to 64" 'Set to max to be safe, because the load/save code will zero out anything above it gen(genNumElements) = 64 END IF '---------------------------------------- IF gen(genVersion) = 0 THEN upgrade_message "Ancient Pre-1999 format (1)" gen(genVersion) = 1 upgrade_message "Flushing New Text Data..." DIM box as TextBox FOR o as integer = 0 TO gen(genMaxTextbox) LoadTextBox box, o 'Zero out the data members that contained random garbage before 1999 WITH box .money_tag = 0 .money = 0 .door_tag = 0 .door = 0 .item_tag = 0 .item = 0 .choice_enabled = NO .no_box = NO .opaque = NO .restore_music = NO .choice(0) = "" .choice_tag(0) = 0 .choice(1) = "" .choice_tag(1) = 0 .game_tag = 0 .game_delete = 0 .game_save = 0 .game_load = 0 .menu_tag = 0 .vertical_offset = 0 .shrink = 0 .textcolor = 0 .boxstyle = 0 .backdrop = 0 .music = 0 .menu = 0 END WITH SaveTextBox box, o NEXT o END IF IF gen(genVersion) = 1 THEN upgrade_message "June 18 1999 format (2)" gen(genVersion) = 2 upgrade_message "Updating Door Format..." ' Read 20 records, 600 bytes each DIM doorbuf(20 * 600 \ 2) as integer IF isfile(game + ".dor") THEN xbload game + ".dor", doorbuf(), "No doors" storerecord doorbuf(), game + ".dox", 20 * 600 \ 2, 0 upgrade_message "Enforcing default font" DIM font(1023) as integer getdefaultfont font() xbsave game + ".fnt", font(), 2048 upgrade_message "rpgfix: Upgrading to customisable tile animations" ' The last two tiles in the tileset used to auto-toggle. Add a default ' tile animation and replace those tiles with animated tiles. FOR i as integer = 0 TO 39 buffer(i) = 0 NEXT i FOR i as integer = 0 TO 1 DIM o as integer = i * 20 buffer(0 + o) = 112 buffer(1 + o) = 0 '--wait 3-- buffer(2 + o + 0) = 5 buffer(11 + o + 0) = 3 '--right 1-- buffer(2 + o + 1) = 3 buffer(11 + o + 1) = 1 '--wait 3-- buffer(2 + o + 2) = 5 buffer(11 + o + 2) = 3 '--left 1-- buffer(2 + o + 3) = 4 buffer(11 + o + 3) = 1 NEXT i FOR i as integer = 0 TO gen(genMaxTile) save_tile_anims i, buffer() NEXT i DIM tx as integer, ty as integer DIM tmap as TileMap FOR i as integer = 0 TO gen(genMaxMap) upgrade_message " map " & i loadtilemap tmap, maplumpname(i, "t") FOR tx = 0 TO tmap.wide - 1 FOR ty = 0 TO tmap.high - 1 IF readblock(tmap, tx, ty) = 158 THEN writeblock tmap, tx, ty, 206 NEXT ty NEXT tx savetilemap tmap, maplumpname(i, "t") NEXT i unloadtilemap tmap END IF '---VERSION 3--- IF gen(genVersion) = 2 THEN upgrade_message "July 8 1999 format (3)" upgrade_message "Zero-out un-used data in .MAP records" 'This fix might be needed for games in later formats, especially ones ' that are already have gen(genVersion) = 3 but unfortunately this ' is the last place we can apply the fix in a safe way that does not ' risk data-loss (particularly map autorun script triggers) zero_out_pre_july_8_1999_general_map_data_garbage() gen(genVersion) = 3 writepassword read_PW1_password 'No need to remove the old password: we just overwrote it with 'a back-compat PW3 blank/garbage password /' This is should no longer be needed, since these are all set by fix_record_count '(Some of these are maximum possible values, some are off-by-one, others are correct) upgrade_message "Put record count defaults in GEN..." gen(genMaxHeroPic) = 40 gen(genMaxEnemy1Pic) = 149 gen(genMaxEnemy2Pic) = 79 gen(genMaxEnemy3Pic) = 29 gen(genMaxNPCPic) = 119 gen(genMaxWeaponPic) = 149 gen(genMaxAttackPic) = 99 gen(genMaxTile) = 14 gen(genMaxAttack) = 200 gen(genMaxHero) = 59 gen(genMaxEnemy) = 500 gen(genMaxFormation) = 1000 gen(genMaxPal) = 99 gen(genMaxTextbox) = 999 '/ END IF '--VERSION 4-- IF gen(genVersion) = 3 THEN upgrade_message "Sept 15 2000 format (4)" gen(genVersion) = 4 upgrade_message "Clearing New Attack Bitsets..." FOR o as integer = 0 TO gen(genMaxAttack) loadoldattackdata buffer(), o buffer(18) = 0 IF readbit(buffer(), 20, 60) THEN buffer(18) = 1 setbit buffer(), 20, 2, 0 FOR i as integer = 21 TO 58 setbit buffer(), 20, i, 0 NEXT i FOR i as integer = 60 TO 63 setbit buffer(), 20, i, 0 NEXT i saveoldattackdata buffer(), o NEXT o setbit gen(), genBits, 6, 0 'no hide readymeter setbit gen(), genBits, 7, 0 'no hide health meter END IF '--VERSION 5-- IF gen(genVersion) = 4 THEN upgrade_message "March 31 2001 format (5)" gen(genVersion) = 5 upgrade_message "Upgrading 16-color Palette Format..." ' Change the .pal header from a 7 byte BSAVE one to a custom 16 byte one. DIM temppal16(8 - 1) as integer xbload game + ".pal", buffer(), "16-color palettes missing from " + sourcerpg killfile game + ".pal" '--find last used palette DIM last as integer = gen(genMaxPal) FOR j as integer = gen(genMaxPal) TO 0 STEP -1 FOR i as integer = 0 TO 7 IF buffer(j * 8 + i) <> 0 THEN last = j EXIT FOR, FOR END IF NEXT i NEXT j upgrade_message "Last used palette is " & last gen(genMaxPal) = last '--write header temppal16(0) = 4444 temppal16(1) = last FOR i as integer = 2 TO 7 temppal16(i) = 0 NEXT i storerecord temppal16(), game + ".pal", 16 \ 2, 0 '--append palettes (really just replacing the header and keeping everything else the same) FOR j as integer = 0 TO last FOR i as integer = 0 TO 7 temppal16(i) = buffer(j * 8 + i) NEXT i storerecord temppal16(), game + ".pal", 16 \ 2, 1 + j NEXT j 'esperable (2003) introduced the bug where harm tiles no longer damage the 'whole active party if the caterpillar is disabled. Fixed for alectormancy. 'The best we can do is not emulating the bug for versions before March 2001 :( setbit gen(), genBits2, 12, 1 'Harm tiles harm non-caterpillar heroes END IF '--VERSION 6-- IF gen(genVersion) = 5 THEN upgrade_message "Serendipity format (6)" 'Shop stuff and song name formats changed, MIDI music added 'Sub version info also added 'Clear battle formation animation data FOR i as integer = 0 TO gen(genMaxFormation) loadrecord buffer(), game + ".for", 80 \ 2, i buffer(34) = 0 buffer(35) = 0 storerecord buffer(), game + ".for", 80 \ 2, i NEXT i gen(genVersion) = 6 END IF '--VERSION 7 and up! ' It is a good idea to increment this number each time a major feature ' has been added, if opening a new game in an old editor would cause data-loss ' Don't be afraid to increment this. Backcompat warnings are a good thing! IF gen(genVersion) < CURRENT_RPG_VERSION THEN upgrade_message "Bumping RPG format version number from " & gen(genVersion) & " to " & CURRENT_RPG_VERSION gen(genVersion) = CURRENT_RPG_VERSION '--update me in const.bi END IF IF gen(genPassVersion) = 256 THEN '--Update PW3 to PW4 upgrade_message "Updating PW3 password storage format" writepassword read_PW3_password ELSEIF gen(genPassVersion) < 256 THEN '--At this point we know the password format is PW2 (not PW1), scattertable upgrade_message "Updating PW2 password storage format" writepassword read_PW2_password END IF 'Zero out the PW2 scattertable (almost all games) as well as some other explained garbage (a few games) in gen() IF getfixbit(fixWipeGEN) = NO THEN upgrade_message "Wiping gen(199...)" setfixbit(fixWipeGEN, YES) FOR i as integer = 199 TO 499 gen(i) = 0 NEXT END IF IF NOT isfile(workingdir + SLASH + "archinym.lmp") THEN upgrade_message "generate default archinym.lmp" '--create archinym information lump OPENFILE(workingdir + SLASH + "archinym.lmp", FOR_OUTPUT, fh) PRINT #fh, RIGHT(game, LEN(game) - LEN(workingdir + SLASH)) PRINT #fh, version + "(previous)" CLOSE #fh END IF 'Initialise some data in general.reld and update either editor_version (Custom) 'or automatic_upgrader_version (Game) update_general_data 'This is corruption recovery, not upgrade, but Custom has always done this IF NOT isfile(game + ".fnt") THEN debug game + ".fnt missing (which should never happen)" DIM font(1023) as integer getdefaultfont font() xbsave game + ".fnt", font(), 2048 END IF IF NOT isfile(game + ".veh") THEN upgrade_message "add vehicle data" '--make sure vehicle lump is present DIM templatefile as string = finddatafile("ohrrpgce.new") IF templatefile <> "" THEN unlumpfile(templatefile, "ohrrpgce.veh", tmpdir) 'Recall it's best to avoid moving files across filesystems copyfile tmpdir & "ohrrpgce.veh", game & ".veh" safekill tmpdir & "ohrrpgce.veh" 'gen(genMaxVehicle) = 2 fix_record_count gen(genMaxVehicle), 80, game & ".veh", "Vehicles" END IF END IF IF NOT isfile(workingdir + SLASH + "attack.bin") THEN upgrade_message "Init extended attack data..." setbinsize binATTACK, curbinsize(binATTACK) flusharray buffer(), dimbinsize(binAttack), 0 FOR i as integer = 0 TO gen(genMaxAttack) savenewattackdata buffer(), i NEXT i '--and while we are at it, clear the old death-string from enemies upgrade_message "Re-init recycled enemy data..." FOR i as integer = 0 TO gen(genMaxEnemy) loadenemydata buffer(), i FOR j as integer = 17 TO 52 buffer(j) = 0 NEXT j saveenemydata buffer(), i NEXT i END IF IF NOT isfile(workingdir + SLASH + "songdata.bin") THEN upgrade_message "Upgrading Song Name format..." DIM song(99) as string DIM temp as integer = FILELEN(game + ".sng") IF temp > 0 THEN OPENFILE(game + ".sng", FOR_INPUT, fh) FOR i as integer = 0 TO 99 INPUT #fh, song(i) NEXT i CLOSE #fh END IF gen(genMaxSong) = 99 FOR i as integer = 99 TO 1 STEP -1 '-- check for midis as well 'cause some people might use a WIP custom or whatnot IF song(i) <> "" OR isfile(game + "." + STR(i)) OR isfile(workingdir + SLASH + "song" + STR(i) + ".mid") THEN gen(genMaxSong) = i EXIT FOR END IF NEXT setbinsize binSONGDATA, curbinsize(binSONGDATA) flusharray buffer(), dimbinsize(binSONGDATA), 0 FOR i as integer = 0 TO gen(genMaxSong) writebinstring song(i), buffer(), 0, 30 storerecord buffer(), workingdir + SLASH + "songdata.bin", curbinsize(binSONGDATA) \ 2, i NEXT ERASE song END IF IF NOT isfile(workingdir + SLASH + "palettes.bin") THEN upgrade_message "Upgrading Master Palette format..." IF NOT isfile(game + ".mas") THEN debug "Warning: " & game & ".mas does not exist (which should never happen)" load_default_master_palette master() ELSE loadpalette master(), 0 'Loads from .mas END IF gen(genMaxMasterPal) = 0 savepalette master(), 0 'Saves to palettes.bin END IF 'Safety-check for negative gen(genMasterPal) because of one known game that somehow had -2 gen(genMasterPal) = bound(gen(genMasterPal), 0, gen(genMaxMasterPal)) IF gen(genHeroWeakHP) = 0 THEN gen(genHeroWeakHP) = 20 END IF IF gen(genEnemyWeakHP) = 0 THEN gen(genEnemyWeakHP) = 20 END IF '--If no stf lump exists, create an empty one. IF NOT isfile(game + ".stf") THEN touchfile game + ".stf" '--check variable record size lumps and reoutput them if records have been extended '--all of the files below should exist, be non zero length and have non zero record size by this point '--...except for menu lumps updaterecordlength workingdir + SLASH + "attack.bin", binATTACK updaterecordlength game + ".stf", binSTF updaterecordlength workingdir + SLASH + "songdata.bin", binSONGDATA updaterecordlength workingdir + SLASH + "sfxdata.bin", binSFXDATA updaterecordlength game + ".map", binMAP updaterecordlength workingdir + SLASH + "menus.bin", binMENUS updaterecordlength workingdir + SLASH + "menuitem.bin", binMENUITEM IF NOT isfile(workingdir + SLASH + "menuitem.bin") THEN upgrade_message "Creating default menu file..." DIM def_menu_set as MenuSet def_menu_set.menufile = workingdir + SLASH + "menus.bin" def_menu_set.itemfile = workingdir + SLASH + "menuitem.bin" DIM menu as MenuDef ' If we're playing an old game without sound effects, SFX volume option is pointless create_default_menu menu, has_sfx SaveMenuData def_menu_set, menu, 0 ClearMenuData menu END IF updaterecordlength game & ".say", binSAY updaterecordlength game & ".dt0", binDT0 updaterecordlength game & ".dt1", binDT1 updaterecordlength game & ".itm", binITM 'Don't update .N binsize until all records have been stretched FOR i as integer = 0 TO gen(genMaxMap) updaterecordlength maplumpname(i, "n"), binN, 7, YES NEXT setbinsize binN, curbinsize(binN) 'If you want to add more colours to uicolors.bin, you'll want to record what the old 'record length was (one record per palette), then updaterecordlength, then fill in 'the new colours, which start zeroed out. 'However, if uicolors.bin is completely empty/missing, then just let the block below 'initialise the lump. updaterecordlength workingdir + SLASH + "uicolors.bin", binUICOLORS '--give each palette a default ui color set DIM uilook_temp(uiColorLast) as integer DIM boxlook_temp(uiBoxLast) as BoxStyle 'This step creates both the uicolors.bin file (obsolete, but kept for compat) 'And the uicolors node in general.reld DIM uirecords as integer = FILELEN(workingdir + SLASH + "uicolors.bin") \ getbinsize(binUICOLORS) IF uirecords < gen(genMaxMasterPal) + 1 THEN upgrade_message "Adding default UI colors..." FOR i as integer = uirecords TO gen(genMaxMasterPal) 'In old games (before uicolors.bin was added), use the old fixed color indices, for compatibility IF original_genVersion <= 7 THEN 'Before Ypsiliform WIPs OldDefaultUIColors uilook_temp(), boxlook_temp() SaveUIColors uilook_temp(), boxlook_temp(), i ELSE 'Otherwise, missing records is a bug. Do nearest match with default loadpalette master_temp(), i GuessDefaultUIColors master_temp(), uilook_temp(), boxlook_temp() SaveUIColors uilook_temp(), boxlook_temp(), i END IF NEXT END IF 'Work around a bug: vehicle shadow colour shouldn't be 0 FOR i as integer = 0 TO gen(genMaxMasterPal) LoadUIColors uilook_temp(), boxlook_temp(), i IF uilook_temp(uiShadow) = 0 THEN loadpalette master_temp(), i 'This is a hack. Unfortunately ellipse slice border and fill colors can't be 0 as that 'counts as transparent. This is a design mistake in ellipse slices, but is too much trouble to fix uilook_temp(uiShadow) = nearcolor(master_temp(), 0, 1) SaveUIColors uilook_temp(), boxlook_temp(), i END IF NEXT 'Workaround bug #2013: Backdrops were drawn immediately above (instead of) spritelayer until 'r5437, which swapped the order. genVersion was increased to 18 in r5477 IF original_genVersion <= 17 THEN setbit gen(), genBits2, 23, 1 'Draw Backdrop slice above Script layer END IF 'Zero out new attack item cost (ammunition) data IF getfixbit(fixAttackitems) = 0 THEN upgrade_message "Zero new ammunition data..." setfixbit(fixAttackitems, 1) OPENFILE(workingdir + SLASH + "attack.bin", FOR_BINARY, fh) REDIM dat(dimbinsize(binATTACK)) as short DIM record_pos as integer = 1 FOR idx as integer = 0 to gen(genMaxAttack) GET #fh, record_pos, dat() FOR y as integer = 53 TO 59 dat(y) = 0 NEXT PUT #fh, record_pos, dat() record_pos += getbinsize(binATTACK) NEXT CLOSE #fh END IF IF getfixbit(fixWeapPoints) = 0 THEN upgrade_message "Reset hero hand points..." DO setfixbit(fixWeapPoints, 1) OPENFILE(game + ".dt0", FOR_BINARY, fh) REDIM dat(dimbinsize(binDT0)) as short FOR i as integer = 0 to gen(genMaxHero) GET #fh, , dat() IF dat(297) <> 0 OR dat(298) <> 0 OR dat(299) <> 0 OR dat(300) <> 0 THEN CLOSE #fh EXIT DO 'they already use hand points, abort! END IF NEXT DIM record_pos as integer = 1 DIM recsize as integer = getbinsize(binDT0) FOR idx as integer = 0 to gen(genMaxHero) GET #fh, record_pos, dat() dat(297) = 24 dat(299) = -20 PUT #fh, record_pos, dat() record_pos += recsize NEXT CLOSE #fh EXIT DO LOOP END IF 'Upgrade attack data DIM fix_stun as integer = (getfixbit(fixStunCancelTarg) = 0) DIM fix_dam_mp as integer = (getfixbit(fixRemoveDamageMP) = 0) AND full_upgrade DIM fix_elem_fails as integer = (getfixbit(fixAttackElementFails) = 0) AND full_upgrade ' Temporarily disable upgrade to new damage/aim attack data, so that the format can still ' be changed if we desire, since the editor doesn't support it yet. ' (Instead, loadattackdata will perform these upgrades, since we don't set the fixbit.) 'DIM fix_multipliers as integer = (getfixbit(fixAttackMultipliers) = 0) AND full_upgrade DIM fix_multipliers as integer = NO IF fix_stun OR fix_dam_mp OR fix_elem_fails OR fix_multipliers THEN IF fix_stun THEN upgrade_message "Target disabling old stun attacks..." setfixbit(fixStunCancelTarg, 1) END IF IF fix_dam_mp THEN upgrade_message "Remove obsolete 'Damage MP' bit..." setfixbit(fixRemoveDamageMP, 1) END IF IF fix_elem_fails THEN upgrade_message "Init attack elemental failure conditions..." setfixbit(fixAttackElementFails, 1) END IF IF fix_multipliers THEN upgrade_message "Init new attack aim & damage data..." setfixbit(fixAttackMultipliers, 1) END IF REDIM dat(40 + dimbinsize(binATTACK)) as integer DIM cond as AttackElementCondition FOR i as integer = 0 to gen(genMaxAttack) DIM saveattack as integer = NO loadattackdata dat(), i IF fix_stun AND dat(18) = 14 THEN '--Target stat is stun register IF readbit(dat(), 20, 0) THEN GOTO skipfix '--cure instead of harm IF dat(5) = 5 OR dat(5) = 6 THEN '--set to percentage IF dat(11) >= 0 THEN GOTO skipfix '-- set to >= 100% END IF 'Turn on the disable target attack bit setbit dat(), 65, 12, YES saveattack = YES END IF skipfix: IF fix_dam_mp THEN IF readbit(dat(), 20, 60) THEN '--Damage MP setbit dat(), 20, 60, NO saveattack = YES IF dat(18) = statHP THEN dat(18) = statMP END IF END IF IF fix_elem_fails THEN FOR j as integer = 0 TO 63 loadoldattackelementalfail cond, dat(), j SerAttackElementCond cond, dat(), 121 + j * 3 NEXT saveattack = YES END IF IF fix_multipliers THEN '.randomization = IIF(.do_not_randomize, 0, 20) dat(337) = IIF(readbit(dat(), 20, 61), 0, 20) setbit dat(), 20, 61, NO 'Clear "do not randomize" SerSingle(dat(), 325, 1.0) '.acc_mult SerSingle(dat(), 327, 1.0) '.dog_mult SerSingle(dat(), 329, 1.0) '.atk_mult SerSingle(dat(), 331, 1.0) '.def_mult SerSingle(dat(), 335, 1.0) '.absorb_rate saveattack = YES END IF IF saveattack THEN saveattackdata dat(), i NEXT END IF '------------------------------------------------------------------------------- ' Enemies '(There's also some code above that initialises some enemy data) IF getfixbit(fixDefaultDissolve) = 0 THEN upgrade_message "Initializing default enemy fade..." setfixbit(fixDefaultDissolve, 1) gen(genEnemyDissolve) = 0 END IF DIM as bool fixdissolves = (getfixbit(fixDefaultDissolveEnemy) = 0) DIM as bool fixelementals = (getfixbit(fixEnemyElementals) = 0) IF full_upgrade ANDALSO (fixdissolves OR fixenemyelementals) THEN IF fixdissolves THEN upgrade_message "Initializing default enemy fade (per enemy)..." END IF IF fixelementals THEN upgrade_message "Initialising enemy elemental resists..." END IF ' loadenemydata will upgrade the data when loading. All we have to do is load and save again. DIM enemy as EnemyDef FOR i as integer = 0 to gen(genMaxEnemy) loadenemydata enemy, i saveenemydata enemy, i NEXT setfixbit(fixDefaultDissolveEnemy, 1) setfixbit(fixEnemyElementals, 1) END IF '------------------------------------------------------------------------------- IF getfixbit(fixPushNPCBugCompat) = 0 THEN upgrade_message "Enabling 'Simulate pushable NPC bug' bitset..." setfixbit(fixPushNPCBugCompat, 1) setbit gen(), genBits2, 0, 1 ' For backcompat END IF IF getfixbit(fixDefaultMaxItem) = 0 THEN setfixbit(fixDefaultMaxItem, 1) 'This is no longer done; genMaxItem has already been set above. 'gen(genMaxItem) = 254 END IF IF getfixbit(fixBlankDoorLinks) = 0 THEN upgrade_message "Disable redundant blank door links..." setfixbit(fixBlankDoorLinks, 1) DIM doorlink_temp(199) as DoorLink DIM found_first as integer FOR i as integer = 0 TO gen(genMaxMap) deserDoorLinks maplumpname(i, "d"), doorlink_temp() found_first = NO FOR j as integer = 0 TO UBOUND(doorlink_temp) WITH doorlink_temp(j) IF .source = 0 AND .tag1 = 0 AND .tag2 = 0 THEN IF found_first = NO THEN 'Ignore the first "always" link for door 0 found_first = YES ELSE IF .dest = 0 AND .dest_map = 0 THEN .source = -1 ' Mark redundant all-zero links as unused END IF END IF END IF END WITH NEXT j serDoorLinks maplumpname(i, "d"), doorlink_temp() NEXT i END IF IF getfixbit(fixShopSounds) = 0 THEN upgrade_message "Set default soundeffects..." setfixbit(fixShopSounds, 1) gen(genItemLearnSFX) = gen(genAcceptSFX) gen(genCantLearnSFX) = gen(genCancelSFX) gen(genBuySFX) = gen(genAcceptSFX) gen(genHireSFX) = gen(genAcceptSFX) gen(genSellSFX) = gen(genAcceptSFX) gen(genCantBuySFX) = gen(genCancelSFX) gen(genCantSellSFX) = gen(genCancelSFX) END IF IF getfixbit(fixExtendedNPCs) = 0 THEN upgrade_message "Initialize extended NPC data..." setfixbit(fixExtendedNPCs, 1) REDIM npctemp(0) as NPCType FOR i as integer = 0 TO gen(genMaxMap) ' These are the garbage data left over from somewhere in the late 90's when ' James decided to make the .N lumps big enough to hold 100 NPC definitions ' even though there was only enough memory available for 36 NPC sprites at a time LoadNPCD maplumpname(i, "n"), npctemp() REDIM PRESERVE npctemp(35) SaveNPCD maplumpname(i, "n"), npctemp() NEXT i END IF IF getfixbit(fixHeroPortrait) = 0 OR getfixbit(fixHeroElementals) = 0 THEN DIM as integer do_portraits = (getfixbit(fixHeroPortrait) = 0) DIM as integer do_elements = (getfixbit(fixHeroElementals) = 0) setfixbit(fixHeroPortrait, 1) setfixbit(fixHeroElementals, 1) DIM as string msgtemp = "Initialize hero " IF do_portraits THEN msgtemp += "portraits" IF do_portraits AND do_elements THEN msgtemp += " and " IF do_elements THEN msgtemp += "elemental resists" upgrade_message msgtemp DIM her as HeroDef FOR i as integer = 0 TO gen(genMaxHero) loadherodata her, i WITH her IF do_portraits THEN .portrait = -1 'Disable .portrait_pal = -1 'Default END IF IF do_elements THEN '.elementals() not initialised, load from old bits FOR i as integer = 0 TO small(7, gen(genNumElements) - 1) .elementals(i) = backcompat_element_dmg(xreadbit(.bits(), i), xreadbit(.bits(), 8 + i), xreadbit(.bits(), 16 + i)) NEXT 'gen(genNumElements) will be more than 8 even in old games after enemytypes are converted to elements FOR i as integer = 8 TO gen(genNumElements) - 1 .elementals(i) = 1 NEXT END IF END WITH saveherodata her, i NEXT i END IF 'This fixbit was introduced at the same time as textbox portraits, 'so if it's not on, then the game doesn't use portraits, so it doesn't 'need to be fixed for Game. IF full_upgrade AND getfixbit(fixTextBoxPortrait) = 0 THEN upgrade_message "Initialize text box portrait data..." setfixbit(fixTextBoxPortrait, 1) 'DIM box as TextBox DIM boxbuf(dimbinsize(binSAY)) as integer DIM recsize as integer = getbinsize(binSAY) \ 2 OPENFILE(game & ".say", FOR_BINARY + ACCESS_READ_WRITE, fh) FOR i as integer = 0 TO gen(genMaxTextBox) 'This was stupefying slow, by far the slowest of all upgrades 'LoadTextBox box, i 'box.portrait_pal = -1 'Default palette 'SaveTextBox box, i loadrecord boxbuf(), fh, recsize, i boxbuf(202) = -1 'Default palette storerecord boxbuf(), fh, recsize, i NEXT i CLOSE #fh END IF IF getfixbit(fixInitDamageDisplay) = 0 THEN upgrade_message "Initialize damage display time/distance data..." setfixbit(fixInitDamageDisplay, 1) gen(genDamageDisplayTicks) = 7 gen(genDamageDisplayRise) = 14 END IF IF getfixbit(fixDefaultLevelCap) = 0 THEN upgrade_message "Set level cap to 99..." setfixbit(fixDefaultLevelCap, 1) gen(genLevelCap) = 99 END IF IF getfixbit(fixDefaultMaxLevel) = 0 THEN upgrade_message "Set max level to 99..." setfixbit(fixDefaultMaxLevel, 1) gen(genMaxLevel) = 99 END IF IF gen(genResolutionX) < 10 THEN gen(genResolutionX) = 320 IF gen(genResolutionY) < 10 THEN gen(genResolutionY) = 200 IF gen(genWindowSize) <= 0 THEN gen(genWindowSize) = 8 '<= 80% of screen IF gen(genLivePreviewWindowSize) <= 0 THEN gen(genLivePreviewWindowSize) = 5 '<= 50% of screen IF getfixbit(fixUNUSED23) = 1 THEN 'James says: this bit was originally planned to initialise gen(192) 'with the maximum tag number 999, but then it was decided that it would 'be better to use a general bitset rather than storing the specific 'number of allowed tags. upgrade_message "Clear wasted fixbit 23" setfixbit(fixUNUSED23, 0) gen(192) = 0 END IF IF getfixbit(fixSetOldAttackPosBit) = 0 THEN upgrade_message "Enabling 'Old attack positioning at bottom-left of target' bit" setfixbit(fixSetOldAttackPosBit, 1) setbit(gen(), genBits2, 20, 1) ' "Old attack positioning at bottom-left of target" END IF IF getfixbit(fixWrapCroppedMapsBit) = 0 THEN setfixbit(fixWrapCroppedMapsBit, 1) 'Since r6473 'Crop' maps no longer wrap IF last_editor_version.branch_revision < 6473 THEN upgrade_message "Enabling 'Wrap map layers over edge of Crop maps' bit" setbit gen(), genBits2, 21, 1 'Wrap map layers over edge of Crop maps END IF END IF IF getfixbit(fixOldElementalFailBit) = 0 THEN upgrade_message "Enabling 'Simulate old fail vs. element resist bit' bitset" setfixbit(fixOldElementalFailBit, 1) setbit gen(), genBits2, 9, 1 END IF IF gen(genItemStackSize) <= 0 OR gen(genItemStackSize) > 99 THEN gen(genItemStackSize) = 99 END IF IF gen(genMillisecPerFrame) = 0 THEN gen(genMillisecPerFrame) = 55 END IF IF getfixbit(fixInitDefaultVolumes) = 0 THEN upgrade_message "Initialising default volumes ..." setfixbit(fixInitDefaultVolumes, 1) gen(genMusicVolume) = 50 gen(genSFXVolume) = 75 END IF IF full_upgrade ANDALSO getfixbit(fixItemElementals) = 0 THEN upgrade_message "Initialising equipment elemental resists..." setfixbit(fixItemElementals, 1) REDIM dat(dimbinsize(binITM)) as integer FOR i as integer = 0 TO gen(genMaxItem) loaditemdata dat(), i FOR j as integer = 0 TO 63 SerSingle(dat(), 82 + j*2, LoadOldItemElemental(dat(), j)) NEXT saveitemdata dat(), i NEXT END IF 'Initialize the non_elemental_for_spawning data IF getfixbit(fixInitNonElementalSpawning) = 0 THEN upgrade_message "Initializing elementals for non-elemental spawning..." setfixbit(fixInitNonElementalSpawning, 1) DIM elementals_node as NodePtr = GetOrCreateChild(general_reld, "elementals") DIM element_node as NodePtr DIM non_elemental_node as NodePtr DIM non_elemental_default as integer FOR i as integer = 0 to 63 non_elemental_default = 0 IF original_genVersion <= 14 ANDALSO (i >= 8 ANDALSO i <= 15) THEN 'This is a type-killer elemental in a game that is being upgraded from RPG version 14 or older non_elemental_default = 1 END IF element_node = AppendChildNode(elementals_node, "element", i) non_elemental_node = AppendChildNode(element_node, "non_elemental", non_elemental_default) NEXT i END IF IF gen(genStartHero) < 0 OR gen(genStartHero) > gen(genMaxHero) THEN upgrade_message "genStartHero invalid: " & gen(genStartHero) gen(genStartHero) = 0 END IF IF gen(genStartTextbox) < 0 OR gen(genStartTextbox) > gen(genMaxTextbox) THEN upgrade_message "genStartTextbox invalid: " & gen(genStartTextbox) gen(genStartTextbox) = 0 END IF IF time_rpg_upgrade THEN upgrade_message "Upgrades complete." debuginfo "Total upgrade time = " & FORMAT(TIMER - upgrade_start_time, ".###") & "s, time wasted on messages = " & FORMAT(upgrade_overhead_time, ".###") & "s" END IF 'Not currently used gen(genErrorLevel) = serrIgnore 'Save changes to GEN lump (important when exiting to the title screen and loading a SAV) xbsave game + ".gen", gen(), 1000 'Save changes to the general.reld lump too! write_general_reld() 'wow! this is quite a big and ugly routine! END SUB SUB zero_out_pre_july_8_1999_general_map_data_garbage () 'Some lost-to-history version of custom.exe wrote garbage to the 'then-unused areas of the .MAP records for General Map Data. ' 'Unfortunately this garbage can only be safely zeroed out for games 'older than July 8 1999, which is the last identifiable version before 'more records were used in the .MAP lump starting with the unidentifiable 'November 9 1999 version that added map wrapping and script-autorun triggers. IF gen(genVersion) > 2 THEN debug "Sanity test: zero_out_pre_july_8_1999_general_map_data_garbage() is unsafe on games newer than July 8 1999" EXIT SUB END IF DIM gmaptmp(dimbinsize(binMAP)) as integer DIM resave as bool = NO FOR i as integer = 0 to gen(genMaxMap) resave = NO loadrecord gmaptmp(), game & ".map", getbinsize(binMAP) \ 2, i FOR j as integer = 4 TO 19 IF gmaptmp(j) <> 0 THEN resave = YES gmaptmp(j) = 0 END IF NEXT j IF resave THEN debug "general map data garbage found and cleared for map " & i storerecord gmaptmp(), game & ".map", getbinsize(binMAP) \ 2, i END IF NEXT i END SUB '========================================================================================== 'Initialise, if needed, a lump that is missing or has length <= headersize by padding the file with zeros so that 'it has a header if any and one record. SUB fix_recordless_lump(lumpname as string, byref record_byte_size as integer, byval header_bytes as integer=0) DIM fh as integer IF OPENFILE(lumpname, FOR_BINARY + ACCESS_READ_WRITE, fh) THEN debugc errError, "Error while opening " & lumpname EXIT SUB END IF DIM total_bytes as integer = LOF(fh) - header_bytes IF total_bytes <= 0 THEN debuginfo "fix_recordless_lump: " & lumpname & " didn't exist or was length " & LOF(fh) extendfile fh, header_bytes + record_byte_size END IF CLOSE #fh END SUB 'Correct a variable holding number of fixed-length-records (last_rec_index + 1 + count_offset) 'to the real amount in a file, and check that the file doesn't have any partial records. 'NOTE: This doesn't fix a lump which is missing or zero length; use fix_recordless_lump for that. SUB fix_record_count(byref last_rec_index as integer, byref record_byte_size as integer, lumpname as string, info as string, byval skip_header_bytes as integer=0, byval count_offset as integer=0) DIM rec_count as integer = last_rec_index + 1 + count_offset IF NOT isfile(lumpname) THEN debuginfo "fix_record_count: " & info & " lump " & trimpath(lumpname) & " does not exist." 'Setting the record count to 1; see below last_rec_index = 0 - count_offset EXIT SUB END IF DIM total_bytes as integer = filelen(lumpname) - skip_header_bytes IF total_bytes <= 0 THEN debuginfo "fix_record_count: " & lumpname & " has no records! (Adjusting record count from " & rec_count & " -> 1)" 'Setting the record count to 0 is likely to cause a crash, set it to 1 instead '(generally this will cause the relevant editor to fix the file (quite by accident)) last_rec_index = 0 - count_offset EXIT SUB END IF IF total_bytes MOD record_byte_size <> 0 THEN DIM diffsize as integer diffsize = total_bytes - record_byte_size * rec_count DIM mismatch as string IF diffsize < 0 THEN mismatch = "file short by " & diffsize & " bytes" ELSE mismatch = "file long by " & diffsize & " bytes" END IF debug "fix_record_count mismatch for " & info & " lump, " & total_bytes & " is not evenly divisible by " & record_byte_size & " (" & mismatch & ")" '--expand the lump to have a valid total size DIM fh as integer OPENFILE(lumpname, FOR_BINARY + ACCESS_READ_WRITE, fh) DO WHILE total_bytes MOD record_byte_size <> 0 total_bytes += 1 PUT #fh, skip_header_bytes + total_bytes, CHR(0) LOOP CLOSE #fh debug "Expanded " & info & " lump to " & total_bytes & " bytes" END IF DIM records as integer = total_bytes \ record_byte_size IF records <> rec_count THEN upgrade_message "Adjusting record count for " & info & " lump, " & rec_count & " -> " & records & " (diff " & records - rec_count & ")" last_rec_index = records - 1 - count_offset END IF END SUB SUB fix_sprite_record_count(byval pt_num as integer) WITH sprite_sizes(pt_num) DIM bytes as integer = .size.x * .size.y * .frames \ 2 '--we divide by 2 because there are 2 pixels per byte DIM lump as string = game & ".pt" & pt_num '.pt7 and .pt8 don't exist in old games fix_recordless_lump lump, bytes fix_record_count gen(.genmax), bytes, lump, .name & " sprites" END WITH END SUB SUB future_rpg_warning () 'This sub displays forward-compat warnings when a new RPG file is loaded in 'an old copy of game, or an old version of custom (ypsiliform or newer) 'Note that if this happens while live-previewing it might be before 'the versions of Game and Custom don't match (we've already shown a warning 'about that), or the game is too new for Custom as well. 'future_rpg_warning can get called multiple times per game STATIC warned_sourcerpg as string IF sourcerpg = warned_sourcerpg THEN EXIT SUB warned_sourcerpg = sourcerpg debug "Unsupported RPG file!" DIM hlcol as integer = uilook(uiText) DIM msg as string msg = hilite("Unsupported RPG File", hlcol) msg += !"\n\nThis game has features that are not supported in this version of the OHRRPGCE. Download the latest version at http://HamsterRepublic.com\n" msg += "Press any key to continue, but " #IFDEF IS_GAME msg += "be aware that some things might not work right..." #ELSE msg += hilite("DO NOT SAVE the game", hlcol) & ", as this will lead to almost certain data corruption!!" #ENDIF clearpage 0 basic_textbox msg, uilook(uiMenuItem), 0 setvispage 0 fadein waitforanykey #IFDEF IS_GAME ' fadeout 0, 0, 0 #ENDIF END SUB SUB check_map_count DIM maplumps(...) as string = {"t", "p", "e", "l", "n", "d"} DIM maplumptypes(...) as string = {"tilemap", "passmap", "foemap", "NPC instances", "NPC definitions", "door links"} 'Zonemaps are optional due to being added later DIM oldmapmax as integer = gen(genMaxMap) gen(genMaxMap) = 0 DO DIM havelump(UBOUND(maplumps)) as bool DIM lumpcount as integer = 0 FOR i as integer = 0 TO UBOUND(maplumps) havelump(i) = isfile(maplumpname(gen(genMaxMap), maplumps(i))) IF havelump(i) THEN lumpcount += 1 NEXT IF lumpcount = 0 THEN gen(genMaxMap) -= 1 EXIT DO END IF IF lumpcount < UBOUND(maplumps) + 1 THEN 'Game actually runs just fine when anything except the foemap is missing; Custom 'has some (crappy) map lump fix code in the map editor FOR i as integer = 0 TO UBOUND(havelump) IF havelump(i) = NO THEN showerror "map " & gen(genMaxMap) & " " & maplumptypes(i) & " is missing!" NEXT 'Continue even if lumps missing, as following maps may be alright END IF gen(genMaxMap) += 1 LOOP 'Note we don't check/fix number of records in .map and .mn IF gen(genMaxMap) < 0 THEN fatalerror "Game has no maps!" IF gen(genMaxMap) <> oldmapmax THEN upgrade_message "Fixed number of maps from " & (oldmapmax + 1) & " to " & (gen(genMaxMap) + 1) END SUB 'Check for corruption and unsupported RPG features (maybe someone forgot to update CURRENT_RPG_VERSION) SUB rpg_sanity_checks 'Check binsize.bin is not from future DIM flen as integer = filelen(workingdir + SLASH + "binsize.bin") IF flen > 2 * (binLASTENTRY + 1) THEN debug "binsize.bin length " & flen future_rpg_warning ELSE FOR bindex as integer = 0 TO binLASTENTRY IF curbinsize(bindex) MOD 2 <> 0 THEN 'curbinsize is INSANE, scream bloody murder to prevent data corruption! fatalerror "Oh noes! curbinsize(" & bindex & ")=" & curbinsize(bindex) & " Please complain to the devs, who may have just done something stupid!" END IF DIM binsize as integer = getbinsize(bindex) IF binsize > curbinsize(bindex) THEN debug "getbinsize(" & bindex & ") = " & binsize & ", but curbinsize = " & curbinsize(bindex) future_rpg_warning END IF NEXT END IF 'Check fixbits.bin is not from future DIM maxbits as integer = filelen(workingdir + SLASH + "fixbits.bin") * 8 FOR i as integer = sizeFixBits TO maxbits - 1 IF getfixbit(i) THEN debug "Unknown fixbit " & i & " set" future_rpg_warning END IF NEXT 'Should this be in upgrade? I can't make up my mind! IF gen(genNumElements) > 64 THEN future_rpg_warning 'We would definitely crash if we didn't cap this gen(genNumElements) = 64 END IF 'Check script file formats IF isfile(game + ".hsp") THEN unlumpfile game + ".hsp", "hs", tmpdir DIM hs_header as HSHeader load_hsp_header tmpdir & "hs", hs_header WITH hs_header IF .valid = NO THEN pop_warning "The scripts in this game appear to be corrupt." ELSEIF .hsp_format > CURRENT_HSP_VERSION OR .script_format > CURRENT_HSZ_VERSION OR .max_function_id > maxScriptCmdID THEN debug "Future HS format: hspeak ver='" & .hspeak_version & "' plotscr ver='" & .plotscr_version _ & "' hsp ver=" & .hsp_format & " (supported=" & CURRENT_HSP_VERSION & ") hsz ver=" & .script_format & " (supported=" _ & CURRENT_HSZ_VERSION & ") max func=" & .max_function_id & " (supported=" & maxScriptCmdID & ")" pop_warning "This game has scripts that are not supported in this version of the OHRRPGCE. Download the latest version at http://HamsterRepublic.com" END IF END WITH END IF END SUB SUB loadglobalstrings 'we load the whole lump into memory because readglobalstring can be called 'hunderds of times a second. It's stored in a raw format; good enough. DIM fh as integer IF OPENFILE(game + ".stt", FOR_BINARY + ACCESS_READ, fh) = fberrOK THEN IF LOF(fh) > 0 THEN global_strings_buffer = STRING(LOF(fh), 0) GET #fh, 1, global_strings_buffer END IF CLOSE #fh END IF END SUB FUNCTION readglobalstring (byval index as integer, default as string, byval maxlen as integer=10) as string IF index * 11 + 2 > LEN(global_strings_buffer) THEN RETURN default ELSE DIM namelen as UBYTE = global_strings_buffer[index * 11] IF maxlen < namelen THEN namelen = maxlen RETURN MID(global_strings_buffer, index * 11 + 2, namelen) END IF END FUNCTION PRIVATE SUB add_item_to_(menu as MenuDef, subtype as integer, hide_if_disabled as bool = NO) append_menu_item(menu, "", mtypeSpecial, subtype) menu.last->hide_if_disabled = hide_if_disabled END SUB ' Initialise the default main menu used for old games SUB create_default_menu(menu as MenuDef, add_sfx_volume as bool = YES) ClearMenuData menu add_item_to_ menu, spItems add_item_to_ menu, spSpells add_item_to_ menu, spStatus add_item_to_ menu, spEquip add_item_to_ menu, spTeamOrOrder add_item_to_ menu, spMapMaybe, YES add_item_to_ menu, spSaveMaybe, YES add_item_to_ menu, spWindowed, YES add_item_to_ menu, spFullscreen, YES IF add_sfx_volume THEN add_item_to_ menu, spMusicVolume add_item_to_ menu, spSoundVolume ELSE ' (When playing old games without sfx) ' Don't need sfx volume, so don't need the Volume menu either, ' but use the caption set for the old Volume option. add_item_to_ menu, spMusicVolume menu.last->caption = readglobalstring(69, "Volume", 10) END IF add_item_to_ menu, spQuit menu.translucent = YES menu.min_chars = 14 END SUB ' Initialise the volume menu used in old games SUB create_volume_menu(menu as MenuDef) ' Load the main menu and copy some of the styling DIM menu_set as MenuSet menu_set.menufile = workingdir + SLASH + "menus.bin" menu_set.itemfile = workingdir + SLASH + "menuitem.bin" DIM mainmenu as MenuDef LoadMenuData menu_set, mainmenu, 0, YES ClearMenuData menu add_item_to_ menu, spMusicVolume add_item_to_ menu, spSoundVolume menu.min_chars = 10 menu.boxstyle = mainmenu.boxstyle menu.textcolor = mainmenu.textcolor menu.translucent = mainmenu.translucent menu.bordersize = mainmenu.bordersize menu.textalign = mainmenu.textalign ClearMenuData mainmenu END SUB FUNCTION bound_arg(n as integer, min as integer, max as integer, argname as zstring ptr, context as zstring ptr = NULL, errlvl as scriptErrEnum = serrBound) as integer 'This function takes zstring ptr arguments because passing strings is actually really expensive '(it performs an allocation, copy, delete), and would be easily noticeable by scripts. IF n < min OR n > max THEN reporterr "invalid " & *argname & " " & n, errlvl, context RETURN NO END IF RETURN YES END FUNCTION 'This is a drop-in replacement for scripterr which can be called from outside 'the script interpreter. It hides some errors when playing a game in release mode, 'just like scripterr. 'context overrides the name of the current script command, if any. It does not override 'the context_string global, which is also reported. SUB reporterr(msg as zstring ptr, errlvl as scriptErrEnum = serrBadOp, context as zstring ptr = NULL) 'It's possible to be currently executing a script command, and for 'context_string to be set, eg "advance textbox" calls advance_text_box, which 'can cause an error. If we have both contexts, then the script command is 'the outer, and context_string the inner, eg "advancetextbox: Textbox 1: invalid hero ID" 'But 'context' is always the innermost DIM full_msg as string = *msg IF context THEN full_msg = *context & ": " END IF IF LEN(context_string) THEN full_msg = context_string & ": " & full_msg END IF #IFDEF IS_GAME IF insideinterpreter THEN IF context = NULL THEN full_msg = interpreter_context_name() & full_msg END IF 'Let scripterr deal with whether to show the error scripterr full_msg, errlvl EXIT SUB END IF IF should_display_error_to_user(errlvl) = NO THEN debugc errDebug, full_msg EXIT SUB END IF #ENDIF IF errlvl >= serrBug THEN debugc errPromptBug, full_msg ELSEIF errlvl >= serrWarn THEN 'errlvl >= serrError: Something like an unreadable data file 'errlvl = serrBadOp or errlvl = serrWarn: Likely something like out-of-bounds data debugc errPromptError, full_msg ELSE 'Info or ignore debugc errInfo, full_msg END IF END SUB Constructor HeroTagsCache() v_new this.checks End Constructor Destructor HeroTagsCache() v_free this.checks End Destructor SUB load_special_tag_caches() DIM her as HeroDef FOR i as integer = 0 TO small(gen(genMaxHero), UBOUND(herotags)) loadherodata her, i herotags(i).have_tag = her.have_tag herotags(i).alive_tag = her.alive_tag herotags(i).leader_tag = her.leader_tag herotags(i).active_tag = her.active_tag v_resize herotags(i).checks, 0 FOR j as integer = 0 to UBOUND(her.checks) v_append herotags(i).checks, her.checks(j) NEXT j NEXT i REDIM item_data(dimbinsize(binITM)) as integer FOR i as integer = 0 TO small(gen(genMaxItem), UBOUND(itemtags)) loaditemdata item_data(), i itemtags(i).have_tag = item_data(74) itemtags(i).in_inventory_tag = item_data(75) itemtags(i).is_equipped_tag = item_data(76) itemtags(i).is_actively_equipped_tag = item_data(77) NEXT i END SUB 'Return whether a tag is automatically set by consulting the 'herotags() and itemtags() arrays and the hero level checks FUNCTION tag_is_autoset(byval tag_id as integer) as bool DIM count as integer = 0 tag_id = ABS(tag_id) IF tag_id <= 1 THEN RETURN NO FOR i as integer = 0 TO small(gen(genMaxHero), UBOUND(herotags)) IF tag_id = herotags(i).have_tag THEN count += 1 IF tag_id = herotags(i).alive_tag THEN count += 1 IF tag_id = herotags(i).leader_tag THEN count += 1 IF tag_id = herotags(i).active_tag THEN count += 1 FOR j as integer = 0 TO v_len(herotags(i).checks) - 1 IF tag_id = herotags(i).checks[j].tag THEN count += 1 NEXT j NEXT i FOR i as integer = 0 TO small(gen(genMaxItem), UBOUND(itemtags)) IF tag_id = itemtags(i).have_tag THEN count += 1 IF tag_id = itemtags(i).in_inventory_tag THEN count += 1 IF tag_id = itemtags(i).is_equipped_tag THEN count += 1 IF tag_id = itemtags(i).is_actively_equipped_tag THEN count += 1 NEXT i RETURN count > 0 END FUNCTION 'Returns one line per place where this tag is autoset. Empty if none. FUNCTION describe_tag_autoset_places(byval tag_id as integer) as string DIM ret as string tag_id = ABS(tag_id) IF tag_id <= 1 THEN RETURN "" DIM kind_name as string FOR i as integer = 0 TO small(gen(genMaxHero), UBOUND(herotags)) '--for each available hero IF tag_id = herotags(i).have_tag THEN ret += "Hero " & i & !" in party tag\n" IF tag_id = herotags(i).alive_tag THEN ret += "Hero " & i & !" is alive tag\n" IF tag_id = herotags(i).leader_tag THEN ret += "Hero " & i & !" is leader tag\n" IF tag_id = herotags(i).active_tag THEN ret += "Hero " & i & !" in active party tag\n" FOR j as integer = 0 TO v_len(herotags(i).checks) - 1 WITH herotags(i).checks[j] SELECT CASE .kind CASE TagRangeCheckKind.level kind_name = "level" CASE ELSE kind_name = "???(" & .kind & ")" END SELECT IF tag_id = .tag THEN ret += "Hero " & i & " " & kind_name & " check " & j & !"\n" END WITH NEXT j NEXT i FOR i as integer = 0 TO maxMaxItems IF tag_id = itemtags(i).have_tag THEN ret += "Item " & i & !" have tag\n" IF tag_id = itemtags(i).in_inventory_tag THEN ret += "Item " & i & !" in inventory tag\n" IF tag_id = itemtags(i).is_equipped_tag THEN ret += "Item " & i & !" is equipped tag\n" IF tag_id = itemtags(i).is_actively_equipped_tag THEN ret += "Item " & i & !" equipped by active hero tag\n" NEXT i RETURN ret END FUNCTION 'This is for *tag conditions*! It makes no sense for bools! Use yesorno or IIF instead. FUNCTION onoroff (byval n as integer) as string IF n >= 0 THEN RETURN "ON" RETURN "OFF" END FUNCTION 'Return a description of a tag condition: ALWAYS/NEVER/IF TAG #=ON/OFF (Name...) 'In Custom consider tag_condition_caption, tag_set_caption, etc. 'zerocap: What zero means. Usually either Always or Never 'maxwidth: Limit string to this many pixels wide FUNCTION describe_tag_condition(tag as integer, zerocap as string, maxwidth as integer = 320) as string IF tag = 0 THEN RETURN zerocap IF tag = 1 THEN RETURN "NEVER" IF tag = -1 THEN RETURN "ALWAYS" DIM ret as string ret = "IF TAG " & ABS(tag) & "=" + onoroff(tag) IF ABS(tag) > 1 THEN ret &= " (" & load_tag_name(ABS(tag)) ret = shorten_to_right(ret, maxwidth - textwidth(")")) ret &= ")" END IF RETURN ret END FUNCTION 'Returns a YES/NO string. Not to be confused with yesno() (in this file) 'which asks an interactive yes/no question. 'If you want to override the defaults, why not use IIF() instead, which is identical? FUNCTION yesorno (byval n as integer, yes_cap as string="YES", no_cap as string="NO") as string IF n THEN RETURN yes_cap RETURN no_cap END FUNCTION 'This is mostly equivalent to '(float * 100) & "%"', however it doesn't show 'exponentials, and it rounds to some number of significant places FUNCTION format_percent(byval float as double, byval sigfigs as integer = 5) as string DIM deciplaces as integer = sigfigs - (INT(LOG(ABS(float * 100)) / LOG(10)) + 1) IF deciplaces > sigfigs THEN deciplaces = sigfigs DIM repr as string = FORMAT(float * 100, "0." & STRING(deciplaces, "#")) 'Unlike STR, FORMAT will add a trailing point IF repr[LEN(repr) - 1] = ASC(".") THEN repr = LEFT(repr, LEN(repr) - 1) RETURN repr + "%" END FUNCTION 'Edit a floating point value and its string representation simultaneously (repr 'effectively stores the editing state). Initialise repr with format_percent(float) 'Returns true if float or repr changed 'decimalplaces actually limits the number of sig. fig.s too, except in front of the decimal point. 'Note: min and max are not in percent: max=1 is 100% FUNCTION percent_grabber(byref float as double, byref repr as string, min as double, max as double, decimalplaces as integer = 4) as bool STATIC clip as double DIM oldfloat as double = float DIM oldrepr as string = repr 'Remove negative (because we trim leading 0's later) and percentage signs repr = LEFT(repr, LEN(repr) - 1) DIM sign as integer = 1 IF LEFT(repr, 1) = "-" THEN sign = -1: repr = MID(repr, 2) '--Textual editing. The following is very similar to strgrabber IF copy_keychord() THEN clip = float IF paste_keychord() THEN float = clip IF keyval(scBackspace) > 1 AND LEN(repr) > 0 THEN repr = LEFT(repr, LEN(repr) - 1) repr += exclusive(getinputtext, "0123456789.") 'Exclude all but last period DIM period as integer = INSTRREV(repr, ".") DO DIM period2 as integer = INSTR(repr, ".") IF period = period2 THEN EXIT DO repr = MID(repr, 1, period2 - 1) + MID(repr, period2 + 1) period -= 1 LOOP 'Enforce sig. fig.s/decimal places limit IF period THEN repr = LEFT(repr, large(period, decimalplaces + 1)) 'Trim leading 0's repr = LTRIM(repr, "0") IF LEN(repr) = 0 ORELSE repr[0] = ASC(".") THEN repr = "0" + repr IF sign = -1 THEN repr = "-" + repr '--Numerical editing. float = VAL(repr) / 100 IF float = 0.0 ANDALSO sign = -1 THEN repr = MID(repr, 2) 'Convert -0 to 0 DIM increment as double = 0.01 period = INSTR(repr, ".") IF period THEN increment *= 0.1 ^ (LEN(repr) - period) END IF DIM changed as integer = NO 'Whether to replace repr IF keyval(scLeft) > 1 THEN float -= increment changed = YES END IF IF keyval(scRight) > 1 THEN float += increment changed = YES END IF IF (keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1) AND min < 0.0 THEN float = -float changed = YES END IF IF (keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1) AND max > 0.0 THEN float = ABS(float) changed = YES END IF 'Cleanup DIM temp as double = float float = bound(float, min, max) IF changed OR float <> temp THEN repr = format_percent(float, decimalplaces) ELSE repr += "%" END IF RETURN (oldfloat <> float) ORELSE (oldrepr <> repr) END FUNCTION FUNCTION percent_grabber(byref float as single, byref repr as string, min as double, max as double, decimalplaces as integer = 4) as bool DIM temp as double = float DIM ret as bool = percent_grabber(temp, repr, min, max, decimalplaces) float = temp RETURN ret END FUNCTION FUNCTION load_tag_name (byval index as integer) as string IF index = 0 THEN RETURN "" IF index = 1 THEN RETURN "Never" IF index = -1 THEN RETURN "Always" IF index > gen(genMaxTagName) THEN RETURN "" RETURN readbadgenericname(ABS(index), game + ".tmn", 42, 0, 20, , NO) END FUNCTION SUB save_tag_name (tagname as string, byval index as integer) DIM buf(20) as integer writebadbinstring tagname, buf(), 0, 20 storerecord buf(), game + ".tmn", 42 \ 2, index END SUB SUB dump_integer_array_as_hex (arraydim as string, byval start as uinteger ptr, byval _ubound as integer, byval nibbles as integer = 8) DIM hexstring as string = " DIM " & arraydim & " = {" FOR i as integer = 0 TO _ubound hexstring = hexstring & "&h" & hex(start[i], nibbles) IF i <> _ubound THEN hexstring = hexstring & "," IF LEN(hexstring) > 88 THEN hexstring = hexstring & "_" debug hexstring hexstring = " " END IF NEXT i hexstring = hexstring & "}" debug hexstring END SUB SUB dump_master_palette_as_hex (master_palette() as RGBColor) dump_integer_array_as_hex("colorcodes(255) as integer", CAST(uinteger ptr, @master_palette(0).col), 255, 6) END SUB 'dump_integer_array_as_hex "default_font(1023) as ushort", @current_font(0), 1023, 4 SUB load_default_master_palette (master_palette() as RGBColor) 'To regenerate this if the default master palette changes, use dump_master_palette_as_hex DIM colorcodes(255) as integer = {&h000000,&h232222,&h312F2B,&h3F3B34,&h4C483C,&h5D5747,_ &h716A54,&h857C61,&h9A8F6D,&hAFA277,&hC4B581,&hD8C68B,&hEAD694,&hFDBC3B,&hFC9D47,_ &hFA7D53,&h0D0F0D,&h121111,&h2A2426,&h41323B,&h583D51,&h6F456D,&h81577B,&h916684,_ &hA2778D,&hB28997,&hC39CA3,&hD3B0B0,&hE3C9C4,&hEEDCD6,&hF4E7E4,&hFAF3F3,&h1F221E,_ &h0C0E1C,&h1C203E,&h2A305E,&h39407D,&h495198,&h5962B1,&h6975C4,&h8084D0,&h9793DD,_ &hAEA2EA,&hC1B8F1,&hD3CEF7,&hE2DFFC,&hECEBFF,&hF6F6FF,&h2F342E,&h15091C,&h2B1239,_ &h411B56,&h562473,&h6C2D90,&h8236AC,&h9740C9,&hAD49E6,&hC154FF,&hCB68FF,&hD57CFF,_ &hDF90FF,&hE9A4FF,&hF2B8FF,&hFCCCFF,&h40463E,&h060E27,&h0E2059,&h153289,&h1B45AE,_ &h1E5DC0,&h2179D3,&h2294DD,&h24B0E6,&h25CEF0,&h27EEF9,&h3DFFFA,&h75FFFB,&hA3FFFC,_ &hC4FFFD,&hE4FFFE,&h4F595A,&h170000,&h340000,&h500000,&h6B0000,&h870000,&hA30000,_ &hBF0000,&hDC2D2D,&hFA5F5F,&hFF7F7F,&hFF9D9D,&hFFB9B9,&hFFD0D0,&hFFE1E1,&hFFF1F1,_ &h5F6B75,&h140F00,&h2D2200,&h463400,&h5E4600,&h765800,&h8E6B00,&hA67D00,&hBF8F00,_ &hD7A100,&hEFB300,&hFFC70D,&hFFDD30,&hFFEF4D,&hFFFC62,&hFFFFB4,&h707D8F,&h140614,_ &h2E0F2E,&h471747,&h5F1F5F,&h782878,&h913091,&hAB3CA2,&hC74AB0,&hE358BE,&hFF67CC,_ &hFF8AD8,&hFFACE3,&hFFC7EC,&hFFDBF3,&hFFEFF9,&h898CA0,&h1B0904,&h3D150A,&h5A2419,_ &h76352C,&h91463F,&hAC5752,&hBF6666,&hD17579,&hE4848C,&hF693A0,&hFFA8B5,&hFFC2CB,_ &hFFD7DD,&hFFE7EA,&hFFF6F7,&hA19CB0,&h080B0E,&h121921,&h1B2733,&h363B45,&h4D484B,_ &h61504B,&h75584B,&h89614B,&h9E694B,&hB1774F,&hC38C56,&hD3A560,&hDFC171,&hE8D67D,_ &hF1EA89,&hBAABC1,&h091207,&h162911,&h20411C,&h285B2A,&h45692A,&h647729,&h7B8639,_ &h90964E,&hA2A860,&hAFBE6C,&hBDD379,&hC9E784,&hD3F88E,&hDFFFA7,&hF1FFD8,&hCCBDD0,_ &h0F0F0F,&h232221,&h363331,&h494D3C,&h436443,&h4F7A54,&h5A8F67,&h64A57D,&h6DBA96,_ &h76CFB1,&h7DE5D0,&h84F9F1,&hA4FFFB,&hC4FFFC,&hE5FFFE,&hDAD0DD,&h161010,&h322524,_ &h4D3836,&h6A4C44,&h836052,&h9A7360,&hAF846C,&hC29478,&hD4A484,&hE1B494,&hEDC2A2,_ &hF6D2B6,&hF9E2CF,&hFBECE1,&hFDF7F2,&hE6E0E8,&h0B230B,&h0E300E,&h123D12,&h154C15,_ &h196119,&h1E771E,&h228B22,&h379F37,&h3CB23A,&h44C53D,&h65D95D,&h6BEB61,&h98FA90,_ &hCCFFCA,&hE5FFE9,&hEFEBF0,&h180B09,&h371916,&h52281E,&h6B3824,&h84492A,&h9E5A30,_ &hB56E24,&hCD8316,&hDF9814,&hE6AD33,&hECC253,&hF3D773,&hF7E69A,&hFAEFBE,&hFCF7E2,_ &hFFFFFF,&h000000,&h001D48,&h002C6F,&h003B95,&h004BBC,&h005AE2,&h076DFF,&h258BFF,_ &h43A9FF,&h61C7FF,&h85D6FF,&hA8E2FF,&hC5EBFF,&hDAF2FF,&hEEF9FF} FOR i as integer = 0 TO 255 master_palette(i).col = colorcodes(i) NEXT i END SUB 'You should probably be calling enter_space_click() instead of calling this directly 'You should call usemenu before calling this, to ensure state.pt is correct! FUNCTION menu_click (state as MenuState) as bool DIM use_mouse as bool = YES #IFDEF IS_GAME use_mouse = get_gen_bool("/mouse/mouse_menus") OR (force_use_mouse > 0) #ENDIF IF use_mouse THEN IF readmouse().release AND mouseleft THEN IF overlay_message_visible() THEN 'If an allmodex overlay message is visible, dismiss it instead. 'NOTE: because code tends to keep a copy of MouseInfo returned from readmouse, 'the following clearclick() may not have an effect show_overlay_message "", 0. readmouse().clearclick(mouseLeft) RETURN NO END IF 'If you click off the menu then .hover = .first - 1, and if you click on an 'unselectable item then also .pt <> .hover. 'Also handle the case where the menu is empty, so .pt = .first - 1 IF state.pt = state.hover AND state.pt >= state.first THEN RETURN YES END IF END IF RETURN NO END FUNCTION FUNCTION menu_click_outside(m as MenuDef) as bool DIM use_mouse as bool = YES #IFDEF IS_GAME use_mouse = get_gen_bool("/mouse/mouse_menus") OR (force_use_mouse > 0) #ENDIF IF use_mouse THEN IF readmouse().release AND mouseLeft THEN IF NOT rect_collide_point(m.rect, readmouse().pos) THEN RETURN YES END IF END IF RETURN NO END FUNCTION 'Whether a mouse click should dismiss a modal message box FUNCTION click_dismiss () as bool IF readmouse.release AND (mouseLeft OR mouseRight) THEN readmouse.clearclick(mouseLeft) readmouse.clearclick(mouseRight) RETURN YES END IF END FUNCTION 'You should call usemenu before calling this, to ensure state.pt is correct! FUNCTION enter_space_click (state as MenuState) as bool IF menu_click(state) THEN RETURN YES RETURN keyval(scAnyEnter) > 1 OR keyval(scSpace) > 1 END FUNCTION FUNCTION enter_or_space () as bool RETURN keyval(scAnyEnter) > 1 OR keyval(scSpace) > 1 END FUNCTION FUNCTION toggle_item (state as MenuState) as bool RETURN keyval(scLeft) > 1 OR keyval(scRight) > 1 OR enter_space_click(state) END FUNCTION 'Automatically sets state.need_update FUNCTION bitgrabber (byref bitsets as integer, whichbit as integer, byref state as MenuState) as bool IF toggle_item(state) THEN bitsets XOR= whichbit state.need_update = YES RETURN YES END IF END FUNCTION 'Automatically sets state.need_update FUNCTION boolgrabber (byref thebool as bool, byref state as MenuState) as bool IF thebool <> 0 ANDALSO thebool <> -1 THEN 'Safety check, to make sure we really act like we're toggling a bool debug "boolgrabber: fixed badness" thebool = YES END IF IF toggle_item(state) THEN thebool XOR= YES state.need_update = YES RETURN YES END IF END FUNCTION FUNCTION copy_keychord () as bool RETURN (keyval(scCtrl) > 0 AND keyval(scInsert) > 1) OR (keyval(scShift) > 0 AND keyval(scDelete) > 0) OR (keyval(scCtrl) > 0 AND keyval(scC) > 1) END FUNCTION FUNCTION paste_keychord () as bool RETURN (keyval(scShift) > 0 AND keyval(scInsert) > 1) OR (keyval(scCtrl) > 0 AND keyval(scV) > 1) END FUNCTION 'Returns -1 for jump to previous 'find' match, 1 for jump to next match, and 0 for neither FUNCTION find_next_or_prev_keychord () as integer 'Emacs: C-s/C-r IF keyval(scCtrl) > 0 AND keyval(scS) > 1 THEN RETURN 1 IF keyval(scCtrl) > 0 AND keyval(scR) > 1 THEN RETURN -1 'Windows and many other editors: F3/Shift-F3 IF keyval(scF3) > 1 THEN RETURN IIF(keyval(scShift), -1, 1) 'Mac: Cmd-G/Shift-Cmd-G IF (keyval(scLeftCommand) OR keyval(scRightCommand)) > 0 AND keyval(scG) > 1 THEN RETURN IIF(keyval(scShift), -1, 1) END IF RETURN 0 END FUNCTION SUB lockstep_tile_animation (tilesets() as TilesetData ptr, byval layer as integer) 'Called after changing a layer's tileset to make sure its tile animation is in phase with other layers of the same tileset FOR i as integer = 0 TO UBOUND(tilesets) IF i <> layer ANDALSO tilesets(i) ANDALSO tilesets(i)->num = tilesets(layer)->num THEN tilesets(layer)->anim(0) = tilesets(i)->anim(0) tilesets(layer)->anim(1) = tilesets(i)->anim(1) EXIT SUB END IF NEXT END SUB SUB unloadtilesetdata (byref tileset as TilesetData ptr) IF tileset <> NULL THEN 'debug "unloading tileset " & tileset->num frame_unload @tileset->spr DELETE tileset tileset = NULL END IF END SUB SUB maptilesetsprint (tilesets() as TilesetData ptr) FOR i as integer = 0 TO UBOUND(tilesets) IF tilesets(i) = NULL THEN debug i & ": NULL" ELSE debug i & ": " & tilesets(i)->num END IF NEXT END SUB SUB loadtilesetdata (tilesets() as TilesetData ptr, byval layer as integer, byval tilesetnum as integer) 'Loads a tileset into tilesets(layer), and sets animation state appropriately 'No effect if the tileset is already set. IF tilesets(layer) <> NULL ANDALSO tilesets(layer)->num = tilesetnum THEN EXIT SUB unloadtilesetdata tilesets(layer) tilesets(layer) = NEW TilesetData WITH *tilesets(layer) .num = tilesetnum .spr = frame_load(sprTypeTileset, tilesetnum) load_tile_anims tilesetnum, .tastuf() END WITH FOR i as integer = 0 TO 1 WITH tilesets(layer)->anim(i) .cycle = 0 .pt = 0 .skip = 0 END WITH NEXT lockstep_tile_animation tilesets(), layer END SUB FUNCTION layer_tileset_index(byval layer as integer) as integer 'return the gmap() index containing a layer's tileset IF layer <= 2 THEN RETURN 22 + layer ELSEIF layer <= 7 THEN RETURN 26 + (layer - 3) ELSEIF layer <= 15 THEN RETURN 370 + (layer - 8) ELSE debugc errPromptBug, "layer_tileset_index: Bad map layer " & layer END IF END FUNCTION SUB loadmaptilesets (tilesets() as TilesetData ptr, gmap() as integer, resetanimations as bool = YES) 'tilesets() may contain already loaded tilesets. In this case, we can reuse them '(Actually, whether they get reused, and therefore whether the animation state is preserved, depends on 'their order -- eww. But noone will ever notice) DIM tileset as integer FOR i as integer = 0 TO UBOUND(tilesets) tileset = gmap(layer_tileset_index(i)) IF tileset <> 0 THEN tileset = tileset - 1 ELSE tileset = gmap(0) END IF loadtilesetdata tilesets(), i, tileset IF resetanimations THEN FOR j as integer = 0 TO 1 WITH tilesets(i)->anim(j) .cycle = 0 .pt = 0 .skip = 0 END WITH NEXT END IF NEXT END SUB SUB reloadtileanimations (tilesets() as TilesetData ptr, gmap() as integer) DIM tileset as integer FOR i as integer = 0 TO UBOUND(tilesets) tileset = gmap(layer_tileset_index(i)) IF tileset <> 0 THEN tileset = tileset - 1 ELSE tileset = gmap(0) END IF load_tile_anims tileset, tilesets(i)->tastuf() FOR j as integer = 0 TO 1 WITH tilesets(i)->anim(j) .cycle = 0 .pt = 0 .skip = 0 END WITH NEXT NEXT END SUB SUB unloadmaptilesets (tilesets() as TilesetData ptr) FOR i as integer = 0 TO UBOUND(tilesets) unloadtilesetdata tilesets(i) NEXT END SUB SUB set_map_edge_draw_mode(gmap() as integer, wrap_layers_over_edge_of_crop_maps as bool = NO) SELECT CASE gmap(5) CASE 0 'Crop IF wrap_layers_over_edge_of_crop_maps THEN setoutside -1 ELSE 'We set an edge tile on crop maps in case the map is smaller than the screen setoutside gmap(6) END IF CASE 1 'Wrap setoutside -1 CASE 2 'Use default edge tile setoutside gmap(6) END SELECT END SUB FUNCTION xreadbit (bitarray() as integer, byval bitoffset as integer, byval intoffset as integer=0) as bool 'This is a wrapper for readbit that returns YES/NO and accepts a default arg of zero for the integer offset RETURN readbit(bitarray(), intoffset, bitoffset) <> 0 END FUNCTION FUNCTION getheroname (byval hero_id as integer) as string DIM her as HeroDef IF hero_id >= 0 THEN loadherodata her, hero_id RETURN her.name END IF RETURN "" END FUNCTION FUNCTION get_text_box_height(byref box as TextBox) as integer IF box.shrink >= 0 THEN RETURN 88 - box.shrink * 4 FOR i as integer = UBOUND(box.text) TO 0 STEP -1 IF LEN(TRIM(box.text(i))) > 0 THEN DIM vsize as integer = 20 + i * 10 IF vsize < 32 AND vsize > 24 THEN RETURN 32 IF vsize <= 24 THEN RETURN 16 RETURN vsize END IF NEXT i RETURN 88 END FUNCTION FUNCTION last_inv_slot() as integer '--If genMaxInventory is 0, return the default inventory size IF gen(genMaxInventory) = 0 THEN RETURN inventoryMax '--Otherwise round genMaxInventory up to the nearest '-- multiple of three (counting the zero-slot) and return it. RETURN ((gen(genMaxInventory) + 3) \ 3) * 3 - 1 END FUNCTION SUB setup_sprite_sizes () 'Populates the global sprite_sizes WITH sprite_sizes(sprTypeFrame) '-1 .name = "Frame or Asset" .size.x = 16 .size.y = 16 .frames = 1 .directions = 1 .genmax = -1 END WITH WITH sprite_sizes(sprTypeHero) '0 .name = "Hero" .size.x = 32 .size.y = 40 .frames = 8 .directions = 4 .genmax = genMaxHeroPic END WITH WITH sprite_sizes(sprTypeSmallEnemy) '1 .name = "Small Enemy" .size.x = 34 .size.y = 34 .frames = 1 .directions = 1 .genmax = genMaxEnemy1Pic END WITH WITH sprite_sizes(sprTypeMediumEnemy) '2 .name = "Medium Enemy" .size.x = 50 .size.y = 50 .frames = 1 .directions = 1 .genmax = genMaxEnemy2Pic END WITH WITH sprite_sizes(sprTypeLargeEnemy) '3 .name = "Large Enemy" .size.x = 80 .size.y = 80 .frames = 1 .directions = 1 .genmax = genMaxEnemy3Pic END WITH WITH sprite_sizes(sprTypeWalkabout) '4 .name = "Walkabout" .size.x = 20 .size.y = 20 .frames = 8 .directions = 4 .genmax = genMaxNPCPic END WITH WITH sprite_sizes(sprTypeWeapon) '5 .name = "Weapon" .size.x = 24 .size.y = 24 .frames = 2 .directions = 1 .genmax = genMaxWeaponPic END WITH WITH sprite_sizes(sprTypeAttack) '6 .name = "Attack" .size.x = 50 .size.y = 50 .frames = 3 .directions = 1 .genmax = genMaxAttackPic END WITH WITH sprite_sizes(sprTypeBoxBorder) '7 .name = "Box Border" .size.x = 16 .size.y = 16 .frames = 16 .directions = 1 .genmax = genMaxBoxBorder END WITH WITH sprite_sizes(sprTypePortrait) '8 .name = "Portrait" .size.x = 50 .size.y = 50 .frames = 1 .directions = 1 .genmax = genMaxPortrait END WITH WITH sprite_sizes(sprTypeBackdrop) '9 .name = "Backdrop" .size.x = 320 .size.y = 200 .frames = 1 .directions = 1 .genmax = genNumBackdrops .genmax_offset = -1 END WITH WITH sprite_sizes(sprTypeTileset) '10 .name = "Tileset" .size.x = 20 .size.y = 160 * 20 .frames = 1 .directions = 1 .genmax = genMaxTile END WITH WITH sprite_sizes(sprTypeOther) '11, only for temporary use in the test arbitrary-size spriteset editor .name = "Experimental" .frames = 1 .directions = 1 .genmax = -1 'this should throw an error rather than corrupting gen(0) END WITH END SUB SUB load_sprite_and_pal (byref img as GraphicPair, byval spritetype as SpriteType, byval index as integer, byval palnum as integer=-1) unload_sprite_and_pal img IF spritetype = sprTypeBackdrop THEN img.sprite = frame_load(spritetype, index) ELSEIF spritetype >= 0 AND spritetype <= sprTypeLastPT THEN img.sprite = frame_load(spritetype, index) img.pal = palette16_load(palnum, spritetype, index) ELSE debug "load_sprite_and_pal: bad spritetype " & spritetype & " (index " & index & " pal " & palnum & ")" END IF END SUB SUB unload_sprite_and_pal (byref img as GraphicPair) frame_unload @img.sprite palette16_unload @img.pal END SUB ' Convert a UTF8 string to the encoding used for the current game (Latin-1 or ASCII) ' Any codepoints not in Latin-1/ASCII get converted to '?' FUNCTION utf8_to_OHR(utf8string as ustring) as string DIM ret as string = utf8_to_latin1(utf8string) DIM as integer icons_low, icons_high IF get_font_type(current_font()) = ftypeLatin1 THEN icons_low = 127 icons_high = 160 ELSE icons_low = 127 icons_high = 255 END IF FOR idx as integer = 0 TO LEN(ret) - 1 IF icons_low <= ret[idx] AND ret[idx] <= icons_high THEN ret[idx] = ASC("?") END IF NEXT RETURN ret END FUNCTION 'context describes the string string (eg line number) 'If show_warnings is true, then warnings about the text encoding 'related to icons in the font are displayed. You only want those 'warnings displayed for imported text files, not for internal file 'formats like slicelookup.txt though, because they may occur if the 'user changes the font type. FUNCTION decode_backslash_codes(s as string, context as string = "", byref show_warnings as bool = NO) as string DIM result as string = "" DIM ch as string DIM mode as integer = 0 DIM nstr as string DIM num as integer DIM as integer icons_low, icons_high DIM encoding_name as string IF get_font_type(current_font()) = ftypeLatin1 THEN icons_low = 127 icons_high = 160 encoding_name = "Latin-1" ELSE icons_low = 127 icons_high = 255 encoding_name = "7-bit ASCII" END IF DIM icon_warn as bool = NO FOR i as integer = 1 TO LEN(s) ch = MID(s, i, 1) SELECT CASE mode CASE 0'--normal IF ch = "\" THEN mode = 1 nstr = "" ELSEIF ch = !"\t" THEN 'Spaces to pad to column number that's a multiple of 4 result &= SPACE(4 - POSMOD(LEN(result), 4)) ELSE IF (ASC(ch) < 32 AND ASC(ch) <> 10) OR (ASC(ch) >= icons_low AND ASC(ch) <= icons_high) THEN debuginfo "decode_backslash_codes: found nonprintable char " & ASC(ch) icon_warn = YES END IF result &= ch END IF CASE 1'--parsing backslash SELECT CASE ch CASE "\" '--an escaped backslash result &= "\" mode = 0 CASE "n" '-- a newline result &= CHR(10) mode = 0 CASE "r" '-- a carriage return result &= CHR(13) mode = 0 CASE "t" '-- a tab result &= CHR(9) mode = 0 CASE "0", "1", "2" nstr &= ch mode = 2 CASE ELSE '--not a valid backslash code, resume without discarding the backslash result &= "\" & ch mode = 0 END SELECT CASE 2'--parsing ascii code number SELECT CASE ch CASE "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" nstr &= ch CASE ELSE 'busted backslash code, print warning visible_debug context & " Bogus backslash ascii escape code in string """ & s & """" mode = 0 END SELECT IF LEN(nstr) >= 3 THEN num = str2int(nstr) IF num > 255 THEN visible_debug context & " Bogus backslash ascii escape code (larger than 255) in string """ & s & """" ELSE result &= CHR(num) END IF mode = 0 END IF END SELECT NEXT IF mode <> 0 THEN visible_debug context & " Bogus backslash code (mode=" & mode & ") at end of string """ & s & """" END IF IF icon_warn AND show_warnings THEN visible_debug context & " This string contains characters which apparently aren't in your font. Maybe your text editor saved the file using the wrong encoding? Expected " & encoding_name & !". (Press ESC to skip warnings)\n\n" & s IF keyval(scESC) THEN show_warnings = NO END IF RETURN result END FUNCTION 'The name is not totally correct, actually escapes non-printable characters and icons in the font, 'which those are depends on the font type FUNCTION escape_nonprintable_ascii(istr as string) as string DIM result as string = "" DIM as integer icons_low, icons_high IF get_font_type(current_font()) = ftypeLatin1 THEN icons_low = 127 icons_high = 160 ELSE icons_low = 127 icons_high = 255 END IF FOR idx as integer = 1 TO LEN(istr) DIM ch as string = MID(istr, idx, 1) SELECT CASE ASC(ch) CASE 92 '--Backslash result &= "\\" CASE 10 result &= "\n" CASE 13 result &= "\r" CASE 9 result &= "\t" CASE 0 TO 31, icons_low TO icons_high DIM nstr as string = STR(ASC(ch)) WHILE LEN(nstr) < 3 nstr = "0" & nstr WEND result &= "\" & nstr 'CASE 32 TO 91, 93 TO 126, latin_low TO latin_high CASE ELSE result &= ch END SELECT NEXT idx RETURN result END FUNCTION 'This is like escape_nonprintable_ascii except that it replaced non printable characters and icons 'with something else or removes them. FUNCTION remove_nonprintable_ascii(s as string, replacement as string = "") as string DIM as integer icons_low, icons_high IF get_font_type(current_font()) = ftypeLatin1 THEN icons_low = 127 icons_high = 160 ELSE icons_low = 127 icons_high = 255 END IF DIM ret as string FOR i as integer = 0 TO LEN(s) - 1 SELECT CASE s[i] CASE IS < 32 'Tabs ignored because OHR strings shouldn't contain them CASE icons_low TO icons_high CASE ELSE ret &= CHR(s[i]) END SELECT NEXT RETURN ret END FUNCTION FUNCTION sanitize_script_identifier (ident as string, allow_whitespace as bool = YES) as string ' :'_~? are allowed DIM to_exclude as string = "`!@#$%^&*()-+=[]{}\\|<>,.;/""" IF allow_whitespace = NO THEN to_exclude &= " " DIM tmp as string = exclude(ident, to_exclude) RETURN remove_nonprintable_ascii(tmp) END FUNCTION 'Prompts the user for a file (or directory) path 'ext: The file extension; must start with '.' 'directory: If blank, default to curdir. Is set to the directory used, even if canceled. 'The filename entered by the user is restricted to ASCII, meaning it doesn't have to 'be encoded before being used, but the whole path return still needs to be run through 'decode_filename() before being displayed. FUNCTION inputfilename (query as string, ext as string, byref directory as string, helpkey as string, default as string="", allow_overwrite as bool=YES) as string DIM filename as string = default DIM tog as integer DIM foldermode as bool = NO 'Can get passed a filename maybe because it's shared with browse() (as it is in importbmp) IF real_isfile(directory) THEN directory = trimfilename(directory) IF directory = "" THEN directory = CURDIR setkeys YES DO setwait 55 setkeys YES tog = tog XOR 1 IF keyval(scEsc) > 1 THEN RETURN "" IF keyval(scF1) > 1 THEN show_help helpkey IF keyval(scUp) > 1 ORELSE keyval(scDown) > 1 THEN foldermode = NOT foldermode IF (foldermode AND enter_or_space()) OR keyval(scTAB) > 1 THEN DIM newdir as string newdir = browse(11, directory, "", "browse_for_folder") IF isdir(newdir) ANDALSO diriswriteable(newdir) THEN directory = newdir foldermode = NO END IF setkeys YES 'clear ENTER END IF IF foldermode = NO THEN strgrabber filename, 40 filename = fixfilename(filename) IF keyval(scAnyEnter) > 1 THEN filename = TRIM(filename) IF filename <> "" THEN IF get_file_type(directory + SLASH + filename + ext) <> fileTypeNonexistent THEN If allow_overwrite THEN IF yesno("That already exists, overwrite?") THEN RETURN directory + SLASH + filename ELSE notification filename & ext & " already exists" END IF ELSE RETURN directory + SLASH + filename END IF END IF END IF END IF clearpage dpage textcolor uilook(uiText), 0 printstr query, pCentered, 20, dpage printstr "Output directory (TAB to select):", pCentered, 35, dpage IF foldermode THEN textcolor uilook(uiSelectedItem + tog), uilook(uiHighlight) ELSE textcolor uilook(uiMenuItem), uilook(uiHighlight) END IF DIM display_dir as string = decode_filename(directory) printstr display_dir, pCenteredRight, 45, dpage IF foldermode THEN textcolor uilook(uiMenuItem), uilook(uiHighlight) ELSE textcolor uilook(uiSelectedItem + tog), uilook(uiHighlight) END IF DIM display_fname as string = IIF(LEN(filename), filename, "") printstr display_fname + fgtag(uilook(uiText)) + ext, pCenteredRight, 60, dpage, YES SWAP vpage, dpage setvispage vpage dowait LOOP END FUNCTION FUNCTION getdisplayname (default as string) as string '--Get game's display name DIM n as string = load_gamename() IF n = "" THEN RETURN default RETURN n END FUNCTION SUB getstatnames(statnames() as string) REDIM statnames(11) statnames(0) = readglobalstring(0, "HP", 10) statnames(1) = readglobalstring(1, "MP", 10) statnames(2) = readglobalstring(2, "Atk", 10) statnames(3) = readglobalstring(3, "Aim", 10) statnames(4) = readglobalstring(5, "Def", 10) statnames(5) = readglobalstring(6, "Dodge", 10) statnames(6) = readglobalstring(29, "Magic", 10) statnames(7) = readglobalstring(30, "Will", 10) statnames(8) = readglobalstring(8, "Speed", 10) statnames(9) = readglobalstring(7, "Ctr", 10) statnames(10) = readglobalstring(31, "MP~", 10) statnames(11) = readglobalstring(4, "Hits", 10) END SUB SUB getelementnames(elmtnames() as string) REDIM elmtnames(gen(genNumElements) - 1) FOR i as integer = 0 TO gen(genNumElements) - 1 DIM default as string default = "Element" & i+1 IF i < 8 THEN 'Original indices changed so maxlen could be expanded default = readglobalstring(17 + i, default, 10) ELSEIF i < 16 THEN 'Next 8 elements map to old enemytypes default = LEFT(readglobalstring(1 + i, "EnemyType" & i, 10) + "-killer", 14) END IF elmtnames(i) = readglobalstring(174 + i*2, default, 14) NEXT i END SUB 'See WriteByteStr for the straight-to-file version SUB writebinstring (savestr as string, array() as integer, byval offset as integer, byval maxlen as integer) DIM s as string '--pad savestr to (at least) the right length s = savestr + STRING(maxlen - LEN(s), CHR(0)) '--odd lengths would result in (harmless) garbage IF maxlen AND 1 THEN s += CHR(0): maxlen += 1 '--write length (current not max) array(offset) = small(LEN(savestr), maxlen) FOR i as integer = 1 TO maxlen \ 2 array(offset + i) = s[2 * i - 2] OR (s[2 * i - 1] SHL 8) NEXT END SUB SUB writebinstring (savestr as string, array() as short, byval offset as integer, byval maxlen as integer) DIM s as string '--pad savestr to (at least) the right length s = savestr + STRING(maxlen - LEN(s), CHR(0)) '--odd lengths would result in (harmless) garbage IF maxlen AND 1 THEN s += CHR(0): maxlen += 1 '--write length (current not max) array(offset) = small(LEN(savestr), maxlen) memcpy(@array(offset + 1), @s[0], maxlen) END SUB 'See WriteVStr for the straight-to-file version SUB writebadbinstring (savestr as string, array() as integer, byval offset as integer, byval maxlen as integer, byval skipword as integer=0) '--write current length array(offset) = small(LEN(savestr), maxlen) FOR i as integer = 1 TO small(LEN(savestr), maxlen) array(offset + skipword + i) = savestr[i - 1] NEXT i FOR i as integer = LEN(savestr) + 1 TO maxlen array(offset + skipword + i) = 0 NEXT i END SUB 'Read a string which has been packed one character per byte, with length as one word (INT), 'starting at array(offset). 'See ReadByteStr for the straight-from-file version FUNCTION readbinstring (array() as integer, byval offset as integer, byval maxlen as integer) as string DIM stringlen as integer = bound(array(offset), 0, maxlen) DIM result as string = STRING(stringlen, 0) 'The following is equivalent to 'array2str array(), offset * 2 + 2, maxlen DIM toggle as integer = 0 DIM index as integer = offset + 1 FOR i as integer = 0 TO stringlen - 1 IF toggle = 0 THEN result[i] = array(index) AND &hff ELSE result[i] = (array(index) SHR 8) AND &hff index += 1 END IF toggle XOR= 1 NEXT RETURN result END FUNCTION 'See the overload above. FUNCTION readbinstring (array() as short, byval offset as integer, byval maxlen as integer) as string DIM stringlen as integer = bound(array(offset), 0, maxlen) DIM result as string = STRING(stringlen, 0) memcpy(@result[0], @array(offset + 1), stringlen) RETURN result END FUNCTION 'Read a string which has been packed one character per word (INT), with length as one word 'and 'skipword' many wasted words after the length. 'See ReadVStr for the straight-from-file version FUNCTION readbadbinstring (array() as integer, byval offset as integer, byval maxlen as integer, byval skipword as integer=0) as string DIM result as string = "" DIM stringlen as integer = bound(array(offset), 0, maxlen) DIM n as integer FOR i as integer = 1 TO stringlen '--read an int n = array(offset + skipword + i) '--if the int is a char use it. IF n >= 0 AND n <= 255 THEN '--use it result = result & CHR(n) END IF NEXT i RETURN result END FUNCTION FUNCTION get_settings_dir () as string 'This is for application-wide settings, and prefsdir for games will be a subdirectory of it. 'Also, Custom uses settings_dir as tmpdir (FIXME: should use a temp dir in a proper place. 'And sharing the same tmpdir for all instances of Custom may cause problems.) DIM settings_dir as string #IFDEF __FB_ANDROID__ settings_dir = CURDIR & SLASH & "settings" #ELSEIF defined(__FB_DARWIN__) 'FIXME: on OSX we should not use ~/.ohrrpgce, but instead ~/Library/Application Support/OHRRPGCE (settings and data), 'or ~/Library/Caches/OHRRPGCE (temp files). See 'https://developer.apple.com/library/mac/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1 'However, we can't just switch away from using .ohrrpgce; have to move existing files over settings_dir = ENVIRON("HOME") & "/.ohrrpgce" IF isdir(settings_dir) = NO THEN settings_dir = ENVIRON("HOME") & "/Library/Application Support/OHRRPGCE" END IF #ELSEIF DEFINED(__FB_UNIX__) settings_dir = ENVIRON("HOME") & "/.ohrrpgce" #ELSEIF DEFINED(__FB_WIN32__) settings_dir = ENVIRON("APPDATA") & SLASH & "OHRRPGCE" '--APPDATA enviroment variable doesn't exist in Windows 98 so we need a fallback IF ENVIRON("APPDATA") = "" THEN settings_dir = exepath & SLASH & "OHRRPGCE-settings" END IF #ELSE #ERROR "Unknown OS" #ENDIF IF NOT isdir(settings_dir) THEN makedir(settings_dir) RETURN settings_dir END FUNCTION FUNCTION get_home_dir() as string #IFDEF __FB_WIN32__ RETURN ENVIRON("USERPROFILE") #ELSE RETURN ENVIRON("HOME") #ENDIF END FUNCTION 'Returns location of user's Documents dir or some fallback, which is the default 'directory for saving user files like new .rpg files 'if there is no more 'appropriate place available. Therefore it must be writable. FUNCTION get_documents_dir() as string DIM ret as string #IF defined(__FB_ANDROID__) 'FIXME: $HOME is (usually) empty on Android; there is no home directory 'This should be set to the user documents directory: 'Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) #ELSEIF defined(__FB_DARWIN__) 'OSX does not change the standard user directory paths depending on the language, 'instead it only changes the "display name" of these directories. We don't support this. 'See https://developer.apple.com/library/mac/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW10 ret = ENVIRON("HOME") & "/Documents" #ELSEIF defined(__FB_UNIX__) ' I don't know how true this is ret = ENVIRON("HOME") & "/Documents" #ELSEIF defined(__FB_WIN32__) 'Localised and customisable; need to call winapi ret = os_get_documents_dir() #ELSE #ERROR "Unknown OS" #ENDIF IF diriswriteable(ret) = NO THEN ret = get_home_dir() & SLASH "Desktop" IF diriswriteable(ret) = NO THEN ret = CURDIR IF diriswriteable(ret) = NO THEN ret = app_dir 'Give up... END IF END IF END IF RETURN ret END FUNCTION PRIVATE FUNCTION help_dir_helper(dirname as string, fname as string) as integer IF LEN(fname) THEN RETURN isfile(dirname + SLASH + fname) ELSE RETURN isdir(dirname) END FUNCTION FUNCTION get_help_dir(helpfile as string="") as string 'what happened to prefsdir? [James: prefsdir only exists for game not custom right now] IF help_dir_helper(documents_dir & SLASH & "ohrhelp", helpfile) THEN RETURN documents_dir & SLASH & "ohrhelp" IF help_dir_helper(exepath & SLASH & "ohrhelp", helpfile) THEN RETURN exepath & SLASH & "ohrhelp" 'platform-specific relative data files path (Mac OS X bundles) IF help_dir_helper(app_resources_dir & SLASH & "ohrhelp", helpfile) THEN RETURN app_resources_dir & SLASH & "ohrhelp" IF LEN(DATAFILES) THEN IF help_dir_helper(DATAFILES & SLASH & "ohrhelp", helpfile) THEN RETURN DATAFILES & SLASH & "ohrhelp" END IF '-- if all else fails, use exepath even if invalid RETURN exepath & SLASH & "ohrhelp" END FUNCTION FUNCTION load_help_file(helpkey as string) as string DIM help_dir as string help_dir = get_help_dir(helpkey & ".txt") IF isdir(help_dir) THEN DIM helpfile as string helpfile = help_dir & SLASH & helpkey & ".txt" IF isfile(helpfile) THEN DIM fh as integer OPENFILE(helpfile, FOR_INPUT, fh) DIM helptext as string = "" DIM s as string DO WHILE NOT EOF(fh) LINE INPUT #fh, s helptext = helptext & s & CHR(10) LOOP CLOSE #fh RETURN helptext END IF END IF RETURN "No help found for """ & helpkey & """" END FUNCTION SUB save_help_file(helpkey as string, text as string) DIM help_dir as string help_dir = get_help_dir() IF NOT isdir(help_dir) THEN IF makedir(help_dir) THEN visible_debug """" & help_dir & """ does not exist and could not be created." EXIT SUB END IF END IF DIM helpfile as string helpfile = help_dir & SLASH & helpkey & ".txt" DIM fh as integer IF OPENFILE(helpfile, FOR_OUTPUT, fh) = 0 THEN DIM trimmed_text as string trimmed_text = RTRIM(text, ANY " " & CHR(13) & CHR(10)) PRINT #fh, trimmed_text CLOSE #fh ELSE visible_debug "help file """ & helpfile & """ is not writeable." END IF END SUB 'OK, NOW it supports negative n too FUNCTION xy_from_int(byval n as integer, byval wide as integer, byval high as integer) as XYPair DIM pair as XYPair n = POSMOD(n, wide * high) 'Mathematical modulo wide*high pair.x = n MOD wide pair.y = n \ wide RETURN pair END FUNCTION FUNCTION int_from_xy(pair as XYPair, byval wide as integer, byval high as integer) as integer RETURN bound(pair.y * wide + pair.x, 0, wide * high - 1) END FUNCTION FUNCTION color_browser_256(byval start_color as integer=0) as integer DIM ret as integer = start_color DIM tog as integer = 0 DIM gridpos as XYPair = (64, 0) DIM tilesize as XYPair = (12, 12) DIM spot as XYPair DIM cursor as XYPair cursor = xy_from_int(start_color, 16, 16) DIM mouse as MouseInfo DIM mouse_active as bool = NO 'Whether the color under the mouse is the one displayed at bottom DIM prev_mouse_vis as CursorVisibility = getcursorvisibility() hidemousecursor setkeys DO setwait 17, 55 setkeys mouse = readmouse IF mouse.moved THEN mouse_active = YES IF keyval(scF1) > 1 THEN show_help "color_browser" DIM mousecursor as XYPair mousecursor = (mouse.pos - gridpos) \ tilesize DIM mouse_over_grid as bool = in_bound(mousecursor.x, 0, 15) AND in_bound(mousecursor.y, 0, 15) DIM mouse_hover_col as integer = int_from_xy(mousecursor, 16, 16) IF keyval(scUp) > 1 THEN mouse_active = NO : cursor.y = loopvar(cursor.y, 0, 15, -1) IF keyval(scDown) > 1 THEN mouse_active = NO : cursor.y = loopvar(cursor.y, 0, 15, 1) IF keyval(scLeft) > 1 THEN mouse_active = NO : cursor.x = loopvar(cursor.x, 0, 15, -1) IF keyval(scRight) > 1 THEN mouse_active = NO : cursor.x = loopvar(cursor.x, 0, 15, 1) ret = int_from_xy(cursor, 16, 16) IF mouse_over_grid THEN IF mouse.buttons THEN cursor = mousecursor IF mouse.release AND mouseLeft THEN ret = mouse_hover_col EXIT DO END IF END IF IF enter_or_space() THEN EXIT DO 'The highlighted color, shown at the bottom DIM showcol as integer = IIF(mouse_over_grid AND mouse_active, mouse_hover_col, ret) 'ESC or right click outside to go back IF keyval(scESC) > 1 ORELSE (mouse_over_grid = NO ANDALSO (mouse.release AND mouseRight)) THEN ret = start_color EXIT DO END IF clearpage dpage 'Draw the grid FOR i as integer = 0 TO 255 spot = xy_from_int(i, 16, 16) DIM where as XYPair = gridpos + spot * tilesize rectangle where.x, where.y, tilesize.x, tilesize.y, i, dpage IF i = showcol THEN drawbox where.x, where.y, tilesize.x, tilesize.y, tog, 1, dpage ELSEIF spot = cursor THEN drawants vpages(dpage), where.x, where.y, tilesize.x, tilesize.y, tog END IF NEXT i textcolor tog, 0 'Mouse cursor tip is offset in the font printstr CHR(2), mouse.x - 2, mouse.y - 2, dpage 'Draw color info textcolor uilook(uiMenuItem), 0 printstr "Color " & showcol, 320 + ancRight, 192, dpage WITH master(showcol) printstr "RGB" & lpad(STR(.r), , 4) & lpad(STR(.g), , 4) & lpad(STR(.b), , 4), 0, 192, dpage END WITH SWAP vpage, dpage setvispage vpage 'Only increment tog every 55ms (secondary setwait timer) IF dowait THEN tog = (tog + 1) MOD 256 LOOP setcursorvisibility prev_mouse_vis RETURN ret END FUNCTION FUNCTION exptolevel (byval level as integer, byval curve as double = 0.2) as integer ' HINT: Customisation goes here :) IF level = 0 THEN RETURN 0 DIM exper as integer = 30 FOR o as integer = 2 TO level exper = exper * (1 + curve) + 5 'FIXME: arbitrary experience cap should be removable IF exper > 1000000 THEN exper = 1000000 NEXT RETURN exper END FUNCTION FUNCTION total_exp_to_level (byval level as integer, byval curve as double = 0.2) as integer DIM total as integer = 0 FOR i as integer = 1 TO level total += exptolevel(i, curve) NEXT RETURN total END FUNCTION FUNCTION current_max_level() as integer RETURN small(gen(genLevelCap), gen(genMaxLevel)) END FUNCTION FUNCTION atlevel (byval lev as integer, byval a0 as integer, byval aMax as integer) as integer 'Stat at a given level, according to an arbitrary curve between two points. IF lev < 0 THEN RETURN 0 RETURN (.8 + lev / 50) * lev * ((aMax - a0) / 275.222) + a0 + .1 END FUNCTION FUNCTION atlevel_quadratic (byval lev as double, byval a0 as double, byval aMax as double, byval midpercent as double) as double 'Stat at a given level, according to an arbitrary curve between two points. 'CHECKME: Is it actually alright to return a double? IF lev < 0 THEN RETURN 0 IF gen(genMaxLevel) <= 0 THEN RETURN aMax DIM as double a, b 'quadratic coefficients (c=0 fixed) b = 4 * midpercent - 1 a = 1 - b DIM as double x = lev / gen(genMaxLevel) RETURN (a * x^2 + b * x) * (aMax - a0) + a0 + .1 END FUNCTION FUNCTION max_tag() as integer IF readbit(gen(), genBits2, 16) THEN 'Don't limit maximum tags to 999 RETURN 15999 ELSE RETURN 999 END IF END FUNCTION FUNCTION ideal_ticks_per_second() as double 'This is an idealized estimate, NOT a measurment of actual FPS 'This is fixed for now, but will be customizable later. RETURN 18.3 END FUNCTION SUB cleanup_global_reload_doc () DIM rnod as reload.NodePtr = reload.DocumentRoot(global_reload_doc) DIM num as integer = reload.NumChildren(rnod) IF num > 0 THEN debug "===WARNING: Leaked " & num & " global reload nodes===" DIM n as reload.NodePtr = reload.FirstChild(rnod) DO WHILE n debug "node: """ & reload.NodeName(n) & """ " & reload.GetString(n) n = reload.NextSibling(n) LOOP END IF FreeChildren rnod END SUB 'Copy a reload node from wherever into the global_reload_doc FUNCTION get_reload_copy (byval n as NodePtr) as NodePtr DIM result as NodePtr result = CloneNodeTree(n, global_reload_doc) AddChild DocumentRoot(global_reload_doc), result RETURN result END FUNCTION 'Return an empty reload node attached to the global_reload_doc FUNCTION get_reload_empty (nodename as string = "") as NodePtr DIM result as NodePtr result = CreateNode(global_reload_doc, nodename) AddChild DocumentRoot(global_reload_doc), result RETURN result END FUNCTION ' This adds any new properties to an existing Node for a hero battle menu ' (and is also used to help create in the first place) SUB upgrade_hero_battle_menu_item(bmenu as NodePtr) IF bmenu."caption".exists = NO THEN SetChildNode(bmenu, "caption", "") IF bmenu."color".exists = NO THEN SetChildNode(bmenu, "color", 0) IF bmenu."enable_tag1".exists = NO THEN SetChildNode(bmenu, "enable_tag1", 0) IF bmenu."enable_tag2".exists = NO THEN SetChildNode(bmenu, "enable_tag2", 0) IF bmenu."hide_disabled".exists = NO THEN SetChildNode(bmenu, "hide_disabled", NO) END SUB FUNCTION add_hero_battle_menu_item(byval parent as NodePtr, kind as string, byval value as integer = 0) as NodePtr DIM bmenu as NodePtr bmenu = AppendChildNode(parent, "menu") DIM kindnode as NodePtr kindnode = SetChildNode(bmenu, "kind") SELECT CASE kind CASE "weapon", "items": SetChildNode(kindnode, kind) CASE "attack", "spells": SetChildNode(kindnode, kind, value) END SELECT ' Initialise other data to defaults upgrade_hero_battle_menu_item bmenu RETURN bmenu END FUNCTION FUNCTION should_hide_hero_stat(hero as HeroDef, byval statnum as integer) as bool DIM hero_node as NodePtr hero_node = hero.reld READNODE hero_node."stat_options" as opt WITHNODE opt."stat" as stat DIM n as integer n = GetInteger(stat) IF n = statnum THEN IF stat."hide".exists THEN RETURN YES END IF END WITHNODE END READNODE RETURN NO END FUNCTION FUNCTION should_hide_hero_stat(byval hero_id as integer, byval statnum as integer) as bool DIM her as HeroDef loadherodata her, hero_id RETURN should_hide_hero_stat(her, statnum) END FUNCTION 'Like INSTR (opposite arg order!), but only returns the offset of the first match starting at a word 'boundary where 'excludeword' doesn't also match, unless query actually starts with excludeword. 'This is intended to be used with select_by_typing on menus where you don't necessarily 'want to start typing from the beginning. 'For example needle="e" excludeword="edit" matches "edit enemy data" but not "edit items" 'Returns 0 on no match. FUNCTION find_on_word_boundary_excluding(haystack as string, needle as string, excludeword as string) as integer DIM excluding as bool = YES IF LEN(excludeword) = 0 OR LEFT(needle, LEN(excludeword)) = excludeword THEN excluding = NO END IF DIM wordboundary as bool = YES FOR index as integer = 1 TO LEN(haystack) DIM alnum as integer = isalnum(haystack[index - 1]) IF alnum AND wordboundary THEN IF MID(haystack, index, LEN(needle)) = needle ANDALSO _ (excluding = NO ORELSE MID(haystack, index, LEN(excludeword)) <> excludeword) THEN RETURN index END IF END IF wordboundary = (alnum = 0) NEXT RETURN 0 END FUNCTION FUNCTION find_on_word_boundary(haystack as string, needle as string) as integer RETURN find_on_word_boundary_excluding(haystack, needle, "") END FUNCTION FUNCTION str_rect(s as string, byval x as integer, byval y as integer) as RectType DIM r as RectType r.x = x r.y = y r.size = textsize(s, rWidth) 'Wrapped to screen size RETURN r END FUNCTION FUNCTION keyval_arrowset_up(arr as ArrowSet) as bool IF arr.U THEN IF keyval(arr.U) > 1 THEN RETURN YES RETURN NO END FUNCTION FUNCTION keyval_arrowset_right(arr as ArrowSet) as bool IF arr.R THEN IF keyval(arr.R) > 1 THEN RETURN YES RETURN NO END FUNCTION FUNCTION keyval_arrowset_down(arr as ArrowSet) as bool IF arr.D THEN IF keyval(arr.D) > 1 THEN RETURN YES RETURN NO END FUNCTION FUNCTION keyval_arrowset_left(arr as ArrowSet) as bool IF arr.L THEN IF keyval(arr.L) > 1 THEN RETURN YES RETURN NO END FUNCTION FUNCTION keyval_arrowset_confirm(arr as ArrowSet) as bool WITH arr IF .confirm THEN IF keyval(.confirm) > 1 THEN RETURN YES IF .confirm2 THEN IF keyval(.confirm2) > 1 THEN RETURN YES IF .confirm3 THEN IF keyval(.confirm3) > 1 THEN RETURN YES END WITH RETURN NO END FUNCTION FUNCTION keyval_arrowset_cancel(arr as ArrowSet) as bool WITH arr IF .cancel THEN IF keyval(.cancel) > 1 THEN RETURN YES IF .cancel2 THEN IF keyval(.cancel2) > 1 THEN RETURN YES END WITH RETURN NO END FUNCTION FUNCTION default_arrowset() as ArrowSet DIM arr as ArrowSet WITH arr .U = scUp .R = scRight .D = scDown .L = scLeft .confirm = scEnter .confirm2 = scSpace .confirm3 = scCTRL .cancel = scESC .cancel2 = scALT END WITH RETURN arr END FUNCTION FUNCTION arrowset_for_multiplayer_gamepad(byval player as integer) as ArrowSet DIM gamepad_node as NodePtr = 0 DIM gen_root as NodePtr gen_root = get_general_reld() IF player = 0 THEN IF gen_root."gamepad".exists THEN gamepad_node = gen_root."gamepad".ptr ELSEIF player >= 1 ANDALSO player <= 3 THEN DIM multi as NodePtr multi = GetChildByName(gen_root, "multiplayer_gamepads") IF multi THEN DIM ch as NodePtr = FirstChild(multi, "player") DO WHILE ch DIM pnum as integer = GetInteger(ch) IF pnum = player THEN gamepad_node = ch END IF ch = NextSibling(multi, "player") LOOP END IF END IF IF gamepad_node <> 0 THEN RETURN arrowset_from_reload(gamepad_node) ELSE debug "arrowset_for_multiplayer_gamepad: failed to find node for player " & player & ". using default arrowset instead" RETURN default_arrowset() END IF END FUNCTION FUNCTION arrowset_from_reload(gamepad as NodePtr) as ArrowSet DIM defarr as ArrowSet = default_arrowset() DIM arr as ArrowSet arr.U = defarr.U arr.R = defarr.R arr.D = defarr.D arr.L = defarr.L IF gamepad."UP".exists THEN arr.U = gamepad."UP".integer IF gamepad."RIGHT".exists THEN arr.R = gamepad."RIGHT".integer IF gamepad."DOWN".exists THEN arr.D = gamepad."DOWN".integer IF gamepad."LEFT".exists THEN arr.L = gamepad."LEFT".integer arr.confirm = gamepad."A".integer IF arr.confirm = 0 THEN debug "WARNING: gamepad node has no confirm, using default" arr.confirm = defarr.confirm END IF arr.cancel = gamepad."B".integer arr.cancel2 = gamepad."X".integer IF arr.cancel = 0 ANDALSO arr.cancel2 = 0 THEN debug "WARNING: gamepad node has no cancel, using default" arr.cancel = defarr.cancel END IF RETURN arr END FUNCTION FUNCTION gamepad_virtual_keyboard (default_str as string, max_length as integer=-1, byval multi_player as integer=-1) as string 'multi_player argument determines which set of keys is used. '-1 means default keyboard keys '0-3 means the keys defined for multiplayer gamepad support. DIM arr as ArrowSet SELECT CASE multi_player CASE -1 arr = default_arrowset() CASE 0 TO 3 arr = arrowset_for_multiplayer_gamepad(multi_player) CASE ELSE debug "WARNING: gamepad_virtual_keyboard: can't get arrowset for multi_player " & multi_player arr = default_arrowset() END SELECT debuginfo "gamepad_virtual_keyboard player=" & multi_player RETURN gamepad_virtual_keyboard(arr, default_str, max_length) END FUNCTION FUNCTION gamepad_virtual_keyboard (arr as ArrowSet, default_str as string, max_length as integer=-1) as string 'This is intended for OUYA where the Android virtual keyboard seems to 'be partly broken. It might prove to be useful for other purposes too. debuginfo "ArrowSet " & arr.U & "," & arr.R & "," & arr.D & "," & arr.L & " confirm=" & arr.confirm & " cancel=" & arr.cancel DIM alphabet as string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 1234567890',.;:?!@#$%^&*~_+-=()[]{}<>/" DIM result as string = default_str '--Preserve whatever screen was already showing as a background DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen copypage vpage, dpage DIM root as Slice Ptr root = NewSliceOfType(slSpecial) root->Fill = YES DIM box as Slice Ptr box = NewSliceOfType(slRectangle, root) WITH *box .x = 0 .y = 10 .width = 210 .height = 80 .AnchorHoriz = 1 .AlignHoriz = 1 .AnchorVert = 0 .AlignVert = 1 END WITH ChangeRectangleSlice box, 0 DIM keybox as Slice Ptr keybox = NewSliceOfType(slRectangle, box) WITH *keybox .x = 20 .y = 5 .width = 130 .height = 70 END WITH ChangeRectangleSlice keybox, , uilook(uiBackground), , borderNone DIM curs as Slice Ptr curs = NewSliceofType(slRectangle, keybox) WITH *curs .width = 10 .height = 10 END WITH ChangeRectangleSlice curs, , uilook(uiHighlight), , borderNone DIM ch as Slice Ptr FOR i as integer = 0 TO LEN(alphabet) - 1 ch = NewSliceOfType(sltext, keybox) ChangeTextSlice ch, MID(alphabet, i+1, 1), uilook(uiMenuItem), YES ch->x = (i MOD 13) * 10 ch->y = INT(i / 13) * 10 NEXT i DIM buttonbox as Slice Ptr buttonbox = NewSliceOfType(slContainer, box) WITH *buttonbox .x = -10 .y = 15 .width = 48 .AnchorHoriz = 2 .AlignHoriz = 2 END WITH DIM del as Slice Ptr del = NewSliceOfType(slRectangle, buttonbox) WITH *del .width = 40 .height = 12 .AnchorHoriz = 1 .AlignHoriz = 1 END WITH ChangeRectangleSlice del, , uilook(uiBackground), , borderNone DIM deltxt as Slice Ptr deltxt = NewSliceOfType(slText, del) WITH *deltxt .AnchorHoriz = 1 .AlignHoriz = 1 .AnchorVert = 1 .AlignVert = 1 END WITH ChangeTextSlice deltxt, "DEL", uilook(uiMenuItem), YES DIM confirm as Slice Ptr confirm = NewSliceOfType(slRectangle, buttonbox) WITH *confirm .y = 20 .width = 40 .height = 12 .AnchorHoriz = 1 .AlignHoriz = 1 END WITH ChangeRectangleSlice confirm, , uilook(uiBackground), , borderNone DIM confirmtxt as Slice Ptr confirmtxt = NewSliceOfType(slText, confirm) WITH *confirmtxt .AnchorHoriz = 1 .AlignHoriz = 1 .AnchorVert = 1 .AlignVert = 1 END WITH ChangeTextSlice confirmtxt, "DONE", uilook(uiMenuItem), YES DIM typingbox as Slice Ptr typingbox = NewSliceOfType(slRectangle, root) WITH *typingbox .y = -4 IF max_length = -1 THEN .width = 320 ELSE .width = small(320, max_length * 8 + 20) END IF .height = 20 .AnchorHoriz = 1 .AlignHoriz = 1 .AnchorVert = 1 .AlignVert = 1 END WITH ChangeRectangleSlice typingbox, 0 DIM typing as Slice Ptr typing = NewSliceOfType(sltext, typingbox) WITH *typing .AnchorHoriz = 1 .AlignHoriz = 1 .AnchorVert = 1 .AlignVert = 1 END WITH DIM p as XYPair DIM pt as integer DIM need_update as bool = YES DIM tog as integer DIM delflash as integer = 0 setkeys DO setwait 17 setkeys tog XOR= 1 IF getquitflag() THEN EXIT DO ' want to close the program! IF keyval_arrowset_cancel(arr) THEN delflash = 5 IF LEN(result) > 0 THEN result = LEFT(result, LEN(result) - 1) need_update = YES END IF END IF IF keyval_arrowset_confirm(arr) THEN IF p.x = -1 THEN SELECT CASE pt CASE 0: IF LEN(result) > 0 THEN result = LEFT(result, LEN(result) - 1) need_update = YES END IF CASE 1: EXIT DO END SELECT ELSE IF LEN(result) < max_length ORELSE max_length = -1 THEN result &= MID(alphabet, 1 + p.y * 13 + p.x, 1) need_update = YES END IF END IF END IF IF p.x = -1 THEN IF keyval_arrowset_up(arr) ORELSE keyval_arrowset_down(arr) THEN pt XOR= 1 need_update = YES END IF IF keyval_arrowset_right(arr) THEN p.x = 0 need_update = YES END IF IF keyval_arrowset_left(arr) THEN p.x = 12 need_update = YES END IF ELSE IF keyval_arrowset_up(arr) THEN p.y = loopvar(p.y, 0, 6, -1) need_update = YES END IF IF keyval_arrowset_down(arr) THEN p.y = loopvar(p.y, 0, 6, 1) need_update = YES END IF IF keyval_arrowset_left(arr) THEN p.x = loopvar(p.x, -1, 12, -1) need_update = YES END IF IF keyval_arrowset_right(arr) THEN p.x = loopvar(p.x, -1, 12, 1) need_update = YES END IF END IF IF need_update THEN need_update = NO IF p.x = -1 THEN SliceSetVisibility curs, NO ELSE SliceSetVisibility curs, YES curs->x = p.x * 10 curs->y = p.y * 10 END IF ChangeRectangleSlice del, , uilook(uiBackground) ChangeRectangleSlice confirm, , uilook(uiBackground) ChangeTextSlice typing, result, , YES END IF ChangeRectangleSlice curs, , uilook(uiHighlight + tog) IF p.x = -1 THEN SELECT CASE pt CASE 0: ChangeRectangleSlice del, , uilook(uiHighlight + tog) CASE 1: ChangeRectangleSlice confirm, , uilook(uiHighlight + tog) END SELECT END IF IF delflash > 0 THEN delflash -= 1 ChangeRectangleSlice del, , uilook(uiHighlight + tog) need_update = YES END IF DrawSlice root, dpage SWAP vpage, dpage setvispage vpage copypage holdscreen, dpage dowait LOOP freepage holdscreen DeleteSlice @root RETURN result END FUNCTION FUNCTION progress_spinner (byval exit_condition_func as FnNoArgsBool, caption as string, byval timeout_seconds as double) as bool 'In contrast to a progress meter, a progress spinner has no idea how long this will take 'so it just needs to animate until it succeeds or times out. 'Returns YES if didn't time out 'exit_condition_func should take no arguments and return YES when the progress is complete. DIM t as double = TIMER DIM holdscreen as integer holdscreen = allocatepage copypage vpage, holdscreen DIM root as Slice Ptr root = NewSliceOfType(slSpecial) WITH *root .Fill = YES END WITH DrawSlice root, vpage DIM rect_sl as Slice Ptr rect_sl = NewSliceOfType(slRectangle, root) WITH *rect_sl .Width = 280 .Height = 50 ' updated again after the text is wrapped .AnchorHoriz = 1 .AlignHoriz = 1 .AnchorVert = 1 .AlignVert = 1 END WITH ChangeRectangleSlice rect_sl, 0 DIM text_sl as Slice Ptr text_sl = NewSliceOfType(slText, rect_sl) WITH *text_sl .Y = 8 .Width = rect_sl->Width - 16 .AnchorHoriz = 1 .AlignHoriz = 1 END WITH ChangeTextSlice text_sl, caption, uiLook(uiText), YES, YES DIM bubbles(7) as Slice Ptr FOR i as integer = 0 TO 7 bubbles(i) = NewSliceOfType(slEllipse, rect_sl) WITH *bubbles(i) .X = rect_sl->Width / 9 * (i+1) .Y = -23 .Width = 10 .Height = 10 .AnchorHoriz = 1 .AlignVert = 1 .AnchorVert = 1 .AlignVert = 2 END WITH ChangeEllipseSlice bubbles(i), uilook(uiHealthBar), uilook(uiHealthBarFlash) NEXT i rect_sl->Height = 8 + text_sl->Height + 8 + 30 + 8 DIM oscil as integer = 0 DIM bub_size as integer = 0 DIM ret as bool = YES '--Display the spinner animation while waiting for the action to succeed (or time-out) ' Spinner is displayed as a row of oscilating bubbles setkeys YES DO WHILE NOT exit_condition_func() IF TIMER - t > timeout_seconds THEN ret = NO : EXIT DO setwait 55 setkeys YES oscil = loopvar(oscil, 0, 29, 1) FOR i as integer = 0 TO 7 bub_size = ABS((oscil + i) MOD 30 - 15) + 5 bubbles(i)->Width = bub_size bubbles(i)->Height = bub_size NEXT i copypage holdscreen, vpage DrawSlice root, vpage setvispage vpage dowait LOOP freepage holdscreen DeleteSlice @root RETURN ret END FUNCTION FUNCTION dissolve_type_caption(n as integer) as string SELECT CASE n CASE 0: RETURN "Random scatter" CASE 1: RETURN "Crossfade" CASE 2: RETURN "Diagonal Vanish" CASE 3: RETURN "Sink into Ground" CASE 4: RETURN "Squash" CASE 5: RETURN "Melt" CASE 6: RETURN "Vapourise" CASE 7: RETURN "Phase out" CASE 8: RETURN "Squeeze" CASE 9: RETURN "Shrink" CASE 10: RETURN "Flicker" CASE ELSE: RETURN n & " Invalid!" END SELECT END FUNCTION FUNCTION appear_type_caption(n as integer) as string 'These are names for the inverted dissolve animations. 'They should only differ where the dissolve name doesn't make sense backwards SELECT CASE n CASE 0: RETURN "Random scatter" CASE 1: RETURN "Crossfade" CASE 2: RETURN "Diagonal Appear" CASE 3: RETURN "Rise from Ground" CASE 4: RETURN "Un-Squash" CASE 5: RETURN "Un-Melt" CASE 6: RETURN "Un-Vapourise" CASE 7: RETURN "Phase In" CASE 8: RETURN "Un-Squeeze" CASE 9: RETURN "Expand" CASE 10: RETURN "Flicker" CASE ELSE: RETURN n & " Invalid!" END SELECT END FUNCTION '--Wrapper functions for getting and setting values from general.reld FUNCTION default_gen_bool(nodepath as string) as bool 'This function supplies a centralized place to manage defaults for 'boolean keys in the general.reld lump. This way it is easy to change 'defaults without having to update every get_gen_bool() call. SELECT CASE nodepath CASE "/mouse/move_hero": RETURN NO CASE "/mouse/move_hero/display_dest": RETURN NO CASE "/mouse/move_hero/cancel_on_battle": RETURN YES CASE "/mouse/move_hero/cancel_on_textbox": RETURN YES CASE "/mouse/move_hero/cancel_on_menu": RETURN YES CASE "/mouse/menu_right_click": RETURN NO CASE "/mouse/mouse_menus": RETURN NO CASE "/mouse/click_textboxes": RETURN NO CASE "/mouse/show_cursor": RETURN NO CASE "/mobile_options/touch_textboxes/enabled": RETURN YES CASE ELSE debuginfo "default_gen_bool: no default for nodepath """ & nodepath & """ falling back to NO" RETURN NO END SELECT END FUNCTION FUNCTION default_gen_int(nodepath as string) as integer 'This function supplies a centralized place to manage defaults for 'integers in the general.reld lump. This way it is easy to change 'defaults without having to update every get_gen_int() call. SELECT CASE nodepath CASE "/mouse/move_hero/max_path_length": RETURN 0 CASE ELSE debuginfo "default_gen_int: no default for nodepath """ & nodepath & """ falling back to 0" RETURN 0 END SELECT END FUNCTION FUNCTION gen_int_limits(nodepath as string) as XYPair 'This function supplies a centralized place to manage 'minimum and maximum values for integers in general.reld SELECT CASE nodepath CASE "/mouse/move_hero/max_path_length": RETURN XY(0, 100000) CASE ELSE debuginfo "gen_int_limits: no limits for nodepath """ & nodepath & """ falling back to something large" RETURN XY(-100000000, 100000000) '100 million is completel arbitrary END SELECT END FUNCTION FUNCTION enforce_gen_int_limits(nodepath as string, byval v as integer) as integer DIM limits as XYPair = gen_int_limits(nodepath) IF v < limits.x THEN debug nodepath & " " & v & " is less than " & limits.x & " (correcting)" RETURN limits.x END IF IF v > limits.y THEN debug nodepath & " " & v & " is greater than " & limits.y & " (correcting)" RETURN limits.y END IF RETURN v END FUNCTION FUNCTION get_gen_bool(nodepath as string) as bool DIM gen_root as NodePtr = get_general_reld() DIM node as NodePtr = NodeByPath(gen_root, nodepath) IF node THEN RETURN IIF(GetInteger(node), YES, NO) RETURN default_gen_bool(nodepath) END FUNCTION FUNCTION get_gen_int(nodepath as string) as integer DIM gen_root as NodePtr = get_general_reld() DIM node as NodePtr = NodeByPath(gen_root, nodepath) IF node THEN RETURN enforce_gen_int_limits(nodepath, GetInteger(node)) RETURN default_gen_int(nodepath) END FUNCTION SUB set_gen_bool(nodepath as string, byval v as bool) 'This changes the node, but does not call write_general_reld() DIM gen_root as NodePtr = get_general_reld() DIM node as NodePtr = NodeByPath(gen_root, nodepath, YES) SetContent(node, IIF(v, 1, 0)) END SUB SUB set_gen_int(nodepath as string, byval v as integer) 'This changes the node, but does not call write_general_reld() DIM gen_root as NodePtr = get_general_reld() DIM node as NodePtr = NodeByPath(gen_root, nodepath, YES) SetContent(node, enforce_gen_int_limits(nodepath, v)) END SUB SUB toggle_gen_bool(nodepath as string) 'This changes the node, but does not call write_general_reld() set_gen_bool(nodepath, NOT get_gen_bool(nodepath)) END SUB FUNCTION gen_intgrabber(nodepath as string) as bool 'This changes the node, but does not call write_general_reld() DIM v as integer = get_gen_int(nodepath) DIM limits as XYPair = gen_int_limits(nodepath) IF intgrabber(v, limits.x, limits.y) THEN set_gen_int(nodepath, v) RETURN YES END IF RETURN NO END FUNCTION ' Functions for reading/writing ohrrpgce_config.ini SUB set_global_config_file() global_config_file = EXEPATH & SLASH & "ohrrpgce_config.ini" IF NOT isfile(global_config_file) THEN global_config_file = settings_dir & SLASH & "ohrrpgce_config.ini" END IF END SUB 'Try to read the value of a key from global_config_file while respecting 'config_prefix and finding the most specific setting that applies. '(Do NOT manually prepend config_prefix.) 'For example if config_prefix is "edit.game_genesis." and key is "hspeak", then try '"edit.game_genesis.hspeak", "edit.hspeak", "hspeak" in order. 'Also, in-game, check the game-specific .ini file first. FUNCTION read_config_str (key as string, default as string="") as string DIM as string ret, prefix = config_prefix #IFDEF IS_GAME IF config_file <> "" THEN 'Settings in the game's .ini file override others (I think it may be nicer 'if this were a more general fallback to other files) ret = read_ini_str(config_file, key, CHR(0)) IF ret <> CHR(0) THEN RETURN ret END IF #ENDIF DO ret = read_ini_str(global_config_file, prefix & key, CHR(0)) IF ret <> CHR(0) THEN RETURN ret IF LEN(prefix) = 0 THEN RETURN default 'Trim end off the prefix prefix = MID(prefix, 1, INSTRREV(prefix, ".", LEN(prefix) - 1)) LOOP END FUNCTION FUNCTION read_config_int (key as string, default as integer=0) as integer RETURN str2int(read_config_str(key), default) END FUNCTION FUNCTION read_config_bool (key as string, default as bool=NO) as bool DIM v as string = LCASE(read_config_str(key)) IF LEN(v) = 0 THEN RETURN default IF v = "no" ORELSE v = "false" ORELSE v = "off" THEN RETURN NO 'Any other value, other than 0, counts as true RETURN str2int(v, -1) <> 0 END FUNCTION 'Write to global_config_file (in general) or the game-specific config_file '(only in Game if key starts with config_prefix). 'If you want a setting to be game-specific, prepend config_prefix (which is '"game.", "edit.", "game.game_.", 'or "edit.game_."). 'Otherwise, Here are some guidelines for naming a key, not nailed down: '-Something specific to an editor, shared between all games should look like ' "mapedit.show_overhead" (eg "sliceedit." can be shared by Game + Custom) '-Shared gfx backend options go under "gfx." '-Backend-specific options go under eg "gfx.sdl." '-A setting for Custom and not falling into another category can start with "edit.". '-Stuff for Game but not game-specific goes under "game." SUB write_config (key as string, value as string) 'FIXME: if "edit.game_foo.thing" exists and you load and then save "thing", 'it gets saved to "thing", resulting in a local setting accidentally becoming global, 'and not being able to change it locally! #IFDEF IS_GAME IF starts_with(key, config_prefix) ANDALSO starts_with(config_prefix, "game.game_") THEN IF LEN(config_file) THEN DIM trimmedkey as string = MID(key, LEN(config_prefix) + 1) write_ini_value config_file, trimmedkey, value EXIT SUB ELSE debug "write_config: config_file isn't set!" END IF END IF #ENDIF write_ini_value global_config_file, key, value END SUB SUB write_config (key as string, value as integer) write_config key, STR(value) END SUB SUB write_config (key as string, value as double) write_config key, FORMAT(value, "0.000") END SUB