'The OHRRPGCE graphics, audio and user input library! 'Please read LICENSE.txt for GNU GPL license details and disclaimer of liability ' 'This module is completely bool-clean (bool always used when appropriate) #include "config.bi" #include "crt/limits.bi" #include "string.bi" #include "common.bi" #include "allmodex.bi" #include "gfx.bi" #include "surface.bi" #include "music.bi" #include "reload.bi" #include "util.bi" #include "const.bi" #include "uiconst.bi" #include "slices.bi" #include "loading.bi" using Reload #ifdef IS_GAME #include "game.bi" 'For exit_gracefully #include "gglobals.bi" 'For carray #endif #ifdef __FB_ANDROID__ 'This is gfx_sdl specific, of course, but a lot of the stuff in our fork of the android fork 'of SDL 1.2 would more make sense to live in totally separate java files, which is something we will 'want to do to support SDL 2 on Android. extern "C" 'Return value is always 1 declare function SDL_ANDROID_EmailFiles(address as zstring ptr, subject as zstring ptr, message as zstring ptr, file1 as zstring ptr = NULL, file2 as zstring ptr = NULL, file3 as zstring ptr = NULL) as integer end extern #endif 'Note: While non-refcounted frames work (at last check), it's not used anywhere, and you most probably do not need it 'NOREFC is also used to indicate uncached Palette16's. Note Palette16's are NOT refcounted in the way as frames const NOREFC = -1234 const FREEDREFC = -4321 type XYPair_node 'only used for floodfill x as integer y as integer nextnode as XYPair_node ptr end type '----------- Local functions ---------- declare function frame_load_uncached(sprtype as SpriteType, record as integer) as Frame ptr declare sub _frame_copyctor cdecl(dest as Frame ptr ptr, src as Frame ptr ptr) declare sub frame_draw_internal(src as Frame ptr, masterpal() as RGBcolor, pal as Palette16 ptr = NULL, x as integer, y as integer, scale as integer = 1, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) declare sub draw_clipped(src as Frame ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) declare sub draw_clipped_scaled(src as Frame ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, scale as integer, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) declare sub draw_clipped_surf(src as Surface ptr, master_pal as RGBPalette ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, trans as bool, dest as Surface ptr) 'declare sub grabrect(page as integer, x as integer, y as integer, w as integer, h as integer, ibuf as ubyte ptr, tbuf as ubyte ptr = 0) declare function write_bmp_header(filen as string, w as integer, h as integer, bitdepth as integer) as integer declare function decode_bmp_bitmask(mask as uint32) as integer declare sub loadbmp32(bf as integer, surf as Surface ptr, infohd as BITMAPV3INFOHEADER) declare sub loadbmp24(bf as integer, surf as Surface ptr) declare sub loadbmp8(bf as integer, fr as Frame ptr) declare sub loadbmp4(bf as integer, fr as Frame ptr) declare sub loadbmp1(bf as integer, fr as Frame ptr) declare sub loadbmprle8(bf as integer, fr as Frame ptr) declare sub loadbmprle4(bf as integer, fr as Frame ptr) declare sub stop_recording_gif() declare sub gif_record_frame(fr as Frame ptr, palette() as RGBcolor) declare function next_unused_screenshot_filename() as string declare sub snapshot_check() declare function calcblock(tmap as TileMap, x as integer, y as integer, overheadmode as integer, pmapptr as TileMap ptr) as integer declare sub screen_size_update () declare sub pollingthread(as any ptr) declare function read_inputtext () as string declare sub update_mouse_state () declare sub load_replay_header () declare sub record_input_tick () declare sub replay_input_tick () declare sub read_replay_length () declare function draw_allmodex_recordable_overlays (page as integer) as bool declare function draw_allmodex_overlays (page as integer) as bool declare sub show_replay_overlay() declare sub hide_overlays () declare sub update_fps_counter (skipped as bool) declare sub allmodex_controls () declare sub replay_controls () declare function time_draw_calls_from_finish() as bool declare function hexptr(p as any ptr) as string declare sub Palette16_delete(f as Palette16 ptr ptr) #define POINT_CLIPPED(x, y) ((x) < clipl orelse (x) > clipr orelse (y) < clipt orelse (y) > clipb) #define PAGEPIXEL(x, y, p) vpages(p)->image[vpages(p)->pitch * (y) + (x)] #define FRAMEPIXEL(x, y, fr) fr->image[fr->pitch * (y) + (x)] ' In a function, pass return value on error #macro CHECK_FRAME_8BIT(fr, what...) if (fr)->image = NULL then ' Probably usually indicates that the Frame is Surface-backed debug __FUNCTION__ & ": NULL Frame.image" return what 'If what isn't given, just "return" end if #endmacro 'Used to dereference a ptr only if not NULL #define IF_PTR(arg) if arg then arg '------------ Global variables ------------ dim modex_initialised as bool = NO dim vpages() as Frame ptr dim vpagesp as Frame ptr ptr 'points to vpages(0) for debugging: fbc outputs typeless debugging symbol dim default_page_bitdepth as integer = 8 '8 or 32. Affects allocatepage only, set by switch_to_*bit_vpages() 'Whether the player has at any point toggled fullscreen/windowed in some low-level way 'like alt+enter or window buttons. dim user_toggled_fullscreen as bool = NO redim fonts(3) as Font ptr 'Toggles 0-1 every time dowait is called dim global_tog as integer 'Convert scancodes to text; Enter does not insert newline! 'This array is a global instead of an internal detail because it's used by charpicker and the font editor 'to work out key mapping for the extended characters. Would be nice if it weren't needed. 'FIXME: discover why this array is filled with empty values on Android 'key2text(0,*): no modifiers 'key2text(1,*): shift 'key2text(2,*): alt 'key2text(3,*): alt+shift dim key2text(3,53) as string*1 => { _ {"", "", "1","2","3","4","5","6","7","8","9","0","-","=","","","q","w","e","r","t","y","u","i","o","p","[","]","","","a","s","d","f","g","h","j","k","l",";","'","`","","\","z","x","c","v","b","n","m",",",".","/"}, _ {"", "", "!","@","#","$","%","^","&","*","(",")","_","+","","","Q","W","E","R","T","Y","U","I","O","P","{","}","","","A","S","D","F","G","H","J","K","L",":","""","~","","|","Z","X","C","V","B","N","M","<",">","?"}, _ {"", "", !"\130",!"\131",!"\132",!"\133",!"\134",!"\135",!"\136",!"\137",!"\138",!"\139",!"\140",!"\141","","",!"\142",!"\143",!"\144",!"\145",!"\146",!"\147",!"\148",!"\149",!"\150",!"\151",!"\152",!"\153","","",!"\154",!"\155",!"\156",!"\157",!"\158",!"\159",!"\160",!"\161",!"\162",!"\163",!"\164",!"\165","",!"\166",!"\167",!"\168",!"\169",!"\170",!"\171",!"\172",!"\173",!"\174",!"\175",!"\176"}, _ {"", "", !"\177",!"\178",!"\179",!"\180",!"\181",!"\182",!"\183",!"\184",!"\185",!"\186",!"\187",!"\188","","",!"\189",!"\190",!"\191",!"\192",!"\193",!"\194",!"\195",!"\196",!"\197",!"\198",!"\199",!"\200","","",!"\201",!"\202",!"\203",!"\204",!"\205",!"\206",!"\207",!"\208",!"\209",!"\210",!"\211",!"\212","",!"\213",!"\214",!"\215",!"\216",!"\217",!"\218",!"\219",!"\220",!"\221",!"\222",!"\223"} _ } ' Translate scancodes scNumpadSlash and up to ASCII. ' Again, Enter is skipped. ' *, -, + are missing, since their scancodes aren't contiguous with the others. dim shared numpad2text(...) as string*1 => {"7","8","9","","4","5","6","","1","2","3","0","."} ' Frame type table DEFINE_VECTOR_OF_TYPE_COMMON(Frame ptr, Frame_ptr, @_frame_copyctor, @frame_unload) '--------- Module shared variables --------- 'For each vpage() element, this records whether it shouldn't be resized when the window size changes (normally is) '(Not fully implemented, as it seems it would only benefit textbox_appearance_editor) 'dim shared fixedsize_vpages() as bool dim shared clippedframe as Frame ptr 'used to track which Frame the clips are set for. dim shared as integer clipl, clipt, clipr, clipb 'drawable area on clippedframe; right, bottom margins are excluded 'The current internal size of the window (takes effect at next setvispage). 'Should only be modified via set_resolution and unlock_resolution dim shared windowsize as XYPair = (320, 200) 'Minimum window size; can't resize width or height below this. Default to (0,0): no bound dim shared minwinsize as XYPair dim shared resizing_enabled as bool = NO 'keeps track of backend state dim shared bordertile as integer 'Tileset animation states dim shared anim1 as integer dim shared anim2 as integer type SkippedFrame page as integer = -1 declare sub drop() declare sub show() end type dim shared waittime as double dim shared flagtime as double = 0.0 dim shared setwait_called as bool dim shared tickcount as integer = 0 dim shared use_speed_control as bool = YES dim shared ms_per_frame as integer = 55 'This is only used by the animation system, not the framerate control dim shared requested_framerate as double 'Set by last setwait dim shared base_fps_multiplier as double = 1.0 'Doesn't include effect of shift+tab dim shared fps_multiplier as double = 1.0 'Effect speed multiplier, affects all setwait/dowaits dim max_display_fps as integer = 90 'Skip frames if drawing more than this. dim shared lastframe as double 'Time at which the last frame was displayed. dim shared blocking_draws as bool = NO 'True if drawing the screen is a blocking call. dim shared skipped_frame as SkippedFrame 'Records the last setvispage call if it was frameskipped. dim shared last_setvispage as integer = -1 'Records the last setvispage. -1 if none. 'Virtually always vpage; in fact using anything other than vpage 'would cause a lot of functions like multichoice to glitch. 'Don't use this directly; call getvispage instead! #IFDEF __FB_DARWIN__ ' On OSX vsync will cause screen draws to block, so we shouldn't try to draw more than the refresh rate. ' (Still doesn't work perfectly) max_display_fps = 60 blocking_draws = YES #ENDIF type KeyboardState setkeys_elapsed_ms as integer 'Time since last setkeys call (used by keyval) keybd(scLAST) as integer 'keyval array key_down_ms(scLAST) as integer 'ms each key has been down diagonalhack as integer = -1 '-1 before call to keyval w/ arrow key, afterwards 0 or 2 delayed_alt_keydown as bool = NO 'Whether have delayed reporting an ALT keypress keyrepeatwait as integer = 500 keyrepeatrate as integer = 55 inputtext as string end type dim shared real_kb as KeyboardState 'Always contains real keyboard state even if replaying dim shared replay_kb as KeyboardState 'Contains replayed state of keyboard while replaying, else unused dim shared last_setkeys_time as double 'Used to compute real_kb.setkeys_elapsed_ms dim shared inputtext_enabled as bool = NO 'Whether to fetch real_kb.inputtext, not applied to replay_kb #IFDEF USE_X11 'As a workaround for bug 2005, we disable native text input by default 'on X11 (Linux/BSD). This can be removed when we figure out a better fix for that bug dim shared disable_native_text_input as bool = YES #ELSE dim shared disable_native_text_input as bool = NO #ENDIF 'Singleton type type ReplayState active as bool 'Currently replaying input and not paused paused as bool 'While paused, keyval, etc, act on real_kb. filename as string 'Used only for error messages. file as integer = -1 'File handle tick as integer = -1 'Counts number of ticks we've replayed fpos as integer 'Debugging only: File offset of the tick chunk nexttick as integer = -1 'If we read the next tickcount from the file before it's needed 'it's stored here. Otherwise -1. next_tick_ms as integer = 55 'Next tick milliseconds read before it's needed. debug as bool = NO 'Set to YES by editing this line; maybe add a commandline option length_ticks as integer 'Length in ticks (max tick num) length_ms as integer 'Approximate length of the replay, in milliseconds play_position_ms as integer 'Approximate position in replay in ms (calculated in same way as length_ms) repeat_count as integer 'Number of times to repeat the playback repeats_done as integer 'Number of repeats already finished. end type 'Singleton type for recording input. type RecordState file as integer = -1 'File handle active as bool 'Currently recording input and not paused. paused as bool 'While paused, calls to setkeys don't affect recording. tick as integer = -1 'Tick number, starting from zero. debug as bool = NO 'Set to YES by editing this line; maybe add a commandline option last_kb as KeyboardState 'Keyboard state during previous recorded tick end type dim shared replay as ReplayState dim shared record as RecordState dim shared macrofile as string 'Singleton type for recording a .gif. type RecordGIFState 'active as bool writer as GifWriter fname as string last_frame_end_time as double 'Nominal time when the delay for the last frame we wrote ends declare property active() as bool declare function delay() as integer end type dim shared recordgif as RecordGIFState dim shared gif_max_fps as integer = 30 dim shared screenshot_record_overlays as bool = NO dim shared gif_show_keys as bool 'While recording a gif, whether to display pressed keys dim shared gif_show_mouse as bool 'While recording a gif, whether to display mouse location dim shared closerequest as bool = NO 'It has been requested to close the program. dim keybdmutex as any ptr '(Global) Controls access to keybdstate(), mouseflags, mouselastflags, various backend functions, 'and generally used to halt the polling thread. dim shared keybdthread as any ptr 'id of the polling thread dim shared endpollthread as bool 'signal the polling thread to quit dim shared keybdstate(scLAST) as integer '"real"time keyboard array (only used internally by pollingthread) dim shared mouseflags as integer dim shared mouselastflags as integer dim shared cursorvisibility as CursorVisibility = cursorDefault 'State of the mouse (set when setkeys is called), includes persistent state dim shared mouse_state as MouseInfo dim shared last_mouse_wheel as integer 'mouse_state.wheel at previous update_mouse_state call. dim shared textfg as integer dim shared textbg as integer dim shared intpal(0 to 255) as RGBcolor 'current palette dim shared updatepal as bool 'setpal called, load new palette at next setvispage dim shared fps_draw_frames as integer = 0 'Frames drawn since fps_time_start dim shared fps_real_frames as integer = 0 'Frames sent to gfx backend since fps_time_start dim shared fps_time_start as double = 0.0 dim shared draw_fps as double 'Current measured frame draw rate, per second dim shared real_fps as double 'Current measured frame display rate, per second dim shared overlay_showfps as integer = 0 'Draw on overlay? 0 (off), 1 (real fps), or 2 (draw fps) dim shared overlays_enabled as bool = YES 'Whether to draw overlays in general dim shared overlay_message as string 'Message to display on screen dim shared overlay_hide_time as double 'Time at which to hide it dim shared overlay_replay_display as bool MAKETYPE_DoubleList(SpriteCacheEntry) MAKETYPE_DListItem(SpriteCacheEntry) 'WARNING: don't add strings to this type SpriteCacheEntry 'cachelist used only if object is a member of sprcacheB cacheB as DListItem(SpriteCacheEntry) hashed as HashedItem p as Frame ptr cost as integer Bcached as bool end type CONST SPRITE_CACHE_MULT = 1000000 dim shared sprcache as HashTable dim shared sprcacheB as DoubleList(SpriteCacheEntry) dim shared sprcacheB_used as integer 'number of slots full 'dim shared as integer cachehit, cachemiss dim shared mouse_grab_requested as bool = NO dim shared mouse_grab_overridden as bool = NO dim shared remember_mouse_grab(3) as integer = {-1, -1, -1, -1} dim shared remember_title as string 'The window title dim shared global_sfx_volume as single = 1. '========================================================================================== ' Initialisation and shutdown '========================================================================================== ' Initialise anything in this module that's independent from the gfx backend private sub modex_init() redim vpages(3) 'redim fixedsize_vpages(3) 'Initially all NO vpagesp = @vpages(0) for i as integer = 0 to 3 vpages(i) = frame_new(320, 200, , YES) next 'other vpages slots are for temporary pages 'They are currently still used in the tileset editor, importbmp, titlescreenbrowse, 'and mapedit_linkdoors. 'Except for the first two, they're assumed to be the same size as pages 0/1. clippedframe = NULL hash_construct(sprcache, offsetof(SpriteCacheEntry, hashed)) dlist_construct(sprcacheB.generic, offsetof(SpriteCacheEntry, cacheB)) sprcacheB_used = 0 ' TODO: tmpdir is shared by all instances of Custom, but when that is fixed this can be removed macrofile = tmpdir & "macro" & get_process_id() & ".ohrkeys" end sub ' Initialise stuff specific to the backend (this is called after gfx_init()) private sub backend_init() 'Polling thread variables endpollthread = NO mouselastflags = 0 mouseflags = 0 keybdmutex = mutexcreate if wantpollingthread then debuginfo "Starting IO polling thread" keybdthread = threadcreate(@pollingthread) end if io_init() 'mouserect(-1,-1,-1,-1) fps_time_start = TIMER fps_draw_frames = 0 fps_real_frames = 0 if gfx_supports_variable_resolution() = NO then debuginfo "Resolution changing not supported" windowsize = XY(320, 200) 'In case we're called from switch_gfx, resize video pages screen_size_update end if end sub ' Initialise this module and backends, create a window sub setmodex() modex_init() 'Select and initialise a graphics/io backend init_preferred_gfx_backend() backend_init() modex_initialised = YES end sub ' Cleans up anything in this module which is independent of the graphics backend private sub modex_quit() stop_recording_input stop_recording_gif for i as integer = 0 to ubound(vpages) frame_unload(@vpages(i)) next for i as integer = 0 to ubound(fonts) font_unload(@fonts(i)) next hash_destruct(sprcache) 'debug "cachehit = " & cachehit & " mis == " & cachemiss releasestack safekill macrofile end sub ' Shuts down the gfx backend and cleans up everything that needs to be private sub backend_quit() 'clean up io stuff if keybdthread then endpollthread = YES threadwait keybdthread keybdthread = 0 end if mutexdestroy keybdmutex skipped_frame.drop() gfx_close() end sub ' Deinitialise this module and backends, destroy the window sub restoremode() if modex_initialised = NO then exit sub modex_initialised = NO backend_quit modex_quit end sub ' Switch to a different gfx backend sub switch_gfx(backendname as string) debuginfo "switch_gfx " & backendname backend_quit() switch_gfx_backend(backendname) backend_init() ' Re-apply settings (this is very incomplete) setwindowtitle remember_title io_setmousevisibility(cursorvisibility) end sub sub mersenne_twister (seed as double) if replay.active orelse replay.paused orelse record.active orelse record.paused then exit sub 'Seeding not allowed in play/record modes end if 'FIXME: reseeding the RNG from scripts needs be allowed. 'Either the seed should be recorded, or just don't allow any source of nondeterminism which could 'be used as a seed (e.g. record results of all nondeterministic script commands). RANDOMIZE seed, 3 debuginfo "mersenne_twister seed=" & seed end sub sub settemporarywindowtitle (title as string) 'just like setwindowtitle but does not memorize the title mutexlock keybdmutex gfx_windowtitle(title) mutexunlock keybdmutex end sub sub setwindowtitle (title as string) remember_title = title mutexlock keybdmutex gfx_windowtitle(title) mutexunlock keybdmutex end sub function allmodex_setoption(opt as string, arg as string) as integer if opt = "no-native-kbd" then disable_native_text_input = YES debuginfo "Native text input disabled" return 1 elseif opt = "native-kbd" then disable_native_text_input = NO debuginfo "Native text input enabled" return 1 elseif opt = "runfast" then debuginfo "Running without speed control" enable_speed_control NO return 1 elseif opt = "maxfps" then dim fps as integer = str2int(arg, -1) if fps > 0 then max_display_fps = fps return 2 else display_help_string "--maxfps: invalid fps" return 1 end if elseif opt = "giffps" then dim fps as integer = str2int(arg, -1) if fps > 0 then gif_max_fps = fps return 2 else display_help_string "--giffps: invalid fps" return 1 end if elseif opt = "recordoverlays" then screenshot_record_overlays = YES return 1 elseif opt = "hideoverlays" then overlays_enabled = NO return 1 elseif opt = "recordinput" then dim fname as string = absolute_with_orig_path(arg) if fileiswriteable(fname) then start_recording_input fname return 2 'arg used else display_help_string "input cannot be recorded to """ & fname & """ because the file is not writeable." & LINE_END return 1 end if elseif opt = "replayinput" then dim fname as string = absolute_with_orig_path(arg) if fileisreadable(fname) then start_replaying_input fname return 2 'arg used else display_help_string "input cannot be replayed from """ & fname & """ because the file is not readable." & LINE_END return 1 end if elseif opt = "showkeys" then gif_show_keys = YES return 1 elseif opt = "showmouse" then gif_show_mouse = YES return 1 end if end function '========================================================================================== ' Video pages '========================================================================================== ' Convert all videopages to 32 bit. Preserves their content sub switch_to_32bit_vpages () default_page_bitdepth = 32 for i as integer = 0 to ubound(vpages) if vpages(i) then frame_convert_to_32bit vpages(i), intpal() end if next end sub ' Convert all videopages to 8 bit Frames (not backed by Surfaces). ' WIPES their contents! sub switch_to_8bit_vpages () default_page_bitdepth = 8 for i as integer = 0 to ubound(vpages) if vpages(i) then 'frame_assign @vpages(i), frame_new(vpages(i)->w, vpages(i)->h) 'Safer to use this, as it keeps extra state like .noresize frame_drop_surface vpages(i) clearpage i end if next end sub sub freepage (page as integer) if page < 0 orelse page > ubound(vpages) orelse vpages(page) = NULL then debug "Tried to free unallocated/invalid page " & page exit sub end if frame_unload(@vpages(page)) end sub 'Adds a Frame ptr to vpages(), returning its index. function registerpage (spr as Frame ptr) as integer if spr->refcount <> NOREFC then spr->refcount += 1 for i as integer = 0 to ubound(vpages) if vpages(i) = NULL then vpages(i) = spr ' Mark as fixed size, so it won't be resized when the window resizes. 'fixedsize_vpages(i) = YES return i end if next redim preserve vpages(ubound(vpages) + 1) vpagesp = @vpages(0) vpages(ubound(vpages)) = spr 'redim preserve fixedsize_vpages(ubound(vpages) + 1) 'fixedsize_vpages(ubound(vpages)) = YES return ubound(vpages) end function 'Create a new video page and return its index. 'bitdepth: 8 for a regular Frame, 32 for a 32-bit Surface-backed page, or -1 to use the default 'Note: the page is filled with color 0, unlike clearpage, which defaults to uiBackground! function allocatepage(w as integer = -1, h as integer = -1, bitdepth as integer = -1) as integer if w < 0 then w = windowsize.w if h < 0 then h = windowsize.h if bitdepth < 0 then bitdepth = default_page_bitdepth if bitdepth <> 8 and bitdepth <> 32 then showerror "allocatepage: Bad bitdepth " & bitdepth end if dim fr as Frame ptr = frame_new(w, h, , YES, , bitdepth = 32) dim ret as integer = registerpage(fr) frame_unload(@fr) 'we're not hanging onto it, vpages() is return ret end function 'creates a copy of a page, registering it (must be freed) function duplicatepage (page as integer) as integer dim fr as Frame ptr = frame_duplicate(vpages(page)) dim ret as integer = registerpage(fr) frame_unload(@fr) 'we're not hanging onto it, vpages() is return ret end function 'Copy contents of one page onto another 'should copying to a page of different size resize that page? sub copypage (src as integer, dest as integer) 'if vpages(src)->w <> vpages(dest)->w or vpages(src)->h <> vpages(dest)->h then ' debug "warning, copied to page of unequal size" 'end if frame_draw vpages(src), , 0, 0, , NO, vpages(dest) end sub sub clearpage (page as integer, colour as integer = -1) if colour = -1 then colour = uilook(uiBackground) frame_clear vpages(page), colour end sub 'The contents are either trimmed or extended with colour uilook(uiBackground). sub resizepage (page as integer, w as integer, h as integer) if vpages(page) = NULL then showerror "resizepage called with null ptr" exit sub end if frame_assign @vpages(page), frame_resized(vpages(page), w, h, 0, 0, uilook(uiBackground)) end sub private function compatpage_internal(pageframe as Frame ptr) as Frame ptr return frame_new_view(vpages(vpage), (vpages(vpage)->w - 320) / 2, (vpages(vpage)->h - 200) / 2, 320, 200) end function 'Return a video page which is a view on vpage hat is 320x200 (or smaller) and centred. 'In order to use this, draw to the returned page, but call setvispage(vpage). 'Do not swap dpage and vpage! 'WARNING: if a menu using compatpage calls another one that does swap dpage and 'vpage, things will break 50% of the time! function compatpage() as integer dim fakepage as integer dim centreview as Frame ptr centreview = compatpage_internal(vpages(vpage)) fakepage = registerpage(centreview) frame_unload @centreview return fakepage end function '========================================================================================== ' Resolution changing '========================================================================================== 'First check if the window was resized by the user, 'then if windowsize has changed (possibly by a call to unlock_resolution/set_resolution) 'resize all videopages (except compatpages) to the new window size. 'The videopages are either trimmed or extended with colour 0. private sub screen_size_update () 'Changes windowsize if user tried to resize, otherwise does nothing if gfx_get_resize(windowsize) then 'debuginfo "User window resize to " & windowsize.w & "*" & windowsize.h show_overlay_message windowsize.w & " x " & windowsize.h, 0.7 end if 'Clamping windowsize to the minwinsize here means trying to override user 'resizes (specific to the case where the backend doesn't support giving the WM 'a min size hint). 'However unfortunately gfx_sdl can't reliably override it, at least with X11+KDE, 'because the window size can't be changed while the user is still dragging the window 'frame. 'So just accept whatever the backend says the actual window size is. 'windowsize.w = large(windowsize.w, minwinsize.w) 'windowsize.h = large(windowsize.h, minwinsize.h) dim oldvpages(ubound(vpages)) as Frame ptr for page as integer = 0 to ubound(vpages) oldvpages(page) = vpages(page) next 'oldvpages pointers will be invalidated 'Resize dpage and vpage (I think it's better to hardcode 0 & 1 rather 'than using dpage and vpage variables in case the later are temporarily changed) 'Update size of all real pages. I think it's better to do so to all pages rather 'than just page 0 and 1, as other pages are generally used as 'holdpages'. 'The alternative is to update all menus using holdpages to clear the screen 'before copying the holdpage over. 'All pages which are not meant to be the same size as the screen 'currently don't persist to the next frame. for page as integer = 0 to ubound(vpages) dim vp as Frame ptr = vpages(page) if vp andalso vp->isview = NO andalso vp->noresize = NO then if vp->w <> windowsize.w or vp->h <> windowsize.h then 'debug "screen_size_update: resizing page " & page & " -> " & windowsize.w & "*" & windowsize.h resizepage page, windowsize.w, windowsize.h end if end if next 'Scan for compatpages (we're assuming all views are compatpages, which isn't true in 'general, but currently true when setvispage is called) and replace each with a new view 'onto the center of the same page if it changed. for page as integer = 0 to ubound(vpages) if vpages(page) andalso vpages(page)->isview then for page2 as integer = 0 to ubound(oldvpages) if vpages(page)->base = oldvpages(page2) and vpages(page2) <> oldvpages(page2) then 'debug "screen_size_update: updating view page " & page & " to new compatpage onto " & page2 frame_unload @vpages(page) vpages(page) = compatpage_internal(vpages(page2)) exit for end if next 'If no match found, do nothing end if next 'Update the size of the Screen slice. 'This removes the need to call UpdateScreenSlice in all menus, but you can 'still call it to find out if the size changed. UpdateScreenSlice NO 'clear_changed_flag=NO end sub 'Set the size of a video page and keep it from being resized as the window size changes. 'TODO: delete this after the tile editor and importbmp stop using video pages 2 and 3 sub lock_page_size(page as integer, w as integer, h as integer) resizepage page, w, h vpages(page)->noresize = 1 end sub 'Revert a video page to following the size of the window 'TODO: delete this after the tile editor and importbmp stop using video pages 2 and 3 sub unlock_page_size(page as integer) resizepage page, windowsize.w, windowsize.h vpages(page)->noresize = 0 end sub 'Makes the window resizeable, and sets a minimum size. 'Whenever the window is resized all videopages (except compatpages) are resized to match. sub unlock_resolution (min_w as integer, min_h as integer) minwinsize.w = min_w minwinsize.h = min_h if gfx_supports_variable_resolution() = NO then exit sub end if debuginfo "unlock_resolution(" & min_w & "," & min_h & ")" resizing_enabled = gfx_set_resizable(YES, minwinsize.w, minwinsize.h) windowsize.w = large(windowsize.w, minwinsize.w) windowsize.h = large(windowsize.h, minwinsize.h) screen_size_update 'Update page size end sub 'Disable window resizing. sub lock_resolution () debuginfo "lock_resolution()" resizing_enabled = gfx_set_resizable(NO, 0, 0) end sub function resolution_unlocked () as bool return resizing_enabled end function 'Set the window size, if possible, subject to min size bound. Doesn't modify resizability state. 'This will resize all videopages (except compatpages) to the new window size. sub set_resolution (w as integer, h as integer) if gfx_supports_variable_resolution() = NO then exit sub end if debuginfo "set_resolution " & w & "*" & h windowsize.w = large(w, minwinsize.w) windowsize.h = large(h, minwinsize.h) 'Update page size screen_size_update 'Tell the gfx backend about the new page size. If we delayed this then a following 'call to set_scale_factor would change scale and recenter window using wrong window size, 'requiring manual recenter. 'TODO: not ideal, should tell backend about size and scale at same time. setvispage vpage, NO end sub 'The current internal window size in pixels (actual window updated at next setvispage) function get_resolution() as XYPair return windowsize end function 'Get resolution of the (primary) monitor. On Windows, this excludes size of the taskbar. sub get_screen_size (byref screenwidth as integer, byref screenheight as integer) 'Prefer os_get_screen_size because on windows it excludes the taskbar, 'and gfx_sdl reports resolution at init time rather than the current values. os_get_screen_size(@screenwidth, @screenheight) if screenwidth <= 0 or screenheight <= 0 then debuginfo "Falling back to gfx_get_screen_size" gfx_get_screen_size(@screenwidth, @screenheight) end if debuginfo "Desktop resolution: " & screenwidth & "*" & screenheight end sub 'Set the size that a pixel appears on the screen. 'Supported by all backends except gfx_alleg. sub set_scale_factor (scale as integer, change_windowsize as bool = YES) 'gfx_sdl and gfx_fb, which use blit.c scaling, are limited to 1x-16x scale = bound(scale, 1, 16) debuginfo "Setting graphics scaling to x" & scale & " change_windowsize=" & change_windowsize if change_windowsize = NO then ' Only supported by gfx_sdl currently if gfx_setoption("zoomonly", str(scale)) then exit sub end if if gfx_setoption("zoom", str(scale)) = 0 then ' Old versions of gfx_directx don't support zoom (TODO: delete this) gfx_setoption("width", str(windowsize.w * scale)) gfx_setoption("height", str(windowsize.h * scale)) end if end sub 'Returns true if successfully queries the fullscreen state, in which case 'fullscreen' is set. '(Note: gfx_fb doesn't know for certain whether it's fullscreen; can't catch alt+enter. function try_check_fullscreen(byref fullscreen as bool) as bool dim winstate as WindowState ptr = gfx_getwindowstate() if winstate andalso winstate->structsize >= 4 then fullscreen = winstate->fullscreen return YES end if return NO end function function supports_fullscreen_well () as bool 'Return YES if we should show the fullscreen/windowed menu options 'and obey a game's fullscreen/windowed setting. 'Note: even if this returns false, you can still try to fullscreen using alt-tab 'or the --fullscreen arg and it might be supported. if running_on_desktop() = NO then return NO end if #IFDEF __GNU_LINUX__ ' At least for me with KDE 4, fbgfx gives horrible results, ' turning off my 2nd monitor and lots of garbage and desktop resolution changing, ' and sometimes gets stuck with a fullscreen black screen. ' SDL 1.2 does something milder (causing the 2nd monitor to switch to mirrored) ' but only when the window size is smaller than the desktop. ' So probably the solution in gfx_sdl is to set the requested resolution to ' be equal to the desktop resolution and add black bars. if gfxbackend = "fb" then return NO end if #ENDIF return YES end function '========================================================================================== ' setvispage and Fading '========================================================================================== declare sub present_internal_frame(drawpage as integer) declare sub present_internal_surface(drawpage as integer) sub SkippedFrame.drop() 'if page >= 0 then freepage page page = -1 end sub ' If the last setvispage was skipped, display it sub SkippedFrame.show () ' Note: setvispage will call SkippedFrame.drop() after displaying the page if page > -1 then setvispage page, NO end if end sub ' The last/currently displayed videopage (or a substitute: guaranteed to be valid) function getvispage() as integer if last_setvispage >= 0 andalso last_setvispage <= ubound(vpages) _ andalso vpages(last_setvispage) then return last_setvispage end if return vpage end function 'Display a videopage. May modify the page! 'Also resizes all videopages to match the window size 'skippable: if true, allowed to frameskip this frame at high framerates 'preserve_page: if true, don't modify page sub setvispage (page as integer, skippable as bool = YES, preserve_page as bool = NO) ' Remember last page last_setvispage = page ' Drop frames to reduce CPU usage if FPS too high if skippable andalso timer - lastframe < 1. / max_display_fps then skipped_frame.drop() skipped_frame.page = page ' To be really cautious we could save a copy, but because page should ' not get modified until it's time to draw the next frame, this isn't really needed. 'skipped_frame.page = duplicatepage(page) update_fps_counter YES exit sub end if update_fps_counter NO if not time_draw_calls_from_finish then lastframe = timer end if dim starttime as double = timer if gfx_supports_variable_resolution() = NO then 'Safety check. We must stick to 320x200, otherwise the backend could crash. 'In future backends should be updated to accept other sizes even if they only support 320x200 '(Actually gfx_directx appears to accept other sizes, but I can't test) if vpages(page)->w <> 320 or vpages(page)->h <> 200 then resizepage page, 320, 200 showerror "setvispage: page was not 320x200 even though gfx backend forbade it" end if end if ' The page to which to draw overlays, and display dim drawpage as integer = page 'We avoid duplicating the page to allow really high fps, but that leads to 'accidentally including overlays in gifs a lot, due to "copypage vpage, holdscreen" if preserve_page or recordgif.active then drawpage = duplicatepage(page) end if 'Draw those overlays that are always recorded in .gifs/screenshots draw_allmodex_recordable_overlays drawpage if screenshot_record_overlays = YES then 'Modifies page. This is bad if displaying a page other than vpage/dpage! draw_allmodex_overlays drawpage end if 'F12 for screenshots handled here (uses real_keyval) snapshot_check gif_record_frame vpages(drawpage), intpal() if screenshot_record_overlays = NO then draw_allmodex_overlays drawpage end if 'the fb backend may freeze up if it collides with the polling thread mutexlock keybdmutex starttime += timer 'Stop timer dim starttime2 as double = timer if vpages(page)->surf then present_internal_surface drawpage else present_internal_frame drawpage end if ' This gets triggered a lot under Win XP because the program freezes while moving ' the window (in all backends, although in gfx_fb it freezes readmouse instead) debug_if_slow(starttime2, 0.05, "gfx_present") starttime -= timer 'Restart timer mutexunlock keybdmutex if preserve_page then freepage drawpage end if if time_draw_calls_from_finish then ' Have to give the backend and driver a millisecond or two to display the frame or we'll miss it lastframe = timer - 0.004 end if skipped_frame.drop() 'Delay dropping old frame; skipped_frame.show() might have called us 'After presenting the page this is a good time to check for window size changes and 'resize the videopages as needed before the next frame is rendered. screen_size_update debug_if_slow(starttime, 0.05, "") end sub 'setvispage internal function for presenting a regular Frame page on the screen private sub present_internal_frame(drawpage as integer) dim surf as Surface ptr if gfx_surfaceCreateFrameView(vpages(drawpage), @surf) then return dim surface_pal as RGBPalette ptr if surf->format = SF_8bit then ' Need to provide a palette gfx_paletteFromRGB(@intpal(0), @surface_pal) end if gfx_present(surf, surface_pal) updatepal = NO 'We just did gfx_paletteDestroy(@surface_pal) gfx_surfaceDestroy(@surf) end sub 'setvispage internal function for presenting a Surface-backed page on the screen private sub present_internal_surface(drawpage as integer) dim drawsurf as Surface ptr = vpages(drawpage)->surf dim surface_pal as RGBPalette ptr if drawsurf->format = SF_8bit then ' Need to provide a palette gfx_paletteFromRGB(@intpal(0), @surface_pal) end if gfx_present(drawsurf, surface_pal) updatepal = NO 'We just did gfx_paletteDestroy(@surface_pal) end sub ' Change the palette at the NEXT setvispage call (or before next screen fade). sub setpal(pal() as RGBcolor) memcpy(@intpal(0), @pal(0), 256 * SIZEOF(RGBcolor)) updatepal = YES end sub ' A gfx_setpal wrapper which may perform frameskipping to limit fps private sub maybe_do_gfx_setpal() updatepal = YES if timer - lastframe < 1. / max_display_fps then update_fps_counter YES exit sub end if update_fps_counter NO if not time_draw_calls_from_finish then lastframe = timer end if mutexlock keybdmutex gfx_setpal(@intpal(0)) mutexunlock keybdmutex updatepal = NO if time_draw_calls_from_finish then ' Have to give the backend and driver a millisecond or two to display the frame or we'll miss it lastframe = timer - 0.004 end if end sub sub fadeto (red as integer, green as integer, blue as integer) dim i as integer dim j as integer dim diff as integer skipped_frame.show() 'If we frame-skipped last frame, better show it if updatepal then maybe_do_gfx_setpal gif_record_frame vpages(getvispage()), intpal() end if for i = 1 to 32 setwait 16.67 ' aim to complete fade in 550ms for j = 0 to 255 'red diff = intpal(j).r - red if diff > 0 then intpal(j).r -= iif(diff >= 8, 8, diff) elseif diff < 0 then intpal(j).r -= iif(diff <= -8, -8, diff) end if 'green diff = intpal(j).g - green if diff > 0 then intpal(j).g -= iif(diff >= 8, 8, diff) elseif diff < 0 then intpal(j).g -= iif(diff <= -8, -8, diff) end if 'blue diff = intpal(j).b - blue if diff > 0 then intpal(j).b -= iif(diff >= 8, 8, diff) elseif diff < 0 then intpal(j).b -= iif(diff <= -8, -8, diff) end if next maybe_do_gfx_setpal if i mod 3 = 0 then ' We're assuming that the page hasn't been modified since the last setvispage gif_record_frame vpages(getvispage()), intpal() end if dowait next 'Make sure the palette gets set on the final pass 'This function was probably called in the middle of timed loop, call 'setwait to avoid "dowait without setwait" warnings setwait 0 end sub sub fadetopal (pal() as RGBcolor) dim i as integer dim j as integer dim diff as integer skipped_frame.show() 'If we frame-skipped last frame, better show it if updatepal then maybe_do_gfx_setpal gif_record_frame vpages(getvispage()), intpal() end if for i = 1 to 32 setwait 16.67 ' aim to complete fade in 550ms for j = 0 to 255 'red diff = intpal(j).r - pal(j).r if diff > 0 then intpal(j).r -= iif(diff >= 8, 8, diff) elseif diff < 0 then intpal(j).r -= iif(diff <= -8, -8, diff) end if 'green diff = intpal(j).g - pal(j).g if diff > 0 then intpal(j).g -= iif(diff >= 8, 8, diff) elseif diff < 0 then intpal(j).g -= iif(diff <= -8, -8, diff) end if 'blue diff = intpal(j).b - pal(j).b if diff > 0 then intpal(j).b -= iif(diff >= 8, 8, diff) elseif diff < 0 then intpal(j).b -= iif(diff <= -8, -8, diff) end if next if i mod 3 = 0 then ' We're assuming that the page hasn't been modified since the last setvispage gif_record_frame vpages(getvispage()), intpal() end if maybe_do_gfx_setpal dowait next 'This function was probably called in the middle of timed loop, call 'setwait to avoid "dowait without setwait" warnings setwait 0 end sub '========================================================================================== ' Waits/Framerate '========================================================================================== sub enable_speed_control(setting as bool = YES) use_speed_control = setting end sub 'Decides whether to time when to display the next frame (deciding whether to skip 'a frame or not) based on when the last gfx_present returned instead of when it 'was called. 'Normally we should just try to display a frame every refresh-interval, but on OSX 'presenting the window blocks until vsync, which means if we time from the call 'time rather than the return time, then we'll always be unnecessarily waiting 'even if speedcontrol is disabled. (If we're not trying to go fast then this waiting 'is OK, because it only happens if we displayed a frame earlier than necessary.) 'So this is only useful if we are frame skipping to run at more than the refresh rate! private function time_draw_calls_from_finish() as bool if blocking_draws = NO then ' Normally this is undesirable return NO else ' Otherwise, only turn this on if we're trying to go FAST ' (But allow for 16 ms = 62.5fps) return (use_speed_control = NO or requested_framerate > max_display_fps + 3) end if end function 'Set number of milliseconds from now when the next call to dowait returns. 'This number is treated as a desired framewait, so actual target wait varies from 0.5-1.5x requested. 'ms: number of milliseconds 'flagms: if nonzero, is a count in milliseconds for the secondary timer, whether this has triggered ' is accessed as the return value from dowait. sub setwait (ms as double, flagms as double = 0) if use_speed_control = NO then ms = 0.001 ms /= fps_multiplier 'flagms /= fps_multiplier requested_framerate = 1. / ms dim thetime as double = timer dim target as double = waittime + ms / 1000 waittime = bound(target, thetime + 0.5 * ms / 1000, thetime + 1.5 * ms / 1000) if flagms <= 0 then flagms = ms end if if thetime > flagtime then flagtime = bound(flagtime + flagms / 1000, thetime + 0.0165, thetime + 1.5 * flagms / 1000) end if setwait_called = YES end sub ' Returns number of dowait calls function get_tickcount() as integer return tickcount end function function dowait () as bool 'wait until alarm time set in setwait() 'returns true if the flag time has passed (since the last time it was passed) 'In freebasic, sleep is in 1000ths, and a value of less than 100 will not 'be exited by a keypress, so sleep for 5ms until timer > waittime. tickcount += 1 global_tog XOR= 1 dim i as integer dim starttime as double = timer do while timer <= waittime - 0.0005 i = bound((waittime - timer) * 1000, 1, 5) sleep i io_waitprocessing() loop ' dowait might be called after waittime has already passed, ignore that ' (the time printed is the unwanted delay). ' On Windows FB sleep calls winapi Sleep(), which has a default of 15.6ms, adjustable ' with timeBeginPeriod(). 15.6ms is very coarse for 60fps games, so we probably ' should request a higher frequency. (Also, Win XP rounds the sleep period up to the ' following tick, while Win 7+ rounds it down, although that probably makes no ' difference due to the avoid while loop. See ' https://randomascii.wordpress.com/2013/04/02/sleep-variation-investigated/ ' If there's a long delay here it's because the system is busy; not interesting. debug_if_slow(large(starttime, waittime), 0.2, "") if setwait_called then setwait_called = NO else debug "dowait called without setwait" end if return timer >= flagtime end function '========================================================================================== ' Music '========================================================================================== sub setupmusic music_init sound_init musicbackendinfo = music_get_info debuginfo musicbackendinfo end sub sub closemusic () music_close sound_close end sub sub resetsfx () ' Stops playback and unloads cached sound effects sound_reset end sub sub loadsong (songname as string) music_play(songname, getmusictype(songname)) end sub 'Doesn't work in SDL_mixer for MIDI music, so avoid 'sub pausesong () ' music_pause() 'end sub ' 'sub resumesong () ' music_resume 'end sub function get_music_volume () as single return music_getvolume end function sub set_music_volume (vol as single) music_setvolume(vol) end sub '========================================================================================== ' Sound effects '========================================================================================== ' loopcount N to play N+1 times, -1 to loop forever ' See set_sfx_volume for description of volume_mult. sub playsfx (num as integer, loopcount as integer = 0, volume_mult as single = 1.) dim slot as integer ' If already loaded can reuse without reloading. ' TODO: However this preempts it if still playing; shouldn't force that ' NOTE: backends vary, music_sdl does nothing if too many sfx playing, ' music_audiere has no limit. slot = sound_slot_with_id(num) if slot = -1 then slot = sound_load(find_sfx_lump(num), num) if slot = -1 then exit sub end if 'debug "playsfx volume_mult=" & volume_mult & " global_sfx_volume " & global_sfx_volume sound_play(slot, loopcount, volume_mult * global_sfx_volume) IF_PTR(sound_slotdata(slot))->original_volume = volume_mult end sub sub stopsfx (num as integer) dim slot as integer slot = sound_slot_with_id(num) if slot = -1 then exit sub sound_stop(slot) end sub sub pausesfx (num as integer) dim slot as integer slot = sound_slot_with_id(num) if slot = -1 then exit sub sound_pause(slot) end sub ' This returns the actual effective sfx volume 0. - 1., combining all volume ' settings and any fade effects the backend might be doing (nothing like ' that is implemented yet). function effective_sfx_volume (num as integer) as single dim slot as integer slot = sound_slot_with_id(num) if slot = -1 then return 0. return sound_getvolume(slot) end function /' Is this needed? function get_sfx_volume (num as integer) as single dim slot as integer slot = sound_slot_with_id(num) if slot = -1 then return 0. return sound_getslot(slot)->original_volume end function '/ ' Set the volume of a sfx to some multiple of its default volume, ' which is the global sfx volume * the volume adjustment defined in Custom sub set_sfx_volume (num as integer, volume_mult as single) dim slot as integer slot = sound_slot_with_id(num) if slot = -1 then exit sub sound_setvolume(slot, volume_mult * global_sfx_volume) IF_PTR(sound_slotdata(slot))->original_volume = volume_mult end sub ' Set the global volume multiplier for sound effects. ' The backends only support a max volume of 1.0, ' but the global volume can be set higher, amplifying ' any sfx with a volume less than 1.0. sub set_global_sfx_volume (volume as single) global_sfx_volume = volume ' Update all SFX for slot as integer = 0 to sound_lastslot() dim slotdata as SFXCommonData ptr slotdata = sound_slotdata(slot) if slotdata = 0 then continue for 'debug "set_global_sfx_volume: refresh volume for " _ ' & slotdata->effectID & " to " & (slotdata->original_volume * global_sfx_volume) sound_setvolume slot, slotdata->original_volume * global_sfx_volume next end sub function get_global_sfx_volume () as single return global_sfx_volume end function ' Only used by Custom's importing interface sub freesfx (num as integer) sound_free(num) end sub function sfxisplaying(num as integer) as bool dim slot as integer slot = sound_slot_with_id(num) if slot = -1 then return NO return sound_playing(slot) end function '========================================================================================== ' Keyboard input '========================================================================================== function real_keyval(a as integer, repeat_wait as integer = 0, repeat_rate as integer = 0) as integer return keyval(a, repeat_wait, repeat_rate, YES) end function function keyval (a as integer, repeat_wait as integer = 0, repeat_rate as integer = 0, real_keys as bool = NO) as integer 'except for special keys (like -1), each key reports 3 bits: ' 'bit 0: key was down at the last setkeys call 'bit 1: keypress event (either new keypress, or key-repeat) during last setkey-setkey interval 'bit 2: new keypress during last setkey-setkey interval ' 'Note: Alt/Ctrl keys may behave strangely with gfx_fb (and old gfx_directx): 'You won't see Left/Right keypresses even when scAlt/scCtrl is pressed, so do not 'check "keyval(scLeftAlt) > 0 or keyval(scRightAlt) > 0" instead of "keyval(scAlt) > 0" dim kbstate as KeyboardState ptr if replay.active andalso real_keys = NO then kbstate = @replay_kb else kbstate = @real_kb end if dim result as integer = kbstate->keybd(a) if a >= 0 then if repeat_wait = 0 then repeat_wait = kbstate->keyrepeatwait if repeat_rate = 0 then repeat_rate = kbstate->keyrepeatrate 'awful hack to avoid arrow keys firing alternatively when not pressed at the same time: 'save state of the first arrow key you query dim arrowkey as bool = NO if a = scLeft or a = scRight or a = scUp or a = scDown then arrowkey = YES if arrowkey and kbstate->diagonalhack <> -1 then return (result and 5) or (kbstate->diagonalhack and result > 0) if kbstate->key_down_ms(a) >= repeat_wait then dim check_repeat as bool = YES 'if a = scAlt then 'alt can repeat (probably a bad idea not to), but only if nothing else has been pressed 'for i as integer = 1 to scLAST ' if kbstate->keybd(i) > 1 then check_repeat = NO 'next 'if delayed_alt_keydown = NO then check_repeat = NO 'end if 'Don't fire repeat presses for special toggle keys (note: these aren't actually 'toggle keys in all backends, eg. gfx_fb) if a = scNumlock or a = scCapslock or a = scScrolllock then check_repeat = NO if check_repeat then 'Keypress event at "wait + i * rate" ms after keydown dim temp as integer = kbstate->key_down_ms(a) - repeat_wait if temp \ repeat_rate > (temp - kbstate->setkeys_elapsed_ms) \ repeat_rate then result or= 2 end if if arrowkey then kbstate->diagonalhack = result and 2 end if end if return result end function sub setkeyrepeat (repeat_wait as integer = 500, repeat_rate as integer = 55) if replay.active then replay_kb.keyrepeatwait = repeat_wait replay_kb.keyrepeatrate = repeat_rate else real_kb.keyrepeatwait = repeat_wait real_kb.keyrepeatrate = repeat_rate end if end sub ' Get text input by assuming a US keyboard layout and reading scancodes rather than using the io backend. ' Also supports alt- combinations for the high 128 characters ' Always returns real input, even if replaying input. function get_ascii_inputtext () as string dim shift as integer = 0 dim ret as string if real_keyval(scCtrl) > 0 then return "" if real_keyval(scShift) and 1 then shift += 1 if real_keyval(scAlt) and 1 then shift += 2 'for characters 128 and up for i as integer = 0 to 53 dim effective_shift as integer = shift if shift <= 1 andalso real_keyval(scCapsLock) > 0 then select case i case scQ to scP, scA to scL, scZ to scM effective_shift xor= 1 end select end if if real_keyval(i) > 1 then ret &= key2text(effective_shift, i) end if next i ' A few keys missing from key2text if real_keyval(scSpace) > 1 then ret &= " " if real_keyval(scNumpadAsterisk) > 1 then ret &= "*" if real_keyval(scNumpadMinus) > 1 then ret &= "-" if real_keyval(scNumpadPlus) > 1 then ret &= "+" ' (Bug: gfx_fb reports both scSlash and scNumpadSlash) if gfxbackend <> "fb" and real_keyval(scNumpadSlash) > 1 then ret &= "/" ' Numpad is missing from key2text ' (Bug: gfx_fb on Windows never reports scNumpad5 at all!) for i as integer = 0 to ubound(numpad2text) if real_keyval(scNumpad7 + i) > 1 then ret &= numpad2text(i) end if next ' Note, we ignore numlock/shift, because backends/OSes differ on when ' they report text input from numpad keys anyway: ' X11 (both FB and SDL): when numlock XOR shift is pressed ' Windows (both FB and SDL): only when numlock on and shift not pressed ' gfx_directx: when numlock is on ' (Also, on Windows, status of numlock is buggy: for gfx_sdl and gfx_directx, ' after user turns it off, state doesn't update until next keypress, ' while gfx_fb doesn't report it at all) return ret end function ' Returns text input from the backend since the last call. ' Always returns real input, even if replaying input. private function read_inputtext () as string if disable_native_text_input then return get_ascii_inputtext() end if 'AFAIK, this is will still work on all platforms except X11 with SDL 'even if inputtext was not enabled; however you'll get a warning when 'getinputtext is called. dim w_in as wstring * 64 if io_textinput then io_textinput(w_in, 64) 'OK, so here's the hack: one of the alt keys (could be either) might be used 'as a 'shift' or compose key, but if it's not, we want to support the old 'method of entering extended characters (128 and up) using it. This will 'backfire if the key face/base characters aren't ASCII dim force_native_input as bool = NO for i as integer = 0 to len(w_in) - 1 if w_in[i] > 127 then force_native_input = YES next if force_native_input = NO andalso real_keyval(scAlt) and 1 then 'Throw away w_in return get_ascii_inputtext() end if 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 if io_textinput then 'if len(w_in) then print #fh, "input :" & w_in ' Now we need to convert from unicode to the game's character set (7-bit ascii or Latin-1) dim ret as string = "" dim force_shift as bool = NO for i as integer = 0 to len(w_in) - 1 if w_in[i] > 255 then select case w_in[i] case &hF700 to &hF746: 'Ignore Mac unicode for arrow keys, pgup+pgdown, ' delete, misc other keys. I don't know if the ' upper bound of &hF746 is high enough, but it ' blocks all the keys I could find on my keyboard. ' --James continue for case 304: 'Ignore COMBINING MACRON on most platforms, but 'use it to shift the next char on Android #IFDEF __FB_ANDROID__ force_shift = YES #ENDIF continue for end select 'debug "unicode char " & w_in[i] ret += "?" elseif w_in[i] = 127 then 'Delete (only sent on OSX). Ignore; we use scancodes instead. elseif w_in[i] >= icons_low and w_in[i] <= icons_high then ret += "?" elseif w_in[i] < 32 then 'Control character. What a waste of 8-bit code-space! 'Note that we ignore newlines... because we've always done it that way else dim ch as string = chr(w_in[i]) if force_shift then force_shift = NO ch = UCASE(ch) select case ch 'FIXME: it would be better to loop through the key2text array 'here, but it fails to initialize on Android case "1": ch = "!" case "2": ch = "@" case "3": ch = "#" case "4": ch = "$" case "5": ch = "%" case "6": ch = "^" case "7": ch = "&" case "8": ch = "*" case "9": ch = "(" case "0": ch = ")" case "-": ch = "_" case "=": ch = "+" case "[": ch = "{" case "]": ch = "}" case ";": ch = ":" case "'": ch = """" case "`": ch = "~" case "\": ch = "|" case ",": ch = "<" case ".": ch = ">" case "/": ch = "?" end select end if ret += ch end if next return ret else return get_ascii_inputtext() end if end function 'If using gfx_sdl and gfx_directx this is Latin-1, while gfx_fb doesn't currently support even that function getinputtext () as string if replay.active then return replay_kb.inputtext end if if disable_native_text_input = NO then 'Only show this message if getinputtext is called incorrectly twice in a row, 'to filter out instances when a menu with inputtext disabled exits back to 'one that expects it enabled, and getinputtext is called before the next call to setkeys. static last_call_was_bad as bool = NO if inputtext_enabled = NO and last_call_was_bad then debuginfo "getinputtext: not enabled" end if last_call_was_bad = (inputtext_enabled = NO) end if return real_kb.inputtext end function 'Checks the keyboard and optionally joystick for keypress events. 'trigger_level: 0 to trigger on a held key, ' 1 to trigger only on new keypress or repeat. 'Returns scancode if one is found, 0 otherwise. 'Use this instead of looping over all keys, to make sure alt filtering and joysticks work function anykeypressed (checkjoystick as bool = YES, checkmouse as bool = YES, trigger_level as integer = 1) as integer dim as integer joybutton, joyx, joyy for i as integer = 0 to scLAST 'check scAlt only, so Alt-filtering (see setkeys) works if i = scLeftAlt or i = scRightAlt or i = scUnfilteredAlt then continue for ' Ignore capslock and numlock because they always appear pressed when on, ' and it doesn't really matter if they doesn't work for 'press a key' prompts. ' To be on the safe said, ignore scroll lock too. Though with gfx_sdl, ' on Windows howing down scrolllock causes SDL to report key_up/key_down ' wait every tick, while on linux it seems to behave like a normal key if i = scCapsLock or i = scNumLock or i = scScrollLock then continue for if keyval(i) > trigger_level then return i end if next if checkjoystick then dim starttime as double = timer if io_readjoysane(0, joybutton, joyx, joyy) then for i as integer = 16 to 1 step -1 if joybutton and (i ^ 2) then return (scJoyButton1 - 1) + i next i end if debug_if_slow(starttime, 0.01, "io_readjoysane") end if if checkmouse then dim bitvec as integer = iif(trigger_level >= 1, mouse_state.release, mouse_state.buttons) for button as integer = 0 to 15 if bitvec and (1 shl button) then return scMouseLeft + button end if next button end if end function 'Waits for a new keyboard key, mouse or joystick button press. Returns the scancode function waitforanykey () as integer dim as integer key, sleepjoy = 3 dim remem_speed_control as bool = use_speed_control use_speed_control = YES skipped_frame.show() 'If we frame-skipped last frame, better show it setkeys do setwait 60, 200 io_pollkeyevents() setkeys key = anykeypressed(sleepjoy = 0, YES, 3) 'New keypresses only if key then snapshot_check 'In case F12 pressed, otherwise it wouldn't work setkeys 'Clear the keypress use_speed_control = remem_speed_control return key end if if sleepjoy > 0 then sleepjoy -= 1 end if if dowait then ' Redraw the screen occasionally in case something like an overlay is drawn setvispage getvispage, , YES 'Preserve contents end if loop end function 'Wait for all keys, and joystick and mouse buttons to be released sub waitforkeyrelease () setkeys 'anykeypressed checks scAlt instead of scUnfilteredAlt while anykeypressed(YES, YES, 0) or keyval(scUnfilteredAlt) if getquitflag() then exit sub io_pollkeyevents() setwait 15 setkeys dowait wend end sub 'Without changing the results of keyval or readmouse, check whether a key has been pressed, 'mouse button clicked, or window close requested since the last call to setkeys. 'NOTE: any such keypresses or mouse clicks are lost! This is OK for the current purposes 'NOTE: This checks the real keyboard state while replaying input. function interrupting_keypress () as bool dim starttime as double = timer dim ret as bool = NO io_pollkeyevents() dim keybd_dummy(scLAST) as integer dim mouse as MouseInfo mutexlock keybdmutex io_keybits(@keybd_dummy(0)) io_mousebits(mouse.x, mouse.y, mouse.wheel, mouse.buttons, mouse.clicks) mutexunlock keybdmutex debug_if_slow(starttime, 0.005, "") ' Check for attempt to quit program if keybd_dummy(scPageup) > 0 and keybd_dummy(scPagedown) > 0 and keybd_dummy(scEsc) > 1 then closerequest = YES if closerequest then #ifdef IS_GAME exit_gracefully() #else ret = YES #endif end if for i as integer = 0 to scLAST 'Check for new keypresses if keybd_dummy(i) and 2 then ret = YES next if mouse.clicks then ret = YES if ret then 'Crap, this is going to desync the replay since the result of interrupting_keypress isn't recorded '(No problem if paused) if record.active then stop_recording_input "Recording ended by interrupting keypress" end if if replay.active then stop_replaying_input "Replay ended by interrupting keypress" end if end if return ret end function 'Poll io backend to update key state bits, and then handle all special scancodes. 'keybd() should be dimmed at least (0 to scLAST) sub setkeys_update_keybd (keybd() as integer, byref delayed_alt_keydown as bool) dim winstate as WindowState ptr winstate = gfx_getwindowstate() mutexlock keybdmutex io_keybits(@keybd(0)) mutexunlock keybdmutex 'State of keybd(0 to scLAST) at this point: 'bit 0: key currently down 'bit 1: key down since last io_keybits call 'bit 2: zero 'debug "raw scEnter = " & keybd(scEnter) & " scAlt = " & keybd(scAlt) 'DELETEME (after a lag period): This is a temporary fix for gfx_directx not knowing about scShift '(or any other of the new scancodes, but none of the rest matter much (maybe 'scPause) since there are no games that use them). '(Ignore bit 2, because that isn't set yet) if ((keybd(scLeftShift) or keybd(scRightShift)) and 3) <> (keybd(scShift) and 3) then keybd(scShift) = keybd(scLeftShift) or keybd(scRightShift) end if keybd(scAnyEnter) = keybd(scEnter) or keybd(scNumpadEnter) 'Backends don't know about scAlt, only scUnfilteredAlt keybd(scAlt) = keybd(scUnfilteredAlt) 'Don't fire ctrl presses when alt down due to large number of WM shortcuts containing ctrl+alt '(Testing delayed_alt_keydown is just a hack to add one tick delay after alt up, 'which is absolutely required) if (keybd(scAlt) and 1) or delayed_alt_keydown then if keybd(scEnter) and 6 then keybd(scEnter) and= 1 delayed_alt_keydown = NO end if keybd(scCtrl) and= 1 keybd(scLeftCtrl) and= 1 keybd(scRightCtrl) and= 1 end if 'Calculate new "new keypress" bit (bit 2) for a as integer = 0 to scLAST keybd(a) and= 3 if a = scAlt then 'Special behaviour for alt, to ignore pesky WM shortcuts like alt+tab, alt+enter: 'Wait until alt has been released, without losing focus, before 'causing a key-down event. 'Also, special case for alt+enter, since that doesn't remove focus 'Note: this is only for scAlt, not scLeftAlt, scRightAlt, which aren't used by 'the engine, only by games. Maybe those shoudl be blocked too 'Note: currently keyval causes key-repeat events for alt if delayed_alt_keydown = YES if keybd(scAlt) and 2 then if delayed_alt_keydown = NO then keybd(scAlt) -= 2 end if delayed_alt_keydown = YES end if /' for scancode as integer = 0 to scLAST if scancode <> scUnfilteredAlt and scancode <> scAlt and scancode <> scLeftAlt and scancode <> scRightAlt and (keybd(scancode) and 1) then delayed_alt_keydown = NO end if next '/ if winstate andalso winstate->focused = NO then delayed_alt_keydown = NO end if if (keybd(scAlt) and 1) = 0 andalso delayed_alt_keydown then keybd(scAlt) or= 6 delayed_alt_keydown = NO end if 'elseif a = scCtrl or a = scLeftCtrl or a = scRightCtrl then else 'Duplicate bit 1 to bit 2 keybd(a) or= (keybd(a) and 2) shl 1 end if next end sub ' Updates kbstate.key_down_ms sub update_keydown_times (kbstate as KeyboardState) 'reset arrow key fire state kbstate.diagonalhack = -1 for a as integer = 0 to scLAST if (kbstate.keybd(a) and 4) or (kbstate.keybd(a) and 1) = 0 then kbstate.key_down_ms(a) = 0 end if if kbstate.keybd(a) and 1 then kbstate.key_down_ms(a) += kbstate.setkeys_elapsed_ms end if next end sub sub setkeys (enable_inputtext as bool = NO) 'Updates the keyboard state to reflect new keypresses 'since the last call, also clears all keypress events (except key-is-down) ' 'Also calls allmodex_controls() to handle key hooks which work everywhere. ' 'enable_inputtext needs to be true for getinputtext to work; 'however there is a one tick delay before coming into effect. 'Passing enable_inputtext may cause certain "combining" keys to stop reporting 'key presses. Currently this only happens with gfx_sdl on X11 (it is an X11 'limitation). And it probably only effects punctuation keys such as ' or ~ '(naturally those keys could be anywhere, but a good rule of thumb seems to be 'to avoid QWERTY punctuation keys) 'For more, see http://en.wikipedia.org/wiki/Dead_key ' 'Note that key repeat is NOT added to kb.keybd() (it's done by "post-processing" in keyval) dim starttime as double = timer if replay.active = NO and disable_native_text_input = NO then if enable_inputtext then enable_inputtext = YES if inputtext_enabled <> enable_inputtext then inputtext_enabled = enable_inputtext io_enable_textinput(inputtext_enabled) end if end if 'While playing back a recording we still poll for keyboard 'input, but this goes in the separate real_kb.keybd() array so it's 'invisible to the game. ' Get real keyboard state real_kb.setkeys_elapsed_ms = bound(1000 * (TIMER - last_setkeys_time), 0, 255) last_setkeys_time = TIMER setkeys_update_keybd real_kb.keybd(), real_kb.delayed_alt_keydown update_keydown_times real_kb real_kb.inputtext = read_inputtext() if replay.active then ' Updates replay_kb.keybd(), .setkeys_elapsed_ms, and .inputtext replay_input_tick () ' Updates replay_kb.key_down_ms(), .diagonalhack update_keydown_times replay_kb end if 'Taking a screenshot with gfx_directx is very slow, so avoid timing that debug_if_slow(starttime, 0.01, replay.active) 'Handle special keys, possibly clear or add keypresses. Might recursively call setkeys. allmodex_controls() ' Record input, after filtering of keys by allmodex_controls. if record.active then record_input_tick () end if ' Call io_mousebits update_mouse_state() ' Custom/Game-specific global controls, done last so that there can't be interference static entered as bool if entered = NO then entered = YES global_setkeys_hook entered = NO end if end sub 'Erase a keypress from the keyboard state. sub clearkey(k as integer) if replay.active then replay_kb.keybd(k) = 0 replay_kb.key_down_ms(k) = 0 else real_kb.keybd(k) = 0 real_kb.key_down_ms(k) = 0 end if end sub 'Mark all keyboard keys and mouse buttons as unpressed. '(TODO: joystick) 'If a key is being held down then this can't hide it. sub clearkeys() for k as integer = 0 to scLAST clearkey(k) next #ifdef IS_GAME flusharray carray(), 7, 0 #endif mouse_state.clearclick(mouseLeft) mouse_state.clearclick(mouseRight) mouse_state.clearclick(mouseMiddle) end sub 'Clear the new keypress flag for a key. sub clear_newkeypress(k as integer) if replay.active then replay_kb.keybd(k) and= 1 else real_kb.keybd(k) and= 1 end if end sub 'Erase a keypress from the real keyboard state even if replaying recorded input. sub real_clearkey(k as integer) real_kb.keybd(k) = 0 real_kb.key_down_ms(k) = 0 end sub 'Clear the new keypress flag for a key. Real keyboard state even if replaying recorded input. sub real_clear_newkeypress(k as integer) real_kb.keybd(k) and= 1 end sub sub setquitflag (newstate as bool = YES) closerequest = newstate end sub function getquitflag () as bool return closerequest end function ' This callback is used by backends. ' Returns INT_MIN if the event was not understood, otherwise return value is event-dependent. function post_event cdecl (event as EventEnum, arg1 as intptr_t = 0, arg2 as intptr_t = 0) as integer select case event case eventTerminate closerequest = YES return 0 case eventFullscreened 'arg1 is the new state user_toggled_fullscreen = YES return 0 end select debuginfo "post_event: unknown event " & event & " " & arg1 & " " & arg2 return INT_MIN end function sub post_terminate_signal cdecl () closerequest = YES end sub '========================================================================================== ' Mouse '========================================================================================== function havemouse() as bool 'atm, all backends support the mouse, or don't know return YES end function ' Cause mouse cursor to be always hidden sub hidemousecursor () io_setmousevisibility(cursorHidden) cursorvisibility = cursorHidden end sub ' Cause mouse cursor to be always visible, except on touchscreen devices sub showmousecursor () io_setmousevisibility(cursorVisible) cursorvisibility = cursorVisible end sub ' Use when the mouse is not in use: ' Hide the mouse cursor in fullscreen, and show it when windowed. sub defaultmousecursor () io_setmousevisibility(cursorDefault) cursorvisibility = cursorDefault end sub sub setcursorvisibility (state as CursorVisibility) select case state case cursorVisible, cursorHidden, cursorDefault io_setmousevisibility(state) cursorvisibility = state case else showerror "Bad setcursorvisibility(" & state & ") call" end select end sub function getcursorvisibility () as CursorVisibility return cursorvisibility end function private sub check_for_released_mouse_button(buttonnum as MouseButton) if (mouse_state.last_buttons and buttonnum) andalso (mouse_state.buttons and buttonnum) = 0 then 'If the button was released since the last tick, turn on .release mouse_state.release or= buttonnum else 'All the rest of the time, .release should be off mouse_state.release and= not buttonnum end if end sub ' Called from setkeys to update the internal mouse state sub update_mouse_state () dim starttime as double = timer mouse_state.lastpos = mouse_state.pos mouse_state.last_buttons = mouse_state.buttons mutexlock keybdmutex 'Just in case io_mousebits(mouse_state.x, mouse_state.y, mouse_state.wheel, mouse_state.buttons, mouse_state.clicks) mutexunlock keybdmutex for button as integer = 0 to 15 check_for_released_mouse_button(1 shl button) next mouse_state.wheel *= -1 mouse_state.wheel_delta = mouse_state.wheel - last_mouse_wheel mouse_state.wheel_clicks = mouse_state.wheel \ 120 - last_mouse_wheel \ 120 last_mouse_wheel = mouse_state.wheel 'Ignore mouse clicks that focus the window. If you clicked, it's already 'focused, so we consider the previous focus state instead. 'FIXME: this doesn't seem to work with gfx_sdl on X11 static prev_focus_state as bool if prev_focus_state = NO then mouse_state.buttons = 0 mouse_state.clicks = 0 end if dim window_state as WindowState ptr = gfx_getwindowstate() prev_focus_state = window_state->focused 'gfx_fb/sdl/alleg return last onscreen position when the mouse is offscreen 'gfx_fb: If you release a mouse button offscreen, it becomes stuck (FB bug) ' wheel scrolls offscreen are registered when you move back onscreen ' Also, may report a mouse position slightly off the screen edge ' (at least on X11) due to freezing mouse input fractionally late. 'gfx_alleg: button state continues to work offscreen but wheel scrolls are not registered 'gfx_sdl: button state works offscreen. Wheel movement is reported if the ' mouse is over the window, even if it's not focused. SDL 1.2 doesn't ' know about the OS's wheel speed setting. mouse_state.moved = mouse_state.lastpos <> mouse_state.pos dim diff as XYPair = mouse_state.lastpos - mouse_state.pos mouse_state.moved_dist = sqrt(diff.x * diff.x + diff.y * diff.y) mouse_state.active = window_state->mouse_over and window_state->focused 'Behaviour of clicking and dragging from inside the window to outside: 'gfx_fb: Mouse input goes dead while outside until moved back into window. ' When button is released, the cursor reappears at actual position on-screen 'gfx_sdl: Mouse acts as if clipped to the window while button is down; but when it's released ' it appears at its actual position on-screen 'directx: Mouse is truely clipped to the window while button is down. 'gfx_alleg:Unknown. if mouse_state.dragging then 'Test whether drag ended if (mouse_state.clicks and mouse_state.dragging) orelse (mouse_state.buttons and mouse_state.dragging) = 0 then mouse_state.dragging = 0 'Preserve .clickstart so that you can see what the drag was upon release else mouse_state.drag_dist += mouse_state.moved_dist end if else 'Dragging is only tracked for a single button at a time, and clickstart is not updated 'while dragging either. So we may now test for new drags or clicks. for button as integer = 0 to 15 dim mask as MouseButton = 1 shl button if mouse_state.clicks and mask then 'Do not flag as dragging until the second tick mouse_state.clickstart = mouse_state.pos elseif mouse_state.buttons and mask then 'Button still down mouse_state.dragging = mask exit for end if next 'Note that we delay zeroing this until the tick after a drag ends mouse_state.drag_dist = 0 end if ' If you released a mouse grab (mouserect) and then click on the ' window, resume the mouse grab. if mouse_state.clicks <> 0 then if mouse_grab_requested andalso mouse_grab_overridden then mouserect remember_mouse_grab(0), remember_mouse_grab(1), remember_mouse_grab(2), remember_mouse_grab(3) end if end if debug_if_slow(starttime, 0.005, mouse_state.clicks) end sub ' Get the state of the mouse at the last setkeys call (or after putmouse, mouserect). ' So make sure you call this AFTER setkeys. function readmouse () byref as MouseInfo return mouse_state end function sub MouseInfo.clearclick(button as MouseButton) clicks and= not button release and= not button ' Cancel for good measure, but not really needed dragging and= not button end sub sub movemouse (x as integer, y as integer) io_setmouse(x, y) ' Don't call io_mousebits to get the new state, since that will cause clicks and movements to get lost, ' and is difficult to support in .ohrkeys. mouse_state.x = x mouse_state.y = y end sub sub mouserect (xmin as integer, xmax as integer, ymin as integer, ymax as integer) ' Set window title to tell the player about scrolllock to escape mouse-grab ' gfx_directx does this itself, including handling scroll lock if gfxbackend = "fb" or gfxbackend = "sdl" then if xmin = -1 and xmax = -1 and ymin = -1 and ymax = -1 then mouse_grab_requested = NO settemporarywindowtitle remember_title else remember_mouse_grab(0) = xmin remember_mouse_grab(1) = xmax remember_mouse_grab(2) = ymin remember_mouse_grab(3) = ymax mouse_grab_requested = YES mouse_grab_overridden = NO #IFDEF __FB_DARWIN__ settemporarywindowtitle remember_title & " (F14 to free mouse)" #ELSE settemporarywindowtitle remember_title & " (ScrlLock to free mouse)" #endIF end if end if mutexlock keybdmutex io_mouserect(xmin, xmax, ymin, ymax) mutexunlock keybdmutex ' Don't call io_mousebits to get the new state, since that will cause clicks and movements to get lost, ' and is difficult to support in .ohrkeys. mouse_state.x = bound(mouse_state.x, xmin, xmax) mouse_state.y = bound(mouse_state.y, ymin, ymax) end sub '========================================================================================== ' Joystick '========================================================================================== function readjoy (joybuf() as integer, jnum as integer) as bool 'Return false if joystick is not present, or true if joystick is present. '(Warning: if gfx_directx can't read a joystick, it is removed and the others 'are renumbered) 'jnum is the joystick to read 'joybuf(0) = Analog X axis (scaled to -100 to 100) 'joybuf(1) = Analog Y axis 'joybuf(2) = button 1: 0=pressed nonzero=not pressed 'joybuf(3) = button 2: 0=pressed nonzero=not pressed 'Other values in joybuf() should be preserved. 'If X and Y axis are not analog, ' upward motion when joybuf(0) < joybuf(9) ' down motion when joybuf(0) > joybuf(10) ' left motion when joybuf(1) < joybuf(11) ' right motion when joybuf(1) > joybuf(12) dim starttime as double = timer dim as integer buttons, x, y dim ret as bool ret = io_readjoysane(jnum, buttons, x, y) if ret then joybuf(0) = x joybuf(1) = y joybuf(2) = (buttons AND 1) = 0 '0 = pressed, not 0 = unpressed (why???) joybuf(3) = (buttons AND 2) = 0 'ditto ret = YES end if debug_if_slow(starttime, 0.01, jnum & " = " & buttons) return ret end function function readjoy (joynum as integer, byref buttons as integer, byref x as integer, byref y as integer) as bool dim starttime as double = timer dim ret as bool = io_readjoysane(joynum, buttons, x, y) debug_if_slow(starttime, 0.01, joynum & " = " & buttons) return ret end function '========================================================================================== ' Compat layer for old graphics backend IO API '========================================================================================== ' These functions are used to supplement gfx backends not supporting ' io_mousebits or io_keybits. 'these are wrappers provided by the polling thread sub io_amx_keybits cdecl (keybdarray as integer ptr) for a as integer = 0 to scLAST keybdarray[a] = keybdstate(a) keybdstate(a) = keybdstate(a) and 1 next end sub sub io_amx_mousebits cdecl (byref mx as integer, byref my as integer, byref mwheel as integer, byref mbuttons as integer, byref mclicks as integer) 'get the mouse state one last time, for good measure io_getmouse(mx, my, mwheel, mbuttons) mclicks = mouseflags or (mbuttons and not mouselastflags) mouselastflags = mbuttons mouseflags = 0 mbuttons = mbuttons or mclicks end sub private sub pollingthread(unused as any ptr) dim as integer a, dummy, buttons while endpollthread = NO mutexlock keybdmutex dim starttime as double = timer io_updatekeys(@keybdstate(0)) debug_if_slow(starttime, 0.005, "io_updatekeys") starttime = timer 'set key state for every key 'highest scancode in fbgfx.bi is &h79, no point overdoing it for a = 0 to scLAST if keybdstate(a) and 8 then 'decide whether to set the 'new key' bit, otherwise the keystate is preserved if (keybdstate(a) and 1) = 0 then 'this is a new keypress keybdstate(a) = keybdstate(a) or 2 end if end if 'move the bit (clearing it) that io_updatekeys sets from 8 to 1 keybdstate(a) = (keybdstate(a) and 2) or ((keybdstate(a) shr 3) and 1) next io_getmouse(dummy, dummy, dummy, buttons) mouseflags = mouseflags or (buttons and not mouselastflags) mouselastflags = buttons mutexunlock keybdmutex debug_if_slow(starttime, 0.01, "io_getmouse") '25ms was found to be sufficient sleep 25 wend end sub '========================================================================================== ' Special overlays and controls '========================================================================================== 'Called from setkeys. This handles keypresses which are global throughout the engine. '(Note that backends also have some hooks, especially gfx_sdl.bas for OSX-specific stuff) private sub allmodex_controls() 'Check to see if the backend has received a request 'to close the window (eg. clicking the window frame's X). 'This form of input isn't recorded, but the ESCs fired in Custom will be recorded, 'so there's no need to check the recorded key state for pageup+pagedown+esc if real_keyval(scPageup) > 0 and real_keyval(scPagedown) > 0 and real_keyval(scEsc) > 1 then closerequest = YES #ifdef IS_CUSTOM 'Fire ESC keypresses to exit every menu if closerequest then if replay.active or replay.paused then stop_replaying_input "Replay ended by quit request" end if real_kb.keybd(scEsc) = 7 end if #elseif defined(IS_GAME) 'Quick abort (could probably do better, just moving this here for now) if closerequest then exit_gracefully() end if #endif ' Crash the program! For testing if keyval(scPageup) > 0 and keyval(scPagedown) > 0 and keyval(scF4) > 1 then dim invalid as integer ptr *invalid = 0 end if ' A breakpoint. If not running under gdb, this will terminate the program if keyval(scTab) > 0 and keyval(scShift) > 0 and keyval(scF4) > 1 then interrupt_self () end if if keyval(scCtrl) > 0 and keyval(scF8) > 1 then gfx_backend_menu end if ' F12 screenshots are handled in setvispage, not here. ' Ctrl+F12 to start/stop recording a .gif if real_keyval(scCtrl) > 0 andalso (real_keyval(scF12) and 4) then toggle_recording_gif end if if real_keyval(scCtrl) > 0 and real_keyval(scTilde) and 4 then toggle_fps_display end if fps_multiplier = base_fps_multiplier if real_keyval(scShift) > 0 and real_keyval(scTab) > 0 then 'speed up while held down fps_multiplier *= 6. end if if replay.active then replay_controls() if real_keyval(scCtrl) > 0 and real_keyval(scF11) > 1 then real_clearkey(scF11) macro_controls() end if 'This is a pause that doesn't show up in recorded input if (replay.active or record.active) and real_keyval(scPause) > 1 then real_clearkey(scPause) pause_replaying_input pause_recording_input notification "Replaying/recording is PAUSED" resume_replaying_input resume_recording_input end if 'Some debug keys for working on resolution independence if keyval(scShift) > 0 and keyval(sc1) > 0 then if keyval(scRightBrace) > 1 then set_resolution windowsize.w + 10, windowsize.h + 10 end if if keyval(scLeftBrace) > 1 then set_resolution windowsize.w - 10, windowsize.h - 10 end if if keyval(scR) > 1 then 'Note: there's also a debug key in the F8 menu in-game. resizing_enabled = gfx_set_resizable(resizing_enabled xor YES, minwinsize.w, minwinsize.h) end if end if if mouse_grab_requested then #IFDEF __FB_DARWIN__ if keyval(scF14) > 1 then clearkey(scF14) #ELSE if keyval(scScrollLock) > 1 then clearkey(scScrollLock) #ENDIF mouserect -1, -1, -1, -1 mouse_grab_requested = YES mouse_grab_overridden = YES end if end if end sub 'Show the menu that comes up when pressing ESC while replaying private sub replay_menu () dim menu(...) as string = {"Resume Replay", "End Replay"} dim choice as integer pause_replaying_input ensure_normal_palette dim previous_speed as double = base_fps_multiplier base_fps_multiplier = 1. choice = multichoice("Stop replaying recorded input?", menu(), 0, 0) if choice = 0 then base_fps_multiplier = previous_speed resume_replaying_input elseif choice = 1 then stop_replaying_input "Playback cancelled." end if restore_previous_palette end sub 'Controls available while replaying input. 'Called from inside setkeys; but it's OK to call setkeys from here if 'pause_replaying_input is called first. If FB had co-routines, this would be implemented as one. private sub replay_controls () 'We call show_help which calls setkeys which calls us. static reentering as bool = NO if reentering then showerror "Reentry of replay_controls shouldn't occur" reentering = YES if real_keyval(scF1) > 1 then dim remem as bool = overlay_replay_display pause_replaying_input() hide_overlays() base_fps_multiplier = 1. show_help("share_replay") setkeys clearkey(scEsc) overlay_replay_display = remem resume_replaying_input() end if if real_keyval(scSpace) > 1 then overlay_replay_display xor= YES end if if real_keyval(scEsc) > 1 then replay_menu end if 'Also scPause, handled in setkeys because it affects record too. if real_keyval(scLeft) > 1 then base_fps_multiplier *= 0.5 show_replay_overlay() end if if real_keyval(scRight) > 1 then base_fps_multiplier *= 2 show_replay_overlay() end if base_fps_multiplier = bound(base_fps_multiplier, 0.5^3, 2.^9) reentering = NO end sub ' Menu of options for playback/recording of macros private sub macro_menu () pause_replaying_input pause_recording_input ensure_normal_palette dim holdscreen as integer = allocatepage copypage vpage, holdscreen dim choice as integer = 3 'Default to playback do 'browse() and inputfilename() clobber vpage copypage holdscreen, vpage fuzzyrect 0, 0, , , uilook(uiBackground), vpage, 40 redim menu(2) as string menu(0) = "Cancel" menu(1) = "Load macro from file" menu(2) = "Start recording macro" if isfile(macrofile) then redim preserve menu(5) menu(3) = "Play back last recorded macro" menu(4) = "Play back last recorded macro # times" menu(5) = "Save last recorded macro to file" end if dim msg as string msg = !"Macro Recording & Replay\n(See F1 help file for information.)" if ubound(menu) < 3 then msg += !"\nNo macro recorded yet." end if choice = multichoice(msg, menu(), choice, 0, "share_macro_menu") if choice = 1 then dim macfile as string macfile = browse(0, "", "*.ohrkeys") if len(macfile) then if not copyfile(macfile, macrofile) THEN visible_debug "ERROR: couldn't make a copy of " & macfile end if end if continue do elseif choice = 2 then show_overlay_message "Recording macro, CTRL+F11 to stop", 2. start_recording_input macrofile elseif choice = 3 then show_overlay_message "Replaying macro" start_replaying_input macrofile elseif choice = 4 then dim repeats as string prompt_for_string repeats, "Number of macro repetitions?" dim repeat_count as integer = str2int(repeats, -1) if repeat_count <= 0 then exit sub end if show_overlay_message "Replaying macro " & replay.repeat_count & " time(s)" start_replaying_input macrofile, repeat_count elseif choice = 5 then dim macfile as string macfile = inputfilename("Input a filename to save to", ".ohrkeys", "", "") 'setkeys if len(macfile) then if not copyfile(macrofile, macfile + ".ohrkeys") THEN visible_debug "ERROR: couldn't write to " & macfile & ".ohrkeys" end if end if continue do end if exit do loop copypage holdscreen, vpage freepage holdscreen restore_previous_palette resume_replaying_input resume_recording_input end sub 'Handles Ctrl+F11 key for macro recording and replay. 'Called from inside setkeys, but it's OK to call setkeys from here as we disallow reentry. 'This can also be called from the in-game debug menu. sub macro_controls () static reentering as bool = NO if reentering then exit sub reentering = YES if record.active then stop_recording_input "Recorded macro, CTRL+F11 to play", errInfo elseif replay.active then show_overlay_message "Ended macro playback early", 2. stop_replaying_input else macro_menu end if reentering = NO end sub 'Display a message above everything else; by default doesn't appear in screenshots. 'Intended for use here in allmodex, but pragmaticlly, can be used in Custom too. 'Note that in-game, you should set gam.showtext/gam.showtext_ticks instead. sub show_overlay_message (msg as string, seconds as double = 3.) overlay_message = msg overlay_hide_time = timer + seconds overlay_replay_display = NO end sub function overlay_message_visible () as bool return len(overlay_message) > 0 and overlay_hide_time > timer end function 'Show the overlay for replaying input private sub show_replay_overlay () overlay_replay_display = YES end sub private sub hide_overlays () overlay_message = "" overlay_replay_display = NO end sub private function ms_to_string (ms as integer) as string return seconds2str(cint(ms * 0.001), "%h:%M:%S") end function sub toggle_fps_display () overlay_showfps = (overlay_showfps + 1) MOD 3 end sub ' Called every time a frame is drawn. ' skipped: true if this frame was frameskipped. private sub update_fps_counter (skipped as bool) fps_draw_frames += 1 if not skipped then fps_real_frames += 1 end if if timer > fps_time_start + 1 then dim nowtime as double = timer draw_fps = fps_draw_frames / (nowtime - fps_time_start) real_fps = fps_real_frames / (nowtime - fps_time_start) fps_time_start = nowtime fps_draw_frames = 0 fps_real_frames = 0 end if end sub 'Draw stuff on top of the video page about to be shown; specially those things 'that are included in .gifs/screenshots even without --recordoverlays 'Returns true if something was drawn. private function draw_allmodex_recordable_overlays (page as integer) as bool dim dirty as bool = NO if gif_show_mouse then with mouse_state dim col as integer = uilook(uiSelectedItem + global_tog) rectangle .x - 4, .y, 9, 1, col, page rectangle .x, .y - 4, 1, 9, col, page if .buttons and mouseLeft then rectangle .x - 3, .y - 3, 3, 3, col, page end if if .buttons and mouseRight then rectangle .x + 1, .y - 3, 3, 3, col, page end if end with dirty = YES end if if gif_show_keys andalso recordgif.active then ' Build up two strings describing keypresses, so that modifiers like LShift ' are sorted to the front. ' FIXME: due to frameskip some keypresses might not be recorded. Should show for more than 1 tick. dim as string modifiers, keys with *iif(replay.active, @replay_kb, @real_kb) for idx as integer = 0 to ubound(.keybd) if .keybd(idx) = 0 then continue for dim keyname as string = scancodename(idx) select case idx case scLeftShift, scRightShift, scLeftAlt, scRightAlt, scLeftCtrl, scRightCtrl modifiers &= "+" & scancodename(idx) case scShift, scAlt, scUnfilteredAlt, scCtrl, scAnyEnter 'Ignore these duplicates case else keys &= "+" & scancodename(idx) end select next idx end with dim keysmsg as string = mid(modifiers & keys, 2) 'trim leading + if any if len(keysmsg) then rectangle pRight, pTop, textwidth(keysmsg) + 2, 10, uilook(uiBackground), page edgeprint keysmsg, pRight - 1, pTop, uilook(uiText), page dirty = YES end if end if return dirty end function 'Draw stuff on top of the video page about to be shown. 'Returns true if something was drawn. private function draw_allmodex_overlays (page as integer) as bool if overlays_enabled = NO then return NO 'show_overlay_message "mouse over:" & gfx_getwindowstate()->mouse_over & " at " & mouse_state.pos dim dirty as bool = NO if overlay_showfps then dim fpsstring as string if overlay_showfps = 2 then fpsstring = "Draw:" & format(draw_fps, "0.0") & " FPS" else fpsstring = "Display:" & format(real_fps, "0.0") & " FPS" end if ' Move the FPS a little to the left, because on OSX+gfx_sdl the handle for resizable ' windows is drawn in the bottom right corner by SDL (not the OS). edgeprint fpsstring, pRight - 14, iif(overlay_replay_display, pTop, pBottom), uilook(uiText), page dirty = YES end if if overlay_replay_display then overlay_hide_time = 0. 'Hides any other message dim repeat_str as string if replay.repeat_count > 1 then repeat_str = "#" & (1 + replay.repeats_done) & "/" & replay.repeat_count end if overlay_message = "Pos: " & ms_to_string(replay.play_position_ms) & "/" & ms_to_string(replay.length_ms) & _ " " & rpad(replay.tick & "/" & replay.length_ticks, " ", 9) & repeat_str & _ !"\nSpeed: " & rpad(fps_multiplier & "x", " ", 5) & "FPS:" & format(draw_fps, "0.0") & " [F1 for help]" elseif overlay_hide_time < timer then overlay_message = "" end if if len(overlay_message) then basic_textbox overlay_message, uilook(uiText), page, rBottom + ancBottom - 2, , YES dirty = YES end if return dirty end function '========================================================================================== ' Recording and replay '========================================================================================== sub start_recording_input (filename as string) if replay.active or replay.paused then debug "Can't record input because already replaying input!" exit sub end if if isfile(filename) then debug "Replacing the input recording that already existed at """ & filename & """" end if record.constructor() 'Clear data if openfile(filename, for_binary + access_write, record.file) then stop_recording_input "Couldn't open " & filename record.file = -1 exit sub end if dim header as string = "OHRRPGCEkeys" put #record.file,, header dim ohrkey_ver as integer = 4 put #record.file,, ohrkey_ver dim seed as double = TIMER RANDOMIZE seed, 3 put #record.file,, seed record.active = YES debuginfo "Recording keyboard input to: """ & filename & """" end sub sub stop_recording_input (msg as string="", errorlevel as ErrorLevelEnum = errError) if msg <> "" then debugc errorlevel, msg show_overlay_message msg end if if record.active or record.paused then close #record.file record.active = NO record.paused = NO debuginfo "STOP recording input" end if end sub ' While recording is paused you can call setkeys without updating the recorded state. ' The keyboard state before pausing is restored when resuming, so it's safe to pause ' and resume recording anywhere. sub pause_recording_input if record.active then record.active = NO record.paused = YES record.last_kb = real_kb end if end sub sub resume_recording_input if record.paused then record.active = YES record.paused = NO real_kb = record.last_kb end if end sub ' Start replaying again from the beginning, used for loop sub restart_replaying_input () replay.tick = -1 replay.nexttick = -1 replay.play_position_ms = 0 seek replay.file, 1 load_replay_header() end sub sub start_replaying_input (filename as string, num_repeats as integer = 1) if record.active or record.paused then debug "Can't replay input because already recording input!" exit sub end if replay.constructor() 'Reset replay_kb.constructor() 'Reset replay.filename = filename if openfile(filename, for_binary + access_read, replay.file) then stop_replaying_input "Couldn't open " & filename replay.file = -1 exit sub end if replay.active = YES replay.repeat_count = num_repeats load_replay_header() end sub sub load_replay_header () dim header as string = STRING(12, 0) GET #replay.file,, header if header <> "OHRRPGCEkeys" then stop_replaying_input "No OHRRPGCEkeys header in """ & replay.filename & """" exit sub end if dim ohrkey_ver as integer = -1 GET #replay.file,, ohrkey_ver if ohrkey_ver <> 4 then stop_replaying_input "Unknown ohrkey version code " & ohrkey_ver & " in """ & replay.filename & """. Only know how to understand version 4" exit sub end if dim seed as double GET #replay.file,, seed RANDOMIZE seed, 3 debuginfo "Replaying keyboard input from: """ & replay.filename & """" read_replay_length() if replay.repeats_done = 0 then show_replay_overlay() end if end sub sub stop_replaying_input (msg as string="", errorlevel as ErrorLevelEnum = errError) if msg <> "" then debugc errorlevel, msg show_overlay_message msg end if if replay.active or replay.paused then close #replay.file replay.file = -1 replay.active = NO replay.paused = NO debugc errorlevel, "STOP replaying input" use_speed_control = YES end if ' Cancel any speedup base_fps_multiplier = 1. end sub ' While replay is paused you can call setkeys without changing the replay state, ' and keyval, etc, return the real state of the keyboard. ' (Safe to try pausing/resuming when not replaying) sub pause_replaying_input ' The replay state is preserved in replay_kb, so pausing and resuming is easy. if replay.active then replay.active = NO replay.paused = YES end if end sub sub resume_replaying_input if replay.paused then replay.active = YES replay.paused = NO end if end sub sub record_input_tick () record.tick += 1 dim presses as ubyte = 0 dim keys_down as integer = 0 for i as integer = 0 to scLAST if real_kb.keybd(i) <> record.last_kb.keybd(i) then presses += 1 end if if real_kb.keybd(i) then keys_down += 1 'must record setkeys_elapsed_ms next i if presses = 0 and keys_down = 0 and len(real_kb.inputtext) = 0 then exit sub dim debugstr as string if record.debug then debugstr = "L:" & LOC(record.file) & " T:" & record.tick & " ms:" & real_kb.setkeys_elapsed_ms & " (" put #record.file,, record.tick put #record.file,, cubyte(real_kb.setkeys_elapsed_ms) put #record.file,, presses for i as ubyte = 0 to scLAST if real_kb.keybd(i) <> record.last_kb.keybd(i) then PUT #record.file,, i PUT #record.file,, cubyte(real_kb.keybd(i)) if record.debug then debugstr &= " " & scancodename(i) & "=" & real_kb.keybd(i) end if next i 'Currently inputtext is Latin-1, format will need changing in future put #record.file,, cubyte(len(real_kb.inputtext)) put #record.file,, real_kb.inputtext if record.debug then debugstr &= " )" if len(real_kb.inputtext) then debugstr &= " input: '" & real_kb.inputtext & "'" debuginfo debugstr end if record.last_kb = real_kb end sub ' Scan the replay file to find its length, setting replay.length_ms and replay.length_ticks ' Assumes replay.file is at start of the data stream. private sub read_replay_length () dim as integer tick, nexttick dim as ubyte tick_ms = 55, presses, input_len dim initial_pos as integer = LOC(replay.file) replay.length_ms = 0 do get #replay.file,, nexttick if eof(replay.file) then exit do if nexttick < tick then visible_debug "Replay corrupt: tick " & replay.nexttick & " occurs after " & tick exit do end if ' Assume any skipped ticks are the same length as the next one, seems to give a vastly better ' estimate than using the previous tick. ' (This could be way off, some ticks are 0ms or 255+ms) get #replay.file,, tick_ms replay.length_ms += tick_ms * (nexttick - tick) ' if (nexttick - tick) > 1 and (tick_ms < 50 or tick_ms > 60) then ' debug "dubious tick_ms estimate " & tick_ms & " at " & tick & " for " & (nexttick - tick) & " ticks" ' end if tick = nexttick get #replay.file,, presses if presses > scLAST + 1 then visible_debug "Replay corrupt: presses=" & presses exit do end if seek #replay.file, 1 + loc(replay.file) + 2 * presses GET #replay.file,, input_len if input_len then seek #replay.file, 1 + loc(replay.file) + input_len end if loop replay.length_ticks = tick seek #replay.file, 1 + initial_pos end sub sub replay_input_tick () replay.tick += 1 do if EOF(replay.file) then replay.repeats_done += 1 'show_overlay_message "Finished replay " & replay.repeats_done & " of " & replay.repeat_count if replay.repeats_done >= replay.repeat_count then stop_replaying_input "The end of the playback file was reached.", errInfo exit sub else restart_replaying_input end if end if 'Check whether it's time to play the next recorded tick in the replay file '(ticks on which nothing happened aren't saved) if replay.nexttick = -1 then replay.fpos = LOC(replay.file) GET #replay.file,, replay.nexttick ' Grab the next tick_ms already, because for some reason it gives far more accurate .play_position_ms estimation dim tick_ms as ubyte GET #replay.file,, tick_ms replay.next_tick_ms = tick_ms end if if replay.nexttick < replay.tick then debug "input replay late for tick " & replay.nexttick & " (" & replay.nexttick - replay.tick & ")" elseif replay.nexttick > replay.tick then 'debug "saving replay input tick " & replay.nexttick & " until its time has come (+" & replay.nexttick - replay.tick & ")" for i as integer = 0 to scLAST 'Check for a corrupt file if replay_kb.keybd(i) then ' There ought to be a tick in the input file so that we can set setkeys_elapsed_ms correctly debug "bad recorded key input: key " & i & " is down, but expected tick " & replay.tick & " is missing" exit for end if next ' Otherwise, this doesn't matter as it won't be used replay_kb.setkeys_elapsed_ms = 1 ' Increment how much we've played so far - not actual play time but at same rate as the .length_ms estimate replay.play_position_ms += replay.next_tick_ms replay_kb.inputtext = "" exit sub end if replay_kb.setkeys_elapsed_ms = replay.next_tick_ms replay.play_position_ms += replay.next_tick_ms dim presses as ubyte GET #replay.file,, presses if presses > scLAST + 1 then stop_replaying_input "input replay tick " & replay.nexttick & " has invalid number of keypresses " & presses exit sub end if dim as string info if replay.debug then info = "L:" & replay.fpos & " T:" & replay.nexttick & " ms:" & replay_kb.setkeys_elapsed_ms & " (" end if dim key as ubyte dim keybits as ubyte for i as integer = 1 to presses GET #replay.file,, key GET #replay.file,, keybits replay_kb.keybd(key) = keybits if replay.debug then info &= " " & scancodename(key) & "=" & keybits next i info &= " )" dim input_len as ubyte GET #replay.file,, input_len if input_len then 'Currently inputtext is Latin-1, format will need changing in future replay_kb.inputtext = space(input_len) GET #replay.file,, replay_kb.inputtext if replay.debug then info &= " input: '" & replay_kb.inputtext & "'" else replay_kb.inputtext = "" end if if replay.debug then debuginfo info 'In case the replay somehow became out of sync, keep looping until we catch up '(Probably hopeless though) if replay.nexttick = replay.tick then replay.nexttick = -1 exit sub end if replay.nexttick = -1 loop end sub '========================================================================================== ' Map rendering '========================================================================================== function readblock (map as TileMap, x as integer, y as integer, default as integer = 112343211) as integer if x < 0 OR x >= map.wide OR y < 0 OR y >= map.high then if default <> 112343211 then return default debug "illegal readblock call " & x & " " & y exit function end if return map.data[x + y * map.wide] end function sub writeblock (map as TileMap, x as integer, y as integer, v as integer) if x < 0 OR x >= map.wide OR y < 0 OR y >= map.high then debug "illegal writeblock call " & x & " " & y exit sub end if map.data[x + y * map.wide] = v end sub 'Calculate which tile to display private function calcblock (tmap as TileMap, x as integer, y as integer, overheadmode as integer, pmapptr as TileMap ptr) as integer 'returns -1 to draw no tile 'overheadmode = 0 : ignore overhead tile bit; draw normally; 'overheadmode = 1 : draw non overhead tiles only (to avoid double draw) 'overheadmode = 2 : draw overhead tiles only dim block as integer 'check bounds if bordertile = -1 then 'wrap while y < 0 y = y + tmap.high wend while y >= tmap.high y = y - tmap.high wend while x < 0 x = x + tmap.wide wend while x >= tmap.wide x = x - tmap.wide wend else if (y < 0) or (y >= tmap.high) or (x < 0) or (x >= tmap.wide) then if tmap.layernum = 0 and overheadmode <= 1 then 'only draw the border tile once! return bordertile else return -1 end if end if end if block = readblock(tmap, x, y) if block = 0 and tmap.layernum > 0 then 'This could be an argument, maybe we could get rid of layernum return -1 end if if overheadmode > 0 then if pmapptr = NULL then debugc errPromptBug, "calcblock: overheadmode but passmap ptr is NULL" block = -1 elseif x >= pmapptr->wide or y >= pmapptr->high then 'Impossible if the passmap is the same size if overheadmode = 2 then block = -1 elseif ((readblock(*pmapptr, x, y) and passOverhead) <> 0) xor (overheadmode = 2) then block = -1 end if end if return block end function 'Given a tile number, possibly animated, translate it to the static tile to display function translate_animated_tile(todraw as integer) as integer if todraw >= 208 then return (todraw - 48 + anim2) mod 160 elseif todraw >= 160 then return (todraw + anim1) mod 160 else return todraw end if end function sub drawmap (tmap as TileMap, x as integer, y as integer, tileset as TilesetData ptr, p as integer, trans as bool = NO, overheadmode as integer = 0, pmapptr as TileMap ptr = NULL, ystart as integer = 0, yheight as integer = -1, pal as Palette16 ptr = NULL) setanim tileset drawmap tmap, x, y, tileset->spr, p, trans, overheadmode, pmapptr, ystart, yheight, , pal end sub sub drawmap (tmap as TileMap, x as integer, y as integer, tilesetsprite as Frame ptr, p as integer, trans as bool = NO, overheadmode as integer = 0, pmapptr as TileMap ptr = NULL, ystart as integer = 0, yheight as integer = -1, largetileset as bool = NO, pal as Palette16 ptr = NULL) 'ystart is the distance from the top to start drawing, yheight the number of lines. yheight=-1 indicates extend to bottom of screen 'There are no options in the X direction because they've never been used, and I don't forsee them being (can use Frames or slices instead) dim mapview as Frame ptr mapview = frame_new_view(vpages(p), 0, ystart, vpages(p)->w, iif(yheight = -1, vpages(p)->h, yheight)) drawmap tmap, x, y, tilesetsprite, mapview, trans, overheadmode, pmapptr, largetileset, pal frame_unload @mapview end sub sub drawmap (tmap as TileMap, x as integer, y as integer, tilesetsprite as Frame ptr, dest as Frame ptr, trans as bool = NO, overheadmode as integer = 0, pmapptr as TileMap ptr = NULL, largetileset as bool = NO, pal as Palette16 ptr = NULL) 'This version of drawmap paints over the entire dest Frame given to it. 'x and y are the camera position at the top left corner of the Frame, not 'the position at which the top left of the map is drawn: this is the OPPOSITE 'to all other drawing commands! 'overheadmode = 0 : draw all tiles normally 'overheadmode = 1 : draw non overhead tiles only (to avoid double draw) 'overheadmode = 2 : draw overhead tiles only 'largetileset : A hack which disables tile animation, instead using tilesets with 256 tiles dim sptr as ubyte ptr dim plane as integer dim ypos as integer dim xpos as integer dim xstart as integer dim yoff as integer dim xoff as integer dim calc as integer dim ty as integer dim tx as integer dim todraw as integer dim tileframe as frame if clippedframe <> dest then setclip , , , , dest end if 'copied from the asm ypos = y \ 20 calc = y mod 20 if calc < 0 then 'adjust for negative coords calc = calc + 20 ypos = ypos - 1 end if yoff = -calc xpos = x \ 20 calc = x mod 20 if calc < 0 then calc = calc + 20 xpos = xpos - 1 end if xoff = -calc xstart = xpos tileframe.w = 20 tileframe.h = 20 tileframe.pitch = 20 ty = yoff while ty < dest->h tx = xoff xpos = xstart while tx < dest->w todraw = calcblock(tmap, xpos, ypos, overheadmode, pmapptr) if largetileset = NO then todraw = translate_animated_tile(todraw) end if 'get the tile if (todraw >= 0) then tileframe.image = tilesetsprite->image + todraw * 20 * 20 if tilesetsprite->mask then 'just in case it happens some day tileframe.mask = tilesetsprite->mask + todraw * 20 * 20 else tileframe.mask = NULL end if 'draw it on the map frame_draw_internal(@tileframe, intpal(), pal, tx, ty, , trans, dest) end if tx = tx + 20 xpos = xpos + 1 wend ty = ty + 20 ypos = ypos + 1 wend end sub 'Set tile animation state for drawmap... yuck sub setanim (cycle1 as integer, cycle2 as integer) anim1 = cycle1 anim2 = cycle2 end sub sub setanim (tileset as TilesetData ptr) anim1 = tileset->tastuf(0) + tileset->anim(0).cycle anim2 = tileset->tastuf(20) + tileset->anim(1).cycle end sub '-2: draw nothing beyond the map edge '-1: wrap map '0+: draw this tile beyong the map edge (but only when drawing layer 0) sub setoutside (defaulttile as integer) bordertile = defaulttile end sub ' Draws all map layers at a single tile coordinate. Used for drawing the minimap. ' Respects setoutside. Changes the setanim (current tileset animation) state. sub draw_layers_at_tile(composed_tile as Frame ptr, tiles() as TileMap, tilesets() as TilesetData ptr, tx as integer, ty as integer, pmapptr as TileMap ptr = NULL) for idx as integer = 0 to ubound(tiles) 'It's possible that layer <> idx if for example drawing a minimap of a single map layer dim layer as integer = tiles(idx).layernum setanim tilesets(idx) with *tilesets(idx) dim todraw as integer = calcblock(tiles(idx), tx, ty, 0, 0) if todraw < 0 then continue for todraw = translate_animated_tile(todraw) frame_draw .spr, , 0, -todraw * 20, 1, (layer > 0), composed_tile if layer = 0 andalso pmapptr andalso (readblock(*pmapptr, tx, ty) and passOverhead) then ' If an overhead tile, return just the layer 0 tile exit for end if end with next end sub '========================================================================================== ' Old graphics API wrappers '========================================================================================== sub drawsprite (pic() as integer, picoff as integer, pal() as integer, po as integer, x as integer, y as integer, page as integer, trans as bool = YES) 'draw sprite from pic(picoff) onto page using pal() starting at po drawspritex(pic(), picoff, pal(), po, x, y, page, 1, trans) end sub sub bigsprite (pic() as integer, pal() as integer, p as integer, x as integer, y as integer, page as integer, trans as bool = YES) drawspritex(pic(), 0, pal(), p, x, y, page, 2, trans) end sub sub hugesprite (pic() as integer, pal() as integer, p as integer, x as integer, y as integer, page as integer, trans as bool = YES) drawspritex(pic(), 0, pal(), p, x, y, page, 4, trans) end sub 'Create a palette from a record in .PAL function Palette16_new_from_buffer(pal() as integer, po as integer = 0) as Palette16 ptr dim ret as Palette16 ptr = Palette16_new() dim word as integer for i as integer = 0 to 15 'palettes are interleaved like everything else word = pal((po + i) \ 2) ' get color from palette if (po + i) mod 2 = 1 then ret->col(i) = (word and &hff00) shr 8 else ret->col(i) = word and &hff end if next return ret end function 'Convert a (deprecated) pixel array representation of a 4 bit sprite to a Frame function frame_new_from_buffer(pic() as integer, picoff as integer = 0) as Frame ptr dim sw as integer dim sh as integer dim hspr as Frame ptr dim dspr as ubyte ptr dim nib as integer dim i as integer dim spix as integer ' 2-byte word read from source dim row as integer sw = pic(picoff) sh = pic(picoff+1) picoff = picoff + 2 hspr = frame_new(sw, sh) dspr = hspr->image 'now do the pixels 'pixels are in columns, so this might not be the best way to do it 'maybe just drawing straight to the screen would be easier nib = 0 row = 0 for i = 0 to (sw * sh) - 1 select case nib ' 2 bytes = 4 nibbles in each int case 0 spix = (pic(picoff) and &h00f0) shr 4 case 1 spix = (pic(picoff) and &h000f) shr 0 case 2 spix = (pic(picoff) and &hf000) shr 12 case 3 spix = (pic(picoff) and &h0f00) shr 8 picoff = picoff + 1 end select *dspr = spix ' set image pixel dspr = dspr + sw row = row + 1 if (row >= sh) then 'ugh dspr = dspr - (sw * sh) dspr = dspr + 1 row = 0 end if nib = nib + 1 nib = nib and 3 next return hspr end function sub drawspritex (pic() as integer, picoff as integer, pal as Palette16 ptr, x as integer, y as integer, page as integer, scale as integer = 1, trans as bool = YES) 'draw sprite scaled, used for drawsprite(x1), bigsprite(x2) and hugesprite(x4) if clippedframe <> vpages(page) then setclip , , , , vpages(page) end if 'convert the buffer into a Frame dim hspr as Frame ptr hspr = frame_new_from_buffer(pic(), picoff) 'now draw the image frame_draw(hspr, pal, x, y, scale, trans, page) 'what a waste frame_unload(@hspr) end sub ' Temp overload whichs exists to help detangle the sprite editor from its bad old ways sub drawspritex (pic() as integer, picoff as integer, pal() as integer, po as integer, x as integer, y as integer, page as integer, scale as integer = 1, trans as bool = YES) 'draw sprite scaled, used for drawsprite(x1), bigsprite(x2) and hugesprite(x4) dim hpal as Palette16 ptr hpal = Palette16_new_from_buffer(pal(), po) drawspritex pic(), picoff, hpal, x, y, page, scale, trans Palette16_unload @hpal end sub sub wardsprite (pic() as integer, picoff as integer, pal() as integer, po as integer, x as integer, y as integer, page as integer, trans as bool = YES) 'this just draws the sprite mirrored 'the coords are still top-left dim sw as integer dim sh as integer dim hspr as Frame ptr dim dspr as ubyte ptr dim nib as integer dim i as integer dim spix as integer ' 2-byte word read from source dim pix as integer dim row as integer if clippedframe <> vpages(page) then setclip , , , , vpages(page) end if sw = pic(picoff) sh = pic(picoff+1) picoff = picoff + 2 hspr = frame_new(sw, sh) dspr = hspr->image dspr = dspr + sw - 1 'jump to last column 'now do the pixels 'pixels are in columns, so this might not be the best way to do it 'maybe just drawing straight to the screen would be easier nib = 0 row = 0 for i = 0 to (sw * sh) - 1 select case nib ' 2 bytes = 4 nibbles in each int case 0 spix = (pic(picoff) and &hf0) shr 4 case 1 spix = (pic(picoff) and &h0f) shr 0 case 2 spix = (pic(picoff) and &hf000) shr 12 case 3 spix = (pic(picoff) and &h0f00) shr 8 picoff = picoff + 1 end select if spix = 0 and trans then pix = 0 ' transparent else 'palettes are interleaved like everything else pix = pal((po + spix) \ 2) ' get color from palette if (po + spix) mod 2 = 1 then pix = (pix and &hff00) shr 8 else pix = pix and &hff end if end if *dspr = pix ' set image pixel dspr = dspr + sw row = row + 1 if (row >= sh) then 'ugh dspr = dspr - (sw * sh) dspr = dspr - 1 ' right to left for wardsprite row = 0 end if nib = nib + 1 nib = nib and 3 next 'now draw the image frame_draw(hspr, NULL, x, y, , trans, page) frame_unload(@hspr) end sub sub stosprite (pic() as integer, picoff as integer, x as integer, y as integer, page as integer) 'This is the opposite of loadsprite, ie store raw sprite data in screen p 'starting at x, y. dim i as integer dim poff as integer dim toggle as integer dim sbytes as integer dim h as integer dim w as integer if clippedframe <> vpages(page) then setclip , , , , vpages(page) end if CHECK_FRAME_8BIT(vpages(page)) poff = picoff w = pic(poff) h = pic(poff + 1) poff += 2 sbytes = ((w * h) + 1) \ 2 'only 4 bits per pixel y += x \ 320 x = x mod 320 'copy from passed int buffer, with 2 bytes per int as usual toggle = 0 for i = 0 to sbytes - 1 if toggle = 0 then PAGEPIXEL(x, y, page) = pic(poff) and &hff toggle = 1 else PAGEPIXEL(x, y, page) = (pic(poff) and &hff00) shr 8 toggle = 0 poff += 1 end if x += 1 if x = 320 then y += 1 x = 0 end if next end sub sub loadsprite (pic() as integer, picoff as integer, x as integer, y as integer, w as integer, h as integer, page as integer) 'reads sprite from given page into pic(), starting at picoff dim i as integer dim poff as integer dim toggle as integer dim sbytes as integer dim temp as integer if clippedframe <> vpages(page) then setclip , , , , vpages(page) end if CHECK_FRAME_8BIT(vpages(page)) sbytes = ((w * h) + 1) \ 2 'only 4 bits per pixel y += x \ 320 x = x mod 320 'copy to passed int buffer, with 2 bytes per int as usual toggle = 0 poff = picoff pic(poff) = w 'these are 4byte ints, not compat w. orig. pic(poff+1) = h poff += 2 for i = 0 to sbytes - 1 temp = PAGEPIXEL(x, y, page) if toggle = 0 then pic(poff) = temp else pic(poff) = pic(poff) or (temp shl 8) poff += 1 end if toggle xor= 1 x += 1 if x = 320 then y += 1 x = 0 end if next end sub sub getsprite (pic() as integer, picoff as integer, x as integer, y as integer, w as integer, h as integer, page as integer) 'This reads a rectangular region of a screen page into sprite buffer array pic() at picoff 'It assumes that all the pixels it encounters will be colors 0-15 of the master palette 'even though those colors will certainly be mapped to some other 16 color palette when drawn dim as ubyte ptr sbase, sptr dim nyb as integer = 0 dim p as integer = 0 dim as integer sw, sh CHECK_FRAME_8BIT(vpages(page)) 'store width and height p = picoff pic(p) = w p += 1 pic(p) = h p += 1 'find start of image sbase = vpages(page)->image + (vpages(page)->pitch * y) + x 'pixels are stored in columns for the sprites (argh) for sh = 0 to small(w, vpages(page)->w) - 1 sptr = sbase for sw = 0 to small(h, vpages(page)->h) - 1 select case nyb case 0 pic(p) = (*sptr and &h0f) shl 4 case 1 pic(p) = pic(p) or ((*sptr and &h0f) shl 0) case 2 pic(p) = pic(p) or ((*sptr and &h0f) shl 12) case 3 pic(p) = pic(p) or (*sptr and &h0f) shl 8 p += 1 end select sptr += vpages(page)->pitch nyb = (nyb + 1) and 3 next sbase = sbase + 1 'next col next end sub 'Convenience wrapper around getsprite to grab an entire Frame instead of a sub-rectangle of a page sub frame_to_buffer(spr as Frame ptr, pic() as integer) dim page as integer = registerpage(spr) getsprite pic(), 0, 0, 0, spr->w, spr->h, page freepage page end sub '========================================================================================== ' Old allmodex IO '========================================================================================== ' These are specifically for reading/writing files. The other obsolete ' graphics stuff is above. sub storemxs (fil as string, record as integer, fr as Frame ptr) 'saves a screen page to a file. Doesn't support non-320x200 pages dim f as integer dim as integer x, y dim sptr as ubyte ptr dim plane as integer CHECK_FRAME_8BIT(fr) if openfile(fil, for_binary + access_read_write, f) then exit sub 'skip to index seek #f, (record*64000) + 1 'will this work with write access? 'modex format, 4 planes for plane = 0 to 3 for y = 0 to 199 sptr = fr->image + fr->pitch * y + plane for x = 0 to (80 - 1) '1/4 of a row put #f, , *sptr sptr = sptr + 4 next next next close #f end sub 'For compatibility: load into an existing Frame. 'NOTE: Don't use this in new code. It bypasses the cache. Use frame_load sub loadmxs (filen as string, record as integer, dest as Frame ptr) dim temp as Frame ptr temp = frame_load_mxs(filen, record) frame_clear dest if temp then frame_draw temp, , 0, 0, , NO, dest frame_unload @temp end if end sub 'Loads a 320x200 mode X format page from a file. 'This should probably only be called directly when loading from file outside an .rpg, 'otherwise use frame_load. function frame_load_mxs (filen as string, record as integer) as Frame ptr dim starttime as double = timer dim fh as integer dim as integer x, y dim sptr as ubyte ptr dim plane as integer dim dest as Frame ptr 'Return blank Frame on failure dest = frame_new(320, 200, , YES) if record < 0 then debugc errBug, "frame_load_mxs: attempted to read a negative record number " & record return dest end if if openfile(filen, for_binary + access_read, fh) then debugc errError, "frame_load_mxs: Couldn't open " & filen return dest end if if lof(fh) < (record + 1) * 64000 then debugc errError, "frame_load_mxs: wanted page " & record & "; " & filen & " is only " & lof(fh) & " bytes" close #fh return dest end if 'skip to index seek #fh, (record*64000) + 1 dim quarter_row(79) as ubyte 'modex format, 4 planes for plane = 0 to 3 for y = 0 to 200 - 1 sptr = dest->image + dest->pitch * y + plane '1/4 of a row get #fh, , quarter_row() for x = 0 to 80 - 1 sptr[x * 4] = quarter_row(x) next next next close #fh debug_if_slow(starttime, 0.1, filen) return dest end function '========================================================================================== ' Graphics primitives '========================================================================================== 'No clipping!! sub putpixel (spr as Frame ptr, x as integer, y as integer, c as integer) if x < 0 orelse x >= spr->w orelse y < 0 orelse y >= spr->h then exit sub end if CHECK_FRAME_8BIT(spr) FRAMEPIXEL(x, y, spr) = c end sub sub putpixel (x as integer, y as integer, c as integer, p as integer) if clippedframe <> vpages(p) then setclip , , , , vpages(p) end if CHECK_FRAME_8BIT(vpages(p)) if POINT_CLIPPED(x, y) then 'debug "attempt to putpixel off-screen " & x & "," & y & "=" & c & " on page " & p exit sub end if PAGEPIXEL(x, y, p) = c end sub function readpixel (spr as Frame ptr, x as integer, y as integer) as integer if x < 0 orelse x >= spr->w orelse y < 0 orelse y >= spr->h then return -1 end if CHECK_FRAME_8BIT(spr, 0) return FRAMEPIXEL(x, y, spr) end function function readpixel (x as integer, y as integer, p as integer) as integer if clippedframe <> vpages(p) then setclip , , , , vpages(p) end if CHECK_FRAME_8BIT(vpages(p), 0) if POINT_CLIPPED(x, y) then debug "attempt to readpixel off-screen " & x & "," & y & " on page " & p return -1 end if return PAGEPIXEL(x, y, p) end function sub drawbox (x as RelPos, y as RelPos, w as RelPos, h as RelPos, col as integer, thickness as integer = 1, p as integer) drawbox vpages(p), x, y, w, h, col, thickness end sub 'Draw a hollow box, with given edge thickness sub drawbox (dest as Frame ptr, x as RelPos, y as RelPos, w as RelPos, h as RelPos, col as integer, thickness as integer = 1) w = relative_pos(w, dest->w) h = relative_pos(h, dest->h) if w < 0 then x = x + w + 1: w = -w if h < 0 then y = y + h + 1: h = -h if w = 0 or h = 0 then exit sub x = relative_pos(x, dest->w, w) y = relative_pos(y, dest->h, h) dim as integer thickx = small(thickness, w), thicky = small(thickness, h) rectangle dest, x, y, w, thicky, col IF h > thicky THEN rectangle dest, x, y + h - thicky, w, thicky, col end IF rectangle dest, x, y, thickx, h, col IF w > thickx THEN rectangle dest, x + w - thickx, y, thickx, h, col end IF end sub 'Draw a box in perspective, its top 'offset' from its bottom; 12 lines in total 'Note, thickness doesn't affect the four angled lines sub drawcube(dest as Frame ptr, rect as RectType, off as XYPair, col as integer, thickness as integer = 1) drawbox dest, rect.x, rect.y, rect.wide, rect.high, col, thickness dim shifted as RectType = rect + off drawbox dest, shifted.x, shifted.y, shifted.wide, shifted.high, col, thickness dim br as XYPair = rect.topleft + rect.size - 1 'bottom-right drawline dest, rect.x, rect.y, rect.x + off.x, rect.y + off.y, col drawline dest, rect.x, br.y, rect.x + off.x, br.y + off.y, col drawline dest, br.x, rect.y, br.x + off.x, rect.y + off.y, col drawline dest, br.x, br.y, br.x + off.x, br.y + off.y, col end sub ' This function is slightly different from drawbox/rectangle, in that draws boxes with ' width/height 0 as width/height 1 instead of not at all. ' color is the main highlight color; if -1, use default ' FIXME: this function doesn't respect clipping! sub drawants(dest as Frame ptr, x as RelPos, y as RelPos, wide as RelPos, high as RelPos, color as integer = -1) if color = -1 then color = uilook(uiText) ' Decode relative positions/sizes to absolute wide = relative_pos(wide, dest->w) high = relative_pos(high, dest->h) x = relative_pos(x, dest->w, wide) y = relative_pos(y, dest->h, high) if wide < 0 then x = x + wide + 1: wide = -wide if high < 0 then y = y + high + 1: high = -high 'if wide <= 0 or high <= 0 then exit sub dim col as integer '--Draw verticals for idx as integer = 0 to large(high - 1, 0) select case (idx + x + y + tickcount) mod 3 case 0: continue for case 1: col = color case 2: col = uilook(uiBackground) end select putpixel dest, x, y + idx, col if wide > 0 then putpixel dest, x + wide - 1, y + idx, col end if next idx '--Draw horizontals for idx as integer = 0 to large(wide - 1, 0) select case (idx + x + y + tickcount) mod 3 case 0: continue for case 1: col = color case 2: col = uilook(uiBackground) end select putpixel dest, x + idx, y, col if high > 0 then putpixel dest, x + idx, y + high - 1, col end if next idx end sub sub rectangle (x as RelPos, y as RelPos, w as RelPos, h as RelPos, c as integer, p as integer) rectangle vpages(p), x, y, w, h, c end sub sub rectangle (fr as Frame Ptr, x as RelPos, y as RelPos, w as RelPos, h as RelPos, c as integer) if clippedframe <> fr then setclip , , , , fr end if ' Decode relative positions/sizes to absolute 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) if w < 0 then x = x + w + 1: w = -w if h < 0 then y = y + h + 1: h = -h 'clip if x + w > clipr then w = (clipr - x) + 1 if y + h > clipb then h = (clipb - y) + 1 if x < clipl then w -= (clipl - x) : x = clipl if y < clipt then h -= (clipt - y) : y = clipt if w <= 0 or h <= 0 then exit sub if fr->surf then dim rect as SurfaceRect = (x, y, x + w - 1, y + h - 1) dim col as uint32 = c if fr->surf->format = SF_32bit then col = intpal(c).col end if gfx_surfaceFill(col, @rect, fr->surf) else dim sptr as ubyte ptr = fr->image + (y * fr->pitch) + x while h > 0 memset(sptr, c, w) sptr += fr->pitch h -= 1 wend end if end sub sub fuzzyrect (x as RelPos, y as RelPos, w as RelPos = rWidth, h as RelPos = rHeight, c as integer, p as integer, fuzzfactor as integer = 50) fuzzyrect vpages(p), x, y, w, h, c, fuzzfactor end sub sub fuzzyrect (fr as Frame Ptr, x as RelPos, y as RelPos, w as RelPos = rWidth, h as RelPos = rHeight, c as integer, fuzzfactor as integer = 50) 'How many magic constants could you wish for? 'These were half generated via magic formulas, and half hand picked (with magic criteria) static grain_table(50) as integer = {_ 50, 46, 42, 38, 38, 40, 41, 39, 26, 38, 30, 36, _ 42, 31, 39, 38, 41, 26, 27, 28, 40, 35, 35, 31, _ 39, 50, 41, 30, 29, 28, 45, 37, 24, 43, 23, 42, _ 21, 28, 11, 16, 20, 22, 18, 17, 19, 32, 17, 16, _ 15, 14, 50} if clippedframe <> fr then setclip , , , , fr end if CHECK_FRAME_8BIT(fr) fuzzfactor = bound(fuzzfactor, 1, 99) ' Decode relative positions/sizes to absolute 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) dim grain as integer dim r as integer = 0 dim startr as integer = 0 if fuzzfactor <= 50 then grain = grain_table(fuzzfactor) else grain = grain_table(100 - fuzzfactor) 'if w = 99 then grain = h mod 100 'for hand picking if w < 0 then x = x + w + 1: w = -w if h < 0 then y = y + h + 1: h = -h 'clip if x + w > clipr then w = (clipr - x) + 1 if y + h > clipb then h = (clipb - y) + 1 if x < clipl then startr += (clipl - x) * fuzzfactor w -= (clipl - x) x = clipl end if if y < clipt then startr += (clipt - y) * grain h -= (clipt - y) y = clipt end if if w <= 0 or h <= 0 then exit sub dim sptr as ubyte ptr = fr->image + (y * fr->pitch) + x while h > 0 startr = (startr + grain) mod 100 r = startr for i as integer = 0 to w-1 r += fuzzfactor if r >= 100 then sptr[i] = c r -= 100 end if next h -= 1 sptr += fr->pitch wend end sub 'Draw either a rectangle or a scrolling chequer pattern. 'bgcolor is either between 0 and 255 (a colour), bgChequerScroll (a scrolling chequered 'background), or bgChequer (a non-scrolling chequered background) 'chequer_scroll is a counter variable which the calling function should increment once per tick. '(If chequer_scroll isn't provided, than bgChequerScroll acts like bgChequer.) 'wide and high default to the whole dest Frame. sub draw_background (dest as Frame ptr, bgcolor as bgType = bgChequerScroll, byref chequer_scroll as integer = 0, x as RelPos = 0, y as RelPos = 0, wide as RelPos = rWidth, high as RelPos = rHeight) const zoom = 3 'Chequer pattern zoom, fixed const rate = 4 'ticks per pixel scrolled, fixed 'static chequer_scroll as integer chequer_scroll = POSMOD(chequer_scroll, (zoom * rate * 2)) wide = relative_pos(wide, dest->w) high = relative_pos(high, dest->h) x = relative_pos(x, dest->w, wide) y = relative_pos(y, dest->h, high) if bgcolor >= 0 then rectangle dest, x, y, wide, high, bgcolor else dim bg_chequer as Frame Ptr bg_chequer = frame_new(wide / zoom + 2, high / zoom + 2) frame_clear bg_chequer, uilook(uiBackground) fuzzyrect bg_chequer, 0, 0, bg_chequer->w, bg_chequer->h, uilook(uiDisabledItem) dim offset as integer = 0 if bgcolor = -1 then offset = chequer_scroll \ rate dim oldclip as ClipState saveclip oldclip shrinkclip x, y, x + wide - 1, y + high - 1, dest frame_draw bg_chequer, NULL, x - offset, y - offset, zoom, NO, dest loadclip oldclip frame_unload @bg_chequer end if end sub sub drawline (x1 as integer, y1 as integer, x2 as integer, y2 as integer, c as integer, p as integer, dash_cycle as integer = 0, dash_len as integer = 0) drawline vpages(p), x1, y1, x2, y2, c, dash_cycle, dash_len end sub 'dash_cycle: ' If nonzero, draw dots/dashes. The cycle length is the number of ' pixels from the start of one dash to the next one. Should be >= 2. 'dash_len: ' Dash length in pixels. Shoudl be < dash_cycle. sub drawline (dest as Frame ptr, x1 as integer, y1 as integer, x2 as integer, y2 as integer, c as integer, dash_cycle as integer = 0, dash_len as integer = 0) 'Uses Bresenham's algorithm if clippedframe <> dest then setclip , , , , dest end if CHECK_FRAME_8BIT(dest) if y1 > y2 then 'swap ends, we only draw downwards swap y1, y2 swap x1, x2 end if dim as integer stepX, stepY if x2 > x1 then stepX = 1 elseif x2 < x1 then stepX = -1 else stepX = 0 end if if y2 > y1 then stepY = 1 else stepY = 0 end if 'If the line is mostly-horizontal, then the 'major' direction 'is X and the minor is Y. 'All the deltas are fractions of a pixel scaled to integers 'by multiplying by 2*deltaMAJOR dim as integer deltaX, deltaY deltax = abs(x2 - x1) deltay = y2 - y1 'is positive due to above swap dim as integer delta 'Accumulated fraction of a pixel error dim as integer delta_add, delta_sub dim as integer length, majorstep, minorstep if deltaX > deltaY then length = deltaX delta_add = 2*deltaY delta_sub = 2*deltaX minorstep = stepY * dest->pitch majorstep = stepX else length = deltaY delta_add = 2*deltaX delta_sub = 2*deltaY minorstep = stepX majorstep = stepY * dest->pitch end if delta = -delta_sub \ 2 'Start at the center of a pixel /' 'Perform clipping (not correct/finished) dim itstart as integer if y1 < clipt then if y2 < clipt then exit sub 'Ensures delta_add & delta_sub > 0 if deltaX > deltaY then delta += (clipt - y1) * delta_add itstart = delta \ delta_add delta = delta mod delta_sub else itstart = clipt - y1 delta += itstart * delta_add x1 += stepX * (delta mod delta_sub) delta = delta mod delta_sub if delta > 0 then x1 += stepX 'sptr += minorstep delta -= delta_sub end if end if y1 = clipt end if '/ dim sptr as ubyte ptr sptr = dest->image + (y1 * dest->pitch) + x1 dim dash_accum as integer for it as integer = 0 to length if POINT_CLIPPED(x1, y1) = NO then if dash_cycle = 0 then *sptr = c else if dash_accum < dash_len then *sptr = c end if dash_accum += 1 if dash_accum = dash_cycle then dash_accum = 0 end if end if delta += delta_add if delta > 0 then sptr += minorstep delta -= delta_sub if deltaX > deltaY then y1 += stepY else x1 += stepX end if sptr += majorstep if deltaX > deltaY then x1 += stepX else y1 += stepY next end sub sub paintat (dest as Frame ptr, x as integer, y as integer, c as integer) 'a floodfill. dim tcol as integer dim queue as XYPair_node ptr = null dim tail as XYPair_node ptr = null dim as integer w, e 'x coords west and east dim i as integer dim tnode as XYPair_node ptr = null if clippedframe <> dest then setclip , , , , dest end if CHECK_FRAME_8BIT(dest) if POINT_CLIPPED(x, y) then exit sub tcol = readpixel(dest, x, y) 'get target colour 'prevent infinite loop if you fill with the same colour if tcol = c then exit sub queue = callocate(sizeof(XYPair_node)) queue->x = x queue->y = y queue->nextnode = null tail = queue 'we only let coordinates within the clip bounds get onto the queue, so there's no need to check them do if FRAMEPIXEL(queue->x, queue->y, dest) = tcol then FRAMEPIXEL(queue->x, queue->y, dest) = c w = queue->x e = queue->x 'find western limit while w > clipl and FRAMEPIXEL(w-1, queue->y, dest) = tcol w -= 1 FRAMEPIXEL(w, queue->y, dest) = c wend 'find eastern limit while e < clipr and FRAMEPIXEL(e+1, queue->y, dest) = tcol e += 1 FRAMEPIXEL(e, queue->y, dest) = c wend 'add bordering XYPair_nodes for i = w to e if queue->y > clipt then 'north if FRAMEPIXEL(i, queue->y-1, dest) = tcol then tail->nextnode = callocate(sizeof(XYPair_node)) tail = tail->nextnode tail->x = i tail->y = queue->y-1 tail->nextnode = null end if end if if queue->y < clipb then 'south if FRAMEPIXEL(i, queue->y+1, dest) = tcol then tail->nextnode = callocate(sizeof(XYPair_node)) tail = tail->nextnode tail->x = i tail->y = queue->y+1 tail->nextnode = null end if end if next end if 'advance queue pointer, and delete behind us tnode = queue queue = queue->nextnode deallocate(tnode) loop while queue <> null 'should only exit when queue has caught up with tail end sub sub ellipse (fr as Frame ptr, x as double, y as double, radius as double, col as integer, fillcol as integer, semiminor as double = 0.0, angle as double = 0.0) 'radius is the semimajor axis if the ellipse is not a circle 'angle is the angle of the semimajor axis to the x axis, in radians counter-clockwise if clippedframe <> fr then setclip , , , , fr end if CHECK_FRAME_8BIT(fr) 'x,y is the pixel to centre the ellipse at - that is, the centre of that pixel, so add half a pixel to 'radius to put the perimeter halfway between two pixels x += 0.5 y += 0.5 radius += 0.5 if semiminor = 0.0 then semiminor = radius else semiminor += 0.5 end if dim as double ypart ypart = fmod(y, 1.0) - 0.5 'Here we add in the fact that we test for intercepts with a line offset 0.5 pixels dim as double sin_2, cos_2, sincos sin_2 = sin(-angle) ^ 2 cos_2 = cos(-angle) ^ 2 sincos = sin(-angle) * cos(-angle) 'Coefficients of the general conic quadratic equation Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0 (D,E = 0) 'Aprime, Cprime are of the unrotated version dim as double Aprime, Cprime Aprime = 1.0 / radius ^ 2 Cprime = 1.0 / semiminor ^ 2 dim as double A, B, C, F A = Aprime * cos_2 + Cprime * sin_2 B = 2 * (Cprime - Aprime) * sincos C = Aprime * sin_2 + Cprime * cos_2 F = -1.0 dim as integer xstart = 999999999, xend = -999999999, lastxstart = 999999999, lastxend = -999999999, xs, yi, ys, maxr = large(radius, semiminor) + 1 for yi = maxr to -maxr step -1 'Note yi is cartesian coordinates, with the centre of the ellipsis at the origin, NOT screen coordinates! 'xs, ys are in screen coordinates ys = int(y) - yi if ys < clipt - 1 or ys > clipb + 1 then continue for 'Fix y (scanline) and solve for x using quadratic formula (coefficients:) dim as double qf_a, qf_b, qf_c qf_a = A qf_b = B * (yi + ypart) qf_c = C * (yi + ypart) ^ 2 + F dim as double discrim discrim = qf_b^2 - 4.0 * qf_a * qf_c if discrim >= 0.0 then discrim = sqr(discrim) 'This algorithm is very sensitive to which way XXX.5 is rounded (normally towards even)... xstart = -int(-(x + (-qf_b - discrim) / (2.0 * qf_a) - 0.5)) 'ceil(x-0.5), ie. round 0.5 down xend = int(x + (-qf_b + discrim) / (2.0 * qf_a) - 0.5) 'floor(x-0.5), ie. round 0.5 up, and subtract 1 if xstart > xend then 'No pixel centres on this scanline lie inside the ellipse if lastxstart <> 999999999 then xend = xstart 'We've already started drawing, so must draw at least one pixel end if end if end if 'Reconsider the previous scanline for xs = lastxstart to xstart - 1 putpixel(fr, xs, ys - 1, col) next for xs = xend + 1 to lastxend putpixel(fr, xs, ys - 1, col) next dim canskip as bool = YES for xs = xstart to xend putpixel(fr, xs, ys, col) if canskip andalso xs >= lastxstart - 1 then 'Draw the bare minimum number of pixels (some of these might be needed, but won't know until next scanline) dim jumpto as integer = small(xend - 1, lastxend) if fillcol <> -1 then for xs = xs + 1 to jumpto putpixel(fr, xs, ys, fillcol) next end if xs = jumpto canskip = NO 'Skipping more than once causes infinite loops end if next lastxstart = xstart lastxend = xend if discrim >= 0 then xend = xstart - 1 'To draw the last scanline, in the next loop next end sub 'Replaces one colour with another, OR if swapcols is true, swaps the two colours. sub replacecolor (fr as Frame ptr, c_old as integer, c_new as integer, swapcols as bool = NO) if clippedframe <> fr then setclip , , , , fr end if CHECK_FRAME_8BIT(fr) for yi as integer = clipt to clipb dim sptr as ubyte ptr = fr->image + (yi * fr->pitch) for xi as integer = clipl to clipr if sptr[xi] = c_old then sptr[xi] = c_new elseif swapcols and (sptr[xi] = c_new) then sptr[xi] = c_old end if next next end sub sub swapcolors(fr as Frame ptr, col1 as integer, col2 as integer) replacecolor fr, col1, col2, YES end sub 'Changes a Frame in-place, applying a remapping sub remap_to_palette (fr as Frame ptr, pal as Palette16 ptr) if clippedframe <> fr then setclip , , , , fr end if CHECK_FRAME_8BIT(fr) for y as integer = clipt to clipb for x as integer = clipl to clipr FRAMEPIXEL(x, y, fr) = pal->col(FRAMEPIXEL(x, y, fr)) next next end sub sub remap_to_palette (fr as Frame ptr, palmapping() as integer) dim pal as Palette16 ptr = Palette16_new_from_indices(palmapping()) remap_to_palette fr, pal Palette16_unload @pal end sub ' Count the number of occurrences of a color in a Frame (just the clipped region) function countcolor (fr as Frame ptr, col as integer) as integer if clippedframe <> fr then setclip , , , , fr end if CHECK_FRAME_8BIT(fr, 0) dim ret as integer = 0 for yi as integer = clipt to clipb for xi as integer = clipl to clipr if FRAMEPIXEL(xi, yi, fr) = col then ret += 1 next next return ret end function '========================================================================================== ' Text routines '========================================================================================== function get_font(fontnum as integer, show_err as bool = NO) as Font ptr if fontnum < 0 orelse fontnum > ubound(fonts) orelse fonts(fontnum) = null then if show_err then debugc errPromptBug, "invalid font num " & fontnum end if return fonts(0) else return fonts(fontnum) end if end function 'Pass a string, a 0-based offset of the start of the tag (it is assumed the first two characters have already 'been matched as ${ or \8{ as desired), and action and arg pointers, to fill with the parse results. (Action in UPPERCASE) 'Returns 0 for an invalidly formed tag, otherwise the (0-based) offset of the closing }. function parse_tag(z as string, offset as integer, action as string ptr, arg as int32 ptr) as integer dim closebrace as integer = INSTR((offset + 4) + 1, z, "}") - 1 if closebrace <> -1 then *action = "" dim j as integer for j = 2 to 5 if isalpha(z[offset + j]) then *action += CHR(toupper(z[offset + j])) else exit for end if next 'dim strarg as string = MID(z, offset + j + 1, closebrace - (offset + j)) '*arg = str2int(strarg) 'The C standard lib seems a tad more practical than BASIC's (watch out though, scanf will stab you in the back if it sees a chance) dim brace as byte if isspace(z[offset + j]) orelse sscanf(@z[offset + j], "%d%c", arg, @brace) <> 2 orelse brace <> asc("}") then *action = "" return 0 end if return closebrace end if return 0 end function 'FIXME: refactor, making use of OO which we can now use type PrintStrState 'Public members (may set before passing to render_text) as Font ptr thefont as long fgcolor 'Used when resetting localpal. May be -1 for none as long bgcolor 'Only used if not_transparent as bool not_transparent 'Force non-transparency of layer 1 'Internal members as Font ptr initial_font 'Used when resetting thefont as long leftmargin as long rightmargin as long x as long y as long startx as long charnum 'Internal members used only if drawing, as opposed to laying out/measuring as Palette16 ptr localpal 'NULL if not initialised as long initial_fgcolor 'Used when resetting fgcolor as long initial_bgcolor 'Used when resetting bgcolor as bool initial_not_trans 'Used when resetting bgcolor declare constructor() declare constructor(rhs as PrintStrState) declare destructor() declare sub duplicate_from(rhs as PrintStrState) end type ' Need a default ctor just because there is a copy ctor constructor PrintStrState() end constructor constructor PrintStrState(rhs as PrintStrState) memcpy(@this, @rhs, sizeof(PrintStrState)) if localpal then this.localpal->refcount += 1 end if end constructor 'Unlike the copy ctor, this duplicates the palette instead of incrementing refcount. sub PrintStrState.duplicate_from(rhs as PrintStrState) memcpy(@this, @rhs, sizeof(PrintStrState)) if localpal then this.localpal = Palette16_duplicate(rhs.localpal) this.localpal->refcount = 1 end if end sub destructor PrintStrState() ' Palette16_unload wouldn't actually delete localpal, because it thinks ' it's cached, so sadly we reimplement it 'Palette16_unload @localpal if localpal then localpal->refcount -= 1 if localpal->refcount <= 0 then Palette16_delete @localpal end if end if end destructor 'Special signalling characters #define tcmdFirst 15 #define tcmdState 15 #define tcmdPalette 16 #define tcmdRepalette 17 #define tcmdFont 18 '1 argument: the font number (possibly -1) #define tcmdLast 18 'Invisible argument: state. (member should not be . prefixed, unfortunately) 'Modifies state, and appends a control sequence to the string outbuf to duplicate the change 'Note: in order to support members that are less than 4 bytes (eg palette colours) some hackery is done, and 'members greater than 4 bytes aren't supported #macro UPDATE_STATE(outbuf, member, value) 'Ugh! FB doesn't allow sizeof in #if conditions! #if typeof(state.member) <> integer and typeof(state.member) <> long #error "UPDATE_STATE: bad member type" #endif outbuf += CHR(tcmdState) & " " *Cast(short ptr, @outbuf[len(outbuf) - 6]) = Offsetof(PrintStrState, member) *Cast(long ptr, @outbuf[len(outbuf) - 4]) = Cast(long, value) state.member = value #endmacro 'Interprets a control sequence (at 0-based offset ch in outbuf) written by UPDATE_STATE, 'modifying state. #define MODIFY_STATE(state, outbuf, ch) _ /' dim offset as long = *Cast(short ptr, @outbuf[ch + 1]) '/ _ /' dim newval as long = *Cast(long ptr, @outbuf[ch + 3]) '/ _ *Cast(long ptr, Cast(byte ptr, @state) + *Cast(short ptr, @outbuf[ch + 1])) = _ *Cast(long ptr, @outbuf[ch + 3]) : _ ch += 6 #define APPEND_CMD0(outbuf, cmd_id) _ outbuf += CHR(cmd_id) #define APPEND_CMD1(outbuf, cmd_id, value) _ outbuf += CHR(cmd_id) & " " : _ *Cast(long ptr, @outbuf[len(outbuf) - 4]) = Cast(long, value) #define READ_CMD(outbuf, ch, variable) _ variable = *Cast(long ptr, @outbuf[ch + 1]) : _ ch += 4 'Processes starting from z[state.charnum] until the end of the line, returning a string 'which describes a line fragment. It contains printing characters plus command sequences 'for modifying state. state is passed byval (upon wrapping we would have to undo changes 'to the state, which is too hard). 'endchar is 0 based, and exclusive - normally len(z). FIXME: endchar appears broken 'We also compute the height (height of the tallest font on the line) and the right edge '(max_x) of the line fragment. You have to know the line height before you can know the y 'coordinate of each character on the line. 'Updates to .x, .y are not written because they can be recreated from the character 'stream, and .charnum is not written (unless updatecharnum is true) because it's too 'expensive. However, .x, .y and .charnum are updated at the end. 'If updatecharnum is true, it is updated only when .charnum jumps; you still need to 'increment after every printing character yourself. private function layout_line_fragment(z as string, endchar as integer, byval state as PrintStrState, byref line_width as integer, byref line_height as integer, wide as integer, withtags as bool, withnewlines as bool, updatecharnum as bool = NO) as string dim lastspace as integer = -1 dim lastspace_x as integer dim lastspace_outbuf_len as integer dim lastspace_line_height as integer dim endchar_x as integer 'x at endchar dim endchar_outbuf_len as integer = 999999 'Length of outbuf at endchar dim ch as integer 'We use this instead of modifying .charnum dim visible_chars as integer 'Number non-control chars we will return dim outbuf as string 'Appending characters one at a time to outbuf is slow, so we delay it. 'chars_to_add counts the number of delayed characters dim chars_to_add as integer = 0 with state 'debug "layout '" & z & "' from " & .charnum & " at " & .x & "," & .y line_height = .thefont->h for ch = .charnum to len(z) - 1 if ch = endchar - 1 then 'If the final character is a newline and maybe other cases, need to record this 'debug "hit endchar" endchar_x = .x endchar_outbuf_len = len(outbuf) + chars_to_add end if if z[ch] = 10 and withnewlines then 'newline 'debug "add " & chars_to_add & " chars before " & ch & " : '" & Mid(z, 1 + ch - chars_to_add, chars_to_add) & "'" outbuf += Mid(z, 1 + ch - chars_to_add, chars_to_add) chars_to_add = 0 'Skip past the newline character, but don't add to outbuf ch += 1 if ch >= endchar then 'FIXME: If the final character is a newline, we don't add a blank line. 'But text slices do! We should probably do the same here, e.g. removing 'this if block (and much more work). 'However, it's difficult to change that, due to other functions depending 'this one. outbuf = left(outbuf, endchar_outbuf_len) line_width = endchar_x UPDATE_STATE(outbuf, x, endchar_x) else line_width = .x UPDATE_STATE(outbuf, x, .startx) end if 'Purposefully past endchar UPDATE_STATE(outbuf, charnum, ch) 'Reset margins for next paragraph? No. 'UPDATE_STATE(outbuf, leftmargin, 0) 'UPDATE_STATE(outbuf, rightmargin, wide) return outbuf elseif z[ch] = 8 then ' ^H, hide tag if z[ch + 1] = asc("{") then dim closebrace as integer = instr((ch + 2) + 1, z, "}") - 1 if closebrace <> -1 then 'Add delayed characters first 'debug "add " & chars_to_add & " chars before " & ch & " : '" & Mid(z, 1 + ch - chars_to_add, chars_to_add) & "'" outbuf += Mid(z, 1 + ch - chars_to_add, chars_to_add) chars_to_add = 0 ch = closebrace if updatecharnum then UPDATE_STATE(outbuf, charnum, ch) end if continue for end if end if elseif z[ch] >= tcmdFirst and z[ch] <= tcmdLast then ' special signalling characters. Not allowed! (FIXME: delete this) 'debug "add " & chars_to_add & " chars before " & ch & " : '" & Mid(z, 1 + ch - chars_to_add, chars_to_add) & "'" outbuf += Mid(z, 1 + ch - chars_to_add, chars_to_add) chars_to_add = 0 ch += 1 'skip if updatecharnum then UPDATE_STATE(outbuf, charnum, ch) end if continue for elseif z[ch] = asc("$") then if withtags and z[ch + 1] = asc("{") then dim action as string dim intarg as int32 dim closebrace as integer = parse_tag(z, ch, @action, @intarg) if closebrace then 'Add delayed characters first 'debug "add " & chars_to_add & " chars before " & ch & " : '" & Mid(z, 1 + ch - chars_to_add, chars_to_add) & "'" outbuf += Mid(z, 1 + ch - chars_to_add, chars_to_add) chars_to_add = 0 if action = "F" then 'Font 'Let's preserve the position offset when changing fonts. That way, plain text in 'the middle of edgetext is also offset +1,+1, so that it lines up visually with it '.x += fonts(intarg)->offset.x - .thefont->offset.x '.y += fonts(intarg)->offset.y - .thefont->offset.y if intarg >= -1 andalso intarg <= ubound(fonts) then if intarg = -1 then 'UPDATE_STATE(outbuf, thefont, .initial_font) .thefont = .initial_font elseif fonts(intarg) then 'UPDATE_STATE(outbuf, thefont, fonts(intarg)) .thefont = fonts(intarg) else goto badtexttag end if APPEND_CMD1(outbuf, tcmdFont, intarg) line_height = large(line_height, .thefont->h) else goto badtexttag end if elseif action = "K" then 'Foreground colour dim col as integer if intarg <= -1 then col = .initial_fgcolor elseif intarg <= 255 THEN col = intarg else goto badtexttag end if 'UPDATE_STATE(outbuf, localpal.col(1), col) UPDATE_STATE(outbuf, fgcolor, col) APPEND_CMD0(outbuf, tcmdRepalette) 'No need to update localpal here by calling build_text_palette elseif action = "KB" then 'Background colour dim col as integer if intarg <= -1 then col = .initial_bgcolor if .not_transparent <> .initial_not_trans then UPDATE_STATE(outbuf, not_transparent, .initial_not_trans) end if elseif intarg <= 255 THEN col = intarg if .not_transparent = NO then UPDATE_STATE(outbuf, not_transparent, YES) end if else goto badtexttag end if 'UPDATE_STATE(outbuf, localpal.col(0), col) UPDATE_STATE(outbuf, bgcolor, col) APPEND_CMD0(outbuf, tcmdRepalette) 'No need to update localpal here by calling build_text_palette elseif action = "KP" then 'Font palette if intarg >= 0 and intarg <= gen(genMaxPal) then APPEND_CMD1(outbuf, tcmdPalette, intarg) 'No need up update palette or fgcolor here '(don't want to duplicate that logic here) else goto badtexttag end if elseif action = "LM" then UPDATE_STATE(outbuf, leftmargin, intarg) elseif action = "RM" then UPDATE_STATE(outbuf, rightmargin, wide - intarg) else goto badtexttag end if ch = closebrace if updatecharnum then UPDATE_STATE(outbuf, charnum, ch) end if continue for end if badtexttag: end if elseif z[ch] = asc(" ") then lastspace = ch lastspace_outbuf_len = len(outbuf) + chars_to_add lastspace_x = .x lastspace_line_height = line_height end if .x += .thefont->w(z[ch]) if .x > .startx + .rightmargin then 'debug "rm = " & .rightmargin & " lm = " & .leftmargin if lastspace > -1 and .x - lastspace_x < 3 * (.rightmargin - .leftmargin) \ 5 then 'Split at the last space if chars_to_add then 'debug "add " & chars_to_add & " chars before " & ch & " : '" & Mid(z, 1 + ch - chars_to_add, chars_to_add) & "'" outbuf += Mid(z, 1 + ch - chars_to_add, chars_to_add) end if outbuf = left(outbuf, small(endchar_outbuf_len, lastspace_outbuf_len)) if lastspace < endchar then line_width = lastspace_x UPDATE_STATE(outbuf, x, .startx + .leftmargin) else line_width = endchar_x end if line_height = lastspace_line_height UPDATE_STATE(outbuf, charnum, lastspace + 1) return outbuf else 'Split the word instead, it would just look ugly to break the line if visible_chars = 0 then 'Always output at least one character chars_to_add += 1 ch += 1 end if exit for end if end if 'Add this character to outbuf. But not immediately. chars_to_add += 1 visible_chars += 1 next 'Hit end of text, or splitting word if chars_to_add then 'debug "add " & chars_to_add & " chars before " & ch & " : '" & Mid(z, 1 + ch - chars_to_add, chars_to_add) & "'" outbuf += Mid(z, 1 + ch - chars_to_add, chars_to_add) end if 'Why do we always set x and charnum at the end of the string? if ch <= endchar then 'debug "exiting layout_line_fragment, ch = " & ch & ", .x = " & .x line_width = .x UPDATE_STATE(outbuf, x, .startx + .leftmargin) else 'debug "exiting layout_line_fragment, ch = " & ch & ", endchar_x = " & endchar_x outbuf = left(outbuf, endchar_outbuf_len) line_width = endchar_x UPDATE_STATE(outbuf, x, endchar_x) end if UPDATE_STATE(outbuf, charnum, ch) 'Preserve .leftmargin and .rightmargin return outbuf end with end function 'Build state.localpal sub build_text_palette(byref state as PrintStrState, srcpal as Palette16 ptr) with state if state.localpal = NULL then ' FIXME: This returns a non-refcounted palette16, but we want ' a refcount, which we're forced to manage ourselves (see destructor) state.localpal = Palette16_new() state.localpal->refcount = 1 end if if srcpal then memcpy(@.localpal->col(0), @srcpal->col(0), srcpal->numcolors) .localpal->numcolors = srcpal->numcolors end if .localpal->col(0) = .bgcolor if .fgcolor > -1 then .localpal->col(1) = .fgcolor end if if srcpal = NULL and .fgcolor = -1 then debug "render_text: Drawing a font without a palette or foreground colour!" end if 'debug "build_text_palette: bg = " & .bgcolor & " fg = "& .fgcolor & " outline = " & .thefont->outline_col 'Outline colours are a hack, hopefully temp. if .thefont->outline_col > 0 then .localpal->col(.thefont->outline_col) = uilook(uiOutline) end if end with end sub 'Processes a parsed line, updating the state passed to it, and also optionally draws one of the layers (if reallydraw) sub draw_line_fragment(dest as Frame ptr, byref state as PrintStrState, layer as integer, parsed_line as string, reallydraw as bool) dim arg as integer dim as Frame charframe charframe.mask = NULL charframe.refcount = NOREFC with state 'debug "draw frag: x=" & .x & " y=" & .y & " char=" & .charnum & " reallydraw=" & reallydraw & " layer=" & layer for ch as integer = 0 to len(parsed_line) - 1 if parsed_line[ch] = tcmdState then 'Control sequence. Make a change to state, and move ch past the sequence MODIFY_STATE(state, parsed_line, ch) elseif parsed_line[ch] = tcmdFont then READ_CMD(parsed_line, ch, arg) if arg >= -1 andalso arg <= ubound(fonts) then if arg = -1 then 'UPDATE_STATE(outbuf, thefont, .initial_font) .thefont = .initial_font elseif fonts(arg) then 'UPDATE_STATE(outbuf, thefont, fonts(arg)) .thefont = fonts(arg) else 'This should be impossible, because layout_line_fragment has already checked this debugc errPromptBug, "draw_line_fragment: NULL font!" end if else 'This should be impossible, because layout_line_fragment has already checked this debugc errPromptBug, "draw_line_fragment: invalid font!" end if if reallydraw then 'In case .fgcolor == -1 and .thefont->pal == NULL. Palette changes are per-font, 'so reset the colour. if .fgcolor = -1 then .fgcolor = .initial_fgcolor 'We rebuild the local palette using either the font's palette or from scratch build_text_palette state, .thefont->pal end if elseif parsed_line[ch] = tcmdPalette then READ_CMD(parsed_line, ch, arg) if reallydraw then dim pal as Palette16 ptr pal = Palette16_load(arg) if pal then 'Palettes override the foreground colour (but not background or outline) .fgcolor = -1 build_text_palette state, pal Palette16_unload @pal end if 'FIXME: in fact pal should be kept around, for tcmdRepalette end if elseif parsed_line[ch] = tcmdRepalette then if reallydraw then 'FIXME: if we want to support switching to a non-font palette, then 'that palette should be stored in state and used here build_text_palette state, .thefont->pal end if else 'Draw a character 'Fun hack! Console support if layer = 1 and gfx_printchar <> NULL then gfx_printchar(parsed_line[ch], .x, .y, .fgcolor) end if 'Print one character past the end of the line if reallydraw and .x <= clipr then if .thefont->layers(layer) <> NULL then with .thefont->layers(layer)->chdata(parsed_line[ch]) charframe.image = state.thefont->layers(layer)->spr->image + .offset charframe.w = .w charframe.h = .h charframe.pitch = .w 'debug " <" & (state.x + .offx) & "," & (state.y + .offy) & ">" dim trans as bool = YES 'FIXME: why do we only allow 1-layer fonts to be non transparent? '(2-layer fonts would need layer 0 to be opaque) 'ALSO, this would stuff up ${KB#} on 2-layer fonts if layer = 1 and state.not_transparent then trans = NO frame_draw_internal(@charframe, intpal(), state.localpal, state.x + .offx, state.y + .offy - state.thefont->h, , trans, dest) end with end if end if 'Note: do not use charframe.w, that's just the width of the sprite .x += .thefont->w(parsed_line[ch]) end if next end with end sub 'Draw a string. You will normally want to use one of the friendlier overloads for this, 'probably the most complicated function in the engine. ' 'Arguments: ' 'Pass in a reference to a (fresh!!) PrintStrState object with .thefont and .fgcolor set '.fgcolor can be -1 for no colour (just use font palette). '.not_transparent and .bgcolor (only used if .not_transparent) may also be set ' 'At least one of pal and the (current) font pal and .fgcolor must be not NULL/-1. 'This can be ensured by starting with either a palette or a .fgcolor!=-1 'FIXME: pal is currently disabled; palette handling needs rewriting. ' 'endchar shouldn't be used; currently broken? ' 'If withtags is false then no tags are processed. 'If withtags is true, the follow "basic texttags" are processed: ' (These will change!) '-${F#} changes to font # or return to initial font if # == -1 '-${K#} changes foreground/first colour, or return to initial colour if # == -1 ' (Note that this does disable the foreground colour, unless the initial fg colour was -1!) '-${KB#} changes the background colour, and turns on not_transparent. ' Specify -1 to restore previous background colour and transparency ' FIXME: ${KB0} does NOT switch to transparency, but an initial bgcol of 0 IS transparent! '-${KP#} changes to palette # (-1 is invalid) (Maybe should make ${F-1} return to the default) ' (Note, palette changes are per-font, and expire when the font changes) '-${LM#} sets left margin for the current line, in pixels '-${RM#} sets right margin for the current line, in pixels 'Purposefully no way to set background colour. 'Unrecognised and invalid basic texttags are printed as normal. 'ASCII character 8 can be used to hide texttags by overwriting the $, like so: \008{X#} ' 'Clipping and wrapping: 'If you specify a page width (the default is "infinite"), then text automatically wraps according 'to current margins. Otherwise there is no limit on the right (not even the edge of the screen). 'xpos is the left limit, and xpos+wide is the right limit from which margins are measured (inwards). '(FIXME: why is wide measured relative to xpos?) 'Drawn text is NOT clipped to this region, use setclip or frame_new_view for that. 'This region may be larger than the clip area. 'If withnewlines is true, then newlines (ASCII character 10) are respected 'instead of printed as normal characters. ' 'If you want to skip some number of lines, you should clip, and draw some number of pixels 'above the clipping rectangle. ' sub render_text (dest as Frame ptr, byref state as PrintStrState, text as string, endchar as integer = 999999, xpos as RelPos, ypos as RelPos, wide as RelPos = 999999, pal as Palette16 ptr = NULL, withtags as bool = YES, withnewlines as bool = YES) ', cached_state as PrintStrStatePtr = NULL, use_cached_state as bool = YES) 'static tog as integer = 0 'tog xor= 1 'dim t as double = timer if dest = null then debug "printstr: NULL dest" : exit sub if clippedframe <> dest then setclip , , , , dest end if 'check bounds skipped because this is now quite hard to tell (checked in draw_clipped) 'debug "printstr '" & text & "' (len=" & len(text) & ") wide = " & wide & " tags=" & withtags & " nl=" & withnewlines wide = relative_pos(wide, dest->w) ' Only pre-compute the text dimensions if required for anchoring, as it's quite expensive dim as AlignType xanchor, yanchor, xshow, yshow RelPos_decode xpos, 0, 0, xanchor, xshow RelPos_decode ypos, 0, 0, yanchor, yshow dim finalsize as StringSize if xanchor <> alignLeft or yanchor <> alignLeft or xshow <> alignCenter or yshow <> alignCenter then text_layout_dimensions @finalsize, text, endchar, , wide, state.thefont, withtags, withnewlines end if with state /' if cached_state <> NULL and use_cached_state then state = *cached_state cached_state = NULL else '/ 'if pal then ' build_text_palette state, pal 'else build_text_palette state, .thefont->pal 'end if .initial_font = .thefont .initial_fgcolor = .fgcolor .initial_bgcolor = .bgcolor .initial_not_trans = .not_transparent .charnum = 0 .x = relative_pos(xpos, dest->w, finalsize.w) + .thefont->offset.x .y = relative_pos(ypos, dest->h, finalsize.h) + .thefont->offset.y .startx = .x 'Margins are measured relative to xpos .leftmargin = 0 .rightmargin = wide 'end if dim as bool visibleline 'Draw this line of text? 'We have to process both layers, even if the current font has only one layer, 'in case the string switches to a font that has two! 'Make sure to use a separate Palette16. dim prev_state as PrintStrState prev_state.duplicate_from(state) dim prev_parse as string dim prev_visible as bool dim draw_layer1 as bool = NO 'Don't draw on first loop if endchar > len(text) then endchar = len(text) do dim line_height as integer dim parsed_line as string = layout_line_fragment(text, endchar, state, 0, line_height, wide, withtags, withnewlines) 'debug "parsed: " + parsed_line 'Print at least one extra line above and below the visible region, in case the 'characters are big (we only approximate this policy, with the current font height) visibleline = (.y + line_height > clipt - .thefont->h AND .y < clipb + .thefont->h) 'if tog then visibleline = NO 'debug "vis: " & visibleline 'FIXME: state caching was meant to kick in after the first visible line of text, not here; 'however need to rethink how it should work /' if cached_state then *cached_state = state cached_state = NULL 'Don't save again end if '/ .y += line_height 'Update state while drawing layer 0 (if visible) draw_line_fragment(dest, state, 0, parsed_line, visibleline) if draw_layer1 then 'Now update prev_state (to the beginning of THIS line) while drawing layer 1 'for the previous line. Afterwards, prev_state will be identical to state 'as it was at the start of this loop. draw_line_fragment(dest, prev_state, 1, prev_parse, prev_visible) 'debug "prev.charnum=" & prev_state.charnum if prev_state.charnum >= endchar then /'debug "text end" :'/ exit do if prev_state.y > clipb + prev_state.thefont->h then exit do end if draw_layer1 = YES prev_parse = parsed_line prev_visible = visibleline prev_state.y += line_height loop end with 't = timer - t 'debug "prinstr" & tog & " len " & len(text) & " in " & t*1000 & "ms" end sub 'Calculate size of part of a block of text when drawn, returned in retsize 'NOTE: Edged font has width 1 pixel more than Plain font, due to .offset.x. sub text_layout_dimensions (retsize as StringSize ptr, z as string, endchar as integer = 999999, maxlines as integer = 999999, wide as integer = 999999, fontp as Font ptr, withtags as bool = YES, withnewlines as bool = YES) 'debug "DIMEN char " & endchar dim state as PrintStrState with state '.localpal/?gcolor/initial_?gcolor/transparency non-initialised .thefont = fontp .initial_font = .thefont .charnum = 0 .x = .thefont->offset.x .y = .thefont->offset.y 'Margins are measured relative to xpos .leftmargin = 0 .rightmargin = wide dim maxwidth as integer = 0 dim line_width as integer = 0 dim line_height as integer = 0 retsize->lines = 0 if endchar > len(z) then endchar = len(z) while .charnum < len(z) if .charnum > endchar then exit while 'If .charnum = endchar, the last line is zero length, but should be included. '.charnum won't advance, so need extra check to prevent infinite loop! dim exitloop as bool = (.charnum = endchar) dim parsed_line as string = layout_line_fragment(z, endchar, state, line_width, line_height, wide, withtags, withnewlines) retsize->lines += 1 'debug "parsed a line, line_width =" & line_width maxwidth = large(maxwidth, line_width) 'Update state .y += line_height draw_line_fragment(NULL, state, 0, parsed_line, NO) 'reallydraw=NO 'debug "now " & .charnum & " at " & .x & "," & .y if exitloop then exit while wend retsize->endchar = .charnum retsize->w = maxwidth retsize->h = .y retsize->lastw = line_width retsize->lasth = line_height retsize->finalfont = .thefont 'debug "end DIM char=" & .charnum end with end sub 'Returns the length in pixels of the longest line of a *non-autowrapped* string. function textwidth(text as string, fontnum as integer = fontPlain, withtags as bool = YES, withnewlines as bool = YES) as integer dim retsize as StringSize text_layout_dimensions @retsize, text, , , , get_font(fontnum), withtags, withnewlines return retsize.w end function 'Returns the width and height of an autowrapped string. 'Specify the wrapping width; 'wide' might include rWidth for the width of the screen '(which is what the page arg is for). function textsize(text as string, wide as RelPos = rWidth, fontnum as integer = fontPlain, withtags as bool = YES, page as integer = -1) as XYPair if page = -1 then page = vpage wide = relative_pos(wide, vpages(page)->w) dim retsize as StringSize text_layout_dimensions @retsize, text, , , wide, get_font(fontnum), withtags, YES return XY(retsize.w, retsize.h) end function 'Returns the default height of a line of text of a certain font. 'Warning: this currently returns 10 for 8x8 fonts, because that's what text slices use. Sigh. 'However standardmenu (calc_menustate_size) by default uses 9 for fontEdged and 8 for fontPlain 'and draw_menu by default uses 10. Nonstandard menus use 8-10. function lineheight(fontnum as integer = fontEdged) as integer return get_font(fontnum, YES)->h end function 'Calculate the position at which a certain character in a block of text will be drawn 'FIXME: this returns the wrong position for a space/newline at the end of a line (it 'returns the start of the next line). 'To fix, render_text (or maybe layout_line_fragment?) needs to be changed. sub find_text_char_position(retsize as StringCharPos ptr, text as string, charnum as integer, wide as RelPos = rWidth, fontnum as integer = fontPlain, withtags as bool = YES, page as integer = -1) if page = -1 then page = vpage wide = relative_pos(wide, vpages(page)->w) dim size as StringSize text_layout_dimensions @size, text, charnum, , wide, get_font(fontnum), withtags, YES 'return XY(retsize.lastw, retsize.h - retsize.lasth) with *retsize .charnum = charnum .exacthit = YES 'Maybe return NO if it's at the end of the line? .pos.x = size.lastw .pos.y = size.h - size.lasth .h = size.finalfont->h .lineh = size.lasth end with end sub 'xpos and ypos passed to use same cached state sub find_point_in_text (retsize as StringCharPos ptr, seekx as integer, seeky as integer, z as string, wide as integer = 999999, xpos as integer = 0, ypos as integer = 0, fontnum as integer, withtags as bool = YES, withnewlines as bool = YES) dim state as PrintStrState with state '.localpal/?gcolor/initial_?gcolor/transparency non-initialised .thefont = get_font(fontnum) .initial_font = .thefont .charnum = 0 .x = xpos + .thefont->offset.x .y = ypos + .thefont->offset.y 'Margins are measured relative to xpos .leftmargin = 0 .rightmargin = wide dim delayedmatch as bool = NO dim line_width as integer dim line_height as integer dim arg as integer retsize->exacthit = NO 'retsize->w = .thefont->h 'Default for if we go off the end of the text while .charnum < len(z) dim parsed_line as string = layout_line_fragment(z, len(z), state, line_width, line_height, wide, withtags, withnewlines, YES) .y += line_height '.y now points to 1 pixel past the bottom of the line fragment 'Update state for ch as integer = 0 to len(parsed_line) - 1 if parsed_line[ch] = tcmdState then 'Make a change to the state .charnum += 1 'FIXME: this looks wrong MODIFY_STATE(state, parsed_line, ch) elseif parsed_line[ch] = tcmdFont then READ_CMD(parsed_line, ch, arg) .thefont = fonts(arg) elseif parsed_line[ch] = tcmdPalette then READ_CMD(parsed_line, ch, arg) else dim w as integer = .thefont->w(parsed_line[ch]) 'Draw a character if delayedmatch then 'retsize->w = w exit while end if .x += w if .y > seeky and .x > seekx then 'debug "FIND IN: hit w/ x = " & .x 'retsize->w = w retsize->exacthit = YES .x -= w exit while end if .charnum += 1 end if next if .y > seeky then 'Position was off the end of the line if .charnum > 0 then dim lastchar as ubyte = z[.charnum - 1] if lastchar = 32 or (lastchar = 10 andalso withnewlines) then 'This point is actually on a space/newline, which was 'not added to parsed_string. So don't delay. retsize->exacthit = YES .x = line_width .charnum -= 1 exit while end if end if delayedmatch = YES 'debug "FIND IN: delayed" end if wend retsize->charnum = .charnum retsize->pos.x = .x retsize->pos.y = .y - .thefont->h retsize->h = .thefont->h retsize->lineh = line_height end with end sub 'the old printstr -- no autowrapping sub printstr (text as string, x as RelPos, y as RelPos, page as integer, withtags as bool = NO, fontnum as integer = fontPlain) dim state as PrintStrState state.thefont = get_font(fontnum) if textbg <> 0 then state.not_transparent = YES state.bgcolor = textbg state.fgcolor = textfg render_text (vpages(page), state, text, , x, y, , , withtags, NO) end sub 'this doesn't autowrap either sub edgeprint (text as string, x as RelPos, y as RelPos, col as integer, page as integer, withtags as bool = NO, withnewlines as bool = NO) 'preserve the old behaviour (edgeprint used to call textcolor) textfg = col textbg = 0 dim state as PrintStrState state.thefont = fonts(fontEdged) state.fgcolor = col render_text (vpages(page), state, text, , x, y, , , withtags, withnewlines) end sub 'A flexible edgeprint/printstr replacement. 'Either specify the colour, or omit it and use textcolor(). 'Wraps the text at 'wide'; pass "rWidth - x" to wrap at the right edge of the screen. sub wrapprint (text as string, x as RelPos, y as RelPos, col as integer = -1, page as integer, wide as RelPos = rWidth, withtags as bool = YES, fontnum as integer = fontEdged) dim state as PrintStrState state.thefont = fonts(fontnum) if col = -1 then state.fgcolor = textfg state.bgcolor = textbg if textbg <> 0 then state.not_transparent = YES else state.fgcolor = col state.bgcolor = 0 end if render_text (vpages(page), state, text, , x, y, wide, , withtags, YES) end sub sub textcolor (fg as integer, bg as integer) textfg = fg textbg = bg end sub function fgcol_text(text as string, colour as integer) as string return "${K" & colour & "}" & text & "${K-1}" end function function bgcol_text(text as string, colour as integer) as string return "${KB" & colour & "}" & text & "${KB-1}" end function '========================================================================================== ' Fonts '========================================================================================== 'This deletes a Font object pointed to by a pointer. It's OK to call on a ptr to a NULL ptr sub font_unload (fontpp as Font ptr ptr) if fontpp = null then showerror "font_unload: passed NULL" : exit sub dim fontp as font ptr = *fontpp if fontp = null then exit sub for i as integer = 0 to 1 if fontp->layers(i) then fontp->layers(i)->refcount -= 1 if fontp->layers(i)->refcount <= 0 then frame_unload @fontp->layers(i)->spr deallocate(fontp->layers(i)) end if fontp->layers(i) = NULL end if next Palette16_unload @fontp->pal deallocate fontp *fontpp = NULL end sub 'Doesn't create a Frame private function fontlayer_new () as FontLayer ptr dim ret as FontLayer ptr ret = callocate(sizeof(FontLayer)) ret->refcount = 1 return ret end function private function fontlayer_duplicate (srclayer as FontLayer ptr) as FontLayer ptr dim ret as FontLayer ptr ret = callocate(sizeof(FontLayer)) memcpy(ret, srclayer, sizeof(FontLayer)) ret->spr = frame_duplicate(srclayer->spr) ret->refcount = 1 return ret end function 'Create a version of a font with an outline around each character (in a new palette colour) function font_create_edged (basefont as Font ptr) as Font ptr if basefont = null then debugc errPromptBug, "font_create_edged wasn't passed a font!" return null end if if basefont->layers(1) = null then debugc errPromptBug, "font_create_edged was passed a blank font!" return null end if CHECK_FRAME_8BIT(basefont->layers(1)->spr, NULL) dim newfont as Font ptr = callocate(sizeof(Font)) newfont->layers(0) = fontlayer_new() 'Share layer 1 newfont->layers(1) = basefont->layers(1) newfont->layers(1)->refcount += 1 dim size as integer 'since you can only WITH one thing at a time dim bchr as FontChar ptr bchr = @basefont->layers(1)->chdata(0) dim as integer ch for ch = 0 to 255 newfont->w(ch) = basefont->w(ch) with newfont->layers(0)->chdata(ch) .offset = size .offx = bchr->offx - 1 .offy = bchr->offy - 1 .w = bchr->w + 2 .h = bchr->h + 2 size += .w * .h end with bchr += 1 next 'This is a hack; create a size*1 size frame, which we use as a buffer for pixel data newfont->layers(0)->spr = frame_new(size, 1, , YES) newfont->h = basefont->h '+ 2 newfont->offset = basefont->offset newfont->cols = basefont->cols if basefont->outline_col = 0 then 'Doesn't already have an outline colour newfont->cols += 1 newfont->outline_col = newfont->cols else newfont->outline_col = basefont->outline_col end if 'Stuff currently hardcoded to keep edged font working as before newfont->offset.x = 1 newfont->offset.y = 1 'newfont->h += 2 'dim as ubyte ptr maskp = basefont->layers(0)->spr->mask dim as ubyte ptr sptr dim as ubyte ptr srcptr = newfont->layers(1)->spr->image dim as integer x, y for ch = 0 to 255 with newfont->layers(0)->chdata(ch) sptr = newfont->layers(0)->spr->image + .offset + .w + 1 for y = 1 to .h - 2 for x = 1 to .w - 2 if *srcptr then sptr[-.w + 0] = newfont->outline_col sptr[ 0 - 1] = newfont->outline_col sptr[ 0 + 1] = newfont->outline_col sptr[ .w + 0] = newfont->outline_col end if 'if *sptr = 0 then *maskp = 0 else *maskp = &hff sptr += 1 srcptr += 1 'maskp += 8 next sptr += 2 next end with next return newfont end function 'Create a version of a font with a drop shadow (in a new palette colour) function font_create_shadowed (basefont as Font ptr, xdrop as integer = 1, ydrop as integer = 1) as Font ptr if basefont = null then debug "createshadowfont wasn't passed a font!" return null end if if basefont->layers(1) = null then debug "createshadowfont was passed a blank font!" return null end if CHECK_FRAME_8BIT(basefont->layers(1)->spr, NULL) dim newfont as Font ptr = callocate(sizeof(Font)) memcpy(newfont, basefont, sizeof(Font)) 'Copy layer 1 from the old font to layer 0 of the new newfont->layers(0) = fontlayer_duplicate(basefont->layers(1)) 'Share layer 1 with the base font newfont->layers(1)->refcount += 1 if newfont->outline_col = 0 then 'Doesn't already have an outline colour newfont->cols += 1 newfont->outline_col = newfont->cols end if for ch as integer = 0 to 255 with newfont->layers(0)->chdata(ch) .offx += xdrop .offy += ydrop end with next with *newfont->layers(0)->spr for i as integer = 0 to .w * .h - 1 if .image[i] then .image[i] = newfont->outline_col end if next end with return newfont end function function font_loadold1bit (fontdata as ubyte ptr) as Font ptr dim newfont as Font ptr = callocate(sizeof(Font)) newfont->layers(1) = fontlayer_new() newfont->layers(1)->spr = frame_new(8, 256 * 8) newfont->h = 10 'I would have said 9, but this is what was used in text slices newfont->offset.x = 0 newfont->offset.y = 0 newfont->cols = 1 newfont->outline_col = 0 'None 'dim as ubyte ptr maskp = newfont->layers(1)->spr->mask dim as ubyte ptr sptr = newfont->layers(1)->spr->image dim as integer ch, x, y dim as integer fi 'font index dim as integer fstep for ch = 0 to 255 newfont->w(ch) = 8 with newfont->layers(1)->chdata(ch) .w = 8 .h = 8 .offset = 64 * ch end with 'find fontdata index, bearing in mind that the data is stored '2-bytes at a time in 4-byte integers, due to QB->FB quirks, 'and fontdata itself is a byte pointer. Because there are 'always 8 bytes per character, we will always use exactly 4 'ints, or 16 bytes, making the initial calc pretty simple. fi = ch * 16 'fi = ch * 8 'index to fontdata fstep = 1 'used because our indexing is messed up, see above for x = 0 to 7 for y = 0 to 7 *sptr = (fontdata[fi] shr y) and 1 'if *sptr = 0 then *maskp = 0 else *maskp = &hff sptr += 8 'maskp += 8 next fi = fi + fstep fstep = iif(fstep = 1, 3, 1) 'uneven steps due to 2->4 byte thunk sptr += 1 - 8 * 8 'maskp += 1 - 8 * 8 next sptr += 8 * 8 - 8 'maskp += 8 * 8 - 8 next return newfont end function 'Load each character from an individual BMP in a directory, falling back to some other 'font for missing BMPs 'This function is for testing purposes only, and will be removed unless this shows some use: 'uses hardcoded values function font_loadbmps (directory as string, fallback as Font ptr = null) as Font ptr dim newfont as Font ptr = callocate(sizeof(Font)) newfont->layers(0) = null newfont->layers(1) = fontlayer_new() 'Hacky: start by allocating 4096 pixels, expand as needed newfont->layers(1)->spr = frame_new(1, 4096) newfont->cols = 1 'hardcoded newfont->outline_col = 0 'None dim maxheight as integer if fallback then maxheight = fallback->h newfont->offset.x = fallback->offset.x newfont->offset.y = fallback->offset.y newfont->cols = fallback->cols end if dim as ubyte ptr image = newfont->layers(1)->spr->image dim as ubyte ptr sptr dim as integer size = 0 dim as integer i dim f as string dim tempfr as Frame ptr dim bchr as FontChar ptr bchr = @fallback->layers(1)->chdata(0) for i = 0 to 255 with newfont->layers(1)->chdata(i) f = finddatafile(directory & SLASH & i & ".bmp", NO) if isfile(f) then 'FIXME: awful stuff tempfr = frame_import_bmp_raw(f) ', master()) .offset = size .offx = 0 .offy = 0 .w = tempfr->w .h = tempfr->h if .h > maxheight then maxheight = .h newfont->w(i) = .w size += .w * .h image = reallocate(image, size) sptr = image + .offset memcpy(sptr, tempfr->image, .w * .h) frame_unload @tempfr else if fallback = null ORELSE fallback->layers(1) = null then debug "font_loadbmps: " & i & ".bmp missing and fallback font not provided" font_unload @newfont return null end if .offset = size .offx = bchr->offx .offy = bchr->offy .w = bchr->w .h = bchr->h newfont->w(i) = .w size += .w * .h image = reallocate(image, size) memcpy(image + .offset, fallback->layers(1)->spr->image + bchr->offset, .w * .h) end if end with bchr += 1 next newfont->layers(1)->spr->image = image newfont->h = maxheight return newfont end function 'Load a font from a BMP which contains all 256 characters in a 16x16 grid (all characters the same size) function font_loadbmp_16x16 (filename as string) as Font ptr dim bmp as Frame ptr bmp = frame_import_bmp_raw(filename) if bmp = NULL then debug "font_loadbmp_16x16: couldn't load " & filename return null end if if bmp->w MOD 16 ORELSE bmp->h MOD 16 then debug "font_loadbmp_16x16: " & filename & ": bad dimensions " & bmp->w & "*" & bmp->h frame_unload @bmp return null end if dim newfont as Font ptr = callocate(sizeof(Font)) dim as integer charw, charh charw = bmp->w \ 16 charh = bmp->h \ 16 newfont->h = charh newfont->offset.x = 0 newfont->offset.y = 0 newfont->outline_col = 0 'None newfont->layers(0) = null newfont->layers(1) = fontlayer_new() '"Linearise" the characters. In future this will be unnecessary newfont->layers(1)->spr = frame_new(charw, charh * 256) dim as integer size = 0 for i as integer = 0 to 255 with newfont->layers(1)->chdata(i) .offset = size .offx = 0 .offy = 0 .w = charw .h = charh newfont->w(i) = .w size += .w * .h dim tempview as Frame ptr tempview = frame_new_view(bmp, charw * (i MOD 16), charh * (i \ 16), charw, charh) 'setclip , charh * i, , charh * (i + 1) - 1, newfont->layers(1)->spr frame_draw tempview, , 0, charh * i, 1, NO, newfont->layers(1)->spr frame_unload @tempview end with next 'Find number of used colours newfont->cols = 0 dim as ubyte ptr image = bmp->image for i as integer = 0 to bmp->pitch * bmp->h - 1 if image[i] > newfont->cols then newfont->cols = image[i] next frame_unload @bmp return newfont end function sub setfont (ohf_font() as integer) font_unload @fonts(fontPlain) font_unload @fonts(fontEdged) font_unload @fonts(fontShadow) fonts(fontPlain) = font_loadold1bit(cast(ubyte ptr, @ohf_font(0))) fonts(fontEdged) = font_create_edged(fonts(fontPlain)) fonts(fontShadow) = font_create_shadowed(fonts(fontPlain), 1, 2) end sub 'NOTE: the following two functions are for the old style fonts, they will 'be removed when switching to the new system supporting unicode fonts 'These old style fonts store the type of the font in first integer (part of character '0). The default "Latin-1.ohf" and "OHRRPGCE Default.ohf" fonts are marked as Latin 1, so 'any font derived from them will be too (ability to change the type only added in Callipygous) function get_font_type (ohf_font() as integer) as fontTypeEnum if ohf_font(0) <> ftypeASCII and ohf_font(0) <> ftypeLatin1 then debugc errPromptBug, "Unknown font type ID " & ohf_font(0) return ftypeASCII end if return ohf_font(0) end function sub set_font_type (ohf_font() as integer, ty as fontTypeEnum) if ty <> ftypeASCII and ty <> ftypeLatin1 then debugc errPromptBug, "set_font_type: bad type " & ty end if ohf_font(0) = ty end sub '========================================================================================== ' BMP routines '========================================================================================== 'other formats are probably quite simple 'with Allegro or SDL or FreeImage, but we'll stick to this for now. sub surface_export_bmp (f as string, surf as Surface Ptr, maspal() as RGBcolor) if surf->format = SF_32bit then surface_export_bmp24(f, surf) else 'A wrapper dim fr as Frame fr.w = surf->width fr.h = surf->height fr.pitch = surf->pitch fr.image = surf->pPaletteData fr.mask = surf->pPaletteData frame_export_bmp8(f, @fr, maspal()) end if end sub sub surface_export_bmp24 (f as string, surf as Surface Ptr) dim argb as RGBQUAD dim as integer of, y, i, skipbytes dim as RGBcolor ptr sptr dim as ubyte buf(3) if surf->format <> SF_32bit then showerror "surface_export_bmp24 got 8bit Surface" exit sub end if of = write_bmp_header(f, surf->width, surf->height, 24) if of = -1 then exit sub skipbytes = 4 - (surf->width * 3 mod 4) if skipbytes = 4 then skipbytes = 0 sptr = surf->pColorData + (surf->height - 1) * surf->pitch for y = surf->height - 1 to 0 step -1 'put is possibly the most screwed up FB builtin; the use of the fput wrapper soothes the soul for x as integer = 0 to surf->width - 1 fput(of, , @sptr[x], 3) next sptr -= surf->pitch 'pad to 4-byte boundary fput(of, , @buf(0), skipbytes) next close #of end sub sub frame_export_bmp8 (f as string, fr as Frame Ptr, maspal() as RGBcolor) dim argb as RGBQUAD dim as integer of, y, i, skipbytes dim as ubyte ptr sptr CHECK_FRAME_8BIT(fr) of = write_bmp_header(f, fr->w, fr->h, 8) if of = -1 then exit sub for i = 0 to 255 argb.rgbRed = maspal(i).r argb.rgbGreen = maspal(i).g argb.rgbBlue = maspal(i).b put #of, , argb next skipbytes = 4 - (fr->w mod 4) if skipbytes = 4 then skipbytes = 0 sptr = fr->image + (fr->h - 1) * fr->pitch for y = fr->h - 1 to 0 step -1 'put is possibly the most screwed up FB builtin; the use of the fput wrapper soothes the soul fput(of, , sptr, fr->w) 'equivalent to "put #of, , *sptr, fr->w" sptr -= fr->pitch 'write some interesting dummy data fput(of, , fr->image, skipbytes) next close #of end sub sub frame_export_bmp4 (f as string, fr as Frame Ptr, maspal() as RGBcolor, pal as Palette16 ptr) dim argb as RGBQUAD dim as integer of, x, y, i, skipbytes dim as ubyte ptr sptr dim as ubyte pix CHECK_FRAME_8BIT(fr) of = write_bmp_header(f, fr->w, fr->h, 4) if of = -1 then exit sub for i = 0 to 15 argb.rgbRed = maspal(pal->col(i)).r argb.rgbGreen = maspal(pal->col(i)).g argb.rgbBlue = maspal(pal->col(i)).b put #of, , argb next skipbytes = 4 - ((fr->w / 2) mod 4) if skipbytes = 4 then skipbytes = 0 sptr = fr->image + (fr->h - 1) * fr->pitch for y = fr->h - 1 to 0 step -1 for x = 0 to fr->w - 1 if (x and 1) = 0 then pix = sptr[x] shl 4 else pix or= sptr[x] put #of, , pix end if next if fr->w mod 2 then put #of, , pix end if sptr -= fr->pitch 'write some interesting dummy data fput(of, , fr->image, skipbytes) next close #of end sub ' Generic 4/8/24-bit BMP export sub frame_export_bmp (fname as string, fr as Frame ptr, maspal() as RGBcolor, pal as Palette16 ptr = NULL) if pal then frame_export_bmp4 fname, fr, maspal(), pal elseif fr->surf then ' todo: 8-bit surfaces surface_export_bmp24 fname, fr->surf else frame_export_bmp8 fname, fr, maspal() end if end sub 'Creates a new file and writes the bmp headers to it. 'Returns a file handle, or -1 on error. private function write_bmp_header(filen as string, w as integer, h as integer, bitdepth as integer) as integer dim header as BITMAPFILEHEADER dim info as BITMAPINFOHEADER dim as integer of, imagesize, imageoff imagesize = ((w * bitdepth + 31) \ 32) * 4 * h imageoff = 54 if bitdepth <= 8 then imageoff += (1 shl bitdepth) * 4 end if header.bfType = 19778 header.bfSize = imageoff + imagesize header.bfReserved1 = 0 header.bfReserved2 = 0 header.bfOffBits = imageoff info.biSize = 40 info.biWidth = w info.biHeight = h info.biPlanes = 1 info.biBitCount = bitdepth info.biCompression = BI_RGB info.biSizeImage = imagesize info.biXPelsPerMeter = &hB12 info.biYPelsPerMeter = &hB12 info.biClrUsed = 1 shl bitdepth info.biClrImportant = 1 shl bitdepth if openfile(filen, for_binary + access_write, of) then 'Truncate debugc errError, "write_bmp_header: couldn't open " & filen return -1 end if put #of, , header put #of, , info return of end function 'Open a BMP file, read its headers, and return a file handle (>= 0), 'or -1 if invalid, or -2 if unsupported. 'Only 1, 4, 8, 24, and 32 bit BMPs are accepted 'Afterwards, the file is positioned at the start of the palette, if there is one function open_bmp_and_read_header(bmp as string, byref header as BITMAPFILEHEADER, byref info as BITMAPV3INFOHEADER) as integer dim bf as integer if openfile(bmp, for_binary + access_read, bf) then debug "open_bmp_and_read_header: couldn't open " & bmp return -1 end if get #bf, , header if header.bfType <> 19778 then close #bf debuginfo bmp & " is not a valid BMP file" return -1 end if dim bisize as integer get #bf, , bisize seek #bf, seek(bf) - 4 if biSize = 12 then 'debuginfo "Ancient BMP2 file" dim info_old as BITMAPCOREHEADER get #bf, , info_old info.biSize = biSize info.biCompression = BI_RGB info.biBitCount = info_old.bcBitCount info.biWidth = info_old.bcWidth info.biHeight = info_old.bcHeight elseif biSize < 40 then close #bf debuginfo "Unsupported DIB header size " & biSize & " in " & bmp return -2 else 'A BITMAPINFOHEADER or one of its extensions get #bf, , info if biSize >= 56 then 'BITMAPV3INFOHEADER or one of its extensions 'We don't support any of those extension features but none of them are important elseif biSize = 52 then 'BITMAPV2INFOHEADER, alpha bitmask doesn't exist info.biAlphaMask = 0 else 'Assumably BITMAPINFOHEADER info.biRedMask = 0 info.biGreenMask = 0 info.biBlueMask = 0 info.biAlphaMask = 0 end if end if if info.biClrUsed <= 0 and info.biBitCount <= 8 then info.biClrUsed = 1 shl info.biBitCount end if 'debuginfo bmp & " header size: " & bisize & " size: " & info.biWidth & "*" & info.biHeight & " bitdepth: " & info.biBitCount & " compression: " & info.biCompression & " colors: " & info.biClrUsed select case info.biBitCount case 1, 4, 8, 24, 32 case else close #bf debuginfo "Unsupported bitdepth " & info.biBitCount & " in " & bmp if info.biBitCount = 2 or info.biBitcount = 16 then return -2 else 'Invalid return -1 end if end select if (info.biCompression = BI_RLE4 and info.biBitCount <> 4) or (info.biCompression = BI_RLE8 and info.biBitCount <> 8) then close #bf debuginfo "Invalid compression scheme " & info.biCompression & " in " & info.biBitCount & "bpp BMP " & bmp return -1 end if if info.biCompression = BI_BITFIELDS and info.biBitCount = 32 then '16 bit (but not 24 bit) BMPs can also use BI_BITFIELDS, but we don't support them. 'Check whether the bitmasks are simple 8 bit masks, aside from the alpha 'mask, which can be 0 (not present) if decode_bmp_bitmask(info.biRedMask) = -1 or _ decode_bmp_bitmask(info.biGreenMask) = -1 or _ decode_bmp_bitmask(info.biBlueMask) = -1 or _ (info.biAlphaMask <> 0 and decode_bmp_bitmask(info.biAlphaMask) = -1) then close #bf debuginfo "Unsupported BMP RGBA bitmasks " & _ HEX(info.biRedMask) & " " & _ HEX(info.biGreenMask) & " " & _ HEX(info.biBlueMask) & " " & _ HEX(info.biAlphaMask) & _ " in 32-bit " & bmp return -2 end if elseif info.biCompression <> BI_RGB and info.biCompression <> BI_RLE4 and info.biCompression <> BI_RLE8 then close #bf debuginfo "Unsupported BMP compression scheme " & info.biCompression & " in " & info.biBitCount & "-bit BMP " & bmp return -2 end if if info.biHeight < 0 then 'A negative height indicates that the image is not stored upside-down. Unimplemented close #bf debuginfo "Unsupported non-flipped image in " & bmp return -2 end if 'Seek to palette '(some extra data might sit between the header and the palette only if the compression is BI_BITFIELDS seek #bf, 1 + sizeof(BITMAPFILEHEADER) + biSize return bf end function 'Loads any supported .bmp file as a Surface, returning NULL on error. 'always_32bit: load paletted BMPs as 32 bit Surfaces instead of 8-bit ones '(in the latter case, you have to load the palette yourself). 'The alpha channel if any is ignored function surface_import_bmp(bmp as string, always_32bit as bool) as Surface ptr dim header as BITMAPFILEHEADER dim info as BITMAPV3INFOHEADER dim bf as integer bf = open_bmp_and_read_header(bmp, header, info) if bf <= -1 then return 0 'navigate to the beginning of the bitmap data seek #bf, header.bfOffBits + 1 dim ret as Surface ptr if info.biBitCount < 24 then dim paletted as Frame ptr paletted = frame_import_bmp_raw(bmp) if paletted then if always_32bit then dim bmppal(255) as RGBcolor loadbmppal(bmp, bmppal()) ' Convert it to 32bit ret = frame_to_surface32(paletted, bmppal()) else ' Keep 8-bit. We don't load the palette gfx_surfaceCreateFrameView(paletted, @ret) 'Increments refcount end if frame_unload @paletted end if else gfx_surfaceCreate(info.biWidth, info.biHeight, SF_32bit, SU_Staging, @ret) if info.biBitCount = 24 then loadbmp24(bf, ret) elseif info.biBitCount = 32 then loadbmp32(bf, ret, info) end if end if close #bf return ret end function 'Loads and palettises the 24-bit or 32-bit bitmap BMP, mapped to palette pal(). 'If there is an alpha channel, fully transparent pixels are mapped to index 0. function frame_import_bmp24_or_32(bmp as string, pal() as RGBcolor, options as QuantizeOptions = TYPE(0, -1)) as Frame ptr dim surf as Surface ptr surf = surface_import_bmp(bmp, YES) if surf = NULL then return NULL return quantize_surface(surf, pal(), options) end function 'Loads any bmp file as an (optionally transparent) 8-bit Frame (ie. with no Palette16), 'remapped to the given master palette; NULL on error. '24 and 32 bit BMPs will have RGB pixels equal to the 'transparency' color (transparency.a should be 0) 'mapped to masterpal() index 0 (by default nothing); 'keep_col0' is ignored. 'Also, in 32 bit BMPs with an alpha channel, fully transparent pixels are mapped to index 0. '8-or-fewer-bit BMPs get palette index 0 mapped to color 0 if 'keep_col0' is true, 'otherwise they have no color 0 pixels; 'transparency' is ignored. function frame_import_bmp_as_8bit(bmpfile as string, masterpal() as RGBcolor, keep_col0 as bool = YES, byval transparency as RGBcolor = TYPE(-1)) as Frame ptr dim info as BITMAPV3INFOHEADER if bmpinfo(bmpfile, info) <> 2 then ' Unreadable, invalid, or unsupported return NULL end if if info.biBitCount <= 8 then dim ret as Frame ptr ret = frame_import_bmp_raw(bmpfile) if ret = NULL then return NULL ' Drop the palette, remapping to the master palette ' (Can't use frame_draw, since we have an array instead of a Palette16) dim palindices(255) as integer convertbmppal(bmpfile, masterpal(), palindices(), 1) if keep_col0 then palindices(0) = 0 end if for y as integer = 0 to ret->h - 1 dim pixptr as ubyte ptr = @FRAMEPIXEL(0, y, ret) for x as integer = 0 to ret->w - 1 pixptr[x] = palindices(pixptr[x]) next next return ret else dim options as QuantizeOptions = (1, transparency) return frame_import_bmp24_or_32(bmpfile, masterpal(), options) end if end function sub bitmap2pal (bmp as string, pal() as RGBcolor) 'loads the 24/32-bit 16x16 palette bitmap bmp into palette pal() 'so, pixel (0,0) holds colour 0, (0,1) has colour 16, and (15,15) has colour 255 dim header as BITMAPFILEHEADER dim info as BITMAPV3INFOHEADER dim col as RGBTRIPLE dim bf as integer dim dummy as ubyte dim as integer w, h bf = open_bmp_and_read_header(bmp, header, info) if bf <= -1 then exit sub if info.biBitCount < 24 OR info.biWidth <> 16 OR info.biHeight <> 16 then close #bf debug "bitmap2pal should not have been called!" exit sub end if 'navigate to the beginning of the bitmap data seek #bf, header.bfOffBits + 1 for h = 15 to 0 step -1 for w = 0 to 15 'read the data get #bf, , col pal(h * 16 + w).r = col.rgbtRed pal(h * 16 + w).g = col.rgbtGreen pal(h * 16 + w).b = col.rgbtBlue next if info.biBitCount = 32 then get #bf, , dummy end if next close #bf end sub function frame_import_bmp_raw(bmp as string) as Frame ptr 'load a 1-, 4- or 8-bit .BMP, ignoring the palette dim header as BITMAPFILEHEADER dim info as BITMAPV3INFOHEADER dim bf as integer dim ret as Frame ptr bf = open_bmp_and_read_header(bmp, header, info) if bf <= -1 then return 0 if info.biBitCount > 8 then close #bf debugc errPromptBug, "frame_import_bmp_raw should not have been called!" return 0 end if 'use header offset to get to data seek #bf, header.bfOffBits + 1 ret = frame_new(info.biWidth, info.biHeight, , YES) if info.biBitCount = 1 then loadbmp1(bf, ret) elseif info.biBitCount = 4 then 'call one of two loaders depending on compression if info.biCompression = BI_RGB then loadbmp4(bf, ret) elseif info.biCompression = BI_RLE4 then frame_clear(ret) loadbmprle4(bf, ret) else debug "frame_import_bmp_raw should not have been called, bad 4-bit compression" end if else if info.biCompression = BI_RGB then loadbmp8(bf, ret) elseif info.biCompression = BI_RLE8 then frame_clear(ret) loadbmprle8(bf, ret) else debug "frame_import_bmp_raw should not have been called, bad 8-bit compression" end if end if close #bf return ret end function 'Given a mask with 8 consecutive bits such as &hff00 returns the number of zero 'bits to the right of the bits. Returns -1 if the mask isn't of this form. private function decode_bmp_bitmask(mask as uint32) as integer for shift as integer = 0 to 24 if mask shr shift = &hFF then return shift end if next return -1 end function 'Takes an open file handle pointing at start of pixel data and an already sized Surface to load into private sub loadbmp32(bf as integer, surf as Surface ptr, infohd as BITMAPV3INFOHEADER) dim bitspix as uint32 dim quadpix as RGBQUAD dim sptr as RGBcolor ptr dim tempcol as RGBcolor dim as integer rshift, gshift, bshift, ashift tempcol.a = 255 'Opaque if infohd.biCompression = BI_BITFIELDS then ' The bitmasks have already been verified to be supported, except ' alpha might be missing rshift = decode_bmp_bitmask(infohd.biRedMask) gshift = decode_bmp_bitmask(infohd.biGreenMask) bshift = decode_bmp_bitmask(infohd.biBlueMask) ashift = decode_bmp_bitmask(infohd.biAlphaMask) end if for y as integer = surf->height - 1 to 0 step -1 sptr = surf->pColorData + y * surf->pitch for x as integer = 0 to surf->width - 1 if infohd.biCompression = BI_BITFIELDS then get #bf, , bitspix tempcol.r = bitspix shr rshift tempcol.g = bitspix shr gshift tempcol.b = bitspix shr bshift if ashift <> -1 then tempcol.a = bitspix shr ashift end if *sptr = tempcol else 'Layout of RGBQUAD and RGBcolor are the same get #bf, , quadpix *sptr = *cast(RGBcolor ptr, @quadpix) end if sptr += 1 next next end sub 'Takes an open file handle pointing at start of pixel data and an already sized Surface to load into private sub loadbmp24(bf as integer, surf as Surface ptr) dim pix as RGBTRIPLE dim ub as ubyte dim sptr as RGBcolor ptr dim pad as integer 'data lines are padded to 32-bit boundaries pad = 4 - ((surf->width * 3) mod 4) if pad = 4 then pad = 0 for y as integer = surf->height - 1 to 0 step -1 sptr = surf->pColorData + y * surf->pitch for x as integer = 0 to surf->width - 1 get #bf, , pix 'First 3 bytes of RGBTRIPLE are the same as RGBcolor *sptr = *cast(RGBcolor ptr, @pix) sptr->a = 255 sptr += 1 next 'padding to dword boundary for w as integer = 0 to pad-1 get #bf, , ub next next end sub private sub loadbmp8(bf as integer, fr as Frame ptr) 'takes an open file handle and an already size Frame pointer, should only be called within loadbmp dim ub as ubyte dim as integer w, h dim sptr as ubyte ptr dim pad as integer pad = 4 - (fr->w mod 4) if pad = 4 then pad = 0 for h = fr->h - 1 to 0 step -1 sptr = fr->image + h * fr->pitch for w = 0 to fr->w - 1 'read the data get #bf, , ub *sptr = ub sptr += 1 next 'padding to dword boundary for w = 0 to pad-1 get #bf, , ub next next end sub private sub loadbmp4(bf as integer, fr as Frame ptr) 'takes an open file handle and an already size Frame pointer, should only be called within loadbmp dim ub as ubyte dim as integer w, h dim sptr as ubyte ptr dim pad as integer dim numbytes as integer = (fr->w + 1) \ 2 'per line pad = 4 - (numbytes mod 4) if pad = 4 then pad = 0 for h = fr->h - 1 to 0 step -1 sptr = fr->image + h * fr->pitch for w = 0 to fr->w - 1 if (w and 1) = 0 then 'read the data get #bf, , ub *sptr = (ub and &hf0) shr 4 else '2nd nybble in byte *sptr = ub and &h0f end if sptr += 1 next 'padding to dword boundary for w = 0 to pad - 1 get #bf, , ub next next end sub private sub loadbmprle4(bf as integer, fr as Frame ptr) 'takes an open file handle and an already size Frame pointer, should only be called within loadbmp dim pix as ubyte dim ub as ubyte dim as integer w, h dim i as integer dim as ubyte bval, v1, v2 w = 0 h = fr->h - 1 'read bytes until we're done while not eof(bf) 'get command byte get #bf, , ub select case ub case 0 'special, check next byte get #bf, , ub select case ub case 0 'end of line w = 0 h -= 1 case 1 'end of bitmap exit while case 2 'delta (how can this ever be used?) get #bf, , ub w = w + ub get #bf, , ub h = h + ub case else 'absolute mode for i = 1 to ub if i and 1 then get #bf, , pix bval = (pix and &hf0) shr 4 else bval = pix and &h0f end if putpixel(fr, w, h, bval) w += 1 next if (ub mod 4 = 1) or (ub mod 4 = 2) then get #bf, , ub 'pad to word bound end if end select case else 'run-length get #bf, , pix '2 colours v1 = (pix and &hf0) shr 4 v2 = pix and &h0f for i = 1 to ub if i and 1 then bval = v1 else bval = v2 end if putpixel(fr, w, h, bval) w += 1 next end select wend end sub private sub loadbmprle8(bf as integer, fr as Frame ptr) 'takes an open file handle and an already size Frame pointer, should only be called within loadbmp dim pix as ubyte dim ub as ubyte dim as integer w, h dim i as integer dim as ubyte bval w = 0 h = fr->h - 1 'read bytes until we're done while not eof(bf) 'get command byte get #bf, , ub select case ub case 0 'special, check next byte get #bf, , ub select case ub case 0 'end of line w = 0 h -= 1 case 1 'end of bitmap exit while case 2 'delta (how can this ever be used?) get #bf, , ub w = w + ub get #bf, , ub h = h + ub case else 'absolute mode for i = 1 to ub get #bf, , pix putpixel(fr, w, h, pix) w += 1 next if ub mod 2 then get #bf, , ub 'pad to word boundary end if end select case else 'run-length get #bf, , pix for i = 1 to ub putpixel(fr, w, h, pix) w += 1 next end select wend end sub private sub loadbmp1(bf as integer, fr as Frame ptr) 'takes an open file handle and an already sized Frame pointer, should only be called within loadbmp dim ub as ubyte dim as integer w, h dim sptr as ubyte ptr dim pad as integer dim numbytes as integer = (fr->w + 7) \ 8 'per line pad = 4 - (numbytes mod 4) if pad = 4 then pad = 0 for h = fr->h - 1 to 0 step -1 sptr = fr->image + h * fr->pitch for w = 0 to fr->w - 1 if (w mod 8) = 0 then get #bf, , ub end if *sptr = ub shr 7 ub = ub shl 1 sptr += 1 next 'padding to dword boundary for w = 0 to pad - 1 get #bf, , ub next next end sub 'Loads the palette of a 1-bit, 4-bit or 8-bit bmp into pal(). 'Returns the number of bits, or 0 if the file can't be read. function loadbmppal (f as string, pal() as RGBcolor) as integer dim header as BITMAPFILEHEADER dim info as BITMAPV3INFOHEADER dim col3 as RGBTRIPLE dim col4 as RGBQUAD dim bf as integer dim i as integer bf = open_bmp_and_read_header(f, header, info) if bf <= -1 then return 0 for i = 0 to ubound(pal) pal(i).r = 0 pal(i).g = 0 pal(i).b = 0 next 'debug "loadbmppal(" & f & "): table at " & (seek(bf) - 1) & " = " & hex(seek(bf) - 1) if info.biBitCount <= 8 then for i = 0 to (1 shl info.biBitCount) - 1 if info.biSize = 12 then 'BITMAPCOREHEADER get #bf, , col3 pal(i).r = col3.rgbtRed pal(i).g = col3.rgbtGreen pal(i).b = col3.rgbtBlue else get #bf, , col4 pal(i).r = col4.rgbRed pal(i).g = col4.rgbGreen pal(i).b = col4.rgbBlue end if next else debugc errBug, "loadbmppal shouldn't have been called!" end if close #bf return info.biBitCount end function sub convertbmppal (f as string, mpal() as RGBcolor, pal() as integer, firstindex as integer = 0) 'Find the nearest match palette mapping from a 1/4/8 bit bmp f to 'the master palette mpal(), and store it in pal(), an array of mpal() indices. 'pal() may contain initial values, used as hints which are used if an exact match. 'Pass firstindex = 1 to prevent anything from getting mapped to colour 0. dim bitdepth as integer dim cols(255) as RGBcolor bitdepth = loadbmppal(f, cols()) if bitdepth = 0 then exit sub for i as integer = 0 to small(UBOUND(pal), (1 SHL bitdepth) - 1) pal(i) = nearcolor(mpal(), cols(i).r, cols(i).g, cols(i).b, firstindex, pal(i)) next end sub 'Returns 0 if invalid, otherwise fills 'info' and returns 1 if valid but unsupported, 2 if supported function bmpinfo (f as string, byref info as BITMAPV3INFOHEADER) as integer dim header as BITMAPFILEHEADER dim bf as integer bf = open_bmp_and_read_header(f, header, info) if bf = -1 then return 0 if bf = -2 then return 1 close #bf return 2 end function 'Returns a non-negative integer which is 0 if both colors in a color table are the same function color_distance(pal() as RGBcolor, index1 as integer, index2 as integer) as integer with pal(index1) dim as integer rdif, bdif, gdif, rmean rmean = (.r + pal(index2).r) shr 1 rdif = .r - pal(index2).r gdif = .g - pal(index2).g bdif = .b - pal(index2).b 'Formula taken from https://www.compuphase.com/cmetric.htm return (((512 + rmean)*rdif*rdif) shr 8) + 4*gdif*gdif + (((767-rmean)*bdif*bdif) shr 8) end with end function function nearcolor(pal() as RGBcolor, red as ubyte, green as ubyte, blue as ubyte, firstindex as integer = 0, indexhint as integer = -1) as ubyte 'Figure out nearest palette colour in range [firstindex..255] using Euclidean distance 'A perfect match against pal(indexhint) is tried first dim as integer i, diff, best, save, rdif, bdif, gdif, rmean if indexhint > -1 and indexhint <= UBOUND(pal) and indexhint >= firstindex then with pal(indexhint) if red = .r and green = .g and blue = .b then return indexhint end with end if best = 1000000 save = 0 for i = firstindex to 255 with pal(i) rmean = (red + .r) shr 1 rdif = red - .r gdif = green - .g bdif = blue - .b end with 'diff = abs(rdif) + abs(gdif) + abs(bdif) 'diff = 3*rdif*rdif + 4*gdif*gdif + 2*bdif*bdif 'Formula taken from https://www.compuphase.com/cmetric.htm diff = (((512 + rmean)*rdif*rdif) shr 8) + 4*gdif*gdif + (((767-rmean)*bdif*bdif) shr 8) if diff = 0 then 'early out on direct hit save = i exit for end if if diff < best then save = i best = diff end if next return save end function function nearcolor(pal() as RGBcolor, index as integer, firstindex as integer = 0) as ubyte with pal(index) return nearcolor(pal(), .r, .g, .b, firstindex) end with end function 'Convert a 32 bit Surface to a paletted Frame. 'Frees surf. 'Only colours firstindex..255 in pal() are used. 'Any pixels with alpha=0 are mapped to 0; otherwise alpha is ignored. 'Optionally, any RGB colour matching 'transparency' gets mapped to index 0 (by default none); 'the Surface's alpha is ignored and transparency.a must be 0 or it won't be matched. function quantize_surface(byref surf as Surface ptr, pal() as RGBcolor, options as QuantizeOptions) as Frame ptr if surf->format <> SF_32bit then showerror "quantize_surface only works on 32 bit Surfaces (bad frame_import_bmp24_or_32 call?)" gfx_surfaceDestroy(@surf) return NULL end if dim ret as Frame ptr ret = frame_new(surf->width, surf->height) dim inptr as RGBcolor ptr dim outptr as ubyte ptr for y as integer = 0 to surf->height - 1 inptr = surf->pColorData + y * surf->pitch outptr = ret->image + y * ret->pitch for x as integer = 0 to surf->width - 1 ' Ignore alpha if inptr->col and &h00ffffff = options.transparency.col then *outptr = 0 elseif inptr->a = 0 then *outptr = 0 else *outptr = nearcolor(pal(), inptr->r, inptr->g, inptr->b, options.firstindex) end if inptr += 1 outptr += 1 next next gfx_surfaceDestroy(@surf) return ret end function '========================================================================================== ' GIF '========================================================================================== ' Create a GifPalette from either the master palette or a Palette16 mapped onto ' a master palette, as needed for calling lib/gif.bi functions directly sub GifPalette_from_pal (byref gpal as GifPalette, masterpal() as RGBcolor, pal as Palette16 ptr = NULL) if pal then ' Avoid using color 0 (transparency), which gets remapped to the nearest match ' by using a colors 1-16 in a 32 colour palette gpal.bitDepth = 5 for idx as integer = 0 to 16 ' Color 0 = 16 dim masteridx as integer = pal->col(idx MOD 16) 'if masteridx = 0 then ' masteridx = uilook(uiBackground) 'end if gpal.r(idx) = masterpal(masteridx).r gpal.g(idx) = masterpal(masteridx).g gpal.b(idx) = masterpal(masteridx).b next else ' Again color 0 will be remapped, but with 256 colours to choose from there's likely ' to be a good match gpal.bitDepth = 8 for idx as integer = 0 to 255 gpal.r(idx) = masterpal(idx).r gpal.g(idx) = masterpal(idx).g gpal.b(idx) = masterpal(idx).b next end if end sub ' Output a single-frame .gif. Ignores mask. sub frame_export_gif (fr as Frame Ptr, fname as string, maspal() as RGBcolor, pal as Palette16 ptr = NULL, transparent as bool = NO) CHECK_FRAME_8BIT(fr) 'TODO: implement 32bit export dim writer as GifWriter dim gifpal as GifPalette GifPalette_from_pal gifpal, maspal(), pal if GifBegin(@writer, fopen(fname, "wb"), fr->w, fr->h, 0, transparent, @gifpal) = NO then debug "GifWriter(" & fname & ") failed" elseif GifWriteFrame8(@writer, fr->image, fr->w, fr->h, 0, NULL) = NO then debug "GifWriteFrame8 failed" elseif GifEnd(@writer) = NO then debug "GifEnd failed" end if end sub property RecordGIFState.active() as bool return writer.f <> NULL end property 'Returns time delay in hundreds of a second to be used for next frame '(We have to say how long the frame will be displayed when we write it, rather than 'just telling how long the last frame was on-screen for.) function RecordGIFState.delay() as integer ' Predict the time that this frame will be shown via the setwait timer. ' But the actual next setvispage might happen after or before that ' (if there are multiple setvispage calls before dowait). dim as double next_frame_time = waittime 'next_next_frame_time = waittime + 1 / requested_framerate if gif_max_fps > 0 andalso next_frame_time - last_frame_end_time < 1. / gif_max_fps then ' Wait until some more time has passed return 0 end if dim ret as integer ret = (next_frame_time - last_frame_end_time) * 100 if ret <= 0 then ' In this case there's no point writing the frame, but this should be rare return 0 end if ' Instead of doing last_frame_end_time = waittime, this accumulates ' the parts less than 0.01s, to avoid rounding error last_frame_end_time += ret * 0.01 return ret end function sub start_recording_gif() dim gifpal as GifPalette ' Use master() rather than actual palette (intpal()), because ' intpal() is affected by fades. We want the master palette, ' because that's likely to be the palette for most frames. GifPalette_from_pal gifpal, master() recordgif.fname = absolute_path(next_unused_screenshot_filename() + ".gif") dim file as FILE ptr = fopen(recordgif.fname, "wb") if GifBegin(@recordgif.writer, file, vpages(vpage)->w, vpages(vpage)->h, 6, NO, @gifpal) then show_overlay_message "Ctrl-F12 to stop recording", 1. recordgif.last_frame_end_time = timer else show_overlay_message "Can't record, GifBegin failed" end if end sub sub stop_recording_gif() if not recordgif.active then exit sub if GifEnd(@recordgif.writer) = NO then show_overlay_message "Recording failed" safekill recordgif.fname exit sub end if dim msg as string = "Recorded " & trimpath(recordgif.fname) ' Compress it using gifsicle, if available dim gifsicle as string = find_helper_app("gifsicle") if len(gifsicle) then debuginfo "Compressing " & recordgif.fname & " with gifsicle; size before = " & filelen(recordgif.fname) dim handle as ProcessHandle handle = open_process(gifsicle, "-O2 " & escape_filename(recordgif.fname) & " -o " & escape_filename(recordgif.fname), NO, NO) if handle = 0 then debug "open_process " & gifsicle & " failed" else msg += " (Compressing...)" end if cleanup_process(@handle) end if show_overlay_message msg, 1.2 end sub 'Perform the effect of pressing Ctrl-F12: start or stop recording a gif sub toggle_recording_gif() if recordgif.active then stop_recording_gif else start_recording_gif end if end sub private sub _gif_pitch_fail(what as string) debugc errPromptBug, "Can't record gif from " & what & " with extra pitch" 'This will cause the following GifWriteFrame* call to fail stop_recording_gif end sub ' Called with every frame that should be included in any ongoing gif recording private sub gif_record_frame(fr as Frame ptr, pal() as RGBcolor) if recordgif.active = NO then exit sub dim delay as integer = recordgif.delay() if delay <= 0 then exit sub dim ret as bool dim bits as integer dim sf as Surface ptr = fr->surf if sf andalso sf->format = SF_32bit then bits = 32 dim image as ubyte ptr = cast(ubyte ptr, sf->pColorData) if sf->width <> sf->pitch then _gif_pitch_fail "32-bit Surface" ret = GifWriteFrame(@recordgif.writer, image, sf->width, sf->height, delay, 8, NO) else ' 8-bit Surface-backed Frames and regular Frames. bits = 8 dim gifpal as GifPalette GifPalette_from_pal gifpal, pal() if sf andalso sf->format = SF_8bit then if sf->width <> sf->pitch then _gif_pitch_fail "8-bit Surface" ret = GifWriteFrame8(@recordgif.writer, sf->pPaletteData, sf->width, sf->height, delay, @gifpal) else if fr->w <> fr->pitch then _gif_pitch_fail "Frame" ret = GifWriteFrame8(@recordgif.writer, fr->image, fr->w, fr->h, delay, @gifpal) end if end if if ret = NO then ' On a write failure, recordgif.active will already be set to false show_overlay_message "Recording failed (GifWriteFrame " & bits & ")" debug "GifWriteFrame failed, bits = " & bits end if end sub '========================================================================================== ' Screenshots '========================================================================================== dim shared as string*4 screenshot_exts(...) => {".bmp", ".png", ".jpg", ".dds", ".gif"} 'Save a screenshot. fname should NOT include the extension, since the gfx backend can decide that. 'Returns the filename it was saved to, with extension function screenshot (basename as string) as string dim ret as string if len(basename) = 0 then basename = next_unused_screenshot_filename() end if 'try external first if gfx_screenshot(basename) = 0 then 'otherwise save it ourselves ret = basename & ".bmp" frame_export_bmp(ret, vpages(getvispage), intpal()) return ret end if ' The reason for this for loop is that we don't know what extension the gfx backend ' might save the screenshot as; have to search for it. for i as integer = 0 to ubound(screenshot_exts) ret = basename & screenshot_exts(i) if isfile(ret) then return ret end if next end function sub bmp_screenshot(basename as string) 'This is for when you explicitly want a bmp screenshot, and NOT the preferred 'screenshot type used by the current gfx backend frame_export_bmp(basename & ".bmp", vpages(getvispage), intpal()) end sub ' Find an available screenshot name in the current directory. ' Returns filename without extension, and ensures it doesn't collide regardless of the ' extension selected from screenshot_extns. private function next_unused_screenshot_filename() as string static search_start as integer static search_gamename as string dim as string ret dim as string gamename = trimextension(trimpath(sourcerpg)) if gamename = "" then ' If we haven't loaded a game yet gamename = "ohrrpgce" end if ' Reset search_start counter if needed if search_gamename <> gamename then search_gamename = gamename search_start = 0 end if for n as integer = search_start to 99999 ret = gamename + right("0000" & n, 4) 'checking curdir, which is export directory for i as integer = 0 to ubound(screenshot_exts) if isfile(ret + screenshot_exts(i)) then continue for, for next search_start = n return ret next return ret 'This won't be reached end function 'Take a single screenshot if F12 is pressed. 'Holding down F12 takes a screenshot each frame, however besides 'the first, they're saved to the temporary directory until key repeat kicks in, and then 'moved, in order to 'debounce' F12 if you only press it for a short while. '(Hmm, now that we can record gifs directly, it probably makes sense to remove the ability to hold F12) 'NOTE: global variables like tmpdir can change between calls, have to be lenient private sub snapshot_check() static as string backlog() initialize_static_dynamic_array(backlog) ' The following are just for the overlay message static as integer num_screenshots_taken static as string first_screenshot dim as integer n, F12bits dim as string shot F12bits = real_keyval(scF12) if F12bits = 0 then ' If key repeat never occurred then delete the backlog. for n = 1 to ubound(backlog) 'debug "killing " & backlog(n) safekill backlog(n) next redim backlog(0) ' Tell what we did if num_screenshots_taken = 1 then show_overlay_message "Saved screenshot " & first_screenshot, 1.5 elseif num_screenshots_taken > 1 then show_overlay_message "Saved " & first_screenshot & " and " & (num_screenshots_taken - 1) & " more", 1.5 end if num_screenshots_taken = 0 elseif real_keyval(scCtrl) = 0 then if F12bits = 1 then ' Take a screenshot, but maybe delete it later shot = tmpdir & get_process_id() & "_tempscreen" & ubound(backlog) str_array_append(backlog(), screenshot(shot)) 'debug "temp save " & backlog(ubound(backlog)) else ' Key repeat has kicked in, so move our backlog of screenshots to the visible location. for n = 1 to ubound(backlog) shot = next_unused_screenshot_filename() & "." & justextension(backlog(n)) 'debug "moving " & backlog(n) & " to " & shot os_shell_move backlog(n), shot num_screenshots_taken += 1 next redim backlog(0) ' Take the new screenshot dim temp as string = screenshot() 'debug "saved " & temp if num_screenshots_taken = 0 then first_screenshot = trimpath(temp) end if num_screenshots_taken += 1 end if 'debug "screen " & shot end if ' This is in case this sub is called more than once before setkeys is called. ' Normally setkeys happens at the beginning of a tick and setvispage at the end, ' so this does no damage. real_clear_newkeypress scF12 end sub '========================================================================================== ' Graphics render clipping '========================================================================================== 'NOTE: there is only one set of clipping values, shared globally for 'all drawing operations... this is probably a bad thing, but that is how 'it works. The frame argument to setclip() is used to determine 'the allowed range of clipping values. 'Set the bounds used by various (not quite all?) video page drawing functions. 'setclip must be called to reset the clip bounds whenever the clippedframe changes, to ensure 'that they are valid (the video page dimensions might differ). sub setclip(l as integer = 0, t as integer = 0, r as integer = 999999, b as integer = 999999, fr as Frame ptr = 0) if fr <> 0 then clippedframe = fr with *clippedframe clipl = bound(l, 0, .w) '.w valid, prevents any drawing clipt = bound(t, 0, .h) clipr = bound(r, 0, .w - 1) clipb = bound(b, 0, .h - 1) end with end sub 'Shrinks clipping area, never grows it sub shrinkclip(l as integer = 0, t as integer = 0, r as integer = 999999, b as integer = 999999, fr as Frame ptr) if clippedframe <> fr then clippedframe = fr clipl = 0 clipt = 0 clipr = 999999 clipb = 999999 end if with *clippedframe clipl = bound(large(clipl, l), 0, .w) '.w valid, prevents any drawing clipt = bound(large(clipt, t), 0, .h) clipr = bound(small(clipr, r), 0, .w - 1) clipb = bound(small(clipb, b), 0, .h - 1) end with end sub sub saveclip(byref buf as ClipState) buf.whichframe = clippedframe buf.clipr = clipr buf.clipl = clipl buf.clipt = clipt buf.clipb = clipb end sub sub loadclip(byref buf as ClipState) clippedframe = buf.whichframe clipr = buf.clipr clipl = buf.clipl clipt = buf.clipt clipb = buf.clipb end sub 'Blit a Frame with setclip clipping. 'trans: draw transparently, either using ->mask if available, or otherwise use colour 0 as transparent 'warning! Make sure setclip has been called before calling this 'write_mask: ' If the destination has a mask, sets the mask for the destination rectangle ' equal to the mask (or color-key) for the source rectangle. Does not OR them. private sub draw_clipped(src as Frame ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) dim as integer startx, starty, endx, endy dim as integer srcoffset startx = x endx = x + src->w - 1 starty = y endy = y + src->h - 1 if startx < clipl then srcoffset = (clipl - startx) startx = clipl end if if starty < clipt then srcoffset += (clipt - starty) * src->pitch starty = clipt end if if endx > clipr then endx = clipr end if if endy > clipb then endy = clipb end if if starty > endy or startx > endx then exit sub blitohr(src, dest, pal, srcoffset, startx, starty, endx, endy, trans, write_mask) end sub ' Blit a Frame with setclip clipping and scale <> 1. private sub draw_clipped_scaled(src as Frame ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, scale as integer, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) if src->surf <> NULL or dest->surf <> NULL then showerror "draw_clipped_scaled: scale " & scale & " not supported with Surface-backed Frames" exit sub end if dim as integer sxfrom, sxto, syfrom, syto sxfrom = large(clipl, x) sxto = small(clipr, x + (src->w * scale) - 1) syfrom = large(clipt, y) syto = small(clipb, y + (src->h * scale) - 1) blitohrscaled (src, dest, pal, x, y, sxfrom, syfrom, sxto, syto, trans, write_mask, scale) end sub ' Blit a Surface with setclip clipping. private sub draw_clipped_surf(src as Surface ptr, master_pal as RGBPalette ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, trans as bool, dest as Surface ptr) ' It's OK for the src and dest rects to have negative size or be off ' the edge of src/dest, because gfx_surfaceCopy properly clips them. dim srcRect as SurfaceRect = (0, 0, src->width - 1, src->height - 1) if x < clipl then srcRect.left = clipl - x x = clipl end if if y < clipt then srcRect.top = clipt - y y = clipt end if dim destRect as SurfaceRect = (x, y, clipr, clipb) if gfx_surfaceCopy(@srcRect, src, master_pal, pal, trans, @destRect, dest) then debug "gfx_surfaceCopy error" end if end sub '========================================================================================== ' Sprite (Frame) cache '========================================================================================== 'not to be used outside of the sprite functions declare sub frame_delete_members(f as Frame ptr) declare sub frame_freemem(f as Frame ptr) declare sub spriteset_freemem(sprset as SpriteSet ptr) 'Assumes pitch == w declare sub frame_add_mask(fr as Frame ptr, clr as bool = NO) 'The sprite cache holds Frame ptrs, which may also be Frame arrays and SpriteSets. Since 'each SpriteSet is associated with a unique Frame array, we don't need a separate cache 'for SpriteSets. SpriteSet data can be loaded after and linked to the cached Frame array 'if it was initially not loaded as a SpriteSet. 'The sprite cache, which is a HashTable (sprcache) containing all loaded sprites, is split in 'two: the A cache containing currently in-use sprites (which is not explicitly tracked), and 'the B cache holding those not in use, which is a LRU list 'sprcacheB' which holds a maximum 'of SPRCACHEB_SZ entries. 'The number/size of in-use sprites is not limited, and does not count towards the B cache 'unless COMBINED_SPRCACHE_LIMIT is defined. It should be left undefined when memory usage 'is not actually important. 'I couldn't find any algorithms for inequal cost caching so invented my own: sprite size is 'measured in 'base size' units, and instead of being added to the head of the LRU list, 'sprites are moved a number of places back from the head equal to their size. This is probably 'an unnecessary complication over LRU, but it's fun. CONST SPRCACHE_BASE_SZ = 4096 'bytes #IFDEF LOWMEM 'Up to 8MB, including in-use sprites CONST SPRCACHEB_SZ = 2048 'in SPRITE_BASE_SZ units #DEFINE COMBINED_SPRCACHE_LIMIT 1 #ELSE 'Max cache size of 16MB, but actual limit will be less due to items smaller than 4KB CONST SPRCACHEB_SZ = 4096 'in SPRITE_BASE_SZ units #ENDIF ' removes a sprite from the cache, and frees it. private sub sprite_remove_cache(entry as SpriteCacheEntry ptr) if entry->p->refcount <> 1 then debug "error: invalidly uncaching sprite " & entry->hashed.hash & " " & frame_describe(entry->p) end if dlist_remove(sprcacheB.generic, entry) hash_remove(sprcache, entry) entry->p->cacheentry = NULL 'help to detect double free frame_freemem(entry->p) #ifdef COMBINED_SPRCACHE_LIMIT sprcacheB_used -= entry->cost #else if entry->Bcached then sprcacheB_used -= entry->cost end if #endif deallocate(entry) end sub 'Free some sprites from the end of the B cache 'Returns true if enough space was freed private function sprite_cacheB_shrink(amount as integer) as bool sprite_cacheB_shrink = (amount <= SPRCACHEB_SZ) if sprcacheB_used + amount <= SPRCACHEB_SZ then exit function dim as SpriteCacheEntry ptr pt, prevpt pt = sprcacheB.last while pt prevpt = pt->cacheB.prev sprite_remove_cache(pt) if sprcacheB_used + amount <= SPRCACHEB_SZ then exit function pt = prevpt wend end function sub sprite_empty_cache_range(minkey as integer, maxkey as integer, leakmsg as string, freeleaks as bool = NO) dim iterstate as integer = 0 dim as SpriteCacheEntry ptr pt, nextpt nextpt = NULL pt = hash_iter(sprcache, iterstate, nextpt) while pt nextpt = hash_iter(sprcache, iterstate, nextpt) 'recall that the cache counts as a reference if pt->p->refcount <> 1 then debug "warning: " & leakmsg & pt->hashed.hash & " with " & pt->p->refcount & " references" if freeleaks then sprite_remove_cache(pt) else sprite_remove_cache(pt) end if pt = nextpt wend end sub 'Unlike sprite_empty_cache, this reloads (in-use) sprites from file, without changing the pointers 'to them. Any sprite that's not actually in use is removed from the cache as it's unnecessary to reload. private sub sprite_update_cache_range(minkey as integer, maxkey as integer) dim iterstate as integer = 0 dim as SpriteCacheEntry ptr pt, nextpt nextpt = NULL pt = hash_iter(sprcache, iterstate, nextpt) while pt nextpt = hash_iter(sprcache, iterstate, nextpt) if pt->hashed.hash < minkey or pt->hashed.hash > maxkey then pt = nextpt continue while end if 'recall that the cache counts as a reference if pt->p->refcount <> 1 then dim sprtype as integer = pt->hashed.hash \ SPRITE_CACHE_MULT dim record as integer = pt->hashed.hash mod SPRITE_CACHE_MULT dim newframe as Frame ptr newframe = frame_load_uncached(sprtype, record) if newframe <> NULL then if newframe->arraylen <> pt->p->arraylen then fatalerror "sprite_update_cache: wrong number of frames!" else 'Transplant the data from the new Frame into the old Frame, so that no 'pointers need to be updated. pt (the SpriteCacheEntry) doesn't need to 'to be modified at all dim refcount as integer = pt->p->refcount dim wantmask as bool = (pt->p->mask <> NULL) 'Remove the host's previous organs frame_delete_members pt->p 'Insert the new organs memcpy(pt->p, newframe, sizeof(Frame) * newframe->arraylen) 'Having removed everything from the donor, dispose of it Deallocate(newframe) 'Fix the bits we just clobbered pt->p->cached = 1 pt->p->refcount = refcount pt->p->cacheentry = pt if pt->p->sprset then 'Update cross-link pt->p->sprset->frames = pt->p end if 'Make sure we don't crash if we were using a mask (might be the wrong mask though) if wantmask then frame_add_mask pt->p end if end if else sprite_remove_cache(pt) end if pt = nextpt wend end sub 'Reload all graphics of certain type sub sprite_update_cache(sprtype as SpriteType) sprite_update_cache_range(SPRITE_CACHE_MULT * sprtype, SPRITE_CACHE_MULT * (sprtype + 1) - 1) end sub 'Attempt to completely empty the sprite cache, detecting memory leaks 'By default, remove everything. With an argument: remove specific type sub sprite_empty_cache(sprtype as SpriteType = sprTypeInvalid) if sprtype = sprTypeInvalid then sprite_empty_cache_range(INT_MIN, INT_MAX, "leaked sprite ") if sprcacheB_used <> 0 or sprcache.numitems <> 0 then debug "sprite_empty_cache: corruption: sprcacheB_used=" & sprcacheB_used & " items=" & sprcache.numitems end if else sprite_empty_cache_range(SPRITE_CACHE_MULT * sprtype, SPRITE_CACHE_MULT * (sprtype + 1) - 1, "leaked sprite ") end if end sub sub sprite_debug_cache() debug "==sprcache==" dim iterstate as integer = 0 dim pt as SpriteCacheEntry ptr = NULL while hash_iter(sprcache, iterstate, pt) debug pt->hashed.hash & " cost=" & pt->cost & " : " & frame_describe(pt->p) wend debug "==sprcacheB== (used units = " & sprcacheB_used & "/" & SPRCACHEB_SZ & ")" pt = sprcacheB.first while pt debug pt->hashed.hash & " cost=" & pt->cost & " : " & frame_describe(pt->p) pt = pt->cacheB.next wend end sub 'a sprite has no references, move it to the B cache private sub sprite_to_B_cache(entry as SpriteCacheEntry ptr) dim pt as SpriteCacheEntry ptr if sprite_cacheB_shrink(entry->cost) = NO then 'fringe case: bigger than the max cache size sprite_remove_cache(entry) exit sub end if 'apply size penalty pt = sprcacheB.first dim tobepaid as integer = entry->cost while pt tobepaid -= pt->cost if tobepaid <= 0 then exit while pt = pt->cacheB.next wend dlist_insertat(sprcacheB.generic, pt, entry) entry->Bcached = YES #ifndef COMBINED_SPRCACHE_LIMIT sprcacheB_used += entry->cost #endif end sub ' move a sprite out of the B cache private sub sprite_from_B_cache(entry as SpriteCacheEntry ptr) dlist_remove(sprcacheB.generic, entry) entry->Bcached = NO #ifndef COMBINED_SPRCACHE_LIMIT sprcacheB_used -= entry->cost #endif end sub ' search cache, update as required if found private function sprite_fetch_from_cache(key as integer) as Frame ptr dim entry as SpriteCacheEntry ptr entry = hash_find(sprcache, key) if entry then 'cachehit += 1 if entry->Bcached then sprite_from_B_cache(entry) end if entry->p->refcount += 1 return entry->p end if return NULL end function ' adds a newly loaded frame to the cache with a given key private sub sprite_add_cache(key as integer, p as Frame ptr) if p = 0 then exit sub dim entry as SpriteCacheEntry ptr entry = callocate(sizeof(SpriteCacheEntry)) entry->hashed.hash = key entry->p = p entry->cost = (p->w * p->h * p->arraylen) \ SPRCACHE_BASE_SZ + 1 'leave entry->cacheB unlinked entry->Bcached = NO 'the cache counts as a reference, but only to the head element of an array!! p->cached = 1 p->refcount += 1 p->cacheentry = entry hash_add(sprcache, entry) #ifdef COMBINED_SPRCACHE_LIMIT sprcacheB_used += entry->cost #endif end sub '========================================================================================== ' Frames '========================================================================================== 'Create a blank Frame or array of Frames 'By default not initialised; pass clr=YES to initialise to 0 'with_surface32: if true, create a 32-it Surface-backed Frame. function frame_new(w as integer, h as integer, frames as integer = 1, clr as bool = NO, wantmask as bool = NO, with_surface32 as bool = NO) as Frame ptr if w < 1 or h < 1 or frames < 1 then debugc errPromptBug, "frame_new: bad size " & w & "*" & h & "*" & frames return 0 end if if with_surface32 then if wantmask then debugc errPromptBug, "frame_new: mask and backing surface mututally exclusive" end if end if dim ret as Frame ptr 'this hack was Mike's idea, not mine! ret = callocate(sizeof(Frame) * frames) 'no memory? shucks. if ret = 0 then debug "Could not create sprite frames, no memory" return 0 end if dim as integer i, j for i = 0 to frames - 1 with ret[i] 'the caller to frame_new is considered to have a ref to the head; and the head to have a ref to each other elem 'so set each refcount to 1 .refcount = 1 .arraylen = frames if i > 0 then .arrayelem = 1 .w = w .h = h .pitch = w .mask = NULL if with_surface32 then if gfx_surfaceCreate(w, h, SF_32bit, SU_Staging, @.surf) then frame_freemem(ret) return NULL end if if clr then gfx_surfaceFill(intpal(0).col, NULL, .surf) end if else if clr then .image = callocate(.pitch * h) if wantmask then .mask = callocate(.pitch * h) else .image = allocate(.pitch * h) if wantmask then .mask = allocate(.pitch * h) end if if .image = 0 or (.mask = 0 and wantmask <> NO) then debug "Could not allocate sprite frames/surfaces" 'well, I don't really see the point freeing memory, but who knows... frame_freemem(ret) return NULL end if end if end with next return ret end function 'Create a frame which is a view onto part of a larger frame 'Can return a zero-size view. Seems to work, but not yet sure that all operations will work correctly on such a frame. function frame_new_view(spr as Frame ptr, x as integer, y as integer, w as integer, h as integer) as Frame ptr dim ret as Frame ptr = callocate(sizeof(Frame)) if ret = 0 then debug "Could not create sprite view, no memory" return 0 end if if x < 0 then w -= -x: x = 0 if y < 0 then h -= -y: y = 0 with *ret .w = bound(w, 0, spr->w - x) .h = bound(h, 0, spr->h - y) if x >= spr->w or y >= spr->h or .w = 0 or .h = 0 then 'this might help to keep things slightly saner .w = 0 .h = 0 end if .pitch = spr->pitch if spr->surf then if gfx_surfaceCreateView(spr->surf, x, y, .w, .h, @.surf) then deallocate ret return NULL end if else .image = spr->image + .pitch * y + x if spr->mask then .mask = spr->mask + .pitch * y + x end if end if .refcount = 1 .arraylen = 1 'at the moment not actually used anywhere on sprites with isview = 1 .isview = 1 'we point .base at the 'root' frame which really owns these pixel buffer(s) if spr->isview then .base = spr->base else .base = spr end if if .base->refcount <> NOREFC then .base->refcount += 1 end with return ret end function ' Returns a Frame which is backed by a Surface. ' Unload/Destroy both the Frame and the Surface: increments refcount for the Surface! function frame_with_surface(surf as Surface ptr) as Frame ptr dim ret as Frame ptr = callocate(sizeof(Frame)) 'Note: normally it makes no sense to call this on a Surface that is itself 'a view of a Frame surf = gfx_surfaceReference(surf) with *ret .surf = surf .w = surf->width .h = surf->height .pitch = surf->pitch 'image and mask are Null .refcount = 1 .arraylen = 1 end with return ret end function ' Creates an (independent) 32 bit Surface which is a copy of an unpaletted Frame. ' This is not the same as gfx_surfaceCreateFrameView, which creates a Surface which ' is just a view of a Frame (and is a temporary hack!) function frame_to_surface32(fr as Frame ptr, masterpal() as RGBcolor, pal as Palette16 ptr = NULL) as Surface ptr if fr->surf then debug "frame_to_surface32 called on a Surface-backed Frame" if fr->surf->format = SF_8bit then showerror "Converting Frame w/ 8bit Surface to 32bit Surface unimplemented" end if return fr->surf end if dim surf as Surface ptr if gfx_surfaceCreate(fr->w, fr->h, SF_32bit, SU_Staging, @surf) then return NULL end if dim wrapper as Frame ptr 'yuck wrapper = frame_with_surface(surf) frame_draw fr, masterpal(), pal, 0, 0, , NO, wrapper frame_unload @wrapper return surf end function ' Turn a regular Frame into a 32-bit Surface-backed Frame. ' Content is preserved. sub frame_convert_to_32bit(fr as Frame ptr, masterpal() as RGBcolor, pal as Palette16 ptr = NULL) if fr->cached then showerror "frame_convert_to_32bit: refusing to clobber cached Frame" exit sub end if fr->surf = frame_to_surface32(fr, masterpal(), pal) deallocate(fr->image) fr->image = NULL deallocate(fr->mask) fr->mask = NULL end sub ' Turn Surface-backed Frame back to a regular Frame. Content IS WIPED! sub frame_drop_surface(fr as Frame ptr) if fr->surf then gfx_surfaceDestroy(@fr->surf) if fr->image = NULL then fr->image = callocate(fr->pitch * fr->h) end if end if end sub private sub frame_delete_members(f as Frame ptr) if f->arrayelem then debug "can't free arrayelem!": exit sub for i as integer = 0 to f->arraylen - 1 deallocate(f[i].image) f[i].image = NULL deallocate(f[i].mask) f[i].mask = NULL if f[i].surf then gfx_surfaceDestroy(@f[i].surf) f[i].refcount = FREEDREFC 'help to detect double free next if f->sprset then delete f->sprset f->sprset = NULL end if end sub ' unconditionally frees a sprite from memory. ' You should never need to call this: use frame_unload ' Should only be called on the head of an array (and not a view, obv)! ' Warning: not all code calls frame_freemem to free sprites! Grrr! private sub frame_freemem(f as Frame ptr) if f = 0 then exit sub frame_delete_members f deallocate(f) end sub 'Public: ' Loads a 4-bit or 8-bit sprite/backdrop/tileset from the appropriate game lump, *with caching*. ' For 4-bit sprites it will return a pointer to the first frame, and subsequent frames ' will be immediately after it in memory. (This is a hack, and will probably be removed) ' For tilesets, the tileset will already be reordered as needed. function frame_load(sprtype as SpriteType, record as integer) as Frame ptr dim key as integer = sprtype * SPRITE_CACHE_MULT + record dim ret as Frame ptr = sprite_fetch_from_cache(key) if ret then return ret ret = frame_load_uncached(sprtype, record) if ret then sprite_add_cache(key, ret) return ret end function private function graphics_file(extn as string) as string if len(game) = 0 then ' Haven't loaded a game, fallback to the engine's default graphics dim gfxdir as string = finddatadir("defaultgfx") if len(gfxdir) = 0 then return "" end if return gfxdir & SLASH "ohrrpgce" & extn end if return game & extn end function ' Loads a 4-bit or 8-bit sprite/backdrop/tileset from the appropriate game lump. See frame_load. private function frame_load_uncached(sprtype as SpriteType, record as integer) as Frame ptr if sprtype < 0 or sprtype > sprTypeLastLoadable or record < 0 then debugc errBug, "frame_load: invalid type=" & sprtype & " and rec=" & record return 0 end if dim ret as Frame ptr dim starttime as double = timer if sprtype = sprTypeBackdrop then ret = frame_load_mxs(graphics_file(".mxs"), record) elseif sprtype = sprTypeTileset then dim mxs as Frame ptr mxs = frame_load_mxs(graphics_file(".til"), record) if mxs = NULL then return NULL ret = mxs_frame_to_tileset(mxs) frame_unload @mxs else with sprite_sizes(sprtype) 'debug "loading " & sprtype & " " & record 'cachemiss += 1 ret = frame_load_4bit(graphics_file(".pt" & sprtype), record, .frames, .size.w, .size.h) end with end if if ret then ret->sprset = new SpriteSet(ret) init_4bit_spriteset_defaults(ret->sprset, sprtype) end if debug_if_slow(starttime, 0.1, sprtype & "," & record) return ret end function ' You can use this to load a .pt?-format 4-bit sprite from some non-standard location. ' No code does this. Does not use a cache. ' It will return a pointer to the first frame (of num frames), and subsequent frames ' will be immediately after it in memory. (This is a hack, and will probably be removed) function frame_load_4bit(filen as string, rec as integer, numframes as integer, wid as integer, hei as integer) as Frame ptr dim ret as Frame ptr dim frsize as integer = wid * hei / 2 dim recsize as integer = frsize * numframes dim fh as integer if openfile(filen, for_binary + access_read, fh) then debugc errError, "frame_load_4bit: could not open " & filen return 0 end if ret = frame_new(wid, hei, numframes) if ret = 0 then close #fh return 0 end if 'find the right sprite (remember, it's base-1) seek #fh, recsize * rec + 1 dim framenum as integer, x as integer, y as integer, z as ubyte 'pixels stored in columns, 2 pixels/byte for framenum = 0 to numframes - 1 with ret[framenum] for x = 0 to wid - 1 for y = 0 to hei - 1 'pull up two pixels get #fh, , z 'the high nybble is the first pixel .image[y * wid + x] = (z SHR 4) y += 1 'and the low nybble is the second one .image[y * wid + x] = z AND 15 next next end with next close #fh return ret end function 'Appends a new "frame" child node 'TODO: Doesn't save metadata about palette or master palette 'TODO: Doesn't save mask, but we don't have any need to serialise masks at the moment function frame_to_node(fr as Frame ptr, parent as NodePtr) as NodePtr dim as NodePtr frame_node, image_node frame_node = AppendChildNode(parent, "frame") AppendChildNode(frame_node, "w", fr->w) AppendChildNode(frame_node, "h", fr->h) if fr->mask then debug "WARNING: frame_to_node can't save masks" end if '"bits" gives the format of the "image" node; whether this Frame 'is a 4 or 8 bit sprite is unknown (and would be stored separately) dim bits as integer = 8 if fr->surf then if fr->surf->format = SF_32bit then bits = 32 end if end if AppendChildNode(frame_node, "bits", bits) image_node = AppendChildNode(frame_node, "image") 'Allocate uninitialised memory SetContent(image_node, NULL, fr->w * fr->h * (bits \ 8)) dim imdata as byte ptr = GetZString(image_node) if fr->surf then dim surf as Surface ptr = fr->surf dim rowbytes as integer = surf->width * bits \ 8 dim pitchbytes as integer = surf->pitch * bits \ 8 for y as integer = 0 TO surf->height - 1 memcpy(imdata + y * rowbytes, cast(byte ptr, surf->pRawData) + y * pitchbytes, rowbytes) next else for y as integer = 0 TO fr->h - 1 memcpy(imdata + y * fr->w, fr->image + y * fr->pitch, fr->w) next end if return frame_node end function 'Loads a Frame from a "frame" node (node name not enforced) function frame_from_node(node as NodePtr) as Frame ptr dim as integer bitdepth = GetChildNodeInt(node, "bits", 8) dim as integer w = GetChildNodeInt(node, "w") dim as integer h = GetChildNodeInt(node, "h") if bitdepth <> 8 and bitdepth <> 32 then debugc errPromptError, "frame_from_node: Unsupported graphics bitdepth " & bitdepth return NULL end if dim image_node as NodePtr = GetChildByName(node, "image") dim imdata as ubyte ptr = GetZString(image_node) dim imlen as integer = GetZStringSize(image_node) if imdata = NULL OR imlen <> w * h * bitdepth \ 8 then debugc errPromptError, "frame_from_node: Couldn't load image; data missing or bad length (" & imlen & " for " & w & "*" & h & ", bitdepth=" & bitdepth & ")" return NULL end if dim fr as Frame ptr if bitdepth = 8 then fr = frame_new(w, h) if fr = NULL then 'If the width or height was bad then an error already shown return NULL end if memcpy(fr->image, imdata, w * h) elseif bitdepth = 32 then dim surf as Surface ptr if gfx_surfaceCreate(w, h, SF_32bit, SU_Staging, @surf) then return NULL end if memcpy(surf->pColorData, imdata, w * h * 4) fr = frame_with_surface(surf) gfx_surfaceDestroy(@surf) end if return fr end function 'Public: ' Releases a reference to a sprite and nulls the pointer. ' If it is refcounted, decrements the refcount, otherwise it is freed immediately. ' A note on frame arrays: you may pass around pointers to frames in it (call frame_reference ' on them) and then unload them, but no memory will be freed until the head pointer refcount reaches 0. ' The head element will have 1 extra refcount if the frame array is in the cache. Each of the non-head ' elements also have 1 refcount, indicating that they are 'in use' by the head element, ' but this is just for feel-good book keeping ' (cdecl so that it can be used in the Frame ptr vector typetable) sub frame_unload cdecl(ppfr as Frame ptr ptr) if ppfr = 0 then exit sub dim fr as Frame ptr = *ppfr *ppfr = 0 if fr = 0 then exit sub if clippedframe = fr then clippedframe = 0 with *fr if .refcount = NOREFC then exit sub end if if .refcount = FREEDREFC then debug frame_describe(fr) & " already freed!" exit sub end if .refcount -= 1 if .refcount < 0 then debug frame_describe(fr) & " has refcount " & .refcount 'if cached, can free two references at once if (.refcount - .cached) <= 0 then if .arrayelem then 'this should not happen, because each arrayelem gets an extra refcount debug "arrayelem with refcount = " & .refcount exit sub end if if .isview then frame_unload @.base deallocate(fr) else for i as integer = 1 to .arraylen - 1 if fr[i].refcount <> 1 then debug frame_describe(@fr[i]) & " array elem freed with bad refcount" end if next if .cached then sprite_to_B_cache(fr->cacheentry) else frame_freemem(fr) end if end if end with end sub 'Takes a 320x200 Frame and produces a 20x3200 Frame in the format expected of tilesets: 'linear series of 20x20 tiles. function mxs_frame_to_tileset(spr as Frame ptr) as Frame ptr CHECK_FRAME_8BIT(spr, NULL) dim tileset as Frame ptr tileset = frame_new(20, 20 * 160) dim as ubyte ptr sptr = tileset->image dim as ubyte ptr srcp dim tilex as integer dim tiley as integer dim px as integer dim py as integer for tiley = 0 to 9 for tilex = 0 to 15 srcp = spr->image + tilex * 20 + tiley * 320 * 20 for py = 0 to 19 for px = 0 to 19 *sptr = *srcp sptr += 1 srcp += 1 next srcp += 320 - 20 next next next return tileset end function function hexptr(p as any ptr) as string return hex(cast(intptr_t, p)) end function function frame_describe(p as Frame ptr) as string if p = 0 then return "'(null)'" dim temp as string if p->sprset then temp = p->sprset->describe() return "'(0x" & hexptr(p) & ") " & p->arraylen & "x" & p->w & "x" & p->h _ & " offset=" & p->offset.x & "," & p->offset.y & " img=0x" & hexptr(p->image) _ & " msk=0x" & hexptr(p->mask) & " pitch=" & p->pitch & " cached=" & p->cached & " aelem=" _ & p->arrayelem & " view=" & p->isview & " base=0x" & hexptr(p->base) & " refc=" & p->refcount & "' " _ & temp end function 'this is mostly just a gimmick function frame_is_valid(p as Frame ptr) as bool if p = 0 then return NO dim ret as bool = YES if p->refcount <> NOREFC and p->refcount <= 0 then ret = NO 'this is an arbitrary test, and in theory, could cause a false-negative, but I can't concieve of 100 thousand references to the same sprite. if p->refcount > 100000 then ret = NO if p->w < 0 or p->h < 0 then ret = NO if p->pitch < p->w then ret = NO if p->surf then if p->image = 0 or p->mask = 0 then ret = NO else if p->image = 0 then ret = NO end if 'Patterns used by Windows and Linux to scrub memory if cint(p->mask) = &hBAADF00D or cint(p->image) = &hBAADF00D then ret = NO if cint(p->mask) = &hFEEEFEEE or cint(p->image) = &hFEEEFEEE then ret = NO if ret = NO then debugc errBug, "Invalid sprite " & frame_describe(p) 'if we get here, we are probably doomed, but this might be a recovery if p->cacheentry then sprite_remove_cache(p->cacheentry) end if return ret end function 'Add a mask. NOTE: Only valid on Frames with pitch == w! 'clr: is true, blank mask, otherwise copy image private sub frame_add_mask(fr as Frame ptr, clr as bool = NO) CHECK_FRAME_8BIT(fr) if fr->mask then exit sub if clr = NO then fr->mask = allocate(fr->w * fr->h) 'we can just copy .image in one go, since we just ensured it's contiguous memcpy(fr->mask, fr->image, fr->w * fr->h) else fr->mask = callocate(fr->w * fr->h) end if end sub 'for a copy you intend to modify. Otherwise use frame_reference 'clr: if true, return a new blank Frame with the same size. 'note: does not copy frame arrays, only single frames function frame_duplicate(p as Frame ptr, clr as bool = NO, addmask as bool = NO) as Frame ptr dim ret as Frame ptr if p = 0 then return 0 if p->surf then if clr or addmask then showerror "frame_duplicate: clr/addmask unimplemented for Surfaces" return 0 end if dim surf as Surface ptr = surface_duplicate(p->surf) ret = frame_with_surface(surf) ret->offset = p->offset gfx_surfaceDestroy(@surf) 'Decrement extra reference return ret end if ret = callocate(sizeof(frame)) if ret = 0 then return 0 ret->w = p->w ret->h = p->h ret->pitch = p->w ret->offset = p->offset ret->refcount = 1 ret->image = 0 ret->mask = 0 ret->arraylen = 1 if p->image then if clr = 0 then ret->image = allocate(ret->w * ret->h) if p->w = p->pitch then 'a little optimisation (we know ret->w == ret->pitch) memcpy(ret->image, p->image, ret->w * ret->h) else for i as integer = 0 to ret->h - 1 memcpy(ret->image + i * ret->pitch, p->image + i * p->pitch, ret->w) next end if else ret->image = callocate(ret->w * ret->h) end if end if if p->mask then if clr = 0 then ret->mask = allocate(ret->w * ret->h) if p->w = p->pitch then 'a little optimisation (we know ret->w == ret->pitch) memcpy(ret->mask, p->mask, ret->w * ret->h) else for i as integer = 0 to ret->h - 1 memcpy(ret->mask + i * ret->pitch, p->mask + i * p->pitch, ret->w) next end if else ret->mask = callocate(ret->w * ret->h) end if elseif addmask then frame_add_mask ret, clr end if return ret end function function frame_reference cdecl(p as Frame ptr) as Frame ptr if p = 0 then return 0 if p->refcount = NOREFC then 'showerror "tried to reference a non-refcounted sprite!" else p->refcount += 1 end if return p end function ' This is a convenience function to set a Frame ptr variable, CHANGING the ' Frame ptr it contains. Useful because many frame functions are not in-place. ' (Use frame_draw with trans=NO, write_mask=YES to set the contents of one Frame ' equal to another. There is no way to do so while changing the Frame size ' (it could be implemented, but only for Frames with no views onto them). sub frame_assign cdecl(ptr_to_replace as Frame ptr ptr, new_value as Frame ptr) frame_unload ptr_to_replace *ptr_to_replace = new_value end sub ' See frame_assign. sub surface_assign cdecl(ptr_to_replace as Surface ptr ptr, new_value as Surface ptr) if *ptr_to_replace then gfx_surfaceDestroy(ptr_to_replace) *ptr_to_replace = new_value end sub ' This is for the Frame ptr vector typetable. Ignore. private sub _frame_copyctor cdecl(dest as Frame ptr ptr, src as Frame ptr ptr) *dest = frame_reference(*src) end sub 'Public: ' draws a sprite to a page. scale must be greater than or equal to 1. if trans is false, the ' mask will be wholly ignored. Just like draw_clipped, masks are optional, otherwise use colourkey 0 ' write_mask: ' If the destination has a mask, sets the mask for the destination rectangle ' equal to the mask (or color-key) for the source rectangle. Does not OR them. sub frame_draw(src as Frame ptr, pal as Palette16 ptr = NULL, x as RelPos, y as RelPos, scale as integer = 1, trans as bool = YES, page as integer, write_mask as bool = NO) frame_draw src, intpal(), pal, x, y, scale, trans, vpages(page), write_mask end sub sub frame_draw(src as Frame ptr, pal as Palette16 ptr = NULL, x as RelPos, y as RelPos, scale as integer = 1, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) frame_draw src, intpal(), pal, x, y, scale, trans, dest, write_mask end sub ' Explicitly specify the master palette to use - it is only used if the src is 8-bit ' and the dest is 32-bit. ' Also, the mask if any is ignored. sub frame_draw overload (src as Frame ptr, masterpal() as RGBcolor, pal as Palette16 ptr = NULL, x as RelPos, y as RelPos, scale as integer = 1, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) if src = NULL or dest = NULL then showerror "trying to draw from/to null frame" exit sub end if if dest <> clippedframe then setclip , , , , dest end if x = relative_pos(x, dest->w, src->w) y = relative_pos(y, dest->h, src->h) x += src->offset.x * scale y += src->offset.y * scale frame_draw_internal src, masterpal(), pal, x, y, scale, trans, dest, write_mask end sub private sub frame_draw_internal(src as Frame ptr, masterpal() as RGBcolor, pal as Palette16 ptr = NULL, x as integer, y as integer, scale as integer = 1, trans as bool = YES, dest as Frame ptr, write_mask as bool = NO) if src->surf <> NULL or dest->surf <> NULL then if dest->surf = NULL then showerror "draw_clipped: trying to draw a Surface-backed Frame to a regular Frame" elseif write_mask <> NO or scale <> 1 then showerror "draw_clipped: write_mask and scale not supported with a Surface-backed Frame" end if dim src_surface as Surface ptr if src->surf then src_surface = src->surf else 'debuginfo "frame_draw_internal: unnecessary allocation" if gfx_surfaceCreateFrameView(src, @src_surface) then return end if dim master_pal as RGBPalette ptr if src_surface->format = SF_8bit then ' TODO: Don't do this every single call! if gfx_paletteFromRGB(@masterpal(0), @master_pal) then debug "gfx_paletteFromRGB failed" goto cleanup end if end if draw_clipped_surf src_surface, master_pal, pal, x, y, trans, dest->surf cleanup: if master_pal then gfx_paletteDestroy(@master_pal) end if if src->surf = NULL then gfx_surfaceDestroy(@src_surface) end if else if scale = 1 then draw_clipped src, pal, x, y, trans, dest else draw_clipped_scaled src, pal, x, y, scale, trans, dest, write_mask end if end if end sub 'Return a copy which has been clipped or extended. Extended portions are filled with bgcol. 'Can also be used to scroll (does not wrap around) function frame_resized(spr as Frame ptr, wide as integer, high as integer, shiftx as integer = 0, shifty as integer = 0, bgcol as integer = 0) as Frame ptr dim as Frame ptr ret ret = frame_new(wide, high, , NO, (spr->mask <> NULL), (spr->surf <> NULL)) frame_clear ret, bgcol frame_draw spr, NULL, shiftx, shifty, 1, NO, ret, (spr->surf = NULL) 'trans=NO, write_mask=not for Surfaces return ret end function 'Scale a Frame to given size. Returns a 32-bit Surface-backed Frame. 'masterpal() only used if src is 8-bit. pal can be NULL. function frame_scaled32(src as Frame ptr, wide as integer, high as integer, masterpal() as RGBcolor, pal as Palette16 ptr = NULL) as Frame ptr dim as Surface ptr src_surface, temp if src->surf then src_surface = src->surf else src_surface = frame_to_surface32(src, masterpal(), pal) end if temp = surface_scale(src_surface, wide, high) if src->surf = NULL then gfx_surfaceDestroy(@src_surface) end if dim ret as Frame ptr = frame_with_surface(temp) gfx_surfaceDestroy(@temp) return ret end function 'Public: ' Returns a (copy of the) sprite (any bitdepth) in the midst of a given fade out. ' tlength is the desired length of the transition (in any time units you please), ' t is the number of elasped time units. style is the specific transition. function frame_dissolved(spr as Frame ptr, tlength as integer, t as integer, style as integer) as Frame ptr CHECK_FRAME_8BIT(spr, NULL) 'Return a blank sprite of same size '(Note that Vapourise and Phase Out aren't blank on t==tlength, while others are, unless tlength=0 if t > tlength then return frame_duplicate(spr, YES) 'Return copy. (Actually Melt otherwise has very slight distortion on frame 0.) if t <= 0 then return frame_duplicate(spr) 'by default, sprites use colourkey transparency instead of masks. 'We could easily not use a mask here, but by using one, this function can be called on 8-bit graphics 'too; just in case you ever want to fade out a backdrop or something? dim startblank as integer = (style = 8 or style = 9) dim cpy as Frame ptr cpy = frame_duplicate(spr, startblank, 1) if cpy = 0 then return 0 dim as integer i, j, sx, sy, tog select case style case 0 'scattered pixel dissolve dim prng_state as unsigned integer = cpy->w * tlength dim cutoff as unsigned integer = 2 ^ 20 * t / (tlength - 0.5) for sy = 0 to cpy->h - 1 dim mptr as ubyte ptr = @cpy->mask[sy * cpy->pitch] for sx = 0 to cpy->w - 1 prng_state = (prng_state * 1103515245 + 12345) if (prng_state shr 12) < cutoff then mptr[sx] = 0 end if next next case 1 'crossfade 'interesting idea: could maybe replace all this with calls to generalised fuzzyrect dim m as integer = cpy->w * cpy->h * t * 2 / tlength dim mptr as ubyte ptr dim xoroff as integer = 0 if t > tlength / 2 then 'after halfway mark: checker whole sprite, then checker the remaining (with tog xor'd 1) for sy = 0 to cpy->h - 1 mptr = cpy->mask + sy * cpy->pitch tog = sy and 1 for sx = 0 to cpy->w - 1 tog = tog xor 1 if tog then mptr[sx] = 0 next next xoroff = 1 m = cpy->w * cpy->h * (t - tlength / 2) * 2 / tlength end if 'checker the first m pixels of the sprite for sy = 0 to cpy->h - 1 mptr = cpy->mask + sy * cpy->pitch tog = (sy and 1) xor xoroff for sx = 0 to cpy->w - 1 tog = tog xor 1 if tog then mptr[sx] = 0 m -= 1 if m <= 0 then exit for, for next next case 2 'diagonal vanish i = cpy->w * t * 2 / tlength j = i for sy = 0 to i j = i - sy if sy >= cpy->h then exit for for sx = 0 to j if sx >= cpy->w then exit for cpy->mask[sy * cpy->pitch + sx] = 0 next next case 3 'sink into ground dim fall as integer = cpy->h * t / tlength for sy = cpy->h - 1 to 0 step -1 if sy < fall then memset(cpy->mask + sy * cpy->pitch, 0, cpy->w) else memcpy(cpy->image + sy * cpy->pitch, cpy->image + (sy - fall) * cpy->pitch, cpy->w) memcpy(cpy->mask + sy * cpy->pitch, cpy->mask + (sy - fall) * cpy->pitch, cpy->w) end if next case 4 'squash for i = cpy->h - 1 to 0 step -1 dim desty as integer = cpy->h * (t / tlength) + i * (1 - t / tlength) desty = bound(desty, 0, cpy->h - 1) if desty > i then memcpy(cpy->image + desty * cpy->pitch, cpy->image + i * cpy->pitch, cpy->w) memcpy(cpy->mask + desty * cpy->pitch, cpy->mask + i * cpy->pitch, cpy->w) memset(cpy->mask + i * cpy->pitch, 0, cpy->w) end if next case 5 'melt 'height and meltmap are fixed point, with 8 bit fractional parts '(an attempt to speed up this dissolve, which is 10x slower than most of the others!) 'the current height of each column above the base of the frame dim height(-1 to cpy->w) as integer dim meltmap(cpy->h - 1) as integer for i = 0 to cpy->h - 1 'Gompertz sigmoid function, exp(-exp(-x)) 'this is very close to 1 when k <= -1.5 'and very close to 0 when k >= 1.5 'so decreases down to 0 with increasing i (height) and t 'meltmap(i) = exp(-exp(-7 + 8.5*(t/tlength) + (-cpy->h + i))) * 256 meltmap(i) = exp(-exp(-8 + 10*(t/tlength) + 6*(i/cpy->h))) * 256 next dim poffset as integer = (cpy->h - 1) * cpy->pitch dim destoff as integer for sy = cpy->h - 1 to 0 step -1 for sx = 0 to cpy->w - 1 destoff = (cpy->h - 1 - (height(sx) shr 8)) * cpy->pitch + sx 'if sx = 24 then 'debug sy & " mask=" & cpy->mask[poffset + sx] & " h=" & height(sx)/256 & " dest=" & (destoff\cpy->pitch) & " " & t/tlength 'end if 'potentially destoff = poffset + sx dim temp as integer = cpy->mask[poffset + sx] cpy->mask[poffset + sx] = 0 cpy->image[destoff] = cpy->image[poffset + sx] cpy->mask[destoff] = temp if temp then height(sx) += meltmap(height(sx) shr 8) else 'empty spaces melt quicker, for flop down of hanging swords,etc 'height(sx) += meltmap(height(sx)) * (1 - t/tlength) 'height(sx) += meltmap((height(sx) shr 8) + 16) height(sx) += meltmap(sy) end if next poffset -= cpy->pitch 'mix together adjacent heights so that hanging pieces don't easily disconnect height(-1) = height(0) height(cpy->w) = height(cpy->w - 1) for sx = (sy mod 3) to cpy->w - 1 step 3 height(sx) = height(sx - 1) \ 4 + height(sx) \ 2 + height(sx + 1) \ 4 next next case 6 'vapourise 'vapoury is the bottommost vapourised row dim vapoury as integer = (cpy->h - 1) * (t / tlength) dim vspeed as integer = large(cint(cpy->h / tlength), 1) for sx = 0 to cpy->w - 1 dim chunklength as integer = randint(vspeed + 5) for i = -2 to 9999 if rando() < 0.3 then exit for next dim fragy as integer = large(vapoury - large(i, 0) - (chunklength - 1), 0) 'position to copy fragment from dim chunkoffset as integer = large(vapoury - (chunklength - 1), 0) * cpy->pitch + sx dim poffset as integer = sx for sy = 0 to vapoury if sy >= fragy and chunklength <> 0 then cpy->image[poffset] = cpy->image[chunkoffset] cpy->mask[poffset] = cpy->mask[chunkoffset] chunkoffset += cpy->pitch chunklength -= 1 else cpy->mask[poffset] = 0 end if poffset += cpy->pitch next next case 7 'phase out dim fall as integer = 1 + (cpy->h - 2) * (t / tlength) 'range 1 to cpy->h-1 'blank out top of sprite for sy = 0 to fall - 2 memset(cpy->mask + sy * cpy->pitch, 0, cpy->w) next for sx = 0 to cpy->w - 1 dim poffset as integer = sx + fall * cpy->pitch 'we stretch the two pixels at the vapour-front up some factor dim beamc1 as integer = -1 dim beamc2 as integer = -1 if cpy->mask[poffset] then beamc1 = cpy->image[poffset] if cpy->mask[poffset - cpy->pitch] then beamc2 = cpy->image[poffset - cpy->pitch] if beamc1 = -1 then continue for for sy = fall to large(fall - 10, 0) step -1 cpy->image[poffset] = beamc1 cpy->mask[poffset] = 1 poffset -= cpy->pitch next if beamc2 = -1 then continue for for sy = sy to large(sy - 10, 0) step -1 cpy->image[poffset] = beamc2 cpy->mask[poffset] = 1 poffset -= cpy->pitch next next case 8 'squeeze (horizontal squash) dim destx(spr->w - 1) as integer for sx = 0 to spr->w - 1 destx(sx) = sx * (1 - t / tlength) + 0.5 * (spr->w - 1) * (t / tlength) next for sy = 0 to spr->h - 1 dim destimage as ubyte ptr = cpy->image + sy * cpy->pitch dim destmask as ubyte ptr = cpy->mask + sy * cpy->pitch dim srcmask as ubyte ptr = iif(spr->mask, spr->mask, spr->image) dim poffset as integer = sy * cpy->pitch for sx = 0 to spr->w - 1 destimage[destx(sx)] = spr->image[poffset] destmask[destx(sx)] = srcmask[poffset] poffset += 1 next next case 9 'shrink (horizontal+vertical squash) dim destx(spr->w - 1) as integer for sx = 0 to spr->w - 1 destx(sx) = sx * (1 - t / tlength) + 0.5 * (spr->w - 1) * (t / tlength) next for sy = 0 to spr->h - 1 dim desty as integer = sy * (1 - t / tlength) + (spr->h - 1) * (t / tlength) dim destimage as ubyte ptr = cpy->image + desty * cpy->pitch dim destmask as ubyte ptr = cpy->mask + desty * cpy->pitch dim srcmask as ubyte ptr = iif(spr->mask, spr->mask, spr->image) dim poffset as integer = sy * cpy->pitch for sx = 0 to spr->w - 1 destimage[destx(sx)] = spr->image[poffset] destmask[destx(sx)] = srcmask[poffset] poffset += 1 next next case 10 'flicker dim state as integer = 0 dim ctr as integer 'percent for i = 0 to t dim cutoff as integer = 60 * (1 - i / tlength) + 25 * (i / tlength) dim inc as integer = 60 * i / tlength ctr += inc if ctr > cutoff then i += ctr \ cutoff 'length of gaps increases if i > t then state = 1 ctr = ctr mod 100 end if next if state then frame_clear(cpy) end select return cpy end function function default_dissolve_time(style as integer, w as integer, h as integer) as integer 'squash, vapourise, phase out, squeeze if style = 4 or style = 6 or style = 7 or style = 8 or style = 9 then return w / 5 else return w / 2 end if end function 'Used by frame_flip_horiz and frame_flip_vert private sub flip_image(pixels as ubyte ptr, d1len as integer, d1stride as integer, d2len as integer, d2stride as integer) for x1 as integer = 0 to d1len - 1 dim as ubyte ptr pixelp = pixels + x1 * d1stride for offset as integer = (d2len - 1) * d2stride to 0 step -2 * d2stride dim as ubyte temp = pixelp[0] pixelp[0] = pixelp[offset] pixelp[offset] = temp pixelp += d2stride next next end sub 'not-in-place isometric transformation of a pixel buffer 'dimensions/strides of source is taken from src, but srcpixels specifies the actual pixel buffer 'destorigin points to the pixel in the destination buffer where the pixel at the (top left) origin should be put private sub transform_image(src as Frame ptr, srcpixels as ubyte ptr, destorigin as ubyte ptr, d1stride as integer, d2stride as integer) for y as integer = 0 to src->h - 1 dim as ubyte ptr sptr = srcpixels + y * src->pitch dim as ubyte ptr dptr = destorigin + y * d1stride for x as integer = 0 to src->w - 1 *dptr = sptr[x] dptr += d2stride next next end sub 'Public: ' flips a sprite horizontally. In place: you are only allowed to do this on sprites with no other references sub frame_flip_horiz(spr as Frame ptr) if spr = 0 then exit sub CHECK_FRAME_8BIT(spr) if spr->refcount > 1 then debug "illegal hflip on " & frame_describe(spr) exit sub end if flip_image(spr->image, spr->h, spr->pitch, spr->w, 1) if spr->mask then flip_image(spr->mask, spr->h, spr->pitch, spr->w, 1) end if end sub 'Public: ' flips a sprite vertically. In place: you are only allowed to do this on sprites with no other references sub frame_flip_vert(spr as Frame ptr) if spr = 0 then exit sub CHECK_FRAME_8BIT(spr) if spr->refcount > 1 then debug "illegal vflip on " & frame_describe(spr) exit sub end if flip_image(spr->image, spr->w, 1, spr->h, spr->pitch) if spr->mask then flip_image(spr->mask, spr->w, 1, spr->h, spr->pitch) end if end sub '90 degree (anticlockwise) rotation. 'Unlike flipping functions, not inplace! function frame_rotated_90(spr as Frame ptr) as Frame ptr if spr = 0 then return NULL CHECK_FRAME_8BIT(spr, NULL) dim ret as Frame ptr = frame_new(spr->h, spr->w, 1, (spr->mask <> NULL)) 'top left corner transformed to bottom left corner transform_image(spr, spr->image, ret->image + ret->pitch * (ret->h - 1), 1, -ret->pitch) if spr->mask <> NULL then transform_image(spr, spr->mask, ret->mask + ret->pitch * (ret->h - 1), 1, -ret->pitch) end if return ret end function '270 degree (anticlockwise) rotation, ie 90 degrees clockwise. 'Unlike flipping functions, not inplace! function frame_rotated_270(spr as Frame ptr) as Frame ptr if spr = 0 then return NULL CHECK_FRAME_8BIT(spr, NULL) dim ret as Frame ptr = frame_new(spr->h, spr->w, 1, (spr->mask <> NULL)) 'top left corner transformed to top right corner transform_image(spr, spr->image, ret->image + (ret->w - 1), -1, ret->pitch) if spr->mask <> NULL then transform_image(spr, spr->mask, ret->mask + (ret->w - 1), -1, ret->pitch) end if return ret end function 'Note that we clear masks to transparent! I'm not sure if this is best (not currently used anywhere), but notice that 'frame_duplicate with clr=1 does the same sub frame_clear(spr as Frame ptr, colour as integer = 0) if spr->surf then gfx_surfaceFill(intpal(colour).col, NULL, spr->surf) exit sub end if if spr->image then if spr->w = spr->pitch then memset(spr->image, colour, spr->w * spr->h) else for i as integer = 0 to spr->h - 1 memset(spr->image + i * spr->pitch, colour, spr->w) next end if end if if spr->mask then if spr->w = spr->pitch then memset(spr->mask, 0, spr->w * spr->h) else for i as integer = 0 to spr->h - 1 memset(spr->mask + i * spr->pitch, 0, spr->w) next end if end if end sub 'Warning: this code is rotting; don't assume ->mask is used, etc. Anyway the whole thing should be replaced with a memmove call or two. ' function frame_scroll(spr as Frame ptr, h as integer = 0, v as integer = 0, wrap as bool = NO, direct as bool = NO) as Frame ptr ' CHECK_FRAME_8BIT(spr, NULL) ' ' dim ret as Frame ptr, x as integer, y as integer ' ' ret = frame_clear(spr, -1) ' ' 'first scroll horizontally ' ' if h <> 0 then ' if h > 0 then ' for y = 0 to spr->h - 1 ' for x = spr->w - 1 to h step -1 ' ret->image[y * spr->h + x] = spr->image[y * spr->h - h + x] ' ret->mask[y * spr->h + x] = spr->mask[y * spr->h - h + x] ' next ' next ' if wrap then ' for y = 0 to spr->h - 1 ' for x = 0 to h - 1 ' ret->image[y * spr->h + x] = spr->image[y * spr->h + (x + spr->w - h)] ' ret->mask[y * spr->h + x] = spr->mask[y * spr->h + (x + spr->w - h)] ' next ' next ' end if ' else if h < 0 then ' for y = 0 to spr->h - 1 ' for x = 0 to abs(h) - 1 ' ret->image[y * spr->h + x] = spr->image[y * spr->h - h + x] ' ret->mask[y * spr->h + x] = spr->mask[y * spr->h - h + x] ' next ' next ' if wrap then ' for y = 0 to spr->h - 1 ' for x = abs(h) to spr->w - 1 ' ret->image[y * spr->h - h + x] = spr->image[y * spr->h + x] ' ret->mask[y * spr->h - h + x] = spr->mask[y * spr->h + x] ' next ' next ' end if ' end if ' end if ' ' 'then scroll vertically ' ' if v <> 0 then ' ' end if ' ' if direct then ' deallocate(spr->image) ' deallocate(spr->mask) ' spr->image = ret->image ' spr->mask = ret->mask ' ret->image = 0 ' ret->mask = 0 ' sprite_delete(@ret) ' return spr ' else ' return ret ' end if ' end function /' private sub grabrect(page as integer, x as integer, y as integer, w as integer, h as integer, ibuf as ubyte ptr, tbuf as ubyte ptr = 0) 'this isn't used anywhere anymore, was used to grab tiles from the tileset videopage before loadtileset 'maybe some possible future use? 'ibuf should be pre-allocated dim sptr as ubyte ptr dim as integer i, j, px, py, l if ibuf = null then exit sub CHECK_FRAME_8BIT(vpages(page)) sptr = vpages(page)->image py = y for i = 0 to h-1 px = x for j = 0 to w-1 l = i * w + j 'ignore clip rect, but check screen bounds if not (px < 0 or px >= vpages(page)->w or py < 0 or py >= vpages(page)->h) then ibuf[l] = sptr[(py * vpages(page)->pitch) + px] if tbuf then if ibuf[l] = 0 then tbuf[l] = &hff else tbuf[l] = 0 end if else ibuf[l] = 0 tbuf[l] = 0 end if px += 1 next py += 1 next end sub '/ '========================================================================================== ' Palette16 '========================================================================================== 'This should be replaced with a real hash 'Note that the palette cache works completely differently to the sprite cache, 'and the palette refcounting system too! type Palette16Cache s as string p as Palette16 ptr end type redim shared palcache(50) as Palette16Cache private sub Palette16_delete(f as Palette16 ptr ptr) if f = 0 then exit sub if *f = 0 then exit sub (*f)->refcount = FREEDREFC 'help detect double frees delete *f *f = 0 end sub 'Completely empty the Palette16 cache 'palettes aren't uncached either when they hit 0 references sub Palette16_empty_cache() dim i as integer for i = 0 to ubound(palcache) with palcache(i) if .p <> 0 then 'debug "warning: leaked palette: " & .s & " with " & .p->refcount & " references" Palette16_delete(@.p) 'elseif .s <> "" then 'debug "warning: phantom cached palette " & .s end if .s = "" end with next end sub function Palette16_find_cache(s as string) as Palette16Cache ptr dim i as integer for i = 0 to ubound(palcache) if palcache(i).s = s then return @palcache(i) next return NULL end function sub Palette16_add_cache(s as string, p as Palette16 ptr, fr as integer = 0) if p = 0 then exit sub if p->refcount = NOREFC then 'sanity check debug "Tried to add a non-refcounted Palette16 to the palette cache!" exit sub end if dim as integer i, sec = -1 for i = fr to ubound(palcache) with palcache(i) if .s = "" then .s = s .p = p exit sub elseif .p->refcount <= 0 then sec = i end if end with next if sec > 0 then Palette16_delete(@palcache(sec).p) palcache(sec).s = s palcache(sec).p = p exit sub end if 'no room? pah. redim preserve palcache(ubound(palcache) * 1.3 + 5) Palette16_add_cache(s, p, i) end sub 'Create a new palette which is not connected to any data file function Palette16_new(numcolors as integer = 16) as Palette16 ptr dim ret as Palette16 ptr ret = new Palette16 ret->numcolors = numcolors '--noncached palettes should be deleted when they are unloaded ret->refcount = NOREFC return ret end function function Palette16_new_identity(numcolors as integer = 16) as Palette16 ptr dim ret as Palette16 ptr = Palette16_new(numcolors) for cidx as integer = 0 TO numcolors - 1 ret->col(cidx) = cidx next return ret end function 'pal() is an array of master palette indices, to convert into a Palette16 function Palette16_new_from_indices(pal() as integer) as Palette16 ptr if ubound(pal) > 255 then fatalerror "Palette indices pal() too long!" end if dim ret as Palette16 ptr = Palette16_new(ubound(pal) + 1) for idx as integer = 0 to ubound(pal) ret->col(idx) = pal(idx) next return ret end function 'Loads and returns a palette from the current game (resolving -1 to default palette), 'returning a blank palette if it didn't exist, or returning NULL if default_blank=NO. '(Note that the blank palette isn't put in the cache, so if that palette is later 'added to the game, it won't auto-update.) 'autotype, spr: spriteset type and id, for default palette lookup. function Palette16_load(num as integer, autotype as SpriteType = sprTypeInvalid, spr as integer = 0, default_blank as bool = YES) as Palette16 ptr dim as Palette16 ptr ret = Palette16_load(graphics_file(".pal"), num, autotype, spr) if ret = 0 then if num >= 0 AND default_blank then ' Only bother to warn if a specific palette failed to load. ' Avoids debug noise when default palette load fails because of a non-existant defpal file debug "failed to load palette " & num end if if default_blank then return Palette16_new() end if end if return ret end function 'Loads and returns a palette from a file (resolving -1 to default palette), 'Returns NULL if the palette doesn't exist! 'autotype, spr: spriteset type and id, for default palette lookup. function Palette16_load(fil as string, num as integer, autotype as SpriteType = sprTypeInvalid, spr as integer = 0) as Palette16 ptr dim starttime as double = timer dim hashstring as string dim cache as Palette16Cache ptr if num <= -1 then if autotype = sprTypeInvalid then return 0 end if num = getdefaultpal(autotype, spr) if num = -1 then return 0 end if end if hashstring = trimpath(fil) & "#" & num 'debug "Loading: " & hashstring cache = Palette16_find_cache(hashstring) if cache <> 0 then cache->p->refcount += 1 return cache->p end if dim fh as integer if openfile(fil, for_binary + access_read, fh) then return 0 dim mag as short get #fh, 1, mag if mag = 4444 then ' File is in new file format get #fh, , mag if num > mag then close #fh return 0 end if seek #fh, 17 + 16 * num else ' .pal file is still in ancient BSAVE format, with exactly 100 ' palettes. This shouldn't happen because upgrade() upgrades it. ' Skip 7-byte BSAVE header. seek #fh, 8 + 16 * num end if dim ret as Palette16 ptr = Palette16_new() if ret = 0 then close #fh debug "Could not create palette, no memory" return 0 end if for idx as integer = 0 to 15 dim byt as ubyte get #fh, , byt ret->col(idx) = byt next close #fh ret->refcount = 1 Palette16_add_cache(hashstring, ret) debug_if_slow(starttime, 0.1, fil) return ret end function sub Palette16_unload(p as Palette16 ptr ptr) if p = 0 then exit sub if *p = 0 then exit sub if (*p)->refcount = NOREFC then '--noncached palettes should be deleted when they are unloaded Palette16_delete(p) else (*p)->refcount -= 1 'debug "unloading palette (" & ((*p)->refcount) & " more copies!)" 'Don't delete: it stays in the cache. Unlike the sprite cache, the much simpler 'palette cache doesn't count as a reference end if *p = 0 end sub function Palette16_duplicate(pal as Palette16 ptr) as Palette16 ptr dim ret as Palette16 ptr = palette16_new(pal->numcolors) for i as integer = 0 to ubound(pal->col) ret->col(i) = pal->col(i) next return ret end function 'update a .pal-loaded palette even while in use elsewhere. '(Won't update localpal in a cached PrintStrState... but caching isn't implemented yet) sub Palette16_update_cache(fil as string, num as integer) dim oldpal as Palette16 ptr dim hashstring as string dim cache as Palette16Cache ptr hashstring = trimpath(fil) & "#" & num cache = Palette16_find_cache(hashstring) if cache then oldpal = cache->p 'force a reload, creating a temporary new palette cache->s = "" cache->p = NULL Palette16_load(num) cache = Palette16_find_cache(hashstring) 'copy to old palette structure dim as integer oldrefcount = oldpal->refcount memcpy(oldpal, cache->p, sizeof(Palette16)) oldpal->refcount = oldrefcount 'this sub is silly Palette16_delete(@cache->p) cache->p = oldpal end if end sub function Palette16_describe(pal as Palette16 ptr) as string if pal = 0 then return "'(null)'" dim temp as string = "numcolors - 1 if idx then temp &= "," temp &= hex(pal->col(idx)) next return temp & ">" end function 'Modifies a palette in-place, tinting it with a color sub Palette16_transform_n_match(pal as Palette16 ptr, method as ColorOperator) for idx as integer = 0 to pal->numcolors - 1 dim as integer r, g, b, temp with intpal(pal->col(idx)) if method = copLuminance then 'Best choice for converting to grey r = .r * 0.3 + .g * 0.59 + .b * 0.11 g = r b = r elseif method = copValue then 'Just the value component of HSV, converted to grey r = iif(.r > .g, .r, .g) r = iif(r > .b, r, .b) g = r b = r elseif method = copTintValue then 'Like copValue, but better suited for tinting: 'only return 255 for pure white for better distrinctions, 'and don't return 0 to allow tinting black. temp = iif(.r > .g, .r, .g) temp = iif(temp > .b, temp, .b) temp = (temp * 4 + .r + .g + .b + 255) \ 8 r = temp g = temp b = temp end if end with pal->col(idx) = nearcolor(intpal(), r, g, b) next end sub 'Modifies a palette in-place, tinting it with a color sub Palette16_mix_n_match(pal as Palette16 ptr, byval col as RGBcolor, colfrac as double, method as ColorMixMethod, scale as double = 1.0) for idx as integer = 0 to pal->numcolors - 1 dim as integer mixr, mixg, mixb with intpal(pal->col(idx)) if method = mixBlend then mixr = scale * .r * (1 - colfrac) + col.r * colfrac mixg = scale * .g * (1 - colfrac) + col.g * colfrac mixb = scale * .b * (1 - colfrac) + col.b * colfrac elseif method = mixMult then dim nonmult as double = 255 * (1 - colfrac) mixr = scale * .r * (nonmult + col.r * colfrac) / 255 mixg = scale * .g * (nonmult + col.g * colfrac) / 255 mixb = scale * .b * (nonmult + col.b * colfrac) / 255 end if end with pal->col(idx) = nearcolor(intpal(), mixr, mixg, mixb) next end sub '========================================================================================== ' SpriteSet/Animation/SpriteState '========================================================================================== ' Number of loops/non-forwards branches that can occur in an animation without a ' wait before it's considered to be stuck in an infinite loop. CONST ANIMATION_LOOPLIMIT = 10 redim anim_op_names(animOpLAST) as string anim_op_names(animOpWait) = "wait" anim_op_names(animOpWaitMS) = "wait" anim_op_names(animOpFrame) = "frame" anim_op_names(animOpRepeat) = "repeat" anim_op_names(animOpSetOffset) = "set offset" anim_op_names(animOpRelOffset) = "add offset" redim anim_op_fullnames(animOpLAST) as string anim_op_fullnames(animOpWait) = "Wait (num frames)" anim_op_fullnames(animOpWaitMS) = "Wait (seconds)" anim_op_fullnames(animOpFrame) = "Set frame" anim_op_fullnames(animOpRepeat) = "Repeat animation" anim_op_fullnames(animOpSetOffset) = "Move to offset (unimp)" anim_op_fullnames(animOpRelOffset) = "Add to offset (unimp)" sub set_animation_framerate(ms as integer) ' We bound to 16-200 because set_speedcontrol does the same thing ms_per_frame = bound(ms, 16, 200) end sub function ms_to_frames(ms as integer) as integer return large(1, INT(ms / ms_per_frame)) end function function frames_to_ms(frames as integer) as integer return frames * ms_per_frame end function ' This should only be called from within allmodex constructor SpriteSet(frameset as Frame ptr) if frameset->arrayelem then fatalerror "SpriteSet needs first Frame in array" 'redim animations(0 to -1) frames = frameset num_frames = frameset->arraylen end constructor ' Load a spriteset from file, or return a reference if already cached. ' This increments the refcount, use spriteset_unload to decrement it, NOT 'DELETE'. function spriteset_load(ptno as SpriteType, record as integer) as SpriteSet ptr ' frame_load will load a Frame array with a corresponding SpriteSet dim frameset as Frame ptr frameset = frame_load(ptno, record) if frameset = NULL then return NULL return frameset->sprset end function ' Used to decrement refcount if was loaded with spriteset_load ' (no need to call this when using frame_load and accessing Frame.sprset). sub spriteset_unload(ss as SpriteSet ptr ptr) 'a SpriteSet and its Frame array are never unloaded separately; 'frame_unload is responsible for all refcounting and unloading if ss = NULL ORELSE *ss = NULL then exit sub dim temp as Frame ptr = (*ss)->frames frame_unload @temp *ss = NULL end sub ' Increment refcount. sub SpriteSet.reference() if frames then frame_reference frames end sub function SpriteSet.describe() as string return "spriteset:<" & num_frames & " frames: 0x" & hexptr(frames) _ & ", " & ubound(animations) & " animations>" end function ' Searches for an animation with a certain name, or NULL if there ' are no animations with that name. ' variantname is either just the name of the animation, or the ' name plus a variant separated by a space, like "walk upleft". ' The variant is optional, and the nearest match is picked amongst animations ' which match the name: ' - prefer variant as specified ' - then prefer an animation with blank variant ' - then prefer the first animation (with that name) function SpriteSet.find_animation(variantname as string) as Animation ptr dim as string name, variant dim spacepos as integer = instr(variantname, " ") if spacepos then name = left(variantname, spacepos - 1) variant = mid(variantname, spacepos + 1) else name = variantname end if dim best_match as Animation ptr for idx as integer = 0 to ubound(animations) if animations(idx).name = name then ' Right name, check how good the match is if animations(idx).variant = variant then return @animations(idx) 'Exact match elseif len(animations(idx).variant) then best_match = @animations(idx) 'Prefer nonvariant animations elseif best_match = NULL then best_match = @animations(idx) 'Otherwise, default to the first variant end if end if next return best_match end function ' Append a new blank animation and return pointer function SpriteSet.new_animation(name as string = "", variant as string = "") as Animation ptr redim preserve animations(ubound(animations) + 1) dim ret as Animation ptr = @animations(ubound(animations)) ret->name = name ret->variant = variant return ret end function constructor Animation() end constructor constructor Animation(name as string, variant as string = "") this.name = name this.variant = variant end constructor sub Animation.append(optype as AnimOpType, arg1 as integer = 0, arg2 as integer = 0) redim preserve ops(ubound(ops) + 1) with ops(ubound(ops)) .type = optype .arg1 = arg1 .arg2 = arg2 end with end sub constructor SpriteState(sprset as SpriteSet ptr) ss = sprset ss->reference() 'Inc refcount, because dec it in destructor frame_num = 0 end constructor constructor SpriteState(ptno as SpriteType, record as integer) ss = spriteset_load(ptno, record) frame_num = 0 end constructor destructor SpriteState() spriteset_unload @ss end destructor ' Lookup an animation and start it. See SpriteSet.find_animation() for documentation ' of variantname (animation name plus optional variant). ' Normally an animation specifies how many times it loops (unimplemented), or ends in Repeat ' to loop forever. loopcount <> 0 overrides this, giving a fixed number of ' times to play, or < 0 to repeat forever sub SpriteState.start_animation(variantname as string, loopcount as integer = 0) anim_wait = 0 anim_step = 0 anim_loop = loopcount anim_looplimit = ANIMATION_LOOPLIMIT anim = ss->find_animation(variantname) end sub function SpriteState.cur_frame() as Frame ptr if ss = NULL then return NULL if frame_num < 0 or frame_num >= ss->num_frames then return NULL return @ss->frames[frame_num] end function ' Advance time until the next wait, skipping the current one, and returns number of frames that the wait was for. ' Returns -1 if not waiting, -2 on error. function SpriteState.skip_wait() as integer if anim = NULL then return -2 ' Look at the current op instead of anim_wait, because it might be a wait ' which we haven't looked at yet. with anim->ops(anim_step) if .type <> animOpWait and .type <> animOpWaitMS then return -1 end if dim ret as integer = ms_to_frames(.arg1) anim_wait = ret if animate() = NO then ret = -2 ' Until next wait return ret end with end function ' Advance the animation by one op. ' Returns true on success, false on an error. ' Does not check for infinite loops; caller must do that. function SpriteState.animate_step() as bool if anim = NULL then return NO ' This condition only If the animation doesn't end up looping, re if anim_step > ubound(anim->ops) then debuginfo "anim done" anim_looplimit -= 1 ' anim_loop = 0 means default number of loops if anim_loop = 0 or anim_loop = 1 then anim = NULL return YES end if if anim_loop > 0 then anim_loop -= 1 anim_step = 0 end if with anim->ops(anim_step) select case .type case animOpWait, animOpWaitMS ' These two opcodes are identical, differing only in how ' they are treated by the editor anim_wait += 1 if anim_wait > ms_to_frames(.arg1) then anim_wait = 0 else anim_looplimit = ANIMATION_LOOPLIMIT 'Reset return YES end if case animOpFrame if .arg1 >= ss->num_frames then debug "Animation '" & anim->name & "': illegal frame number " & .arg1 anim = NULL return NO end if frame_num = .arg1 case animOpRepeat ' If a loop count was specified when playing the animation, ' then only loop that many times, otherwise repeat forever if anim_loop > 0 then anim_loop -= 1 if anim_loop = 0 then anim = NULL return YES end if end if anim_step = 0 anim_looplimit -= 1 return YES case animOpSetOffset offset.x = .arg1 offset.y = .arg2 case animOpRelOffset offset.x += .arg1 offset.y += .arg2 case else debug "bad animation opcode " & .type & " in '" & anim->name & "'" anim = NULL return NO end select end with anim_step += 1 return YES end function ' Advance time by one tick. True on success, false on an error/infinite loop function SpriteState.animate() as bool if anim = NULL then return NO while anim_looplimit > 0 if animate_step() = NO then return NO 'stop on error if anim_wait > 0 then return YES 'stop if waiting wend ' Exceeded the loop limit debug "animation '" & anim->name & "' got stuck in an infinite loop" anim = NULL return NO end function /' sub SpriteState.draw(x as integer, y as integer, scale as integer = 1, trans as bool = YES, page as integer) dim as integer realx, realy realx = x + offset.x realy = y + offset.y frame_draw(cur_frame(), pal, realx, realy, scale, trans, page) end sub '/ '========================================================================================== ' Platform specific wrapper functions '========================================================================================== sub show_virtual_keyboard() 'Does nothing on platforms that have real keyboards debuginfo "show_virtual_keyboard" io_show_virtual_keyboard() end sub sub hide_virtual_keyboard() 'Does nothing on platforms that have real keyboards debuginfo "hide_virtual_keyboard" io_hide_virtual_keyboard() end sub sub show_virtual_gamepad() 'Does nothing on platforms that have real keyboards io_show_virtual_gamepad() end sub sub hide_virtual_gamepad() 'Does nothing on platforms that have real keyboards io_hide_virtual_gamepad() end sub sub remap_android_gamepad(player as integer, gp as GamePadMap) 'Does nothing on non-android non-ouya platforms 'debuginfo "remap_android_gamepad " & gp.Ud & " " & gp.Rd & " " & gp.Dd & " " & gp.Ld & " " & gp.A & " " & gp.B & " " & gp.X & " " & gp.Y & " " & gp.L1 & " " & gp.R1 & " " & gp.L2 & " " & gp.R2 io_remap_android_gamepad(player, gp) end sub sub remap_touchscreen_button (button_id as integer, ohr_scancode as integer) 'Does nothing on platforms without touch screens 'debuginfo "remap_android_gamepad " & button_id & " " & ohr_scancode io_remap_touchscreen_button(button_id, ohr_scancode) end sub function running_on_desktop() as bool #IFDEF __FB_ANDROID__ return NO #ELSE return YES #ENDIF end function function running_on_console() as bool 'Currently supports OUYA, GameStick, Fire-TV #IFDEF __FB_ANDROID__ static cached as bool = NO static cached_result as bool if not cached then cached_result = io_running_on_console() cached = YES end if return cached_result #ELSE return NO #ENDIF end function function running_on_ouya() as bool 'Only use this for things that strictly require OUYA, like the OUYA store #IFDEF __FB_ANDROID__ static cached as bool = NO static cached_result as bool if not cached then cached_result = io_running_on_ouya() cached = YES end if return cached_result #ELSE return NO #ENDIF end function function running_on_mobile() as bool #IFDEF __FB_ANDROID__ '--return true for all Android except OUYA static cached as bool = NO static cached_result as bool if not cached then cached_result = NOT io_running_on_console() cached = YES end if return cached_result #ELSE return NO #ENDIF end function function get_safe_zone_margin () as integer '--returns and integer from 0 to 10 representing the percentage ' of the screen edges reserved for TV safe zones. Only returns non-zero ' values on backends that support this feature. dim margin as integer = int(gfx_get_safe_zone_margin() * 100) return large(0, small(10, margin)) end function sub set_safe_zone_margin (margin as integer) 'the margin argument is an integer from 0 to 10 representing ' the percentage of the screen edges reserved for TV safe zones. ' this has no effect on backends that don't support this feature. margin = bound(margin, 0, 10) gfx_set_safe_zone_margin(margin / 100) end sub function supports_safe_zone_margin () as bool 'Returns YES if the current backend supports safe zone margins return gfx_supports_safe_zone_margin() end function sub ouya_purchase_request (dev_id as string, identifier as string, key_der as string) 'Only works on OUYA. Should do nothing on other platforms debug "ouya_purchase_request for product " & identifier gfx_ouya_purchase_request(dev_id, identifier, key_der) end sub function ouya_purchase_is_ready () as bool 'Wait until the OUYA store has replied. Always return YES on other platforms return gfx_ouya_purchase_is_ready() end function function ouya_purchase_succeeded () as bool 'Returns YES if the OUYA purchase was completed successfully. 'Always returns NO on other platforms return gfx_ouya_purchase_succeeded() end function sub ouya_receipts_request (dev_id as string, key_der as string) 'Start a request for reciepts. They may take some time. 'Does nothing if the platform is not OUYA gfx_ouya_receipts_request(dev_id, key_der) end sub function ouya_receipts_are_ready () as bool 'Wait until the OUYA store has replied. Always return YES on other platforms return gfx_ouya_receipts_are_ready () end function function ouya_receipts_result () as string 'Returns a newline delimited list of OUYA product identifiers that 'have already been purchased. 'Always returns "" on other platforms return gfx_ouya_receipts_result() end function sub email_files(address as string, subject as string, message as string, file1 as zstring ptr = NULL, file2 as zstring ptr = NULL, file3 as zstring ptr = NULL) debuginfo "Emailing " & *file1 & " " & *file2 & " " & *file3 & " to " & address debuginfo " subject: '" & subject & "' body: '" & message & "'" #ifdef __FB_ANDROID__ ' Omitted files should be NULL, not "". if len(*file1) = 0 then file1 = NULL if len(*file2) = 0 then file2 = NULL if len(*file3) = 0 then file3 = NULL SDL_ANDROID_EmailFiles(address, subject, message, file1, file2, file3) #else debug "email_files only supported on Android" #endif end sub