X68k, ST, and Amiga optimizations.
This commit is contained in:
parent
5adf55e80f
commit
f6c4ad3747
8 changed files with 385 additions and 56 deletions
|
|
@ -14,15 +14,18 @@ output level running from one utterance into the next, so a phrase whose
|
|||
first word ends on the low level plays its second word inverted. Both
|
||||
versions are stored and the player picks by the running parity.
|
||||
speech.bin (little-endian):
|
||||
"STSP" u8 version(2) u8 pitchBase u8 pitchCount u8 count
|
||||
"STSP" u8 version(3) u8 pitchBase u8 pitchCount u8 count
|
||||
u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream)
|
||||
u8 pad[3] u32 dataBytes
|
||||
entries[pitchCount * count * 2]: u8 code, u8 parity, u16 samples,
|
||||
u32 offset -- polarity is the innermost index (0 = as decoded,
|
||||
1 = inverted). parity = 1 when the utterance ends on the
|
||||
opposite level from the one it started on.
|
||||
u8 polarities (1 or 2) u8 pad[2] u32 dataBytes
|
||||
entries[pitchCount * count * polarities]: u8 code, u8 parity,
|
||||
u16 samples, u32 offset -- polarity is the innermost index
|
||||
(0 = as decoded, 1 = inverted). parity = 1 when the utterance
|
||||
ends on the opposite level from the one it started on.
|
||||
data: sample bytes
|
||||
usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased]
|
||||
With --single only polarity 0 is written, halving the file; the player
|
||||
negates the samples itself when the running parity calls for it. The
|
||||
X68000 needs that: its Human68k floppy cannot spare the other 60 KB.
|
||||
usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased] [--single]
|
||||
"""
|
||||
import re
|
||||
import struct
|
||||
|
|
@ -77,6 +80,8 @@ def decode(rle, perOn):
|
|||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
biased = "--biased" in sys.argv[1:]
|
||||
single = "--single" in sys.argv[1:]
|
||||
polarities = 1 if single else 2
|
||||
src, dst = args[0], args[1]
|
||||
entries, data = parseSource(src)
|
||||
records = []
|
||||
|
|
@ -86,7 +91,7 @@ def main():
|
|||
for code, off, length in entries:
|
||||
samples, level = decode(data[off:off + length], perOn)
|
||||
parity = 1 if level < 0 else 0
|
||||
for pol in range(2):
|
||||
for pol in range(polarities):
|
||||
pcm = bytearray()
|
||||
for s in samples:
|
||||
v = -s if pol else s
|
||||
|
|
@ -94,12 +99,14 @@ def main():
|
|||
records.append((code, parity, len(samples), len(blob)))
|
||||
blob += pcm
|
||||
with open(dst, "wb") as f:
|
||||
f.write(b"STSP" + struct.pack("<BBBBBBBBI", 2, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, 0, 0, 0, len(blob)))
|
||||
f.write(b"STSP" + struct.pack("<BBBBBBBBI", 3, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, polarities, 0, 0, len(blob)))
|
||||
for code, parity, n, off in records:
|
||||
f.write(struct.pack("<BBHI", code, parity, n, off))
|
||||
f.write(blob)
|
||||
total = sum(n for _, _, n, _ in records)
|
||||
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x 2 polarities, {total} samples, {len(blob)} data bytes, {'biased' if biased else 'signed'}")
|
||||
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x {polarities} "
|
||||
f"polarit{'y' if polarities == 1 else 'ies'}, {total} samples, {len(blob)} data bytes, "
|
||||
f"{'biased' if biased else 'signed'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
98
spacetaxi.c
98
spacetaxi.c
|
|
@ -40,6 +40,11 @@
|
|||
#define ST_GAME_OVER_TICKS 60u
|
||||
// Menu joystick repeat delay ($4101 with X = $82).
|
||||
#define ST_MENU_REPEAT_TICKS 8u
|
||||
// How long a fatal load message stays up, in display frames, and how many
|
||||
// must pass before a joystick press can cut it short (a button held from
|
||||
// before must not skip the message).
|
||||
#define ST_FATAL_HOLD_FRAMES 600u
|
||||
#define ST_FATAL_ARM_FRAMES 30u
|
||||
|
||||
// The immortal cabbies ($4A89 image, $4E35 screen).
|
||||
#define ST_HS_ENTRIES 10u
|
||||
|
|
@ -87,6 +92,7 @@ typedef struct {
|
|||
uint8_t hsCursor; // $4F4D the blinking cursor's current glyph
|
||||
uint8_t hsBlinkTicks; // ticks toward the next cursor flip
|
||||
bool paused; // $4FCB: SHIFT is holding the game
|
||||
bool fatal; // an asset on the disk would not load
|
||||
} StGameT;
|
||||
|
||||
// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd"
|
||||
|
|
@ -150,6 +156,14 @@ static const uint8_t *kAboutLines[7] = {
|
|||
(const uint8_t *)"ALSO TRY RESCUE SQUAD BY JOHN KUTCHER",
|
||||
};
|
||||
static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN";
|
||||
// Fatal load messages. Two short centred lines in the game's own charset,
|
||||
// upper case because the C64 font has no lower case in this bank.
|
||||
static const char kTextFatalMemory[] = "OUT OF MEMORY";
|
||||
static const char kTextFatalSpeech[] = "SPEECH WILL NOT LOAD";
|
||||
static const char kTextFatalSprites[] = "SPRITES WILL NOT COMPILE";
|
||||
static const char kTextFatalLoad[] = "DISK ERROR";
|
||||
static const char kTextFatalTitle[] = "CANNOT LOAD TITLE SCREEN";
|
||||
static const char kTextFatalLevel[] = "CANNOT LOAD LEVEL";
|
||||
|
||||
static StGameT gGame;
|
||||
static StSimT gSim;
|
||||
|
|
@ -163,6 +177,8 @@ static StLevelT gLevel;
|
|||
|
||||
static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg);
|
||||
static void enterTitle(void);
|
||||
static void fatalHold(void);
|
||||
static void fatalStop(const char *line1, const char *line2);
|
||||
static void gameOverEnter(void);
|
||||
static void gameOverFinish(void);
|
||||
static void highScoreDraw(void);
|
||||
|
|
@ -207,9 +223,10 @@ static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg) {
|
|||
static void enterTitle(void) {
|
||||
gSim.demoMode = 0u;
|
||||
if (!stLevelLoad(&gLevel, "levels/title.dat")) {
|
||||
ST_LOG("spacetaxi: ! title load FAILED");
|
||||
fatalStop(kTextFatalLoad, kTextFatalTitle);
|
||||
return;
|
||||
}
|
||||
stRenderLevelCels(gStage, 0); // the last level's cels go
|
||||
(void)stRenderLevelCels(gStage, 0); // the last level's cels go
|
||||
stTitleEnter(&gSim, &gLevel);
|
||||
stRenderSceneChanged(&gSim);
|
||||
stRenderTitleLogo(&gSim);
|
||||
|
|
@ -442,6 +459,45 @@ static void runNameEntry(uint8_t ticks) {
|
|||
}
|
||||
|
||||
|
||||
// Hold a fatal message on screen long enough to be read, then let the
|
||||
// caller shut down. FIRE or a key cuts the wait short.
|
||||
static void fatalHold(void) {
|
||||
uint8_t last = (uint8_t)jlFrameCount();
|
||||
uint16_t held = 0u;
|
||||
|
||||
// Count display frames, not loop turns: presenting an unchanged
|
||||
// screen costs almost nothing, so a turn count would run out in well
|
||||
// under a second on the faster ports.
|
||||
while (held < ST_FATAL_HOLD_FRAMES) {
|
||||
uint8_t now;
|
||||
|
||||
jlInputPoll();
|
||||
if (held > ST_FATAL_ARM_FRAMES && (joyMask() & 0x1Fu) != 0u) {
|
||||
return;
|
||||
}
|
||||
now = (uint8_t)jlFrameCount();
|
||||
held = (uint16_t)(held + (uint8_t)(now - last));
|
||||
last = now;
|
||||
jlStagePresent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// An asset the game ships with will not load. Say so on screen and stop:
|
||||
// a mute game, or one running every sprite through the interpreter, reads
|
||||
// as a slow machine rather than a broken install, and that is worse than
|
||||
// not starting.
|
||||
static void fatalStop(const char *line1, const char *line2) {
|
||||
ST_LOG("spacetaxi: ! FATAL -- %s / %s", line1, line2);
|
||||
ST_LOG_FLUSH();
|
||||
stAudioSilence();
|
||||
stAudioNoise(false);
|
||||
jlMusicStop();
|
||||
stRenderFatal(gStage, line1, line2);
|
||||
gGame.fatal = true;
|
||||
}
|
||||
|
||||
|
||||
// $5FBB -- "GAME OVER!" over the play field, sprites hidden.
|
||||
static void gameOverEnter(void) {
|
||||
stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u);
|
||||
|
|
@ -505,8 +561,7 @@ static bool loadLevel(uint8_t index) {
|
|||
ST_LOG("spacetaxi: ! cannot load %s", path);
|
||||
return false;
|
||||
}
|
||||
stRenderLevelCels(gStage, &gLevel);
|
||||
return true;
|
||||
return stRenderLevelCels(gStage, &gLevel);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -842,12 +897,25 @@ static void startDemoScreen(void) {
|
|||
(void)stSimAdvancePlayer(&gSim);
|
||||
gSim.levelState++;
|
||||
if (!loadLevel(e->levelIndex)) {
|
||||
enterTitle();
|
||||
fatalStop(kTextFatalLoad, kTextFatalLevel);
|
||||
return;
|
||||
}
|
||||
// $40CA: blank the character screen first. Without this the two
|
||||
// green lines land on top of the title, which is still up -- logo,
|
||||
// byline, copyright and all -- where the original shows them on a
|
||||
// cleared screen (observed on real hardware, 2026-09-23). Same
|
||||
// clear stIntroEnter does, and it leaves the sprite mask alone so
|
||||
// the line below still keeps sprites 0..2.
|
||||
memset(gSim.screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
|
||||
memset(gSim.color, 0, ST_SCREEN_CELLS);
|
||||
stSimDirtyAll(&gSim);
|
||||
stSimGlyphReset(&gSim);
|
||||
gSim.charDirtyAll = true;
|
||||
gSim.charDirtyCount = 0u;
|
||||
stSimDrawText(&gSim, 10u, 8u, kTextGetReady, 5u);
|
||||
stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u);
|
||||
gSim.frame.enableMask &= 0x07u;
|
||||
stRenderSceneChanged(&gSim);
|
||||
gGame.waitTicks = ST_GET_READY_TICKS;
|
||||
gGame.state = ST_STATE_GET_READY;
|
||||
}
|
||||
|
|
@ -881,7 +949,7 @@ static void startNewScreen(void) {
|
|||
}
|
||||
gGame.pendingLevel = next;
|
||||
if (!loadLevel(next)) {
|
||||
enterTitle();
|
||||
fatalStop(kTextFatalLoad, kTextFatalLevel);
|
||||
return;
|
||||
}
|
||||
gSim.levelState++;
|
||||
|
|
@ -931,21 +999,31 @@ int main(void) {
|
|||
memset(&gSim, 0, sizeof(gSim));
|
||||
stRenderInit(stage);
|
||||
stRenderLoading(stage);
|
||||
stAudioInit();
|
||||
if (!stAudioInit()) {
|
||||
fatalStop(kTextFatalMemory, kTextFatalSpeech);
|
||||
}
|
||||
// Boot: the playback buffer as the C64 leaves it, and the state a
|
||||
// fresh machine has when the first title intro starts ($4092 + the
|
||||
// title's own sparkle/walk history from the dump).
|
||||
stSimNewGame(&gSim, 1u, false);
|
||||
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
|
||||
gSim.waveIdx = 3u;
|
||||
stRenderPrewarm(stage, &gSim);
|
||||
if (!gGame.fatal && !stRenderPrewarm(stage, &gSim)) {
|
||||
fatalStop(kTextFatalMemory, kTextFatalSprites);
|
||||
}
|
||||
highScoreLoad(); // $23E5
|
||||
enterTitle();
|
||||
if (!gGame.fatal) {
|
||||
enterTitle();
|
||||
}
|
||||
gGame.lastFrame = (uint8_t)jlFrameCount();
|
||||
|
||||
for (;;) {
|
||||
uint8_t ticks;
|
||||
|
||||
if (gGame.fatal) {
|
||||
fatalHold();
|
||||
break;
|
||||
}
|
||||
jlInputPoll();
|
||||
// $4FCB runStopWatcher, on the game tick, the intro and the menus:
|
||||
// SHIFT held freezes everything with the sound gated off and any
|
||||
|
|
@ -1056,5 +1134,5 @@ int main(void) {
|
|||
stAudioShutdown();
|
||||
stRenderShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
return gGame.fatal ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
18
spacetaxi.h
18
spacetaxi.h
|
|
@ -25,9 +25,11 @@
|
|||
#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000)
|
||||
#define ST_LOG_RESET() ((void)0)
|
||||
#define ST_LOG(...) ((void)0)
|
||||
#define ST_LOG_FLUSH() ((void)0)
|
||||
#else
|
||||
#define ST_LOG_RESET() jlLogReset()
|
||||
#define ST_LOG(...) jlLogF(__VA_ARGS__)
|
||||
#define ST_LOG_FLUSH() jlLogFlush()
|
||||
#endif
|
||||
|
||||
// Level loading through the JoeyLib data path (DATA/levels/...).
|
||||
|
|
@ -54,12 +56,22 @@ void stRenderTitleLogo(StSimT *sim);
|
|||
// animates without a single pixel moving. Call after
|
||||
// stRenderSceneChanged, which turns it back off.
|
||||
void stRenderIntroStars(void);
|
||||
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim);
|
||||
// Fill the stage with a two-line message and present it. Used when an
|
||||
// asset the game ships with will not load, so a player sees why the
|
||||
// game stopped instead of a silently degraded ride.
|
||||
void stRenderFatal(jlSurfaceT *stage, const char *line1, const char *line2);
|
||||
// Compile every always-on cel. False when a shipped asset is present but
|
||||
// unusable, so main can stop with a message instead of running the game
|
||||
// on interpreted cels.
|
||||
bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim);
|
||||
// Ready a level's hook cels before its ride (evicts the previous level's).
|
||||
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level);
|
||||
// False only when this level's baked bank exists and will not load; a
|
||||
// port that ships no per-level banks compiles them and returns true.
|
||||
bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level);
|
||||
|
||||
// Audio sink setup and the per-tick release timers ($4320).
|
||||
void stAudioInit(void);
|
||||
// False when the shipped speech blob will not load.
|
||||
bool stAudioInit(void);
|
||||
void stAudioShutdown(void);
|
||||
void stAudioTick(void);
|
||||
|
||||
|
|
|
|||
142
stAudio.c
142
stAudio.c
|
|
@ -29,6 +29,13 @@
|
|||
|
||||
#define ST_ATTEN_LOUD 4u
|
||||
#define ST_ATTEN_SOFT 8u
|
||||
// The jet's noise pitch: jlAudioNoise takes 0..31 in YM noise-period
|
||||
// style, where 31 is the lowest rumble (0 is the brightest hiss).
|
||||
#define ST_NOISE_JET_PITCH 31u
|
||||
// Noise sound effects (the crash and the wreck's impact) use the same
|
||||
// deep end of the range. The C64 takes the rate from the program's
|
||||
// frequency word; this is the one simplification in that mapping.
|
||||
#define ST_NOISE_SFX_PITCH 31u
|
||||
#define ST_ATTEN_OFF 15u
|
||||
|
||||
// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096.
|
||||
|
|
@ -56,10 +63,16 @@
|
|||
// the ride half a second on every "HEY TAXI"; the 1-bit intermediate
|
||||
// format that followed still needed a per-byte expansion loop.
|
||||
#define ST_SPEECH_FILE "speech.bin"
|
||||
#define ST_SPEECH_VERSION 2u
|
||||
#define ST_SPEECH_VERSION 3u
|
||||
#define ST_SPEECH_HEADER_BYTES 16u
|
||||
#define ST_SPEECH_ENTRY_BYTES 8u
|
||||
#define ST_SPEECH_POLARITIES 2u
|
||||
// speech.bin carries each utterance in one polarity or two. Two is the
|
||||
// cheap case -- the player just picks the baked variant -- but it doubles
|
||||
// the file, and the X68000's Human68k floppy cannot spare the other
|
||||
// 60 KB, so that port bakes one and the player negates the samples
|
||||
// itself. Negation is the same byte arithmetic in both sample formats:
|
||||
// signed 0 - v, and 0x80-biased 0x100 - v, are one operation.
|
||||
#define ST_SPEECH_POLARITIES_MAX 2u
|
||||
#define ST_SPEECH_FORMAT_SIGNED 0u
|
||||
#define ST_SPEECH_FORMAT_BIASED 1u
|
||||
#define ST_SPEECH_LOAD_CHUNK 16384u
|
||||
|
|
@ -88,6 +101,9 @@ typedef struct {
|
|||
static StSfxSlotT gSfx; // SID voice 1 (programs + sweep)
|
||||
static StSfxSlotT gTone; // SID voice 2
|
||||
static bool gNoiseOn;
|
||||
// Release countdown for a NOISE sound effect, in game ticks, mirroring
|
||||
// the two tone slots' ticksLeft. 0 = no burst running.
|
||||
static uint16_t gNoiseTicks;
|
||||
|
||||
// Speech playback state. The fill callback walks a queue of utterances so
|
||||
// a phrase like "HT" ("Hey" + "Taxi") runs as one continuous stream
|
||||
|
|
@ -109,7 +125,9 @@ static const uint8_t *gSpeechPcm; // current utterance's samples
|
|||
static uint16_t gSpeechSample; // next sample index in it
|
||||
static uint16_t gSpeechSamples; // its length
|
||||
static uint8_t gSpeechParity; // it ends on the opposite level
|
||||
static uint8_t gSpeechPol; // 0 / 1: which baked polarity the next utterance plays
|
||||
static uint8_t gSpeechPol; // 0 / 1: the polarity the next utterance plays in
|
||||
static uint8_t gSpeechPolarities; // how many the file carries, 1 or 2
|
||||
static bool gSpeechInvert; // negate on the way out (1-polarity file, odd polarity)
|
||||
// Copied-ahead PCM ring for the stream (see speechFill). Allocated by
|
||||
// stAudioInit rather than static: 8 KB of BSS is a fifth of the IIgs
|
||||
// entry bank, which text, rodata, BSS and the C heap all share.
|
||||
|
|
@ -121,7 +139,7 @@ static bool gSpeechDecoding; // the decoder still has stream left
|
|||
|
||||
|
||||
static uint16_t sidHz(uint16_t word);
|
||||
static void speechLoad(void);
|
||||
static bool speechLoad(void);
|
||||
static bool speechNext(void);
|
||||
static void speechArm(void);
|
||||
static uint16_t speechDecode(uint8_t *dst, uint16_t count);
|
||||
|
|
@ -145,7 +163,7 @@ static uint16_t sidHz(uint16_t word) {
|
|||
// is empty, which ends the stream.
|
||||
// DATA/speech.bin into one jlAlloc block; without it the game is mute
|
||||
// but otherwise fine.
|
||||
static void speechLoad(void) {
|
||||
static bool speechLoad(void) {
|
||||
FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb");
|
||||
uint8_t head[ST_SPEECH_HEADER_BYTES];
|
||||
uint8_t format;
|
||||
|
|
@ -155,22 +173,30 @@ static void speechLoad(void) {
|
|||
|
||||
gSpeechBlob = 0;
|
||||
if (fp == 0) {
|
||||
return;
|
||||
ST_LOG("staudio: ! cannot open DATA/%s", ST_SPEECH_FILE);
|
||||
return false;
|
||||
}
|
||||
format = (jlAudioSfxRawFormat() == JL_SFX_RAW_UNSIGNED8_NOZERO) ? ST_SPEECH_FORMAT_BIASED : ST_SPEECH_FORMAT_SIGNED;
|
||||
if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format) {
|
||||
if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format
|
||||
|| head[9] == 0u || head[9] > ST_SPEECH_POLARITIES_MAX) {
|
||||
ST_LOG("staudio: ! speech.bin rejected (want v%u fmt%u)", ST_SPEECH_VERSION, format);
|
||||
fclose(fp);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
gSpeechPolarities = head[9];
|
||||
gSpeechPitchBase = head[5];
|
||||
gSpeechPitchCount = head[6];
|
||||
gSpeechCount = head[7];
|
||||
dataBytes = (uint32_t)head[12] | ((uint32_t)head[13] << 8) | ((uint32_t)head[14] << 16) | ((uint32_t)head[15] << 24);
|
||||
total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + dataBytes;
|
||||
total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES + dataBytes;
|
||||
gSpeechBlob = (uint8_t *)jlAlloc(total);
|
||||
if (gSpeechBlob == 0) {
|
||||
// The blob is ~120 KB of sample bytes, so on the tighter
|
||||
// machines this is the failure most likely to bite. It stops
|
||||
// the game: a silent Space Taxi is a broken Space Taxi.
|
||||
ST_LOG("staudio: ! speech alloc FAILED (%lu bytes)", (unsigned long)total);
|
||||
fclose(fp);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
memcpy(gSpeechBlob, head, sizeof(head));
|
||||
// The block spans banks on the IIgs; read it in bank-safe pieces.
|
||||
|
|
@ -182,6 +208,7 @@ static void speechLoad(void) {
|
|||
want = ST_SPEECH_LOAD_CHUNK;
|
||||
}
|
||||
if (fread(gSpeechBlob + done, 1u, (size_t)want, fp) != (size_t)want) {
|
||||
ST_LOG("staudio: ! speech read FAILED at %lu of %lu", (unsigned long)done, (unsigned long)total);
|
||||
jlFree(gSpeechBlob);
|
||||
gSpeechBlob = 0;
|
||||
break;
|
||||
|
|
@ -189,6 +216,11 @@ static void speechLoad(void) {
|
|||
done += want;
|
||||
}
|
||||
fclose(fp);
|
||||
if (gSpeechBlob == 0) {
|
||||
return false;
|
||||
}
|
||||
ST_LOG("staudio: speech %lu bytes, %u utterances x %u pitches x %u polarities", (unsigned long)total, gSpeechCount, gSpeechPitchCount, gSpeechPolarities);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -206,19 +238,24 @@ static bool speechNext(void) {
|
|||
}
|
||||
while (gSpeechHead != gSpeechTail) {
|
||||
uint8_t code = gSpeechQueue[gSpeechHead];
|
||||
const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES;
|
||||
const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES;
|
||||
uint8_t k;
|
||||
|
||||
gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE);
|
||||
for (k = 0u; k < gSpeechCount; k++, e += ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES) {
|
||||
for (k = 0u; k < gSpeechCount; k++, e += gSpeechPolarities * ST_SPEECH_ENTRY_BYTES) {
|
||||
if (e[0] == code) {
|
||||
const uint8_t *v = e + (uint16_t)gSpeechPol * ST_SPEECH_ENTRY_BYTES;
|
||||
// Two polarities baked: take the variant. One: take the
|
||||
// only one and negate it on the way out when the running
|
||||
// polarity is the odd one.
|
||||
uint8_t pol = (gSpeechPolarities == 2u) ? gSpeechPol : 0u;
|
||||
const uint8_t *v = e + (uint16_t)pol * ST_SPEECH_ENTRY_BYTES;
|
||||
uint32_t offset = (uint32_t)v[4] | ((uint32_t)v[5] << 8) | ((uint32_t)v[6] << 16) | ((uint32_t)v[7] << 24);
|
||||
|
||||
gSpeechInvert = (gSpeechPolarities == 1u && gSpeechPol != 0u);
|
||||
gSpeechParity = v[1];
|
||||
gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8));
|
||||
gSpeechSample = 0u;
|
||||
gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + offset;
|
||||
gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES + offset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +289,17 @@ static uint16_t speechDecode(uint8_t *dst, uint16_t count) {
|
|||
if (take > left) {
|
||||
take = left;
|
||||
}
|
||||
jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take);
|
||||
if (gSpeechInvert) {
|
||||
const uint8_t *from = gSpeechPcm + gSpeechSample;
|
||||
uint8_t *to = dst + n;
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0u; i < take; i++) {
|
||||
to[i] = (uint8_t)(0u - (uint16_t)from[i]);
|
||||
}
|
||||
} else {
|
||||
jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take);
|
||||
}
|
||||
gSpeechSample = (uint16_t)(gSpeechSample + take);
|
||||
n = (uint16_t)(n + take);
|
||||
}
|
||||
|
|
@ -368,14 +415,27 @@ void stAudioTick(void) {
|
|||
jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF);
|
||||
}
|
||||
}
|
||||
if (gNoiseTicks != 0u) {
|
||||
gNoiseTicks--;
|
||||
if (gNoiseTicks == 0u) {
|
||||
gNoiseOn = false;
|
||||
jlAudioNoise(0u, ST_ATTEN_OFF);
|
||||
} else {
|
||||
// Re-assert: one call buys about 80 ms on the IIgs.
|
||||
jlAudioNoise(ST_NOISE_SFX_PITCH, ST_ATTEN_LOUD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void stAudioInit(void) {
|
||||
bool stAudioInit(void) {
|
||||
bool ok;
|
||||
|
||||
(void)jlAudioInit();
|
||||
gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES);
|
||||
speechLoad();
|
||||
ok = (gSpeechStage != 0) && speechLoad();
|
||||
gSfx.voice = ST_VOICE_SFX;
|
||||
gNoiseTicks = 0u;
|
||||
gSfx.ticksLeft = 0u;
|
||||
gTone.voice = ST_VOICE_TONE;
|
||||
gTone.ticksLeft = 0u;
|
||||
|
|
@ -385,6 +445,8 @@ void stAudioInit(void) {
|
|||
gSpeechActive = false;
|
||||
gSpeechPitch = ST_SPEECH_PITCH_BASE;
|
||||
gSpeechPol = 0u;
|
||||
gSpeechInvert = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -401,16 +463,38 @@ void stAudioShutdown(void) {
|
|||
|
||||
|
||||
// $6D6A / $6A3B -- voice 3 jet noise gate.
|
||||
// $6D6A's voice-3 gate. The C64 holds SID voice 3 open for as long as a
|
||||
// direction is held, so the jet has to keep sounding for the whole burn.
|
||||
//
|
||||
// This re-asserts on EVERY call rather than only on the rising edge,
|
||||
// because jlAudioNoise is a per-frame assertion and not a latch: the
|
||||
// IIgs plays one ~80 ms one-shot per call and expects the caller to keep
|
||||
// asking, exactly as jlMusicPlay's sequencer does for a held noise note
|
||||
// (include/joey/audio.h). Latching it on the edge is why the thruster
|
||||
// used to sound one short burst and then die with the taxi still
|
||||
// climbing. The ports that DO latch take the repeat harmlessly: the ST
|
||||
// rewrites the same YM registers, and Paula's redundant DMA set is
|
||||
// click-free by design.
|
||||
void stAudioNoise(bool on) {
|
||||
if (on == gNoiseOn) {
|
||||
if (on && gNoiseTicks != 0u) {
|
||||
// A sound effect owns the voice until its release runs out, so
|
||||
// the jet does not talk over the crash. The C64 shares SID
|
||||
// voice 3 between them the same way and silences the jet before
|
||||
// firing one. An explicit silence below still wins, which
|
||||
// matters because stAudioMusic clears this voice before a tune.
|
||||
return;
|
||||
}
|
||||
gNoiseOn = on;
|
||||
if (on) {
|
||||
jlAudioNoise(31u, ST_ATTEN_SOFT);
|
||||
} else {
|
||||
jlAudioNoise(0u, ST_ATTEN_OFF);
|
||||
gNoiseOn = true;
|
||||
jlAudioNoise(ST_NOISE_JET_PITCH, ST_ATTEN_SOFT);
|
||||
return;
|
||||
}
|
||||
if (!gNoiseOn && gNoiseTicks == 0u) {
|
||||
return;
|
||||
}
|
||||
gNoiseOn = false;
|
||||
gNoiseTicks = 0u;
|
||||
jlAudioNoise(0u, ST_ATTEN_OFF);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -423,9 +507,17 @@ void stAudioSfx(const uint8_t *program9) {
|
|||
StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx;
|
||||
|
||||
if ((ctrl & 0x80u) != 0u) {
|
||||
// Noise waveform: a burst on the noise generator.
|
||||
jlAudioNoise(31u, ST_ATTEN_LOUD);
|
||||
gNoiseOn = true;
|
||||
// Noise waveform: a burst on the noise generator, held for the
|
||||
// program's release time exactly as the tone slots below hold
|
||||
// theirs. It used to fire one jlAudioNoise and return, which is
|
||||
// not a burst at all: that call is a per-frame ASSERTION, not a
|
||||
// latch (include/joey/audio.h), so on the IIgs the wreck's
|
||||
// explosion lasted the ~80 ms of one one-shot and was easy to
|
||||
// miss entirely, while on Paula it would have hung until some
|
||||
// later stAudioNoise(false). stAudioTick re-asserts it.
|
||||
gNoiseTicks = (uint16_t)(ticks + 1u);
|
||||
gNoiseOn = true;
|
||||
jlAudioNoise(ST_NOISE_SFX_PITCH, ST_ATTEN_LOUD);
|
||||
return;
|
||||
}
|
||||
jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD);
|
||||
|
|
|
|||
25
stCels.c
25
stCels.c
|
|
@ -44,6 +44,31 @@ const StCelDefT kStCels[] = {
|
|||
const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0]));
|
||||
|
||||
|
||||
// Cels the title screen draws that are deliberately NOT pre-compiled.
|
||||
//
|
||||
// Its five decoration sprites are recoloured by logoColorCycle ($4861),
|
||||
// which rewrites sprites 3..7 with the next logo colour every cycle
|
||||
// step, so each is seen in all eight of kLogoColor's values -- forty
|
||||
// combinations. The parked cab's jet is a forty-first, colour 6 rather
|
||||
// than the flame list's 7. None of them are worth a bank slot: on the
|
||||
// 65816 a compile costs about half a second, so baking the forty would
|
||||
// add some twenty seconds to startup, and raising kStCelCount at all is
|
||||
// risky -- 37 made the runtime reject the whole precompiled bank (see
|
||||
// the note in the project memory). They draw interpreted, which
|
||||
// a static title screen can well afford. stRender consults this list so
|
||||
// the mid-ride warning stays about real misses.
|
||||
// A zero colour means "in any colour": the decorations cycle through all
|
||||
// eight. The jet is listed for the cab colour alone, because the flame
|
||||
// list above already carries it in colour 7 for the ride itself.
|
||||
const StCelDefT kStInterpretedCels[] = {
|
||||
{ 0xDB, 0u, 1u, MC0, MC1 }, { 0xDE, 0u, 1u, MC0, MC1 },
|
||||
{ 0xDF, 0u, 1u, MC0, MC1 }, { 0xE0, 0u, 1u, MC0, MC1 },
|
||||
{ 0xE1, 0u, 1u, MC0, MC1 },
|
||||
{ 0xD8, CAB, 1u, MC0, MC1 },
|
||||
};
|
||||
const uint16_t kStInterpretedCount = (uint16_t)(sizeof(kStInterpretedCels) / sizeof(kStInterpretedCels[0]));
|
||||
|
||||
|
||||
void stCelBlob(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1, uint8_t *out) {
|
||||
uint8_t pal[4];
|
||||
uint8_t tx;
|
||||
|
|
|
|||
4
stCels.h
4
stCels.h
|
|
@ -30,6 +30,10 @@ typedef struct {
|
|||
|
||||
extern const StCelDefT kStCels[];
|
||||
extern const uint16_t kStCelCount;
|
||||
// Cels the title screen draws interpreted by design; a zero colour
|
||||
// entry matches any colour. See the note beside the table in stCels.c.
|
||||
extern const StCelDefT kStInterpretedCels[];
|
||||
extern const uint16_t kStInterpretedCount;
|
||||
|
||||
// The cels a level's hook program can show (stLevelCels.c): at most `max`
|
||||
// into `out`, no duplicates. 0 for a level without hook sprites.
|
||||
|
|
|
|||
125
stRender.c
125
stRender.c
|
|
@ -155,6 +155,7 @@ static StSpriteCacheT *cacheFreeSlot(void);
|
|||
static void levelCelsEvict(void);
|
||||
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
|
||||
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total);
|
||||
static bool celDrawsInterpreted(uint8_t ptr, uint8_t color);
|
||||
static uint8_t cellCol(uint16_t cell, uint8_t row);
|
||||
static uint8_t cellRow(uint16_t cell);
|
||||
static void collectGlyphs(StSimT *sim);
|
||||
|
|
@ -166,10 +167,11 @@ static bool loadPrecompiledCels(void);
|
|||
static bool logoBuildTile(const StSimT *sim);
|
||||
static uint8_t logoFrameIndex(const StSimT *sim);
|
||||
static void logoPaletteApply(const StSimT *sim, uint8_t frame);
|
||||
static void starPaletteApply(void);
|
||||
static void paintCells(jlSurfaceT *stage, StSimT *sim);
|
||||
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by);
|
||||
static bool prewarmReport(uint16_t expect);
|
||||
static bool spriteOnScreen(int16_t px, int16_t py);
|
||||
static void starPaletteApply(void);
|
||||
static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
|
||||
|
||||
|
||||
|
|
@ -278,7 +280,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
|
|||
if (idx <= 2u && gPrewarming) {
|
||||
cacheCompile(slot);
|
||||
}
|
||||
if (!gPrewarming) {
|
||||
if (!gPrewarming && !celDrawsInterpreted(ptr, color)) {
|
||||
ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level);
|
||||
}
|
||||
slot->lastUse = gRender.cacheStamp;
|
||||
|
|
@ -313,6 +315,26 @@ static const uint8_t kCellRow[125] = {
|
|||
// Column of a cell whose row is known: cell - row * 40 through a table,
|
||||
// because the two-shift form of the multiply came back as a __mulhi3
|
||||
// library call on the 65816.
|
||||
// True for a cel that is MEANT to be built here rather than pre-listed,
|
||||
// so the warning below stays about real misses. Only the title screen's
|
||||
// decorations qualify: logoColorCycle recolours them every cycle step,
|
||||
// which is forty ptr/colour pairs that would each cost a compile (see
|
||||
// the note beside kStTitleDecorCels in stCels.c).
|
||||
static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) {
|
||||
uint16_t k;
|
||||
|
||||
for (k = 0u; k < kStInterpretedCount; k++) {
|
||||
if (kStInterpretedCels[k].ptr != ptr) {
|
||||
continue;
|
||||
}
|
||||
if (kStInterpretedCels[k].color == 0u || kStInterpretedCels[k].color == color) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static uint8_t cellCol(uint16_t cell, uint8_t row) {
|
||||
static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
|
||||
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
|
||||
|
|
@ -874,21 +896,22 @@ static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total) {
|
|||
// here behind the LOADING bar. Either way nothing compiles once the
|
||||
// level is running. Keyed exactly as cachedSprite keys them, so every
|
||||
// lookup during the ride is a hit.
|
||||
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
||||
bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
||||
static StCelDefT list[ST_LEVEL_CELS_MAX];
|
||||
jlSpriteT *cels[ST_LEVEL_CELS_MAX];
|
||||
char path[24];
|
||||
uint8_t n;
|
||||
uint8_t got;
|
||||
uint8_t ready = 0u;
|
||||
uint8_t k;
|
||||
|
||||
levelCelsEvict();
|
||||
if (level == 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX);
|
||||
if (n == 0u) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
memcpy(path, "sprites/level", 13u);
|
||||
path[13] = (char)('0' + (level->levelIndex + 1u) / 10u);
|
||||
|
|
@ -938,11 +961,17 @@ void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
|||
slot->used = true;
|
||||
slot->pinned = true;
|
||||
slot->lastUse = gRender.cacheStamp;
|
||||
ready++;
|
||||
}
|
||||
gPrewarming = false;
|
||||
if (got != n) {
|
||||
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
|
||||
}
|
||||
if (ready < n) {
|
||||
ST_LOG("spacetaxi: ! level %u has %u of %u cels", level->levelIndex, ready, n);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -976,11 +1005,54 @@ static bool loadPrecompiledCels(void) {
|
|||
e->used = true;
|
||||
e->pinned = true;
|
||||
e->lastUse = 0u;
|
||||
// The bank's cels arrive compiled ONLY if the arena had room for
|
||||
// them: jlSpriteBankLoadPrecompiled loads a cel either way and
|
||||
// leaves the ones that did not fit to the interpreter. So ask
|
||||
// the sprite instead of assuming, or the flag lies and any
|
||||
// future cacheCompile skips a cel that really does need work.
|
||||
e->compiled = (jlSpriteCompiledSize(e->sprite) != 0u);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// A fatal message, centred, in the game's own charset. Used when an
|
||||
// asset that IS on the disk will not load: the game then stops instead
|
||||
// of playing on in a degraded state, because a mute game or one running
|
||||
// every sprite through the interpreter looks like a slow machine rather
|
||||
// than a broken install.
|
||||
void stRenderFatal(jlSurfaceT *stage, const char *line1, const char *line2) {
|
||||
const uint8_t *charset = stC64Charset();
|
||||
const char *lines[2];
|
||||
uint8_t i;
|
||||
uint8_t k;
|
||||
|
||||
if (stage == 0) {
|
||||
return;
|
||||
}
|
||||
lines[0] = line1;
|
||||
lines[1] = line2;
|
||||
jlSurfaceClear(stage, 0u);
|
||||
for (i = 0u; i < 2u; i++) {
|
||||
uint8_t len;
|
||||
uint8_t col;
|
||||
|
||||
if (lines[i] == 0) {
|
||||
continue;
|
||||
}
|
||||
len = 0u;
|
||||
while (lines[i][len] != '\0' && len < ST_SCREEN_COLS) {
|
||||
len++;
|
||||
}
|
||||
col = (uint8_t)((ST_SCREEN_COLS - len) / 2u);
|
||||
for (k = 0u; k < len; k++) {
|
||||
jlTilePasteGlyph(stage, (uint8_t)(col + k), (uint8_t)(ST_LOADING_ROW + i * 2u),
|
||||
&charset[(uint8_t)lines[i][k] * 8u], ST_LOADING_COLOR, 0u);
|
||||
}
|
||||
}
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
// "LOADING..." centred on an otherwise black stage. The charset is the
|
||||
// game's own, indexed by ASCII (the C64 font's letters sit at their
|
||||
|
|
@ -999,12 +1071,13 @@ void stRenderLoading(jlSurfaceT *stage) {
|
|||
jlStagePresent();
|
||||
}
|
||||
|
||||
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
||||
bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
||||
static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 };
|
||||
static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 };
|
||||
static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 };
|
||||
uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u);
|
||||
uint8_t done = 0u;
|
||||
bool ok;
|
||||
uint8_t k;
|
||||
StFrameT saved;
|
||||
|
||||
|
|
@ -1012,7 +1085,7 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
|||
// runtime build below on any port without a matching .spc.
|
||||
if (loadPrecompiledCels()) {
|
||||
ST_LOG("spacetaxi: cels from sprites/staxicels.spc");
|
||||
return;
|
||||
return prewarmReport(kStCelCount);
|
||||
}
|
||||
ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm");
|
||||
saved = sim->frame;
|
||||
|
|
@ -1050,7 +1123,45 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
|||
(void)cachedSprite(sim, 2u);
|
||||
sim->frame = saved;
|
||||
gPrewarming = false;
|
||||
ok = prewarmReport(total);
|
||||
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
// Say how the prewarmed cels came out, and judge them. A cel past the end
|
||||
// of the codegen arena still DRAWS, interpreted, so a port whose arena
|
||||
// holds only some of the cels is slower but sound -- that is the ST at
|
||||
// 2 MB, 12 of 37 compiled, and it is not an error. Zero compiled is:
|
||||
// it means the arena itself never came, which on a 1 MB ST is the speech
|
||||
// blob having taken the heap, and every sprite then runs about forty
|
||||
// times slower. The caller stops on a false rather than let that pass
|
||||
// for a slow machine.
|
||||
static bool prewarmReport(uint16_t expect) {
|
||||
uint32_t bytes = 0u;
|
||||
uint8_t used = 0u;
|
||||
uint8_t hot = 0u;
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
|
||||
uint32_t n;
|
||||
|
||||
if (!gRender.cache[k].used) {
|
||||
continue;
|
||||
}
|
||||
n = jlSpriteCompiledSize(gRender.cache[k].sprite);
|
||||
used++;
|
||||
if (n != 0u) {
|
||||
hot++;
|
||||
bytes += n;
|
||||
}
|
||||
}
|
||||
ST_LOG("spacetaxi: prewarm %u cels: %u compiled, %u interpreted, %lu arena bytes", used, hot, (uint8_t)(used - hot), (unsigned long)bytes);
|
||||
if (used < expect) {
|
||||
ST_LOG("spacetaxi: ! only %u of %u cels built", used, expect);
|
||||
return false;
|
||||
}
|
||||
return hot != 0u;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ int main(void) {
|
|||
memset(&gGame, 0, sizeof(gGame));
|
||||
memset(&gSim, 0, sizeof(gSim));
|
||||
stRenderInit(stage);
|
||||
stAudioInit();
|
||||
(void)stAudioInit();
|
||||
stSimNewGame(&gSim, 2u, false);
|
||||
|
||||
// ---- ranking ($4D92) ----
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue