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
357
agi.c
357
agi.c
|
|
@ -13,6 +13,8 @@
|
|||
// screen the VM can't yet advance through. 0 means "use
|
||||
// whatever logic.0 picks", same as omitting the argument.)
|
||||
|
||||
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include "joey/audio.h"
|
||||
|
|
@ -21,6 +23,7 @@
|
|||
#include "joey/draw.h"
|
||||
#include "joey/input.h"
|
||||
#include "joey/palette.h"
|
||||
#include "joey/platform.h"
|
||||
#include "joey/present.h"
|
||||
#include "joey/sprite.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 void agiTickAdvance(void);
|
||||
static void agiVoiceTick(uint8_t voice);
|
||||
static void psgNoiseApply(void);
|
||||
static void psgRecompute(uint8_t chipCh);
|
||||
static void psgWriteData(uint8_t val);
|
||||
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 cbViewLoopCount(void *ctx, uint8_t viewId);
|
||||
static bool decodePicInto(uint8_t picId, bool clearFirst);
|
||||
static void agiInputCapture(void);
|
||||
static void agiParseCommand(void);
|
||||
static void dispatchAllPressedKeys(void);
|
||||
static void drawObjects(jlSurfaceT *stage);
|
||||
static const AgiViewT *fetchView(uint8_t viewId);
|
||||
|
|
@ -138,7 +144,6 @@ static uint8_t parseArgU8(const char *s);
|
|||
static void releaseLogicCache(void);
|
||||
static void releaseViewCache(void);
|
||||
static void renderFrame(jlSurfaceT *stage);
|
||||
static const char *resolveGameDir(int argc, char **argv);
|
||||
static uint8_t resolveStartingRoom(int argc, char **argv);
|
||||
static void runVmCycle(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) {
|
||||
(void)ctx;
|
||||
(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
|
||||
// `chipCh` from its current freqCount + attenuation + genType.
|
||||
// Called whenever the chip state for a tone channel changes;
|
||||
// noise channels aren't synthesized and skip recompute.
|
||||
// Called whenever the chip state for a tone channel changes; the
|
||||
// noise channel isn't synthesized into the PCM stream -- it goes to
|
||||
// the hardware noise voice instead.
|
||||
static void psgRecompute(uint8_t chipCh) {
|
||||
PsgChannelT *p;
|
||||
uint32_t freqHz;
|
||||
|
||||
if (chipCh >= 3u) {
|
||||
psgNoiseApply();
|
||||
return;
|
||||
}
|
||||
p = &gPsg[chipCh];
|
||||
|
|
@ -631,7 +804,9 @@ static void psgWriteData(uint8_t val) {
|
|||
case 2u: gPsg[3].freqCount = 128u; 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) {
|
||||
reg = (uint8_t)((val >> 5) & 0x03u);
|
||||
gPsgReg = reg;
|
||||
|
|
@ -652,6 +827,9 @@ static void agiSoundReset(void) {
|
|||
uint8_t v;
|
||||
|
||||
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++) {
|
||||
gVoiceSched[v].chOffset = 0u;
|
||||
gVoiceSched[v].eventFramesRem = 0u;
|
||||
|
|
@ -804,6 +982,7 @@ static void agiTickAdvance(void) {
|
|||
}
|
||||
if (allDone) {
|
||||
gSoundActive = false;
|
||||
jlAudioNoise(0u, 15u); // percussion must not outlive the song
|
||||
jlLogF("synth EOS wallMs=%u v0done=%u v1done=%u v2done=%u v3done=%u",
|
||||
(unsigned)nowMs,
|
||||
(unsigned)gVoiceSched[0].done,
|
||||
|
|
@ -1236,35 +1415,44 @@ static void dispatchAllPressedKeys(void) {
|
|||
};
|
||||
uint8_t k;
|
||||
uint8_t i;
|
||||
bool captureText;
|
||||
|
||||
if (jlKeyPressed(KEY_RETURN)) {
|
||||
agiVmDispatchKey(&gVm, 13u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
if (jlKeyPressed(KEY_SPACE)) {
|
||||
agiVmDispatchKey(&gVm, 32u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
// 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)) {
|
||||
agiVmDispatchKey(&gVm, 13u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
if (jlKeyPressed(KEY_SPACE)) {
|
||||
agiVmDispatchKey(&gVm, 32u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
if (jlKeyPressed(KEY_BACKSPACE)) {
|
||||
agiVmDispatchKey(&gVm, 8u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
for (k = 0u; k < 26u; k++) {
|
||||
if (jlKeyPressed((jlKeyE)((uint8_t)KEY_A + k))) {
|
||||
agiVmDispatchKey(&gVm, (uint8_t)('a' + k), 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
}
|
||||
for (k = 0u; k < 10u; k++) {
|
||||
if (jlKeyPressed((jlKeyE)((uint8_t)KEY_0 + k))) {
|
||||
agiVmDispatchKey(&gVm, (uint8_t)('0' + k), 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (jlKeyPressed(KEY_TAB)) {
|
||||
agiVmDispatchKey(&gVm, 9u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
if (jlKeyPressed(KEY_BACKSPACE)) {
|
||||
agiVmDispatchKey(&gVm, 8u, 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
for (k = 0u; k < 26u; k++) {
|
||||
if (jlKeyPressed((jlKeyE)((uint8_t)KEY_A + k))) {
|
||||
agiVmDispatchKey(&gVm, (uint8_t)('a' + k), 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
}
|
||||
for (k = 0u; k < 10u; k++) {
|
||||
if (jlKeyPressed((jlKeyE)((uint8_t)KEY_0 + k))) {
|
||||
agiVmDispatchKey(&gVm, (uint8_t)('0' + k), 0u);
|
||||
gAnyKeyHit = true;
|
||||
}
|
||||
}
|
||||
for (i = 0u; i < 10u; i++) {
|
||||
if (jlKeyPressed(fnKeys[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
|
||||
// surface. Chunky-fast: a single memcpy per row from backdrop pixels
|
||||
// to stage pixels. Bypassing jlDrawPixel keeps per-frame restore
|
||||
// cost ~1KB memcpy per object instead of per-pixel function calls.
|
||||
// Planar (s->pixels NULL) is uncommon at this point; fall back to
|
||||
// jlSurfaceCopy of the whole surface (slower but correct).
|
||||
// to stage pixels via the library's rect copy (hunt-3 rank 3). The
|
||||
// old body hand-rolled a chunky memcpy loop and, on the planar 68k
|
||||
// ports (stage->pixels NULL post-Phase-9/10), fell back to a FULL
|
||||
// 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) {
|
||||
int16_t row;
|
||||
int16_t byteX;
|
||||
int16_t byteW;
|
||||
int16_t endY;
|
||||
|
||||
if (stage == NULL || gBackdrop == NULL || w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
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);
|
||||
jlSurfaceCopyRect(stage, gBackdrop, x, y, (uint16_t)w, (uint16_t)h);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1549,6 +1714,31 @@ static void renderFrame(jlSurfaceT *stage) {
|
|||
}
|
||||
// Step 2: restore + draw animated/drawn objects.
|
||||
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 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) {
|
||||
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 parseArgU8(argv[2]);
|
||||
return parseArgU8(argv[1]);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1615,6 +1806,11 @@ static void runVmCycle(void) {
|
|||
if (reason == AGI_VM_HALT_RETURN) {
|
||||
agiVmTickAnimation(&gVm);
|
||||
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;
|
||||
logic0 = cbFetchLogic(NULL, 0u, &length);
|
||||
if (logic0 != NULL) {
|
||||
|
|
@ -1696,7 +1892,6 @@ static void updateEgoFromInput(void) {
|
|||
int main(int argc, char **argv) {
|
||||
jlConfigT cfg;
|
||||
AgiVmCallbacksT cb;
|
||||
const char *gameDir;
|
||||
const uint8_t *logic0;
|
||||
uint16_t length;
|
||||
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
|
||||
// from the platform's large-allocator. jlAlloc does not zero its
|
||||
// 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);
|
||||
|
||||
gameDir = resolveGameDir(argc, argv);
|
||||
startingRoom = resolveStartingRoom(argc, argv);
|
||||
gOverrideRoom = startingRoom;
|
||||
gOverrideRoomActive = (startingRoom != 0u);
|
||||
|
|
@ -1752,7 +1947,7 @@ int main(int argc, char **argv) {
|
|||
gPicReady = false;
|
||||
gAnyKeyHit = false;
|
||||
|
||||
if (agiResOpen(&gGame, gameDir)) {
|
||||
if (agiResOpen(&gGame)) {
|
||||
picOk = agiPicAlloc(&gPic);
|
||||
if (picOk) {
|
||||
agiPicClear(&gPic);
|
||||
|
|
@ -1814,6 +2009,12 @@ int main(int argc, char **argv) {
|
|||
gAnyKeyHit = false;
|
||||
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
|
||||
// print modal up. This drives music event playback AND
|
||||
// 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
|
||||
// is armed) or libxmp + SFX overlay (when not).
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
agi.h
56
agi.h
|
|
@ -71,8 +71,38 @@ typedef struct {
|
|||
FILE *volFiles[AGI_MAX_VOLUMES];
|
||||
uint16_t resCount[AGI_RES_COUNT];
|
||||
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;
|
||||
|
||||
// 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;
|
||||
// only the low nibble (4 bits) of each byte carries actual color
|
||||
|
|
@ -388,6 +418,15 @@ typedef struct {
|
|||
// ----- strings -----
|
||||
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 -----
|
||||
// The VM polls callbacks.isPlayingSound() each tick; when it
|
||||
// transitions from true to false, the sound-done flags fire.
|
||||
|
|
@ -410,12 +449,17 @@ void agiResClose(AgiGameT *game);
|
|||
// resource payload length in bytes.
|
||||
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
|
||||
// path to a directory containing LOGDIR/PICDIR/VIEWDIR/SNDDIR plus
|
||||
// VOL.0..VOL.N. Returns true if every directory file and at least
|
||||
// one VOL.* file opened. Partial-success on missing optional
|
||||
// volumes; agiResLoad reports the missing-volume case per resource.
|
||||
bool agiResOpen(AgiGameT *game, const char *gameDir);
|
||||
// Open the AGI v2 game whose LOGDIR/PICDIR/VIEWDIR/SNDDIR and VOL.0..VOL.N
|
||||
// files are staged under DATA/ (opened by bare name via jlDataOpen).
|
||||
// Returns true if every directory file and at least one VOL.* file opened.
|
||||
// Partial-success on missing optional volumes; agiResLoad reports the
|
||||
// missing-volume case per resource.
|
||||
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) -----
|
||||
|
|
|
|||
255
agiRes.c
255
agiRes.c
|
|
@ -22,6 +22,8 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
|
||||
|
||||
// AGI directory files are flat arrays of 3-byte entries. The maximum
|
||||
// directory length the loader will accept is bounded by AGI_MAX_-
|
||||
|
|
@ -35,9 +37,9 @@
|
|||
#define AGI_VOL_COMPRESSED 0x80
|
||||
#define AGI_VOL_VOLUME_MASK 0x7F
|
||||
|
||||
// Max directory + path string size. AGI filenames are 7 chars max;
|
||||
// gameDir is application-controlled. 256 leaves comfortable margin.
|
||||
#define AGI_PATH_MAX 256
|
||||
// Resource file names are opened relative to DATA/ via jlDataOpen (see
|
||||
// joey/file.h); the longest name AGI forms is "VOL.<n>". 16 is ample.
|
||||
#define AGI_VOL_NAME_MAX 16
|
||||
|
||||
// Empty-slot sentinel: must be >= AGI_MAX_VOLUMES so agiResLoad
|
||||
// rejects empty entries. The on-disk empty marker is 0xFFFFFF (high
|
||||
|
|
@ -55,44 +57,24 @@
|
|||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file);
|
||||
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum);
|
||||
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName);
|
||||
static bool openVolumes(AgiGameT *game, const char *gameDir);
|
||||
static bool buildVolName(char *out, size_t outSize, uint8_t volNum);
|
||||
static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
||||
static bool loadWords(AgiGameT *game);
|
||||
static bool openVolumes(AgiGameT *game);
|
||||
static const char *resTypeFileName(AgiResTypeE type);
|
||||
static bool wordEqCI(const char *a, const char *b);
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file) {
|
||||
size_t dirLen;
|
||||
size_t fileLen;
|
||||
static bool buildVolName(char *out, size_t outSize, uint8_t volNum) {
|
||||
char *p;
|
||||
|
||||
dirLen = strlen(dir);
|
||||
fileLen = strlen(file);
|
||||
if (dirLen + 1 + fileLen + 1 > outSize) {
|
||||
// "VOL." + up to 2 digits + NUL = 7 chars max.
|
||||
if (outSize < 8u) {
|
||||
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;
|
||||
|
||||
dirLen = strlen(dir);
|
||||
// "/VOL." + up to 2 digits + NUL = 8 chars max.
|
||||
if (dirLen + 8 > outSize) {
|
||||
return false;
|
||||
}
|
||||
memcpy(out, dir, dirLen);
|
||||
p = out + dirLen;
|
||||
*p++ = '/';
|
||||
p = out;
|
||||
*p++ = 'V';
|
||||
*p++ = 'O';
|
||||
*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) {
|
||||
char path[AGI_PATH_MAX];
|
||||
FILE *fp;
|
||||
long fileSize;
|
||||
uint16_t entryCount;
|
||||
uint16_t i;
|
||||
uint8_t buf[AGI_DIR_ENTRY_BYTES];
|
||||
uint8_t volNibble;
|
||||
uint32_t offset;
|
||||
static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type) {
|
||||
// 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;
|
||||
uint16_t entryCount;
|
||||
uint16_t i;
|
||||
uint8_t volNibble;
|
||||
uint32_t offset;
|
||||
size_t got;
|
||||
|
||||
if (!buildPath(path, AGI_PATH_MAX, gameDir, fileName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fp = fopen(path, "rb");
|
||||
// fileName is a bare name relative to DATA/ (e.g. "LOGDIR").
|
||||
fp = jlDataOpen(fileName, "rb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0L, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
fileSize = ftell(fp);
|
||||
if (fileSize < 0 || fileSize > (long)(AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES)) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
if ((fileSize % AGI_DIR_ENTRY_BYTES) != 0) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
if (fseek(fp, 0L, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
// Read the whole flat 3-byte-entry array in one call rather than one
|
||||
// read per entry: on the IIgs each fread is a GS/OS round trip through
|
||||
// SmartPort firmware, and a directory never exceeds AGI_MAX_RESOURCES
|
||||
// entries, so the fixed buffer holds any valid file. A non-multiple-of-3
|
||||
// length means the file is malformed.
|
||||
got = fread(dirBytes, 1, sizeof(dirBytes), fp);
|
||||
fclose(fp);
|
||||
if (got == 0u || (got % AGI_DIR_ENTRY_BYTES) != 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entryCount = (uint16_t)(fileSize / AGI_DIR_ENTRY_BYTES);
|
||||
entryCount = (uint16_t)(got / AGI_DIR_ENTRY_BYTES);
|
||||
for (i = 0; i < entryCount; i++) {
|
||||
if (fread(buf, 1, AGI_DIR_ENTRY_BYTES, fp) != AGI_DIR_ENTRY_BYTES) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
const uint8_t *e = &dirBytes[(uint32_t)i * AGI_DIR_ENTRY_BYTES];
|
||||
|
||||
volNibble = (uint8_t)(buf[0] >> 4);
|
||||
volNibble = (uint8_t)(e[0] >> 4);
|
||||
// 20-bit offset, cast to uint32_t before shift so a
|
||||
// 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].offset = 0u;
|
||||
} else {
|
||||
|
|
@ -165,24 +135,55 @@ static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir,
|
|||
game->resDir[type][i].offset = offset;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
game->resCount[type] = entryCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool openVolumes(AgiGameT *game, const char *gameDir) {
|
||||
char path[AGI_PATH_MAX];
|
||||
static bool loadWords(AgiGameT *game) {
|
||||
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;
|
||||
bool anyOpened;
|
||||
|
||||
anyOpened = false;
|
||||
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||
if (!buildVolPath(path, AGI_PATH_MAX, gameDir, v)) {
|
||||
if (!buildVolName(name, sizeof(name), v)) {
|
||||
return false;
|
||||
}
|
||||
game->volFiles[v] = fopen(path, "rb");
|
||||
game->volFiles[v] = jlDataOpen(name, "rb");
|
||||
if (game->volFiles[v] != NULL) {
|
||||
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) -----
|
||||
|
||||
void agiResClose(AgiGameT *game) {
|
||||
|
|
@ -217,6 +243,11 @@ void agiResClose(AgiGameT *game) {
|
|||
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||
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;
|
||||
|
||||
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++) {
|
||||
if (!loadDirectory(game, (AgiResTypeE)t, gameDir, resTypeFileName((AgiResTypeE)t))) {
|
||||
if (!loadDirectory(game, resTypeFileName((AgiResTypeE)t), (AgiResTypeE)t)) {
|
||||
agiResClose(game);
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
81
agiVm.c
81
agiVm.c
|
|
@ -301,7 +301,7 @@ static bool popFrame(AgiVmT *vm);
|
|||
static bool pushFrame(AgiVmT *vm);
|
||||
static bool readByte(AgiVmT *vm, uint8_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 vmStepMisc(AgiVmT *vm, uint8_t op);
|
||||
static VmDispatchE vmStepObject(AgiVmT *vm, uint8_t op);
|
||||
|
|
@ -416,11 +416,37 @@ static bool evalOneTest(AgiVmT *vm, uint8_t testOp) {
|
|||
}
|
||||
|
||||
if (testOp == TEST_SAID) {
|
||||
// 1-byte word count, followed by 2 bytes per word.
|
||||
if (!skipTestArgs(vm, testOp)) {
|
||||
// Variable-length: 1-byte word count, then that many 16-bit
|
||||
// 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;
|
||||
}
|
||||
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];
|
||||
|
|
@ -877,37 +903,34 @@ static bool readU16(AgiVmT *vm, uint16_t *out) {
|
|||
}
|
||||
|
||||
|
||||
// Walk past the argument bytes of a test command without evaluating.
|
||||
// For the variable-length `said` test, the first byte is the word
|
||||
// count and the rest are 2 bytes per word.
|
||||
static bool skipTestArgs(AgiVmT *vm, uint8_t testOp) {
|
||||
uint8_t scratch;
|
||||
uint8_t wordCount;
|
||||
uint16_t toSkip;
|
||||
uint16_t i;
|
||||
// Match the parsed player command against a said() word pattern.
|
||||
// AGI_WORD_REST (9999) matches the rest of the line (including nothing);
|
||||
// AGI_WORD_ANYWORD (1) matches exactly one arbitrary word;
|
||||
// any other id matches that exact group id.
|
||||
// An exact match requires every typed word to be consumed by the pattern.
|
||||
static bool saidMatch(const AgiVmT *vm, const uint16_t *pat, uint8_t patCount) {
|
||||
uint8_t pi;
|
||||
uint8_t wi;
|
||||
|
||||
if (testOp == TEST_SAID) {
|
||||
if (!readByte(vm, &wordCount)) {
|
||||
pi = 0u;
|
||||
wi = 0u;
|
||||
while (pi < patCount) {
|
||||
uint16_t pw;
|
||||
|
||||
pw = pat[pi];
|
||||
if (pw == AGI_WORD_REST) {
|
||||
return true;
|
||||
}
|
||||
if (wi >= vm->parsedWordCount) {
|
||||
return false;
|
||||
}
|
||||
toSkip = (uint16_t)((uint16_t)wordCount * 2u);
|
||||
for (i = 0u; i < toSkip; i++) {
|
||||
if (!readByte(vm, &scratch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (testOp == 0u || testOp >= TEST_MAX) {
|
||||
return false;
|
||||
}
|
||||
toSkip = (uint16_t)kTestArgBytes[testOp];
|
||||
for (i = 0u; i < toSkip; i++) {
|
||||
if (!readByte(vm, &scratch)) {
|
||||
if (pw != AGI_WORD_ANYWORD && pw != vm->parsedWords[wi]) {
|
||||
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):
|
||||
# LOGDIR PICDIR VIEWDIR SNDDIR VOL.0 [VOL.1 ...] [OBJECT] [WORDS.TOK]
|
||||
#
|
||||
# IIgs is not yet wired (needs the disk packager extended to a larger
|
||||
# 2mg volume plus case-folded ProDOS file names); the script reports
|
||||
# what's missing rather than launching.
|
||||
# IIgs delegates to run-iigs.sh's AGI mode, which packs AGI + the game
|
||||
# onto a 2MB CFFA2 hard-disk volume (an 800KB floppy can't hold both) and
|
||||
# boots GS/OS from a second CFFA2 partition. See make-agi-iigs-disk.sh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -64,15 +64,19 @@ fi
|
|||
# filesystems (e.g. Amiga FFS: "AGI" vs our "Agi").
|
||||
stageGameData() {
|
||||
local destDir=$1
|
||||
local dataDir=$destDir/DATA
|
||||
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
|
||||
if [[ -f $gamedata/$f ]]; then
|
||||
cp "$gamedata/$f" "$destDir/$f"
|
||||
cp "$gamedata/$f" "$dataDir/$f"
|
||||
fi
|
||||
done
|
||||
for f in "$gamedata"/VOL.*; do
|
||||
[[ -f $f ]] && cp "$f" "$destDir/$(basename "$f")"
|
||||
[[ -f $f ]] && cp "$f" "$dataDir/$(basename "$f")"
|
||||
done
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +87,7 @@ runDos() {
|
|||
local work
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
|
|
@ -116,13 +120,13 @@ runDos() {
|
|||
cp "$bin" "$work/AGI.EXE"
|
||||
stageGameData "$work"
|
||||
|
||||
# AGI.EXE reads game data from its CWD (we mount $work as C: and
|
||||
# cd there first), so argv[1] is "." -- the starting-room override
|
||||
# lives in argv[2]. Skip the argv when room=0 so the binary
|
||||
# AGI.EXE reads game data from DATA/ under its CWD (we mount $work as C:
|
||||
# and cd there first), so no game-directory argument is needed; the
|
||||
# starting-room override is argv[1]. Skip it when room=0 so the binary
|
||||
# exercises its default code path.
|
||||
local agi_cmd="AGI.EXE"
|
||||
if (( room != 0 )); then
|
||||
agi_cmd="AGI.EXE . $room"
|
||||
agi_cmd="AGI.EXE $room"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
|
@ -160,7 +164,7 @@ runAtariST() {
|
|||
local work
|
||||
|
||||
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
|
||||
fi
|
||||
if [[ ! -f $tos ]]; then
|
||||
|
|
@ -198,7 +202,7 @@ runAmiga() {
|
|||
local work
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
|
|
@ -243,25 +247,21 @@ EOF
|
|||
|
||||
|
||||
runIigs() {
|
||||
cat <<EOF >&2
|
||||
IIgs AGI run is not wired up yet.
|
||||
|
||||
The existing IIgs disk packager (toolchains/iigs/package-disk.sh) builds
|
||||
joey.2mg from build/iigs/bin/* only; it doesn't accept arbitrary game
|
||||
data, the disk size is fixed at 800K (KQ3's ~1.5 MB of resources won't
|
||||
fit), and ProDOS case-folding for game-data file names has not been
|
||||
audited.
|
||||
|
||||
To wire this up the package-disk.sh tool needs a larger volume option
|
||||
and an "extra data" path argument, and run-iigs.sh needs an AGI mode
|
||||
that points GSplus at the resulting 2mg. Tracked separately.
|
||||
|
||||
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
|
||||
# IIgs uses a CFFA2 hard disk (the 800KB floppy can't hold AGI + KQ3),
|
||||
# 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,
|
||||
# and launches AGI. The starting-room override is DOS-only (the IIgs
|
||||
# autostart path can't pass argv), so $room is not forwarded here.
|
||||
if (( room != 0 )); then
|
||||
echo "note: starting-room override is DOS-only; ignored on IIgs" >&2
|
||||
fi
|
||||
# The IIgs build target is the binary path, not EXAMPLE=agi.
|
||||
if [[ ! -f "$repo/build/iigs/bin/AGI" ]]; then
|
||||
echo "$repo/build/iigs/bin/AGI not built." >&2
|
||||
echo "Build it with: make -f make/iigs.mk $repo/build/iigs/bin/AGI" >&2
|
||||
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'
|
||||
#include "joey/draw.h"
|
||||
#include "joey/core.h"
|
||||
#include "joey/file.h"
|
||||
#include "surfaceInternal.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 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(); }
|
||||
uint16_t jlRandomRange(uint16_t bound) { return (bound == 0u) ? 0u : (uint16_t)((unsigned)rand() % bound); }
|
||||
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; }
|
||||
EOF
|
||||
|
||||
|
|
@ -60,6 +87,7 @@ echo "Building host tests..."
|
|||
gcc "${hostCflags[@]}" \
|
||||
"$repo/tests/agi/testAgiRes.c" \
|
||||
"$repo/examples/agi/agiRes.c" \
|
||||
"$stubSrc" \
|
||||
-o "$testRes"
|
||||
|
||||
gcc "${hostCflags[@]}" \
|
||||
|
|
@ -131,6 +159,9 @@ for game in "${games[@]}"; do
|
|||
continue
|
||||
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
|
||||
echo "--- resource loader ---"
|
||||
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]);
|
||||
return 2;
|
||||
}
|
||||
if (!agiResOpen(&game, argv[1])) {
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ int main(int argc, char **argv) {
|
|||
return 2;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&game, argv[1])) {
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ int main(int argc, char **argv) {
|
|||
maxCycles = (uint32_t)MAX_CYCLES_DEFAULT;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&gGame, argv[1])) {
|
||||
if (!agiResOpen(&gGame)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
|
||||
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]);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ int main(int argc, char **argv) {
|
|||
return 2;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&game, argv[1])) {
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ int main(int argc, char **argv) {
|
|||
return 2;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&game, argv[1])) {
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue