Massive optimizations. New music player. Serial support.
This commit is contained in:
parent
a22f35078a
commit
94cc14ff28
13 changed files with 705 additions and 225 deletions
319
agi.c
319
agi.c
|
|
@ -13,6 +13,8 @@
|
||||||
// screen the VM can't yet advance through. 0 means "use
|
// screen the VM can't yet advance through. 0 means "use
|
||||||
// whatever logic.0 picks", same as omitting the argument.)
|
// whatever logic.0 picks", same as omitting the argument.)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#include "agi.h"
|
#include "agi.h"
|
||||||
|
|
||||||
#include "joey/audio.h"
|
#include "joey/audio.h"
|
||||||
|
|
@ -21,6 +23,7 @@
|
||||||
#include "joey/draw.h"
|
#include "joey/draw.h"
|
||||||
#include "joey/input.h"
|
#include "joey/input.h"
|
||||||
#include "joey/palette.h"
|
#include "joey/palette.h"
|
||||||
|
#include "joey/platform.h"
|
||||||
#include "joey/present.h"
|
#include "joey/present.h"
|
||||||
#include "joey/sprite.h"
|
#include "joey/sprite.h"
|
||||||
#include "joey/surface.h"
|
#include "joey/surface.h"
|
||||||
|
|
@ -110,6 +113,7 @@ static void agiSoundReset(void);
|
||||||
static uint32_t agiSynthFill(void *ctx, int8_t *dst, uint32_t count);
|
static uint32_t agiSynthFill(void *ctx, int8_t *dst, uint32_t count);
|
||||||
static void agiTickAdvance(void);
|
static void agiTickAdvance(void);
|
||||||
static void agiVoiceTick(uint8_t voice);
|
static void agiVoiceTick(uint8_t voice);
|
||||||
|
static void psgNoiseApply(void);
|
||||||
static void psgRecompute(uint8_t chipCh);
|
static void psgRecompute(uint8_t chipCh);
|
||||||
static void psgWriteData(uint8_t val);
|
static void psgWriteData(uint8_t val);
|
||||||
static void cbAddToPic(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel,
|
static void cbAddToPic(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel,
|
||||||
|
|
@ -129,6 +133,8 @@ static void cbStopSound(void *ctx);
|
||||||
static uint8_t cbViewCelCount(void *ctx, uint8_t viewId, uint8_t loopId);
|
static uint8_t cbViewCelCount(void *ctx, uint8_t viewId, uint8_t loopId);
|
||||||
static uint8_t cbViewLoopCount(void *ctx, uint8_t viewId);
|
static uint8_t cbViewLoopCount(void *ctx, uint8_t viewId);
|
||||||
static bool decodePicInto(uint8_t picId, bool clearFirst);
|
static bool decodePicInto(uint8_t picId, bool clearFirst);
|
||||||
|
static void agiInputCapture(void);
|
||||||
|
static void agiParseCommand(void);
|
||||||
static void dispatchAllPressedKeys(void);
|
static void dispatchAllPressedKeys(void);
|
||||||
static void drawObjects(jlSurfaceT *stage);
|
static void drawObjects(jlSurfaceT *stage);
|
||||||
static const AgiViewT *fetchView(uint8_t viewId);
|
static const AgiViewT *fetchView(uint8_t viewId);
|
||||||
|
|
@ -138,7 +144,6 @@ static uint8_t parseArgU8(const char *s);
|
||||||
static void releaseLogicCache(void);
|
static void releaseLogicCache(void);
|
||||||
static void releaseViewCache(void);
|
static void releaseViewCache(void);
|
||||||
static void renderFrame(jlSurfaceT *stage);
|
static void renderFrame(jlSurfaceT *stage);
|
||||||
static const char *resolveGameDir(int argc, char **argv);
|
|
||||||
static uint8_t resolveStartingRoom(int argc, char **argv);
|
static uint8_t resolveStartingRoom(int argc, char **argv);
|
||||||
static void runVmCycle(void);
|
static void runVmCycle(void);
|
||||||
static void updateEgoFromInput(void);
|
static void updateEgoFromInput(void);
|
||||||
|
|
@ -332,6 +337,149 @@ static void cbAddToPic(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Accumulate typed characters into gVm.inputLine while the command line is
|
||||||
|
// active (accept.input). Printable keys append, backspace deletes, and Enter
|
||||||
|
// hands the finished line to agiParseCommand. Called every frame from the
|
||||||
|
// main loop; the on-screen echo is drawn in renderFrame.
|
||||||
|
static void agiInputCapture(void) {
|
||||||
|
uint8_t k;
|
||||||
|
|
||||||
|
if (jlKeyPressed(KEY_BACKSPACE)) {
|
||||||
|
if (gVm.inputLen > 0u) {
|
||||||
|
gVm.inputLen--;
|
||||||
|
gVm.inputLine[gVm.inputLen] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jlKeyPressed(KEY_SPACE) && gVm.inputLen < AGI_INPUT_MAX) {
|
||||||
|
gVm.inputLine[gVm.inputLen] = ' ';
|
||||||
|
gVm.inputLen++;
|
||||||
|
gVm.inputLine[gVm.inputLen] = '\0';
|
||||||
|
}
|
||||||
|
for (k = 0u; k < 26u; k++) {
|
||||||
|
if (jlKeyPressed((jlKeyE)((uint8_t)KEY_A + k)) && gVm.inputLen < AGI_INPUT_MAX) {
|
||||||
|
gVm.inputLine[gVm.inputLen] = (char)('a' + k);
|
||||||
|
gVm.inputLen++;
|
||||||
|
gVm.inputLine[gVm.inputLen] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (k = 0u; k < 10u; k++) {
|
||||||
|
if (jlKeyPressed((jlKeyE)((uint8_t)KEY_0 + k)) && gVm.inputLen < AGI_INPUT_MAX) {
|
||||||
|
gVm.inputLine[gVm.inputLen] = (char)('0' + k);
|
||||||
|
gVm.inputLen++;
|
||||||
|
gVm.inputLine[gVm.inputLen] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jlKeyPressed(KEY_RETURN)) {
|
||||||
|
if (gVm.inputLen > 0u) {
|
||||||
|
agiParseCommand();
|
||||||
|
}
|
||||||
|
gVm.inputLine[0] = '\0';
|
||||||
|
gVm.inputLen = 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tokenize gVm.inputLine into dictionary group IDs (gVm.parsedWords), using
|
||||||
|
// greedy longest-match so multi-word entries ("crystal ball", "get in") beat
|
||||||
|
// their prefixes. Ignore-words (group 0) drop out; a word absent from the
|
||||||
|
// dictionary records its 1-based position in var 9 and voids the command
|
||||||
|
// (no parsedWords, so said() won't match and the room can report the miss).
|
||||||
|
// Sets flag 2 so said() tests fire this cycle.
|
||||||
|
static void agiParseCommand(void) {
|
||||||
|
static char toks[AGI_MAX_PARSED][AGI_WORD_MAX_LEN + 1u];
|
||||||
|
static uint8_t tokLen[AGI_MAX_PARSED];
|
||||||
|
char phrase[AGI_WORD_MAX_LEN + 1u];
|
||||||
|
uint8_t tokCount;
|
||||||
|
uint8_t ti;
|
||||||
|
uint16_t i;
|
||||||
|
uint8_t pos;
|
||||||
|
bool unknown;
|
||||||
|
|
||||||
|
// Split into lowercase alphanumeric tokens (cap at AGI_MAX_PARSED).
|
||||||
|
tokCount = 0u;
|
||||||
|
ti = 0u;
|
||||||
|
for (i = 0u; i <= gVm.inputLen; i++) {
|
||||||
|
char c;
|
||||||
|
|
||||||
|
c = (i < gVm.inputLen) ? gVm.inputLine[i] : ' ';
|
||||||
|
if (c >= 'A' && c <= 'Z') {
|
||||||
|
c = (char)(c + ('a' - 'A'));
|
||||||
|
}
|
||||||
|
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
||||||
|
if (tokCount < AGI_MAX_PARSED && ti < AGI_WORD_MAX_LEN) {
|
||||||
|
toks[tokCount][ti] = c;
|
||||||
|
ti++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ti > 0u && tokCount < AGI_MAX_PARSED) {
|
||||||
|
toks[tokCount][ti] = '\0';
|
||||||
|
tokLen[tokCount] = ti;
|
||||||
|
tokCount++;
|
||||||
|
}
|
||||||
|
ti = 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
gVm.parsedWordCount = 0u;
|
||||||
|
gVm.vars[AGI_VAR_UNKNOWN_WORD] = 0u;
|
||||||
|
unknown = false;
|
||||||
|
pos = 0u;
|
||||||
|
while (pos < tokCount) {
|
||||||
|
uint16_t bestId;
|
||||||
|
uint8_t best;
|
||||||
|
uint8_t span;
|
||||||
|
|
||||||
|
best = 0u;
|
||||||
|
bestId = AGI_WORD_UNKNOWN;
|
||||||
|
// Longest join first: try tokens[pos..end], then shorter, down to one.
|
||||||
|
for (span = (uint8_t)(tokCount - pos); span >= 1u; span--) {
|
||||||
|
uint8_t pl;
|
||||||
|
uint8_t s;
|
||||||
|
uint16_t id;
|
||||||
|
|
||||||
|
pl = 0u;
|
||||||
|
for (s = 0u; s < span; s++) {
|
||||||
|
uint8_t j;
|
||||||
|
|
||||||
|
if (s > 0u && pl < AGI_WORD_MAX_LEN) {
|
||||||
|
phrase[pl] = ' ';
|
||||||
|
pl++;
|
||||||
|
}
|
||||||
|
for (j = 0u; j < tokLen[pos + s] && pl < AGI_WORD_MAX_LEN; j++) {
|
||||||
|
phrase[pl] = toks[pos + s][j];
|
||||||
|
pl++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
phrase[pl] = '\0';
|
||||||
|
id = agiWordId(&gGame, phrase);
|
||||||
|
if (id != AGI_WORD_UNKNOWN) {
|
||||||
|
best = span;
|
||||||
|
bestId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best == 0u) {
|
||||||
|
if (!unknown) {
|
||||||
|
gVm.vars[AGI_VAR_UNKNOWN_WORD] = (uint8_t)(pos + 1u);
|
||||||
|
unknown = true;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (bestId != AGI_WORD_IGNORE && gVm.parsedWordCount < AGI_MAX_PARSED) {
|
||||||
|
gVm.parsedWords[gVm.parsedWordCount] = bestId;
|
||||||
|
gVm.parsedWordCount++;
|
||||||
|
}
|
||||||
|
pos = (uint8_t)(pos + best);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknown) {
|
||||||
|
gVm.parsedWordCount = 0u;
|
||||||
|
}
|
||||||
|
gVm.flags[AGI_FLAG_INPUT] = 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void cbDiscardPic(void *ctx, uint8_t picId) {
|
static void cbDiscardPic(void *ctx, uint8_t picId) {
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
(void)picId;
|
(void)picId;
|
||||||
|
|
@ -569,15 +717,40 @@ static void cbShowPic(void *ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Push the chip's noise-channel state (gPsg[3]) to the JoeyLib noise
|
||||||
|
// voice. The SN76489 freqCount maps onto the portable 0..31 pitch as
|
||||||
|
// freqCount >> 3 (rate codes 0/1/2 -> 4/8/16, preserving the chip's
|
||||||
|
// octave-per-step; mode 3 tracks tone channel 2's snapshot). The
|
||||||
|
// white/periodic bit has no portable representation and renders as
|
||||||
|
// plain hiss.
|
||||||
|
static void psgNoiseApply(void) {
|
||||||
|
uint16_t count;
|
||||||
|
uint8_t pitch;
|
||||||
|
|
||||||
|
if (gPsg[3].attenuation >= 15u || gPsg[3].freqCount == 0u) {
|
||||||
|
jlAudioNoise(0u, 15u);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count = gPsg[3].freqCount;
|
||||||
|
pitch = (uint8_t)(count >> 3);
|
||||||
|
if (pitch > 31u) {
|
||||||
|
pitch = 31u;
|
||||||
|
}
|
||||||
|
jlAudioNoise(pitch, gPsg[3].attenuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Recompute derived (phaseInc, amplitude) for chip channel
|
// Recompute derived (phaseInc, amplitude) for chip channel
|
||||||
// `chipCh` from its current freqCount + attenuation + genType.
|
// `chipCh` from its current freqCount + attenuation + genType.
|
||||||
// Called whenever the chip state for a tone channel changes;
|
// Called whenever the chip state for a tone channel changes; the
|
||||||
// noise channels aren't synthesized and skip recompute.
|
// noise channel isn't synthesized into the PCM stream -- it goes to
|
||||||
|
// the hardware noise voice instead.
|
||||||
static void psgRecompute(uint8_t chipCh) {
|
static void psgRecompute(uint8_t chipCh) {
|
||||||
PsgChannelT *p;
|
PsgChannelT *p;
|
||||||
uint32_t freqHz;
|
uint32_t freqHz;
|
||||||
|
|
||||||
if (chipCh >= 3u) {
|
if (chipCh >= 3u) {
|
||||||
|
psgNoiseApply();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
p = &gPsg[chipCh];
|
p = &gPsg[chipCh];
|
||||||
|
|
@ -631,7 +804,9 @@ static void psgWriteData(uint8_t val) {
|
||||||
case 2u: gPsg[3].freqCount = 128u; break;
|
case 2u: gPsg[3].freqCount = 128u; break;
|
||||||
default: gPsg[3].freqCount = (uint16_t)(gPsg[2].freqCount * 2u); break;
|
default: gPsg[3].freqCount = (uint16_t)(gPsg[2].freqCount * 2u); break;
|
||||||
}
|
}
|
||||||
// No _reg update; no synth recompute (noise not synthesized).
|
// No _reg update -- the DATA byte that follows still targets
|
||||||
|
// the previously latched channel.
|
||||||
|
psgNoiseApply();
|
||||||
} else if (val & 0x80u) {
|
} else if (val & 0x80u) {
|
||||||
reg = (uint8_t)((val >> 5) & 0x03u);
|
reg = (uint8_t)((val >> 5) & 0x03u);
|
||||||
gPsgReg = reg;
|
gPsgReg = reg;
|
||||||
|
|
@ -652,6 +827,9 @@ static void agiSoundReset(void) {
|
||||||
uint8_t v;
|
uint8_t v;
|
||||||
|
|
||||||
jlAudioStopSfx(0u);
|
jlAudioStopSfx(0u);
|
||||||
|
// The noise voice is live hardware, separate from the PCM stream
|
||||||
|
// slot -- without an explicit off it latches on forever.
|
||||||
|
jlAudioNoise(0u, 15u);
|
||||||
for (v = 0u; v < AGI_SOUND_CHANNELS; v++) {
|
for (v = 0u; v < AGI_SOUND_CHANNELS; v++) {
|
||||||
gVoiceSched[v].chOffset = 0u;
|
gVoiceSched[v].chOffset = 0u;
|
||||||
gVoiceSched[v].eventFramesRem = 0u;
|
gVoiceSched[v].eventFramesRem = 0u;
|
||||||
|
|
@ -804,6 +982,7 @@ static void agiTickAdvance(void) {
|
||||||
}
|
}
|
||||||
if (allDone) {
|
if (allDone) {
|
||||||
gSoundActive = false;
|
gSoundActive = false;
|
||||||
|
jlAudioNoise(0u, 15u); // percussion must not outlive the song
|
||||||
jlLogF("synth EOS wallMs=%u v0done=%u v1done=%u v2done=%u v3done=%u",
|
jlLogF("synth EOS wallMs=%u v0done=%u v1done=%u v2done=%u v3done=%u",
|
||||||
(unsigned)nowMs,
|
(unsigned)nowMs,
|
||||||
(unsigned)gVoiceSched[0].done,
|
(unsigned)gVoiceSched[0].done,
|
||||||
|
|
@ -1236,7 +1415,15 @@ static void dispatchAllPressedKeys(void) {
|
||||||
};
|
};
|
||||||
uint8_t k;
|
uint8_t k;
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
bool captureText;
|
||||||
|
|
||||||
|
// While the command line is live (and no modal is up), printable keys,
|
||||||
|
// Enter, Space and Backspace belong to the input line (agiInputCapture),
|
||||||
|
// not to controller/key-binding dispatch. Tab, function keys and the
|
||||||
|
// joystick stay as controllers so status/menu/action keys still work.
|
||||||
|
captureText = gVm.acceptInput && !gVm.printModal.active;
|
||||||
|
|
||||||
|
if (!captureText) {
|
||||||
if (jlKeyPressed(KEY_RETURN)) {
|
if (jlKeyPressed(KEY_RETURN)) {
|
||||||
agiVmDispatchKey(&gVm, 13u, 0u);
|
agiVmDispatchKey(&gVm, 13u, 0u);
|
||||||
gAnyKeyHit = true;
|
gAnyKeyHit = true;
|
||||||
|
|
@ -1245,10 +1432,6 @@ static void dispatchAllPressedKeys(void) {
|
||||||
agiVmDispatchKey(&gVm, 32u, 0u);
|
agiVmDispatchKey(&gVm, 32u, 0u);
|
||||||
gAnyKeyHit = true;
|
gAnyKeyHit = true;
|
||||||
}
|
}
|
||||||
if (jlKeyPressed(KEY_TAB)) {
|
|
||||||
agiVmDispatchKey(&gVm, 9u, 0u);
|
|
||||||
gAnyKeyHit = true;
|
|
||||||
}
|
|
||||||
if (jlKeyPressed(KEY_BACKSPACE)) {
|
if (jlKeyPressed(KEY_BACKSPACE)) {
|
||||||
agiVmDispatchKey(&gVm, 8u, 0u);
|
agiVmDispatchKey(&gVm, 8u, 0u);
|
||||||
gAnyKeyHit = true;
|
gAnyKeyHit = true;
|
||||||
|
|
@ -1265,6 +1448,11 @@ static void dispatchAllPressedKeys(void) {
|
||||||
gAnyKeyHit = true;
|
gAnyKeyHit = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (jlKeyPressed(KEY_TAB)) {
|
||||||
|
agiVmDispatchKey(&gVm, 9u, 0u);
|
||||||
|
gAnyKeyHit = true;
|
||||||
|
}
|
||||||
for (i = 0u; i < 10u; i++) {
|
for (i = 0u; i < 10u; i++) {
|
||||||
if (jlKeyPressed(fnKeys[i])) {
|
if (jlKeyPressed(fnKeys[i])) {
|
||||||
agiVmDispatchKey(&gVm, 0u, fnScancodes[i]);
|
agiVmDispatchKey(&gVm, 0u, fnScancodes[i]);
|
||||||
|
|
@ -1282,44 +1470,21 @@ static void dispatchAllPressedKeys(void) {
|
||||||
|
|
||||||
// Restore the exact stage rect `(x, y, w, h)` from the gBackdrop
|
// Restore the exact stage rect `(x, y, w, h)` from the gBackdrop
|
||||||
// surface. Chunky-fast: a single memcpy per row from backdrop pixels
|
// surface. Chunky-fast: a single memcpy per row from backdrop pixels
|
||||||
// to stage pixels. Bypassing jlDrawPixel keeps per-frame restore
|
// to stage pixels via the library's rect copy (hunt-3 rank 3). The
|
||||||
// cost ~1KB memcpy per object instead of per-pixel function calls.
|
// old body hand-rolled a chunky memcpy loop and, on the planar 68k
|
||||||
// Planar (s->pixels NULL) is uncommon at this point; fall back to
|
// ports (stage->pixels NULL post-Phase-9/10), fell back to a FULL
|
||||||
// jlSurfaceCopy of the whole surface (slower but correct).
|
// 32KB jlSurfaceCopy per call -- 3-5 times per frame -- which also
|
||||||
|
// re-marked the whole stage AND the SCB/palette dirty, defeating the
|
||||||
|
// group-skip presents. jlSurfaceCopyRect does everything this needs:
|
||||||
|
// clips, widens x to the 16-px group grid (safe -- the erase-all-
|
||||||
|
// then-redraw-all object pass repaints anything the widening grazes),
|
||||||
|
// routes to each port's fast copy (ST move.l spans, Amiga blitter
|
||||||
|
// arm, IIgs inline word, DOS memcpy), and marks exactly the rect.
|
||||||
static void restoreRectFromBackdrop(jlSurfaceT *stage, int16_t x, int16_t y, int16_t w, int16_t h) {
|
static void restoreRectFromBackdrop(jlSurfaceT *stage, int16_t x, int16_t y, int16_t w, int16_t h) {
|
||||||
int16_t row;
|
|
||||||
int16_t byteX;
|
|
||||||
int16_t byteW;
|
|
||||||
int16_t endY;
|
|
||||||
|
|
||||||
if (stage == NULL || gBackdrop == NULL || w <= 0 || h <= 0) {
|
if (stage == NULL || gBackdrop == NULL || w <= 0 || h <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (x < 0) { w += x; x = 0; }
|
jlSurfaceCopyRect(stage, gBackdrop, x, y, (uint16_t)w, (uint16_t)h);
|
||||||
if (y < 0) { h += y; y = 0; }
|
|
||||||
if (x >= (int16_t)SURFACE_WIDTH || y >= (int16_t)SURFACE_HEIGHT) { return; }
|
|
||||||
if (x + w > (int16_t)SURFACE_WIDTH) { w = (int16_t)SURFACE_WIDTH - x; }
|
|
||||||
if (y + h > (int16_t)SURFACE_HEIGHT) { h = (int16_t)SURFACE_HEIGHT - y; }
|
|
||||||
if (w <= 0 || h <= 0) { return; }
|
|
||||||
|
|
||||||
if (stage->pixels == NULL || gBackdrop->pixels == NULL) {
|
|
||||||
// Planar fallback -- restore from backdrop via full copy.
|
|
||||||
// Wasteful but rare in current ports (Amiga only).
|
|
||||||
jlSurfaceCopy(stage, gBackdrop);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Round to byte boundaries (4bpp packed: byte index = x/2).
|
|
||||||
// Both x and w come from cel boundaries already aligned to 2px
|
|
||||||
// (pixel doubling) so this is exact.
|
|
||||||
byteX = (int16_t)(x >> 1);
|
|
||||||
byteW = (int16_t)((w + 1) >> 1);
|
|
||||||
endY = (int16_t)(y + h);
|
|
||||||
for (row = y; row < endY; row++) {
|
|
||||||
memcpy(&stage->pixels[row * (int16_t)SURFACE_BYTES_PER_ROW + byteX],
|
|
||||||
&gBackdrop->pixels[row * (int16_t)SURFACE_BYTES_PER_ROW + byteX],
|
|
||||||
(size_t)byteW);
|
|
||||||
}
|
|
||||||
surfaceMarkDirtyRect(stage, x, y, w, h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1549,6 +1714,31 @@ static void renderFrame(jlSurfaceT *stage) {
|
||||||
}
|
}
|
||||||
// Step 2: restore + draw animated/drawn objects.
|
// Step 2: restore + draw animated/drawn objects.
|
||||||
drawObjects(stage);
|
drawObjects(stage);
|
||||||
|
// Command line: echo "> " + the typed text (with a cursor) on the input
|
||||||
|
// row while accept.input is live; blank our prompt when it turns off so
|
||||||
|
// the line disappears. This reuses the text-row plane so agiTextRender
|
||||||
|
// and the dirty-row bookkeeping below draw and wipe it like any message.
|
||||||
|
{
|
||||||
|
char *dst = gVm.textRows[AGI_INPUT_ROW].text;
|
||||||
|
|
||||||
|
if (gVm.acceptInput) {
|
||||||
|
uint8_t n;
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
n = 0u;
|
||||||
|
dst[n++] = '>';
|
||||||
|
dst[n++] = ' ';
|
||||||
|
for (i = 0u; i < gVm.inputLen && n < (uint8_t)AGI_TEXT_COLS; i++) {
|
||||||
|
dst[n++] = gVm.inputLine[i];
|
||||||
|
}
|
||||||
|
if (n < (uint8_t)AGI_TEXT_COLS) {
|
||||||
|
dst[n++] = '_';
|
||||||
|
}
|
||||||
|
dst[n] = '\0';
|
||||||
|
} else if (dst[0] == '>') {
|
||||||
|
dst[0] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
// Step 3: draw current text rows on top. Mark dirty as we go so
|
// Step 3: draw current text rows on top. Mark dirty as we go so
|
||||||
// step 1 of the next frame knows to wipe them.
|
// step 1 of the next frame knows to wipe them.
|
||||||
{
|
{
|
||||||
|
|
@ -1569,19 +1759,20 @@ static void renderFrame(jlSurfaceT *stage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static const char *resolveGameDir(int argc, char **argv) {
|
|
||||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
|
||||||
return argv[1];
|
|
||||||
}
|
|
||||||
return ".";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static uint8_t resolveStartingRoom(int argc, char **argv) {
|
static uint8_t resolveStartingRoom(int argc, char **argv) {
|
||||||
if (argc <= 2 || argv[2] == NULL) {
|
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||||
|
// GS/OS launches an S16 app with no usable argv (the crt0 leaves
|
||||||
|
// argc/argv uninitialized -- garbage), so always start at the title
|
||||||
|
// room. Game data comes from DATA/ regardless of argv now.
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
return 0u;
|
||||||
|
#else
|
||||||
|
if (argc <= 1 || argv[1] == NULL) {
|
||||||
return 0u;
|
return 0u;
|
||||||
}
|
}
|
||||||
return parseArgU8(argv[2]);
|
return parseArgU8(argv[1]);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1615,6 +1806,11 @@ static void runVmCycle(void) {
|
||||||
if (reason == AGI_VM_HALT_RETURN) {
|
if (reason == AGI_VM_HALT_RETURN) {
|
||||||
agiVmTickAnimation(&gVm);
|
agiVmTickAnimation(&gVm);
|
||||||
gVm.flags[5] = 0u;
|
gVm.flags[5] = 0u;
|
||||||
|
// The player's command was offered to every logic this cycle;
|
||||||
|
// consume it so said() can't re-fire it on later cycles. (A
|
||||||
|
// matching said() already cleared it; this covers the no-match
|
||||||
|
// case.)
|
||||||
|
gVm.flags[AGI_FLAG_INPUT] = 0u;
|
||||||
length = 0u;
|
length = 0u;
|
||||||
logic0 = cbFetchLogic(NULL, 0u, &length);
|
logic0 = cbFetchLogic(NULL, 0u, &length);
|
||||||
if (logic0 != NULL) {
|
if (logic0 != NULL) {
|
||||||
|
|
@ -1696,7 +1892,6 @@ static void updateEgoFromInput(void) {
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
jlConfigT cfg;
|
jlConfigT cfg;
|
||||||
AgiVmCallbacksT cb;
|
AgiVmCallbacksT cb;
|
||||||
const char *gameDir;
|
|
||||||
const uint8_t *logic0;
|
const uint8_t *logic0;
|
||||||
uint16_t length;
|
uint16_t length;
|
||||||
uint8_t startingRoom;
|
uint8_t startingRoom;
|
||||||
|
|
@ -1732,6 +1927,7 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The view cache is too large for bank-0 BSS on the IIgs; pull it
|
// The view cache is too large for bank-0 BSS on the IIgs; pull it
|
||||||
// from the platform's large-allocator. jlAlloc does not zero its
|
// from the platform's large-allocator. jlAlloc does not zero its
|
||||||
// result, so clear it so every slot starts with loaded == false.
|
// result, so clear it so every slot starts with loaded == false.
|
||||||
|
|
@ -1742,7 +1938,6 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
memset(gViewCache, 0, sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES);
|
memset(gViewCache, 0, sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES);
|
||||||
|
|
||||||
gameDir = resolveGameDir(argc, argv);
|
|
||||||
startingRoom = resolveStartingRoom(argc, argv);
|
startingRoom = resolveStartingRoom(argc, argv);
|
||||||
gOverrideRoom = startingRoom;
|
gOverrideRoom = startingRoom;
|
||||||
gOverrideRoomActive = (startingRoom != 0u);
|
gOverrideRoomActive = (startingRoom != 0u);
|
||||||
|
|
@ -1752,7 +1947,7 @@ int main(int argc, char **argv) {
|
||||||
gPicReady = false;
|
gPicReady = false;
|
||||||
gAnyKeyHit = false;
|
gAnyKeyHit = false;
|
||||||
|
|
||||||
if (agiResOpen(&gGame, gameDir)) {
|
if (agiResOpen(&gGame)) {
|
||||||
picOk = agiPicAlloc(&gPic);
|
picOk = agiPicAlloc(&gPic);
|
||||||
if (picOk) {
|
if (picOk) {
|
||||||
agiPicClear(&gPic);
|
agiPicClear(&gPic);
|
||||||
|
|
@ -1814,6 +2009,12 @@ int main(int argc, char **argv) {
|
||||||
gAnyKeyHit = false;
|
gAnyKeyHit = false;
|
||||||
dispatchAllPressedKeys();
|
dispatchAllPressedKeys();
|
||||||
|
|
||||||
|
// When the command line is live (and no modal is up), typed
|
||||||
|
// characters build the input line instead of firing controllers.
|
||||||
|
if (gVm.acceptInput && !gVm.printModal.active) {
|
||||||
|
agiInputCapture();
|
||||||
|
}
|
||||||
|
|
||||||
// Pump the AGI tick generator EVERY frame, even with a
|
// Pump the AGI tick generator EVERY frame, even with a
|
||||||
// print modal up. This drives music event playback AND
|
// print modal up. This drives music event playback AND
|
||||||
// the v11 seconds counter -- both keyed to the same
|
// the v11 seconds counter -- both keyed to the same
|
||||||
|
|
@ -1863,6 +2064,14 @@ int main(int argc, char **argv) {
|
||||||
// pulls the next chunk from agiSynthFill (when music
|
// pulls the next chunk from agiSynthFill (when music
|
||||||
// is armed) or libxmp + SFX overlay (when not).
|
// is armed) or libxmp + SFX overlay (when not).
|
||||||
jlAudioFrameTick();
|
jlAudioFrameTick();
|
||||||
|
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||||
|
// The IIgs noise voice is an ~80 ms one-shot; re-assert
|
||||||
|
// held noise once per frame so it sustains (same model as
|
||||||
|
// the music sequencer's per-pump re-assert).
|
||||||
|
if (gSoundActive && gPsg[3].attenuation < 15u) {
|
||||||
|
psgNoiseApply();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
jlWaitVBL();
|
jlWaitVBL();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
agi.h
56
agi.h
|
|
@ -71,8 +71,38 @@ typedef struct {
|
||||||
FILE *volFiles[AGI_MAX_VOLUMES];
|
FILE *volFiles[AGI_MAX_VOLUMES];
|
||||||
uint16_t resCount[AGI_RES_COUNT];
|
uint16_t resCount[AGI_RES_COUNT];
|
||||||
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
|
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
|
||||||
|
// WORDS.TOK dictionary, loaded whole. Prefix-compressed entries are
|
||||||
|
// walked on demand by agiWordId (parse is infrequent -- once per typed
|
||||||
|
// command -- so a flat scan beats building a hashed table).
|
||||||
|
uint8_t *words;
|
||||||
|
uint32_t wordsLen;
|
||||||
} AgiGameT;
|
} AgiGameT;
|
||||||
|
|
||||||
|
// Reserved AGI word group IDs.
|
||||||
|
#define AGI_WORD_IGNORE 0u // "a", "the", ... -- dropped by parse
|
||||||
|
#define AGI_WORD_ANYWORD 1u // said() wildcard: matches any one word
|
||||||
|
#define AGI_WORD_REST 9999u // said() wildcard: matches rest of line
|
||||||
|
#define AGI_WORD_UNKNOWN 0xFFFFu // agiWordId: typed word not in dictionary
|
||||||
|
|
||||||
|
// WORDS.TOK layout + limits. The header is 26 big-endian 2-byte offsets
|
||||||
|
// (one per initial letter); word entries follow. AGI v2 dictionaries are a
|
||||||
|
// few KB (KQ3 is ~5.7 KB); 16 KB is ample headroom.
|
||||||
|
#define AGI_WORDS_HEADER_BYTES 52u
|
||||||
|
#define AGI_WORDS_MAX 16384u
|
||||||
|
#define AGI_WORD_MAX_LEN 40u
|
||||||
|
|
||||||
|
// Command-line parser limits + the reserved flag/var the interpreter uses
|
||||||
|
// to talk to logic scripts about input:
|
||||||
|
// flag 2 -- set when the player has entered a command this cycle; said()
|
||||||
|
// matches only while it is set and clears it on a match so only
|
||||||
|
// the first matching said() in the logic scan fires.
|
||||||
|
// var 9 -- 1-based position of a typed word that isn't in the dictionary.
|
||||||
|
#define AGI_INPUT_MAX 40u // chars in the on-screen command line
|
||||||
|
#define AGI_MAX_PARSED 12u // word IDs kept from one command
|
||||||
|
#define AGI_INPUT_ROW 24u // bottom text row shows the command line
|
||||||
|
#define AGI_FLAG_INPUT 2u
|
||||||
|
#define AGI_VAR_UNKNOWN_WORD 9u
|
||||||
|
|
||||||
|
|
||||||
// Decoded PIC working buffers. Both planes are 160x168 chunky 8bpp;
|
// Decoded PIC working buffers. Both planes are 160x168 chunky 8bpp;
|
||||||
// only the low nibble (4 bits) of each byte carries actual color
|
// only the low nibble (4 bits) of each byte carries actual color
|
||||||
|
|
@ -388,6 +418,15 @@ typedef struct {
|
||||||
// ----- strings -----
|
// ----- strings -----
|
||||||
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
|
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
|
||||||
|
|
||||||
|
// ----- parser / command input -----
|
||||||
|
// inputLine is the raw text the player is typing; on Enter agi.c
|
||||||
|
// tokenizes it against the dictionary into parsedWords[] (group IDs,
|
||||||
|
// ignore-words dropped) and sets flag 2 for said() to test.
|
||||||
|
char inputLine[AGI_INPUT_MAX + 1u];
|
||||||
|
uint8_t inputLen;
|
||||||
|
uint16_t parsedWords[AGI_MAX_PARSED];
|
||||||
|
uint8_t parsedWordCount;
|
||||||
|
|
||||||
// ----- sound playback state -----
|
// ----- sound playback state -----
|
||||||
// The VM polls callbacks.isPlayingSound() each tick; when it
|
// The VM polls callbacks.isPlayingSound() each tick; when it
|
||||||
// transitions from true to false, the sound-done flags fire.
|
// transitions from true to false, the sound-done flags fire.
|
||||||
|
|
@ -410,12 +449,17 @@ void agiResClose(AgiGameT *game);
|
||||||
// resource payload length in bytes.
|
// resource payload length in bytes.
|
||||||
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
|
||||||
|
|
||||||
// Open an AGI v2 game directory. gameDir is a relative or absolute
|
// Open the AGI v2 game whose LOGDIR/PICDIR/VIEWDIR/SNDDIR and VOL.0..VOL.N
|
||||||
// path to a directory containing LOGDIR/PICDIR/VIEWDIR/SNDDIR plus
|
// files are staged under DATA/ (opened by bare name via jlDataOpen).
|
||||||
// VOL.0..VOL.N. Returns true if every directory file and at least
|
// Returns true if every directory file and at least one VOL.* file opened.
|
||||||
// one VOL.* file opened. Partial-success on missing optional
|
// Partial-success on missing optional volumes; agiResLoad reports the
|
||||||
// volumes; agiResLoad reports the missing-volume case per resource.
|
// missing-volume case per resource.
|
||||||
bool agiResOpen(AgiGameT *game, const char *gameDir);
|
bool agiResOpen(AgiGameT *game);
|
||||||
|
|
||||||
|
// Look up a word in the loaded WORDS.TOK dictionary (case-insensitive) and
|
||||||
|
// return its AGI group ID, or AGI_WORD_UNKNOWN if the word is absent (or no
|
||||||
|
// dictionary loaded). Synonyms share a group ID; group 0 = ignore-words.
|
||||||
|
uint16_t agiWordId(const AgiGameT *game, const char *word);
|
||||||
|
|
||||||
|
|
||||||
// ----- Picture decoder (agiPic.c) -----
|
// ----- Picture decoder (agiPic.c) -----
|
||||||
|
|
|
||||||
241
agiRes.c
241
agiRes.c
|
|
@ -22,6 +22,8 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "joey/file.h"
|
||||||
|
|
||||||
|
|
||||||
// AGI directory files are flat arrays of 3-byte entries. The maximum
|
// AGI directory files are flat arrays of 3-byte entries. The maximum
|
||||||
// directory length the loader will accept is bounded by AGI_MAX_-
|
// directory length the loader will accept is bounded by AGI_MAX_-
|
||||||
|
|
@ -35,9 +37,9 @@
|
||||||
#define AGI_VOL_COMPRESSED 0x80
|
#define AGI_VOL_COMPRESSED 0x80
|
||||||
#define AGI_VOL_VOLUME_MASK 0x7F
|
#define AGI_VOL_VOLUME_MASK 0x7F
|
||||||
|
|
||||||
// Max directory + path string size. AGI filenames are 7 chars max;
|
// Resource file names are opened relative to DATA/ via jlDataOpen (see
|
||||||
// gameDir is application-controlled. 256 leaves comfortable margin.
|
// joey/file.h); the longest name AGI forms is "VOL.<n>". 16 is ample.
|
||||||
#define AGI_PATH_MAX 256
|
#define AGI_VOL_NAME_MAX 16
|
||||||
|
|
||||||
// Empty-slot sentinel: must be >= AGI_MAX_VOLUMES so agiResLoad
|
// Empty-slot sentinel: must be >= AGI_MAX_VOLUMES so agiResLoad
|
||||||
// rejects empty entries. The on-disk empty marker is 0xFFFFFF (high
|
// rejects empty entries. The on-disk empty marker is 0xFFFFFF (high
|
||||||
|
|
@ -55,44 +57,24 @@
|
||||||
|
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
||||||
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file);
|
static bool buildVolName(char *out, size_t outSize, uint8_t volNum);
|
||||||
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum);
|
static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
||||||
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName);
|
static bool loadWords(AgiGameT *game);
|
||||||
static bool openVolumes(AgiGameT *game, const char *gameDir);
|
static bool openVolumes(AgiGameT *game);
|
||||||
static const char *resTypeFileName(AgiResTypeE type);
|
static const char *resTypeFileName(AgiResTypeE type);
|
||||||
|
static bool wordEqCI(const char *a, const char *b);
|
||||||
|
|
||||||
|
|
||||||
// ----- Internal helpers (alphabetical) -----
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file) {
|
static bool buildVolName(char *out, size_t outSize, uint8_t volNum) {
|
||||||
size_t dirLen;
|
|
||||||
size_t fileLen;
|
|
||||||
|
|
||||||
dirLen = strlen(dir);
|
|
||||||
fileLen = strlen(file);
|
|
||||||
if (dirLen + 1 + fileLen + 1 > outSize) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
memcpy(out, dir, dirLen);
|
|
||||||
out[dirLen] = '/';
|
|
||||||
memcpy(out + dirLen + 1, file, fileLen);
|
|
||||||
out[dirLen + 1 + fileLen] = '\0';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum) {
|
|
||||||
size_t dirLen;
|
|
||||||
char *p;
|
char *p;
|
||||||
|
|
||||||
dirLen = strlen(dir);
|
// "VOL." + up to 2 digits + NUL = 7 chars max.
|
||||||
// "/VOL." + up to 2 digits + NUL = 8 chars max.
|
if (outSize < 8u) {
|
||||||
if (dirLen + 8 > outSize) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memcpy(out, dir, dirLen);
|
p = out;
|
||||||
p = out + dirLen;
|
|
||||||
*p++ = '/';
|
|
||||||
*p++ = 'V';
|
*p++ = 'V';
|
||||||
*p++ = 'O';
|
*p++ = 'O';
|
||||||
*p++ = 'L';
|
*p++ = 'L';
|
||||||
|
|
@ -108,56 +90,44 @@ static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t vol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName) {
|
static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type) {
|
||||||
char path[AGI_PATH_MAX];
|
// One directory is resident at a time; a file-scope buffer keeps this
|
||||||
|
// ~768-byte read off the shallow IIgs soft stack.
|
||||||
|
static uint8_t dirBytes[AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES];
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
long fileSize;
|
|
||||||
uint16_t entryCount;
|
uint16_t entryCount;
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
uint8_t buf[AGI_DIR_ENTRY_BYTES];
|
|
||||||
uint8_t volNibble;
|
uint8_t volNibble;
|
||||||
uint32_t offset;
|
uint32_t offset;
|
||||||
|
size_t got;
|
||||||
|
|
||||||
if (!buildPath(path, AGI_PATH_MAX, gameDir, fileName)) {
|
// fileName is a bare name relative to DATA/ (e.g. "LOGDIR").
|
||||||
return false;
|
fp = jlDataOpen(fileName, "rb");
|
||||||
}
|
|
||||||
|
|
||||||
fp = fopen(path, "rb");
|
|
||||||
if (fp == NULL) {
|
if (fp == NULL) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fseek(fp, 0L, SEEK_END) != 0) {
|
// Read the whole flat 3-byte-entry array in one call rather than one
|
||||||
fclose(fp);
|
// read per entry: on the IIgs each fread is a GS/OS round trip through
|
||||||
return false;
|
// SmartPort firmware, and a directory never exceeds AGI_MAX_RESOURCES
|
||||||
}
|
// entries, so the fixed buffer holds any valid file. A non-multiple-of-3
|
||||||
fileSize = ftell(fp);
|
// length means the file is malformed.
|
||||||
if (fileSize < 0 || fileSize > (long)(AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES)) {
|
got = fread(dirBytes, 1, sizeof(dirBytes), fp);
|
||||||
fclose(fp);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ((fileSize % AGI_DIR_ENTRY_BYTES) != 0) {
|
|
||||||
fclose(fp);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (fseek(fp, 0L, SEEK_SET) != 0) {
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
if (got == 0u || (got % AGI_DIR_ENTRY_BYTES) != 0u) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
entryCount = (uint16_t)(fileSize / AGI_DIR_ENTRY_BYTES);
|
entryCount = (uint16_t)(got / AGI_DIR_ENTRY_BYTES);
|
||||||
for (i = 0; i < entryCount; i++) {
|
for (i = 0; i < entryCount; i++) {
|
||||||
if (fread(buf, 1, AGI_DIR_ENTRY_BYTES, fp) != AGI_DIR_ENTRY_BYTES) {
|
const uint8_t *e = &dirBytes[(uint32_t)i * AGI_DIR_ENTRY_BYTES];
|
||||||
fclose(fp);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
volNibble = (uint8_t)(buf[0] >> 4);
|
volNibble = (uint8_t)(e[0] >> 4);
|
||||||
// 20-bit offset, cast to uint32_t before shift so a
|
// 20-bit offset, cast to uint32_t before shift so a
|
||||||
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
|
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
|
||||||
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
|
offset = ((uint32_t)(e[0] & 0x0F) << 16) | ((uint32_t)e[1] << 8) | (uint32_t)e[2];
|
||||||
|
|
||||||
if (buf[0] == 0xFF && buf[1] == 0xFF && buf[2] == 0xFF) {
|
if (e[0] == 0xFF && e[1] == 0xFF && e[2] == 0xFF) {
|
||||||
game->resDir[type][i].volume = AGI_DIR_EMPTY_VOLUME;
|
game->resDir[type][i].volume = AGI_DIR_EMPTY_VOLUME;
|
||||||
game->resDir[type][i].offset = 0u;
|
game->resDir[type][i].offset = 0u;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -165,24 +135,55 @@ static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir,
|
||||||
game->resDir[type][i].offset = offset;
|
game->resDir[type][i].offset = offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
game->resCount[type] = entryCount;
|
game->resCount[type] = entryCount;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool openVolumes(AgiGameT *game, const char *gameDir) {
|
static bool loadWords(AgiGameT *game) {
|
||||||
char path[AGI_PATH_MAX];
|
FILE *fp;
|
||||||
|
uint8_t *buf;
|
||||||
|
size_t got;
|
||||||
|
|
||||||
|
game->words = NULL;
|
||||||
|
game->wordsLen = 0u;
|
||||||
|
|
||||||
|
// The dictionary is optional: a game with no WORDS.TOK simply can't
|
||||||
|
// match said()/parse (no crash), so a missing file is not an error.
|
||||||
|
fp = jlDataOpen("WORDS.TOK", "rb");
|
||||||
|
if (fp == NULL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
buf = (uint8_t *)malloc(AGI_WORDS_MAX);
|
||||||
|
if (buf == NULL) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
got = fread(buf, 1, AGI_WORDS_MAX, fp);
|
||||||
|
fclose(fp);
|
||||||
|
// Anything shorter than the letter-index header can't hold a word.
|
||||||
|
if (got <= AGI_WORDS_HEADER_BYTES) {
|
||||||
|
free(buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
game->words = buf;
|
||||||
|
game->wordsLen = (uint32_t)got;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool openVolumes(AgiGameT *game) {
|
||||||
|
char name[AGI_VOL_NAME_MAX];
|
||||||
uint8_t v;
|
uint8_t v;
|
||||||
bool anyOpened;
|
bool anyOpened;
|
||||||
|
|
||||||
anyOpened = false;
|
anyOpened = false;
|
||||||
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||||
if (!buildVolPath(path, AGI_PATH_MAX, gameDir, v)) {
|
if (!buildVolName(name, sizeof(name), v)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
game->volFiles[v] = fopen(path, "rb");
|
game->volFiles[v] = jlDataOpen(name, "rb");
|
||||||
if (game->volFiles[v] != NULL) {
|
if (game->volFiles[v] != NULL) {
|
||||||
anyOpened = true;
|
anyOpened = true;
|
||||||
}
|
}
|
||||||
|
|
@ -202,6 +203,31 @@ static const char *resTypeFileName(AgiResTypeE type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Case-insensitive ASCII equality. Both dictionary words and (lowercased)
|
||||||
|
// typed words are ASCII; fold A-Z so a stray uppercase still matches.
|
||||||
|
static bool wordEqCI(const char *a, const char *b) {
|
||||||
|
while (*a != '\0' && *b != '\0') {
|
||||||
|
char ca;
|
||||||
|
char cb;
|
||||||
|
|
||||||
|
ca = *a;
|
||||||
|
cb = *b;
|
||||||
|
if (ca >= 'A' && ca <= 'Z') {
|
||||||
|
ca = (char)(ca + ('a' - 'A'));
|
||||||
|
}
|
||||||
|
if (cb >= 'A' && cb <= 'Z') {
|
||||||
|
cb = (char)(cb + ('a' - 'A'));
|
||||||
|
}
|
||||||
|
if (ca != cb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
a++;
|
||||||
|
b++;
|
||||||
|
}
|
||||||
|
return *a == '\0' && *b == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----- Public API (alphabetical) -----
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
void agiResClose(AgiGameT *game) {
|
void agiResClose(AgiGameT *game) {
|
||||||
|
|
@ -217,6 +243,11 @@ void agiResClose(AgiGameT *game) {
|
||||||
for (t = 0; t < AGI_RES_COUNT; t++) {
|
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||||
game->resCount[t] = 0u;
|
game->resCount[t] = 0u;
|
||||||
}
|
}
|
||||||
|
if (game->words != NULL) {
|
||||||
|
free(game->words);
|
||||||
|
game->words = NULL;
|
||||||
|
game->wordsLen = 0u;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -287,22 +318,90 @@ uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool agiResOpen(AgiGameT *game, const char *gameDir) {
|
bool agiResOpen(AgiGameT *game) {
|
||||||
uint8_t t;
|
uint8_t t;
|
||||||
|
|
||||||
memset(game, 0, sizeof(*game));
|
memset(game, 0, sizeof(*game));
|
||||||
|
|
||||||
|
// Resource files are opened by bare name relative to DATA/ (jlDataOpen);
|
||||||
|
// the game's data is whatever the packager staged there, so there is no
|
||||||
|
// runtime game-directory to thread through.
|
||||||
for (t = 0; t < AGI_RES_COUNT; t++) {
|
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||||
if (!loadDirectory(game, (AgiResTypeE)t, gameDir, resTypeFileName((AgiResTypeE)t))) {
|
if (!loadDirectory(game, resTypeFileName((AgiResTypeE)t), (AgiResTypeE)t)) {
|
||||||
agiResClose(game);
|
agiResClose(game);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!openVolumes(game, gameDir)) {
|
if (!openVolumes(game)) {
|
||||||
|
agiResClose(game);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dictionary is optional (loadWords only fails on OOM); a game
|
||||||
|
// without WORDS.TOK still runs, just with no parser matches.
|
||||||
|
if (!loadWords(game)) {
|
||||||
agiResClose(game);
|
agiResClose(game);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
uint16_t agiWordId(const AgiGameT *game, const char *word) {
|
||||||
|
const uint8_t *p;
|
||||||
|
const uint8_t *end;
|
||||||
|
char cur[AGI_WORD_MAX_LEN + 1u];
|
||||||
|
uint8_t len;
|
||||||
|
|
||||||
|
if (game->words == NULL || word == NULL) {
|
||||||
|
return AGI_WORD_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word entries follow the fixed 52-byte letter-index header. Each entry:
|
||||||
|
// 1 byte prefixLen -- chars shared with the previous word
|
||||||
|
// n bytes suffix -- each (c & 0x7F) ^ 0x7F; the char with bit 7
|
||||||
|
// set is the last one of the word
|
||||||
|
// 2 bytes groupId -- big-endian
|
||||||
|
// `cur` persists across entries so prefixLen chars carry forward.
|
||||||
|
p = game->words + AGI_WORDS_HEADER_BYTES;
|
||||||
|
end = game->words + game->wordsLen;
|
||||||
|
len = 0u;
|
||||||
|
while (p < end) {
|
||||||
|
uint8_t prefixLen;
|
||||||
|
uint16_t id;
|
||||||
|
|
||||||
|
prefixLen = *p++;
|
||||||
|
if (prefixLen > AGI_WORD_MAX_LEN) {
|
||||||
|
break; // malformed dictionary
|
||||||
|
}
|
||||||
|
len = prefixLen;
|
||||||
|
for (;;) {
|
||||||
|
uint8_t c;
|
||||||
|
|
||||||
|
if (p >= end) {
|
||||||
|
return AGI_WORD_UNKNOWN;
|
||||||
|
}
|
||||||
|
c = *p++;
|
||||||
|
if (len < AGI_WORD_MAX_LEN) {
|
||||||
|
cur[len] = (char)((c & 0x7Fu) ^ 0x7Fu);
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
if ((c & 0x80u) != 0u) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur[len] = '\0';
|
||||||
|
|
||||||
|
if (p + 1 >= end) {
|
||||||
|
return AGI_WORD_UNKNOWN;
|
||||||
|
}
|
||||||
|
id = (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
|
||||||
|
p += 2;
|
||||||
|
if (wordEqCI(cur, word)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AGI_WORD_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
|
||||||
75
agiVm.c
75
agiVm.c
|
|
@ -301,7 +301,7 @@ static bool popFrame(AgiVmT *vm);
|
||||||
static bool pushFrame(AgiVmT *vm);
|
static bool pushFrame(AgiVmT *vm);
|
||||||
static bool readByte(AgiVmT *vm, uint8_t *out);
|
static bool readByte(AgiVmT *vm, uint8_t *out);
|
||||||
static bool readU16(AgiVmT *vm, uint16_t *out);
|
static bool readU16(AgiVmT *vm, uint16_t *out);
|
||||||
static bool skipTestArgs(AgiVmT *vm, uint8_t testOp);
|
static bool saidMatch(const AgiVmT *vm, const uint16_t *pat, uint8_t patCount);
|
||||||
static VmDispatchE vmStepCore(AgiVmT *vm, uint8_t op);
|
static VmDispatchE vmStepCore(AgiVmT *vm, uint8_t op);
|
||||||
static VmDispatchE vmStepMisc(AgiVmT *vm, uint8_t op);
|
static VmDispatchE vmStepMisc(AgiVmT *vm, uint8_t op);
|
||||||
static VmDispatchE vmStepObject(AgiVmT *vm, uint8_t op);
|
static VmDispatchE vmStepObject(AgiVmT *vm, uint8_t op);
|
||||||
|
|
@ -416,12 +416,38 @@ static bool evalOneTest(AgiVmT *vm, uint8_t testOp) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (testOp == TEST_SAID) {
|
if (testOp == TEST_SAID) {
|
||||||
// 1-byte word count, followed by 2 bytes per word.
|
// Variable-length: 1-byte word count, then that many 16-bit
|
||||||
if (!skipTestArgs(vm, testOp)) {
|
// little-endian word IDs. Read the pattern (always, to keep pc in
|
||||||
|
// sync) then match it against what the player typed this cycle.
|
||||||
|
uint16_t pat[AGI_MAX_PARSED];
|
||||||
|
uint8_t count;
|
||||||
|
uint8_t lo;
|
||||||
|
uint8_t hi;
|
||||||
|
uint8_t k;
|
||||||
|
bool matched;
|
||||||
|
|
||||||
|
if (!readByte(vm, &count)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
for (k = 0u; k < count; k++) {
|
||||||
|
if (!readByte(vm, &lo) || !readByte(vm, &hi)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (k < AGI_MAX_PARSED) {
|
||||||
|
pat[k] = (uint16_t)((uint16_t)lo | ((uint16_t)hi << 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// said() fires only on the cycle input was entered; a match consumes
|
||||||
|
// that input (clears flag 2) so no later said() double-handles it.
|
||||||
|
if (vm->flags[AGI_FLAG_INPUT] == 0u || count > AGI_MAX_PARSED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matched = saidMatch(vm, pat, count);
|
||||||
|
if (matched) {
|
||||||
|
vm->flags[AGI_FLAG_INPUT] = 0u;
|
||||||
|
}
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
argBytes = kTestArgBytes[testOp];
|
argBytes = kTestArgBytes[testOp];
|
||||||
for (i = 0u; i < argBytes; i++) {
|
for (i = 0u; i < argBytes; i++) {
|
||||||
|
|
@ -877,37 +903,34 @@ static bool readU16(AgiVmT *vm, uint16_t *out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Walk past the argument bytes of a test command without evaluating.
|
// Match the parsed player command against a said() word pattern.
|
||||||
// For the variable-length `said` test, the first byte is the word
|
// AGI_WORD_REST (9999) matches the rest of the line (including nothing);
|
||||||
// count and the rest are 2 bytes per word.
|
// AGI_WORD_ANYWORD (1) matches exactly one arbitrary word;
|
||||||
static bool skipTestArgs(AgiVmT *vm, uint8_t testOp) {
|
// any other id matches that exact group id.
|
||||||
uint8_t scratch;
|
// An exact match requires every typed word to be consumed by the pattern.
|
||||||
uint8_t wordCount;
|
static bool saidMatch(const AgiVmT *vm, const uint16_t *pat, uint8_t patCount) {
|
||||||
uint16_t toSkip;
|
uint8_t pi;
|
||||||
uint16_t i;
|
uint8_t wi;
|
||||||
|
|
||||||
if (testOp == TEST_SAID) {
|
pi = 0u;
|
||||||
if (!readByte(vm, &wordCount)) {
|
wi = 0u;
|
||||||
return false;
|
while (pi < patCount) {
|
||||||
}
|
uint16_t pw;
|
||||||
toSkip = (uint16_t)((uint16_t)wordCount * 2u);
|
|
||||||
for (i = 0u; i < toSkip; i++) {
|
pw = pat[pi];
|
||||||
if (!readByte(vm, &scratch)) {
|
if (pw == AGI_WORD_REST) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (testOp == 0u || testOp >= TEST_MAX) {
|
if (wi >= vm->parsedWordCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
toSkip = (uint16_t)kTestArgBytes[testOp];
|
if (pw != AGI_WORD_ANYWORD && pw != vm->parsedWords[wi]) {
|
||||||
for (i = 0u; i < toSkip; i++) {
|
|
||||||
if (!readByte(vm, &scratch)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
wi++;
|
||||||
|
pi++;
|
||||||
}
|
}
|
||||||
return true;
|
return wi == vm->parsedWordCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
74
scripts/make-agi-iigs-disk.sh
Executable file
74
scripts/make-agi-iigs-disk.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# make-agi-iigs-disk.sh - Build the 2MB ProDOS data volume that holds the
|
||||||
|
# IIgs AGI interpreter plus one AGI v2 game's resources. run-iigs.sh mounts
|
||||||
|
# it as the CFFA2 card's second partition (-hard2); GS/OS boots from the
|
||||||
|
# first partition and auto-mounts this one.
|
||||||
|
#
|
||||||
|
# A single 800KB 3.5" floppy can't hold the ~240KB AGI binary plus KQ3's
|
||||||
|
# ~644KB of required resources (878KB total), and MAME's IIgs 3.5 drive is
|
||||||
|
# 800K-GCR-only (it rejects a 1.44MB image). A CFFA2 hard-disk partition
|
||||||
|
# has no such cap, so the whole payload lands on one 2MB ProDOS volume.
|
||||||
|
#
|
||||||
|
# Usage: scripts/make-agi-iigs-disk.sh <output.2mg> [game-name]
|
||||||
|
# game-name : subdirectory under examples/agi/gamedata/ (default kq3)
|
||||||
|
# Requires: toolchains/env.sh sourced (LLVM816_ROOT), built build/iigs/bin/AGI.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
: "${LLVM816_ROOT:?make-agi-iigs-disk.sh: source toolchains/env.sh first}"
|
||||||
|
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
|
||||||
|
BINDIR="${BINDIR:-$repo/build/iigs/bin}"
|
||||||
|
|
||||||
|
out=${1:?usage: make-agi-iigs-disk.sh <output.2mg> [game]}
|
||||||
|
game=${2:-kq3}
|
||||||
|
gamedata=$repo/examples/agi/gamedata/$game
|
||||||
|
agiBin=$BINDIR/AGI
|
||||||
|
VOL=AGI
|
||||||
|
|
||||||
|
# The AGI volume name and the game files use uppercase ProDOS names. AGI
|
||||||
|
# fopen()s LOGDIR / VOL.x etc. from its launch directory; GS/OS sets the
|
||||||
|
# prefix to the volume it launched from, and ProDOS lookups are
|
||||||
|
# case-insensitive, so the uppercase names match the lowercase fopen paths.
|
||||||
|
[ -x "$CADIUS" ] || { echo "make-agi-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
|
||||||
|
[ -f "$agiBin" ] || { echo "make-agi-iigs-disk.sh: $agiBin not built (make -f make/iigs.mk $agiBin)" >&2; exit 1; }
|
||||||
|
[ -d "$gamedata" ] || { echo "make-agi-iigs-disk.sh: $gamedata is not an AGI v2 game directory" >&2; exit 1; }
|
||||||
|
[ -f "$gamedata/LOGDIR" ] || { echo "make-agi-iigs-disk.sh: $gamedata has no LOGDIR -- not AGI v2" >&2; exit 1; }
|
||||||
|
|
||||||
|
work=$(mktemp -d -t agi-iigs-disk.XXXXXX)
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
|
rm -f "$out"
|
||||||
|
"$CADIUS" CREATEVOLUME "$out" "$VOL" 2048KB >/dev/null
|
||||||
|
|
||||||
|
# AGI interpreter as a ProDOS type $B3 (S16 / GS-OS application). It stays
|
||||||
|
# at the volume root so Finder lists and launches it.
|
||||||
|
cp "$agiBin" "$work/AGI#B30000"
|
||||||
|
"$CADIUS" ADDFILE "$out" "/$VOL" "$work/AGI#B30000" >/dev/null
|
||||||
|
|
||||||
|
# Game resources live under DATA/ -- JoeyLib forces every data-file open
|
||||||
|
# through that prefix (jlDataOpen), so they must sit in /VOL/DATA. GS/OS
|
||||||
|
# sets prefix 0 to the launch volume, so AGI's "DATA/LOGDIR" resolves here.
|
||||||
|
"$CADIUS" CREATEFOLDER "$out" "/$VOL/DATA" >/dev/null
|
||||||
|
|
||||||
|
# Game resources as plain ProDOS BIN ($06). Only the subset AGI v2 reads
|
||||||
|
# (the four DIR indexes, the VOL.x archives, OBJECT, WORDS.TOK) is copied;
|
||||||
|
# Sierra's original binary / OVL drivers are left behind.
|
||||||
|
added=0
|
||||||
|
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do
|
||||||
|
if [ -f "$gamedata/$f" ]; then
|
||||||
|
cp "$gamedata/$f" "$work/${f}#060000"
|
||||||
|
"$CADIUS" ADDFILE "$out" "/$VOL/DATA" "$work/${f}#060000" >/dev/null
|
||||||
|
added=$((added + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for f in "$gamedata"/VOL.*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
b=$(basename "$f")
|
||||||
|
cp "$f" "$work/${b}#060000"
|
||||||
|
"$CADIUS" ADDFILE "$out" "/$VOL/DATA" "$work/${b}#060000" >/dev/null
|
||||||
|
added=$((added + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
free=$("$CADIUS" CATALOG "$out" 2>/dev/null | grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1)
|
||||||
|
echo "agi-iigs-disk: $out (volume /$VOL, game $game)"
|
||||||
|
echo " AGI + $added resource files, $free blocks free"
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
# Required files in the game directory (others are ignored):
|
# Required files in the game directory (others are ignored):
|
||||||
# LOGDIR PICDIR VIEWDIR SNDDIR VOL.0 [VOL.1 ...] [OBJECT] [WORDS.TOK]
|
# LOGDIR PICDIR VIEWDIR SNDDIR VOL.0 [VOL.1 ...] [OBJECT] [WORDS.TOK]
|
||||||
#
|
#
|
||||||
# IIgs is not yet wired (needs the disk packager extended to a larger
|
# IIgs delegates to run-iigs.sh's AGI mode, which packs AGI + the game
|
||||||
# 2mg volume plus case-folded ProDOS file names); the script reports
|
# onto a 2MB CFFA2 hard-disk volume (an 800KB floppy can't hold both) and
|
||||||
# what's missing rather than launching.
|
# boots GS/OS from a second CFFA2 partition. See make-agi-iigs-disk.sh.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -64,15 +64,19 @@ fi
|
||||||
# filesystems (e.g. Amiga FFS: "AGI" vs our "Agi").
|
# filesystems (e.g. Amiga FFS: "AGI" vs our "Agi").
|
||||||
stageGameData() {
|
stageGameData() {
|
||||||
local destDir=$1
|
local destDir=$1
|
||||||
|
local dataDir=$destDir/DATA
|
||||||
local f
|
local f
|
||||||
|
|
||||||
|
# JoeyLib forces every data-file open under DATA/ (jlDataOpen), so the
|
||||||
|
# AGI resources are staged into a DATA subdirectory of the mount root.
|
||||||
|
mkdir -p "$dataDir"
|
||||||
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do
|
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do
|
||||||
if [[ -f $gamedata/$f ]]; then
|
if [[ -f $gamedata/$f ]]; then
|
||||||
cp "$gamedata/$f" "$destDir/$f"
|
cp "$gamedata/$f" "$dataDir/$f"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
for f in "$gamedata"/VOL.*; do
|
for f in "$gamedata"/VOL.*; do
|
||||||
[[ -f $f ]] && cp "$f" "$destDir/$(basename "$f")"
|
[[ -f $f ]] && cp "$f" "$dataDir/$(basename "$f")"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +87,7 @@ runDos() {
|
||||||
local work
|
local work
|
||||||
|
|
||||||
if [[ ! -f $bin ]]; then
|
if [[ ! -f $bin ]]; then
|
||||||
echo "$bin not built. Run 'make -f make/dos.mk EXAMPLE=agi' first." >&2
|
echo "$bin not built. Run 'make -f make/dos.mk $bin' first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -116,13 +120,13 @@ runDos() {
|
||||||
cp "$bin" "$work/AGI.EXE"
|
cp "$bin" "$work/AGI.EXE"
|
||||||
stageGameData "$work"
|
stageGameData "$work"
|
||||||
|
|
||||||
# AGI.EXE reads game data from its CWD (we mount $work as C: and
|
# AGI.EXE reads game data from DATA/ under its CWD (we mount $work as C:
|
||||||
# cd there first), so argv[1] is "." -- the starting-room override
|
# and cd there first), so no game-directory argument is needed; the
|
||||||
# lives in argv[2]. Skip the argv when room=0 so the binary
|
# starting-room override is argv[1]. Skip it when room=0 so the binary
|
||||||
# exercises its default code path.
|
# exercises its default code path.
|
||||||
local agi_cmd="AGI.EXE"
|
local agi_cmd="AGI.EXE"
|
||||||
if (( room != 0 )); then
|
if (( room != 0 )); then
|
||||||
agi_cmd="AGI.EXE . $room"
|
agi_cmd="AGI.EXE $room"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
@ -160,7 +164,7 @@ runAtariST() {
|
||||||
local work
|
local work
|
||||||
|
|
||||||
if [[ ! -f $bin ]]; then
|
if [[ ! -f $bin ]]; then
|
||||||
echo "$bin not built. Run 'make -f make/atarist.mk EXAMPLE=agi' first." >&2
|
echo "$bin not built. Run 'make -f make/atarist.mk $bin' first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ ! -f $tos ]]; then
|
if [[ ! -f $tos ]]; then
|
||||||
|
|
@ -198,7 +202,7 @@ runAmiga() {
|
||||||
local work
|
local work
|
||||||
|
|
||||||
if [[ ! -f $bin ]]; then
|
if [[ ! -f $bin ]]; then
|
||||||
echo "$bin not built. Run 'make -f make/amiga.mk EXAMPLE=agi' first." >&2
|
echo "$bin not built. Run 'make -f make/amiga.mk $bin' first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -243,25 +247,21 @@ EOF
|
||||||
|
|
||||||
|
|
||||||
runIigs() {
|
runIigs() {
|
||||||
cat <<EOF >&2
|
# IIgs uses a CFFA2 hard disk (the 800KB floppy can't hold AGI + KQ3),
|
||||||
IIgs AGI run is not wired up yet.
|
# driven entirely by run-iigs.sh's AGI mode. Delegate to it: it builds
|
||||||
|
# the two-partition CFFA image via make-agi-iigs-disk.sh, boots GS/OS,
|
||||||
The existing IIgs disk packager (toolchains/iigs/package-disk.sh) builds
|
# and launches AGI. The starting-room override is DOS-only (the IIgs
|
||||||
joey.2mg from build/iigs/bin/* only; it doesn't accept arbitrary game
|
# autostart path can't pass argv), so $room is not forwarded here.
|
||||||
data, the disk size is fixed at 800K (KQ3's ~1.5 MB of resources won't
|
if (( room != 0 )); then
|
||||||
fit), and ProDOS case-folding for game-data file names has not been
|
echo "note: starting-room override is DOS-only; ignored on IIgs" >&2
|
||||||
audited.
|
fi
|
||||||
|
# The IIgs build target is the binary path, not EXAMPLE=agi.
|
||||||
To wire this up the package-disk.sh tool needs a larger volume option
|
if [[ ! -f "$repo/build/iigs/bin/AGI" ]]; then
|
||||||
and an "extra data" path argument, and run-iigs.sh needs an AGI mode
|
echo "$repo/build/iigs/bin/AGI not built." >&2
|
||||||
that points GSplus at the resulting 2mg. Tracked separately.
|
echo "Build it with: make -f make/iigs.mk $repo/build/iigs/bin/AGI" >&2
|
||||||
|
|
||||||
Workaround for now: build with 'make -f make/iigs.mk EXAMPLE=agi' to
|
|
||||||
prove the IIgs link still works, but launch via run-iigs.sh and pick
|
|
||||||
JOEYLIB:AGI in Finder -- it will fail to find LOGDIR (no game data on
|
|
||||||
disk), which is the expected current state.
|
|
||||||
EOF
|
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
JOEY_AGI_GAME="$game" exec "$repo/scripts/run-iigs.sh" agi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,41 @@ stubSrc=$build/hostStubs.c
|
||||||
cat > "$stubSrc" <<'EOF'
|
cat > "$stubSrc" <<'EOF'
|
||||||
#include "joey/draw.h"
|
#include "joey/draw.h"
|
||||||
#include "joey/core.h"
|
#include "joey/core.h"
|
||||||
|
#include "joey/file.h"
|
||||||
#include "surfaceInternal.h"
|
#include "surfaceInternal.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
// Host jlDataOpen: the real one (assetLoad.c) forces a DATA/ prefix, but
|
||||||
|
// the test gamedata dirs hold their files flat. Open <AGI_DATA_DIR>/<name>
|
||||||
|
// so agiRes.c's bare-name jlDataOpen calls resolve against the game dir the
|
||||||
|
// harness points us at.
|
||||||
|
FILE *jlDataOpen(const char *name, const char *mode) {
|
||||||
|
const char *base = getenv("AGI_DATA_DIR");
|
||||||
|
char path[1024];
|
||||||
|
if (base == NULL) { base = "."; }
|
||||||
|
if ((size_t)snprintf(path, sizeof(path), "%s/%s", base, name) >= sizeof(path)) { return NULL; }
|
||||||
|
return fopen(path, mode);
|
||||||
|
}
|
||||||
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t c) { (void)s; (void)x; (void)y; (void)c; }
|
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t c) { (void)s; (void)x; (void)y; (void)c; }
|
||||||
void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t c) { (void)s; (void)x; (void)y; (void)w; (void)h; (void)c; }
|
void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t c) { (void)s; (void)x; (void)y; (void)w; (void)h; (void)c; }
|
||||||
uint32_t jlRandom(void) { return (uint32_t)rand(); }
|
uint32_t jlRandom(void) { return (uint32_t)rand(); }
|
||||||
uint16_t jlRandomRange(uint16_t bound) { return (bound == 0u) ? 0u : (uint16_t)((unsigned)rand() % bound); }
|
uint16_t jlRandomRange(uint16_t bound) { return (bound == 0u) ? 0u : (uint16_t)((unsigned)rand() % bound); }
|
||||||
void jlRandomSeed(uint32_t seed) { srand((unsigned)seed); }
|
void jlRandomSeed(uint32_t seed) { srand((unsigned)seed); }
|
||||||
void surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h) { (void)s; (void)x; (void)y; (void)w; (void)h; }
|
void *jlAlloc(uint32_t bytes) { return malloc((size_t)bytes); }
|
||||||
|
void jlFree(void *p) { free(p); }
|
||||||
|
void jlLogF(const char *fmt, ...) { (void)fmt; }
|
||||||
|
void jlLogFlush(void) { }
|
||||||
|
void jlLog(const char *msg) { (void)msg; }
|
||||||
|
/* surfaceMarkDirtyRect is a MACRO since the Phase-6 mark rework -- the
|
||||||
|
* host TUs (agiPic/agiView) expand it inline, so the stubs provide the
|
||||||
|
* band state it references instead of a function. gStage stays NULL,
|
||||||
|
* so every expansion takes the s != gStage no-op branch; the symbols
|
||||||
|
* only need to exist to link. */
|
||||||
|
jlSurfaceT *gStage = 0;
|
||||||
|
uint8_t gStageMinWord[SURFACE_HEIGHT];
|
||||||
|
uint8_t gStageMaxWord[SURFACE_HEIGHT];
|
||||||
|
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { (void)y; (void)yEnd; (void)minWord; (void)maxWord; }
|
||||||
void surfaceMarkDirtyAll(const jlSurfaceT *s) { (void)s; }
|
void surfaceMarkDirtyAll(const jlSurfaceT *s) { (void)s; }
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|
@ -60,6 +87,7 @@ echo "Building host tests..."
|
||||||
gcc "${hostCflags[@]}" \
|
gcc "${hostCflags[@]}" \
|
||||||
"$repo/tests/agi/testAgiRes.c" \
|
"$repo/tests/agi/testAgiRes.c" \
|
||||||
"$repo/examples/agi/agiRes.c" \
|
"$repo/examples/agi/agiRes.c" \
|
||||||
|
"$stubSrc" \
|
||||||
-o "$testRes"
|
-o "$testRes"
|
||||||
|
|
||||||
gcc "${hostCflags[@]}" \
|
gcc "${hostCflags[@]}" \
|
||||||
|
|
@ -131,6 +159,9 @@ for game in "${games[@]}"; do
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# agiRes.c opens data by bare name via jlDataOpen; the host stub
|
||||||
|
# resolves those against AGI_DATA_DIR (see stubSrc above).
|
||||||
|
export AGI_DATA_DIR="$dir"
|
||||||
gameOk=1
|
gameOk=1
|
||||||
echo "--- resource loader ---"
|
echo "--- resource loader ---"
|
||||||
if ! "$testRes" "$dir"; then
|
if ! "$testRes" "$dir"; then
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ int main(int argc, char **argv) {
|
||||||
fprintf(stderr, "usage: %s <game-directory> <logic-id> [end-pc]\n", argv[0]);
|
fprintf(stderr, "usage: %s <game-directory> <logic-id> [end-pc]\n", argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (!agiResOpen(&game, argv[1])) {
|
if (!agiResOpen(&game)) {
|
||||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ int main(int argc, char **argv) {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agiResOpen(&game, argv[1])) {
|
if (!agiResOpen(&game)) {
|
||||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ int main(int argc, char **argv) {
|
||||||
maxCycles = (uint32_t)MAX_CYCLES_DEFAULT;
|
maxCycles = (uint32_t)MAX_CYCLES_DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agiResOpen(&gGame, argv[1])) {
|
if (!agiResOpen(&gGame)) {
|
||||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("Opening AGI game directory: %s\n", argv[1]);
|
printf("Opening AGI game directory: %s\n", argv[1]);
|
||||||
if (!agiResOpen(&game, argv[1])) {
|
if (!agiResOpen(&game)) {
|
||||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s' (missing files? wrong format?)\n", argv[1]);
|
fprintf(stderr, "FAIL: agiResOpen rejected '%s' (missing files? wrong format?)\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ int main(int argc, char **argv) {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agiResOpen(&game, argv[1])) {
|
if (!agiResOpen(&game)) {
|
||||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ int main(int argc, char **argv) {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agiResOpen(&game, argv[1])) {
|
if (!agiResOpen(&game)) {
|
||||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue