From f80c9cca246b8395b7a385603245489616374137 Mon Sep 17 00:00:00 2001 From: "casriel." <232106036+casrielasriel@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:44:05 -0700 Subject: [PATCH 1/3] feat: add Sequences ## Summary Add a sequence system Parse the SEQN chunk Implement eleven core sequence functions Tested in Undertale Yellow and TS! Underswap --- src/data_win.c | 284 ++++++++++++++-- src/data_win.h | 98 ++++++ src/instance.h | 1 + src/runner.c | 106 ++++-- src/runner.h | 7 + src/sequence.c | 849 ++++++++++++++++++++++++++++++++++++++++++++++ src/sequence.h | 40 +++ src/text_utils.h | 2 +- src/vm_builtins.c | 159 +++++---- 9 files changed, 1419 insertions(+), 127 deletions(-) create mode 100644 src/sequence.c create mode 100644 src/sequence.h diff --git a/src/data_win.c b/src/data_win.c index f1509ab1c..8c5080ea9 100644 --- a/src/data_win.c +++ b/src/data_win.c @@ -1106,6 +1106,218 @@ static void parseSCPT(BinaryReader* reader, DataWin* dw) { free(ptrs); } +static void readAnimCurve(BinaryReader* reader, DataWin* dw, AnimCurve* curve, bool named) { + curve->present = true; + curve->name = named ? readStringPtr(reader, dw) : nullptr; + curve->graphType = BinaryReader_readUint32(reader); + curve->channelCount = BinaryReader_readUint32(reader); + curve->channels = (AnimCurveChannel*)safeCalloc(curve->channelCount, sizeof(AnimCurveChannel)); + repeat(curve->channelCount, c) { + AnimCurveChannel* ch = &curve->channels[c]; + ch->name = readStringPtr(reader, dw); + ch->curveType = (AnimCurveType)BinaryReader_readUint32(reader); + ch->iterations = BinaryReader_readUint32(reader); + ch->pointCount = BinaryReader_readUint32(reader); + ch->points = (AnimCurvePoint*)safeCalloc(ch->pointCount, sizeof(AnimCurvePoint)); + repeat(ch->pointCount, p) { + AnimCurvePoint* pt = &ch->points[p]; + pt->x = BinaryReader_readFloat32(reader); + pt->value = BinaryReader_readFloat32(reader); + if (DataWin_isVersionAtLeast(dw, 2, 3, 1, 0)) { + pt->bezierX0 = BinaryReader_readFloat32(reader); + pt->bezierY0 = BinaryReader_readFloat32(reader); + pt->bezierX1 = BinaryReader_readFloat32(reader); + pt->bezierY1 = BinaryReader_readFloat32(reader); + } else { + BinaryReader_skip(reader, 4); + } + } + } +} + +static void freeAnimCurve(AnimCurve* curve) { + repeat(curve->channelCount, c) free(curve->channels[c].points); + free(curve->channels); +} + +static void readSequenceKeys(BinaryReader* reader, DataWin* dw, SequenceTrackType type, uint32_t* count, SequenceKeyframe** keys) { + BinaryReader_seek(reader, (BinaryReader_getPosition(reader) + 3u) & ~(size_t)3u); + *count = BinaryReader_readUint32(reader); + *keys = (SequenceKeyframe*)safeCalloc(*count, sizeof(SequenceKeyframe)); + repeat(*count, k) { + SequenceKeyframe* key = &(*keys)[k]; + key->key = BinaryReader_readFloat32(reader); + key->length = BinaryReader_readFloat32(reader); + key->stretch = BinaryReader_readBool32(reader); + key->disabled = BinaryReader_readBool32(reader); + key->channelCount = BinaryReader_readUint32(reader); + key->channels = (SequenceChannel*)safeCalloc(key->channelCount, sizeof(SequenceChannel)); + repeat(key->channelCount, c) { + SequenceChannel* channel = &key->channels[c]; + channel->id = BinaryReader_readInt32(reader); + channel->curveIndex = -1; + switch (type) { + case SEQUENCE_TRACK_REAL: + case SEQUENCE_TRACK_COLOR: { + if (type == SEQUENCE_TRACK_COLOR) channel->integer = BinaryReader_readInt32(reader); + else channel->real = BinaryReader_readFloat32(reader); + bool embedded = BinaryReader_readBool32(reader); + channel->curveIndex = BinaryReader_readInt32(reader); + if (embedded) { + channel->embeddedCurve = (AnimCurve*)safeCalloc(1, sizeof(AnimCurve)); + readAnimCurve(reader, dw, channel->embeddedCurve, false); + } + break; + } + case SEQUENCE_TRACK_MESSAGE: + case SEQUENCE_TRACK_MOMENT: + channel->messageCount = BinaryReader_readUint32(reader); + channel->messages = (const char**)safeCalloc(channel->messageCount, sizeof(const char*)); + repeat(channel->messageCount, m) channel->messages[m] = readStringPtr(reader, dw); + break; + case SEQUENCE_TRACK_TEXT: + channel->text = readStringPtr(reader, dw); + channel->wrap = BinaryReader_readBool32(reader); + channel->alignment = BinaryReader_readInt32(reader); + channel->fontIndex = BinaryReader_readInt32(reader); + if (DataWin_isVersionAtLeast(dw, 2024, 14, 0, 0)) BinaryReader_skip(reader, 8); + break; + case SEQUENCE_TRACK_STRING: + channel->text = readStringPtr(reader, dw); + break; + default: + channel->integer = BinaryReader_readInt32(reader); + if (type == SEQUENCE_TRACK_AUDIO) { + BinaryReader_skip(reader, 4); + channel->audioMode = BinaryReader_readInt32(reader); + } + break; + } + } + } +} + +static bool readSequenceTrack(BinaryReader* reader, DataWin* dw, SequenceTrack* track) { + const char* model = readStringPtr(reader, dw); + const char* models[] = { + "GMGroupTrack", "GMGraphicTrack", "GMInstanceTrack", "GMSequenceTrack", + "GMAudioTrack", "GMTextTrack", "GMRealTrack", "GMColourTrack", + "GMBoolTrack", "GMStringTrack", "GMSpriteFramesTrack" + }; + bool found = false; + repeat(sizeof(models) / sizeof(models[0]), t) { + if (strcmp(model, models[t]) == 0) { + track->type = (SequenceTrackType)t; + found = true; + break; + } + } + if (!found) { + logWarn("Unsupported sequence track type: %s\n", model); + return false; + } + track->name = readStringPtr(reader, dw); + track->builtinName = BinaryReader_readUint32(reader); + track->traits = BinaryReader_readUint32(reader); + track->creationTrack = BinaryReader_readBool32(reader); + track->enabled = true; + track->visible = true; + uint32_t tagCount = BinaryReader_readUint32(reader); + track->curveCount = BinaryReader_readUint32(reader); + track->trackCount = BinaryReader_readUint32(reader); + repeat(tagCount, t) { + int32_t tag = BinaryReader_readInt32(reader); + if (tag == 1) track->visible = false; + else if (tag == 2) track->enabled = false; + } + track->curves = (AnimCurve*)safeCalloc(track->curveCount, sizeof(AnimCurve)); + repeat(track->curveCount, c) { + const char* resourceType = readStringPtr(reader, dw); + requireMessageFormatted(__FILE__, __LINE__, strcmp(resourceType, "GMAnimCurve") == 0, "Unsupported sequence resource: %s", resourceType); + readAnimCurve(reader, dw, &track->curves[c], true); + } + track->tracks = (SequenceTrack*)safeCalloc(track->trackCount, sizeof(SequenceTrack)); + repeat(track->trackCount, t) { + if (!readSequenceTrack(reader, dw, &track->tracks[t])) return false; + } + if (track->type == SEQUENCE_TRACK_GROUP) return true; + if (track->type == SEQUENCE_TRACK_REAL || track->type == SEQUENCE_TRACK_COLOR) { + BinaryReader_seek(reader, (BinaryReader_getPosition(reader) + 3u) & ~(size_t)3u); + track->interpolation = BinaryReader_readInt32(reader); + } + readSequenceKeys(reader, dw, track->type, &track->keyframeCount, &track->keyframes); + return true; +} + +static void freeSequenceKeys(uint32_t count, SequenceKeyframe* keys) { + repeat(count, k) { + repeat(keys[k].channelCount, c) { + SequenceChannel* channel = &keys[k].channels[c]; + if (channel->embeddedCurve != nullptr) { + freeAnimCurve(channel->embeddedCurve); + free(channel->embeddedCurve); + } + free(channel->messages); + } + free(keys[k].channels); + } + free(keys); +} + +static void freeSequenceTrack(SequenceTrack* track) { + repeat(track->trackCount, t) freeSequenceTrack(&track->tracks[t]); + repeat(track->curveCount, c) freeAnimCurve(&track->curves[c]); + freeSequenceKeys(track->keyframeCount, track->keyframes); + free(track->curves); + free(track->tracks); +} + +static void parseSEQN(BinaryReader* reader, DataWin* dw) { + BinaryReader_seek(reader, (BinaryReader_getPosition(reader) + 3u) & ~(size_t)3u); + uint32_t version = BinaryReader_readUint32(reader); + requireMessageFormatted(__FILE__, __LINE__, version == 1, "Unsupported SEQN version %u", version); + uint32_t* pointers = readPointerTable(reader, &dw->seqn.count); + dw->seqn.sequences = (Sequence*)safeCalloc(dw->seqn.count, sizeof(Sequence)); + repeat(dw->seqn.count, s) { + if (pointers[s] == 0) continue; + BinaryReader_seek(reader, pointers[s]); + Sequence* seq = &dw->seqn.sequences[s]; + seq->present = true; + seq->name = readStringPtr(reader, dw); + seq->playback = BinaryReader_readUint32(reader); + seq->playbackSpeed = BinaryReader_readFloat32(reader); + seq->playbackSpeedType = BinaryReader_readUint32(reader); + seq->length = BinaryReader_readFloat32(reader); + seq->originX = BinaryReader_readInt32(reader); + seq->originY = BinaryReader_readInt32(reader); + seq->volume = BinaryReader_readFloat32(reader); + if (DataWin_isVersionAtLeast(dw, 2024, 13, 0, 0)) BinaryReader_skip(reader, 8); + readSequenceKeys(reader, dw, SEQUENCE_TRACK_MESSAGE, &seq->messageCount, &seq->messages); + seq->trackCount = BinaryReader_readUint32(reader); + seq->tracks = (SequenceTrack*)safeCalloc(seq->trackCount, sizeof(SequenceTrack)); + bool supported = true; + repeat(seq->trackCount, t) { + if (!readSequenceTrack(reader, dw, &seq->tracks[t])) { + supported = false; + break; + } + } + if (!supported) { + logWarn("Sequence %s cannot be played because it contains an unsupported track\n", seq->name); + seq->present = false; + continue; + } + seq->functionCount = BinaryReader_readUint32(reader); + seq->functions = (SequenceFunction*)safeCalloc(seq->functionCount, sizeof(SequenceFunction)); + repeat(seq->functionCount, f) { + seq->functions[f].id = BinaryReader_readInt32(reader); + seq->functions[f].name = readStringPtr(reader, dw); + } + readSequenceKeys(reader, dw, SEQUENCE_TRACK_MOMENT, &seq->momentCount, &seq->moments); + } + free(pointers); +} + static void parseACRV(BinaryReader* reader, DataWin* dw) { Acrv* a = &dw->acrv; @@ -1145,9 +1357,6 @@ static void parseACRV(BinaryReader* reader, DataWin* dw) { a->count = count; if (count == 0) { free(ptrs); a->curves = nullptr; return; } - // Whether the per-point format includes the 4 trailing float bezier handles (24 bytes) instead of the 2.3.0 layout (12 bytes, just X/Value + 4 pad). - bool isV231Plus = DataWin_isVersionAtLeast(dw, 2, 3, 1, 0); - a->curves = (AnimCurve *)safeCalloc(count, sizeof(AnimCurve)); uint32_t globalChannelCount = 0; @@ -1155,32 +1364,9 @@ static void parseACRV(BinaryReader* reader, DataWin* dw) { if (ptrs[i] == 0) continue; BinaryReader_seek(reader, ptrs[i]); AnimCurve* cur = &a->curves[i]; - cur->present = true; - cur->name = readStringPtr(reader, dw); - cur->graphType = BinaryReader_readUint32(reader); - cur->channelCount = BinaryReader_readUint32(reader); - cur->channels = (cur->channelCount > 0) ? (AnimCurveChannel *)safeCalloc(cur->channelCount, sizeof(AnimCurveChannel)) : nullptr; + readAnimCurve(reader, dw, cur, true); repeat(cur->channelCount, c) { AnimCurveChannel* ch = &cur->channels[c]; - ch->name = readStringPtr(reader, dw); - ch->curveType = (AnimCurveType) BinaryReader_readUint32(reader); - ch->iterations = BinaryReader_readUint32(reader); - ch->pointCount = BinaryReader_readUint32(reader); - ch->points = (ch->pointCount > 0) ? (AnimCurvePoint *)safeMalloc(ch->pointCount * sizeof(AnimCurvePoint)) : nullptr; - repeat(ch->pointCount, p) { - AnimCurvePoint* pt = &ch->points[p]; - pt->x = BinaryReader_readFloat32(reader); - pt->value = BinaryReader_readFloat32(reader); - if (isV231Plus) { - pt->bezierX0 = BinaryReader_readFloat32(reader); - pt->bezierY0 = BinaryReader_readFloat32(reader); - pt->bezierX1 = BinaryReader_readFloat32(reader); - pt->bezierY1 = BinaryReader_readFloat32(reader); - } else { - BinaryReader_readUint32(reader); // padding - pt->bezierX0 = pt->bezierY0 = pt->bezierX1 = pt->bezierY1 = 0.0f; - } - } ch->globalId = (int32_t) globalChannelCount; globalChannelCount++; } @@ -1800,6 +1986,9 @@ static void readRoomLayers(BinaryReader* reader, DataWin* dw, Room* room) { RoomLayerAssetsData* assets = (RoomLayerAssetsData *)safeMalloc(sizeof(RoomLayerAssetsData)); uint32_t legacyTilesPtr = BinaryReader_readUint32(reader); uint32_t spritesPtr = BinaryReader_readUint32(reader); + uint32_t sequencesPtr = DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) ? BinaryReader_readUint32(reader) : 0; + assets->sequenceCount = 0; + assets->sequences = nullptr; BinaryReader_seek(reader, legacyTilesPtr); uint32_t *innerTilePtrs = readPointerTable(reader, &assets->legacyTileCount); @@ -1852,6 +2041,28 @@ static void readRoomLayers(BinaryReader* reader, DataWin* dw, Room* room) { } free(spritePtrs); + if (sequencesPtr != 0) { + BinaryReader_seek(reader, sequencesPtr); + uint32_t* sequencePtrs = readPointerTable(reader, &assets->sequenceCount); + assets->sequences = (SequenceInstance*)safeCalloc(assets->sequenceCount, sizeof(SequenceInstance)); + repeat(assets->sequenceCount, k) { + BinaryReader_seek(reader, sequencePtrs[k]); + SequenceInstance* seq = &assets->sequences[k]; + seq->name = readStringPtr(reader, dw); + seq->sequenceIndex = BinaryReader_readInt32(reader); + seq->x = BinaryReader_readInt32(reader); + seq->y = BinaryReader_readInt32(reader); + seq->scaleX = BinaryReader_readFloat32(reader); + seq->scaleY = BinaryReader_readFloat32(reader); + seq->color = BinaryReader_readUint32(reader); + seq->animationSpeed = BinaryReader_readFloat32(reader); + seq->animationSpeedType = BinaryReader_readUint32(reader); + seq->frameIndex = BinaryReader_readFloat32(reader); + seq->rotation = BinaryReader_readFloat32(reader); + } + free(sequencePtrs); + } + layer->assetsData = assets; break; } @@ -3061,6 +3272,7 @@ DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) { } else if (memcmp(chunkName, "SEQN", 4) == 0) { // Sequences chunk (GMS 2.3+) DataWin_bumpVersionTo(dw, 2, 3, 0, 0); + parseSEQN(&reader, dw); } else if (memcmp(chunkName, "TAGS", 4) == 0) { // Tags chunk (GMS 2.3+) DataWin_bumpVersionTo(dw, 2, 3, 0, 0); @@ -3313,17 +3525,20 @@ void DataWin_free(DataWin* dw) { // ACRV if (dw->acrv.curves) { repeat(dw->acrv.count, i) { - AnimCurve* cur = &dw->acrv.curves[i]; - if (cur->channels) { - repeat(cur->channelCount, c) { - free(cur->channels[c].points); - } - free(cur->channels); - } + freeAnimCurve(&dw->acrv.curves[i]); } free(dw->acrv.curves); } free(dw->acrv.allChannels); + repeat(dw->seqn.count, s) { + Sequence* seq = &dw->seqn.sequences[s]; + repeat(seq->trackCount, t) freeSequenceTrack(&seq->tracks[t]); + freeSequenceKeys(seq->messageCount, seq->messages); + freeSequenceKeys(seq->momentCount, seq->moments); + free(seq->functions); + free(seq->tracks); + } + free(dw->seqn.sequences); // ROOM if (dw->room.rooms) { @@ -3410,6 +3625,7 @@ void DataWin_freeRoomPayload(Room* room) { if (layer->assetsData) { free(layer->assetsData->legacyTiles); free(layer->assetsData->sprites); + free(layer->assetsData->sequences); free(layer->assetsData); } if (layer->backgroundData) free(layer->backgroundData); diff --git a/src/data_win.h b/src/data_win.h index c6c07f67c..2ebe957da 100644 --- a/src/data_win.h +++ b/src/data_win.h @@ -631,11 +631,25 @@ typedef struct { float rotation; } SpriteInstance; +typedef struct { + const char* name; + int32_t sequenceIndex; + int32_t x, y; + float scaleX, scaleY; + uint32_t color; + float animationSpeed; + uint32_t animationSpeedType; + float frameIndex; + float rotation; +} SequenceInstance; + typedef struct { uint32_t legacyTileCount; RoomTile *legacyTiles; uint32_t spriteCount; SpriteInstance *sprites; + uint32_t sequenceCount; + SequenceInstance* sequences; } RoomLayerAssetsData; typedef struct { @@ -782,6 +796,89 @@ typedef struct { AnimCurveChannel** allChannels; } Acrv; +typedef enum { + SEQUENCE_TRACK_GROUP, + SEQUENCE_TRACK_GRAPHIC, + SEQUENCE_TRACK_INSTANCE, + SEQUENCE_TRACK_SEQUENCE, + SEQUENCE_TRACK_AUDIO, + SEQUENCE_TRACK_TEXT, + SEQUENCE_TRACK_REAL, + SEQUENCE_TRACK_COLOR, + SEQUENCE_TRACK_BOOL, + SEQUENCE_TRACK_STRING, + SEQUENCE_TRACK_SPRITE_FRAMES, + SEQUENCE_TRACK_MESSAGE, + SEQUENCE_TRACK_MOMENT +} SequenceTrackType; + +typedef struct { + int32_t id; + float real; + int32_t integer; + int32_t curveIndex; + AnimCurve* embeddedCurve; + const char* text; + uint32_t messageCount; + const char** messages; + bool wrap; + int32_t alignment; + int32_t fontIndex; + int32_t audioMode; +} SequenceChannel; + +typedef struct { + float key, length; + bool stretch, disabled; + uint32_t channelCount; + SequenceChannel* channels; +} SequenceKeyframe; + +typedef struct SequenceTrack { + const char* name; + SequenceTrackType type; + uint32_t builtinName; + uint32_t traits; + bool creationTrack; + bool enabled, visible; + uint32_t trackCount; + struct SequenceTrack* tracks; + uint32_t curveCount; + AnimCurve* curves; + int32_t interpolation; + uint32_t keyframeCount; + SequenceKeyframe* keyframes; +} SequenceTrack; + +typedef struct { + int32_t id; + const char* name; +} SequenceFunction; + +typedef struct { + bool present; + const char* name; + uint32_t playback; + float playbackSpeed; + uint32_t playbackSpeedType; + float length; + int32_t originX, originY; + float volume; + uint32_t trackCount; + SequenceTrack* tracks; + uint32_t messageCount; + SequenceKeyframe* messages; + uint32_t momentCount; + SequenceKeyframe* moments; + uint32_t functionCount; + SequenceFunction* functions; +} Sequence; + +typedef struct { + uint32_t count; + Sequence* sequences; +} Seqn; + // ===[ TPAG - Texture Page Items ]=== typedef struct { bool present; @@ -943,6 +1040,7 @@ struct DataWin { // DAFL is empty, no field needed Tgin tgin; Acrv acrv; + Seqn seqn; Tpag tpag; Code code; Vari vari; diff --git a/src/instance.h b/src/instance.h index 513c4af9b..fa4edd913 100644 --- a/src/instance.h +++ b/src/instance.h @@ -39,6 +39,7 @@ struct Instance { // Used to track which alarms are set without looping through the entire alarm array uint16_t activeAlarmMask; bool cleanupEventFired; + bool sequenceControlled; int32_t maskIndex; // collision mask sprite override (-1 = use spriteIndex) int32_t* collisionCells; // Used to track where we are uint32_t lastCollisionQueryId; diff --git a/src/runner.c b/src/runner.c index 0bf7c8d46..dce0d3971 100644 --- a/src/runner.c +++ b/src/runner.c @@ -51,6 +51,11 @@ void Runner_freeRuntimeLayer(RuntimeLayer* runtimeLayer) { free(el->spriteElement); el->spriteElement = nullptr; } + if (el->sequenceElement != nullptr) { + el->sequenceElement->destroyed = true; + Sequence_release(el->sequenceElement); + el->sequenceElement = nullptr; + } if (el->tileElement != nullptr && el->tileElementOwned) { free(el->tileElement); el->tileElement = nullptr; @@ -912,7 +917,7 @@ static void rebuildDrawableCacheIfDirty(Runner* runner) { } } -static void drawInstanceNormally(Runner* runner, Instance* inst) { +void Runner_drawInstance(Runner* runner, Instance* inst) { if (runner->drawEnabled) { int32_t ownerObjectIndex = -1; int32_t codeId = findEventCodeIdAndOwner(runner, inst->objectIndex, EVENT_DRAW, DRAW_NORMAL, &ownerObjectIndex); @@ -1029,10 +1034,10 @@ void Runner_draw(Runner* runner) { Instance* inst = d->instance; if (d->layerOrder != 0) continue; // Filter inactive/invisible instances at draw time so the cache doesn't need invalidation when those flags toggle. - if (!inst->active || !inst->visible) continue; + if (!inst->active || !inst->visible || inst->sequenceControlled) continue; int32_t previousShader = Runner_pushLayerShader(runner, inst->layer); - drawInstanceNormally(runner, inst); + Runner_drawInstance(runner, inst); Runner_popLayerShader(runner, previousShader); } else if (d->type == DRAWABLE_PARTICLE_SYSTEM) { // Filtered at draw time, like instance visibility: part_system_automatic_draw can be @@ -1076,8 +1081,8 @@ void Runner_draw(Runner* runner) { RuntimeLayerElement* el = &runtimeLayer->elements[j]; if (el->type != RuntimeLayerElementType_Instance) continue; Instance* inst = hmget(runner->instancesById, el->instanceId); - if (inst == nullptr || !inst->active || !inst->visible || inst->destroyed) continue; - drawInstanceNormally(runner, inst); + if (inst == nullptr || !inst->active || !inst->visible || inst->destroyed || inst->sequenceControlled) continue; + Runner_drawInstance(runner, inst); } runtimeLayer = Runner_findRuntimeLayerById(runner, d->runtimeLayerId); if (runtimeLayer == nullptr) { @@ -1091,8 +1096,12 @@ void Runner_draw(Runner* runner) { float roomH = (float) runner->currentRoom->height; size_t elementCount = arrlenu(runtimeLayer->elements); repeat(elementCount, j) { + runtimeLayer = Runner_findRuntimeLayerById(runner, d->runtimeLayerId); + if (runtimeLayer == nullptr || (size_t)j >= arrlenu(runtimeLayer->elements)) break; RuntimeLayerElement* layerElement = &runtimeLayer->elements[j]; - if (layerElement->type == RuntimeLayerElementType_Background && layerElement->backgroundElement != nullptr) { + if (layerElement->type == RuntimeLayerElementType_Sequence) { + if (layerElement->visible) Sequence_draw(layerElement->sequenceElement); + } else if (layerElement->type == RuntimeLayerElementType_Background && layerElement->backgroundElement != nullptr) { renderBackgroundElement(runner, layerElement->backgroundElement, roomW, roomH, layerOffsetX, layerOffsetY); } else if (layerElement->type == RuntimeLayerElementType_Tile && layerElement->tileElement != nullptr && layerElement->tileElementOwned) { if (!layerElement->visible) continue; @@ -1769,6 +1778,20 @@ static void initRoom(Runner* runner, int32_t roomIndex) { if (layerSource->type != RoomLayerType_Assets || layerSource->assetsData == nullptr) continue; RoomLayerAssetsData* assets = layerSource->assetsData; RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; + repeat(assets->sequenceCount, j) { + SequenceInstance* src = &assets->sequences[j]; + int32_t id = Sequence_create(runner, (int32_t)layerSource->id, src->sequenceIndex, (float)src->x, (float)src->y); + RuntimeSequence* seq = Sequence_find(runner, id); + if (seq != nullptr) { + seq->scaleX = src->scaleX; + seq->scaleY = src->scaleY; + seq->angle = src->rotation; + seq->headPosition = src->frameIndex; + seq->speedScale = src->animationSpeed; + seq->blend = src->color & 0xFFFFFFu; + seq->alpha = (float)BGR_A(src->color) / 255.0f; + } + } repeat(assets->spriteCount, j) { SpriteInstance* src = &assets->sprites[j]; RuntimeSpriteElement* spriteElement = (RuntimeSpriteElement *)safeMalloc(sizeof(RuntimeSpriteElement)); @@ -2650,6 +2673,13 @@ Runner* Runner_create(DataWin* dataWin, VMContext* vm, Renderer* renderer, FileS } } + { + repeat(dataWin->seqn.count, i) { + if (!dataWin->seqn.sequences[i].present) continue; + shput(runner->assetsByName, dataWin->seqn.sequences[i].name, i); + } + } + { repeat(shlen(vm->builtinMap), i) { bool isRegistered = shgeti(vm->codeIndexByName, vm->builtinMap[i].key) != -1; @@ -4349,34 +4379,41 @@ static void queueSpriteMessages(Runner* runner, PendingSpriteMessage** pending, queueSpriteMessageRange(pending, sprite, elementId, start, end); } +void Runner_broadcastMessage(Runner* runner, const char* type, const char* message, int32_t elementId) { + DsMapEntry* map = nullptr; + int32_t mapId = -1; + repeat(arrlenu(runner->dsMapPool), j) { + if (runner->dsMapPool[j] == nullptr) { + mapId = (int32_t)j; + break; + } + } + if (mapId < 0) { + arrput(runner->dsMapPool, map); + mapId = (int32_t)arrlen(runner->dsMapPool) - 1; + } + DsMapEntry** mapPtr = &runner->dsMapPool[mapId]; + shput(*mapPtr, safeStrdup("event_type"), RValue_makeOwnedString(safeStrdup(type))); + shput(*mapPtr, safeStrdup("element_id"), RValue_makeReal((GMLReal)elementId)); + shput(*mapPtr, safeStrdup("message"), RValue_makeOwnedString(safeStrdup(message))); + int32_t previousMapId = runner->eventDataMapId; + runner->eventDataMapId = mapId; + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_BROADCAST_MESSAGE); + runner->eventDataMapId = previousMapId; + mapPtr = &runner->dsMapPool[mapId]; + if (*mapPtr != nullptr) { + repeat(shlen(*mapPtr), j) { + free((*mapPtr)[j].key); + RValue_free(&(*mapPtr)[j].value); + } + shfree(*mapPtr); + *mapPtr = nullptr; + } +} + static void dispatchSpriteMessages(Runner* runner, PendingSpriteMessage* pending) { repeat(arrlenu(pending), i) { - DsMapEntry* map = nullptr; - int32_t mapId = -1; - repeat(arrlenu(runner->dsMapPool), j) { - if (runner->dsMapPool[j] == nullptr) { mapId = (int32_t) j; break; } - } - if (mapId < 0) { - arrput(runner->dsMapPool, map); - mapId = (int32_t) arrlen(runner->dsMapPool) - 1; - } - DsMapEntry** mapPtr = &runner->dsMapPool[mapId]; - shput(*mapPtr, safeStrdup("event_type"), RValue_makeOwnedString(safeStrdup("sprite event"))); - shput(*mapPtr, safeStrdup("element_id"), RValue_makeReal((GMLReal) pending[i].elementId)); - shput(*mapPtr, safeStrdup("message"), RValue_makeOwnedString(safeStrdup(pending[i].message))); - int32_t previousMapId = runner->eventDataMapId; - runner->eventDataMapId = mapId; - Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_BROADCAST_MESSAGE); - runner->eventDataMapId = previousMapId; - mapPtr = &runner->dsMapPool[mapId]; - if (*mapPtr != nullptr) { - repeat(shlen(*mapPtr), j) { - free((*mapPtr)[j].key); - RValue_free(&(*mapPtr)[j].value); - } - shfree(*mapPtr); - *mapPtr = nullptr; - } + Runner_broadcastMessage(runner, "sprite event", pending[i].message, pending[i].elementId); } } @@ -4601,6 +4638,8 @@ void Runner_step(Runner* runner) { dispatchMouseEvents(runner); + Sequence_stepRoom(runner); + // Execute Normal Step for all instances Runner_executeEventForAll(runner, EVENT_STEP, STEP_NORMAL); @@ -4613,6 +4652,7 @@ void Runner_step(Runner* runner) { repeat(motionCount, mi) { Instance* inst = runner->instances[mi]; if (!inst->active) continue; + if (inst->sequenceControlled) continue; if (runner->physics) { if (inst->pathIndex >= 0 && (!inst->physicsBody || !PhysicsEngine_variable(inst->physicsBody, PHY_DYNAMIC, 0, 0, (float)Runner_getEffectiveGameSpeed(runner)))) { if (adaptPath(runner, inst)) Runner_executeEvent(runner, inst, EVENT_OTHER, OTHER_END_OF_PATH); @@ -5285,6 +5325,8 @@ void Runner_free(Runner* runner) { arrfree(runner->cachedDrawables); runner->cachedDrawables = nullptr; arrfree(runner->instanceSnapshots); + arrfree(runner->sequenceSnapshots); + arrfree(runner->sequenceEvents); runner->instanceSnapshots = nullptr; arrfree(runner->eventDispatchInstances); runner->eventDispatchInstances = nullptr; diff --git a/src/runner.h b/src/runner.h index ba5dd3dcd..d85955c05 100644 --- a/src/runner.h +++ b/src/runner.h @@ -16,6 +16,7 @@ #include "runner_mouse.h" #include "vm.h" #include "random.h" +#include "sequence.h" // ===[ Event Type Constants ]=== #define EVENT_ROOM_CREATION -2 @@ -231,6 +232,7 @@ typedef enum { RuntimeLayerElementType_Sprite = 4, RuntimeLayerElementType_Tilemap = 5, RuntimeLayerElementType_Tile = 7, + RuntimeLayerElementType_Sequence = 8, } RuntimeLayerElementType; typedef struct { @@ -241,6 +243,7 @@ typedef struct { uint32_t blend; // GameMaker-HTML5's m_imageBlend RuntimeBackgroundElement* backgroundElement; // owned; set for every background element RuntimeSpriteElement* spriteElement; // owned; nullptr if type != Sprite + RuntimeSequence* sequenceElement; RoomTile* tileElement; // borrowed (legacyTiles) or owned (layer_tile_create); nullptr if type != Tile bool tileElementOwned; // true if tileElement was heap-allocated and must be freed RoomLayerTilesData* tilemapData; // borrowed, points into the parsed RoomLayer; nullptr if type != Tilemap @@ -713,6 +716,8 @@ struct Runner { // Any loop that might fire user code iterates a copy so that in-flight mutations (instance_change swap-remove, spawns, destroys) don't corrupt it. // Each call pushes its snapshot (append) and pops on normal loop exit; nesting is safe because pushes/pops are LIFO and outer ranges stay untouched under newer pushes. Instance** instanceSnapshots; + RuntimeSequence** sequenceSnapshots; + SequenceEvent* sequenceEvents; SpatialGrid* spatialGrid; uint32_t collisionQueryCounter; int32_t pendingRoom; // -1 = none @@ -939,6 +944,8 @@ void Runner_setLives(Runner* runner, GMLReal value); // Used to fire events when the values are equal to or lesser than 0. void Runner_setHealth(Runner* runner, GMLReal value); void Runner_draw(Runner* runner); +void Runner_drawInstance(Runner* runner, Instance* inst); +void Runner_broadcastMessage(Runner* runner, const char* type, const char* message, int32_t elementId); // Ensures the application_surface exists at the right size, mirrors the renderer's ID into runner+renderer state, then // invokes renderer->vtable->beginFrame. Every platform main should call this instead of beginFrame directly. void Runner_beginFrame(Runner* runner, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH, int32_t framebufferW, int32_t framebufferH); diff --git a/src/sequence.c b/src/sequence.c new file mode 100644 index 000000000..e490cee18 --- /dev/null +++ b/src/sequence.c @@ -0,0 +1,849 @@ +#include "sequence.h" +#include "runner.h" +#include "text_utils.h" +#include "stb_ds.h" +#include "math_compat.h" + +typedef struct { + int32_t instanceId; + int32_t soundId; + float soundDirection; + RuntimeSequence* nested; + PreprocessedText text; + int32_t wrapWidth; + bool textPrepared; +} SequenceChannelState; + +typedef struct { + float x, y, scaleX, scaleY, rotation, originX, originY; + float color[4]; + float imageSpeed, imageIndex, gain, pitch; + float frameWidth, frameHeight, characterSpacing, lineSpacing, paragraphSpacing; + bool hasImageSpeed, hasImageIndex; +} SequenceValues; + +struct SequenceNode { + const SequenceTrack* track; + SequenceNode* children; + SequenceChannelState* states; + uint32_t stateCount; + int32_t activeKey; + bool visible; + Matrix4f matrix; + SequenceValues values; +}; + +struct SequenceEvent { + int32_t elementId; + const char* message; + bool moment; +}; + +static void evaluateSequence(RuntimeSequence* seq, const Matrix4f* parent, const float* color, float previous, SequenceEvent** events, bool update, bool visible); + +static float smoothCurveValue(const AnimCurveChannel* ch, uint32_t lo, float x) { + float minimum = ch->points[0].value, maximum = minimum; + for (uint32_t i = 1; i < ch->pointCount; i++) { + minimum = fminf(minimum, ch->points[i].value); + maximum = fmaxf(maximum, ch->points[i].value); + } + float scale = fmaxf(1.0f, maximum - minimum); + uint32_t indices[4] = {lo > 0 ? lo - 1 : lo, lo, lo + 1, + lo + 2 < ch->pointCount ? lo + 2 : lo + 1}; + float px[4], py[4], times[4] = {0}; + repeat(4, i) { + px[i] = ch->points[indices[i]].x; + py[i] = (ch->points[indices[i]].value - minimum) / scale; + if (i > 0) { + float dx = px[i] - px[i - 1], dy = py[i] - py[i - 1]; + float distance = dx * dx + dy * dy; + times[i] = times[i - 1] + sqrtf(sqrtf(distance == 0.0f ? 0.0001f : distance)); + } + } + uint32_t iterations = ch->iterations > 0 ? ch->iterations : 1; + float lastX = px[1], lastY = py[1]; + for (uint32_t i = 1; i <= iterations; i++) { + float t = times[1] + (times[2] - times[1]) * ((float)i / iterations); + float ax[3], ay[3], bx[2], by[2]; + repeat(3, j) { + float f = (t - times[j]) / (times[j + 1] - times[j]); + ax[j] = px[j] + (px[j + 1] - px[j]) * f; + ay[j] = py[j] + (py[j + 1] - py[j]) * f; + } + repeat(2, j) { + float f = (t - times[j]) / (times[j + 2] - times[j]); + bx[j] = ax[j] + (ax[j + 1] - ax[j]) * f; + by[j] = ay[j] + (ay[j + 1] - ay[j]) * f; + } + float f = (t - times[1]) / (times[2] - times[1]); + float cx = fminf(px[2], fmaxf(lastX, bx[0] + (bx[1] - bx[0]) * f)); + float cy = by[0] + (by[1] - by[0]) * f; + if (cx >= x && cx > lastX) { + return (lastY + (cy - lastY) * (x - lastX) / (cx - lastX)) * scale + minimum; + } + lastX = cx; + lastY = cy; + } + return ch->points[lo + 1].value; +} + +float AnimCurveChannel_evaluate(const AnimCurveChannel* ch, float x) { + if (ch == nullptr || ch->pointCount == 0) return 0.0f; + if (ch->pointCount == 1 || x <= ch->points[0].x) return ch->points[0].value; + if (x >= ch->points[ch->pointCount - 1].x) return ch->points[ch->pointCount - 1].value; + uint32_t lo = 0, hi = ch->pointCount - 1; + while (hi - lo > 1) { + uint32_t mid = (lo + hi) / 2; + if (ch->points[mid].x <= x) lo = mid; + else hi = mid; + } + const AnimCurvePoint* a = &ch->points[lo]; + const AnimCurvePoint* b = &ch->points[hi]; + if (b->x <= a->x) return a->value; + float t = (x - a->x) / (b->x - a->x); + if (ch->curveType == ANIMCURVE_TYPE_SMOOTH) { + return smoothCurveValue(ch, lo, x); + } + if (ch->curveType == ANIMCURVE_TYPE_BEZIER) { + float left = 0.0f, right = 1.0f; + repeat(20, n) { + t = (left + right) * 0.5f; + float q = 1.0f - t; + float bx = q*q*q*a->x + 3*q*q*t*(a->x + a->bezierX1) + 3*q*t*t*(b->x + b->bezierX0) + t*t*t*b->x; + if (bx < x) left = t; + else right = t; + } + float q = 1.0f - t; + return q*q*q*a->value + 3*q*q*t*(a->value + a->bezierY1) + 3*q*t*t*(b->value + b->bezierY0) + t*t*t*b->value; + } + return a->value + (b->value - a->value) * t; +} + +static bool isParameter(const SequenceTrack* track) { + return track->type >= SEQUENCE_TRACK_REAL; +} + +static const SequenceChannel* findChannel(const SequenceKeyframe* key, int32_t id) { + for (uint32_t c = 0; c < key->channelCount; c++) { + if (key->channels[c].id == id) return &key->channels[c]; + } + return nullptr; +} + +static float keyEnd(const SequenceTrack* track, uint32_t index, float length) { + const SequenceKeyframe* key = &track->keyframes[index]; + if (key->stretch) return index + 1 < track->keyframeCount ? track->keyframes[index + 1].key : length; + return key->key + key->length; +} + +static int32_t activeKey(const SequenceTrack* track, float head, float length) { + for (int32_t k = (int32_t)track->keyframeCount - 1; k >= 0; k--) { + const SequenceKeyframe* key = &track->keyframes[k]; + if (key->disabled) continue; + float end = keyEnd(track, (uint32_t)k, length); + if (head >= key->key && head < end) return k; + if ((uint32_t)k == track->keyframeCount - 1 && head == length && end == length) return k; + } + return -1; +} + +static const SequenceChannel* parameterChannel(const SequenceKeyframe* key, int32_t id) { + const SequenceChannel* channel = findChannel(key, id); + if (channel == nullptr && key->channelCount > 0) { + const SequenceChannel* first = &key->channels[0]; + if (first->embeddedCurve != nullptr || first->curveIndex >= 0) channel = first; + } + return channel; +} + +static float channelValue(const SequenceTrack* track, const SequenceChannel* channel, int32_t component) { + if (track->type != SEQUENCE_TRACK_COLOR) return channel->real; + return (float)(((uint32_t)channel->integer >> (component * 8)) & 255u) / 255.0f; +} + +static float evaluateParameter(Runner* runner, const SequenceTrack* track, int32_t channelId, int32_t component, float head, float length, float fallback) { + const SequenceChannel* previous = nullptr; + const SequenceChannel* next = nullptr; + uint32_t keyIndex = 0; + float previousTime = 0.0f, nextTime = 0.0f; + for (uint32_t k = 0; k < track->keyframeCount; k++) { + const SequenceKeyframe* key = &track->keyframes[k]; + if (key->disabled) continue; + const SequenceChannel* channel = parameterChannel(key, channelId); + if (channel == nullptr) continue; + if (key->key > head && previous != nullptr) { + next = channel; + nextTime = key->key; + break; + } + previous = channel; + keyIndex = k; + previousTime = key->key; + if (key->key > head) break; + } + if (previous == nullptr) return fallback; + const SequenceKeyframe* key = &track->keyframes[keyIndex]; + const AnimCurve* curve = previous->embeddedCurve; + if (curve == nullptr && previous->curveIndex >= 0 && (uint32_t)previous->curveIndex < runner->dataWin->acrv.count) { + curve = &runner->dataWin->acrv.curves[previous->curveIndex]; + } + float end = keyEnd(track, keyIndex, length + 1.0f); + if (curve != nullptr && curve->channelCount > 0) { + uint32_t c = (uint32_t)channelId < curve->channelCount ? (uint32_t)channelId : curve->channelCount - 1; + float fraction = end > key->key ? (head - key->key) / (end - key->key) : 0.0f; + return AnimCurveChannel_evaluate(&curve->channels[c], fraction); + } + float value = channelValue(track, previous, component); + if (!track->interpolation || next == nullptr || key->stretch) return value; + if (key->length > 1.0f) previousTime = end; + if (head <= previousTime || nextTime <= previousTime) return value; + float fraction = (head - previousTime) / (nextTime - previousTime); + return value + (channelValue(track, next, component) - value) * fraction; +} + +static SequenceValues evaluateValues(RuntimeSequence* seq, const SequenceTrack* track, float head) { + SequenceValues value = {0}; + value.scaleX = value.scaleY = value.imageSpeed = value.gain = value.pitch = 1.0f; + repeat(4, c) value.color[c] = 1.0f; + for (uint32_t t = 0; t < track->trackCount; t++) { + const SequenceTrack* parameter = &track->tracks[t]; + if (!isParameter(parameter) || !parameter->enabled) continue; + float at = parameter->creationTrack ? 0.0f : head; + float x = evaluateParameter(seq->runner, parameter, 0, 0, at, seq->asset->length, 0.0f); + float y = evaluateParameter(seq->runner, parameter, 1, 0, at, seq->asset->length, 0.0f); + switch (parameter->builtinName) { + case 5: value.gain = x; break; + case 6: value.pitch = x; break; + case 8: value.rotation = x; break; + case 10: + repeat(4, c) value.color[c] = evaluateParameter(seq->runner, parameter, 0, c, at, seq->asset->length, 1.0f); + break; + case 14: value.x = x; value.y = y; break; + case 15: value.scaleX = x; value.scaleY = y; break; + case 16: value.originX = x; value.originY = y; break; + case 17: value.imageSpeed = x; value.hasImageSpeed = true; break; + case 18: value.imageIndex = x; value.hasImageIndex = true; break; + case 20: value.frameWidth = x; value.frameHeight = y; break; + case 21: value.characterSpacing = x; break; + case 22: value.lineSpacing = x; break; + case 23: value.paragraphSpacing = x; break; + } + } + return value; +} + +static RuntimeSequence* newSequence(Runner* runner, int32_t assetIndex, int32_t elementId, int32_t layerId) { + if (assetIndex < 0 || (uint32_t)assetIndex >= runner->dataWin->seqn.count) return nullptr; + Sequence* asset = &runner->dataWin->seqn.sequences[assetIndex]; + if (!asset->present) return nullptr; + RuntimeSequence* seq = (RuntimeSequence*)safeCalloc(1, sizeof(RuntimeSequence)); + seq->runner = runner; + seq->root = seq; + seq->asset = asset; + seq->assetIndex = assetIndex; + seq->elementId = elementId; + seq->layerId = layerId; + seq->scaleX = seq->scaleY = seq->direction = seq->speedScale = seq->alpha = 1.0f; + seq->blend = 0xFFFFFFu; + seq->references = 1; + return seq; +} + +RuntimeSequence* Sequence_find(Runner* runner, int32_t elementId) { + RuntimeLayerElement* el = Runner_findLayerElementById(runner, elementId, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sequence) return nullptr; + return el->sequenceElement; +} + +int32_t Sequence_create(Runner* runner, int32_t layerId, int32_t assetIndex, float x, float y) { + RuntimeLayer* layer = Runner_findRuntimeLayerById(runner, layerId); + if (layer == nullptr) return -1; + int32_t elementId = (int32_t)Runner_getNextLayerId(runner); + RuntimeSequence* seq = newSequence(runner, assetIndex, elementId, layerId); + if (seq == nullptr) return -1; + seq->x = x; + seq->y = y; + RuntimeLayerElement el = {0}; + el.id = (uint32_t)elementId; + el.type = RuntimeLayerElementType_Sequence; + el.sequenceElement = seq; + el.visible = true; + el.alpha = 1.0f; + el.blend = 0xFFFFFFu; + arrput(layer->elements, el); + runner->drawableListStructureDirty = true; + return elementId; +} + +static void freeNodes(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count) { + repeat(count, t) { + SequenceNode* node = &nodes[t]; + freeNodes(seq, node->children, node->track->trackCount); + repeat(node->stateCount, c) { + SequenceChannelState* state = &node->states[c]; + if (state->soundId >= 0 && seq->runner->audioSystem != nullptr) { + seq->runner->audioSystem->vtable->stopSound(seq->runner->audioSystem, state->soundId); + } + if (state->nested != nullptr) Sequence_release(state->nested); + PreprocessedText_free(state->text); + } + free(node->states); + } + free(nodes); +} + +void Sequence_release(RuntimeSequence* seq) { + if (seq == nullptr || --seq->references != 0) return; + if (seq->nodes != nullptr) freeNodes(seq, seq->nodes, seq->asset->trackCount); + free(seq); +} + +static void destroyNodeInstances(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count) { + repeat(count, t) { + SequenceNode* node = &nodes[t]; + destroyNodeInstances(seq, node->children, node->track->trackCount); + repeat(node->stateCount, c) { + SequenceChannelState* state = &node->states[c]; + if (state->instanceId >= 0) { + Instance* inst = hmget(seq->runner->instancesById, state->instanceId); + if (inst != nullptr && !inst->destroyed) Runner_destroyInstance(seq->runner, inst, true); + } + if (state->nested != nullptr && state->nested->nodes != nullptr) { + destroyNodeInstances(state->nested, state->nested->nodes, state->nested->asset->trackCount); + } + } + } +} + +void Sequence_destroy(Runner* runner, int32_t elementId) { + RuntimeLayer* layer = nullptr; + RuntimeLayerElement* el = Runner_findLayerElementById(runner, elementId, &layer); + if (el == nullptr || el->type != RuntimeLayerElementType_Sequence) return; + RuntimeSequence* seq = el->sequenceElement; + seq->destroyed = true; + arrdel(layer->elements, el - layer->elements); + if (seq->nodes != nullptr) destroyNodeInstances(seq, seq->nodes, seq->asset->trackCount); + Sequence_release(seq); +} + +static void pauseNodes(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count, bool paused) { + repeat(count, t) { + SequenceNode* node = &nodes[t]; + pauseNodes(seq, node->children, node->track->trackCount, paused); + repeat(node->stateCount, c) { + SequenceChannelState* state = &node->states[c]; + AudioSystem* audio = seq->runner->audioSystem; + if (state->soundId >= 0 && audio != nullptr) { + if (paused) audio->vtable->pauseSound(audio, state->soundId); + else audio->vtable->resumeSound(audio, state->soundId); + } + if (state->nested != nullptr) Sequence_setPaused(state->nested, paused); + } + } +} + +void Sequence_setPaused(RuntimeSequence* seq, bool paused) { + if (seq == nullptr || seq->paused == paused) return; + seq->paused = paused; + if (seq->nodes != nullptr) pauseNodes(seq, seq->nodes, seq->asset->trackCount, paused); +} + +void Sequence_play(RuntimeSequence* seq) { + if (seq == nullptr) return; + Sequence_setPaused(seq, false); + if (seq->finished) { + seq->headPosition = seq->direction < 0.0f ? fmaxf(0.0f, seq->asset->length - 1.0f) : 0.0f; + seq->finished = false; + } +} + +static SequenceNode* createNodes(RuntimeSequence* seq, const SequenceTrack* tracks, uint32_t count, uint32_t depth) { + requireMessageFormatted(__FILE__, __LINE__, depth < 32, "Sequence nesting exceeds 32 levels"); + SequenceNode* nodes = (SequenceNode*)safeCalloc(count, sizeof(SequenceNode)); + repeat(count, t) { + SequenceNode* node = &nodes[t]; + node->track = &tracks[t]; + node->activeKey = -1; + node->children = createNodes(seq, tracks[t].tracks, tracks[t].trackCount, depth + 1); + if (isParameter(node->track)) continue; + for (uint32_t k = 0; k < tracks[t].keyframeCount; k++) node->stateCount += tracks[t].keyframes[k].channelCount; + node->states = (SequenceChannelState*)safeCalloc(node->stateCount, sizeof(SequenceChannelState)); + uint32_t index = 0; + for (uint32_t k = 0; k < tracks[t].keyframeCount; k++) { + const SequenceKeyframe* key = &tracks[t].keyframes[k]; + for (uint32_t c = 0; c < key->channelCount; c++) { + SequenceChannelState* state = &node->states[index++]; + state->instanceId = state->soundId = -1; + if (tracks[t].type == SEQUENCE_TRACK_SEQUENCE) { + state->nested = newSequence(seq->runner, key->channels[c].integer, seq->elementId, seq->layerId); + if (state->nested != nullptr) { + state->nested->root = seq->root; + state->nested->nodes = createNodes(state->nested, state->nested->asset->tracks, state->nested->asset->trackCount, depth + 1); + state->nested->initialized = true; + } + } + } + } + } + return nodes; +} + +static void initializeNodeInstances(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count) { + repeat(count, t) { + SequenceNode* node = &nodes[t]; + const SequenceTrack* track = node->track; + uint32_t index = 0; + for (uint32_t k = 0; k < track->keyframeCount; k++) { + const SequenceKeyframe* key = &track->keyframes[k]; + for (uint32_t c = 0; c < key->channelCount; c++) { + if (isParameter(track)) continue; + if (seq->root->destroyed) return; + SequenceChannelState* state = &node->states[index++]; + if (track->type == SEQUENCE_TRACK_INSTANCE && key->channels[c].integer >= 0) { + Instance* inst = Runner_createInstanceWithLayer(seq->runner, 0, 0, key->channels[c].integer, seq->layerId); + if (inst != nullptr) { + state->instanceId = (int32_t)inst->instanceId; + if (seq->root->destroyed) { + if (!inst->destroyed) Runner_destroyInstance(seq->runner, inst, true); + return; + } + inst->sequenceControlled = true; + inst->visible = false; + } + } else if (state->nested != nullptr) { + initializeNodeInstances(state->nested, state->nested->nodes, state->nested->asset->trackCount); + } + } + } + if (seq->root->destroyed) return; + initializeNodeInstances(seq, node->children, track->trackCount); + } +} + +static void initializeSequence(RuntimeSequence* seq) { + if (seq->initialized) return; + seq->initialized = true; + seq->nodes = createNodes(seq, seq->asset->tracks, seq->asset->trackCount, 0); + initializeNodeInstances(seq, seq->nodes, seq->asset->trackCount); +} + +static float speedPerSecond(const Sequence* asset, float fps) { + return asset->playbackSpeedType == 0 ? asset->playbackSpeed : asset->playbackSpeed * fps; +} + +static void wrapSequenceHead(RuntimeSequence* seq) { + float length = seq->asset->length; + if (length <= 0.0f) { + seq->headPosition = 0.0f; + seq->finished = true; + return; + } + if (seq->asset->playback == 0) { + if (seq->headPosition >= length) { + seq->headPosition = length; + seq->finished = seq->direction > 0.0f; + } else if (seq->headPosition <= 0.0f) { + seq->headPosition = 0.0f; + seq->finished = seq->direction < 0.0f; + } + } else if (seq->asset->playback == 1) { + seq->headPosition = fmodf(seq->headPosition, length); + if (seq->headPosition < 0.0f) seq->headPosition += length; + } else if (seq->headPosition < 0.0f || seq->headPosition >= length) { + float position = fabsf(seq->headPosition); + float cycle = fmodf(position, length * 2.0f); + seq->direction = cycle >= length ? -1.0f : 1.0f; + seq->headPosition = cycle >= length ? length * 2.0f - cycle : cycle; + } +} + +static float imageSpeedDistance(RuntimeSequence* seq, const SequenceTrack* track, float start, float end) { + float distance = 0.0f; + float position = start; + while (position < end) { + float next = end; + for (uint32_t k = 0; k < track->keyframeCount; k++) { + const SequenceKeyframe* key = &track->keyframes[k]; + if (key->disabled) continue; + if (key->key > position && key->key < next) next = key->key; + if (key->length > 1.0f && !key->stretch) { + float heldEnd = keyEnd(track, k, seq->asset->length + 1.0f); + if (heldEnd > position && heldEnd < next) next = heldEnd; + } + } + float middle = position + (next - position) * 0.5f; + float speed = evaluateParameter(seq->runner, track, 0, 0, middle, seq->asset->length, 0.0f); + distance += (next - position) * speed; + position = next; + } + return fmaxf(0.0f, distance); +} + +static float spriteFrame(RuntimeSequence* seq, const SequenceNode* node, int32_t spriteIndex, float elapsed) { + if (node->values.hasImageIndex) return node->values.imageIndex; + if (spriteIndex < 0 || (uint32_t)spriteIndex >= seq->runner->dataWin->sprt.count) return 0.0f; + Sprite* sprite = &seq->runner->dataWin->sprt.sprites[spriteIndex]; + float fps = (float)Runner_getEffectiveGameSpeed(seq->runner); + float speed = speedPerSecond(seq->asset, fps); + if (speed == 0.0f) return 0.0f; + float spriteSpeed = sprite->gms2PlaybackSpeedType == 0 ? sprite->gms2PlaybackSpeed : sprite->gms2PlaybackSpeed * fps; + float distance = elapsed * node->values.imageSpeed; + if (node->values.hasImageSpeed) { + for (uint32_t t = 0; t < node->track->trackCount; t++) { + const SequenceTrack* parameter = &node->track->tracks[t]; + if (parameter->builtinName != 17 || !parameter->enabled || parameter->creationTrack) continue; + distance = imageSpeedDistance(seq, parameter, seq->headPosition - elapsed, seq->headPosition); + } + } + return distance * spriteSpeed / speed; +} + +static uint32_t packedColor(const float* color) { + uint32_t result = 0; + repeat(3, c) result |= (uint32_t)(fminf(1.0f, fmaxf(0.0f, color[c])) * 255.0f) << (c * 8); + return result; +} + +static void buildMatrix(Matrix4f* matrix, const Matrix4f* parent, const SequenceValues* value) { + *matrix = *parent; + Matrix4f_translate(matrix, value->x, value->y, 0.0f); + Matrix4f_rotateZ(matrix, -value->rotation * (float)(M_PI / 180.0)); + Matrix4f_scale(matrix, value->scaleX, value->scaleY, 1.0f); + Matrix4f_translate(matrix, -value->originX, -value->originY, 0.0f); +} + +static void hideNodeInstances(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count) { + repeat(count, t) { + SequenceNode* node = &nodes[t]; + hideNodeInstances(seq, node->children, node->track->trackCount); + repeat(node->stateCount, c) { + SequenceChannelState* state = &node->states[c]; + if (state->instanceId >= 0) { + Instance* inst = hmget(seq->runner->instancesById, state->instanceId); + if (inst != nullptr && !inst->destroyed) inst->visible = false; + } + if (state->nested != nullptr) { + hideNodeInstances(state->nested, state->nested->nodes, state->nested->asset->trackCount); + } + } + } +} + +static void evaluateNodes(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count, const Matrix4f* parent, const float* parentColor, float previous, SequenceEvent** events, bool update, bool parentVisible) { + Runner* runner = seq->runner; + float head = seq->headPosition; + repeat(count, t) { + if (seq->root->destroyed) return; + SequenceNode* node = &nodes[t]; + const SequenceTrack* track = node->track; + if (isParameter(track) || !track->enabled) continue; + node->visible = parentVisible && track->visible; + node->values = evaluateValues(seq, track, head); + repeat(4, c) node->values.color[c] *= parentColor[c]; + int32_t selected = activeKey(track, head, seq->asset->length); + node->activeKey = selected; + const SequenceKeyframe* key = selected >= 0 ? &track->keyframes[selected] : nullptr; + const SequenceChannel* channel = key != nullptr && key->channelCount > 0 ? &key->channels[0] : nullptr; + if (track->type == SEQUENCE_TRACK_GRAPHIC && channel != nullptr && channel->integer >= 0 && (uint32_t)channel->integer < runner->dataWin->sprt.count) { + const Sprite* sprite = &runner->dataWin->sprt.sprites[channel->integer]; + node->values.originX += (float)sprite->originX; + node->values.originY += (float)sprite->originY; + } + buildMatrix(&node->matrix, parent, &node->values); + uint32_t stateIndex = 0; + for (uint32_t k = 0; k < track->keyframeCount; k++) { + const SequenceKeyframe* current = &track->keyframes[k]; + for (uint32_t c = 0; c < current->channelCount; c++) { + SequenceChannelState* state = &node->states[stateIndex++]; + const SequenceChannel* data = ¤t->channels[c]; + bool active = (int32_t)k == selected; + if (track->type == SEQUENCE_TRACK_INSTANCE && state->instanceId >= 0) { + Instance* inst = hmget(runner->instancesById, state->instanceId); + if (inst == nullptr || inst->destroyed) continue; + inst->visible = active && node->visible; + inst->sequenceControlled = true; + if (!active) continue; + Matrix4f_transformPoint(&node->matrix, node->values.originX, node->values.originY, &inst->x, &inst->y); + inst->imageAngle = -atan2f(node->matrix.m[1], node->matrix.m[0]) * (180.0f / (float)M_PI); + inst->imageXscale = hypotf(node->matrix.m[0], node->matrix.m[1]); + float determinant = node->matrix.m[0]*node->matrix.m[5] - node->matrix.m[1]*node->matrix.m[4]; + inst->imageYscale = inst->imageXscale != 0.0f ? determinant / inst->imageXscale : 0.0f; + inst->imageBlend = packedColor(node->values.color); + inst->imageAlpha = node->values.color[3]; + if (node->values.hasImageIndex || node->values.hasImageSpeed) inst->imageIndex = spriteFrame(seq, node, inst->spriteIndex, head - current->key); + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } else if (track->type == SEQUENCE_TRACK_AUDIO && update && runner->audioSystem != nullptr) { + AudioSystem* audio = runner->audioSystem; + active = active && node->visible; + bool finished = seq->finished || seq->root->finished; + bool restarted = state->soundDirection != seq->direction || (head - previous) * state->soundDirection < 0.0f; + if ((!active || finished || restarted) && state->soundId >= 0) { + audio->vtable->stopSound(audio, state->soundId); + state->soundId = -1; + } + if (active && !finished && !seq->paused) { + if (state->soundId < 0) { + state->soundId = audio->vtable->playSound(audio, data->integer, 1, data->audioMode == 1); + state->soundDirection = seq->direction; + float speed = speedPerSecond(seq->asset, (float)Runner_getEffectiveGameSpeed(runner)); + if (state->soundId >= 0 && speed > 0.0f && head > current->key) audio->vtable->setTrackPosition(audio, state->soundId, (head - current->key) / speed); + } + if (state->soundId >= 0) { + audio->vtable->setSoundGain(audio, state->soundId, node->values.gain * seq->asset->volume, 0); + audio->vtable->setSoundPitch(audio, state->soundId, node->values.pitch); + } + } + } else if (state->nested != nullptr) { + RuntimeSequence* nested = state->nested; + if (active) { + float fps = (float)Runner_getEffectiveGameSpeed(runner); + float parentSpeed = speedPerSecond(seq->asset, fps); + float ratio = parentSpeed != 0.0f ? speedPerSecond(nested->asset, fps) / parentSpeed : 0.0f; + float old = nested->headPosition; + nested->headPosition = (head - current->key) * ratio; + Sequence_setPaused(nested, seq->paused); + nested->direction = seq->direction; + nested->finished = false; + wrapSequenceHead(nested); + evaluateSequence(nested, &node->matrix, node->values.color, old, events, update, node->visible); + } else if (update && nested->nodes != nullptr) { + Sequence_setPaused(nested, true); + hideNodeInstances(nested, nested->nodes, nested->asset->trackCount); + } + } + } + } + Matrix4f childrenMatrix = node->matrix; + if ((track->traits & 1u) == 0) Matrix4f_translate(&childrenMatrix, node->values.originX, node->values.originY, 0.0f); + evaluateNodes(seq, node->children, track->trackCount, &childrenMatrix, node->values.color, previous, events, update, node->visible); + } +} + +static void queueEvents(RuntimeSequence* seq, SequenceKeyframe* keys, uint32_t count, float previous, bool moment, SequenceEvent** events) { + bool forward = seq->asset->playbackSpeed * seq->speedScale * seq->direction >= 0.0f; + bool wrapped = forward ? seq->headPosition < previous : seq->headPosition > previous; + uint32_t passes = wrapped ? 2 : 1; + repeat(passes, pass) { + repeat(count, i) { + uint32_t k = forward ? i : count - i - 1; + SequenceKeyframe* key = &keys[k]; + bool afterStart = forward ? key->key >= previous : key->key <= previous; + bool beforeEnd = forward ? key->key < seq->headPosition : key->key > seq->headPosition; + bool crossed = afterStart && beforeEnd; + if (wrapped) crossed = pass == 0 ? afterStart : beforeEnd; + if (key->disabled || !crossed) continue; + repeat(key->channelCount, c) { + SequenceChannel* channel = &key->channels[c]; + repeat(channel->messageCount, m) { + SequenceEvent event = {seq->elementId, channel->messages[m], moment}; + arrput(*events, event); + } + } + } + } +} + +static void evaluateSequence(RuntimeSequence* seq, const Matrix4f* parent, const float* color, float previous, SequenceEvent** events, bool update, bool visible) { + initializeSequence(seq); + if (seq->root->destroyed) return; + Matrix4f matrix = *parent; + Matrix4f_translate(&matrix, -seq->asset->originX, -seq->asset->originY, 0.0f); + evaluateNodes(seq, seq->nodes, seq->asset->trackCount, &matrix, color, previous, events, update, visible); + if (update && !seq->paused && !seq->root->destroyed) { + queueEvents(seq, seq->asset->messages, seq->asset->messageCount, previous, false, events); + queueEvents(seq, seq->asset->moments, seq->asset->momentCount, previous, true, events); + } +} + +static void evaluateRoot(RuntimeSequence* seq, float previous, SequenceEvent** events, bool update) { + RuntimeLayer* layer = Runner_findRuntimeLayerById(seq->runner, seq->layerId); + if (layer == nullptr) return; + Matrix4f matrix; + Matrix4f_identity(&matrix); + Matrix4f_translate(&matrix, seq->x + layer->xOffset, seq->y + layer->yOffset, 0.0f); + Matrix4f_rotateZ(&matrix, -seq->angle * (float)(M_PI / 180.0)); + Matrix4f_scale(&matrix, seq->scaleX, seq->scaleY, 1.0f); + float color[4] = { + (float)BGR_R(seq->blend) / 255.0f, + (float)BGR_G(seq->blend) / 255.0f, + (float)BGR_B(seq->blend) / 255.0f, + seq->alpha + }; + evaluateSequence(seq, &matrix, color, previous, events, update, true); + seq->transformDirty = false; +} + +void Sequence_stepRoom(Runner* runner) { + RuntimeSequence** snapshot = runner->sequenceSnapshots; + arrsetlen(snapshot, 0); + repeat(arrlenu(runner->runtimeLayers), l) { + RuntimeLayer* layer = &runner->runtimeLayers[l]; + repeat(arrlenu(layer->elements), e) { + RuntimeSequence* seq = layer->elements[e].sequenceElement; + if (seq == nullptr) continue; + seq->references++; + arrput(snapshot, seq); + } + } + SequenceEvent* events = runner->sequenceEvents; + arrsetlen(events, 0); + repeat(arrlenu(snapshot), s) { + RuntimeSequence* seq = snapshot[s]; + if (seq->destroyed) continue; + initializeSequence(seq); + if (seq->destroyed || runner->pendingRoom >= 0) continue; + float previous = seq->headPosition; + if (!seq->paused && !seq->finished) { + float fps = (float)Runner_getEffectiveGameSpeed(runner); + float advance = fps > 0.0f ? speedPerSecond(seq->asset, fps) / fps : 0.0f; + seq->headPosition += advance * seq->speedScale * seq->direction; + wrapSequenceHead(seq); + } + evaluateRoot(seq, previous, &events, !seq->paused); + } + repeat(arrlenu(events), e) { + if (runner->pendingRoom >= 0) break; + SequenceEvent* event = &events[e]; + if (Sequence_find(runner, event->elementId) == nullptr) continue; + if (!event->moment) { + Runner_broadcastMessage(runner, "sequence event", event->message, event->elementId); + } else { + VMContext* ctx = runner->vmContext; + ptrdiff_t index = shgeti(ctx->codeIndexByName, event->message); + if (index >= 0) { + RValue result = VM_executeCode(ctx, ctx->codeIndexByName[index].value, ctx->globalScopeInstance, nullptr, CODE_NOT_RELATED_TO_EVENTS, EVENT_OTHER, OTHER_BROADCAST_MESSAGE, nullptr, 0); + RValue_free(&result); + } + } + if (runner->pendingRoom >= 0) break; + } + repeat(arrlenu(snapshot), s) Sequence_release(snapshot[s]); + arrsetlen(snapshot, 0); + arrsetlen(events, 0); + runner->sequenceSnapshots = snapshot; + runner->sequenceEvents = events; +} + +static void drawSequenceText(Renderer* renderer, const SequenceChannel* channel, SequenceChannelState* state, const SequenceValues* values) { + if (channel->fontIndex < 0 || (uint32_t)channel->fontIndex >= renderer->dataWin->font.count) return; + Font* font = &renderer->dataWin->font.fonts[channel->fontIndex]; + int32_t savedFont = renderer->drawFont; + int32_t savedHalign = renderer->drawHalign, savedValign = renderer->drawValign; + renderer->drawFont = channel->fontIndex; + renderer->drawHalign = renderer->drawValign = 0; + int32_t wrapWidth = -1; + if (channel->wrap && values->frameWidth > 0.0f) wrapWidth = (int32_t)values->frameWidth; + if (!state->textPrepared || state->wrapWidth != wrapWidth) { + PreprocessedText_free(state->text); + state->text = (PreprocessedText){0}; + if (wrapWidth >= 0) state->text = TextUtils_wrapText(font, channel->text, wrapWidth); + else state->text.text = channel->text; + state->wrapWidth = wrapWidth; + state->textPrepared = true; + } + const char* str = state->text.text; + int32_t length = (int32_t)strlen(str); + int32_t start = 0; + float y = 0.0f; + int32_t lineCount = length > 0 ? 1 : 0; + for (int32_t pos = 0; pos < length;) { + if (str[pos] == '\r' || str[pos] == '\n') { + lineCount++; + pos = TextUtils_skipNewline(str, pos, length); + } else pos++; + } + float stride = TextUtils_lineStride(font) + values->lineSpacing + values->paragraphSpacing; + float height = lineCount > 0 ? TextUtils_lineStride(font) + (lineCount - 1) * stride : 0.0f; + int32_t valign = (channel->alignment >> 8) & 255; + if (valign == 1) y = (values->frameHeight - height) * 0.5f; + else if (valign == 2) y = values->frameHeight - height; + uint32_t color = packedColor(values->color); + if (values->characterSpacing == 0.0f) { + int32_t align = channel->alignment & 255; + float x = 0.0f; + if (align == 1) x = values->frameWidth * 0.5f; + else if (align == 2) x = values->frameWidth; + renderer->drawHalign = align; + renderer->vtable->drawTextColor(renderer, str, x, y, 1.0f, 1.0f, 0.0f, color, color, color, color, values->color[3], stride * font->scaleY); + } + while (values->characterSpacing != 0.0f && start < length) { + int32_t end = start; + while (end < length && str[end] != '\r' && str[end] != '\n') end++; + float width = TextUtils_measureLineWidth(font, str + start, end - start); + width += values->characterSpacing * fmaxf(0.0f, (float)TextUtils_utf8CodepointCount(str + start, end - start) - 1.0f); + int32_t align = channel->alignment & 255; + float x = align == 1 ? (values->frameWidth - width) * 0.5f : align == 2 ? values->frameWidth - width : 0.0f; + int32_t pos = start; + while (pos < end) { + int32_t before = pos; + uint16_t ch = TextUtils_decodeUtf8(str, end, &pos); + char glyphText[5] = {0}; + memcpy(glyphText, str + before, (size_t)(pos - before)); + renderer->vtable->drawTextColor(renderer, glyphText, x, y, 1.0f, 1.0f, 0.0f, color, color, color, color, values->color[3], -1.0f); + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + if (glyph != nullptr) { + x += glyph->shift + values->characterSpacing; + if (pos < end) { + int32_t nextPos = pos; + uint16_t next = TextUtils_decodeUtf8(str, end, &nextPos); + x += TextUtils_getKerningOffset(glyph, next); + } + } + } + y += stride; + start = end < length ? TextUtils_skipNewline(str, end, length) : end; + } + renderer->drawFont = savedFont; + renderer->drawHalign = savedHalign; + renderer->drawValign = savedValign; +} + +static void drawNodes(RuntimeSequence* seq, SequenceNode* nodes, uint32_t count, const Matrix4f* world) { + Renderer* renderer = seq->runner->renderer; + for (uint32_t t = count; t > 0; t--) { + if (seq->root->destroyed) return; + SequenceNode* node = &nodes[t - 1]; + const SequenceTrack* track = node->track; + if (isParameter(track) || !track->enabled || !node->visible) continue; + Matrix4f matrix; + Matrix4f_multiply(&matrix, world, &node->matrix); + if (node->activeKey >= 0) { + const SequenceKeyframe* key = &track->keyframes[node->activeKey]; + uint32_t stateIndex = 0; + for (int32_t k = 0; k < node->activeKey; k++) stateIndex += track->keyframes[k].channelCount; + for (uint32_t c = 0; c < key->channelCount; c++) { + const SequenceChannel* channel = &key->channels[c]; + SequenceChannelState* state = &node->states[stateIndex + c]; + if (track->type == SEQUENCE_TRACK_INSTANCE) { + Instance* inst = hmget(seq->runner->instancesById, state->instanceId); + if (inst != nullptr && inst->active && inst->visible && !inst->destroyed && inst->sequenceControlled) { + renderer->vtable->setMatrix(renderer, MATRIX_WORLD, *world); + Runner_drawInstance(seq->runner, inst); + } + } else if (state->nested != nullptr) { + drawNodes(state->nested, state->nested->nodes, state->nested->asset->trackCount, world); + } else if (track->type == SEQUENCE_TRACK_GRAPHIC) { + renderer->vtable->setMatrix(renderer, MATRIX_WORLD, matrix); + int32_t subimg = (int32_t)spriteFrame(seq, node, channel->integer, seq->headPosition - key->key); + int32_t tpag = Renderer_resolveTPAGIndex(renderer->dataWin, channel->integer, subimg); + if (tpag >= 0) renderer->vtable->drawSprite(renderer, tpag, 0, 0, 0, 0, 1, 1, 0, packedColor(node->values.color), node->values.color[3]); + } else if (track->type == SEQUENCE_TRACK_TEXT) { + renderer->vtable->setMatrix(renderer, MATRIX_WORLD, matrix); + drawSequenceText(renderer, channel, state, &node->values); + } + } + } + drawNodes(seq, node->children, track->trackCount, world); + } +} + +void Sequence_draw(RuntimeSequence* seq) { + if (seq == nullptr || seq->destroyed || seq->nodes == nullptr || seq->runner->renderer == nullptr) return; + seq->references++; + Renderer* renderer = seq->runner->renderer; + Matrix4f world = renderer->gmlMatrices[MATRIX_WORLD]; + if (seq->transformDirty) evaluateRoot(seq, seq->headPosition, nullptr, false); + drawNodes(seq, seq->nodes, seq->asset->trackCount, &world); + renderer->vtable->setMatrix(renderer, MATRIX_WORLD, world); + Sequence_release(seq); +} diff --git a/src/sequence.h b/src/sequence.h new file mode 100644 index 000000000..799b90245 --- /dev/null +++ b/src/sequence.h @@ -0,0 +1,40 @@ +#ifndef _BS_SEQUENCE_H_ +#define _BS_SEQUENCE_H_ + +#include "common.h" +#include "data_win.h" + +typedef struct SequenceNode SequenceNode; +typedef struct SequenceEvent SequenceEvent; +struct Runner; +typedef struct RuntimeSequence { + struct Runner* runner; + struct RuntimeSequence* root; + Sequence* asset; + int32_t assetIndex; + int32_t elementId; + int32_t layerId; + float x, y; + float scaleX, scaleY, angle; + uint32_t blend; + float alpha; + float headPosition; + float direction; + float speedScale; + bool paused, finished, initialized, destroyed; + bool transformDirty; + uint32_t references; + SequenceNode* nodes; +} RuntimeSequence; + +float AnimCurveChannel_evaluate(const AnimCurveChannel* channel, float x); +int32_t Sequence_create(struct Runner* runner, int32_t layerId, int32_t assetIndex, float x, float y); +RuntimeSequence* Sequence_find(struct Runner* runner, int32_t elementId); +void Sequence_destroy(struct Runner* runner, int32_t elementId); +void Sequence_release(RuntimeSequence* sequence); +void Sequence_setPaused(RuntimeSequence* sequence, bool paused); +void Sequence_play(RuntimeSequence* sequence); +void Sequence_stepRoom(struct Runner* runner); +void Sequence_draw(RuntimeSequence* sequence); + +#endif diff --git a/src/text_utils.h b/src/text_utils.h index eabd43291..84522b895 100644 --- a/src/text_utils.h +++ b/src/text_utils.h @@ -366,7 +366,7 @@ static inline PreprocessedText TextUtils_wrapText(Font* font, const char* text, } int32_t _end = end; if (_end > start) { - while (_end > 0 && pNew[_end - 1] == ' ') _end--; + while (_end > start && pNew[_end - 1] == ' ') _end--; } // else if (end == start) // if we're back to the START of the string... look for the next space - or string end. // { diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 63609ad43..1fd600343 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -16254,22 +16254,27 @@ static RValue builtin_layer_create(VMContext* ctx, RValue* args, int32_t argCoun static RValue builtin_layer_destroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { Runner* runner = ctx->runner; int32_t id = resolveLayerIdArg(runner, args[0]); + RuntimeLayer* layer = Runner_findRuntimeLayerById(runner, id); + if (layer == nullptr) return RValue_makeUndefined(); + int32_t* sequenceIds = nullptr; + int32_t* instanceIds = nullptr; + repeat(arrlenu(layer->elements), e) { + RuntimeLayerElement* el = &layer->elements[e]; + if (el->type == RuntimeLayerElementType_Sequence) arrput(sequenceIds, (int32_t)el->id); + else if (el->type == RuntimeLayerElementType_Instance) arrput(instanceIds, el->instanceId); + } + repeat(arrlenu(sequenceIds), e) Sequence_destroy(runner, sequenceIds[e]); + repeat(arrlenu(instanceIds), e) { + Instance* inst = hmget(runner->instancesById, instanceIds[e]); + if (inst != nullptr && !inst->destroyed) Runner_destroyInstance(runner, inst, true); + } + arrfree(sequenceIds); + arrfree(instanceIds); size_t count = arrlenu(runner->runtimeLayers); repeat(count, i) { if ((int32_t) runner->runtimeLayers[i].id != id) continue; - // GameMaker allows destroying room-defined layers at runtime. - // When destroying an instance layer, destroy the instances bound to that layer. - size_t elementCount = arrlenu(runner->runtimeLayers[i].elements); - repeat(elementCount, j) { - RuntimeLayerElement* el = &runner->runtimeLayers[i].elements[j]; - if (el->type != RuntimeLayerElementType_Instance) continue; - Instance* inst = hmget(runner->instancesById, el->instanceId); - if (inst == nullptr || inst->destroyed) continue; - Runner_destroyInstance(runner, inst, true); - } - Runner_freeRuntimeLayer(&runner->runtimeLayers[i]); arrdel(runner->runtimeLayers, i); @@ -16293,6 +16298,77 @@ static RValue builtin_layer_script_begin(VMContext* ctx, RValue* args, MAYBE_UNU return RValue_makeUndefined(); } +static RValue builtin_layer_sequence_create(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_create", 4, RValue_makeReal(-1)); + int32_t layerId = resolveLayerIdArg(ctx->runner, args[0]); + int32_t id = Sequence_create(ctx->runner, layerId, RValue_toInt32(args[3]), (float)RValue_toReal(args[1]), (float)RValue_toReal(args[2])); + return RValue_makeReal(id); +} + +static RValue builtin_layer_sequence_destroy(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_destroy", 1, RValue_makeUndefined()); + Sequence_destroy(ctx->runner, RValue_toInt32(args[0])); + return RValue_makeUndefined(); +} + +static RValue builtin_layer_sequence_play(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_play", 1, RValue_makeReal(-1)); + Sequence_play(Sequence_find(ctx->runner, RValue_toInt32(args[0]))); + return RValue_makeReal(-1); +} + +static RValue builtin_layer_sequence_pause(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_pause", 1, RValue_makeReal(-1)); + Sequence_setPaused(Sequence_find(ctx->runner, RValue_toInt32(args[0])), true); + return RValue_makeReal(-1); +} + +static RValue builtin_layer_sequence_is_finished(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_is_finished", 1, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + return seq != nullptr ? RValue_makeBool(seq->finished) : RValue_makeReal(-1); +} + +static RValue builtin_layer_sequence_is_paused(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_is_paused", 1, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + return seq != nullptr ? RValue_makeBool(seq->paused) : RValue_makeReal(-1); +} + +static RValue builtin_layer_sequence_get_headpos(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_get_headpos", 1, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + return RValue_makeReal(seq != nullptr ? seq->headPosition : -1); +} + +static RValue builtin_layer_sequence_get_length(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_get_length", 1, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + return RValue_makeReal(seq != nullptr ? seq->asset->length : -1); +} + +static RValue builtin_layer_sequence_get_x(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_get_x", 1, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + return RValue_makeReal(seq != nullptr ? seq->x : -1); +} + +static RValue builtin_layer_sequence_get_y(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_get_y", 1, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + return RValue_makeReal(seq != nullptr ? seq->y : -1); +} + +static RValue builtin_layer_sequence_x(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("layer_sequence_x", 2, RValue_makeReal(-1)); + RuntimeSequence* seq = Sequence_find(ctx->runner, RValue_toInt32(args[0])); + if (seq != nullptr) { + seq->x = (float)RValue_toReal(args[1]); + seq->transformDirty = true; + } + return RValue_makeReal(-1); +} + static RValue builtin_layer_script_end(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { int32_t layerId = resolveLayerIdArg(ctx->runner, args[0]); int32_t codeIndex; @@ -18589,54 +18665,6 @@ static RValue builtin_animcurve_get_channel_index(VMContext* ctx, RValue* args, return RValue_makeReal(-1.0); } -// Catmull-Rom interpolation: 4 control points P0..P3, parameter t in [0,1] from P1 to P2. -static float animcurveCatmullRom(float p0, float p1, float p2, float p3, float t) { - float t2 = t * t; - float t3 = t2 * t; - return 0.5f * ((2.0f * p1) + - (-p0 + p2) * t + - (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + - (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3); -} - -static float animcurveChannelEvaluate(const AnimCurveChannel* ch, float x) { - if (ch == nullptr || ch->pointCount == 0) return 0.0f; - if (ch->pointCount == 1) return ch->points[0].value; - - // Clamp to the channel's x range - float x0 = ch->points[0].x; - float xn = ch->points[ch->pointCount - 1].x; - if (x0 > x) return ch->points[0].value; - if (x > xn) return ch->points[ch->pointCount - 1].value; - - // Binary search for the interval [i, i+1] containing x - uint32_t lo = 0; - uint32_t hi = ch->pointCount - 1; - while (hi - lo > 1) { - uint32_t mid = (lo + hi) / 2; - if (ch->points[mid].x <= x) lo = mid; - else hi = mid; - } - - const AnimCurvePoint* a = &ch->points[lo]; - const AnimCurvePoint* b = &ch->points[lo + 1]; - float span = b->x - a->x; - if (0.0f >= span) return a->value; - float t = (x - a->x) / span; - - if (ch->curveType == ANIMCURVE_TYPE_SMOOTH) { - // Catmull-Rom on the value channel, using neighbor points clamped at the ends - float p0 = (lo > 0) ? ch->points[lo - 1].value : a->value; - float p1 = a->value; - float p2 = b->value; - float p3 = (lo + 2 < ch->pointCount) ? ch->points[lo + 2].value : b->value; - return animcurveCatmullRom(p0, p1, p2, p3, t); - } - - // Linear (also used as fallback for bezier; full bezier handle eval not yet implemented) - return a->value + (b->value - a->value) * t; -} - // animcurve_channel_evaluate(channel_handle, posx) static RValue builtin_animcurve_channel_evaluate(VMContext* ctx, RValue* args, int32_t argCount) { REQUIRE_ARGC_AT_LEAST("animcurve_channel_evaluate", 2, RValue_makeReal(0.0)); @@ -18646,7 +18674,7 @@ static RValue builtin_animcurve_channel_evaluate(VMContext* ctx, RValue* args, i if (0 > handle || (uint32_t) handle >= a->allChannelsCount) return RValue_makeReal(0.0); AnimCurveChannel* ch = a->allChannels[handle]; float x = (float) RValue_toReal(args[1]); - return RValue_makeReal((GMLReal) animcurveChannelEvaluate(ch, x)); + return RValue_makeReal((GMLReal)AnimCurveChannel_evaluate(ch, x)); } // ===[ MP_GRID FUNCTIONS ]=== @@ -25451,6 +25479,17 @@ void VMBuiltins_registerAll(VMContext* ctx) { VM_registerBuiltin(ctx, "layer_create", builtin_layer_create); VM_registerBuiltin(ctx, "layer_destroy", builtin_layer_destroy); VM_registerBuiltin(ctx, "layer_script_begin", builtin_layer_script_begin); + VM_registerBuiltin(ctx, "layer_sequence_create", builtin_layer_sequence_create); + VM_registerBuiltin(ctx, "layer_sequence_destroy", builtin_layer_sequence_destroy); + VM_registerBuiltin(ctx, "layer_sequence_play", builtin_layer_sequence_play); + VM_registerBuiltin(ctx, "layer_sequence_pause", builtin_layer_sequence_pause); + VM_registerBuiltin(ctx, "layer_sequence_is_finished", builtin_layer_sequence_is_finished); + VM_registerBuiltin(ctx, "layer_sequence_is_paused", builtin_layer_sequence_is_paused); + VM_registerBuiltin(ctx, "layer_sequence_get_headpos", builtin_layer_sequence_get_headpos); + VM_registerBuiltin(ctx, "layer_sequence_get_length", builtin_layer_sequence_get_length); + VM_registerBuiltin(ctx, "layer_sequence_get_x", builtin_layer_sequence_get_x); + VM_registerBuiltin(ctx, "layer_sequence_get_y", builtin_layer_sequence_get_y); + VM_registerBuiltin(ctx, "layer_sequence_x", builtin_layer_sequence_x); VM_registerBuiltin(ctx, "layer_script_end", builtin_layer_script_end); VM_registerBuiltin(ctx, "layer_background_create", builtin_layer_background_create); VM_registerBuiltin(ctx, "layer_background_exists", builtin_layer_background_exists); From 359882e5972081d57b591595fc7435363ce116e8 Mon Sep 17 00:00:00 2001 From: "casriel." <232106036+casrielasriel@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:57:53 -0700 Subject: [PATCH 2/3] fix building --- src/data_win.c | 15 ++++++++------- src/runner.c | 4 ++-- src/sequence.c | 43 ++++++++++++++++++++++--------------------- src/vm_builtins.c | 10 +++++----- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/src/data_win.c b/src/data_win.c index 8c5080ea9..26d1533e8 100644 --- a/src/data_win.c +++ b/src/data_win.c @@ -1170,11 +1170,12 @@ static void readSequenceKeys(BinaryReader* reader, DataWin* dw, SequenceTrackTyp break; } case SEQUENCE_TRACK_MESSAGE: - case SEQUENCE_TRACK_MOMENT: + case SEQUENCE_TRACK_MOMENT: { channel->messageCount = BinaryReader_readUint32(reader); channel->messages = (const char**)safeCalloc(channel->messageCount, sizeof(const char*)); repeat(channel->messageCount, m) channel->messages[m] = readStringPtr(reader, dw); break; + } case SEQUENCE_TRACK_TEXT: channel->text = readStringPtr(reader, dw); channel->wrap = BinaryReader_readBool32(reader); @@ -1205,9 +1206,9 @@ static bool readSequenceTrack(BinaryReader* reader, DataWin* dw, SequenceTrack* "GMBoolTrack", "GMStringTrack", "GMSpriteFramesTrack" }; bool found = false; - repeat(sizeof(models) / sizeof(models[0]), t) { - if (strcmp(model, models[t]) == 0) { - track->type = (SequenceTrackType)t; + repeat(sizeof(models) / sizeof(models[0]), modelIndex) { + if (strcmp(model, models[modelIndex]) == 0) { + track->type = (SequenceTrackType)modelIndex; found = true; break; } @@ -1225,7 +1226,7 @@ static bool readSequenceTrack(BinaryReader* reader, DataWin* dw, SequenceTrack* uint32_t tagCount = BinaryReader_readUint32(reader); track->curveCount = BinaryReader_readUint32(reader); track->trackCount = BinaryReader_readUint32(reader); - repeat(tagCount, t) { + repeat(tagCount, tagIndex) { int32_t tag = BinaryReader_readInt32(reader); if (tag == 1) track->visible = false; else if (tag == 2) track->enabled = false; @@ -1237,8 +1238,8 @@ static bool readSequenceTrack(BinaryReader* reader, DataWin* dw, SequenceTrack* readAnimCurve(reader, dw, &track->curves[c], true); } track->tracks = (SequenceTrack*)safeCalloc(track->trackCount, sizeof(SequenceTrack)); - repeat(track->trackCount, t) { - if (!readSequenceTrack(reader, dw, &track->tracks[t])) return false; + repeat(track->trackCount, trackIndex) { + if (!readSequenceTrack(reader, dw, &track->tracks[trackIndex])) return false; } if (track->type == SEQUENCE_TRACK_GROUP) return true; if (track->type == SEQUENCE_TRACK_REAL || track->type == SEQUENCE_TRACK_COLOR) { diff --git a/src/runner.c b/src/runner.c index dce0d3971..a4feef1ad 100644 --- a/src/runner.c +++ b/src/runner.c @@ -1778,8 +1778,8 @@ static void initRoom(Runner* runner, int32_t roomIndex) { if (layerSource->type != RoomLayerType_Assets || layerSource->assetsData == nullptr) continue; RoomLayerAssetsData* assets = layerSource->assetsData; RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; - repeat(assets->sequenceCount, j) { - SequenceInstance* src = &assets->sequences[j]; + repeat(assets->sequenceCount, sequenceIndex) { + SequenceInstance* src = &assets->sequences[sequenceIndex]; int32_t id = Sequence_create(runner, (int32_t)layerSource->id, src->sequenceIndex, (float)src->x, (float)src->y); RuntimeSequence* seq = Sequence_find(runner, id); if (seq != nullptr) { diff --git a/src/sequence.c b/src/sequence.c index e490cee18..5f394d4bc 100644 --- a/src/sequence.c +++ b/src/sequence.c @@ -43,11 +43,11 @@ static void evaluateSequence(RuntimeSequence* seq, const Matrix4f* parent, const static float smoothCurveValue(const AnimCurveChannel* ch, uint32_t lo, float x) { float minimum = ch->points[0].value, maximum = minimum; - for (uint32_t i = 1; i < ch->pointCount; i++) { - minimum = fminf(minimum, ch->points[i].value); - maximum = fmaxf(maximum, ch->points[i].value); + for (uint32_t pointIndex = 1; pointIndex < ch->pointCount; pointIndex++) { + minimum = (float)GMLReal_fmin(minimum, ch->points[pointIndex].value); + maximum = (float)GMLReal_fmax(maximum, ch->points[pointIndex].value); } - float scale = fmaxf(1.0f, maximum - minimum); + float scale = (float)GMLReal_fmax(1.0f, maximum - minimum); uint32_t indices[4] = {lo > 0 ? lo - 1 : lo, lo, lo + 1, lo + 2 < ch->pointCount ? lo + 2 : lo + 1}; float px[4], py[4], times[4] = {0}; @@ -62,21 +62,21 @@ static float smoothCurveValue(const AnimCurveChannel* ch, uint32_t lo, float x) } uint32_t iterations = ch->iterations > 0 ? ch->iterations : 1; float lastX = px[1], lastY = py[1]; - for (uint32_t i = 1; i <= iterations; i++) { - float t = times[1] + (times[2] - times[1]) * ((float)i / iterations); + for (uint32_t sampleIndex = 1; sampleIndex <= iterations; sampleIndex++) { + float t = times[1] + (times[2] - times[1]) * ((float)sampleIndex / iterations); float ax[3], ay[3], bx[2], by[2]; repeat(3, j) { float f = (t - times[j]) / (times[j + 1] - times[j]); ax[j] = px[j] + (px[j + 1] - px[j]) * f; ay[j] = py[j] + (py[j + 1] - py[j]) * f; } - repeat(2, j) { - float f = (t - times[j]) / (times[j + 2] - times[j]); - bx[j] = ax[j] + (ax[j + 1] - ax[j]) * f; - by[j] = ay[j] + (ay[j + 1] - ay[j]) * f; + repeat(2, bIndex) { + float f = (t - times[bIndex]) / (times[bIndex + 2] - times[bIndex]); + bx[bIndex] = ax[bIndex] + (ax[bIndex + 1] - ax[bIndex]) * f; + by[bIndex] = ay[bIndex] + (ay[bIndex + 1] - ay[bIndex]) * f; } float f = (t - times[1]) / (times[2] - times[1]); - float cx = fminf(px[2], fmaxf(lastX, bx[0] + (bx[1] - bx[0]) * f)); + float cx = (float)GMLReal_fmin(px[2], GMLReal_fmax(lastX, bx[0] + (bx[1] - bx[0]) * f)); float cy = by[0] + (by[1] - by[0]) * f; if (cx >= x && cx > lastX) { return (lastY + (cy - lastY) * (x - lastX) / (cx - lastX)) * scale + minimum; @@ -215,9 +215,10 @@ static SequenceValues evaluateValues(RuntimeSequence* seq, const SequenceTrack* case 5: value.gain = x; break; case 6: value.pitch = x; break; case 8: value.rotation = x; break; - case 10: + case 10: { repeat(4, c) value.color[c] = evaluateParameter(seq->runner, parameter, 0, c, at, seq->asset->length, 1.0f); break; + } case 14: value.x = x; value.y = y; break; case 15: value.scaleX = x; value.scaleY = y; break; case 16: value.originX = x; value.originY = y; break; @@ -352,7 +353,7 @@ void Sequence_play(RuntimeSequence* seq) { if (seq == nullptr) return; Sequence_setPaused(seq, false); if (seq->finished) { - seq->headPosition = seq->direction < 0.0f ? fmaxf(0.0f, seq->asset->length - 1.0f) : 0.0f; + seq->headPosition = seq->direction < 0.0f ? (float)GMLReal_fmax(0.0f, seq->asset->length - 1.0f) : 0.0f; seq->finished = false; } } @@ -366,7 +367,7 @@ static SequenceNode* createNodes(RuntimeSequence* seq, const SequenceTrack* trac node->activeKey = -1; node->children = createNodes(seq, tracks[t].tracks, tracks[t].trackCount, depth + 1); if (isParameter(node->track)) continue; - for (uint32_t k = 0; k < tracks[t].keyframeCount; k++) node->stateCount += tracks[t].keyframes[k].channelCount; + for (uint32_t keyIndex = 0; keyIndex < tracks[t].keyframeCount; keyIndex++) node->stateCount += tracks[t].keyframes[keyIndex].channelCount; node->states = (SequenceChannelState*)safeCalloc(node->stateCount, sizeof(SequenceChannelState)); uint32_t index = 0; for (uint32_t k = 0; k < tracks[t].keyframeCount; k++) { @@ -476,7 +477,7 @@ static float imageSpeedDistance(RuntimeSequence* seq, const SequenceTrack* track distance += (next - position) * speed; position = next; } - return fmaxf(0.0f, distance); + return (float)GMLReal_fmax(0.0f, distance); } static float spriteFrame(RuntimeSequence* seq, const SequenceNode* node, int32_t spriteIndex, float elapsed) { @@ -500,7 +501,7 @@ static float spriteFrame(RuntimeSequence* seq, const SequenceNode* node, int32_t static uint32_t packedColor(const float* color) { uint32_t result = 0; - repeat(3, c) result |= (uint32_t)(fminf(1.0f, fmaxf(0.0f, color[c])) * 255.0f) << (c * 8); + repeat(3, c) result |= (uint32_t)(GMLReal_fmin(1.0f, GMLReal_fmax(0.0f, color[c])) * 255.0f) << (c * 8); return result; } @@ -564,8 +565,8 @@ static void evaluateNodes(RuntimeSequence* seq, SequenceNode* nodes, uint32_t co inst->sequenceControlled = true; if (!active) continue; Matrix4f_transformPoint(&node->matrix, node->values.originX, node->values.originY, &inst->x, &inst->y); - inst->imageAngle = -atan2f(node->matrix.m[1], node->matrix.m[0]) * (180.0f / (float)M_PI); - inst->imageXscale = hypotf(node->matrix.m[0], node->matrix.m[1]); + inst->imageAngle = (float)(-atan2(node->matrix.m[1], node->matrix.m[0]) * (180.0 / M_PI)); + inst->imageXscale = (float)sqrt((double)node->matrix.m[0] * node->matrix.m[0] + (double)node->matrix.m[1] * node->matrix.m[1]); float determinant = node->matrix.m[0]*node->matrix.m[5] - node->matrix.m[1]*node->matrix.m[4]; inst->imageYscale = inst->imageXscale != 0.0f ? determinant / inst->imageXscale : 0.0f; inst->imageBlend = packedColor(node->values.color); @@ -717,7 +718,7 @@ void Sequence_stepRoom(Runner* runner) { } if (runner->pendingRoom >= 0) break; } - repeat(arrlenu(snapshot), s) Sequence_release(snapshot[s]); + repeat(arrlenu(snapshot), releaseIndex) Sequence_release(snapshot[releaseIndex]); arrsetlen(snapshot, 0); arrsetlen(events, 0); runner->sequenceSnapshots = snapshot; @@ -735,7 +736,7 @@ static void drawSequenceText(Renderer* renderer, const SequenceChannel* channel, if (channel->wrap && values->frameWidth > 0.0f) wrapWidth = (int32_t)values->frameWidth; if (!state->textPrepared || state->wrapWidth != wrapWidth) { PreprocessedText_free(state->text); - state->text = (PreprocessedText){0}; + ZERO_STRUCT(state->text); if (wrapWidth >= 0) state->text = TextUtils_wrapText(font, channel->text, wrapWidth); else state->text.text = channel->text; state->wrapWidth = wrapWidth; @@ -770,7 +771,7 @@ static void drawSequenceText(Renderer* renderer, const SequenceChannel* channel, int32_t end = start; while (end < length && str[end] != '\r' && str[end] != '\n') end++; float width = TextUtils_measureLineWidth(font, str + start, end - start); - width += values->characterSpacing * fmaxf(0.0f, (float)TextUtils_utf8CodepointCount(str + start, end - start) - 1.0f); + width += values->characterSpacing * (float)GMLReal_fmax(0.0f, (float)TextUtils_utf8CodepointCount(str + start, end - start) - 1.0f); int32_t align = channel->alignment & 255; float x = align == 1 ? (values->frameWidth - width) * 0.5f : align == 2 ? values->frameWidth - width : 0.0f; int32_t pos = start; diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 1fd600343..78d57c495 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -16258,14 +16258,14 @@ static RValue builtin_layer_destroy(VMContext* ctx, RValue* args, MAYBE_UNUSED i if (layer == nullptr) return RValue_makeUndefined(); int32_t* sequenceIds = nullptr; int32_t* instanceIds = nullptr; - repeat(arrlenu(layer->elements), e) { - RuntimeLayerElement* el = &layer->elements[e]; + repeat(arrlenu(layer->elements), elementIndex) { + RuntimeLayerElement* el = &layer->elements[elementIndex]; if (el->type == RuntimeLayerElementType_Sequence) arrput(sequenceIds, (int32_t)el->id); else if (el->type == RuntimeLayerElementType_Instance) arrput(instanceIds, el->instanceId); } - repeat(arrlenu(sequenceIds), e) Sequence_destroy(runner, sequenceIds[e]); - repeat(arrlenu(instanceIds), e) { - Instance* inst = hmget(runner->instancesById, instanceIds[e]); + repeat(arrlenu(sequenceIds), sequenceIndex) Sequence_destroy(runner, sequenceIds[sequenceIndex]); + repeat(arrlenu(instanceIds), instanceIndex) { + Instance* inst = hmget(runner->instancesById, instanceIds[instanceIndex]); if (inst != nullptr && !inst->destroyed) Runner_destroyInstance(runner, inst, true); } arrfree(sequenceIds); From 59e3a876d2db20e84ff7e1361405c0337d9cc634 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Mon, 2 Nov 2026 15:17:11 +0000 Subject: [PATCH 3/3] try fix ps2 --- src/sequence.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sequence.c b/src/sequence.c index 5f394d4bc..020ca55b8 100644 --- a/src/sequence.c +++ b/src/sequence.c @@ -565,8 +565,8 @@ static void evaluateNodes(RuntimeSequence* seq, SequenceNode* nodes, uint32_t co inst->sequenceControlled = true; if (!active) continue; Matrix4f_transformPoint(&node->matrix, node->values.originX, node->values.originY, &inst->x, &inst->y); - inst->imageAngle = (float)(-atan2(node->matrix.m[1], node->matrix.m[0]) * (180.0 / M_PI)); - inst->imageXscale = (float)sqrt((double)node->matrix.m[0] * node->matrix.m[0] + (double)node->matrix.m[1] * node->matrix.m[1]); + inst->imageAngle = -GMLReal_atan2(node->matrix.m[1], node->matrix.m[0]) * (180.0 / M_PI); + inst->imageXscale = sqrtf(node->matrix.m[0] * node->matrix.m[0] + node->matrix.m[1] * node->matrix.m[1]); float determinant = node->matrix.m[0]*node->matrix.m[5] - node->matrix.m[1]*node->matrix.m[4]; inst->imageYscale = inst->imageXscale != 0.0f ? determinant / inst->imageXscale : 0.0f; inst->imageBlend = packedColor(node->values.color);