///////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015 The 8-Bit Bunch. Licensed under the Apache License, Version 1.1
// (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-1.1>.
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.
///////////////////////////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////////////////////////
// Fixed memory locations
const seed          = $4E     // Incremented continuously by keyboard read routine
const displayEngine = $6000   // main mem (raycaster and tile engine at same location)

///////////////////////////////////////////////////////////////////////////////////////////////////
// Other constants
const CHAR_WND_HEALTH_X = 112

const ANIM_PAUSE_MAX = 150

const CLOCK_ADV_2D_HOURS = 1
const CLOCK_ADV_2D_MINS  = 0
const CLOCK_ADV_2D_SECS  = 0

const CLOCK_ADV_3D_HOURS = 0
const CLOCK_ADV_3D_MINS  = 0
const CLOCK_ADV_3D_SECS  = 30

const CLOCK_ADV_COMBAT_HOURS = 0
const CLOCK_ADV_COMBAT_MINS  = 10
const CLOCK_ADV_COMBAT_SECS  = 0

// Raycaster tables - must match values in render.i
const RAY_TABLE_START = $A800
const RAY_TABLE_SIZE  = $1781

// Max gold
const GOLD_MAX = 20000

include "globalDefs.plh"
include "playtype.plh"
include "gen_images.plh"
include "gen_modules.plh"
include "gen_enemies.plh"
include "gen_players.plh"
include "gen_items.plh"
include "combat.plh"
include "party.plh"
include "store.plh"
include "diskops.plh"
include "godmode.plh"
include "automap.plh"
include "questlog.plh"
include "story.plh"
include "util3d.plh"

///////////////////////////////////////////////////////////////////////////////////////////////////
// Data structures
include "playtype.pla"

export word global  // the global heap object, from which all live objects must be reachable

///////////////////////////////////////////////////////////////////////////////////////////////////
// Predefined functions, for circular calls or out-of-order calls
predef setWindow2()#0
predef initCmds()#0
predef nextAnimFrame()#0
predef checkEncounter(x, y, force)#0
predef doCombat(mapCode, backUpOnFlee)#1
predef clearPortrait()#0
predef showMapName(mapName)#0
predef doRender()#0
predef pause(count)#1
predef printf1(str, arg1)#0
predef printf2(str, arg1, arg2)#0
predef playerDeath()#0
predef saveGame()#1
predef setStat(player, statName, val)#0
predef startGame(firstTime, ask)#0
predef showAnimFrame()#0
predef showParty()#0
predef textureControl(flg)#0
predef clearEncounterZones()#0
predef adjustPartyStat(statName, val)#0
predef printMem()#1

///////////////////////////////////////////////////////////////////////////////////////////////////
// Global variables
export byte mapNum = -1
export byte mapIs3D = 0
export word totalMapWidth
export word totalMapHeight
export word pCurMap
word mapNameHash = 0

export byte needRender = FALSE
byte needShowParty = FALSE
byte renderLoaded = FALSE
byte texturesLoaded = FALSE
byte textDrawn = FALSE
byte anyInteraction = FALSE
byte textClearCountdown = 0
byte forceRawScrDisp = FALSE
export byte isPlural = 0
byte inScript = FALSE
export byte isFloppyVer
export byte isJace

byte scriptModule = 0
byte prevScriptModule = 0

export word skyNum = 9
export word groundNum = 10
export byte portraitNum = 0

word triggerOriginX, triggerOriginY
word triggerTbl

byte cmdKey  // last command key pressed
word cmdTbl[96]   // ASCII $00..$5F
byte frameLoaded = 0
word curEngine = NULL
word pModUtil3d = NULL
word util3d = NULL
export word pResourceIndex = NULL
export word pGlobalTileset = NULL
export byte curMapPartition = 0
export word typeHash = 0
export byte curHeapPct = 0
byte lastMoveDir = $FF

// Queue setMap / teleport, since otherwise script might be replaced while executing
byte q_mapIs3D = 0
byte q_mapNum = 1
word q_x = 0
word q_y = 0
byte q_dir = 0

// Queue of player death
byte q_playerDeath = FALSE

// Script tracking
const MAX_MAP_SCRIPTS = 4
byte nMapScripts = 0
word mapScripts[MAX_MAP_SCRIPTS]

// For decimal conversion and display tabbing
byte decimalBuf[7]
byte fontPosBuf[4]
byte tabBuf[5]

// Animation and lamp tracking
word curPortrait = NULL
byte curPortraitNum = 0
byte preEnginePortraitNum = 0
word curFullscreenImg = NULL
byte animDirCt
byte anyAnims = TRUE
word animPauseCt
byte showingLamp = TRUE
export byte lampFrame = 0
export word lampDir = 1
byte storyMode = FALSE

// Time and clock
export byte prevClockColor, prevClockHour, prevClockMinute
export byte nextSignificantMinute
byte snoozeX0, snoozeX1, snoozeY
word timeEventFunc
byte mapEmuSound = 0

export byte[] S_GAME1_FILENAME = "GAME.1.SAVE"

// Context for lambda functions (in lieu of closures, for now at least)
export word ctx

// Shared string constants
export byte[] S_INTELLIGENCE = "Intelligence"
export byte[] S_STRENGTH = "Strength"
export byte[] S_AGILITY = "Agility"
export byte[] S_STAMINA = "Stamina"
export byte[] S_CHARISMA = "Charisma"
export byte[] S_SPIRIT = "Spirit"
export byte[] S_LUCK = "Luck"
export byte[] S_HEALTH = "Health"
export byte[] S_MAX_HEALTH = "Max health"
export byte[] S_AIMING = "Aiming"
export byte[] S_HAND_TO_HAND = "Hand-to-hand"
export byte[] S_DODGING = "Dodging"
export byte[] S_GOLD = "Gold"
export byte[] S_TIME = "Time"
export byte[] S_XP = "XP"
export byte[] S_SP = "SP"
export byte[] S_BANK_BAL = "Bank bal"
export byte[] S_PACK_SIZE = "Pack size"
export byte[] S_ENTER = "Enter"
export byte[] S_LEAVE = "Leave"
export byte[] S_USE = "Use"
export byte[] S_HIS = "his"
export byte[] S_HER = "her"
export byte[] S_THEIR = "their"

word lastTick = 0

export byte recordMode = 0
export word recordSeed

export byte diskLimit = 0

export word playerUsing = NULL

///////////////////////////////////////////////////////////////////////////////////////////////////
// Definitions used by assembly code
asm _defs

; Use hi-bit ASCII for Apple II
!convtab "../../include/hiBitAscii.ct"

; Headers
!source "../../include/global.i"
!source "../../include/plasma.i"
!source "../../include/mem.i"
!source "../../include/fontEngine.i"
!source "../../include/sound.i"
!source "../../include/prorwts.i"

; Optional debug printing support
DEBUG   = 0

; General use
tmp         = $2
pTmp        = $4
ysav        = $34
ysav1       = $35

; 16-bit random number seed - incremented by ROM kbd routine
seed        = $4E
MAGIC       = $7FED ; largest prime < $8000

; NOTE ABOUT ABSOLUTE CODE ADDRESSING (e.g. STA .var, JMP .label, etc.)
; We cannot use it: this code will be preceded by stubs for the PLASMA routines, hence
; absolute addressing must be done carefully, adding ABS_OFFSET below.
;
; So don't JMP to labels, declare any variables as !byte or !word here, etc. without
; accounting for that.
;
; The way to account for that is to use ABS_OFFSET, and only within this module. This
; method works because this module is the only one loaded at an absolute address.
;
; See examples below.
ABS_OFFSET = (_DEFCNT*5) - 13 ; 13 for plasma's module header (stripped by packer)

end

///////////////////////////////////////////////////////////////////////////////////////////////////
// After scriptDisplayStr is called, we clear the PLASMA string pool. That way, many long strings
// can be used in a single function.
export asm scriptDisplayStr(str)#0
  lda .callScriptDisplay + ABS_OFFSET + 2    ; first time init?
  beq +
.callScriptDisplay
  jsr 0     ; self-modified below
  lda framePtr
  sta outerFramePtr
  lda framePtr+1
  sta outerFramePtr+1
  rts
+ ; first-time init
  lda evalStkL,x
  sta .callScriptDisplay + ABS_OFFSET + 1
  lda evalStkH,x
  sta .callScriptDisplay + ABS_OFFSET + 2
  inx
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// API to call rendering engine (same API for raycaster and tile engine)
asm initDisplay(mapPartition, mapNum, pMapData, x, y, dir)#0
  +asmPlasmNoRet 6
  jmp $6000
end
export asm flipToPage1()#0
  +asmPlasmNoRet 0
  jmp $6003
end
export asm getPos(px, py)#0
  +asmPlasmNoRet 2
  jmp $6006
end
asm setPos(x, y)#0
  +asmPlasmNoRet 2
  jmp $6009
end
export asm getDir()#1   // returns: dir (0-15)
  +asmPlasmRet 0
  jmp $600C
end
export asm setDir(dir)#0
  +asmPlasmNoRet 1
  jmp $600F
end
asm advance(nSteps)#1         // returns: 0 if same pos, 1 if new pos, 2 if new pos and scripted
  +asmPlasmRet 1
  jmp $6012
end
asm setColor(slot, color)#0  // params: slot (0=sky/1=ground), color (0-17)
  +asmPlasmNoRet 2
  jmp $6015
end
asm render(intrOnKbd)#0
  +asmPlasmNoRet 1
  jmp $6018
end
asm _texControl(doLoad)#0
  +asmPlasmNoRet 1
  jmp $601B
end
asm getMapScript()#1
  +asmPlasmRet 0
  jmp $601E
end
asm setAvatar(tileNum)#0 // tile number (in the global tileset)
  +asmPlasmNoRet 1
  jmp $6021
end
asm copyTile(fromX, fromY, toX, toY)#0
  +asmPlasmNoRet 4
  jmp $6024
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Memory copy - Non-overlapping regions only!
export asm memcpy(pSrc, pDst, len, auxWr)#0
  +asmPlasmNoRet 4
  lda evalStkL+3,x    ; source ptr
  sta tmp
  lda evalStkH+3,x
  sta tmp+1
  lda evalStkL+2,x    ; dest ptr
  sta pTmp
  lda evalStkH+2,x
  sta pTmp+1
  ldy evalStkL,x      ; auxWr
  sei                 ; prevent interrupts while possibly in aux
  sta clrAuxWr,y
  lda evalStkH+1,x    ; len hi
  pha
  lda evalStkL+1,x    ; len lo
  tax
  ldy #0
.pglup:
  pla
  sec
  sbc #1
  bcc .part
  pha
- lda (tmp),y
  sta (pTmp),y
  iny
  bne -
  inc tmp+1
  inc pTmp+1
  bne .pglup          ; always taken
.part:
  cpx #0
  beq .done
- lda (tmp),y
  sta (pTmp),y
  iny
  dex
  bne -
.done
  sta clrAuxWr
  cli                 ; inerrupts ok now that we're back in main
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export asm memset(pDst, val, len)#0
  +asmPlasmNoRet 3
  ldy #0
  lda evalStkL+2,x    ; dest ptr
  sta pTmp
  lda evalStkH+2,x
  sta pTmp+1
  lda evalStkL+1,x    ; value
  sta tmp
  lda evalStkL,x      ; len lo
  pha
  lda evalStkH,x      ; len hi
  tax
  beq +
  lda tmp
- sta (pTmp),y
  iny
  bne -
  inc pTmp+1
  dex
  bne -
+ pla
  beq +
  tax
  lda tmp
- sta (pTmp),y
  iny
  dex
  bne -
+ rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export asm readAuxByte(ptr)#1
  +asmPlasmRet 1
  sta pTmp
  sty pTmp+1
  ldy #12
- lda .rdauxb-1+ABS_OFFSET, y
  sta $10-1,y
  dey
  bne -
  jmp $10
.rdauxb
  sei     ; prevent interrupts while in aux mem
  sta setAuxRd
  lda (pTmp),y
  sta clrAuxRd
  cli
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// String building for display with the font engine. Includes plurality processing to handily
// handle things like "Dirt bag(s)" and "his/their"
export asm buildString()#0
  +asmPlasmNoRet 0
  lda cswl
  sta prevCSWL+ABS_OFFSET
  lda cswh
  sta prevCSWL+1+ABS_OFFSET
  lda #<addToString+ABS_OFFSET
  sta cswl
  lda #>addToString+ABS_OFFSET
  sta cswh
  lda #0
  sta inbuf
  rts
addToString:
  sty ysav1
  inc inbuf
  ldy inbuf
  and #$7F
  sta inbuf,y
  ldy ysav1
  rts
prevCSWL !word 0
end

// Complete string building (including plural processing), and return pointer
// to the string (in the input buffer)
export asm finishString(isPlural)#1
!zone {
  +asmPlasmRet 1
  tax         ; test for isPlural == 0
  beq +
  lda #$40    ; for setting V later
+ sta tmp     ; save isPlural flag
  lda prevCSWL+ABS_OFFSET ; put the cout vector back to default
  sta cswl
  lda prevCSWL+1+ABS_OFFSET ; put the cout vector back to default
  sta cswh
  clv         ; V flag for prev-is-alpha
  ldy #0      ; dest offset in Y (will be incremented before store)
  ldx #0      ; source offset in X (will be incremented before load)
  cpx inbuf
  beq .done   ; failsafe: handle zero-length string
.fetch
  inx
  lda inbuf,x ; get next input char
  iny
  and #$7F    ; clear hi-bit for final string, in case it gets interned
  sta inbuf,y ; by default copy the char to output
  ora #$80
  cmp #"("    ; plural processing triggered by parentheses
  bne .notpar
  bvc .notpar ; but only parens directly after alpha char, e.g. preserving "Happy (and safe)."

  dey         ; undo copy of the paren
  stx tmp+1   ; save position in input
  dex         ; needed for failsafe operation
  bit tmp     ; set copy flag (V) initially to same as isPlural flag
.findsl       ; see if there's a slash within the parens
  inx
  cpx inbuf
  lda inbuf,x
  ora #$80    ; normalize hi-bit for comparisons below
  cmp #"/"
  bne +
  php
  pla
  eor #$40    ; flip V flag, meaning singular text is before slash, plural after.
  pha
  plp
+ cmp #")"    ; scan until ending paren
  beq +
  cpx inbuf
  bcc .findsl ; loop to scan next char
  bcs .done   ; failsafe: handle missing end-paren (always taken)
+ ldx tmp+1   ; get back to start of parens
  ; copy mode flag is now in V: if slash present, single=copy,   plural=nocopy
  ;                             if no slash:      single=nocopy, plural=copy
.plup
  inx
  lda inbuf,x
  ora #$80
  cmp #"/"
  bne +
  php
  pla
  eor #$40    ; flip from copying to not-copying, or vice-versa
  pha
  plp
  bcs .plup   ; always taken
+ cmp #")"
  beq .notpar ; stop at closing paren
  bvc .plup   ; if not in copy mode, skip copy
  iny
  and #$7F    ; clear hi-bit for final string, in case it gets interned
  sta inbuf,y ; else do copy
  bne .plup   ; always taken
.notpar
  bit fixedRTS; set prev-is-alpha flag
  cmp #"A"    ; if >= ASCII "A", consider it alpha
  bcs .next
  clv         ; clear prev-is-alpha flag

.next
  cpx inbuf   ; compare src offset to length
  bcc .fetch  ; loop while less than

.done
  sty inbuf   ; save new length
  lda #<inbuf ; return pointer to string
  ldy #>inbuf
  rts
}
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export asm blit(isAux, srcData, dstScreenPtr, nLines, lineSize)#0
  +asmPlasmNoRet 5

  ; Save line size
  sta ysav

  ; Save nLines
  lda evalStkL+1,x
  pha

  ; Save the dest pointer
  lda evalStkL+2,x
  sta pTmp
  lda evalStkH+2,x
  sta pTmp+1

  ; Save the source pointer
  lda evalStkL+3,x
  sta tmp
  lda evalStkH+3,x
  sta tmp+1

  ; Save aux/main flag
  lda evalStkL+4,x
  lsr                 ; to carry bit
  bcc +
  ldy #15       ; put aux copy routine in zero page
- lda .cpaux + ABS_OFFSET,y
  sta $10,y
  dey
  bpl -
+ pla       ; get line count
  tax
--
  ldy ysav  ; get byte count
  dey
  bcc +
  jsr $10   ; copy pixel bytes (aux version)
  bcs ++
+
- lda (tmp),y
  sta (pTmp),y
  dey
  bpl -
++
  php
  lda tmp   ; advance to next row of data
  clc
  adc ysav
  sta tmp
  bcc +
  inc tmp+1
+ jsr NextScreenLine  ; and next screen line
  plp
  dex
  bne --     ; Loop until we've done all rows.
  rts
.cpaux
  sei        ; avoid interrupts while reading aux
  sta setAuxRd
- lda (tmp),y
  sta (pTmp),y
  dey
  bpl -
  sta clrAuxRd
  cli
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
asm vline(dstScreenPtr, val, nLines)#0
  +asmPlasmNoRet 3

  ; Save number of lines
  pha

  ; Save value
  lda evalStkL+1,x
  sta tmp

  ; Save the dest pointer
  lda evalStkL+2,x
  sta pTmp
  lda evalStkH+2,x
  sta pTmp+1

  pla       ; line count
  tax
- ldy #0
  lda tmp
  sta (pTmp),y
  jsr NextScreenLine
  dex
  bne -
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Simply retrieve the X register. Used to double-check that we're not leaking PLASMA eval
// stack entries.
asm getXReg()#1
  +asmPlasmRet 0
  txa
  ldy #0
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate 16-bit hash of a buffer. Note: ignores anything beyond 256 bytes.
export asm hashBuffer(ptr, len)#1
  +asmPlasmRet 2
  lda evalStkL+1,x  ; first arg is buffer pointer
  sta pTmp
  lda evalStkH+1,x
  sta pTmp+1
  lda evalStkL,x    ; second arg is length
  tax
  ldy #0
  sty tmp
  tya
- clc
  adc (pTmp),y
  bcc +
  inc tmp
+ lsr
  ror tmp
  bcc +
  ora #$80
+ lsr
  ror tmp
  bcc +
  ora #$80
+ iny
  dex
  bne -
  ldy tmp
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a string to the current character output vector
export asm puts(str)#0
  +asmPlasmNoRet 1
  sta pTmp
  lda #'!'
  ldx #1
  sty pTmp+1
  beq +       ; safety: print '!' instead of null string
  ldy #0
  lda (pTmp),y
  tax
  beq ++      ; handle empty string
  iny
- lda (pTmp),y
+ ora #$80
  +safeCout
  iny
  dex
  bne -
++ rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a character from the keyboard
export asm rdkey()#1
  +asmPlasmRet 0
  +safeRdkey
  ldy #0
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print part of a string, until we hit the end or a '%' code. Return how far we got, or -1 for end.
asm partialPrintf(str, pos)#1
!zone {
  +asmPlasmRet 2
  lda evalStkL+1,x    ; get string pointer
  sta pTmp
  lda evalStkH+1,x
  sta pTmp+1
  ldy #0
  lda (pTmp),y        ; get length byte
  sec
  sbc evalStkL,x      ; minus offset
  sta tmp             ; to count of characters left to print
  bcc .eos            ; avoid overrunning
  beq .eos
  lda evalStkL,x      ; get desired offset into string
  tay
  iny                 ; increment past length byte
- lda (pTmp),y
  ora #$80
  cmp #'%'            ; stop if we hit % code
  beq +
  +safeCout
  iny
  dec tmp             ; otherwise go until end of string
  bne -
.eos
  ldy #$FF            ; if we hit end of string, return -1
  tya
  rts
+ dey                 ; adjust back for length byte
  tya                 ; that's the lo byte of return
  ldy #0              ; hi byte of return is zero
  rts
}
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a 16-bit hex value
export asm printHex(num)#0
  +asmPlasmNoRet 1
  pha
  tya
  +safePrbyte
  pla
  jmp _safePrbyte
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a single character
export asm printChar(ch)#0
  +asmPlasmNoRet 1
  ora #$80
  jmp _safeCout
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a carriage return
export asm crout()#0
  +asmPlasmNoRet 0
  lda #$8D
  jmp _safeCout
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Send a command to the memory manager
export asm mmgr(cmd, wordParam)#1
  +asmPlasmRet 2
  jsr .setmmgr+ABS_OFFSET
  jsr mainLoader          ; ret value in X=lo/Y=hi
  txa                     ;    to A=lo/Y=hi for asmPlasm
  rts
.setmmgr
  lda evalStkL+1,x        ; command code
  pha
  ldy evalStkH,x          ; address (or other param)... hi byte in Y
  lda evalStkL,x
  tax                     ;     ...lo byte in X
  pla
  rts
end

// Aux version of memory manager command
export asm auxMmgr(cmd, wordParam)#1
  +asmPlasmRet 2
  jsr .setmmgr+ABS_OFFSET
  jsr auxLoader           ; ret value in X=lo/Y=hi
  txa                     ;    to A=lo/Y=hi for asmPlasm
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get mem mgr's flag telling if this is the floppy version or not
asm getFloppyFlg()#1
  +asmPlasmRet 0
  lda floppyFlg
  ldy #0
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Generate a sound on the Apple II speaker
export asm genSound(dnnoise, dnvelo, dndelay, upnoise, upvelo, updelay, dur)#0
  +asmPlasmNoRet 7
  jmp genSound
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to clear the current text window.
// Parameters: top, bottom, left, right
export asm setWindow(top, bottom, left, right)#0
  +asmPlasmNoRet 4
  jmp SetWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the font engine's current text window
// Returns: top, bottom, left, right
export asm getWindow()#4
  bit setLcRW+lcBank2
  jmp GetWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the cursor position - returns X, Y
export asm getCursor()#2
  bit setLcRW+lcBank2
  jmp GetCursor
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a keyboard string up to 40 chars using the Font Engine. Returns a zero-terminated string
// in the input buffer at $200, with the length stored in $2FF.
asm rawGetStr()#1
  +asmPlasmRet 0
  jmp GetStr
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to clear the current text window
export asm clearWindow()#0
  +asmPlasmNoRet 0
  jmp ClearWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to copy the current text window to hi-res page 2 (or vice-versa)
// If flip=0, copies page 1 to page 2
// If flip=$60, copies page 2 to page 1
export asm copyWindow(flip)#0
  +asmPlasmNoRet 1
  tay
  jmp CopyWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a character using the font engine.
export asm displayChar(ch)#0
  +asmPlasmNoRet 1
  jmp DisplayChar
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a string using the font engine.
asm internalDisplayStr(str)#0
  +asmPlasmNoRet 1
  jmp DisplayStr
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate string width using the font engine.
export asm calcWidth(pStr)#1
  +asmPlasmRet 1
  jmp CalcWidth
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the address of the given hi-res screen line
export asm getScreenLine(n)#1
  +asmPlasmRet 1
  jmp GetScreenLine
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a string using the font engine but not its parser. Also, interpret "^A" as ctrl chars.
export asm rawDisplayStr(pStr)#0
  +asmPlasmNoRet 1
  sta pTmp
  sty pTmp+1
  ldy #0
  lda (pTmp),y
  sta tmp
- cpy tmp
  bcc +
  rts
+ iny
  lda (pTmp),y
  ora #$80
  cmp #"^"
  bne +
  iny
  lda (pTmp),y
  cmp #"^"
  beq +
  and #$1F
  ora #$80
+ sty tmp+1
  jsr DisplayChar
  ldy tmp+1
  bne -
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Random number generator. A standard linear congruential prng featuring a full cycle (i.e.
// every value from 1..MAGIC-1 is part of the sequence).
asm internal_rand16()#1
!zone {
  +asmPlasmRet 0
  ldx #6        ; start check = 1, plus 5 loops to mul by 32 = 6 total
  ldy seed
  lda seed+1
  and #$7F      ; in case kbd routine has advanced past MAGIC
  sta seed+1
  bne .chk      ; zero check - hi byte
  tya
  bne .chk      ; zero check - lo byte
  lda #1        ; force 0 seed to go to 1
  bne .ret      ; always taken
.shift
  asl
  tay
  rol seed+1
.chk
  cpy #<MAGIC
  lda seed+1
  sbc #>MAGIC
  bcc .next
  sta seed+1
  tya
  sbc #<MAGIC
  !byte $C9  ; CMP imm - to skip next opcode
.next
  tya
  dex
  bne .shift   ; 32 / 25
.ret
  sta seed
  ldy seed+1
  rts
}
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear the text-mode screen, and put the text cursor at the top of it.
export asm textHome()#0
  +asmPlasmNoRet 0
  +safeHome
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Compare two strings for equality, ignoring case.
export asm streqi(a, b)#1
  +asmPlasmRet 2
  sta tmp
  sty tmp+1
  tya
  beq .null
  lda evalStkL+1,x
  sta pTmp
  lda evalStkH+1,x
  sta pTmp+1
  beq .null
  ldy #0
  lda (tmp),y
  cmp (pTmp),y
  beq +            ; if lengths not equal, strings can't be equal
.noteqi
  lda #0
  tay
  rts
+ tax              ; count up to (verified same) length of the strings
- iny
  lda (tmp),y
  cmp #('z'&$7F)+1 ; convert to upper case
  bcs +
  cmp #'a'&$7F
  bcc +
  sbc #$20
+ sta ysav
  lda (pTmp),y
  cmp #('z'&$7F)+1 ; convert to upper case
  bcs +
  cmp #'a'&$7F
  bcc +
  sbc #$20
+ cmp ysav
  bne .noteqi      ; abort on inequality
  dex
  bne -
  lda #1
  ldy #0           ; okay, they're equal. Return 1 (not just any char; so that PLASMA when...is can work)
  rts
.null
  lda #$13         ; inverse 'S'
  sta $7D0
  brk
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Multiply 16 bit number by 8-bit ratio, and drop lower 8 bits of result. In effect this scales
// num by the approximate ratio 0=0% .. 128=50% .. 255=99%
asm mulRatio(num, ratio)#1
  +asmPlasmRet 2
.ratio = evalStkL
.numL  = evalStkL+1
.numH  = evalStkH+1
.numHH = tmp
.accLL = tmp+1
.accL  = pTmp
.accH  = pTmp+1
  lda .ratio,x        ; save ratio
  ldy #0
  sty .numHH          ; zero what will become upper 8 bits of num
  sty .accLL          ; clear accumulation area
  sty .accL
  sty .accH
  ldy #8              ; loop over 8 bits of ratio
- lsr                 ; get next bit
  bcc +               ; skip add if clear
  pha
  clc
  lda .numL,x         ; 24-bit add
  adc .accLL
  sta .accLL
  lda .numH,x
  adc .accL
  sta .accL
  lda .numHH
  adc .accH
  sta .accH
  pla
+ asl .numL,x         ; shift number up
  rol .numH,x
  rol .numHH
  dey                 ; and loop again
  bne -
  ldy .accH           ; final result in Y/A
  lda .accL
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Check for script(s) attached to the given location, and establish the array of map scripts.
// Does not call any of them -- that's the job of scriptEvent().
// Structure of trigger table:
//   Y (1 byte)
//   entry length (1 byte, includes Y and this length)
//     X (1 byte)
//     script ptr (2 bytes)
//     X
//     script ptr...
//   Y (or $FF for end of table)
//     ...
asm scanScripts(x, y, triggerTbl, mapScripts)#1
!zone {
.pMapScripts = tmp
.pTriggerTbl = pTmp
.outOffset = ysav
.limit = ysav1
.xCoord = evalStkL+3
.yCoord = evalStkL+2
  +asmPlasmRet 4
  sta .pMapScripts      ; pointer to output mapScripts
  sty .pMapScripts+1
  lda evalStkL+1,x      ; pointer to trigger tbl
  sta .pTriggerTbl
  lda evalStkH+1,x
  sta .pTriggerTbl+1
  lda #0                ; zero initial output offset
  sta .outOffset
.ylup
  ldy #0
  lda (.pTriggerTbl),y
  cmp #$FF              ; Y=$FF marks end of trigger table
  bne +
.done
  lda .outOffset
  lsr                   ; divide offset by 2 to get # of output scripts
  ldy #0
  rts                   ; all done
+ iny
  cmp .yCoord,x         ; check for Y coordinate we want
  beq .goty
  lda (.pTriggerTbl),y  ; not found, add offset to find next Y section
  bne +
  brk
+ clc
  adc .pTriggerTbl
  sta .pTriggerTbl
  bcc .ylup
  inc .pTriggerTbl+1
  bne .ylup             ; always taken
.goty
  lda (.pTriggerTbl),y  ; get # of bytes in X table
  sta .limit
  iny
.xlup
  lda (.pTriggerTbl),y
  iny
  cmp .xCoord,x
  bne .nextx
  tya
  pha                   ; save trigger table pointer
  iny
  lda (.pTriggerTbl),y  ; hi byte of script
  pha
  dey
  lda (.pTriggerTbl),y  ; lo byte of script
  ldy .outOffset
  sta (.pMapScripts),y
  iny
  pla
  sta (.pMapScripts),y
  iny
  sty .outOffset
  pla                   ; back to scanning trigger tbl
  tay
.nextx
  iny
  iny
  cpy .limit            ; stop at end of X/ptr list
  bcs .done
  bcc .xlup             ; always taken
}
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Params: cmd | open<<8, filename, addr, size. Returns: status (for open only)
export asm callProRWTS(cmdPlusOpenFlg, filename, addr, size)#1
    +asmPlasmRet 4
    sei   ; inhibit interrupts during aux operations
    ; Params come to us from PLASMA in reverse order
    sta setAuxZP
    sta sizelo
    sty sizehi

    sta clrAuxZP
    lda evalStkL+1,x  ; addr
    ldy evalStkH+1,x
    sta setAuxZP
    sta ldrlo
    sty ldrhi

    sta clrAuxZP
    lda evalStkL+2,x  ; filename
    ldy evalStkH+2,x
    sta setAuxZP
    sta namlo
    sty namhi

    sta clrAuxZP
    lda evalStkL+3,x  ; openFlg
    ldy evalStkH+3,x  ; cmd
    sta setAuxZP
    sta reqcmd
    lda #0
    sta auxreq

    bit setLcRW+lcBank1
    bit setLcRW+lcBank1
    lda #$4C      ; set up JMP to the requested proRWTS vector
    sta $44
    sty $45
    lda #>proRWTS
    sta $46
    jsr $44
    lda status
    ldy #0
    bit setLcRW+lcBank2
    bit setLcRW+lcBank2
    sta clrAuxZP
    cli   ; interrupts ok again
    rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// The following only used for speed testing
//asm readNoSlotClock(dstBuf)#0  // param: dstBuf (will receive 8 BCD bytes)
//!zone {
//  +asmPlasmNoRet 1
//  ; record dst ptr
//  sta tmp
//  sty tmp+1
//  ; obtain a pointer to our little table of magic values
//  lda #$60
//  sta pTmp
//.base = *+2
//  jsr pTmp
//  tsx
//  dex
//  dex
//  txs
//  pla
//  sta pTmp
//  pla
//  sta pTmp+1
//  ldy #<(.tbl - .base)
//  ; record state of slot ROM, then turn on C3 ROM
//  sei
//  lda $CFFF
//  pha
//  sta $C300
//  lda $C304
//  ldx #8
//.wr1:
//  lda (pTmp),y
//  sec
//  ror
//.wr2:
//  bcs +
//  bit $C300
//  bcc ++
//+ bit $C301
//++ lsr
//  bne .wr2
//  iny
//  dex
//  bne .wr1
//  ldy #7
//.rd1:
//  ldx #8
//.rd2:
//  lda $C304
//  lsr
//  ror pTmp
//  dex
//  bne .rd2
//  lda pTmp
//  sta (tmp),y
//  dey
//  bpl .rd1
//  ; restore slot ROM state
//  pla
//  bmi +
//  sta $CFFF
//+ cli
//  rts
//.tbl !byte $C5,$3A,$A3,$5C,$C5,$3A,$A3,$5C
//} ; end zone
//end
//
//def getTick()
//  byte timeBuf[8]
//  word tick
//  readNoSlotClock(@timeBuf)
//  tick = (timeBuf[7] & $F) + ((timeBuf[7] >> 4) * 10)
//  tick = tick + ((timeBuf[6] & $F) * 100) + ((timeBuf[6] >> 4) * 1000)
//  return tick + (((timeBuf[5] & $F) % 5) * 6000)
//end
//
//def tickDiff(tStart, tEnd)
//  word diff
//  diff = tEnd - tStart
//  if diff >= 0; return diff; fin
//  return diff + 30000
//end
//
//def prtick(str)#0
//  if lastTick
//    printf2("%s:%d ", str, tickDiff(lastTick, getTick()))
//  fin
//  lastTick = getTick()
//end

///////////////////////////////////////////////////////////////////////////////////////////////////
// General methods

///////////////////////////////////////////////////////////////////////////////////////////////////
// Generate a sound that indicates being blocked or failing
export def beep()#0
  genSound(0, 600, 60200, 0, 0, 60200, 30)
end

export def rand16()#1
  word result
  if recordMode
    *seed = recordSeed
    //printf1("%x:", recordSeed)
    result = internal_rand16()
    recordSeed = *seed
    //printf1("%x ", recordSeed)
    return result
  fin
  return internal_rand16()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Fatal error: print message and stop the system.
// Doesn't really return a value, but can be handy for chaining.
export def fatal(msg)#1
  return mmgr(FATAL_ERROR, msg)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the max of two signed 16-bit numbers
export def max(a, b)#1
  return a > b ?? a :: b
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the min of two signed 16-bit numbers
export def min(a, b)#1
  return a < b ?? a :: b
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the absolute value of a number
export def abs(n)#1
  return n < 0 ?? -n :: n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert a lower-case character to upper-case (or return unchanged if it's not lower-case)
export def charToUpper(c)#1
  return (c >= 'a' and c <= 'z') ?? (c - $20) :: c
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def recordChar(ch)#0
  if ch < $20
    printChar('^')
    ch = ch + $40
  fin
  printChar(ch)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def recordDisplay(str)#0
  word i
  puts("DISP:")
  for i = 1 to ^str
    recordChar(^(str + i) & $7F)
  next
  crout
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def displayStr(str)#0
  if recordMode; recordDisplay(str); fin
  internalDisplayStr(str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a string from the keyboard using the font manager, and intern it to the heap.
export def getStringResponse()#1
  word p
  rawGetStr()
  rawDisplayStr("\n") // so Outlaw user doesn't have to remember to make a newline
  if recordMode
    puts("STRING:"); puts($200); crout
  fin
  return mmgr(HEAP_INTERN, $200)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert signed decimal to string in decimalBuf (@decimalBuf returned)
def convertDec(n)#1
  word n0
  word p
  p = @decimalBuf + 1
  if n < 0;     ^p = '-';           p=p+1; n = -n;      fin
  n0 = n
  if n0 > 9999; ^p = '0' + n/10000; p=p+1; n = n%10000; fin
  if n0 > 999;  ^p = '0' + n/1000;  p=p+1; n = n%1000;  fin
  if n0 > 99;   ^p = '0' + n/100;   p=p+1; n = n%100;   fin
  if n0 > 9;    ^p = '0' + n/10;    p=p+1; n = n%10;    fin
  ^p = '0' + n; p=p+1
  decimalBuf[0] = p - @decimalBuf - 1 // record final length of string
  return @decimalBuf
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert byte to 3-char string in decimalBuf, suitable for font engine, e.g. ^T065
// (@fontPosBuf returned so as to not disturb decimalBuf)
def convert3Dec(n)#1
  fontPosBuf[0] = 3
  fontPosBuf[1] = '0' + (n / 100); n = n%100
  fontPosBuf[2] = '0' + (n / 10);  n = n%10
  fontPosBuf[3] = '0' + n
  return @fontPosBuf
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a formatted string a'la C printf, with up to three parameters.
export def printf3(str, arg1, arg2, arg3)#0
  word pos
  word curArg
  word p
  if !str
    printChar('!')  // Safety valve for NULL string pointer
    return
  fin
  pos = 0
  curArg = @arg1
  while TRUE
    pos = partialPrintf(str, pos)
    if pos < 0
      break
    fin
    p = str + pos + 2
    when ^p
      is 'd'  // %d = decimal
        puts(convertDec(*curArg)); break
      is 's' // %s = string
        puts(*curArg); break
      is 'D' // %D = 3-char decimal suitable for font engine ctrl codes
        puts(convert3Dec(*curArg)); break
      is 'c'  // %c = character
        printChar(*curArg); break
      is 'x' // %x = hex with '$'
        printHex(*curArg); break
      is '%' // %% = perfect
        printChar('%'); break
      otherwise
        printHex(^p); fatal("Unknown % code")
    wend
    curArg = curArg + 2
    pos = pos + 2
  loop
end

export def printf1(str, arg1)#0; printf3(str, arg1, 0, 0); end
export def printf2(str, arg1, arg2)#0; printf3(str, arg1, arg2, 0); end

// Like printf, but displays text using font engine
export def displayf3(str, arg1, arg2, arg3)#0
  buildString()
  printf3(str, arg1, arg2, arg3)
  displayStr(finishString(isPlural))
end

export def displayf1(str, arg1)#0; displayf3(str, arg1, 0, 0); end
export def displayf2(str, arg1, arg2)#0; displayf3(str, arg1, arg2, 0); end

// Like printf, but buffers string in $200
export def sprintf3(str, arg1, arg2, arg3)#1
  buildString()
  printf3(str, arg1, arg2, arg3)
  return finishString(isPlural)
end

export def sprintf1(str, arg1); return sprintf3(str, arg1, 0, 0); end
export def sprintf2(str, arg1, arg2); return sprintf3(str, arg1, arg2, 0); end

// Like printf, but displays text using font engine
export def rawDisplayf1(str, arg1)#0; rawDisplayStr(sprintf3(str, arg1, 0, 0)); end
export def rawDisplayf2(str, arg1, arg2)#0; rawDisplayStr(sprintf3(str, arg1, arg2, 0)); end
export def rawDisplayf3(str, arg1, arg2, arg3)#0; rawDisplayStr(sprintf3(str, arg1, arg2, arg3)); end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the cursor position in the font engine
export def setCursor(x, y)#0
  rawDisplayf2("^V%D^T%D", y, x)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def parseDec(str)#1
  word n
  word pend
  word p
  byte neg
  neg = FALSE
  n = 0
  p = str + 1
  pend = p + ^str
  while p < pend
    if p == (str+1) and ^p == '-'
      neg = TRUE
    elsif ^p >= '0' and ^p <= '9'
      n = (n*10) + (^p - '0')
    else
      break
    fin
    p = p+1
  loop

  if neg; return -n; fin
  return n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Used in record mode to print out a keystroke. Also handles caret -> ctrl translation during
// playback of recordings.
export def recordKey()#1
  byte key
  key = ^kbd & $7F
  ^kbdStrobe
  if recordMode
    if key == '^'
      while ^kbd < 128; loop
      key = (^kbd & $7F) - $40
      ^kbdStrobe
    fin
    puts("KEY:")
    recordChar(key)
    puts(" SEED:$")
    printHex(recordSeed)
    crout
  fin
  return key
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a keystroke and convert it to upper case
export def getUpperKey()#1
  byte key

  // Make sure same text displays on both hi-res while animating
  if anyAnims and textDrawn and mapIs3D and texturesLoaded
    copyWindow(0)
  fin

  // Now wait for a key, and animate while doing so.
  while ^kbd < 128
    if recordMode
      pause(30000)
    else
      // pause() will terminate on keypress, returning the count it did
      *seed = *seed + pause(30000)
    fin
  loop
  key = recordKey
  return charToUpper(key)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a single char from the keyboard, and intern it (as a string) to the heap.
export def getCharResponse()#1
  if needShowParty; showParty(); fin
  ^$200 = 1
  ^$201 = getUpperKey()
  return mmgr(HEAP_INTERN, $200)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Select the num'th entry in the list for which the selector returns TRUE. A NULL selector
// is considered to always be TRUE.
export def select(p, sel, num)#1
  while p
    if !sel
      if !num; break; fin
      num--
    elsif sel(p)
      if !num; break; fin
      num--
    fin
    p = p=>p_nextObj
  loop
  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def first(p, sel)#1
  return select(p, sel, 0)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def index(p, num)#1
  return select(p, NULL, num)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def sum(p, sel, func)#1
  word sum
  sum = 0
  while p
    if !sel
      sum = sum + func(p)
    elsif sel(p)
      sum = sum + func(p)
    fin
    p = p=>p_nextObj
  loop
  return sum
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def forSome(p, sel, do)#0
  while p
    if !sel
      do(p)#0
    elsif sel(p)
      do(p)#0
    fin
    p = p=>p_nextObj
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def forEach(p, do)#0
  forSome(p, NULL, do)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Pause for a specified count period, advancing the animation periodically. Terminates early
// if a key is pressed. In either case, returns the number of counts waited.
export def pause(count)#1
  word i, prevSeed
  if recordMode; prevSeed = recordSeed; fin
  for i = 0 to count
    if ^kbd >= 128; break; fin
    animPauseCt--
    if animPauseCt < 0
      nextAnimFrame()  // also resets animPauseCt
    fin
  next
  if recordMode; recordSeed = prevSeed; fin
  return i
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate markup on a price, where ratio is an 8.8 fixed-point number. Some approx ratios:
// $0000 = 0%
// $0026 = 15%
// $0080 = 50%
// $0100 = 100%
// $0180 = 150%
export def addRatio(start, ratio)#1
  word markup
  markup = 0
  while ratio > 255
    markup = markup + start
    ratio = ratio - 256
  loop
  return markup + mulRatio(start, ratio)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def percentToRatio(pct)#1
  return addRatio(pct, 656) // Scale 0..100 to 0..255
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def addPercent(start, pct)#1
  return addRatio(start, percentToRatio(pct))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def encodeDice(nDice, dieSize, add)#1  // ndice=0..15, dieSize=0..15, add=0..255
  return (nDice << 12) | (dieSize << 8) | add
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def rollDice(encoded)#1
  byte nDice, dieSize
  word result
  nDice = (encoded >> 12) & $F  // must mask off replicated hi-bits
  dieSize = (encoded >> 8) & $F
  result = encoded & $FF // initial add
  while nDice
    result = result + (rand16() % dieSize) + 1
    nDice--
  loop
  return result
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Look up the partition for a resource.
// sectioNum: 0=version num (do not use here), 1=map2d, 2=map3d, 3=portrait, 4=story
export def lookupResourcePart(sectionNum, resourceNum)#1
  word ptr
  byte n, i

  // Special case: main frame img laods before resource index
  if !pResourceIndex; return 1; fin

  // Skip to the requested section (starting just after version num)
  ptr = pResourceIndex
  while sectionNum > 0
    ptr = ptr + readAuxByte(ptr) + 1
    sectionNum--
  loop

  // And grab the number from that section's table
  n = readAuxByte(ptr)
  if resourceNum > n; fatal("lkup1"); fin
  n = readAuxByte(ptr + resourceNum)

  // If resource is on the current map's disk, prefer that
  if n & (1<<(curMapPartition-1))
    return curMapPartition
  fin

  // Otherwise return the first disk it's on
  for i = 0 to 7
    if n & (1<<i); return i+1; fin
  next
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the sky color (relevant to 3D display only)
export def setSky(num)#0
  if num <> skyNum
    skyNum = num
    setColor(0, skyNum)
    needRender = TRUE
    showingLamp = mapIs3D and texturesLoaded and (skyNum == 0 or skyNum == 8)
    if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin // to update image
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the ground color (relevant to 3D display only)
export def setGround(num)#0
  if num <> groundNum
    groundNum = num
    setColor(1, groundNum)
    needRender = TRUE
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def heapCollect()#0
  word pct
  mmgr(CHECK_MEM, 0)
  global=>w_heapSize = mmgr(HEAP_COLLECT, 0) - HEAP_BOTTOM
  curHeapPct = min(99, max(0, ((global=>w_heapSize / 10) * 100) / (HEAP_SIZE / 10)))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def resetAnimPause()#0
  anyAnims = TRUE  // for now; might get cleared if we discover otherwise on advance
  animDirCt = 1
  animPauseCt = ANIM_PAUSE_MAX
  showingLamp = mapIs3D and texturesLoaded and (skyNum == 0 or skyNum == 8)
  lampFrame = 0
  lampDir = 1
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the Frame Image, and lock it.
export def loadFrameImg(img)#0
  // Skip redundant reload
  if frameLoaded == img; return; fin

  // Free prev img and/or portrait (if any)
  if curFullscreenImg
    // must clear img directly, to avoid loading main frame img only to replace it
    auxMmgr(FREE_MEMORY, curFullscreenImg)
    curFullscreenImg = NULL
  fin
  clearPortrait()

  // Make room in aux mem by throwing out textures
  textureControl(FALSE)

  // Load the image data into aux mem
  if img
    auxMmgr(START_LOAD, lookupResourcePart(4, img))
    if img == 1
      auxMmgr(SET_MEM_TARGET, $4000) // well above where expander loads at startup
    fin
    curFullscreenImg = auxMmgr(QUEUE_LOAD, img<<8 | RES_TYPE_SCREEN)
    auxMmgr(FINISH_LOAD, 0)

    resetAnimPause()

    // And show the first frame of the screen image
    ^EMUSIG_FULL_COLOR
    showAnimFrame()
  else
    curFullscreenImg = NULL
    anyAnims = FALSE
  fin

  frameLoaded = img

  // Do not render over the image
  needRender = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the map name bar
export def setWindow1()#0
  setWindow(8, 17, 35, 119) // Top, Bottom, Left, Right
  mapNameHash = 0  // on the assumption that it's being set because somebody's going to print there
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the large upper right bar
export def setWindow2()#0
  setWindow(24, 132, 154, 267) // Top, Bottom, Left, Right
  displayChar('N'-$40) // Set normal mode - clear all special modes (like underline, etc.)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the mid-size lower right bar
export def setWindow3()#0
  setWindow(144, 181, 154, 267) // Top, Bottom, Left, Right
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear map window, then set a text area within it.
export def useMapWindow()#0
  byte bottom1, bottom2
  if frameLoaded == 3  // don't check mapIs3D, since we might be in an engine
    bottom1 = 154; bottom2 = 150
  else
    bottom1 = 169; bottom2 = 168
  fin
  setWindow(24, bottom1, 14, 140) // Top, Bottom, Left, Right
  clearWindow()
  setWindow(24, bottom2, 14, 140) // Top, Bottom, Left, Right
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def clearTextWindow()#0
  if textDrawn or textClearCountdown
    setWindow2(); clearWindow()
    if mapIs3D and texturesLoaded; copyWindow(0); fin
    textDrawn = FALSE
    textClearCountdown = 0
    snoozeX1 = -1
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def soundPlayEmu(numAndFlgs)#0
  if mapEmuSound == $FF       // if in initMap...
    mapEmuSound = numAndFlgs  // ... then record the music for resume after combat
  fin
  ^EMUSOUND_PLAY = numAndFlgs
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def hline(addr, startByte, midByte, midSize, endByte)#0
  ^addr = startByte
  memset(addr+1, midByte, midSize)
  ^(addr+midSize+1) = endByte
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window that covers the entire inner area (destroys frame image, so be sure to loadMainFrame
// afterwards)
export def setBigWindow()#0
  // Draw border (if not already drawn)
  if frameLoaded
    hline(getScreenLine(BIGWIN_TOP-4)+1, 0, 0, 36, 0)
    hline(getScreenLine(BIGWIN_TOP-3)+1, $F8, $FF, 36, $8F)
    hline(getScreenLine(BIGWIN_TOP-2)+1, $FC, $FF, 36, $9F)
    hline(getScreenLine(BIGWIN_TOP-1)+1, $8C, 0, 36, $98)
    vline(getScreenLine(BIGWIN_TOP)+1, $8C, 174)
    vline(getScreenLine(BIGWIN_TOP)+38, $98, 174)
    hline(getScreenLine(BIGWIN_BOTTOM-1)+1, $8C, 0, 36, $98)
    hline(getScreenLine(BIGWIN_BOTTOM)+1, $FC, $FF, 36, $9F)
    hline(getScreenLine(BIGWIN_BOTTOM+1)+1, $F8, $FF, 36, $8F)
    hline(getScreenLine(BIGWIN_BOTTOM+2)+1, 0, 0, 36, 0)
    frameLoaded = 0 // since we just destroyed it
    textDrawn = FALSE
    textClearCountdown = 0
    ^EMUSIG_FULL_TEXT
  fin
  setWindow(BIGWIN_TOP, BIGWIN_BOTTOM, BIGWIN_LEFT, BIGWIN_RIGHT)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def rightJustifyStr(str, rightX)#0
  word space
  space = rightX - calcWidth(str)
  if (space > 0)
    rawDisplayStr("^T")  // do not use printf variants, since it might overwrite str
    rawDisplayStr(convert3Dec(space))
  fin
  rawDisplayStr(str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def rightJustifyNum(num, rightX)#0
  rightJustifyStr(convertDec(num), rightX)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def centerStr(str, windowWidth)#0
  word x
  x = (windowWidth - calcWidth(str)) >> 1
  if (x >= 0)
    rawDisplayStr("^T")  // do not use printf variants, since it might overwrite str
    rawDisplayStr(convert3Dec(x))
  fin
  rawDisplayStr(str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display the party data on the screen
def showPartyLine(p)#0
  if p <> global=>p_players; displayChar('\n'); fin
  if p->b_skillPoints; rawDisplayStr("^I"); fin // inverse for chars needing lvl-up
  rawDisplayf1("%s^N", p=>s_name)
  rightJustifyStr(sprintf2("%d/%d", p=>w_health, p=>w_maxHealth), CHAR_WND_HEALTH_X)
end
export def showParty()#0
  word p, cursX, cursY

  cursX, cursY = getCursor()
  setWindow3()
  clearWindow()

  // Display header (or LEVEL UP message if any player has un-applied skill pts)
  if first(global=>p_players, &(p) p->b_skillPoints)
    rawDisplayStr("^Y^I LEVEL U)P ^N\n")
  else
    rawDisplayStr("^LName")
    rightJustifyStr(@S_HEALTH, CHAR_WND_HEALTH_X)  // begin underline mode
    rawDisplayStr("^N\n")
  fin

  // Display each character's name and health
  forEach(global=>p_players, @showPartyLine)

  // Finish up
  if mapIs3D and texturesLoaded; copyWindow(0); fin
  setWindow2()
  setCursor(cursX, cursY)
  needShowParty = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def getArgCount(pFunc)#1
  word pBytecode

  // skip over JMP to plasma interp, get addr in aux mem
  pBytecode = pFunc=>3

  // Check if the function starts with ENTER op
  if readAuxByte(pBytecode) == $58
    return readAuxByte(pBytecode+2)
  fin

  // Zero-arg functions sometimes omit ENTER altogether.
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def setTextCountdown()#0
  textClearCountdown = 3
  if mapIs3D and texturesLoaded
    copyWindow(0)
  fin
  textDrawn = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Send an event to the scripts on the current map square
export def scriptEvent(event, param)#0
  byte i, argCount
  word script
  if !nMapScripts or q_playerDeath; return; fin

  if inScript; return; fin // avoid doing scripted events inside other scripts
  inScript = TRUE 

  setWindow2()

  textDrawn = FALSE
  for i = 0 to nMapScripts-1
    script = mapScripts[i]
    argCount = getArgCount(script)
    if argCount == 0 and event == @S_ENTER  // zero-param scripts are assumed to be strictly 'enter' handlers
      script()
    elsif argCount == 1
      script(event)
    elsif argCount == 2
      script(event, param)
    fin
  next
  
  inScript = FALSE

  if textDrawn; setTextCountdown; fin
  clearPortrait()
  if needShowParty; showParty(); fin
  if global=>p_players=>w_health == 0; q_playerDeath = TRUE; fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def loadMainFrameImg()#0
  if curFullscreenImg
    auxMmgr(FREE_MEMORY, curFullscreenImg)
    curFullscreenImg = NULL
  fin
  loadFrameImg(mapIs3D+2)
  if mapIs3D
    ^EMUSIG_3D_MAP
  else
    ^EMUSIG_2D_MAP
  fin
  if curFullscreenImg
    auxMmgr(FREE_MEMORY, curFullscreenImg) // we don't allow animated main frames, so save memory
    curFullscreenImg = NULL
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def loadUtil3d()#0
  if !pModUtil3d and mapIs3D
    mmgr(START_LOAD, 1) // code is in partition 1
    pModUtil3d = mmgr(QUEUE_LOAD, MOD_UTIL3D<<8 | RES_TYPE_MODULE)
    mmgr(FINISH_LOAD, 0)
    util3d = pModUtil3d()
    initCmds() // rebuild the command table with new function ptrs
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load code and data, set up everything to display a 2D or 3D map
def initMap(x, y, dir)#0
  //AUTOMAP_CHECK// word pDiskOps

  // If we have a renderer loaded, let it know to flush automap marks
  textureControl(FALSE)

  // Reset memory (our module will stay since memory manager locked it upon load)
  mmgr(RESET_MEMORY, 0)

  // Reset all pointers to non-locked memory, since the stuff might go away
  pModUtil3d = NULL
  util3d = NULL
  pCurMap = NULL
  curEngine = NULL
  pGlobalTileset = NULL
  curPortrait = NULL
  curPortraitNum = 0
  curFullscreenImg = NULL
  renderLoaded = FALSE // leave it this way until all scripts done, else scriptDisplayStr renders

  // Load the frame image, then raycaster or tile engine
  loadMainFrameImg()
  mmgr(START_LOAD, 1)
  mmgr(SET_MEM_TARGET, displayEngine)
  if mapIs3D
    mmgr(QUEUE_LOAD, CODE_RENDER<<8 | RES_TYPE_CODE)
    // Reserve memory for the raycaster's tables
    mmgr(SET_MEM_TARGET, RAY_TABLE_START)
    mmgr(REQUEST_MEMORY, RAY_TABLE_SIZE)
  else
    mmgr(QUEUE_LOAD, CODE_TILE_ENGINE<<8 | RES_TYPE_CODE)
  fin
  pGlobalTileset = mmgr(QUEUE_LOAD, 1<<8 | RES_TYPE_TILESET)  // even in 3d, need tiles for lamp/etc.
  //AUTOMAP_CHECK// pDiskOps = mmgr(QUEUE_LOAD, MOD_DISKOPS<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  if mapIs3D; loadUtil3d(); fin

  //AUTOMAP_CHECK// pDiskOps()=>diskops_checkAutomap()
  //AUTOMAP_CHECK// mmgr(FREE_MEMORY, pDiskOps)

  // Set up the command table
  initCmds() // must be after loading util3d

  // Load the map
  curMapPartition = lookupResourcePart(mapIs3D+1, mapNum)
  if !curMapPartition; fatal("lkup2"); fin
  mmgr(START_LOAD, curMapPartition)
  pCurMap = mmgr(QUEUE_LOAD, mapNum<<8 | (RES_TYPE_2D_MAP+mapIs3D))
  mmgr(FINISH_LOAD, 0)

  // Clear all the windows to the background color (hi-bit set)
  // except window3 because showParty does it for us
  setWindow1(); clearWindow()
  setWindow2(); clearWindow()

  // Clear the list of encounter zones from any previous maps
  clearEncounterZones

  // Start up the display engine with map data and starting position. This will also load and
  // init the script module, if any, which will end up calling us back at the setScriptInfo
  triggerTbl = NULL
  setWindow2()
  skyNum = 9 // default
  groundNum = 10 // default
  timeEventFunc = NULL
  mapEmuSound = $FF // special value to mark init
  initDisplay(curMapPartition, mapNum, pCurMap, x, y, dir)
  if mapEmuSound == $FF // if no music assigned...
    mapEmuSound = 0     // ...then record that fact.
  fin
  texturesLoaded = TRUE
  needRender = FALSE
  textDrawn = FALSE
  textClearCountdown = 0
  curEngine = NULL
  curPortrait = NULL
  curPortraitNum = 0
  curFullscreenImg = NULL
  prevClockColor = 99
  snoozeX1 = -1
  if timeEventFunc; timeEventFunc(global->b_hour); fin
  if mapIs3D
    if util3d
      util3d=>util3d_showCompassDir(dir)
      util3d=>util3d_showClock()
    fin
  elsif global->b_curAvatar <> 0
    setAvatar(global->b_curAvatar)
    doRender()
  fin

  // Init the floppy flag after the first full load, which will have certainly
  // hit some disk besides 1 if there is such a disk.
  isFloppyVer = getFloppyFlg

  // Assume there might be animations until we learn otherwise
  resetAnimPause

  // Populate script handlers for the current square, so that leave handlers will trigger right.
  nMapScripts = scanScripts(x-triggerOriginX, y-triggerOriginY, triggerTbl, @mapScripts)

  // Display the party characters
  showParty()

  // All done.
  renderLoaded = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def scriptSetAvatar(avatarTileNum)#0
  global->b_curAvatar = avatarTileNum
  if renderLoaded; setAvatar(avatarTileNum); fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def textureControl(flg)#0
  if renderLoaded
    flipToPage1
    if flg <> texturesLoaded
      if flg
        _texControl(1)
        if mapIs3D; loadUtil3d; fin
        showingLamp = mapIs3D and (skyNum == 0 or skyNum == 8)
      else
        _texControl(0)
        if !mapIs3D
          pCurMap = NULL // tile engine frees all maps, even current one
        fin
        if pModUtil3d and mapIs3D and !inScript   // e.g. strafe -> script -> combat: need strafe to stay in mem!
          mmgr(FREE_MEMORY, pModUtil3d)
          pModUtil3d = NULL
          util3d = NULL
        fin
        showingLamp = FALSE
        needRender = FALSE
      fin
    fin
    texturesLoaded = flg
  else
    texturesLoaded = FALSE
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a portrait drawing (typically called from scripts)
export def clearPortrait()#0
  if curPortrait
    auxMmgr(FREE_MEMORY, curPortrait)
    curPortrait = NULL
    curPortraitNum = 0
    needRender = TRUE
  fin
  if curFullscreenImg
    auxMmgr(FREE_MEMORY, curFullscreenImg)
    curFullscreenImg = NULL
    loadMainFrameImg()
    needRender = TRUE
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Perform rendering, copy if necessary, clear appropriate flags
def doRender()#0
  if curPortrait; clearPortrait(); fin
  textureControl(TRUE)
  if showingLamp; util3d=>util3d_nextLampFrame(); fin
  render(0) // Don't interrupt on kbd - must finish the render
  needRender = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def advTime(hours, mins, secs)#0
  byte redrawClock, runScript, prevHour, add
  word player

  if secs
    global->b_second = global->b_second + secs
    while global->b_second >= 60
      global->b_second = global->b_second - 60
      mins++
    loop
  fin

  if mins or hours
    redrawClock = FALSE
    runScript = FALSE
    global->b_minute = global->b_minute + mins
    if global->b_minute >= nextSignificantMinute; redrawClock = TRUE; fin
    while global->b_minute >= 60
      global->b_minute = global->b_minute - 60
      hours++
    loop

    if hours
      redrawClock = TRUE
      prevHour = global->b_hour
      global->b_hour = global->b_hour + hours
      while global->b_hour >= 24
        global->b_hour = global->b_hour - 24
      loop
      runScript = TRUE

      // Heal over time every 12 hours
      if (prevHour / 12) <> (global->b_hour / 12)
        player = global=>p_players
        while player
          add = (rand16 % (player->b_stamina+1))/3
          setStat(player, @S_HEALTH, player=>w_health + add)
          player = player=>p_nextObj
        loop
        if needShowParty; showParty(); fin
      fin
    fin

    if mapIs3D and redrawClock and renderLoaded and texturesLoaded and util3d
      util3d=>util3d_showClock()
    fin

    if runScript and timeEventFunc
      timeEventFunc(global->b_hour)
    fin
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by scripts to display a string. We set the flag noting that something has been
// displayed, then use an assembly routine to do the work.
def _scriptDisplayStr(str)#0
  if needRender and renderLoaded and texturesLoaded and !curPortrait and !curFullscreenImg
    doRender()
    flipToPage1()
    needRender = FALSE
  fin
  if textClearCountdown; clearTextWindow(); fin
  if forceRawScrDisp
    rawDisplayStr(str)
  else
    displayStr(str)
  fin
  textDrawn = TRUE
  anyInteraction = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def promptAnyKey(clearAfter)#0
  scriptDisplayStr("\n(press any key)")
  getUpperKey
  if clearAfter; clearWindow; fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def snooze()#1
  word cursX, cursY
  cursX, cursY = getCursor()
  if cursX == snoozeX1 and cursY == snoozeY
    if mapIs3D
      advTime(0, 14 - (global->b_minute % 15), 60 - global->b_second)  // next 15 min mark
    else
      advTime(0, 59 - global->b_minute, 60 - global->b_second)  // start of next hour
    fin
  fin

  nextAnimFrame()

  cursX, cursY = getCursor()
  if cursX == snoozeX1 and cursY == snoozeY
    rawDisplayf1("^T%D", snoozeX0)
  else
    if cursY; rawDisplayStr("\n"); fin
    if cursX; rawDisplayStr("\n"); fin
    rawDisplayStr("The time:   ")
    snoozeX0, snoozeY = getCursor()
  fin
  rawDisplayf1("^C%d:", global->b_hour == 0 ?? 12 :: global->b_hour == 12 ?? 12 :: global->b_hour % 12)
  rawDisplayf2("%s%d ", global->b_minute < 10 ?? "0" :: "", global->b_minute)
  rawDisplayf1("%s.", global->b_hour < 12 ?? "am" :: "pm")
  snoozeX1, snoozeY = getCursor()
  setTextCountdown
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def moveInternal(facingDir, moveDir, beepOK, shouldAdvTime)#1
  byte val, i
  word x, y

  setDir(moveDir)
  val = advance(global->b_moveMode)
  lastMoveDir = moveDir
  setDir(facingDir)

  // If not blocked, render at the new position.
  if val == 0
    if beepOK and !inScript // don't beep for scripted moves
      clearTextWindow
      rawDisplayStr("Blocked!\n")
      beep
      setTextCountdown
    fin
  else
    if !mapIs3D
      doRender()
    else
      needRender = TRUE
    fin
  fin

  // Advance time if requested
  if shouldAdvTime
    if mapIs3D
      for i = 1 to global->b_moveMode
        advTime(CLOCK_ADV_3D_HOURS, CLOCK_ADV_3D_MINS, CLOCK_ADV_3D_SECS)
      next
    else
      advTime(CLOCK_ADV_2D_HOURS, CLOCK_ADV_2D_MINS, CLOCK_ADV_2D_SECS)
    fin
  fin

  // If we're on a new map tile, run leave handlers from old tile.
  if val >= 2
    scriptEvent(@S_LEAVE, NULL)
    nMapScripts = 0

    // If there are script(s) on the new tile, run them.
    getPos(@x, @y)
    nMapScripts = scanScripts(x-triggerOriginX, y-triggerOriginY, triggerTbl, @mapScripts)
    if nMapScripts
      scriptEvent(@S_ENTER, NULL)
    elsif global=>p_encounterZones
      checkEncounter(x, y, FALSE)
    fin
  fin

  return val
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Advance one step forward (works for either 3D or 2D maps)
def moveForward()#1
  byte dir
  dir = getDir()
  return moveInternal(dir, dir, TRUE, TRUE)  // beep ok, adv time
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Move backward two quarter-steps (3D mode), or one step (2D mode). This is often used when exiting a
// building or fleeing combat, so we don't want to generate any random encounters.
export def moveWayBackward()#1
  byte facingDir, moveDir
  facingDir = getDir()
  if lastMoveDir <> $FF
    moveDir = (lastMoveDir + 8) & 15  // reverse of last move
    moveInternal(facingDir, moveDir, FALSE, TRUE) // no beep, but do adv time
    if global->b_moveMode == 1
      moveInternal(facingDir, moveDir, FALSE, TRUE) // no beep, but do adv time
    fin
  fin
  lastMoveDir = $FF // avoid problems if moveWayBackward called repeatedly
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Move to the north/east/south/west (2D mode)
def moveNorth()#1
  word x, y
  getPos(@x, @y)
  if y > 4
    setDir(0)
    moveForward()
  else
    beep()
  fin
  return 0
end

def moveEast()#1
  word x, y
  getPos(@x, @y)
  if x < totalMapWidth-5
    setDir(4)
    moveForward()
  else
    beep()
  fin
  return 0
end

def moveSouth()#1
  word x, y
  getPos(@x, @y)
  if y < totalMapHeight-5
    setDir(8)
    moveForward()
  else
    beep()
  fin
  return 0
end

def moveWest()#1
  word x, y
  getPos(@x, @y)
  if x > 4
    setDir(12)
    moveForward()
  else
    beep()
  fin
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Switch to a new map (2D or 3D) and establish position on it
export def setMap(is3D, num, x, y, dir)#0
  if is3D == mapIs3D and num == mapNum
    setPos(x, y)
    setDir(dir)
    needRender = TRUE
  else
    flipToPage1()
    showMapName("Traveling...")
    useMapWindow()
    mapIs3D = is3D
    mapNum = num
    initMap(x, y, dir)
    saveGame
  fin
  // Don't send enter event, because we often land on an "Exit to wilderness?" script
  //NO:scriptEvent(S_ENTER, NULL)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def callGlobalFunc(moduleNum, arg1, arg2, arg3)#1
  word pModule, pFunc, ret

  // First load the module
  if renderLoaded; flipToPage1(); fin
  mmgr(START_LOAD, moduleNum <= LAST_REQ_GS_MOD ?? 1 :: curMapPartition)
  pModule = mmgr(QUEUE_LOAD, moduleNum<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)

  // Call the function, passing it the number of args it expects
  pFunc = pModule()
  when getArgCount(pFunc)
    is 0; ret = pFunc(); break
    is 1; ret = pFunc(arg1); break
    is 2; ret = pFunc(arg1, arg2); break
    is 3; ret = pFunc(arg1, arg2, arg3); break
    otherwise fatal("maxGlobParams")
  wend

  // Unload the module and we're done.
  mmgr(FREE_MEMORY, pModule)
  return ret
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def queue_setMap(is3D, num, x, y, dir)#0
  byte part
  part = lookupResourcePart(is3d+1, num)
  if diskLimit and part > diskLimit
    clearWindow
    callGlobalFunc(GS_DISK_LIMIT, 1, 0, 0)
    return
  fin
  q_mapIs3D = is3D
  q_mapNum = num
  q_x = x
  q_y = y
  q_dir = dir
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def queue_teleport(x, y, dir)#0
  queue_setMap(mapIs3D, mapNum, x, y, dir)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a key and dispatch it to a command. Then do it again, forever.
def kbdLoop()#0
  word func, tmp
  byte xreg
  xreg = getXReg()
  while TRUE
    // If the asm routines all work correctly, by the time we get to the top of this loop
    // the X register should always have the same value.
    tmp = getXReg()
    if tmp <> xreg; printHex(xreg<<8 | tmp); fatal("xRegChg"); fin
    cmdKey = getUpperKey()
    if cmdKey >= 0 and cmdKey < $60
      func = cmdTbl[cmdKey]
      if func
        if textClearCountdown
          if textClearCountdown == 1; clearTextWindow(); fin
          textClearCountdown--
        fin
        func()
      fin
    fin
    if q_playerDeath; playerDeath(); continue; fin
    if q_mapNum; setMap(q_mapIs3D, q_mapNum, q_x, q_y, q_dir); q_mapNum = 0; fin
    if needRender; doRender(); fin
    if prevScriptModule
      if prevScriptModule <> scriptModule
        tmp = mmgr(FIND_IN_MEM, prevScriptModule<<8 | RES_TYPE_MODULE)
        if tmp; mmgr(FREE_MEMORY, tmp); fin
      fin
      prevScriptModule = 0
    fin
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def hashString(str)#1
  return hashBuffer(str+1, ^str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def showMapName(mapName)#0
  word newNameHash

  newNameHash = hashString(mapName)
  if newNameHash <> mapNameHash
    setWindow1()
    clearWindow()
    centerStr(mapName, 84)
    if mapIs3D and texturesLoaded; copyWindow(0); fin
    mapNameHash = newNameHash
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set initial info for the scripts on this map: the name of the map, its trigger table, and the
// maximum extent (width, height). This is called by the init function for the scripts.
export def setScriptInfo(mapName, moduleNum, timeFn, trigTbl, wdt, hgt)#0
  word tmp

  // Record the time event function, if any
  timeEventFunc = timeFn

  // Record new script, and prepare to free old one. While theoretically there could be a
  // circumstance where the old prev is still un-freed, it's rare enough that it'd be hard to
  // test freeing it here correctly. So we leave freeing to the main keyboard loop.
  if scriptModule <> moduleNum
    prevScriptModule = scriptModule
    scriptModule = moduleNum
  fin

  // Grab the trigger table origins (used so the table can be more compact)
  triggerOriginX = trigTbl=>0
  triggerOriginY = trigTbl=>2

  // Record the trigger table pointer
  triggerTbl = trigTbl + 4

  // Record the maximum width and height
  totalMapWidth = wdt
  totalMapHeight = hgt

  // Display map name
  global=>s_mapName = mmgr(HEAP_INTERN, mapName)
  showMapName(mapName)

  // Get ready for new encounter zones
  clearEncounterZones

  // Back to the main text window.
  setWindow2()

  // If there's a time script, run it so it can set sky color, etc.
  if timeFn; timeFn(global->b_hour); fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by scripts to swap a map tile. We set a flag noting we need to re-render, then use an
// assembly routine to do the work.
export def scriptCopyTile(fromX, fromY, toX, toY)#0
  needRender = TRUE
  copyTile(fromX, fromY, toX, toY)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a key, and don't return until it's Y or N (or lower-case of those). Returns 1 for Y.
export def getYN()#1
  byte key
  while TRUE
    key = getUpperKey()
    if key == 'Y'
      return 1
    elsif key == 'N'
      if frameLoaded
        clearTextWindow()
      fin
      break
    fin
    beep()
  loop
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Show the current animation frame
def showAnimFrame()#0
  byte top
  if curPortrait
    if storyMode
      blit(1, curPortrait + 2, getScreenLine(0)+20, 128, 18)
    else
      // Blit portrait to the appropriate area on the screen
      top = 32   // start at 4th text line in 2D
      if frameLoaded == 3  // 3D-mode frame? Note: don't check mapIs3D, because we might be in an engine
        top = 24 // start at 4th text line in 3D
      fin
      blit(1, curPortrait + 2, getScreenLine(top)+2, 128, 18)
    fin
    needRender = FALSE  // suppress display of map for this frame
    if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin
  elsif curFullscreenImg
    blit(1, curFullscreenImg + 2, getScreenLine(0), 192, 40) // the +2 is to skip anim hdr offset
    needRender = FALSE  // suppress display of map for this frame
  elsif texturesLoaded
    if mapIs3D
      if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin
      render($FF) // it's only animation, so do interrupt if a key is pressed
      if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin
    else
      render(0) // 2d map view
    fin
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Advance to next frame of current animation, if any
def nextAnimFrame()#0
  word flags, param
  byte dir, randNum

  animPauseCt = ANIM_PAUSE_MAX

  if !anyAnims; return; fin

  // Choose a new direction based on the flags. Do this the first time, and once every 3-7 frames.
  animDirCt = animDirCt - 1
  if animDirCt <= 0
    animDirCt = (rand16() % 5) + 3
  fin

  // Advance animations.
  // First part is whether to switch directions on fwd/back anims.
  // Second part is how many frames to advance random anims.
  param = ((animDirCt==1) & 1) | (((rand16() % 10)+1)<<8)
  flags = auxMmgr(ADVANCE_ANIMS, param) | mmgr(ADVANCE_ANIMS, param)
  if (flags >> 8)
    // An animation was changed -- display it
    showAnimFrame()
  elsif (flags & $FF) == 0
    // No animations in memory; turn off future checking
    anyAnims = FALSE
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a portrait drawing (typically called from scripts)
export def setPortrait(portraitNum)#0
  word srcData
  byte part, cx, cy, cursX, cursY

  if portraitNum == curPortraitNum; return; fin

  clearPortrait()

  // We're going to switch windows. Save the cursor pos in the text window.
  cursX, cursY = getCursor()

  // Make room by unloading the textures (only if renderer is loaded)
  textureControl(FALSE)

  // Now clear out the map area
  useMapWindow()

  // Restore the cursor position
  setWindow2()
  setCursor(cursX, cursY)

  // Load the portrait image and display it
  part = lookupResourcePart(3, portraitNum)
  if !part; fatal("lkup3"); fin
  // Commented out below, because it prevents cycling thru all portraits (in god mode)
  // NO: if part > 1; part = curMapPartition; fin  // Look on disk 1 or current disk only
  mmgr(START_LOAD, part)
  curPortrait = auxMmgr(QUEUE_LOAD, portraitNum<<8 | RES_TYPE_PORTRAIT)
  curPortraitNum = portraitNum
  mmgr(FINISH_LOAD, 0)
  resetAnimPause

  // And show the first frame
  showAnimFrame()

  // Do not render over the portrait
  needRender = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def countList(p)#1
  return sum(p, NULL, &(p) 1)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Call like this: addToList(@player=>p_items, itemToAdd)
export def addToList(addTo, p)#0
  // Get to the end of the list
  while *addTo
    addTo = (*addTo) + p_nextObj
  loop
  p=>p_nextObj = *addTo
  *addTo = p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Call like this: removeFromList(@player=>items, itemToRemove)
export def removeFromList(pList, toRemove)#0
  word p

  p = *pList
  while p and p <> toRemove
    pList = p + p_nextObj
    p = *pList
  loop

  if p
    *pList = p=>p_nextObj
    p=>p_nextObj = NULL
  else
    fatal("unlink")
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def saveMapPos()#0
  global->b_mapIs3D = mapIs3D
  global->b_mapNum = mapNum
  getPos(@global=>w_mapX, @global=>w_mapY)
  global->b_mapDir = getDir()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def showMoveMode()#0
  rawDisplayStr("Move mode:\n")
  if global->b_moveMode == 1; rawDisplayStr("^I"); fin
  rawDisplayStr("Default^N  ")
  if global->b_moveMode == 2; rawDisplayStr("^I"); fin
  rawDisplayStr("Fast^N  ")
  if global->b_moveMode == 4; rawDisplayStr("^I"); fin
  rawDisplayStr("Classic^N\n")
  setTextCountdown
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def restoreMapPos()#0
  mapIs3D = global->b_mapIs3D
  mapNum = global->b_mapNum
  initMap(global=>w_mapX, global=>w_mapY, global->b_mapDir)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def loadEngine(moduleNum)#1
  if curEngine; fatal("dblEng"); fin
  preEnginePortraitNum = curPortraitNum
  clearPortrait()
  textureControl(FALSE) // also flips to page 1
  mmgr(START_LOAD, 1) // code is in partition 1
  curEngine = mmgr(QUEUE_LOAD, moduleNum<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  return curEngine()  // return function table
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def returnFromEngine(render)#0
  if curEngine
    mmgr(FREE_MEMORY, curEngine)
    curEngine = NULL
    loadMainFrameImg()
    if preEnginePortraitNum
      setPortrait(preEnginePortraitNum)
    else
      clearPortrait()
    fin
    textureControl(TRUE)
    mapNameHash = 0; showMapName(global=>s_mapName)
    clearTextWindow()
    if render
      doRender()
    else
      needRender = TRUE
    fin
    if mapIs3D
      prevClockColor = 99
      if util3d
        util3d=>util3d_showCompassDir(getDir())
        util3d=>util3d_showClock()
      fin
    fin
    showParty()
    setWindow2()  // in case we're mid-script
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the automap engine and display the map
def showAutomap()#1
  loadEngine(MOD_AUTOMAP)=>automap_show(9999, 9999)  // 9999 = no quest target
  returnFromEngine(TRUE)
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the Party engine and show data for the given player
def showPlayerSheet(num)#1
  word sNameToUse, oldFlg, pItemutilModule
  if num+1 > countList(global=>p_players); beep; return 0; fin
  sNameToUse = loadEngine(MOD_PARTY)=>party_showPlayerSheet(num)
  returnFromEngine(TRUE)
  // General 'use' handled here in case it triggers graphical effects
  if sNameToUse
    anyInteraction = FALSE
    scriptEvent(@S_USE, sNameToUse)
    if !anyInteraction
      scriptDisplayStr("Nothing happens.")
      setTextCountdown
    fin
  fin
  playerUsing = NULL
  return 0
end

def showPlayer1()#1
  return showPlayerSheet(0)
end
def showPlayer2()#1
  return showPlayerSheet(1)
end
def showPlayer3()#1
  return showPlayerSheet(2)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Level up the first character that's applicable
def levelUp()#1
  word player
  byte n
  player = global=>p_players
  n = 0
  while player
    if player->b_skillPoints
      showPlayerSheet(n)
      return 0
    fin
    player = player=>p_nextObj
    n++
  loop
  beep
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def addEncounterZone(code, x, y, dist, chance)#0
  word p
  p = mmgr(HEAP_ALLOC, TYPE_ENCOUNTER_ZONE)
  p=>s_name = mmgr(HEAP_INTERN, code)
  p=>w_encX = x
  p=>w_encY = y
  p=>w_encMaxDist = dist
  p=>w_encChance = chance
  addToList(@global=>p_encounterZones, p)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def clearEncounterZones()#0
  global=>p_encounterZones = NULL
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by user-defined map scripts to initiate a combat encounter.
export def scriptCombat(mapCode)#1
  return doCombat(mapCode, TRUE)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def playerDeath()#0
  setWindow2; clearWindow
  callGlobalFunc(GS_DEATH, 0, 0, 0)
  startGame(FALSE, FALSE) // don't ask, just load
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def resumeMapMusic()#0
  if mapEmuSound > 0 and mapEmuSound <> $FF
    soundPlayEmu(mapEmuSound)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def doCombat(mapCode, backUpOnFlee)#1
  word result

  // Give a clue as to what's happening
  if needRender; doRender; fin
  flipToPage1
  setWindow2()
  clearWindow()
  callGlobalFunc(GS_COMBAT_INTRO, 0, 0, 0)

  // Remainder handled in a separate module. Clear enemies out of the heap when finished.
  result = loadEngine(MOD_COMBAT)=>combat_zoneEncounter(mapCode)
  global=>p_enemyGroups = NULL
  heapCollect()

  if (result == -99)
    q_playerDeath = TRUE
    return 0
  fin
  returnFromEngine(!inScript) // only re-render if outside script
  resumeMapMusic

  // Advance time a little. It's tricky advancing during combat; this is an ok substitute.
  advTime(CLOCK_ADV_COMBAT_HOURS, CLOCK_ADV_COMBAT_MINS, CLOCK_ADV_COMBAT_SECS)

  // If the party fled the combat instead of winning, back up to previous square.
  if !result and backUpOnFlee
    moveWayBackward()
  fin
  return result
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Check for a random encounter at this position
export def checkEncounter(x, y, force)#0
  word p
  word p_bestZone, bestDist
  word d

  // Don't check for encounter during scripted move
  if inScript; return; fin

  // Find the zone that's closest, but not too far.
  bestDist = INT_MAX
  p_bestZone = NULL
  p = global=>p_encounterZones
  while p
    d = min(abs(x - p=>w_encX), abs(y - p=>w_encY))
    // Using '<=' below so that later-added zones added by a scripted event take precedence
    if d <= bestDist and (p=>w_encMaxDist == 0 or d <= p=>w_encMaxDist)
      p_bestZone = p
      bestDist = d
    fin
    p = p=>p_nextObj
  loop

  // Roll for an encounter in the zone.
  d = rand16() % 1000
  if p_bestZone
    if (d < p_bestZone=>w_encChance or force)
      // Encounter!
      doCombat(p_bestZone=>s_name, !force)
    fin
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def rwGame(cmd)#0
  while TRUE
    if callProRWTS(cmd | RWTS_OPENDIR, @S_GAME1_FILENAME, LOAD_SAVE_BUF, HEAP_SIZE) == 0
      break
    fin
    textHome()
    ^$c051
    puts("Insert disk 1")
    rdkey()
    ^$c050
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def saveGame()#1
  word cursX, cursY

  cursX, cursY = getCursor()
  saveMapPos()
  showMapName("Saving game...")

  textureControl(FALSE) // also flips to page 1

  // Perform garbage collection and record the size of the heap so we can restore it correctly
  // (also does a CHECK_MEM to be sure we never save corrupted heap)
  heapCollect()

  //AUTOMAP_CHECK// readDiskMarks; checkMarks

  // Copy data to main memory, and write it out.
  memcpy(HEAP_BOTTOM, LOAD_SAVE_BUF, HEAP_SIZE, 0)  // LC to low mem
  rwGame(RWTS_WRITE)

  mapNameHash = 0; showMapName(global=>s_mapName)
  doRender
  setWindow2()
  setCursor(cursX, cursY)

  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def loadGame()#1
  // Since old map may have been modified by scripts, force it to really be gone.
  if pCurMap
    mmgr(FREE_MEMORY, pCurMap)
    mmgr(SET_MEM_TARGET, pCurMap)
    mmgr(REQUEST_MEMORY, 5)
    mmgr(FREE_MEMORY, pCurMap)
    pCurMap = NULL
  fin
  loadEngine(MOD_DISKOPS)=>diskops_loadGame()
  restoreMapPos()
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def help()#1
  flipToPage1
  setBigWindow; clearWindow
  forceRawScrDisp = TRUE
  loadEngine(GS_HELP)()
  forceRawScrDisp = FALSE
  returnFromEngine(TRUE)
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def toggleGodMode()#1
  byte key
  key = getUpperKey
  if key == 15  // ctrl-O
    key = getUpperKey
    if key == 4 // ctrl-D
      flipToPage1()
      clearTextWindow()
      if key == 4 // ctrl-D
        global->b_godmode = !global->b_godmode
        displayf1("gm:%d\n", global->b_godmode & 1)
        initCmds() // rebuild the command table with new commands
      fin
      textDrawn = TRUE
      beep; beep
      clearTextWindow()
    fin
  fin
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def cheatCmd()#1
  byte key
  word pModule

  // We don't use laodEngine, since godmode may use it too, and we'd get double modules
  textureControl(FALSE) // seems to be necessary for teleport-to-3d to work right
  flipToPage1
  mmgr(START_LOAD, 1) // code is in partition 1
  pModule = mmgr(QUEUE_LOAD, MOD_GODMODE<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)

  pModule()=>godmode_cheatCmd(cmdKey)

  mmgr(FREE_MEMORY, pModule)
  textureControl(TRUE)
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// This one godmode command needs to not load a module -- print memory without disturbing it
export def printMem()#1
  flipToPage1
  ^$c051
  mmgr(DEBUG_MEM, 0)
  rdkey
  auxMmgr(DEBUG_MEM, 0)
  rdkey
  ^$c050
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def showQuests()#1
  loadEngine(MOD_QUESTLOG)=>questlog_showQuests()
  returnFromEngine(TRUE) // do render
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def switchMoveMode()#1
  byte dir
  word x, y

  dir = getDir()
  if global->b_moveMode == 1
    global->b_moveMode = 2
    dir = dir & ~1
  elsif global->b_moveMode == 2
    global->b_moveMode = 4
    dir = dir & ~3
  else
    global->b_moveMode = 1
  fin

  setDir(dir)
  getPos(@x, @y)
  setPos(x, y) // to round it
  needRender = TRUE

  flipToPage1()
  clearTextWindow()
  showMoveMode

  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the command table for 3D mode
def initCmds()#0

  // Clear the command table
  byte i
  for i = 0 to 95
    cmdTbl[i] = 0
  next

  // Commands common to both 2D and 3D
  cmdTbl['1'] = @showPlayer1
  cmdTbl['2'] = @showPlayer2
  cmdTbl['3'] = @showPlayer3
  cmdTbl['U'] = @levelUp
  cmdTbl[$13] = @saveGame // ctrl-S
  cmdTbl[$0c] = @loadGame // ctrl-L
  cmdTbl['?'] = @help
  cmdTbl['/'] = @help // in case they forget to hit shift
  cmdTbl[$07] = @toggleGodMode // ctrl-G
  cmdTbl[' '] = @snooze  // "space out" (snooze)
  cmdTbl['M'] = @showAutomap
  cmdTbl['Q'] = @showQuests
  if global->b_godmode
    // install cheat commands
    cmdTbl['T'] = @cheatCmd // teleport
    cmdTbl[$10] = @cheatCmd // ctrl-P: show pos
    cmdTbl['>'] = @cheatCmd // next portrait
    cmdTbl['<'] = @cheatCmd // prev portrait
    cmdTbl['!'] = @cheatCmd // test combat
    cmdTbl['Y'] = @cheatCmd // next sky
    cmdTbl['G'] = @cheatCmd // next ground
    cmdTbl['&'] = @printMem // print mem
    cmdTbl['^'] = @cheatCmd // edit flags
    cmdTbl['*'] = @cheatCmd // soundGen test
  fin

  // Commands handled differently in 3D vs 2D
  if mapIs3D
    cmdTbl[3]   = @switchMoveMode // ctrl-C

    cmdTbl['W'] = @moveForward
    cmdTbl['A'] = util3d=>util3d_rotateLeft
    cmdTbl['D'] = util3d=>util3d_rotateRight
    cmdTbl['S'] = util3d=>util3d_moveBackward
    cmdTbl['Z'] = util3d=>util3d_strafeLeft
    cmdTbl['C'] = util3d=>util3d_strafeRight

    cmdTbl['I'] = @moveForward
    cmdTbl['J'] = util3d=>util3d_rotateLeft
    cmdTbl['L'] = util3d=>util3d_rotateRight
    cmdTbl['K'] = util3d=>util3d_moveBackward
    cmdTbl['H'] = util3d=>util3d_strafeLeft
    cmdTbl[';'] = util3d=>util3d_strafeRight

    cmdTbl[11] = @moveForward  // up-arrow
    cmdTbl[8]  = util3d=>util3d_rotateLeft   // left-arrow
    cmdTbl[21] = util3d=>util3d_rotateRight  // right-arrow
    cmdTbl[10] = util3d=>util3d_moveBackward // down-arrow
  else
    cmdTbl['W'] = @moveNorth
    cmdTbl['D'] = @moveEast
    cmdTbl['S'] = @moveSouth
    cmdTbl['A'] = @moveWest

    cmdTbl['I'] = @moveNorth
    cmdTbl['J'] = @moveWest
    cmdTbl['L'] = @moveEast
    cmdTbl['K'] = @moveSouth

    cmdTbl[11] = @moveNorth  // up-arrow
    cmdTbl[8]  = @moveWest   // left-arrow
    cmdTbl[21] = @moveEast   // right-arrow
    cmdTbl[10] = @moveSouth  // down-arrow
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the small-object heap. Set loadedSize to zero on initial, or non-zero for loaded game.
export def initHeap(loadedSize)#0
  byte i

  if loadedSize
    mmgr(SET_MEM_TARGET, HEAP_BOTTOM + loadedSize)
  fin
  mmgr(HEAP_SET,       HEAP_BOTTOM)
  i = 0
  while typeTbls[i]
    mmgr(HEAP_ADD_TYPE, typeTbls[i])
    i = i+1
  loop
  if loadedSize
    global = HEAP_BOTTOM
  else
    global = mmgr(HEAP_ALLOC, TYPE_GLOBAL)
    global=>w_typeHash = typeHash
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Constructor: create a modifier given its name and value
export def makeModifier(name, value)#1
  word p; p = mmgr(HEAP_ALLOC, TYPE_MODIFIER)
  p=>s_name = mmgr(HEAP_INTERN, name)
  p=>w_modValue = value
  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Recalculate player's armor score based on their currently equipped armor
export def calcPlayerArmor(pl)#0
  pl->b_armor = sum(pl=>p_items, &(p) p->t_type == TYPE_ARMOR and p->b_flags & ITEM_FLAG_EQUIP, &(p) p->b_armorValue)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Add gold, return the amount
export def addGold(amount)#1
  if amount <= 0; return 0; fin
  amount = min(amount, GOLD_MAX - global=>w_gold)
  global=>w_gold = global=>w_gold + amount
  return amount
end

// Pay out gold, return the amount
export def payGold(amount)#1
  if amount > global=>w_gold
    return 0
  fin
  global=>w_gold = global=>w_gold - amount
  return amount
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def scanForNamedObj(list, name)#1
  // We don't use lambda here, to preserve context of outer lambdas
  while list
    if streqi(list=>s_name, name); break; fin
    list = list=>p_nextObj
  loop
  return list
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def createStruct(moduleID, creationFuncNum)#1
  word p_module, funcTbl, func, p_thing

  // Unload textures to make room for the module (also flips to page 1 if needed)
  textureControl(FALSE)

  // Load the module that is capable of creating the thing
  mmgr(START_LOAD, 1) // code is in partition 1
  p_module = mmgr(QUEUE_LOAD, moduleID<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)

  // Figure out which creation function to call there, and create the thing
  funcTbl = p_module()
  func = *(funcTbl + creationFuncNum)
  p_thing = func() // full

  // Finished with the module now.
  mmgr(FREE_MEMORY, p_module)
  return p_thing
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def roomInPack(p_player)#1
  // Note: initial pack size is established by gen_players.pla, by code generated in PackPartitions.
  return countList(p_player=>p_items) < p_player->b_packSize
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def partyHasItem(itemName)#1
  ctx = itemName; return first(global=>p_players, &(p) scanForNamedObj(p=>p_items, ctx))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def createItem(itemNum)#1
  word pItem
  if itemNum <= NUM_WEAPONS
    pItem = createStruct(MOD_GEN_WEAPONS, (itemNum-1)<<1)
  elsif itemNum <= NUM_WEAPONS+NUM_ARMORS
    pItem = createStruct(MOD_GEN_ARMORS, (itemNum-NUM_WEAPONS-1)<<1)
  else
    pItem = createStruct(MOD_GEN_MISC_ITEMS, (itemNum-NUM_WEAPONS-NUM_ARMORS-1)<<1)
    if pItem->t_type == TYPE_FANCY_ITEM
      pItem=>w_count = pItem=>w_storeAmount
    fin
  fin
  return pItem
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Returns:  0 = dupe
//          -1 = no room
//           1 = added normally
//           2 = override-added quest item
//           3 = combined with existing stackable
export def fullAddItem(pItem, doit)#1
  word pPlayer, pComp, addToPlayer
  pPlayer = global=>p_players
  addToPlayer = NULL
  while pPlayer
    pComp = scanForNamedObj(pPlayer=>p_items, pItem=>s_name)
    if pComp
      if pComp->t_type == TYPE_FANCY_ITEM and pComp=>w_count > 0
        pComp=>w_count =  min(30000, pComp=>w_count + pItem=>w_count)
        return 3 // combined
      fin
      if !pComp=>w_price
        return 0 // dupe quest item
      fin
    fin
    if !addToPlayer and roomInPack(pPlayer); addToPlayer = pPlayer; fin
    pPlayer = pPlayer=>p_nextObj
  loop

  if addToPlayer
    if doit; addToList(@addToPlayer=>p_items, pItem); fin
    return 1
  fin

  if pItem=>w_price
    return -1 // no room, and item has a price so it's not a quest item
  fin

  // Override: quest items allowed to exceed pack limitations
  if doit; addToList(@global=>p_players=>p_items, pItem); fin
  return 2
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def giveItemToParty(p_item, displayFunc)#0
  when fullAddItem(p_item, TRUE)
    is 0
      displayFunc("Duplicate item.\n")#0
      beep
      break
    is -1
      displayFunc(sprintf1("\nYou have no room; %s dropped.\n", p_item=>s_name))#0
      beep
      break
    otherwise
      heapCollect
  wend
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Add XP to one player, leveling them up as appropriate
def addXP(player, val)#0
  word n
  if val < 0; return; fin
  // Add the value plus an intelligence-based bonus
  player=>w_curXP = player=>w_curXP + val + addPercent(val, 10 * player->b_intelligence)
  // Enforce cap on number of points to stay within limit of 15 bits
  if player=>w_curXP < 0 or player=>w_curXP >= 32760  // goes neg if wrapped around
    player=>w_curXP = 32760
  fin
  while player=>w_curXP >= player=>w_nextXP and player=>w_curXP < 32761
    // Level up!
    player->b_level++
    player->b_skillPoints = player->b_skillPoints + callGlobalFunc(GS_LEVEL_S_P, player->b_level, 0, 0)
    player=>w_maxHealth = player=>w_maxHealth + ((player->b_stamina+1)>>1) + rollDice($2600) // roundup(stam/2) + 2d6
    player=>w_health = player=>w_maxHealth // let's make leveling up an extra nice thing
    needShowParty = TRUE

    // Check XP for next level, and enforce level cap if any
    n = callGlobalFunc(GS_LEVEL_X_P, player->b_level + 1, 0, 0)
    if n > player=>w_nextXP
      player=>w_nextXP = n
    else
      player=>w_nextXP = 32767 // XP cap reached
      break
    fin
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Initialize XP (and skill pts) for newly created character (called by packer code)
export def initPlayerXP(player)#0
  player->b_skillPoints = callGlobalFunc(GS_LEVEL_S_P, player->b_level, 0, 0)
  player=>w_curXP = callGlobalFunc(GS_LEVEL_X_P, player->b_level, 0, 0)
  player=>w_nextXP = callGlobalFunc(GS_LEVEL_X_P, player->b_level + 1, 0, 0)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def equipItem(item)#0
  if item
    item->b_flags = item->b_flags | ITEM_FLAG_EQUIP
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Arm newly created player (or NPC) with first weapon and all armor.
export def girdPlayer(player)#0
  equipItem(first(player=>p_items, &(p) p->t_type == TYPE_WEAPON))
  forSome(player=>p_items, &(p) p->t_type == TYPE_ARMOR, @equipItem)
  calcPlayerArmor(player)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def addPlayerToParty(playerFuncNum, displayFunc)#0
  word p_player

  if countList(global=>p_players) == MAX_PARTY
    displayFunc("Party too large.\n")#0
    beep
    return
  fin

  // Create the player (NPC flag will be established inside the creation func)
  p_player = createStruct(MOD_GEN_PLAYERS, playerFuncNum)

  // Add if not dupe
  if !scanForNamedObj(global=>p_players, p_player=>s_name)
    addToList(@global=>p_players, p_player)
    adjustPartyStat(@S_XP, 0) // to set initial skill pts
    heapCollect
    needShowParty = TRUE
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def removeNamed(name, pList)#1
  word p_thing
  p_thing = scanForNamedObj(*pList, name)
  if p_thing
    // If it's stackable and there's more than one, just reduce the count. Otherwise take it all.
    if p_thing->t_type == TYPE_FANCY_ITEM and p_thing=>w_count > 1
      p_thing=>w_count--
    else
      removeFromList(pList, p_thing)
    fin
    return TRUE
  fin
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def takeItemFromParty(itemName)#0
  ctx = itemName; first(global=>p_players, &(p) removeNamed(ctx, @p=>p_items))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def removePlayerFromParty(playerName)#0
  removeNamed(playerName, @global=>p_players)
  needShowParty = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def partyHasPlayer(playerName)#1
  return scanForNamedObj(global=>p_players, playerName)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def getStat(player, statName)#1
  word pSkill
  when 1
    is streqi(statName, @S_INTELLIGENCE); return player->b_intelligence
    is streqi(statName, @S_STRENGTH);     return player->b_strength
    is streqi(statName, @S_AGILITY);      return player->b_agility
    is streqi(statName, @S_STAMINA);      return player->b_stamina
    is streqi(statName, @S_CHARISMA);     return player->b_charisma
    is streqi(statName, @S_SPIRIT);       return player->b_spirit
    is streqi(statName, @S_LUCK);         return player->b_luck
    is streqi(statName, @S_HEALTH);       return player=>w_health
    is streqi(statName, @S_MAX_HEALTH);   return player=>w_maxHealth
    is streqi(statName, @S_AIMING);       return player->b_aiming
    is streqi(statName, @S_HAND_TO_HAND); return player->b_handToHand
    is streqi(statName, @S_DODGING);      return player->b_dodging
    is streqi(statName, @S_GOLD);         return global=>w_gold
    is streqi(statName, @S_TIME);         return global->b_hour
    is streqi(statName, @S_XP);           return player=>w_curXP
    is streqi(statName, @S_SP);           return player->b_skillPoints
    is streqi(statName, @S_PACK_SIZE);    return player->b_packSize
    is streqi(statName, @S_BANK_BAL);     return global=>w_bankBal
  wend
  pSkill = scanForNamedObj(player=>p_skills, statName)
  if pSkill; return pSkill=>w_modValue; fin
  puts(statName); return fatal("getStat")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return an appropriate pronoun for the given gender (M/F/N...)
export def hisHerTheir(gender)#1
  if gender == 'M'; return @S_HIS; fin
  if gender == 'F'; return @S_HER; fin
  return @S_THEIR
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def clampByte(val)#1
  return max(0, min(255, val))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def setHour(val) #0
  val = val % 24 // clamp to range 0..23
  if val <> global->b_hour or global->b_minute
    if global->b_hour >= val; val = val + 24; fin // to ensure positive difference
    advTime(val - global->b_hour - 1, 59 - global->b_minute, 60 - global->b_second)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Adjust the given stat for every (living) player in the party
export def adjustPartyStat(statName, val)#0
  word p_player
  when 1
    is streqi(statName, @S_GOLD);         global=>w_gold = max(0, global=>w_gold + val); needShowParty = TRUE; break
    is streqi(statName, @S_TIME);         setHour(global->b_hour + val); break;
    is streqi(statName, @S_BANK_BAL);     global=>w_bankBal = global=>w_bankBal + val; break
    otherwise
      p_player = global=>p_players
      while p_player
        if p_player=>w_health > 0
          setStat(p_player, statName, getStat(p_player, statName) + val)
        fin
        p_player = p_player=>p_nextObj
      loop
  wend
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the max value of the stat for any (living) character in the party.
// Except if a particular player is using a skill or item in which case it's their stat only.
export def getStatInContext(statName)#1
  word p_player, val
  val = 0
  when 1
    is streqi(statName, @S_GOLD);         return global=>w_gold
    is streqi(statName, @S_TIME);         return global->b_hour
    is streqi(statName, @S_BANK_BAL);     return global=>w_bankBal
    is playerUsing <> NULL;               return getStat(playerUsing, statName)
    otherwise
      p_player = global=>p_players
      while p_player
        val = max(val, getStat(p_player, statName))
        p_player = p_player=>p_nextObj
      loop
  wend
  return val
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def setStat(player, statName, val)#0
  word pSkill
  when 1
    is streqi(statName, @S_INTELLIGENCE); player->b_intelligence = clampByte(val); break
    is streqi(statName, @S_STRENGTH);     player->b_strength = clampByte(val); break
    is streqi(statName, @S_AGILITY);      player->b_agility = clampByte(val); break
    is streqi(statName, @S_STAMINA);      player->b_stamina = clampByte(val); break
    is streqi(statName, @S_CHARISMA);     player->b_charisma = clampByte(val); break
    is streqi(statName, @S_SPIRIT);       player->b_spirit = clampByte(val); break
    is streqi(statName, @S_LUCK);         player->b_luck = clampByte(val); break
    is streqi(statName, @S_HEALTH);       player=>w_health = max(0, min(player=>w_maxHealth, val))
                                          needShowParty = TRUE; break
    is streqi(statName, @S_MAX_HEALTH);   player=>w_maxHealth = max(0, val); break
    is streqi(statName, @S_AIMING);       player->b_aiming = clampByte(val); break
    is streqi(statName, @S_HAND_TO_HAND); player->b_handToHand = clampByte(val); break
    is streqi(statName, @S_DODGING);      player->b_dodging = clampByte(val); break
    is streqi(statName, @S_GOLD);         global=>w_gold = max(0, val); needShowParty = TRUE; break
    is streqi(statName, @S_TIME);         setHour(val); break;
    is streqi(statName, @S_XP);           addXP(player, val - player=>w_curXP); needShowParty = TRUE; break
    is streqi(statName, @S_SP);           player->b_skillPoints = clampByte(max(0, val)); needShowParty = TRUE; break
    is streqi(statName, @S_PACK_SIZE);    player->b_packSize = clampByte(max(player->b_packSize, val)); break
    is streqi(statName, @S_BANK_BAL);     global=>w_bankBal = val; break
    otherwise
      pSkill = scanForNamedObj(player=>p_skills, statName)
      if pSkill
        pSkill=>w_modValue = max(0, val)
      else
        puts(statName); fatal("setStat")
      fin
  wend
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def setGameFlag(flagNum, val)#0
  byte byteNum, mask
  byteNum = flagNum >> 3
  mask = 1<<(flagNum & 7)
  if val
    global->ba_gameFlags[byteNum] = global->ba_gameFlags[byteNum] | mask
  else
    global->ba_gameFlags[byteNum] = global->ba_gameFlags[byteNum] & ~mask
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def getGameFlag(flagNum)#1
  byte byteNum, mask
  byteNum = flagNum >> 3
  mask = 1<<(flagNum & 7)
  return global->ba_gameFlags[byteNum] & mask
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def setStoryMode(enable)#0
  // Story mode only exists on 800K or hard drive builds, and not the disk-limited ones.
  if !isFloppyVer and !diskLimit
    storyMode = enable
    if enable
      loadEngine(MOD_STORY)
      curEngine()=>story_mode(TRUE, preEnginePortraitNum)
      needRender = FALSE
      frameLoaded = 0 // since we just destroyed it
      textDrawn = FALSE
      textClearCountdown = 0
    else
      curEngine()=>story_mode(FALSE, 0)
      preEnginePortraitNum = 0 // so we don't restore it
      returnFromEngine(TRUE)
    fin
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def displayStory(storyNum)#0
  curEngine()=>story_display(storyNum)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def benchPlayer()#0
  loadEngine(MOD_PARTY)=>party_benchPlayer()
  returnFromEngine(TRUE)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def unbenchPlayer()#0
  loadEngine(MOD_PARTY)=>party_unbenchPlayer()
  returnFromEngine(TRUE)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
export def buySell(storeCode, profitRatio)#0
  if storeCode
    loadEngine(MOD_STORE)=>store_buyFromStore(storeCode, profitRatio)
  else
    loadEngine(MOD_STORE)=>store_sellToStore(profitRatio)
  fin
  returnFromEngine(FALSE) // no render, we're mid-script
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def startGame(firstTime, ask)#0
  word p_module

  typeHash = hashBuffer(@typeTbl_TGlobal, @typeTbls - @typeTbl_TGlobal) ^ HEAP_BOTTOM

  // Create a new game or load an existing one
  mmgr(START_LOAD, 1) // code is in partition 1
  if firstTime
    // Temporarily reserve a big chunk of aux mem to keep diskops bytecode out
    auxMmgr(SET_MEM_TARGET, $800) // well above where resource index loads at startup
    auxMmgr(REQUEST_MEMORY, $7F00) // plenty of room for resource index + temp expander
    // Reserve space for the small-object heap
    mmgr(SET_MEM_TARGET, HEAP_BOTTOM)
    mmgr(REQUEST_MEMORY, HEAP_SIZE)
    mmgr(LOCK_MEMORY,    HEAP_BOTTOM)
  fin
  p_module = mmgr(QUEUE_LOAD, MOD_DISKOPS<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  q_playerDeath = FALSE
  if firstTime; p_module()=>diskops_startup(); fin
  if p_module()=>diskops_newOrLoadGame(ask)
    mapIs3D = q_mapIs3D
    mapNum = q_mapNum
    q_mapNum = 0
    initMap(q_x, q_y, q_dir)
    saveGame()
    setTextCountdown
  else
    q_mapNum = 0
    restoreMapPos()
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Main code.
//
scriptDisplayStr(@_scriptDisplayStr) // 1-time init
startGame(TRUE, TRUE) // first time init; ask whether new or load
kbdLoop()

done
