From 375c496791519a90def8433db5364a5a6391c2e8 Mon Sep 17 00:00:00 2001 From: foxtacles Date: Sun, 5 Apr 2026 10:43:43 -0700 Subject: [PATCH] Add rabbits extension with two animated rabbits on the mountain top (#791) Add SiLoader EnableWith directive and HandleEnable hook to trigger loading when a world becomes active. Build rabbits.si from pre-built model (.mod) and animation (.ani) assets using the same ReplaceWithFile pattern as other asset generators. Two rabbits (Blaubart and Fluse) hop between plants with synchronized paths, ear twitching, and tail wagging. --- .gitattributes | 1 + LEGO1/lego/legoomni/src/entity/legoworld.cpp | 4 + assets/main.cpp | 117 +++++++++++++++++++ assets/rabbits/blaubart.mod | 3 + assets/rabbits/fluse.mod | 3 + assets/rabbits/rabbits.ani | 3 + extensions/include/extensions/siloader.h | 5 + extensions/src/siloader.cpp | 65 ++++++++++- 8 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 assets/rabbits/blaubart.mod create mode 100644 assets/rabbits/fluse.mod create mode 100644 assets/rabbits/rabbits.ani diff --git a/.gitattributes b/.gitattributes index 430dfc15..a2ec39dd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ **/*.desktop text eol=lf assets/widescreen/** filter=lfs diff=lfs merge=lfs -text assets/hdmusic/** filter=lfs diff=lfs merge=lfs -text +assets/rabbits/** filter=lfs diff=lfs merge=lfs -text diff --git a/LEGO1/lego/legoomni/src/entity/legoworld.cpp b/LEGO1/lego/legoomni/src/entity/legoworld.cpp index ab141f38..7d920223 100644 --- a/LEGO1/lego/legoomni/src/entity/legoworld.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoworld.cpp @@ -751,6 +751,8 @@ void LegoWorld::Enable(MxBool p_enable) AnimationManager()->Resume(); } + Extension::Call(Extensions::SI::HandleEnable, this, TRUE); + GameState()->ResetROI(); #ifndef BETA10 SetIsWorldActive(TRUE); @@ -760,6 +762,8 @@ void LegoWorld::Enable(MxBool p_enable) Extension::Call(MP::HandleWorldEnable, this, TRUE); } else if (!p_enable && m_disabledObjects.size() == 0) { + Extension::Call(Extensions::SI::HandleEnable, this, FALSE); + MxPresenter* presenter; LegoPathController* controller; LegoPathActor* actor = UserActor(); diff --git a/assets/main.cpp b/assets/main.cpp index 0900f715..13969f64 100644 --- a/assets/main.cpp +++ b/assets/main.cpp @@ -6,6 +6,22 @@ #include #include +void ZeroInitObject(si::Object* o) +{ + o->unknown1_ = 0; + o->flags_ = 0; + o->unknown4_ = 0; + o->duration_ = 0; + o->loops_ = 0; + o->unknown26_ = 0; + o->unknown27_ = 0; + o->unknown28_ = 0; + o->unknown29_ = 0; + o->unknown30_ = 0; + o->volume_ = 0; + o->time_offset_ = 0; +} + si::Interleaf::Version version = si::Interleaf::Version2_2; uint32_t bufferSize = 65536; uint32_t bufferCount = 8; @@ -293,6 +309,106 @@ void CreateBadEnd() si.Write(result.c_str()); } +void CreateRabbits() +{ + std::string result = out + "/rabbits.si"; + + si::Interleaf si; + mxHd.seek(0, si::MemoryBuffer::SeekStart); + si.Read(&mxHd); + + // Object 0 (id=0): Blaubart model (fire-and-forget, loaded by chained Prepend) + si::Object* blaubart = new si::Object; + ZeroInitObject(blaubart); + std::string file = std::string("rabbits/blaubart.mod"); + std::string extra = "Prepend:/lego/extra/rabbits;1"; + blaubart->id_ = 0; + blaubart->type_ = si::MxOb::Object; + blaubart->presenter_ = "LegoModelPresenter"; + blaubart->name_ = "blaubart"; + blaubart->flags_ = MxDSAction::c_enabled; + blaubart->filetype_ = si::MxOb::OBJ; + blaubart->loops_ = 1; + blaubart->unknown28_ = 1; + blaubart->unknown29_ = 1; + blaubart->location_ = si::Vector3(-65.5f, 14.0f, 23.5f); + blaubart->direction_ = si::Vector3(0, 0, 1); + blaubart->up_ = si::Vector3(0, 1, 0); + blaubart->extra_ = si::bytearray(extra.c_str(), extra.length() + 1); + if (!blaubart->ReplaceWithFile(file.c_str())) { + abort(); + } + + si.AppendChild(blaubart); + depfile << result << ": " << (std::filesystem::current_path() / file).string() << std::endl; + + // Object 1 (id=1): Fluse model (fire-and-forget, loaded by chained Prepend from Blaubart) + si::Object* fluse = new si::Object; + ZeroInitObject(fluse); + file = std::string("rabbits/fluse.mod"); + fluse->id_ = 1; + fluse->type_ = si::MxOb::Object; + fluse->presenter_ = "LegoModelPresenter"; + fluse->name_ = "fluse"; + fluse->flags_ = MxDSAction::c_enabled; + fluse->filetype_ = si::MxOb::OBJ; + fluse->loops_ = 1; + fluse->unknown28_ = 1; + fluse->unknown29_ = 1; + fluse->location_ = si::Vector3(-62.0f, 14.0f, 28.0f); + fluse->direction_ = si::Vector3(0, 0, 1); + fluse->up_ = si::Vector3(0, 1, 0); + if (!fluse->ReplaceWithFile(file.c_str())) { + abort(); + } + + si.AppendChild(fluse); + depfile << result << ": " << (std::filesystem::current_path() / file).string() << std::endl; + + // Object 2 (id=2): LegoAnimMMPresenter (composite) + // EnableWith triggers when Isle world becomes active + // Prepend chains: loads Blaubart model (id=0), which chains to Fluse model (id=1) + si::Object* composite = new si::Object; + ZeroInitObject(composite); + extra = "EnableWith:\\Lego\\Scripts\\Isle\\Isle;0, Prepend:/lego/extra/rabbits;0"; + composite->id_ = 2; + composite->type_ = si::MxOb::Presenter; + composite->presenter_ = "LegoAnimMMPresenter"; + composite->flags_ = MxDSAction::c_enabled | MxDSAction::c_bit3; + composite->loops_ = 1; + composite->extra_ = si::bytearray(extra.c_str(), extra.length() + 1); + composite->name_ = "rabbits_composite"; + composite->direction_ = si::Vector3(0, 0, 1); + composite->up_ = si::Vector3(0, 1, 0); + si.AppendChild(composite); + + // Child (id=3): Combined animation (both rabbits in one anim tree) + si::Object* anim = new si::Object; + ZeroInitObject(anim); + file = std::string("rabbits/rabbits.ani"); + anim->id_ = 3; + anim->type_ = si::MxOb::Object; + anim->presenter_ = "LegoLoopingAnimPresenter"; + anim->name_ = "rabbits_anim"; + anim->flags_ = MxDSAction::c_enabled | si::MxOb::NoLoop; + anim->filetype_ = si::MxOb::OBJ; + anim->duration_ = -1; + anim->loops_ = 1; + anim->unknown28_ = 1; + anim->unknown29_ = 1; + anim->location_ = si::Vector3(0, 0, 0); + anim->direction_ = si::Vector3(0, 0, 1); + anim->up_ = si::Vector3(0, 1, 0); + if (!anim->ReplaceWithFile(file.c_str())) { + abort(); + } + + composite->AppendChild(anim); + depfile << result << ": " << (std::filesystem::current_path() / file).string() << std::endl; + + si.Write(result.c_str()); +} + int main(int argc, char* argv[]) { out = argv[1]; @@ -307,5 +423,6 @@ int main(int argc, char* argv[]) CreateWidescreen(); CreateHDMusic(); CreateBadEnd(); + CreateRabbits(); return 0; } diff --git a/assets/rabbits/blaubart.mod b/assets/rabbits/blaubart.mod new file mode 100644 index 00000000..eb31fb8f --- /dev/null +++ b/assets/rabbits/blaubart.mod @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac169fa2b91c3cda2748a1164a05d4d9a4e1daabe01377512a509e2f406925b0 +size 1686652 diff --git a/assets/rabbits/fluse.mod b/assets/rabbits/fluse.mod new file mode 100644 index 00000000..fe41c7ee --- /dev/null +++ b/assets/rabbits/fluse.mod @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb4093239917c305075a8ed952b264cbfa504210c5a67da616756342f57f7f36 +size 1687240 diff --git a/assets/rabbits/rabbits.ani b/assets/rabbits/rabbits.ani new file mode 100644 index 00000000..181708bd --- /dev/null +++ b/assets/rabbits/rabbits.ani @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5687e8f5b0cd15d19740b0b04724b5bf994e1a0e9659f556429c268fea280ebd +size 16334 diff --git a/extensions/include/extensions/siloader.h b/extensions/include/extensions/siloader.h index 5a06074d..9f03bb01 100644 --- a/extensions/include/extensions/siloader.h +++ b/extensions/include/extensions/siloader.h @@ -27,6 +27,7 @@ class SiLoaderExt { static std::optional HandleRemove(StreamObject p_object, LegoWorld* p_world); static std::optional HandleDelete(MxDSAction& p_action); static MxBool HandleEndAction(MxEndActionNotificationParam& p_param); + static MxBool HandleEnable(LegoWorld* p_world, MxBool p_enable); template static std::optional ReplacedIn(MxDSAction& p_action, Args... p_args); @@ -41,10 +42,12 @@ class SiLoaderExt { static std::vector directives; static std::vector> startWith; static std::vector> removeWith; + static std::vector> enableWith; static std::vector> replace; static std::vector> prepend; static std::vector fullScreenMovie; static std::vector disable3d; + static std::vector firedPrepend; static bool LoadFile(const char* p_file); static bool LoadDirective(const char* p_directive); @@ -84,6 +87,7 @@ constexpr auto HandleWorld = &SiLoaderExt::HandleWorld; constexpr auto HandleRemove = &SiLoaderExt::HandleRemove; constexpr auto HandleDelete = &SiLoaderExt::HandleDelete; constexpr auto HandleEndAction = &SiLoaderExt::HandleEndAction; +constexpr auto HandleEnable = &SiLoaderExt::HandleEnable; constexpr auto ReplacedIn = [](auto&&... args) { return SiLoaderExt::ReplacedIn(std::forward(args)...); }; @@ -95,6 +99,7 @@ constexpr decltype(&SiLoaderExt::HandleWorld) HandleWorld = nullptr; constexpr decltype(&SiLoaderExt::HandleRemove) HandleRemove = nullptr; constexpr decltype(&SiLoaderExt::HandleDelete) HandleDelete = nullptr; constexpr decltype(&SiLoaderExt::HandleEndAction) HandleEndAction = nullptr; +constexpr decltype(&SiLoaderExt::HandleEnable) HandleEnable = nullptr; constexpr auto ReplacedIn = [](auto&&... args) -> std::optional { ((void) args, ...); return std::nullopt; diff --git a/extensions/src/siloader.cpp b/extensions/src/siloader.cpp index e09981a9..4255a5c7 100644 --- a/extensions/src/siloader.cpp +++ b/extensions/src/siloader.cpp @@ -19,10 +19,12 @@ std::vector SiLoaderExt::files; std::vector SiLoaderExt::directives; std::vector> SiLoaderExt::startWith; std::vector> SiLoaderExt::removeWith; +std::vector> SiLoaderExt::enableWith; std::vector> SiLoaderExt::replace; std::vector> SiLoaderExt::prepend; std::vector SiLoaderExt::fullScreenMovie; std::vector SiLoaderExt::disable3d; +std::vector SiLoaderExt::firedPrepend; bool SiLoaderExt::enabled = false; void SiLoaderExt::Initialize() @@ -108,6 +110,10 @@ std::optional SiLoaderExt::HandleStart(MxDSAction& p_action) if (p_action.GetExtraLength() == 0 || !SDL_strstr(p_action.GetExtraData(), prependedMarker)) { for (const auto& key : prepend) { if (key.first == object) { + auto it = std::find(firedPrepend.begin(), firedPrepend.end(), key.second); + if (it != firedPrepend.end()) { + firedPrepend.erase(it); + } MxDSAction action; MxResult result = start(key.second, p_action, action); @@ -155,6 +161,46 @@ MxBool SiLoaderExt::HandleWorld(LegoWorld* p_world) return TRUE; } +MxBool SiLoaderExt::HandleEnable(LegoWorld* p_world, MxBool p_enable) +{ + StreamObject object{p_world->GetAtomId(), p_world->GetEntityId()}; + + if (p_enable) { + firedPrepend.clear(); + + auto start = [](const StreamObject& p_object, MxDSAction& p_out) { + if (!OpenStream(p_object.first.GetInternal())) { + return; + } + + p_out.SetAtomId(p_object.first); + p_out.SetObjectId(p_object.second); + p_out.SetUnknown24(-1); + Start(&p_out); + }; + + for (const auto& key : enableWith) { + if (key.first == object) { + MxDSAction action; + start(key.second, action); + } + } + } + else { + for (const auto& key : enableWith) { + if (key.first == object) { + MxDSAction action; + action.SetAtomId(key.second.first); + action.SetObjectId(key.second.second); + action.SetUnknown24(-1); + DeleteObject(action); + } + } + } + + return TRUE; +} + std::optional SiLoaderExt::HandleRemove(StreamObject p_object, LegoWorld* p_world) { for (const auto& key : removeWith) { @@ -226,7 +272,9 @@ MxBool SiLoaderExt::HandleEndAction(MxEndActionNotificationParam& p_param) if (!p_param.GetSender() || !p_param.GetSender()->IsA("MxCompositePresenter")) { for (const auto& key : prepend) { - if (key.second == object) { + if (key.second == object && + std::find(firedPrepend.begin(), firedPrepend.end(), object) == firedPrepend.end()) { + firedPrepend.push_back(object); MxDSAction action; start(key.first, *p_param.GetAction(), action); } @@ -280,6 +328,12 @@ bool SiLoaderExt::LoadDirective(const char* p_directive) StreamObject{MxAtomId{targetAtom, e_lowerCase2}, targetId} ); } + else if (SDL_sscanf(p_directive, "EnableWith:%255[^:;]%*[:;]%u%*[:;]%255[^:;]%*[:;]%u", originAtom, &originId, targetAtom, &targetId) == 4) { + enableWith.emplace_back( + StreamObject{MxAtomId{originAtom, e_lowerCase2}, originId}, + StreamObject{MxAtomId{targetAtom, e_lowerCase2}, targetId} + ); + } else if (SDL_sscanf(p_directive, "Replace:%255[^:;]%*[:;]%u%*[:;]%255[^:;]%*[:;]%u", originAtom, &originId, targetAtom, &targetId) == 4) { replace.emplace_back( StreamObject{MxAtomId{originAtom, e_lowerCase2}, originId}, @@ -342,6 +396,15 @@ void SiLoaderExt::ParseExtra(const MxAtomId& p_atom, si::Core* p_core) } } + if ((directive = SDL_strstr(extra.c_str(), "EnableWith:"))) { + if (SDL_sscanf(directive, "EnableWith:%255[^:;]%*[:;]%u", atom, &id) == 2) { + enableWith.emplace_back( + StreamObject{MxAtomId{atom, e_lowerCase2}, id}, + StreamObject{p_atom, object->id_} + ); + } + } + if ((directive = SDL_strstr(extra.c_str(), "Replace:"))) { if (SDL_sscanf(directive, "Replace:%255[^:;]%*[:;]%u", atom, &id) == 2) { replace.emplace_back(