'OHRRPGCE GAME - Even more various unsorted routines '(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. ' #include "config.bi" #include "allmodex.bi" #include "cmdline.bi" #include "common.bi" #include "gglobals.bi" #include "const.bi" #include "scrconst.bi" #include "uiconst.bi" #include "loading.bi" #include "reload.bi" #include "reloadext.bi" #include "scripting.bi" #include "game.bi" #include "scriptcommands.bi" #include "yetmore2.bi" #include "walkabouts.bi" #include "moresubs.bi" #include "menustuf.bi" #include "savegame.bi" #include "bmodsubs.bi" #include "steam.bi" #include "achievements.bi" Using Reload Using Reload.Ext 'Local subs and functions DECLARE SUB drawants_for_tile(tile as XYPair, byval direction as DirNum) ' This is called after every setkeys unless we're already inside global_setkeys_hook ' It should be fine to call any allmodex function in here, but beware we might ' not have loaded a game yet! SUB global_setkeys_hook #IFNDEF NO_TEST_GAME ' Process messages from Custom IF gam.ingame ANDALSO running_under_Custom THEN try_reload_lumps_anywhere #ENDIF END SUB SUB initgamedefaults 'Exists to initialise game state which needs to start at a value 'other than the default initialisation (zero/empty string/etc) '(Generally you can and should use constructors (or default UDT member values) 'to initialise data to something other than zero) lastsaveslot = 0 '--items CleanInventory inventory() '--money gold = gen(genStartMoney) '--hero's speed FOR i as integer = 0 TO 3 herow(i).speed = 4 NEXT i '--hero's position (herox(0)) = gen(genStartX) * 20 (heroy(0)) = gen(genStartY) * 20 (herodir(0)) = dirDown resetcaterpillar () 'plotstring colours FOR i as integer = 0 TO UBOUND(plotstr) plotstr(i).col = -1 'default to uilook(uiText) NEXT i END SUB 'revive_dead_heroes should -2 for default, or a bool SUB innRestore (revive_dead_heroes as integer = -2) IF revive_dead_heroes = -2 THEN revive_dead_heroes = (prefbit(4) = NO) '"!Inns Revive Dead Heroes" END IF FOR i as integer = 0 TO 3 IF gam.hero(i).id >= 0 THEN '--hero exists IF gam.hero(i).stat.cur.hp <= 0 ANDALSO revive_dead_heroes = NO THEN '--hero is dead and inn-revive is disabled ELSE '--normal revive gam.hero(i).stat.cur.hp = gam.hero(i).stat.max.hp gam.hero(i).stat.cur.mp = gam.hero(i).stat.max.mp reset_levelmp gam.hero(i) END IF END IF NEXT i party_change_updates END SUB SUB center_camera_on_slice(byval sl as Slice ptr) BUG_IF(sl = NULL, "NULL slice") RefreshSliceScreenPos sl mapx = sl->ScreenX + sl->Width/2 - SliceTable.MapRoot->ScreenX - vpages(dpage)->w \ 2 mapy = sl->ScreenY + sl->Height/2 - SliceTable.MapRoot->ScreenY - vpages(dpage)->h \ 2 END SUB SUB center_camera_on_walkabout(byval walkabout_cont as Slice ptr) IF walkabout_cont = NULL THEN debug "NULL walkabout slice in center_camera_on_walkabout" : EXIT SUB 'Note: the sprite component .Y value is equal to the foot-offset minus the hero/NPC Z. 'Note: genCameraOnWalkaboutFocus 0 and 2 are identical when hero sprites are 20px high. 'Option 0 was the old default (early days of walktall). 'Option 2 became the default in Alectormancy (2012) 'Option 0 made the default in Fufluns (2019), because it means `put camera(hero pixel x, hero pixel y)` 'will not move the camera if it was following the leader. IF gen(genCameraOnWalkaboutFocus) = 0 THEN 'Center on the container slice (hitbox)/tile center_camera_on_slice walkabout_cont ELSE 'Center on the walkabout sprite DIM sprsl as Slice ptr sprsl = LookupSlice(SL_WALKABOUT_SPRITE_COMPONENT, walkabout_cont) center_camera_on_slice sprsl IF gen(genCameraOnWalkaboutFocus) = 2 THEN 'Center on sprite minus Z/offset ' For compatibility (maybe some old games use setheroz for falling from the sky?) ' ignore the Z component of the sprite slice, and also the foot offset. mapy -= sprsl->Y END IF END IF END SUB 'Set the camera position. SUB setmapxy IF gam.debug_camera_pan THEN 'F7 debug key DIM speed as integer = IIF(keyval(scShift) > 0, 50, 15) IF carray(ccUp) > 0 THEN mapy -= speed IF carray(ccDown) > 0 THEN mapy += speed IF carray(ccLeft) > 0 THEN mapx -= speed IF carray(ccRight) > 0 THEN mapx += speed IF game_check_cancel_key() THEN gam.showtext = "Normal camera restored" gam.showtext_ticks = 45 gam.debug_camera_pan = NO END IF ELSE 'Normal camera SELECT CASE gen(genCameraMode) CASE herocam DIM sl as Slice ptr = herow(gen(genCameraArg1)).sl IF sl ANDALSO sl->Parent <> SliceTable.Reserve THEN center_camera_on_walkabout sl CASE npccam IF gen(genCameraArg1) > UBOUND(npc) ORELSE npc(gen(genCameraArg1)).id <= 0 THEN gen(genCameraMode) = stopcam ELSE center_camera_on_walkabout npc(gen(genCameraArg1)).sl END IF CASE slicecam 'We also check the slice didn't just get deleted after changing map DIM sl as Slice ptr sl = get_handle_slice(gen(genCameraArg1), serrIgnore) IF sl THEN center_camera_on_slice sl ELSE 'stopping seems more appropriate than resetting to hero gen(genCameraMode) = stopcam END IF CASE pancam ' 1=dir, 2=ticks, 3=step IF gen(genCameraArg2) > 0 THEN aheadxy mapx, mapy, gen(genCameraArg1), gen(genCameraArg3) gen(genCameraArg2) -= 1 END IF IF gen(genCameraArg2) <= 0 THEN gen(genCameraMode) = stopcam CASE focuscam ' 1=x, 2=y, 3=x step, 4=y step DIM camdiff as integer camdiff = gen(genCameraArg1) - mapx IF ABS(camdiff) <= gen(genCameraArg3) THEN gen(genCameraArg3) = 0 mapx = gen(genCameraArg1) ELSE mapx += SGN(camdiff) * gen(genCameraArg3) END IF camdiff = gen(genCameraArg2) - mapy IF ABS(camdiff) <= gen(genCameraArg4) THEN gen(genCameraArg4) = 0 mapy = gen(genCameraArg2) ELSE mapy += SGN(camdiff) * gen(genCameraArg4) END IF limitcamera mapx, mapy IF gen(genCameraArg3) = 0 AND gen(genCameraArg4) = 0 THEN gen(genCameraMode) = stopcam END SELECT END IF limitcamera mapx, mapy END SUB SUB showplotstrings FOR i as integer = 0 TO UBOUND(plotstr) WITH plotstr(i) IF .bits AND 1 THEN '-- only display visible strings DIM col as integer = .col IF col = -1 THEN col = uilook(uiText) textcolor col, .bgcol DIM fontnum as integer = IIF(.bits AND 2, fontPlain, fontEdged) 'Don't autowrap (infinite width), but do support newlines wrapprint .s, .x, .y, , dpage, INT_MAX, NO, fontnum 'withtags=NO END IF END WITH NEXT i END SUB SUB makebackups 'what is this for? Since some lumps can be modified at run time, we need to keep a 'backup copy, and then only edit the copy. The original is never used directly. 'enemy data writeablecopyfile game + ".dt1", tmpdir & "dt1.tmp" 'formation data writeablecopyfile game + ".for", tmpdir & "for.tmp" 'If you add lump-modding commands, you better well add them here >:( 'Also, add CASEs to try_to_reload_lumps_* to handle updates to those lumps END SUB SUB make_map_backups 'This back-ups certain map lumps so that when live previewing a game, changes can be three-way merged writeablecopyfile maplumpname(gam.map.id, "t"), tmpdir & "mapbackup.t" writeablecopyfile maplumpname(gam.map.id, "p"), tmpdir & "mapbackup.p" writeablecopyfile maplumpname(gam.map.id, "l"), tmpdir & "mapbackup.l" 'Global lump which doesn't need to be backed up on every map change... actually maybe doing that interferes 'with merging? Well, putting this here keeps things simple writeablecopyfile game + ".map", tmpdir & "mapbackup.map" END SUB SUB update_backdrop_slice DIM backdrop as integer DIM transparent as bool = NO IF gen(genTextboxBackdrop) THEN backdrop = gen(genTextboxBackdrop) - 1 transparent = txt.box.backdrop_trans ELSEIF gen(genScrBackdrop) THEN backdrop = gen(genScrBackdrop) - 1 ELSE SliceTable.Backdrop->Visible = NO EXIT SUB END IF SliceTable.Backdrop->Visible = YES ChangeSpriteSlice SliceTable.Backdrop, sprTypeBackdrop, backdrop, , , , , transparent END SUB FUNCTION checkfordeath () as bool RETURN liveherocount = 0 END FUNCTION 'Note that this is called both from reset_game_final_cleanup(), in which case lots of stuff 'has already been deallocated, or from exit_gracefully(), in which case no cleanup has been done! SUB exitprogram(byval need_fade_out as bool = NO, byval errorout as integer = 0) debuginfo "Cleaning up and terminating " & errorout gam.ingame = NO 'uncomment for slice debugging 'DestroyGameSlices YES IF need_fade_out THEN fadeout uilook(uiFadeoutQuit) releasestack '--scripts 'Also prints script profiling info resetinterpreter destroystack(scrst) '--reset audio closemusic debuginfo "Deleting tmpdir " & tmpdir killdir tmpdir, YES 'recursively deletes playing.tmp if it exists v_free modified_lumps restoremode Steam.uninitialize() debuginfo "End." terminate_program errorout END SUB SUB verify_quit DIM quitprompt as string = readglobalstring(55, "Quit Playing?", 20) DIM quityes as string = readglobalstring(57, "Yes", 10) DIM quitno as string = readglobalstring(58, "No", 10) DIM direction as DirNum = dirSouth DIM box as XYPair = XY(200, 42) DIM walkthreshold as integer DIM usethreshold as integer DIM ptr2 as integer = 0 DIM tog as integer DIM wtog as integer DIM col as integer DIM holdscreen as integer = allocatepage copypage vpage, holdscreen show_virtual_gamepad() DIM sprsl as Slice ptr sprsl = LookupSlice(SL_WALKABOUT_SPRITE_COMPONENT, herow(0).sl) IF sprsl = NULL THEN showbug "verify_quit: null slice" ELSE box.h = large(sprsl->Height, 20) + 22 'Remove foot offset and hero Z; these will immediately be added back next tick sprsl->Y = 0 END IF box.w = small(200, vpages(vpage)->w - 6) walkthreshold = box.w \ 4 usethreshold = box.w \ 10 DIM walkspeed as integer = large(2, herow(0).speed) setkeys DO setwait speedcontrol setkeys tog = tog XOR 1 playtimer 'Keyboard controls IF game_check_cancel_key() THEN EXIT DO IF (game_check_use_key() AND ABS(ptr2) > usethreshold) OR ABS(ptr2) > walkthreshold THEN IF ptr2 < 0 THEN gam.quit = YES: fadeout uilook(uiFadeoutQuit) EXIT DO END IF IF carray(ccLeft) > 0 THEN ptr2 = ptr2 - walkspeed: direction = dirLeft IF carray(ccRight) > 0 THEN ptr2 = ptr2 + walkspeed: direction = dirRight DIM centerx as integer = vpages(vpage)->w \ 2 DIM centery as integer = vpages(vpage)->h \ 2 IF get_gen_bool("/mouse/mouse_menus") ANDALSO (carray(ccLeft) = 0 ANDALSO carray(ccRight) = 0) THEN 'Only do the mouse controls when you are not using the arrow keys 'The hero walks faster in mouseover mode, because we are counting on a click IF rect_collide_point(XYWH(centerx - box.w \ 2, centery - box.h \ 2, box.w \ 2 - usethreshold, box.h), readmouse.pos) THEN 'Yes (to the left) ptr2 = large(ptr2 - 5, -walkthreshold) direction = dirLeft IF (readmouse.release AND mouseLeft) ANDALSO ptr2 <= 0 THEN gam.quit = YES fadeout uilook(uiFadeoutQuit) EXIT DO END IF ELSEIF rect_collide_point(XYWH(centerx + usethreshold, centery - box.h \ 2, box.w \ 2 - usethreshold, box.h), readmouse.pos) THEN 'No (to the right) ptr2 = small(ptr2 + 5, walkthreshold) direction = dirRight END IF IF readmouse.release AND (mouseLeft OR mouseRight) THEN 'A click anywhere other than the "Yes" area EXIT DO END IF END IF copypage holdscreen, vpage centerbox centerx, centery - 5, box.w, box.h, 15, vpage loopvar wtog, 0, max_wtog(herow(0).sl, direction) set_walkabout_frame herow(0).sl, direction, wtog_to_frame(wtog) DrawSliceAt herow(0).sl, centerx - 10 + ptr2, centery + box.h \ 2 - 21 - 10, 20, 20, vpage, YES edgeprint quitprompt, pCentered, centery - box.h \ 2 + 1, uilook(uiText), vpage col = uilook(uiMenuItem) IF ptr2 < -usethreshold THEN col = uilook(uiSelectedItem + tog) edgeprint quityes, rCenter - box.w \ 2 + 10, centery - 4, col, vpage col = uilook(uiMenuItem) IF ptr2 > usethreshold THEN col = uilook(uiSelectedItem + tog) edgeprint quitno, rCenter + box.w \ 2 - 10 + ancRight, centery - 4, col, vpage setvispage vpage dowait LOOP freepage holdscreen setkeys END SUB FUNCTION titlescreen () as bool DIM ret as bool = YES DIM backdrop as Frame ptr backdrop = frame_load(sprTypeBackdrop, gen(genTitle)) DIM titletext as string = load_titletext() IF gen(genTitleMus) > 0 THEN wrappedsong gen(genTitleMus) - 1 setkeys DO setwait speedcontrol setkeys ' Draw the screen at least once and fade in before skipping the title screen ' (This is not required to avoid any bug, it simply ensures this function acts consistently.) IF gam.need_fade_in = NO THEN IF game_check_cancel_key() ANDALSO exit_from_game_is_allowed() THEN ret = NO EXIT DO END IF IF anykeypressed(YES, YES) THEN EXIT DO 'Joystick and mouse included END IF clearpage vpage frame_draw backdrop, , pCentered, pCentered, NO, vpage edgeprint titletext, 8 + showLeft, pBottom, uilook(uiText), vpage setvispage vpage check_for_queued_fade_in dowait LOOP frame_unload @backdrop RETURN ret END FUNCTION FUNCTION mapstatetemp(mapnum as integer, prefix as string) as string RETURN tmpdir & prefix & mapnum END FUNCTION SUB savemapstate_gmap(mapnum as integer, prefix as string) DIM fh as integer OPENFILE(mapstatetemp(mapnum, prefix) & "_map.tmp", FOR_BINARY + ACCESS_WRITE, fh) PUT #fh, , gmap() CLOSE #fh END SUB SUB savemapstate_npcl(mapnum as integer, prefix as string) DIM filename as string = mapstatetemp(mapnum, prefix) & "_l.reld.tmp" save_npc_instances filename, npc() END SUB SUB savemapstate_npcd(mapnum as integer, prefix as string) SaveNPCD mapstatetemp(mapnum, prefix) & "_n.tmp", npool(0).npcs() END SUB SUB savemapstate_tilemap(mapnum as integer, prefix as string) savetilemaps maptiles(), mapstatetemp(mapnum, prefix) & "_t.tmp" END SUB SUB savemapstate_passmap(mapnum as integer, prefix as string) savetilemap pass, mapstatetemp(mapnum, prefix) & "_p.tmp" END SUB SUB savemapstate_zonemap(mapnum as integer, prefix as string) SaveZoneMap zmap, mapstatetemp(mapnum, prefix) & "_z.tmp" END SUB 'Used only by the "save map state" command 'If prefix="state" then mapnum is a custom state ID SUB savemapstate_bitmask (mapnum as integer, savemask as integer = 255, prefix as string) IF savemask AND 1 THEN savemapstate_gmap mapnum, prefix END IF IF savemask AND 2 THEN savemapstate_npcl mapnum, prefix END IF IF savemask AND 4 THEN savemapstate_npcd mapnum, prefix END IF IF savemask AND 8 THEN savemapstate_tilemap mapnum, prefix END IF IF savemask AND 16 THEN savemapstate_passmap mapnum, prefix END IF IF savemask AND 32 THEN savemapstate_zonemap mapnum, prefix END IF END SUB SUB loadmapstate_gmap (mapnum as integer, prefix as string, dontfallback as bool = NO) DIM fh as integer DIM filebase as string = mapstatetemp(mapnum, prefix) IF NOT isfile(filebase & "_map.tmp") THEN IF dontfallback = NO THEN loadmap_gmap mapnum EXIT SUB END IF lump_reloading.gmap.dirty = NO 'Not correct, but too much trouble to do correctly lump_reloading.gmap.changed = NO OPENFILE(filebase & "_map.tmp", FOR_BINARY + ACCESS_READ, fh) GET #fh, , gmap() CLOSE #fh gmap_updates END SUB SUB loadmapstate_npcl (mapnum as integer, prefix as string, dontfallback as bool = NO) DIM filename as string filename = mapstatetemp(mapnum, prefix) & "_l.reld.tmp" IF NOT isfile(filename) THEN IF dontfallback = NO THEN loadmap_npcl mapnum EXIT SUB END IF load_npc_instances filename, npc() '--Evaluate whether NPCs should appear or disappear based on tags visnpc END SUB SUB loadmapstate_npcd (mapnum as integer, prefix as string, dontfallback as bool = NO) DIM filebase as string = mapstatetemp(mapnum, prefix) IF NOT isfile(filebase & "_n.tmp") THEN IF dontfallback = NO THEN loadmap_npcd mapnum EXIT SUB END IF LoadNPCD filebase & "_n.tmp", npool(0).npcs() 'Evaluate whether NPCs should appear or disappear based on tags visnpc 'load NPC graphics reset_npc_graphics END SUB SUB loadmapstate_tilemap (mapnum as integer, prefix as string, dontfallback as bool = NO) DIM filebase as string = mapstatetemp(mapnum, prefix) IF NOT isfile(filebase + "_t.tmp") THEN IF dontfallback = NO THEN loadmap_tilemap mapnum ELSE DIM as TilemapInfo statesize, propersize GetTilemapInfo maplumpname(gam.map.id, "t"), propersize GetTilemapInfo filebase + "_t.tmp", statesize IF statesize.size = propersize.size THEN 'Changing number of map layers is OK, however lump_reloading.maptiles.dirty = NO 'Not correct, but too much trouble to do correctly lump_reloading.maptiles.changed = NO loadtilemaps maptiles(), filebase + "_t.tmp" mapsizetiles = maptiles(0).size update_map_slices_for_new_tilemap '--as soon as we know the dimensions of the map, enforce hero position boundaries cropposition herox(0), heroy(0), 20 ELSE DIM errmsg as string = " Tried to load saved tilemap state which is size " & statesize.size.wh & ", while the map is size " & propersize.size.wh IF insideinterpreter THEN scripterr current_command_name() + errmsg, 4 ELSE debug "loadmapstate_tilemap(" + filebase + "_t.tmp): " + errmsg END IF IF dontfallback = NO THEN loadmap_tilemap mapnum END IF END IF END SUB SUB loadmapstate_passmap (mapnum as integer, prefix as string, dontfallback as bool = NO) DIM filebase as string = mapstatetemp(mapnum, prefix) IF NOT isfile(filebase + "_p.tmp") THEN IF dontfallback = NO THEN loadmap_passmap mapnum ELSE DIM as TilemapInfo statesize, propersize GetTilemapInfo maplumpname(gam.map.id, "p"), propersize GetTilemapInfo filebase + "_p.tmp", statesize IF statesize.size = propersize.size THEN lump_reloading.passmap.dirty = NO 'Not correct, but too much trouble to do correctly lump_reloading.passmap.changed = NO loadtilemap pass, filebase + "_p.tmp" ELSE DIM errmsg as string = "tried to load saved passmap state which is size " & statesize.size.wh & ", while the map is size " & propersize.size.wh IF insideinterpreter THEN scripterr current_command_name() + errmsg, 4 ELSE debug "loadmapstate_passmap(" + filebase + "_p.tmp): " + errmsg END IF IF dontfallback = NO THEN loadmap_passmap mapnum END IF END IF END SUB SUB loadmapstate_zonemap (mapnum as integer, prefix as string, dontfallback as bool = NO) DIM filebase as string = mapstatetemp(mapnum, prefix) IF NOT isfile(filebase + "_z.tmp") THEN IF dontfallback = NO THEN loadmap_zonemap mapnum ELSE 'Unlike tile- and passmap loading, this doesn't leave the zonemap intact if the 'saved state is the wrong size; instead the zonemap is blanked lump_reloading.zonemap.dirty = NO 'Not correct, but too much trouble to do correctly lump_reloading.zonemap.changed = NO LoadZoneMap zmap, filebase + "_z.tmp" IF zmap.size <> mapsizetiles THEN DIM errmsg as string = "tried to load saved zonemap state which is size " & zmap.size.wh & ", while the map is size " & mapsizetiles.wh IF insideinterpreter THEN scripterr current_command_name() + errmsg, 4 ELSE debug "loadmapstate_zonemap(" + filebase + "_z.tmp): " + errmsg END IF IF dontfallback THEN 'Get rid of badly sized zonemap CleanZoneMap zmap, mapsizetiles.x, mapsizetiles.y ELSE loadmap_zonemap mapnum END IF END IF END IF END SUB 'This function is used only by the "load map state" command 'If prefix="state" then mapnum is a custom state ID SUB loadmapstate_bitmask (mapnum as integer, loadmask as integer, prefix as string, dontfallback as bool = NO) IF loadmask AND 1 THEN loadmapstate_gmap mapnum, prefix, dontfallback END IF IF loadmask AND 2 THEN loadmapstate_npcl mapnum, prefix, dontfallback END IF IF loadmask AND 4 THEN loadmapstate_npcd mapnum, prefix, dontfallback END IF IF loadmask AND 8 THEN loadmapstate_tilemap mapnum, prefix, dontfallback END IF IF loadmask AND 16 THEN loadmapstate_passmap mapnum, prefix, dontfallback END IF IF loadmask AND 32 THEN loadmapstate_zonemap mapnum, prefix, dontfallback END IF END SUB SUB deletemapstate (mapnum as integer, killmask as integer, prefix as string) DIM filebase as string = mapstatetemp(mapnum, prefix) IF killmask AND 1 THEN safekill filebase + "_map.tmp" IF killmask AND 2 THEN safekill filebase + "_l.reld.tmp" IF killmask AND 4 THEN safekill filebase + "_n.tmp" IF killmask AND 8 THEN safekill filebase + "_t.tmp" IF killmask AND 16 THEN safekill filebase + "_p.tmp" IF killmask AND 32 THEN safekill filebase + "_z.tmp" END SUB 'Note a differing number of layers is allowed! FUNCTION tilemap_is_same_size (lumptype as string, what as string) as bool DIM as TilemapInfo newsize GetTilemapInfo maplumpname(gam.map.id, lumptype), newsize IF newsize.size <> mapsizetiles THEN notification "Could not reload " + what + " because the map size has changed. The map must be reloaded. You can do so by pressing F5 to access the Live Preview Debug Menu and selecting 'Reload map'." RETURN NO END IF RETURN YES END FUNCTION #DEFINE debug_reloadmap(what) debuginfo __FUNCTION__ " " #what ".dirty=" & lump_reloading.what.dirty & " " #what ".changed=" & lump_reloading.what.changed & " " #what ".mode=" & lump_reloading.what.mode 'Reload gmap() but don't update map layer slices or tilesets. See also reloadmap_tilemap_and_tilesets. 'Ignores changes to tilesets and to enabled layers because: '1) it must be done after we have the correct number of tilemap layers ' (when called from reload_MAP_lump) '2) When called from Live Preview debug menu, maybe only want to update gmap, ' not tilemaps/tilesets. SUB reloadmap_gmap_no_tilesets() debug_reloadmap(gmap) lump_reloading.gmap.dirty = NO lump_reloading.gmap.changed = NO REDIM gmaptmp(dimbinsize(binMAP)) as integer loadrecord gmaptmp(), game + ".map", getbinsize(binMAP) \ 2, gam.map.id FOR i as integer = 0 TO UBOUND(gmap) IF gmap_index_affects_tiles(i) = NO THEN gmap(i) = gmaptmp(i) NEXT gmap_updates 'does actually reload tilesets, using those in the old gmap() ' Behaviour specific to live-previewing 'Delete saved gmap state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_map.tmp" IF gmap(1) > 0 THEN wrappedsong gmap(1) - 1 ELSEIF gmap(1) = 0 THEN stopsong END IF END SUB SUB reloadmap_npcl(merge as bool) lump_reloading.npcl.changed = NO 'Delete saved state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_l.reld.tmp" DIM filename as string = maplumpname(gam.map.id, "l") lump_reloading.npcl.hash = file_hash64(filename) IF merge THEN REDIM npcnew(UBOUND(npc)) as NPCInst REDIM npcbase(UBOUND(npc)) as NPCInst LoadNPCL filename, npcnew() LoadNPCL tmpdir + "mapbackup.l", npcbase() FOR i as integer = 0 TO UBOUND(npc) 'The .L file format currently doesn't contain extra data, and should never load .curzones or .sl IF npcnew(i).extravec ORELSE npcbase(i).extravec THEN showbug "reload npcl: extravec not implemented" IF memcmp(@npcnew(i), @npcbase(i), SIZEOF(NPCInst)) THEN 'If an NPC was removed in Custom, and meanwhile it's been replaced in-game, then 'don't overwrite it. IF npc(i).id <> npcbase(i).id AND npcnew(i).id = 0 THEN CONTINUE FOR 'Otherwise Custom has priority CleanNPCInst npc(i) npc(i) = npcnew(i) END IF NEXT ELSE LoadNPCL filename, npc() writeablecopyfile filename, tmpdir + "mapbackup.l" END IF 'Evaluate whether NPCs should appear or disappear based on tags or validity of pool/ID visnpc END SUB SUB reloadmap_npcd() 'Delete saved state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_n.tmp" loadmap_npcd gam.map.id END SUB SUB reloadmap_tilemap_and_tilesets(merge as bool) debug_reloadmap(maptiles) 'Delete saved state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_t.tmp" IF tilemap_is_same_size("t", "tilemaps") THEN lump_reloading.maptiles.changed = NO DIM filename as string = maplumpname(gam.map.id, "t") lump_reloading.maptiles.hash = file_hash64(filename) IF merge THEN 'Note: Its possible for this to fail if the number of layers differs MergeTileMaps maptiles(), filename, tmpdir + "mapbackup.t" ELSE lump_reloading.maptiles.dirty = NO LoadTileMaps maptiles(), filename writeablecopyfile filename, tmpdir + "mapbackup.t" END IF 'Now reload tileset and layering info REDIM gmaptmp(dimbinsize(binMAP)) as integer loadrecord gmaptmp(), game + ".map", getbinsize(binMAP) \ 2, gam.map.id FOR i as integer = 0 TO UBOUND(gmap) IF gmap_index_affects_tiles(i) THEN gmap(i) = gmaptmp(i) NEXT 'Calls refresh_map_slice, updating number of layers, tilemaps, 'layer visibility (gmap(19)) and position of walkabout layer (gmap(31)) update_map_slices_for_new_tilemap loadmaptilesets tilesets(), gmap() refresh_map_slice_tilesets END IF END SUB SUB reloadmap_passmap(merge as bool) debug_reloadmap(passmap) 'Delete saved state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_p.tmp" IF tilemap_is_same_size("p", "wallmap") THEN lump_reloading.passmap.changed = NO DIM filename as string = maplumpname(gam.map.id, "p") lump_reloading.passmap.hash = file_hash64(filename) IF merge THEN MergeTileMap pass, filename, tmpdir + "mapbackup.p" ELSE lump_reloading.passmap.dirty = NO LoadTileMap pass, filename writeablecopyfile filename, tmpdir + "mapbackup.p" END IF END IF END SUB SUB reloadmap_foemap() debug_reloadmap(foemap) 'Delete saved state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_e.tmp" IF tilemap_is_same_size("e", "foemap") THEN loadmap_foemap gam.map.id END IF END SUB SUB reloadmap_zonemap() debug_reloadmap(zonemap) 'Delete saved state to prevent regressions safekill mapstatetemp(gam.map.id, "map") + "_z.tmp" loadmap_zonemap gam.map.id END SUB SUB deletetemps 'deletes game-state temporary files from tmpdir when exiting back to the titlescreen or loading a game REDIM filelist() as string findfiles tmpdir, ALLFILES, fileTypeFile, YES, filelist() DIM filename as string FOR i as integer = 0 TO UBOUND(filelist) filename = LCASE(filelist(i)) IF ends_with(filename, ".tmp") ANDALSO (starts_with(filename, "map") ORELSE starts_with(filename, "state")) THEN killfile tmpdir + filelist(i) END IF NEXT END SUB '============================================================================== 'Print all NPCs to g_debug.txt (not used) SUB debug_npcs () FOR p as integer = 0 to 1 debug npc_pool_name(p) & " NPC types:" FOR i as integer = 0 TO UBOUND(npool(p).npcs) debug " ID " & i & ": pic=" & npool(p).npcs(i).picture & " pal=" & npool(p).npcs(i).palette NEXT NEXT p debug "NPC instances:" FOR i as integer = 0 TO UBOUND(npc) WITH npc(i) IF .id <> 0 THEN DIM npcinfo as string npcinfo = " " & i & ": ID=" & (ABS(.id) - 1) & IIF(.id < 0, " (hidden)", "") & " pos=" & .pos DIM where as XYPair IF framewalkabout(npc(i).pos + XY(0, gmap(11)), where, mapsizetiles * 20, gmap(5), 0) THEN npcinfo &= " screenpos=" & where END IF debug npcinfo END IF END WITH NEXT END SUB FUNCTION describe_npctype(npcid as NPCTypeID, pool as integer) as string DIM info as string, appearinfo as string IF npcid > UBOUND(npool(pool).npcs) THEN RETURN "NPC Type ID invalid (not loaded)!" WITH npool(pool).npcs(npcid) info &= fgcol_text("NPC Type: ", uilook(uiSelectedItem)) _ & "Pic `" & .picture & "` Pal `" & .palette _ & "` Speed `" & .speed _ & "` MOVETYPE: `" & npc_movetypes(.movetype) _ & "`, ACTIVATION: `" & npc_usetypes(.activation) _ & "`, FACE: `" & npc_facetypes(.facetype) _ & "`, PUSH: `" & npc_pushtypes(.pushtype) & "`" IF .tag1 THEN appearinfo &= " tag `" & ABS(.tag1) & "=" & onoroff(.tag1) & "`" IF .tag2 THEN appearinfo &= " tag `" & ABS(.tag2) & "=" & onoroff(.tag2) & "`" IF .usetag THEN appearinfo &= " Onetime-use flag `" & .usetag & "`" IF LEN(appearinfo) THEN info &= " APPEAR:" & appearinfo END WITH RETURN ticklite(info, findrgb(160,210,160)) END FUNCTION FUNCTION describe_npcinst(npcnum as NPCIndex) as string DIM info as string WITH npc(npcnum) DIM npcid as NPCTypeID = ABS(.id) - 1 'Calculate NPC copy number DIM copynum as integer FOR i as integer = 0 TO npcnum - 1 IF npc(i).id - 1 = npcid ANDALSO npc(i).pool = .pool THEN copynum += 1 NEXT info = npc_pool_name(.pool) & " ID `" & npcid & "`" IF .id < 0 THEN info &= " (DISABLED)" ELSE info &= " copy `" & copynum & "`" END IF info &= " npcref `" & (-1 - npcnum) & !"`\n" _ & describe_npctype(npcid, .pool) & !"\n" _ & fgcol_text("NPC Inst: ", uilook(uiSelectedItem)) _ & "At `" & .pos & "` Z `" & .z _ & "` tile `" & (.pos \ 20) & "` dir `" & CHR(("NESW")[.dir]) & "`" IF .sl THEN DIM sprite as Slice ptr sprite = LookupSlice(SL_WALKABOUT_SPRITE_COMPONENT, .sl) IF sprite ANDALSO sprite->SliceType = slSprite THEN info &= " frame `" & sprite->SpriteData->frame & "`" END IF END IF DIM extralen as integer = IIF(.extravec, v_len(.extravec), 3) info &= !"\nExtra (len " & extralen & ") `[" FOR i as integer = 0 TO small(10, extralen) - 1 IF i > 0 THEN info &= ", " info &= STR(get_extra(.extravec, i)) NEXT IF extralen > 10 THEN info &= " ..." info &= !"]`\nAI: `" & yesorno(NOT .suspend_ai) _ & "` Usable: `" & yesorno(NOT .suspend_use) _ & "` Walls: `" & yesorno(NOT .ignore_walls) _ & "` Obstruction: `" & yesorno(NOT .not_obstruction) _ & !"`\nXYgo: `" & .xygo _ & "` Pathing: " WITH .pathover IF .override <> NPCOverrideMove.NONE THEN IF .override = NPCOverrideMove.NPC THEN info &= "to NPC `" & .dest_npc & "` stop when reached: `" & yesorno(.stop_when_npc_reached) ELSEIF .override = NPCOverrideMove.POS THEN info &= "to `" & .dest_pos END IF info &= !"`\nCooldown: `" & .cooldown _ & "` Stillticks: `" & npc(npcnum).stillticks _ & "`, Stops after: `" & .stop_after_stillticks & "`" ELSE info &= "`N/A`" END IF END WITH END WITH RETURN ticklite(info, findrgb(160,210,160)) END FUNCTION 'Draw tooltip with info about the NPCs under the mouse cursor 'Draws to dpage PRIVATE SUB npc_debug_display_tooltip () IF readmouse.active = NO THEN EXIT SUB DIM pos as XYPair = readmouse.pos + XY(mapx, mapy) wrapxy pos, 20 'Find each NPC at pos DIM info as string FOR copy as integer = 0 TO 999 DIM npcidx as NPCIndex = npc_at_pixel(pos, copy, YES) IF npcidx = -1 THEN EXIT FOR IF LEN(info) THEN info &= !"\n" info &= describe_npcinst(npcidx) NEXT IF LEN(info) THEN 'info = bgcol_text(info, uilook(uiShadow)) CONST maxwidth = 280 DIM boxsize as XYPair = textsize(info, maxwidth, fontEdged) DIM as RelPos x = readmouse.x + showLeft, y = readmouse.y + 7 + showTop ' If near the bottom of the screen, show the tooltip above the mouse IF readmouse.y + 7 + boxsize.h > vpages(dpage)->h THEN y = readmouse.y + showTop + ancBottom END IF trans_rectangle vpages(dpage), Type(x, y, boxsize.w, boxsize.h), curmasterpal(uilook(uiShadow)), .60 wrapprint info, x, y, uilook(uiText), dpage, maxwidth, , fontEdged END IF END SUB SUB npc_debug_display (draw_walls as bool) DIM temp as string FOR i as integer = 0 TO UBOUND(npc) WITH npc(i) IF .id <> 0 THEN DIM where as XYPair ' Use a margin of 20 pixels, for one extra tile IF framewalkabout(npc(i).pos + XY(0, gmap(11)), where, mapsizetiles * 20, gmap(5), 20) THEN IF draw_walls THEN ' Draw the neighbouring obstructions for each NPC. ' Draw tile edges which the NPC can't pass ' Don't bother to draw obstruction for disabled NPCs IF .id > 0 THEN FOR yoff as integer = -1 TO 1 FOR xoff as integer = -1 TO 1 DIM tile as XYPair = npc(i).pos / 20 + XY(xoff, yoff) IF npc_collision_check_at(npc(i), tile, dirNorth) THEN drawants_for_tile tile, dirNorth IF npc_collision_check_at(npc(i), tile, dirEast) THEN drawants_for_tile tile, dirEast IF npc_collision_check_at(npc(i), tile, dirSouth) THEN drawants_for_tile tile, dirSouth IF npc_collision_check_at(npc(i), tile, dirWest) THEN drawants_for_tile tile, dirWest NEXT xoff NEXT yoff END IF END IF ' Draw the NPC ID and negative NPC reference number (e.g 7 instead of -7) DIM col as integer = IIF(.id < 0, uilook(uiSelectedDisabled), uilook(uiText)) 'the numbers can overlap quite badly, try to squeeze them in temp = STR(ABS(.id) - 1) & IIF(.pool = 1, "g", "") edgeprint MID(temp, 1, 1), where.x, where.y + 3, col, dpage edgeprint MID(temp, 2, 1), where.x + 7, where.y + 3, col, dpage edgeprint MID(temp, 3, 1), where.x + 14, where.y + 3, col, dpage edgeprint MID(temp, 4, 1), where.x + 21, where.y + 3, col, dpage col = uilook(uiDescription) temp = STR(i + 1) edgeprint MID(temp, 1, 1), where.x, where.y + 11, col, dpage edgeprint MID(temp, 2, 1), where.x + 7, where.y + 11, col, dpage edgeprint MID(temp, 3, 1), where.x + 14, where.y + 11, col, dpage 'printstr STR(npc(i).stillticks), where.x, where.y + 20, dpage END IF END IF END WITH NEXT npc_debug_display_tooltip END SUB SUB drawants_for_tile(tile as XYPair, byval direction as DirNum) DIM where as XYPair IF framewalkabout(tile * 20, where, mapsizetiles * 20, gmap(5), 0) THEN SELECT CASE direction CASE dirNorth: drawants vpages(dpage), where.x , where.y , 20, 1 CASE dirEast: drawants vpages(dpage), where.x + 20-1, where.y , 1, 20 CASE dirSouth: drawants vpages(dpage), where.x , where.y + 20-1, 20, 1 CASE dirWest: drawants vpages(dpage), where.x , where.y , 1, 20 CASE ELSE debuginfo "drawants_for_tile: " & tile.x & " " & tile.y & " invalid direction " & direction END SELECT END IF END SUB '============================================================================== ' Add a line (possibly with no time/bar) to the "menu" ' smooth_time: the main time value, shown as a bar. Preferrably smoothed over multiple frames ' frame_time: optional; if available, the actual time for this frame ' skip_zero: remove (eventually) if the time is zero SUB CPUUsageMode.addline(name as string, smooth_time as double = 0., frame_time as double = -1., display_time as double = 0., skip_zero as bool = NO) IF skip_zero ANDALSO smooth_time = 0.0 THEN EXIT SUB DIM idx as integer = UBOUND(this.menu) + 1 REDIM PRESERVE this.menu(0 TO idx) WITH this.menu(idx) .name = name .smooth_time = smooth_time .frame_time = IIF(frame_time = -1, smooth_time, frame_time) .display_time = display_time END WITH END SUB SUB CPUUsageMode.addline(name as string, exptimer as ExpSmoothedTimer, skip_zero as bool = NO) IF skip_zero THEN IF exptimer.cur_time = 0.0 ANDALSO exptimer.smooth_time < 0.05e-3 THEN exptimer.hide_delay -= 1 IF exptimer.hide_delay < 0 THEN exptimer.hide = YES : EXIT SUB ELSE exptimer.hide = NO exptimer.hide_delay = 2 * requested_framerate 'two sec END IF END IF addline name, exptimer.smooth_time, exptimer.cur_time, exptimer.display_time END SUB SUB CPUUsageMode.addline(name as string, id as TimerIDs, skip_zero as bool = NO) addline name, main_timer.timers(id), skip_zero END SUB SUB CPUUsageMode.update() 'We have to stop main_timer in order to read times & update smoothing, so time steps 'start/end here instead of dowait. Time spent sleeping in dowait is counted towards 'TimerIDs.Paused; as a benefit time spent in backend processing in dowait is counted. 'We still specially count the time spent in this sub towards DrawDebug. DIM update_time as double = -TIMER this.halflife = requested_framerate / 3 '1/3 of a second main_timer.finish_timestep this.halflife, this.is_update_tick STATIC last_update_tick as double this.is_update_tick = (last_update_tick + 0.1 < TIMER) '10 times a second IF this.is_update_tick THEN last_update_tick = TIMER ERASE this.menu addline "Gameplay logic (NPCs...)", TimerIDs.Default addline "Scripts", TimerIDs.Scripts, YES addline "Input & backend processing", TimerIDs.IOBackend, YES addline "File IO/loading data", TimerIDs.FileIO, YES addline "Update slices (" & count_slices(SliceTable.root) & " total)", TimerIDs.UpdateSlices DIM draw_total as ExpSmoothedTimer draw_total += main_timer.timers(TimerIDs.DrawSlices) draw_total += main_timer.timers(TimerIDs.DrawOther) draw_total += main_timer.timers(TimerIDs.DrawDebug) addline "Draw (total):", draw_total addline " Draw " & NumDrawnSlices & " slices", TimerIDs.DrawSlices WITH gfx_slice_timer addline " By slice type:" addline " -Map layers", .timers(TimerIDs.Map), YES addline " -Text", .timers(TimerIDs.Text), YES addline " -Sprites/other", .timers(TimerIDs.Default) END WITH WITH gfx_op_timer addline " By draw type:" addline " -Dissolving", .timers(TimerIDs.Dissolve), YES addline " -Rotate/zoom/flip", .timers(TimerIDs.Rotozoom), YES addline " -Blended/transparent", .timers(TimerIDs.Blend), YES addline " -Normal", .timers(TimerIDs.Default) END WITH addline " Draw other (eg menus)", TimerIDs.DrawOther addline " Draw debug displays", TimerIDs.DrawDebug, YES addline "Update screen", TimerIDs.UpdateScreen addline "Total", TimerIDs.Total /' DIM theoretical_fps as integer = 1. / main_timer.timers(TimerIDs.Total).display_time addline " (Equivalent FPS " & theoretical_fps & ")" ' Show 100% - Total as Unused CPU or Excess CPU VAR extratime = main_timer.timers(TimerIDs.Total) DIM targettime as double = 1. / requested_framerate extratime.display_time = targettime - extratime.display_time extratime.smooth_time = targettime - extratime.smooth_time extratime.cur_time = targettime - extratime.cur_time addline IIF(extratime.display_time < 0, "Excess", "Unused") & " CPU", extratime ' Time spent sleeping in dowait. this is ususally almost equal to Unused CPU addline "Sleep (spare time)", TimerIDs.Pause '/ update_time += TIMER main_timer.begin_timestep main_timer.add_time TimerIDs.DrawDebug, update_time END SUB ' Draw timing overlay SUB CPUUsageMode.display(page as integer) this.update() main_timer.switch TimerIDs.DrawDebug CONST lineheight = 12 CONST barheight = 7 DIM red as integer = findrgb(255,0,0) DIM blue as integer = findrgb(50,50,255) DIM yellow as integer = findrgb(255,255,80) edgeprint "F1 help", pRight, pTop, uilook(uiMenuItem), page DIM targettime as double = 1. / requested_framerate DIM sec_to_px as double = vpages(page)->w / targettime FOR idx as integer = 0 TO UBOUND(this.menu) WITH this.menu(idx) DIM text as string = .name IF .display_time <> 0. THEN ' .display_time = 0 indicates a note DIM col as integer = IIF(.smooth_time < 0, red, blue) ' Draw a bar, with 100% of targettime shown as the full screen width edgebox 0, idx * lineheight, ABS(.smooth_time) * sec_to_px, barheight, col, uilook(uiBackground), page ' If above 100%, wrap around to left of screen, overlaying a red bar IF .smooth_time > targettime THEN edgebox 0, idx * lineheight, (.smooth_time - targettime) * sec_to_px, barheight, red, uilook(uiBackground), page END IF rectangle (.frame_time * sec_to_px) MOD vpages(page)->w, idx * lineheight, 1, barheight, yellow, page text = rpad(text, "." , 27) IF gam.debug_timings = 1 THEN text &= strprintf("%5.1f", 100 * .display_time / targettime) & "%" ELSE text &= strprintf("%4.1f", .display_time * 1e3) & "ms" END IF END IF edgeprint text, 0, idx * lineheight + 2, uilook(uiText), page END WITH NEXT END SUB ' Called to stop timing SUB CPUUsageMode.disable() ' If main_timer.enabled this stops it, otherwise does nothing main_timer.finish_timestep this.halflife, NO gam.debug_timings = 0 END SUB '============================================================================== SUB limitcamera (byref x as integer, byref y as integer) ' The slice the map is drawn "onto" DIM mapview as Slice ptr mapview = SliceTable.Root IF gmap(5) = mapEdgeCrop THEN ' When cropping the camera to the map, stop camera movements that attempt to go over the edge DIM oldmapx as integer = x DIM oldmapy as integer = y ' IF the map is smaller than the screen, centre it. DIM as integer padleft, padtop padleft = -large(0, mapview->Width - mapsizetiles.x * 20) \ 2 padtop = -large(0, mapview->Height - mapsizetiles.y * 20) \ 2 ' Need to call large, small in this order rather than using bound ' to handle maps which are larger than the screen correctly. x = padleft + large(small(x, mapsizetiles.x * 20 - mapview->Width), 0) y = padtop + large(small(y, mapsizetiles.y * 20 - mapview->Height), 0) IF oldmapx <> x THEN IF gen(genCameraMode) = pancam THEN gen(genCameraMode) = stopcam END IF IF oldmapy <> y THEN IF gen(genCameraMode) = pancam THEN gen(genCameraMode) = stopcam END IF END IF IF gmap(5) = mapEdgeWrap THEN ' Wrapping map. Wrap the camera according to the center, not the top-left x += mapview->Width \ 2 y += mapview->Height \ 2 wrapxy x, y, 20 x -= mapview->Width \ 2 y -= mapview->Height \ 2 END IF END SUB FUNCTION game_setoption(opt as string, arg as string) as integer IF opt = "errlvl" THEN IF parse_int(arg, @err_suppress_lvl) THEN RETURN 2 ELSE err_suppress_lvl = serrSuspicious 'Hide warnings and 'suspicious' warnings RETURN 1 END IF ELSEIF opt = "autotest" THEN debug "Autotesting mode enabled!" autotestmode = YES use_speed_control = NO RETURN 1 'arg not used ELSEIF opt = "scriptlog" THEN gam.script_log.enabled = YES RETURN 1 /' There's a Debug Menu function to set time_specific_cmdid, so this arg isn't needed ELSEIF opt = "timecmd" THEN 'Can't accept a command name because haven't loaded command names from an .hsp 'file (scrcommands.bi only has a subset). DIM cmdid as integer IF parse_int(arg, @cmdid) ANDALSO cmdid >= 0 ANDALSO cmdid <= maxScriptCmdID THEN time_specific_cmdid = cmdid RETURN 2 ELSE debug "Invalid --timecmd arg " & arg RETURN 1 END IF '/ ELSEIF opt = "print" THEN gam.print_trace = YES RETURN 1 ELSEIF opt = "printonly" THEN gam.print_trace = YES gam.print_trace_only = YES RETURN 1 ELSEIF opt = "debugkeys" THEN 'Undocumented debuginfo "--debugkeys used" always_enable_debug_keys = YES RETURN 1 'arg not used ELSEIF opt = "autosnap" then IF parse_int(arg, @autosnap) THEN debug "Autosnap mode enabled every " & autosnap & " ticks" RETURN 2 ELSE debug "WARNING: autosnap argument was ignored because it should be followed by an integer" RETURN 1 END IF #IFNDEF NO_TEST_GAME ELSEIF opt = "from_Custom" THEN IF arg = "" THEN debug "-from_Custom option ignored because channel not specified" RETURN 1 END IF IF channel_open_client(channel_to_Custom, arg) THEN running_under_Custom = YES debuginfo "Reading commands from channel_to_Custom '" & arg & "'" hook_after_attach_to_Custom YES RETURN 2 ELSE debug "Failed to open channel '" & arg & "'" hook_after_attach_to_Custom NO terminate_program 10 RETURN 1 END IF #ENDIF ELSEIF opt = "reset_platform_achievements" THEN debug "Enqueuing platform achievement reset" Achievements.definitions_reset RETURN 1 ELSEIF opt = "debug-achieve" THEN Achievements.enable_debug = true RETURN 1 ELSEIF opt = "noexit" THEN debuginfo "Disable exit to OS" disable_exit = YES RETURN 1 'arg not used END IF RETURN 0 END FUNCTION 'Decide on the scale factor given a target maximum fraction of the screen size FUNCTION automatic_scale_factor (screen_fraction as double) as integer DIM scale as integer = 2 DIM as integer screenwidth, screenheight get_screen_size screenwidth, screenheight debuginfo "automatic_scale_factor(" & screen_fraction & "), screen size: " & screenwidth & "*" & screenheight IF screenwidth > 0 AND screenheight > 0 THEN scale = small((screen_fraction * screenwidth) / gen(genResolutionX), (screen_fraction * screenheight) / gen(genResolutionY)) IF scale < 1 THEN scale = 1 'Reduce the scale until it fits on the monitor 'Kludge: Allow a little extra margin in the height to allow space for titlebar and taskbar. '(Note: under Windows os_get_screen_size already excludes taskbar but not titlebar) WHILE scale * gen(genResolutionX) > screenwidth OR scale * gen(genResolutionY) > screenheight - 20 IF scale = 1 THEN EXIT WHILE scale -= 1 WEND END IF RETURN scale END FUNCTION 'Set the game resolution, size of the window, and bitdepth. 'This can be called when live-previewing when resolution settings change SUB apply_game_window_settings (reloading as bool = NO) IF gen(gen32bitMode) THEN switch_to_32bit_vpages ELSE switch_to_8bit_vpages 'This can happen while live-previewing, or maybe messing around with writegeneral IF gen(genResolutionX) < 10 OR gen(genResolutionY) < 10 THEN EXIT SUB lock_resolution IF XY(gen(genResolutionX), gen(genResolutionY)) <> get_resolution() THEN 'get_resolution() will be 320x200 if the backend doesn't support anything else IF gfx_supports_variable_resolution() = NO THEN DIM varresbackends(...) as string = {"sdl2", "sdl", "fb"} FOR idx as integer = 0 TO UBOUND(varresbackends) DIM name as string = varresbackends(idx) IF have_gfx_backend(name) THEN debuginfo "Attempting to switch to gfx_" & name & " for flexible resolution" IF switch_gfx(name) THEN EXIT FOR END IF NEXT END IF IF gfx_supports_variable_resolution() = NO THEN notification "This game requires use of the gfx_sdl/gfx_sdl2/fb backend; other graphics backends do not support customisable resolution. Continuing anyway, but the game will probably be unplayable!" ELSE 'Changes video page size, but not window size immediately gfx_recenter_window_hint() set_resolution(gen(genResolutionX), gen(genResolutionY)) IF SliceTable.Root THEN 'This SUB gets called before SetupGameSlices 'When resolution changes, change Slice Root too SliceTable.Root->width = gen(genResolutionX) SliceTable.Root->height = gen(genResolutionY) END IF 'Always recenter (need to call setvispage immediately, or the hint may be lost by calling e.g. set_safe_zone_margin) gfx_recenter_window_hint() setvispage vpage, NO 'Calling this is only needed when live-previewing UpdateScreenSlice() END IF END IF IF overrode_default_zoom = NO THEN 'Didn't specify scaling on cmdline, so figure out what scale to use. 'TODO: screen_size_percent == 100 should be treated specially by maximising the window 'and adding black bars, once any backends support that. DIM scale as integer IF running_under_Custom THEN scale = automatic_scale_factor(0.1 * gen(genLivePreviewWindowSize)) ELSE scale = automatic_scale_factor(0.1 * gen(genWindowSize)) END IF 'This should cause backend to automatically recenter window if necessary. set_scale_factor scale END IF IF gam.shared_fullscreen_setting = NO THEN gam.fullscreen_config_file = game_config_file END IF IF supports_fullscreen_toggling_well() AND overrode_default_fullscreen = NO AND _ user_toggled_fullscreen = NO AND running_under_Custom = NO AND _ gam.shared_fullscreen_setting = NO THEN DIM setting as string = read_ini_prefixed_str(gam.fullscreen_config_file, "gfx.fullscreen") IF setting = "" THEN setting = read_config_str("gfx.fullscreen") 'Also reads game_config_file, I think that's OK DIM fullscreen as bool = str2bool(setting, gen(genFullscreen)) ' genFullscreen is used only if the player has never customised the setting, and there's no global override debuginfo "config gfx.fullscreen = " & setting & ", genFullscreen = " & gen(genFullscreen) gfx_setwindowed(fullscreen = NO) ELSE debuginfo "Preserving fullscreen/windowed state" END IF IF gam.started_by_run_game THEN debuginfo "A previous game set shared_fullscreen_setting" ELSE debuginfo "genRungameFullscreenIndependent: " & gen(genRungameFullscreenIndependent) gam.shared_fullscreen_setting = (gen(genRungameFullscreenIndependent) = 0) END IF END SUB SUB set_speedcontrol (byval millisec_per_frame as integer) ' See also set_animation_framerate speedcontrol = bound(millisec_per_frame, 5, 200) IF gfx_vsync_supported() = NO THEN ' Special cases to hit exact values like 60fps rather than 62.5. ' Targetting slightly above 60fps can cause gfx_present to start doing a lot of ' waiting for vsync (e.g. gfx_sdl2 under X11 with compositing) ' Note that setvispage performs frameskipping above 60. ' Disabled under gfx_directx, where have to try to run slightly faster than 60/30 ' so that vsync can add a wait. IF speedcontrol = 8 THEN '120 FPS speedcontrol = 8.333 ELSEIF speedcontrol = 11 THEN '90 FPS speedcontrol = 11.111 ELSEIF speedcontrol = 16 THEN '60 FPS speedcontrol = 16.666 ELSEIF speedcontrol = 33 THEN '30 FPS speedcontrol = 33.333 END IF END IF END SUB '============================================================================== ' Live-preview reloading '============================================================================== #IFNDEF NO_TEST_GAME SUB wrong_spawned_version_fatal_error fatalerror !"This version of Game differs from the version of Custom which spawned it and cannot be used for the ""Test Game"" option. Download and place matching versions in the same directory before trying again.\n" _ "Game player is version " + short_version & !"\n" _ "Editor is version " + custom_version END SUB SUB check_Game_Custom_versions_match () IF short_version <> custom_version THEN pop_warning !"Warning: This version of Game is not exactly identical to the version of Custom that spawned it. There's no chance of corrupting your game, but something might go haywire.\n" _ "Game is version " + short_version + !"\n" _ "Custom is version " + custom_version END IF END SUB SUB handshake_with_Custom () DIM line_in as string FOR i as integer = 1 TO 3 IF channel_input_line(channel_to_Custom, line_in) = 0 THEN 'Custom is meant to have already sent the initialisation messages by now debuginfo "handshake_with_Custom: no message on channel" fatalerror "Could not communicate with Custom" END IF debuginfo "Received message from Custom: " & line_in SELECT CASE i CASE 1 'Parse version string REDIM pieces() as string split line_in, pieces(), "," IF pieces(0) <> "V OHRRPGCE" THEN fatalerror "Could not communicate with Custom" END IF IF UBOUND(pieces) >= 3 THEN custom_version = pieces(3) ELSE custom_version = "<unknown>" END IF IF pieces(1) <> STR(CURRENT_TESTING_IPC_VERSION) THEN 'wrong protocol version wrong_spawned_version_fatal_error END IF CASE 2 'Get sourcerpg IF LEFT(line_in, 2) <> "G " THEN fatalerror "Unexpected command from Custom" END IF sourcerpg = MID(line_in, 3) CASE 3 'Get workingdir IF LEFT(line_in, 2) <> "W " THEN fatalerror "Unexpected command from Custom" END IF workingdir = MID(line_in, 3) IF isdir(workingdir) = 0 THEN fatalerror !"Communication error with Custom:\n" & workingdir & !"\ndoes not exist" END IF END SELECT NEXT 'Set this hook to throw an error on any detected write in workingdir; 'also needed to set a shared lock when reading a file. 'Also, we disallow LAZYCLOSE to ensure we don't interfere with Custom 'trying to delete/replace files on Windows (although renamefile 'should work anyway, so we could allow lazyclose). set_OPEN_hook @inworkingdir, NO, NO can_write_to_workingdir = NO END SUB 'Reads and handles messages from Custom, updating modified_lumps SUB receive_file_updates () 'This sub may be called from all sorts of places, so prevent reentering STATIC entered as bool = NO IF entered THEN EXIT SUB entered = YES DIM line_in as string REDIM pieces() as string WHILE channel_input_line(channel_to_Custom, line_in) debuginfo "msg: " & line_in split RTRIM(line_in), pieces(), " " IF pieces(0) = "M" THEN 'file modified/created/deleted line_in = MID(line_in, 3) v_append_once modified_lumps, line_in ELSEIF pieces(0) = "CM" THEN 'please close music file DIM songnum as integer = str2int(pieces(1)) IF songnum = presentsong THEN music_stop 'Send confirmation channel_write_line(channel_to_Custom, line_in) ELSEIF pieces(0) = "PAL" THEN 'palette changed (path of least resistance...) DIM palnum as integer = str2int(pieces(1)) palette16_update_cache(palnum) ELSEIF pieces(0) = "SCREEN" THEN 'Write a screenshot to file every tick IF pieces(1) = "STOP" THEN stop_recording_video ELSE start_forwarding_screen MID(line_in, 8) END IF ELSEIF pieces(0) = "Q" THEN 'quit! music_stop 'DIR might be holding a handle for the last directory on which it was run, which could prevent 'Custom from deleting workingdir. So reset it. '(findfiles() now does this automatically, but in case something else calls DIR...) #IFDEF __FB_WIN32__ DIM dummy as string = DIR("C:\") #ENDIF 'Send confirmation channel_write_line(channel_to_Custom, "Q ") channel_close(channel_to_Custom) EXIT WHILE ELSEIF pieces(0) = "P" THEN 'ping ELSE debug "Did not understand message from Custom: " & line_in END IF WEND IF channel_to_Custom = NULL THEN 'Opps, it closed. Better quit immediately because workingdir is probably gone (crashy) /' This isn't very useful. IF yesno("Lost connection to Custom; the game has to be closed. Do you want to save the game first? (WARNING: resulting save might be corrupt)", NO) THEN DIM slot as integer = picksave() IF slot >= 0 THEN savegame slot END IF '/ exitprogram NO, 0 END IF entered = NO END SUB 'Live-previewing: Try to update stuff after .gen is written to by Custom 'This is very probably far less complete than it could be SUB reload_gen() REDIM newgen(499) as integer xbload game + ".gen", newgen(), "reload lumps: .gen unreadable" 'If "load palette" has been used (sets gam.current_master_palette), don't update palette. '(Not that it really matters) IF gam.current_master_palette = gen(genMasterPal) _ AND newgen(genMasterPal) <> gen(genMasterPal) THEN gam.current_master_palette = newgen(genMasterPal) load_master_and_uicol gam.current_master_palette setpal master() END IF DIM should_reset_window as bool = NO FOR j as integer = 0 TO UBOUND(gen) IF gen(j) <> newgen(j) THEN 'Before updating gen() SELECT CASE j CASE 44 TO 54, genTextboxBackdrop, genJoy '44-54, 58, 60 CONTINUE FOR 'Ignore. ' Don't need to ignore genMusicVolume, genSFXVolume, since they're only read once END SELECT gen(j) = newgen(j) 'After updating gen() SELECT CASE j CASE genResolutionX, genResolutionY, genWindowSize, genLivePreviewWindowSize, genRungameFullscreenIndependent, gen32bitMode should_reset_window = YES CASE genMillisecPerFrame set_speedcontrol gen(genMillisecPerFrame) set_animation_framerate gen(genMillisecPerFrame) CASE genInventSlotx1Display 'Reset inventory slot names FOR slot as integer = 0 TO last_inv_slot() update_inventory_caption slot NEXT END SELECT END IF NEXT 'TODO: does anything else need to be reloaded when gen() changes? 'Number of elements maybe? IF should_reset_window THEN apply_game_window_settings YES REDIM PRESERVE remembered_menu_pts(gen(genMaxMenu)) END SUB 'Live-previewing. SUB reload_general_reld() ' Do not call close_general_reld(), that writes it, which is an error. FreeDocument gen_reld_doc gen_reld_doc = 0 LoadUIColors uilook(), boxlook(), gam.current_master_palette, master() load_non_elemental_elements gam.non_elemental_elements() 'Not bothering to reload: button codenames, purchases, default safe zone margin, arrowsets/gamepad settings END SUB 'Live-previewing: Reload parts of a HeroState when its HeroDef may have changed 'Currently almost nothing is reloaded! PRIVATE SUB update_hero_state(herost as HeroState, hero as HeroDef) IF herost.hand_pos_overridden = NO THEN FOR i as integer = 0 TO 1 herost.hand_pos(i) = hero.hand_pos(i) NEXT END IF herost.rename_on_status = xreadbit(hero.bits(), 25) END SUB SUB reload_heroes_reld() DIM doc as DocPtr doc = LoadDocument(workingdir & SLASH & "heroes.reld") IF doc = 0 THEN EXIT SUB 'Shouldn't happen! 'Update each hero in the party (this time, we can skip over empty party slots) FOR slot as integer = 0 TO UBOUND(gam.hero) DIM id as integer = gam.hero(slot).id IF id < 0 THEN CONTINUE FOR DIM heronode as NodePtr = NodeByPath(doc, "/hero[" & id & "]") IF heronode THEN DIM hero as HeroDef load_hero_from_reload hero, heronode, id update_hero_state gam.hero(slot), hero ELSE debug "reload_heroes_reld: missing hero ID " & id END IF NEXT FreeDocument doc load_special_tag_caches 'includes hero tags END SUB 'Ignores changes to tilesets. That is handled by try_reload_map_lump and happens only when .T changes. SUB reload_MAP_lump() WITH lump_reloading 'Here we only mark stuff to be reloaded (or state to be deleted), actual 'reloading is in reloadmap_gmap_no_tilesets. 'Only compare part of each MAP record... OK, this is getting really perfectionist 'Thank goodness this will be simpler when the map file format is replaced... 'We ignore changes to tilesets and to enabled layers as this gets called before all 'map lumps have been reloaded. Tilemaps must be loaded first, so we have right number of Map slices. REDIM compare_mask(dimbinsize(binMAP)) as bool FOR i as integer = 0 TO UBOUND(compare_mask) compare_mask(i) = (gmap_index_affects_tiles(i) = NO) NEXT 'Compare with backup to find the changes REDIM changed_records(0) as integer IF compare_files_by_record(changed_records(), game + ".map", tmpdir + "mapbackup.map", getbinsize(binMAP) \ 2, @compare_mask(0)) = NO THEN debug "reload_MAP_lump: couldn't compare!" EXIT SUB END IF FOR mapno as integer = 0 TO UBOUND(changed_records) 'delete saved state IF changed_records(mapno) <> 0 THEN IF .gmap.mode <> loadmodeNever THEN 'Merge/always/if unchanged only safekill mapstatetemp(mapno, "map") + "_map.tmp" END IF END IF NEXT IF changed_records(gam.map.id) <> 0 THEN '--never/always/if unchanged only .gmap.changed = YES IF .gmap.dirty THEN IF .gmap.mode = loadmodeAlways THEN reloadmap_gmap_no_tilesets ELSE IF .gmap.mode <> loadmodeNever THEN reloadmap_gmap_no_tilesets END IF END IF 'Check whether layer settings (eg tilesets) have changed, so 'we will need to reload .T## even if it hasn't changed REDIM gmaptmp(dimbinsize(binMAP)) as integer loadrecord gmaptmp(), game + ".map", getbinsize(binMAP) \ 2, gam.map.id FOR i as integer = 0 TO UBOUND(gmap) IF gmap(i) <> gmaptmp(i) ANDALSO gmap_index_affects_tiles(i) THEN debuginfo "reload_MAP_lump: layers changed" lump_reloading.maptiles.changed = YES EXIT FOR END IF NEXT END WITH END SUB 'Check whether a lump is an .RGFX, .PT#, .MXS or .TIL lump. Reload them. FUNCTION try_reload_gfx_lump(lumpname as string, extn as string) as bool IF extn = "til" THEN sprite_update_cache sprTypeTileset RETURN YES ELSEIF extn = "mxs" THEN sprite_update_cache sprTypeBackdrop RETURN YES ELSEIF lumpname = "enemies.rgfx" THEN 'Multiple sprtypes in the same file sprite_update_cache sprTypeSmallEnemy sprite_update_cache sprTypeMediumEnemy sprite_update_cache sprTypeLargeEnemy sprite_update_cache sprTypeEnemy ELSEIF extn = "rgfx" THEN DIM sprtype as integer = a_find(rgfx_lumpnames(), lumpname) IF sprtype = -1 THEN RETURN NO sprite_update_cache sprtype RETURN YES ELSEIF LEFT(extn, 2) = "pt" THEN DIM ptno as integer IF parse_int(MID(extn, 3), @ptno) THEN sprite_update_cache ptno RETURN YES END IF END IF RETURN NO END FUNCTION 'Check whether a lump is a (supported) map lump, and if so return YES and reload it if needed. 'Also updates lump_reloading flags and deletes mapstate data as required. 'Currently supports: T, P, E, Z, N, L 'Elsewhere: MAP (except tilesets), DOX, globalnpcs#.n 'No need to reload: D 'Not going to bother with: MN 'Tilesets are reloaded only when .T changes. Which isn't perfect right now, but will make sense when tilemap format is replaced. FUNCTION try_reload_map_lump(basename as string, extn as string) as bool DIM typecode as string DIM mapnum as integer = -1 'Check for <archinym>.X## and ###.X DIM extnnum as integer = -1 IF LEN(extn) = 3 THEN extnnum = str2int(MID(extn, 2), -1) DIM basenum as integer = str2int(basename, -1) IF extnnum <> -1 ANDALSO basename = trimpath(game) THEN mapnum = extnnum ELSEIF basenum >= 100 ANDALSO LEN(extn) = 1 THEN mapnum = basenum ELSE RETURN NO 'Isn't a map lump END IF typecode = LEFT(extn, 1) IF INSTR("tpeznl", typecode) = 0 THEN RETURN NO 'Isn't a recognised/supported map lump WITH lump_reloading IF mapnum <> gam.map.id THEN 'Affects map(s) other then the current one. However, we should still delete saved map state. 'Not really sure what to do if the mode loadmodeIfUnchanged or loadmodeMerge... deleting seems safest bet. DIM statefile as string = mapstatetemp(mapnum, "map") + "_" + typecode IF typecode = "l" THEN statefile += ".reld.tmp" ELSE statefile += ".tmp" SELECT CASE typecode CASE "t" IF .maptiles.mode <> loadmodeNever THEN safekill statefile CASE "p" IF .passmap.mode <> loadmodeNever THEN safekill statefile CASE "e" IF .foemap.mode <> loadmodeNever THEN safekill statefile CASE "z" IF .foemap.mode <> loadmodeNever THEN safekill statefile CASE "l" IF .npcl.mode <> loadmodeNever THEN safekill statefile CASE "n" IF .npcd.mode <> loadmodeNever THEN safekill statefile CASE ELSE RETURN NO END SELECT 'This is a lump for a specific map other than the current, stop. RETURN YES END IF 'This is one of the current map's lumps DIM newhash as ulongint = file_hash64(workingdir + basename + "." + extn) SELECT CASE typecode CASE "t" '--all modes supported IF .maptiles.changed = NO THEN 'reload_MAP_lump sets .maptiles.changed = YES if 'the tilesets have changed and we need to reload. '(Warning: this assumes that the .t lump will always be rewritten by the map 'editor after modifying .map... which is currently the case. We'll replace 'the map file formats before that assumption is violated) IF .maptiles.hash = newhash THEN RETURN YES END IF .maptiles.changed = YES IF .maptiles.dirty THEN IF .maptiles.mode = loadmodeAlways THEN reloadmap_tilemap_and_tilesets NO IF .maptiles.mode = loadmodeMerge THEN reloadmap_tilemap_and_tilesets YES ELSE IF .maptiles.mode <> loadmodeNever THEN reloadmap_tilemap_and_tilesets NO END IF CASE "p" '--all modes supported IF .passmap.hash = newhash THEN RETURN YES .passmap.changed = YES IF .passmap.dirty THEN IF .passmap.mode = loadmodeAlways THEN reloadmap_passmap NO IF .passmap.mode = loadmodeMerge THEN reloadmap_passmap YES ELSE IF .passmap.mode <> loadmodeNever THEN reloadmap_passmap NO END IF CASE "e" '--never/always only IF .foemap.hash = newhash THEN RETURN YES .foemap.changed = YES IF .foemap.mode = loadmodeAlways THEN reloadmap_foemap CASE "z" '--never/always/if unchanged only IF .zonemap.hash = newhash THEN RETURN YES .zonemap.changed = YES IF .zonemap.dirty THEN IF .zonemap.mode = loadmodeAlways THEN reloadmap_zonemap ELSE IF .zonemap.mode <> loadmodeNever THEN reloadmap_zonemap END IF CASE "l" '--never/always/merge only IF .npcl.hash = newhash THEN RETURN YES .npcl.changed = YES IF .npcl.mode <> loadmodeNever THEN reloadmap_npcl (.npcl.mode = loadmodeMerge) CASE "n" '--never/always/if unchanged only IF .npcd.hash = newhash THEN RETURN YES .npcd.changed = YES IF .npcd.dirty THEN IF .npcd.mode = loadmodeAlways THEN reloadmap_npcd ELSE IF .npcd.mode <> loadmodeNever THEN reloadmap_npcd END IF CASE ELSE '??? RETURN NO END SELECT END WITH RETURN YES END FUNCTION FUNCTION try_reload_global_npcs(filename as string) as bool IF filename <> "globalnpcs1.n" THEN RETURN NO /' Future: support multiple NPC pools DIM pool_id as integer = -1 IF extn = "n" ANDALSO starts_with(basename, "globalnpcs") THEN pool_id = str2int(MID(basename, 10), -1) END IF IF pool_id = -1 THEN RETURN NO '/ DIM newhash as ulongint = file_hash64(workingdir + filename) WITH lump_reloading IF .globalnpcs.hash = newhash THEN RETURN YES .globalnpcs.changed = YES IF .globalnpcs.dirty THEN IF .globalnpcs.mode = loadmodeAlways THEN load_global_npcs ELSE IF .globalnpcs.mode <> loadmodeNever THEN load_global_npcs END IF END WITH RETURN YES END FUNCTION 'Returns true (and reloads as needed) if this file is a music file (.## or song##.xxx) FUNCTION try_reload_music_lump(basename as string, extn as string) as bool DIM songnum as integer = str2int(extn, -1) 'BAM songs IF songnum = -1 THEN IF LEFT(basename, 4) = "song" THEN songnum = str2int(MID(basename, 5)) END IF IF songnum = -1 THEN RETURN NO IF songnum = presentsong THEN stopsong playsongnum presentsong END IF RETURN YES END FUNCTION 'Returns true if this file is a sound effect FUNCTION try_reload_sfx_lump(basename as string, extn as string) as bool IF LEFT(basename, 3) <> "sfx" THEN RETURN NO DIM sfxnum as integer = str2int(MID(basename, 4)) IF sfxnum = -1 THEN RETURN NO freesfx sfxnum ' Stop & clear from cache. Don't bother to restart if playing. RETURN YES END FUNCTION 'This sub does lump reloading which is safe to do from anywhere SUB try_reload_lumps_anywhere () ' This is called from global_setkeys_hook but is not reentrant STATIC entered as bool = NO IF entered THEN EXIT SUB entered = YES receive_file_updates DIM i as integer = 0 WHILE i < v_len(modified_lumps) DIM handled as bool = NO DIM basename as string = trimextension(modified_lumps[i]) DIM extn as string = justextension(modified_lumps[i]) 'pal handled with special message IF INSTR(" mn tmn d dor pal sng efs ", " " & extn & " ") THEN handled = YES ELSEIF extn = "gen" THEN '.GEN reload_gen() handled = YES ELSEIF modified_lumps[i] = "general.reld" THEN 'GENERAL.RELD reload_general_reld() handled = YES 'We correctly handle binsize.bin & fixbits.bin updates, but there's no good reason for 'them to happen while live previewing ELSEIF modified_lumps[i] = "binsize.bin" THEN 'BINSIZE.BIN clear_binsize_cache showbug "Received binsize.bin modification, should not happen!" handled = YES ELSEIF modified_lumps[i] = "fixbits.bin" THEN 'FIXBITS.BIN clear_fixbits_cache showbug "Received fixbits.bin modification, should not happen!" handled = YES ELSEIF modified_lumps[i] = "palettes.bin" THEN 'PALETTES.BIN loadpalette master(), gam.current_master_palette setpal master() handled = YES ELSEIF modified_lumps[i] = "uicolors.bin" THEN 'UICOLORS.BIN 'UI colors are now stored in general.reld, but still written to uicolors.bin 'for forwards-compatibility. So ignore this file. handled = YES ELSEIF modified_lumps[i] = "menus.bin" THEN 'MENUS.BIN 'This is far from complete 'Cause cache in getmenuname to be dropped game_unique_id = STR(randint(INT_MAX)) ELSEIF try_reload_gfx_lump(modified_lumps[i], extn) THEN '.RGFX, .PT#, .TIL, .MXS handled = YES ELSEIF extn = "fnt" THEN '.FNT xbload game + ".fnt", current_font(), "Font not loaded" setfont current_font() handled = YES ELSEIF try_reload_music_lump(basename, extn) THEN '.## and song##.xxx (music) handled = YES ELSEIF try_reload_sfx_lump(basename, extn) THEN 'sfx##.xxx (sound effects) handled = YES ELSEIF modified_lumps[i] = "heroes.reld" THEN 'HEROES.RELD reload_heroes_reld handled = YES ELSEIF extn = "dt0" THEN '.DT0 'Ignore: old hero data (redundant to heroes.reld) only for compatibility handled = YES ELSEIF extn = "itm" THEN '.ITM FOR slot as integer = 0 TO last_inv_slot() update_inventory_caption slot NEXT load_special_tag_caches 'includes item tags 'Does anything else need to be done? handled = YES ELSEIF extn = "stt" THEN '.STT loadglobalstrings getstatnames statnames() handled = YES ELSEIF modified_lumps[i] = "browse.txt" THEN 'BROWSE.TXT handled = YES 'ignore ELSEIF extn = "veh" THEN '.VEH reload_vehicle handled = YES ''' Script stufff ELSEIF extn = "hsp" THEN '.HSP lump_reloading.hsp.changed = YES IF lump_reloading.hsp.mode = loadmodeAlways THEN reload_scripts NO handled = YES ELSEIF modified_lumps[i] = "plotscr.lst" THEN 'PLOTSCR.LST load_script_triggers_and_names 'Reloads both plotscr.lst and lookup1.bin handled = YES ELSEIF modified_lumps[i] = "lookup1.bin" THEN 'LOOKUP1.BIN load_script_triggers_and_names 'Reloads both plotscr.lst and lookup1.bin handled = YES END IF IF handled THEN v_delete_slice modified_lumps, i, i + 1 ELSE i += 1 END IF WEND entered = NO END SUB SUB try_to_reload_lumps_onmap () main_timer.substart(TimerIDs.FileIO) 'calls receive_file_updates try_reload_lumps_anywhere DIM i as integer = 0 WHILE i < v_len(modified_lumps) DIM handled as bool = NO DIM basename as string = trimextension(modified_lumps[i]) DIM extn as string = justextension(modified_lumps[i]) IF extn = "map" THEN '.MAP reload_MAP_lump handled = YES ELSEIF extn = "dox" THEN '.DOX DeSerDoors(game + ".dox", gam.map.door(), gam.map.id) handled = YES ELSEIF try_reload_map_lump(basename, extn) THEN '.T, .P, .E, .Z, .N, .L handled = YES ELSEIF try_reload_global_npcs(modified_lumps[i]) THEN 'GLOBALNPCS#.N handled = YES ELSEIF extn = "tap" THEN '.TAP reloadtileanimations tilesets(), gmap() handled = YES ELSEIF extn = "dt1" THEN '.DT1 ' This wipes all changes to all records due to scripts; ' it would be possible to do a record-by-record merge instead, ' but enemy (and formation) edits are probably normally done ' immediately before a battle. writeablecopyfile game + ".dt1", tmpdir & "dt1.tmp" handled = YES ELSEIF extn = "for" THEN '.FOR ' Ditto as for .dt1 writeablecopyfile game + ".for", tmpdir & "for.tmp" handled = YES ELSE debuginfo "did not reload " & modified_lumps[i] handled = YES END IF IF handled THEN v_delete_slice modified_lumps, i, i + 1 ELSE i += 1 END IF WEND main_timer.substop(TimerIDs.FileIO) END SUB FUNCTION lump_reload_mode_to_string (mode as LoadModeEnum) as string IF mode = loadmodeNever THEN RETURN "Never" IF mode = loadmodeAlways THEN RETURN "Always" IF mode = loadmodeIfUnchanged THEN RETURN "If no in-game changes" IF mode = loadmodeMerge THEN RETURN "Merge in-game changes" END FUNCTION SUB LPM_append_reload_mode_item (menu as MenuDef, tooltips() as string, what as zstring ptr, info as LumpReloadState, byval extradata as integer = 0) append_menu_item menu, "Reload " + *what + ": " + lump_reload_mode_to_string(info.mode) menu.last->extra(0) = extradata REDIM PRESERVE tooltips(menu.numitems - 1) END SUB SUB LPM_append_force_reload_item (menu as MenuDef, tooltips() as string, what as zstring ptr, info as LumpReloadState, byval extradata as integer = 0, byval ignore_dirtiness as bool = NO) append_menu_item menu, "Force reload of " + *what menu.last->extra(0) = extradata REDIM PRESERVE tooltips(menu.numitems - 1) DIM tmp as string IF info.changed = 0 AND info.dirty = 0 THEN tmp = "No changes" menu.last->disabled = YES ELSE tmp = "Modified by" IF info.dirty AND ignore_dirtiness = NO THEN tmp += " scripts" IF info.changed THEN tmp += ", by" END IF IF info.changed THEN tmp += " Custom" END IF END IF tooltips(menu.numitems - 1) = tmp END SUB SUB LPM_update (menu1 as MenuDef, st1 as MenuState, tooltips() as string) WITH lump_reloading DeleteMenuItems menu1 REDIM tooltips(0) append_menu_item menu1, "Exit" : menu1.last->extra(0) = 1 append_menu_item menu1, "Reload map" : menu1.last->extra(0) = 2 IF running_under_Custom THEN LPM_append_reload_mode_item menu1, tooltips(), "gen. map data", .gmap, 10 LPM_append_reload_mode_item menu1, tooltips(), "tilemap", .maptiles, 11 LPM_append_reload_mode_item menu1, tooltips(), "wallmap", .passmap, 12 LPM_append_reload_mode_item menu1, tooltips(), "foemap", .foemap, 13 LPM_append_reload_mode_item menu1, tooltips(), "zonemap", .zonemap, 14 LPM_append_reload_mode_item menu1, tooltips(), "npc instances", .npcl, 15 LPM_append_reload_mode_item menu1, tooltips(), "local npc defs.", .npcd, 16 LPM_append_reload_mode_item menu1, tooltips(), "global npc defs.", .globalnpcs, 17 LPM_append_reload_mode_item menu1, tooltips(), "scripts", .hsp, 20 tooltips(UBOUND(tooltips)) += " (Read Help file!)" END IF LPM_append_force_reload_item menu1, tooltips(), "general map data", .gmap, 100 LPM_append_force_reload_item menu1, tooltips(), "tiles", .maptiles, 101 LPM_append_force_reload_item menu1, tooltips(), "wallmap", .passmap, 102 LPM_append_force_reload_item menu1, tooltips(), "foemap", .foemap, 103 LPM_append_force_reload_item menu1, tooltips(), "zones", .zonemap, 104 LPM_append_force_reload_item menu1, tooltips(), "npc instances", .npcl, 105, YES 'NPCL is virtually always dirty LPM_append_force_reload_item menu1, tooltips(), "local npc defs.", .npcd, 106 LPM_append_force_reload_item menu1, tooltips(), "global npc defs.", .globalnpcs, 107 IF running_under_Custom THEN 'Not useful otherwise LPM_append_force_reload_item menu1, tooltips(), "scripts", .hsp, 110 tooltips(UBOUND(tooltips)) += " (See F1 Help!)" END IF init_menu_state st1, menu1 REDIM PRESERVE tooltips(menu1.numitems - 1) END WITH END SUB SUB live_preview_menu () DIM holdscreen as integer holdscreen = duplicatepage(vpage) DIM st1 as MenuState st1.active = YES DIM menu1 as MenuDef menu1.textalign = alignLeft menu1.boxstyle = 3 menu1.translucent = YES menu1.min_chars = 38 menu1.offset.y = -6 REDIM tooltips() as string push_and_reset_gfxio_state DO setwait 55 setkeys IF running_under_Custom THEN try_to_reload_lumps_onmap IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "game_live_preview_menu" LPM_update menu1, st1, tooltips() usemenu st1 SELECT CASE menu1.items[st1.pt]->extra(0) CASE 1 '--exit IF enter_space_click(st1) THEN EXIT DO CASE 2 '--reload map IF enter_space_click(st1) THEN 'delete everything deletemapstate gam.map.id, -1, "map" prepare_map NO, YES END IF CASE 10 '--gmap reload mode st1.need_update OR= intgrabber(lump_reloading.gmap.mode, 0, 2) '3 --merging not implemented CASE 11 '--tile reload mode st1.need_update OR= intgrabber(lump_reloading.maptiles.mode, -1, 2) CASE 12 '--wallmap reload mode st1.need_update OR= intgrabber(lump_reloading.passmap.mode, -1, 2) CASE 13 '--foemap reload mode st1.need_update OR= intgrabber(lump_reloading.foemap.mode, 0, 1) 'Not even possible to modify, so don't confuse people CASE 14 '--zones reload mode st1.need_update OR= intgrabber(lump_reloading.zonemap.mode, 0, 2) 'Can't merge zones CASE 15 '--npcl reload mode st1.need_update OR= intgrabber(lump_reloading.npcl.mode, -1, 1) CASE 16 '--npcd reload mode st1.need_update OR= intgrabber(lump_reloading.npcd.mode, 0, 2) CASE 17 '--global npcs reload mode st1.need_update OR= intgrabber(lump_reloading.globalnpcs.mode, 0, 2) CASE 20 '--script reload mode st1.need_update OR= intgrabber(lump_reloading.hsp.mode, 0, 1) CASE 100 '--force gmap reload IF enter_space_click(st1) THEN 'User asked to reload general map data, not tilemaps, so don't update tilesets and map layers reloadmap_gmap_no_tilesets END IF CASE 101 '--force tile reload IF enter_space_click(st1) THEN reloadmap_tilemap_and_tilesets NO END IF CASE 102 '--force wallmap reload IF enter_space_click(st1) THEN reloadmap_passmap NO END IF CASE 103 '--force foemap reload IF enter_space_click(st1) THEN reloadmap_foemap END IF CASE 104 '--force zonemap reload IF enter_space_click(st1) THEN reloadmap_zonemap END IF CASE 105 '--force npcl reload IF enter_space_click(st1) THEN reloadmap_npcl NO END IF CASE 106 '--force npcd reload IF enter_space_click(st1) THEN reloadmap_npcd END IF CASE 107 '--force global npcs reload IF enter_space_click(st1) THEN load_global_npcs END IF CASE 110 '--force scripts reload IF enter_space_click(st1) THEN reload_scripts END IF END SELECT 'Draw screen copypage holdscreen, vpage draw_menu menu1, st1, vpage IF LEN(tooltips(st1.pt)) THEN basic_textbox tooltips(st1.pt), , vpage, pBottom, , YES, YES END IF setvispage vpage dowait LOOP pop_gfxio_state freepage holdscreen END SUB #ENDIF 'NO_TEST_GAME