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.
This commit is contained in:
foxtacles 2026-04-05 10:43:43 -07:00 committed by GitHub
parent 8215544b02
commit 375c496791
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 200 additions and 1 deletions

1
.gitattributes vendored
View File

@ -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

View File

@ -751,6 +751,8 @@ void LegoWorld::Enable(MxBool p_enable)
AnimationManager()->Resume();
}
Extension<Extensions::SiLoaderExt>::Call(Extensions::SI::HandleEnable, this, TRUE);
GameState()->ResetROI();
#ifndef BETA10
SetIsWorldActive(TRUE);
@ -760,6 +762,8 @@ void LegoWorld::Enable(MxBool p_enable)
Extension<MultiplayerExt>::Call(MP::HandleWorldEnable, this, TRUE);
}
else if (!p_enable && m_disabledObjects.size() == 0) {
Extension<Extensions::SiLoaderExt>::Call(Extensions::SI::HandleEnable, this, FALSE);
MxPresenter* presenter;
LegoPathController* controller;
LegoPathActor* actor = UserActor();

View File

@ -6,6 +6,22 @@
#include <interleaf.h>
#include <object.h>
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;
}

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac169fa2b91c3cda2748a1164a05d4d9a4e1daabe01377512a509e2f406925b0
size 1686652

3
assets/rabbits/fluse.mod Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb4093239917c305075a8ed952b264cbfa504210c5a67da616756342f57f7f36
size 1687240

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5687e8f5b0cd15d19740b0b04724b5bf994e1a0e9659f556429c268fea280ebd
size 16334

View File

@ -27,6 +27,7 @@ class SiLoaderExt {
static std::optional<MxBool> HandleRemove(StreamObject p_object, LegoWorld* p_world);
static std::optional<MxBool> HandleDelete(MxDSAction& p_action);
static MxBool HandleEndAction(MxEndActionNotificationParam& p_param);
static MxBool HandleEnable(LegoWorld* p_world, MxBool p_enable);
template <typename... Args>
static std::optional<StreamObject> ReplacedIn(MxDSAction& p_action, Args... p_args);
@ -41,10 +42,12 @@ class SiLoaderExt {
static std::vector<std::string> directives;
static std::vector<std::pair<StreamObject, StreamObject>> startWith;
static std::vector<std::pair<StreamObject, StreamObject>> removeWith;
static std::vector<std::pair<StreamObject, StreamObject>> enableWith;
static std::vector<std::pair<StreamObject, StreamObject>> replace;
static std::vector<std::pair<StreamObject, StreamObject>> prepend;
static std::vector<StreamObject> fullScreenMovie;
static std::vector<StreamObject> disable3d;
static std::vector<StreamObject> 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<decltype(args)>(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<SiLoaderExt::StreamObject> {
((void) args, ...);
return std::nullopt;

View File

@ -19,10 +19,12 @@ std::vector<std::string> SiLoaderExt::files;
std::vector<std::string> SiLoaderExt::directives;
std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::startWith;
std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::removeWith;
std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::enableWith;
std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::replace;
std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::prepend;
std::vector<SiLoaderExt::StreamObject> SiLoaderExt::fullScreenMovie;
std::vector<SiLoaderExt::StreamObject> SiLoaderExt::disable3d;
std::vector<SiLoaderExt::StreamObject> SiLoaderExt::firedPrepend;
bool SiLoaderExt::enabled = false;
void SiLoaderExt::Initialize()
@ -108,6 +110,10 @@ std::optional<MxResult> 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<MxBool> 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(