From 428725d9cef4fc6d7c1188de516f0f3f96b7e653 Mon Sep 17 00:00:00 2001 From: Jan Racek Date: Sun, 29 Mar 2026 18:41:58 +0200 Subject: [PATCH] animz --- Makefile | 2 + src/animation.cpp | 297 +++++++++++++++++++++++++++++++++++++ src/animation.hh | 71 +++++++++ src/cutscene.cpp | 133 +++-------------- src/cutscene.hh | 6 +- src/gameobject_bytecode.h | 257 ++++++++++++++++---------------- src/interpolation.cpp | 115 +++++++++++++++ src/interpolation.hh | 19 +++ src/lua.cpp | 158 +++++++++++++++++--- src/lua.h | 1 + src/luaapi.cpp | 298 +++++++++++++++++++++++++------------- src/luaapi.hh | 19 ++- src/scenemanager.cpp | 74 +++++++++- src/scenemanager.hh | 5 + src/splashpack.cpp | 85 ++++++++++- src/splashpack.hh | 4 + 16 files changed, 1173 insertions(+), 371 deletions(-) create mode 100644 src/animation.cpp create mode 100644 src/animation.hh create mode 100644 src/interpolation.cpp create mode 100644 src/interpolation.hh diff --git a/Makefile b/Makefile index 785a9f1..d93e963 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ src/profiler.cpp \ src/collision.cpp \ src/bvh.cpp \ src/cutscene.cpp \ +src/interpolation.cpp \ +src/animation.cpp \ src/uisystem.cpp \ src/loadingscreen.cpp \ src/memoverlay.cpp \ diff --git a/src/animation.cpp b/src/animation.cpp new file mode 100644 index 0000000..3a1118b --- /dev/null +++ b/src/animation.cpp @@ -0,0 +1,297 @@ +#include "animation.hh" +#include "interpolation.hh" + +#include +#include +#include "streq.hh" +#include "uisystem.hh" + +namespace psxsplash { + +void AnimationPlayer::init(Animation* animations, int count, UISystem* uiSystem) { + m_animations = animations; + m_animCount = count; + m_uiSystem = uiSystem; + for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) { + m_slots[i].anim = nullptr; + m_slots[i].onCompleteRef = LUA_NOREF; + } +} + +Animation* AnimationPlayer::findByName(const char* name) const { + if (!name || !m_animations) return nullptr; + for (int i = 0; i < m_animCount; i++) { + if (m_animations[i].name && streq(m_animations[i].name, name)) + return &m_animations[i]; + } + return nullptr; +} + +bool AnimationPlayer::play(const char* name, bool loop) { + Animation* anim = findByName(name); + if (!anim) return false; + + // Find a free slot + int freeSlot = -1; + for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) { + if (!m_slots[i].anim) { + freeSlot = i; + break; + } + } + if (freeSlot < 0) return false; + + ActiveSlot& slot = m_slots[freeSlot]; + slot.anim = anim; + slot.frame = 0; + slot.loop = loop; + // onCompleteRef is set separately via setOnCompleteRef before play() + + captureInitialValues(anim); + return true; +} + +void AnimationPlayer::stop(const char* name) { + if (!name) return; + for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) { + if (m_slots[i].anim && m_slots[i].anim->name && streq(m_slots[i].anim->name, name)) { + fireSlotComplete(m_slots[i]); + m_slots[i].anim = nullptr; + } + } +} + +void AnimationPlayer::stopAll() { + for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) { + if (m_slots[i].anim) { + fireSlotComplete(m_slots[i]); + m_slots[i].anim = nullptr; + } + } +} + +bool AnimationPlayer::isPlaying(const char* name) const { + if (!name) return false; + for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) { + if (m_slots[i].anim && m_slots[i].anim->name && streq(m_slots[i].anim->name, name)) + return true; + } + return false; +} + +void AnimationPlayer::setOnCompleteRef(const char* name, int ref) { + // Find the most recently started slot for this animation (highest index with frame 0) + // Fallback: find first slot with this name + for (int i = MAX_SIMULTANEOUS_ANIMS - 1; i >= 0; i--) { + if (m_slots[i].anim && m_slots[i].anim->name && streq(m_slots[i].anim->name, name)) { + m_slots[i].onCompleteRef = ref; + return; + } + } +} + +void AnimationPlayer::tick() { + for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) { + ActiveSlot& slot = m_slots[i]; + if (!slot.anim) continue; + + // Apply all tracks + for (uint8_t ti = 0; ti < slot.anim->trackCount; ti++) { + applyTrack(slot.anim->tracks[ti], slot.frame); + } + + slot.frame++; + if (slot.frame > slot.anim->totalFrames) { + if (slot.loop) { + slot.frame = 0; + } else { + Animation* finished = slot.anim; + slot.anim = nullptr; + fireSlotComplete(slot); + (void)finished; + } + } + } +} + +void AnimationPlayer::captureInitialValues(Animation* anim) { + for (uint8_t ti = 0; ti < anim->trackCount; ti++) { + CutsceneTrack& track = anim->tracks[ti]; + track.initialValues[0] = track.initialValues[1] = track.initialValues[2] = 0; + switch (track.trackType) { + case TrackType::ObjectPosition: + if (track.target) { + track.initialValues[0] = (int16_t)track.target->position.x.value; + track.initialValues[1] = (int16_t)track.target->position.y.value; + track.initialValues[2] = (int16_t)track.target->position.z.value; + } + break; + case TrackType::ObjectRotation: + break; + case TrackType::ObjectActive: + if (track.target) { + track.initialValues[0] = track.target->isActive() ? 1 : 0; + } + break; + case TrackType::UICanvasVisible: + if (m_uiSystem) { + track.initialValues[0] = m_uiSystem->isCanvasVisible(track.uiHandle) ? 1 : 0; + } + break; + case TrackType::UIElementVisible: + if (m_uiSystem) { + track.initialValues[0] = m_uiSystem->isElementVisible(track.uiHandle) ? 1 : 0; + } + break; + case TrackType::UIProgress: + if (m_uiSystem) { + track.initialValues[0] = m_uiSystem->getProgress(track.uiHandle); + } + break; + case TrackType::UIPosition: + if (m_uiSystem) { + int16_t px, py; + m_uiSystem->getPosition(track.uiHandle, px, py); + track.initialValues[0] = px; + track.initialValues[1] = py; + } + break; + case TrackType::UIColor: + if (m_uiSystem) { + uint8_t cr, cg, cb; + m_uiSystem->getColor(track.uiHandle, cr, cg, cb); + track.initialValues[0] = cr; + track.initialValues[1] = cg; + track.initialValues[2] = cb; + } + break; + default: + break; + } + } +} + +void AnimationPlayer::applyTrack(CutsceneTrack& track, uint16_t frame) { + if (track.keyframeCount == 0 || !track.keyframes) return; + + int16_t out[3]; + + switch (track.trackType) { + case TrackType::ObjectPosition: { + if (!track.target) return; + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out); + track.target->position.x.value = (int32_t)out[0]; + track.target->position.y.value = (int32_t)out[1]; + track.target->position.z.value = (int32_t)out[2]; + break; + } + + case TrackType::ObjectRotation: { + if (!track.target) return; + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out); + psyqo::Angle rx, ry, rz; + rx.value = (int32_t)out[0]; + ry.value = (int32_t)out[1]; + rz.value = (int32_t)out[2]; + auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, m_trig); + auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, m_trig); + auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, m_trig); + auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX); + track.target->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ); + break; + } + + case TrackType::ObjectActive: { + if (!track.target) return; + CutsceneKeyframe* kf = track.keyframes; + uint8_t count = track.keyframeCount; + int16_t activeVal = (count > 0 && frame < kf[0].getFrame()) + ? track.initialValues[0] + : kf[0].values[0]; + for (uint8_t i = 0; i < count; i++) { + if (kf[i].getFrame() <= frame) { + activeVal = kf[i].values[0]; + } else { + break; + } + } + track.target->setActive(activeVal != 0); + break; + } + + case TrackType::UICanvasVisible: { + if (!m_uiSystem) return; + CutsceneKeyframe* kf = track.keyframes; + uint8_t count = track.keyframeCount; + int16_t val = (count > 0 && frame < kf[0].getFrame()) + ? track.initialValues[0] : kf[0].values[0]; + for (uint8_t i = 0; i < count; i++) { + if (kf[i].getFrame() <= frame) val = kf[i].values[0]; + else break; + } + m_uiSystem->setCanvasVisible(track.uiHandle, val != 0); + break; + } + + case TrackType::UIElementVisible: { + if (!m_uiSystem) return; + CutsceneKeyframe* kf = track.keyframes; + uint8_t count = track.keyframeCount; + int16_t val = (count > 0 && frame < kf[0].getFrame()) + ? track.initialValues[0] : kf[0].values[0]; + for (uint8_t i = 0; i < count; i++) { + if (kf[i].getFrame() <= frame) val = kf[i].values[0]; + else break; + } + m_uiSystem->setElementVisible(track.uiHandle, val != 0); + break; + } + + case TrackType::UIProgress: { + if (!m_uiSystem) return; + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out); + int16_t v = out[0]; + if (v < 0) v = 0; + if (v > 100) v = 100; + m_uiSystem->setProgress(track.uiHandle, (uint8_t)v); + break; + } + + case TrackType::UIPosition: { + if (!m_uiSystem) return; + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out); + m_uiSystem->setPosition(track.uiHandle, out[0], out[1]); + break; + } + + case TrackType::UIColor: { + if (!m_uiSystem) return; + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out); + uint8_t cr = (out[0] < 0) ? 0 : ((out[0] > 255) ? 255 : (uint8_t)out[0]); + uint8_t cg = (out[1] < 0) ? 0 : ((out[1] > 255) ? 255 : (uint8_t)out[1]); + uint8_t cb = (out[2] < 0) ? 0 : ((out[2] > 255) ? 255 : (uint8_t)out[2]); + m_uiSystem->setColor(track.uiHandle, cr, cg, cb); + break; + } + + default: + break; + } +} + +void AnimationPlayer::fireSlotComplete(ActiveSlot& slot) { + if (slot.onCompleteRef == LUA_NOREF || !m_luaState) return; + psyqo::Lua L(m_luaState); + L.rawGetI(LUA_REGISTRYINDEX, slot.onCompleteRef); + if (L.isFunction(-1)) { + if (L.pcall(0, 0) != LUA_OK) { + L.pop(); + } + } else { + L.pop(); + } + luaL_unref(m_luaState, LUA_REGISTRYINDEX, slot.onCompleteRef); + slot.onCompleteRef = LUA_NOREF; +} + +} // namespace psxsplash diff --git a/src/animation.hh b/src/animation.hh new file mode 100644 index 0000000..b514167 --- /dev/null +++ b/src/animation.hh @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +#include "cutscene.hh" +#include + +namespace psxsplash { + +class UISystem; + +static constexpr int MAX_ANIMATIONS = 16; +static constexpr int MAX_ANIM_TRACKS = 8; +static constexpr int MAX_SIMULTANEOUS_ANIMS = 8; + +struct Animation { + const char* name; + uint16_t totalFrames; + uint8_t trackCount; + uint8_t pad; + CutsceneTrack tracks[MAX_ANIM_TRACKS]; +}; + +class AnimationPlayer { +public: + void init(Animation* animations, int count, UISystem* uiSystem = nullptr); + + /// Play animation by name. Returns false if not found or no free slots. + bool play(const char* name, bool loop = false); + + /// Stop all instances of animation by name. + void stop(const char* name); + + /// Stop all running animations. + void stopAll(); + + /// True if any instance of the named animation is playing. + bool isPlaying(const char* name) const; + + /// Set a Lua callback for the next play() call. + void setOnCompleteRef(const char* name, int ref); + + /// Set the lua_State for callbacks. + void setLuaState(lua_State* L) { m_luaState = L; } + + /// Advance all active animations one frame. + void tick(); + +private: + struct ActiveSlot { + Animation* anim = nullptr; + uint16_t frame = 0; + bool loop = false; + int onCompleteRef = LUA_NOREF; + }; + + Animation* m_animations = nullptr; + int m_animCount = 0; + ActiveSlot m_slots[MAX_SIMULTANEOUS_ANIMS]; + UISystem* m_uiSystem = nullptr; + lua_State* m_luaState = nullptr; + psyqo::Trig<> m_trig; + + Animation* findByName(const char* name) const; + void applyTrack(CutsceneTrack& track, uint16_t frame); + void captureInitialValues(Animation* anim); + void fireSlotComplete(ActiveSlot& slot); +}; + +} // namespace psxsplash diff --git a/src/cutscene.cpp b/src/cutscene.cpp index 38b9788..38cbe02 100644 --- a/src/cutscene.cpp +++ b/src/cutscene.cpp @@ -1,4 +1,5 @@ #include "cutscene.hh" +#include "interpolation.hh" #include #include @@ -114,6 +115,16 @@ void CutscenePlayer::stop() { fireOnComplete(); } +bool CutscenePlayer::hasCameraTracks() const { + if (!m_active) return false; + for (uint8_t i = 0; i < m_active->trackCount; i++) { + auto t = m_active->tracks[i].trackType; + if (t == TrackType::CameraPosition || t == TrackType::CameraRotation) + return true; + } + return false; +} + void CutscenePlayer::tick() { if (!m_active) return; @@ -162,114 +173,6 @@ void CutscenePlayer::fireOnComplete() { m_onCompleteRef = LUA_NOREF; } -static int32_t applyCurve(int32_t t, InterpMode mode) { - switch (mode) { - default: - case InterpMode::Linear: - return t; - case InterpMode::Step: - return 0; - case InterpMode::EaseIn: - return (int32_t)((int64_t)t * t >> 12); - case InterpMode::EaseOut: - return (int32_t)(((int64_t)t * (8192 - t)) >> 12); - case InterpMode::EaseInOut: { - int64_t t2 = (int64_t)t * t; - int64_t t3 = t2 * t; - return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12); - } - } -} - -static bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, - uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]) { - if (count == 0) { - out[0] = out[1] = out[2] = 0; - return false; - } - if (frame <= kf[0].getFrame() || count == 1) { - out[0] = kf[0].values[0]; - out[1] = kf[0].values[1]; - out[2] = kf[0].values[2]; - return false; - } - if (frame >= kf[count - 1].getFrame()) { - out[0] = kf[count - 1].values[0]; - out[1] = kf[count - 1].values[1]; - out[2] = kf[count - 1].values[2]; - return false; - } - b = 1; - while (b < count && kf[b].getFrame() <= frame) b++; - a = b - 1; - uint16_t span = kf[b].getFrame() - kf[a].getFrame(); - if (span == 0) { - out[0] = kf[a].values[0]; - out[1] = kf[a].values[1]; - out[2] = kf[a].values[2]; - return false; - } - uint32_t num = (uint32_t)(frame - kf[a].getFrame()) << 12; - int32_t rawT = (int32_t)(num / span); - t = applyCurve(rawT, kf[b].getInterp()); - return true; -} - -void CutscenePlayer::lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) { - uint8_t a, b; - int32_t t; - if (!findKfPair(kf, count, m_frame, a, b, t, out)) { - if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) { - uint16_t span = kf[0].getFrame(); - uint32_t num = (uint32_t)m_frame << 12; - int32_t rawT = (int32_t)(num / span); - int32_t ct = applyCurve(rawT, kf[0].getInterp()); - for (int i = 0; i < 3; i++) { - int32_t delta = (int32_t)kf[0].values[i] - (int32_t)initial[i]; - out[i] = (int16_t)((int32_t)initial[i] + ((delta * ct) >> 12)); - } - } - return; - } - - for (int i = 0; i < 3; i++) { - int32_t delta = (int32_t)kf[b].values[i] - (int32_t)kf[a].values[i]; - out[i] = (int16_t)((int32_t)kf[a].values[i] + ((delta * t) >> 12)); - } -} - -static constexpr int32_t ANGLE_FULL_CIRCLE = 2048; -static constexpr int32_t ANGLE_HALF_CIRCLE = 1024; - -void CutscenePlayer::lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) { - uint8_t a, b; - int32_t t; - if (!findKfPair(kf, count, m_frame, a, b, t, out)) { - if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) { - uint16_t span = kf[0].getFrame(); - uint32_t num = (uint32_t)m_frame << 12; - int32_t rawT = (int32_t)(num / span); - int32_t ct = applyCurve(rawT, kf[0].getInterp()); - for (int i = 0; i < 3; i++) { - int32_t from = (int32_t)initial[i]; - int32_t to = (int32_t)kf[0].values[i]; - int32_t delta = to - from; - delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE; - out[i] = (int16_t)(from + ((delta * ct) >> 12)); - } - } - return; - } - - for (int i = 0; i < 3; i++) { - int32_t from = (int32_t)kf[a].values[i]; - int32_t to = (int32_t)kf[b].values[i]; - int32_t delta = to - from; - delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE; - out[i] = (int16_t)(from + ((delta * t) >> 12)); - } -} - void CutscenePlayer::applyTrack(CutsceneTrack& track) { if (track.keyframeCount == 0 || !track.keyframes) return; @@ -278,7 +181,7 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { switch (track.trackType) { case TrackType::CameraPosition: { if (!m_camera) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); psyqo::FixedPoint<12> x, y, z; x.value = (int32_t)out[0]; y.value = (int32_t)out[1]; @@ -289,7 +192,7 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { case TrackType::CameraRotation: { if (!m_camera) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); psyqo::Angle rx, ry, rz; rx.value = (int32_t)out[0]; ry.value = (int32_t)out[1]; @@ -300,7 +203,7 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { case TrackType::ObjectPosition: { if (!track.target) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); track.target->position.x.value = (int32_t)out[0]; track.target->position.y.value = (int32_t)out[1]; track.target->position.z.value = (int32_t)out[2]; @@ -309,7 +212,7 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { case TrackType::ObjectRotation: { if (!track.target) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); psyqo::Angle rx, ry, rz; rx.value = (int32_t)out[0]; ry.value = (int32_t)out[1]; @@ -372,7 +275,7 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { case TrackType::UIProgress: { if (!m_uiSystem) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); int16_t v = out[0]; if (v < 0) v = 0; if (v > 100) v = 100; @@ -382,14 +285,14 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { case TrackType::UIPosition: { if (!m_uiSystem) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); m_uiSystem->setPosition(track.uiHandle, out[0], out[1]); break; } case TrackType::UIColor: { if (!m_uiSystem) return; - lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out); uint8_t cr = (out[0] < 0) ? 0 : ((out[0] > 255) ? 255 : (uint8_t)out[0]); uint8_t cg = (out[1] < 0) ? 0 : ((out[1] > 255) ? 255 : (uint8_t)out[1]); uint8_t cb = (out[2] < 0) ? 0 : ((out[2] > 255) ? 255 : (uint8_t)out[2]); diff --git a/src/cutscene.hh b/src/cutscene.hh index 12fc087..f969b6d 100644 --- a/src/cutscene.hh +++ b/src/cutscene.hh @@ -97,6 +97,10 @@ public: /// True if a cutscene is currently active. bool isPlaying() const { return m_active != nullptr; } + /// True if the active cutscene has camera tracks (position or rotation). + /// Use this to decide whether to suppress player camera follow. + bool hasCameraTracks() const; + /// Set a Lua registry reference to call when the cutscene finishes. /// Pass LUA_NOREF to clear. The callback is called ONCE when the /// cutscene ends (not on each loop iteration - only when it truly stops). @@ -125,8 +129,6 @@ private: psyqo::Trig<> m_trig; void applyTrack(CutsceneTrack& track); - void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]); - void lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]); void fireOnComplete(); }; diff --git a/src/gameobject_bytecode.h b/src/gameobject_bytecode.h index 5857ffe..fe4fe8e 100644 --- a/src/gameobject_bytecode.h +++ b/src/gameobject_bytecode.h @@ -1,140 +1,141 @@ #pragma once // Pre-compiled PS1 Lua bytecode for GAMEOBJECT_SCRIPT -// Generated by luac_psx - do not edit manually -// 1581 bytes +// Generated by luac_psx (32-bit, LUA_NUMBER=long) - do not edit manually +// 943 bytes +// +// Original Lua source: +// return function(metatable) +// local get_position = metatable.get_position +// local set_position = metatable.set_position +// local get_active = metatable.get_active +// local set_active = metatable.set_active +// local get_rotation = metatable.get_rotation +// local set_rotation = metatable.set_rotation +// local get_rotationY = metatable.get_rotationY +// local set_rotationY = metatable.set_rotationY +// +// metatable.get_position = nil +// metatable.set_position = nil +// metatable.get_active = nil +// metatable.set_active = nil +// metatable.get_rotation = nil +// metatable.set_rotation = nil +// metatable.get_rotationY = nil +// metatable.set_rotationY = nil +// +// function metatable.__index(self, key) +// local raw = rawget(self, key) +// if raw ~= nil then return raw end +// if key == "position" then +// return get_position(self.__cpp_ptr) +// elseif key == "active" then +// return get_active(self.__cpp_ptr) +// elseif key == "rotation" then +// return get_rotation(self.__cpp_ptr) +// elseif key == "rotationY" then +// return get_rotationY(self.__cpp_ptr) +// end +// return nil +// end +// +// function metatable.__newindex(self, key, value) +// if key == "position" then +// set_position(self.__cpp_ptr, value) +// return +// elseif key == "active" then +// set_active(self.__cpp_ptr, value) +// return +// elseif key == "rotation" then +// set_rotation(self.__cpp_ptr, value) +// return +// elseif key == "rotationY" then +// set_rotationY(self.__cpp_ptr, value) +// return +// end +// rawset(self, key, value) +// end +// end static const unsigned char GAMEOBJECT_BYTECODE[] = { 0x1b, 0x4c, 0x75, 0x61, 0x52, 0x00, 0x01, 0x04, 0x04, 0x04, 0x04, 0x01, 0x19, 0x93, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x08, 0x11, 0x00, 0x00, 0x00, 0x47, 0x00, 0x40, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x0a, 0x15, 0x00, 0x00, 0x00, 0x47, 0x00, 0x40, 0x00, 0x87, 0x40, 0x40, 0x00, 0xc7, 0x80, 0x40, 0x00, 0x07, 0xc1, 0x40, 0x00, - 0x47, 0x01, 0x41, 0x00, 0x87, 0x41, 0x41, 0x00, 0x0a, 0x80, 0x41, 0x80, - 0x0a, 0x80, 0xc1, 0x80, 0x0a, 0x80, 0x41, 0x81, 0x0a, 0x80, 0xc1, 0x81, - 0x0a, 0x80, 0x41, 0x82, 0x0a, 0x80, 0xc1, 0x82, 0xe5, 0x01, 0x00, 0x00, - 0x0a, 0xc0, 0x81, 0x83, 0xe5, 0x41, 0x00, 0x00, 0x0a, 0xc0, 0x01, 0x84, - 0x1f, 0x00, 0x80, 0x00, 0x09, 0x00, 0x00, 0x00, 0x04, 0x0d, 0x00, 0x00, - 0x00, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x00, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0b, 0x00, 0x00, - 0x00, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, - 0x04, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x00, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, - 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x04, - 0x0e, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00, - 0x5f, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x04, 0x0b, 0x00, 0x00, - 0x00, 0x5f, 0x5f, 0x6e, 0x65, 0x77, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x05, 0x1e, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x00, 0xc0, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x9d, 0x80, 0x80, 0x01, 0x58, - 0x40, 0x40, 0x01, 0x17, 0x00, 0x00, 0x80, 0x9f, 0x00, 0x00, 0x01, 0x18, - 0x80, 0xc0, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x00, 0x07, - 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00, 0x00, 0x17, - 0x00, 0x03, 0x80, 0x18, 0x00, 0xc1, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5, - 0x00, 0x00, 0x01, 0x07, 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf, - 0x00, 0x00, 0x00, 0x17, 0x40, 0x01, 0x80, 0x18, 0x40, 0xc1, 0x00, 0x17, - 0xc0, 0x00, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x07, 0xc1, 0x40, 0x00, 0xde, - 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xdf, - 0x00, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, - 0x07, 0x00, 0x00, 0x00, 0x72, 0x61, 0x77, 0x67, 0x65, 0x74, 0x00, 0x00, - 0x04, 0x09, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x63, 0x70, 0x70, - 0x5f, 0x70, 0x74, 0x72, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x61, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x72, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x03, 0x01, 0x05, - 0x0f, 0x00, 0x00, 0x00, 0x67, 0x61, 0x6d, 0x65, 0x6f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x2e, 0x6c, 0x75, 0x61, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x11, - 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x11, - 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x12, - 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, - 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, - 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, - 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x16, - 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, - 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1a, - 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x73, 0x65, 0x6c, 0x66, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x6b, 0x65, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x72, 0x61, 0x77, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x5f, 0x45, 0x4e, 0x56, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, - 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x0b, 0x00, - 0x00, 0x00, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, - 0x00, 0x0e, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x29, - 0x00, 0x00, 0x00, 0x03, 0x00, 0x07, 0x1d, 0x00, 0x00, 0x00, 0x18, 0x00, - 0xc0, 0x00, 0x17, 0x40, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x00, 0x07, 0x41, - 0x40, 0x00, 0x40, 0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, 0x00, - 0x80, 0x00, 0x17, 0x80, 0x03, 0x80, 0x18, 0x80, 0xc0, 0x00, 0x17, 0x40, - 0x01, 0x80, 0xc5, 0x00, 0x80, 0x00, 0x07, 0x41, 0x40, 0x00, 0x40, 0x01, - 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x17, 0x80, - 0x01, 0x80, 0x18, 0xc0, 0xc0, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5, 0x00, - 0x00, 0x01, 0x07, 0x41, 0x40, 0x00, 0x40, 0x01, 0x00, 0x01, 0xdd, 0x40, - 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0xc6, 0x00, 0xc1, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x80, 0x01, 0x00, 0x01, 0xdd, 0x40, - 0x00, 0x02, 0x1f, 0x00, 0x80, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x09, - 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, - 0x04, 0x0a, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x63, 0x70, 0x70, 0x5f, 0x70, - 0x74, 0x72, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x61, 0x63, 0x74, 0x69, - 0x76, 0x65, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x72, - 0x61, 0x77, 0x73, 0x65, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x02, 0x01, 0x04, 0x01, 0x06, 0x00, 0x00, 0x0f, 0x00, - 0x00, 0x00, 0x67, 0x61, 0x6d, 0x65, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x2e, 0x6c, 0x75, 0x61, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, - 0x00, 0x1e, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, - 0x00, 0x1f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, - 0x00, 0x20, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, - 0x00, 0x22, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, - 0x00, 0x22, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, - 0x00, 0x24, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, - 0x00, 0x25, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, - 0x00, 0x26, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, - 0x00, 0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, - 0x00, 0x29, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, - 0x00, 0x73, 0x65, 0x6c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, - 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6b, 0x65, 0x79, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x0b, 0x00, 0x00, - 0x00, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, - 0x0e, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x45, - 0x4e, 0x56, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, - 0x00, 0x67, 0x61, 0x6d, 0x65, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, - 0x6c, 0x75, 0x61, 0x00, 0x11, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x6d, 0x65, 0x74, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x67, 0x65, - 0x74, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x73, + 0x47, 0x01, 0x41, 0x00, 0x87, 0x41, 0x41, 0x00, 0xc7, 0x81, 0x41, 0x00, + 0x07, 0xc2, 0x41, 0x00, 0x0a, 0x00, 0x42, 0x80, 0x0a, 0x00, 0xc2, 0x80, + 0x0a, 0x00, 0x42, 0x81, 0x0a, 0x00, 0xc2, 0x81, 0x0a, 0x00, 0x42, 0x82, + 0x0a, 0x00, 0xc2, 0x82, 0x0a, 0x00, 0x42, 0x83, 0x0a, 0x00, 0xc2, 0x83, + 0x65, 0x02, 0x00, 0x00, 0x0a, 0x40, 0x82, 0x84, 0x65, 0x42, 0x00, 0x00, + 0x0a, 0x40, 0x02, 0x85, 0x1f, 0x00, 0x80, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x04, 0x0d, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0x67, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x73, - 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x67, 0x65, - 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, - 0x73, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x59, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x45, 0x4e, 0x56, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x67, 0x61, 0x6d, - 0x65, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x6c, 0x75, 0x61, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x5f, 0x45, 0x4e, 0x56, 0x00, + 0x04, 0x0b, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x00, 0x04, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, + 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x04, 0x0d, 0x00, 0x00, + 0x00, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x00, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x72, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0e, 0x00, 0x00, + 0x00, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x59, 0x00, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, + 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x00, 0x04, + 0x08, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00, + 0x04, 0x0b, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x6e, 0x65, 0x77, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x05, 0x25, 0x00, 0x00, 0x00, 0x86, + 0x00, 0x40, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x9d, + 0x80, 0x80, 0x01, 0x58, 0x40, 0x40, 0x01, 0x17, 0x00, 0x00, 0x80, 0x9f, + 0x00, 0x00, 0x01, 0x18, 0x80, 0xc0, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5, + 0x00, 0x80, 0x00, 0x07, 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf, + 0x00, 0x00, 0x00, 0x17, 0xc0, 0x04, 0x80, 0x18, 0x00, 0xc1, 0x00, 0x17, + 0x00, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x01, 0x07, 0xc1, 0x40, 0x00, 0xde, + 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00, 0x00, 0x17, 0x00, 0x03, 0x80, 0x18, + 0x40, 0xc1, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x07, + 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00, 0x00, 0x17, + 0x40, 0x01, 0x80, 0x18, 0x80, 0xc1, 0x00, 0x17, 0xc0, 0x00, 0x80, 0xc5, + 0x00, 0x00, 0x02, 0x07, 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf, + 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x01, 0x1f, + 0x00, 0x80, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, + 0x72, 0x61, 0x77, 0x67, 0x65, 0x74, 0x00, 0x00, 0x04, 0x09, 0x00, 0x00, + 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0a, + 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x63, 0x70, 0x70, 0x5f, 0x70, 0x74, 0x72, + 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x00, 0x04, 0x09, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x03, 0x01, 0x05, 0x01, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x07, 0x25, 0x00, 0x00, 0x00, 0x18, 0x00, 0xc0, 0x00, 0x17, + 0x40, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x00, 0x07, 0x41, 0x40, 0x00, 0x40, + 0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x17, + 0x80, 0x05, 0x80, 0x18, 0x80, 0xc0, 0x00, 0x17, 0x40, 0x01, 0x80, 0xc5, + 0x00, 0x80, 0x00, 0x07, 0x41, 0x40, 0x00, 0x40, 0x01, 0x00, 0x01, 0xdd, + 0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x17, 0x80, 0x03, 0x80, 0x18, + 0xc0, 0xc0, 0x00, 0x17, 0x40, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x01, 0x07, + 0x41, 0x40, 0x00, 0x40, 0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, + 0x00, 0x80, 0x00, 0x17, 0x80, 0x01, 0x80, 0x18, 0x00, 0xc1, 0x00, 0x17, + 0x00, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x07, 0x41, 0x40, 0x00, 0x40, + 0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0xc6, + 0x40, 0x41, 0x02, 0x00, 0x01, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x80, + 0x01, 0x00, 0x01, 0xdd, 0x40, 0x00, 0x02, 0x1f, 0x00, 0x80, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x04, 0x09, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x5f, 0x5f, + 0x63, 0x70, 0x70, 0x5f, 0x70, 0x74, 0x72, 0x00, 0x04, 0x07, 0x00, 0x00, + 0x00, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x04, 0x09, 0x00, 0x00, + 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0a, + 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, + 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x72, 0x61, 0x77, 0x73, 0x65, 0x74, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01, + 0x04, 0x01, 0x06, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; + diff --git a/src/interpolation.cpp b/src/interpolation.cpp new file mode 100644 index 0000000..fdcb388 --- /dev/null +++ b/src/interpolation.cpp @@ -0,0 +1,115 @@ +#include "interpolation.hh" + +namespace psxsplash { + +int32_t applyCurve(int32_t t, InterpMode mode) { + switch (mode) { + default: + case InterpMode::Linear: + return t; + case InterpMode::Step: + return 0; + case InterpMode::EaseIn: + return (int32_t)((int64_t)t * t >> 12); + case InterpMode::EaseOut: + return (int32_t)(((int64_t)t * (8192 - t)) >> 12); + case InterpMode::EaseInOut: { + int64_t t2 = (int64_t)t * t; + int64_t t3 = t2 * t; + return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12); + } + } +} + +bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, + uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]) { + if (count == 0) { + out[0] = out[1] = out[2] = 0; + return false; + } + if (frame <= kf[0].getFrame() || count == 1) { + out[0] = kf[0].values[0]; + out[1] = kf[0].values[1]; + out[2] = kf[0].values[2]; + return false; + } + if (frame >= kf[count - 1].getFrame()) { + out[0] = kf[count - 1].values[0]; + out[1] = kf[count - 1].values[1]; + out[2] = kf[count - 1].values[2]; + return false; + } + b = 1; + while (b < count && kf[b].getFrame() <= frame) b++; + a = b - 1; + uint16_t span = kf[b].getFrame() - kf[a].getFrame(); + if (span == 0) { + out[0] = kf[a].values[0]; + out[1] = kf[a].values[1]; + out[2] = kf[a].values[2]; + return false; + } + uint32_t num = (uint32_t)(frame - kf[a].getFrame()) << 12; + int32_t rawT = (int32_t)(num / span); + t = applyCurve(rawT, kf[b].getInterp()); + return true; +} + +void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, + const int16_t initial[3], int16_t out[3]) { + uint8_t a, b; + int32_t t; + if (!findKfPair(kf, count, frame, a, b, t, out)) { + if (count > 0 && kf[0].getFrame() > 0 && frame < kf[0].getFrame()) { + uint16_t span = kf[0].getFrame(); + uint32_t num = (uint32_t)frame << 12; + int32_t rawT = (int32_t)(num / span); + int32_t ct = applyCurve(rawT, kf[0].getInterp()); + for (int i = 0; i < 3; i++) { + int32_t delta = (int32_t)kf[0].values[i] - (int32_t)initial[i]; + out[i] = (int16_t)((int32_t)initial[i] + ((delta * ct) >> 12)); + } + } + return; + } + + for (int i = 0; i < 3; i++) { + int32_t delta = (int32_t)kf[b].values[i] - (int32_t)kf[a].values[i]; + out[i] = (int16_t)((int32_t)kf[a].values[i] + ((delta * t) >> 12)); + } +} + +static constexpr int32_t ANGLE_FULL_CIRCLE = 2048; +static constexpr int32_t ANGLE_HALF_CIRCLE = 1024; + +void lerpAngles(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, + const int16_t initial[3], int16_t out[3]) { + uint8_t a, b; + int32_t t; + if (!findKfPair(kf, count, frame, a, b, t, out)) { + if (count > 0 && kf[0].getFrame() > 0 && frame < kf[0].getFrame()) { + uint16_t span = kf[0].getFrame(); + uint32_t num = (uint32_t)frame << 12; + int32_t rawT = (int32_t)(num / span); + int32_t ct = applyCurve(rawT, kf[0].getInterp()); + for (int i = 0; i < 3; i++) { + int32_t from = (int32_t)initial[i]; + int32_t to = (int32_t)kf[0].values[i]; + int32_t delta = to - from; + delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE; + out[i] = (int16_t)(from + ((delta * ct) >> 12)); + } + } + return; + } + + for (int i = 0; i < 3; i++) { + int32_t from = (int32_t)kf[a].values[i]; + int32_t to = (int32_t)kf[b].values[i]; + int32_t delta = to - from; + delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE; + out[i] = (int16_t)(from + ((delta * t) >> 12)); + } +} + +} // namespace psxsplash diff --git a/src/interpolation.hh b/src/interpolation.hh new file mode 100644 index 0000000..5486be2 --- /dev/null +++ b/src/interpolation.hh @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "cutscene.hh" + +namespace psxsplash { + +int32_t applyCurve(int32_t t, InterpMode mode); + +bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, + uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]); + +void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, + const int16_t initial[3], int16_t out[3]); + +void lerpAngles(CutsceneKeyframe* kf, uint8_t count, uint16_t frame, + const int16_t initial[3], int16_t out[3]); + +} // namespace psxsplash diff --git a/src/lua.cpp b/src/lua.cpp index e4b3eb3..ca9315d 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -26,18 +26,24 @@ extern "C" void *lua_oom_realloc(void *ptr, size_t size) { // Lua helpers -static constexpr lua_Number kFixedScale = 4096; +static constexpr int32_t kFixedScale = 4096; + +// Accept FixedPoint object or plain number from Lua +static psyqo::FixedPoint<12> readFP(psyqo::Lua& L, int idx) { + if (L.isFixedPoint(idx)) return L.toFixedPoint(idx); + return psyqo::FixedPoint<12>(static_cast(L.toNumber(idx) * kFixedScale), psyqo::FixedPoint<12>::RAW); +} static int gameobjectGetPosition(psyqo::Lua L) { auto go = L.toUserdata(1); L.newTable(); - L.pushNumber(static_cast(go->position.x.raw()) / kFixedScale); + L.push(go->position.x); L.setField(2, "x"); - L.pushNumber(static_cast(go->position.y.raw()) / kFixedScale); + L.push(go->position.y); L.setField(2, "y"); - L.pushNumber(static_cast(go->position.z.raw()) / kFixedScale); + L.push(go->position.z); L.setField(2, "z"); return 1; @@ -49,15 +55,15 @@ static int gameobjectSetPosition(psyqo::Lua L) { auto go = L.toUserdata(1); L.getField(2, "x"); - go->position.x = psyqo::FixedPoint<>(static_cast(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); + go->position.x = readFP(L, 3); L.pop(); L.getField(2, "y"); - go->position.y = psyqo::FixedPoint<>(static_cast(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); + go->position.y = readFP(L, 3); L.pop(); - L.getField(2, "z"); - go->position.z = psyqo::FixedPoint<>(static_cast(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); + L.getField(2, "z"); + go->position.z = readFP(L, 3); L.pop(); return 0; @@ -76,10 +82,8 @@ static int gameobjectSetActive(psyqo::Lua L) { return 0; } -static constexpr lua_Number kAngleScale = 1024; static psyqo::Trig<> s_trig; - static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) { psyqo::Angle result; if (cosVal == 0 && sinVal == 0) { result.value = 0; return result; } @@ -99,20 +103,85 @@ static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) { return result; } +static int gameobjectGetRotation(psyqo::Lua L) { + auto go = L.toUserdata(1); + // Decompose Y-axis rotation from the matrix (vs[0].x = cos, vs[0].z = sin) + // For full XYZ, we extract approximate Euler angles from the rotation matrix. + // Row 0: [cos(Y)cos(Z), -cos(Y)sin(Z), sin(Y)] + // This is a simplified extraction assuming common rotation order Y*X*Z. + int32_t sinY = go->rotation.vs[0].z.raw(); + int32_t cosY = go->rotation.vs[0].x.raw(); + int32_t sinX = -go->rotation.vs[1].z.raw(); + int32_t cosX = go->rotation.vs[2].z.raw(); + int32_t sinZ = -go->rotation.vs[0].y.raw(); + int32_t cosZ = go->rotation.vs[0].x.raw(); + + auto toFP12 = [](psyqo::Angle a) -> psyqo::FixedPoint<12> { + psyqo::FixedPoint<12> fp; + fp.value = a.value << 2; + return fp; + }; + + L.newTable(); + L.push(toFP12(fastAtan2(sinX, cosX))); + L.setField(2, "x"); + L.push(toFP12(fastAtan2(sinY, cosY))); + L.setField(2, "y"); + L.push(toFP12(fastAtan2(sinZ, cosZ))); + L.setField(2, "z"); + + return 1; +} + +static int gameobjectSetRotation(psyqo::Lua L) { + auto go = L.toUserdata(1); + + L.getField(2, "x"); + psyqo::FixedPoint<12> fpX = readFP(L, 3); + L.pop(); + + L.getField(2, "y"); + psyqo::FixedPoint<12> fpY = readFP(L, 3); + L.pop(); + + L.getField(2, "z"); + psyqo::FixedPoint<12> fpZ = readFP(L, 3); + L.pop(); + + // Convert FixedPoint<12> to Angle (FixedPoint<10>) + psyqo::Angle rx, ry, rz; + rx.value = fpX.value >> 2; + ry.value = fpY.value >> 2; + rz.value = fpZ.value >> 2; + + // Compose Y * X * Z rotation matrix + auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, s_trig); + auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, s_trig); + auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, s_trig); + auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX); + go->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ); + return 0; +} + + static int gameobjectGetRotationY(psyqo::Lua L) { auto go = L.toUserdata(1); int32_t sinRaw = go->rotation.vs[0].z.raw(); int32_t cosRaw = go->rotation.vs[0].x.raw(); psyqo::Angle angle = fastAtan2(sinRaw, cosRaw); - L.pushNumber(static_cast(angle.value) / kAngleScale); + // Angle is FixedPoint<10> (pi-units). Convert to FixedPoint<12> for Lua. + psyqo::FixedPoint<12> fp12; + fp12.value = angle.value << 2; + L.push(fp12); return 1; } static int gameobjectSetRotationY(psyqo::Lua L) { auto go = L.toUserdata(1); - lua_Number piUnits = L.toNumber(2); + // Accept FixedPoint<12> from Lua, convert to Angle (FixedPoint<10>) + psyqo::FixedPoint<12> fp12 = readFP(L, 2); psyqo::Angle angle; - angle.value = static_cast(piUnits * kAngleScale); + angle.value = fp12.value >> 2; go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig); return 0; } @@ -137,6 +206,12 @@ void psxsplash::Lua::Init() { L.push(gameobjectSetActive); L.setField(-2, "set_active"); + L.push(gameobjectGetRotation); + L.setField(-2, "get_rotation"); + + L.push(gameobjectSetRotation); + L.setField(-2, "set_rotation"); + L.push(gameobjectGetRotationY); L.setField(-2, "get_rotationY"); @@ -168,6 +243,49 @@ void psxsplash::Lua::Init() { L.newTable(); m_luascriptsReference = L.ref(); + + // Add __concat to the FixedPoint metatable so FixedPoint values work with .. + // psyqo-lua doesn't provide this, but scripts need it for Debug.Log etc. + L.getField(LUA_REGISTRYINDEX, "psyqo.FixedPoint"); + if (L.isTable(-1)) { + L.push([](psyqo::Lua L) -> int { + // Convert both operands to strings and concatenate + char buf[64]; + int len = 0; + + for (int i = 1; i <= 2; i++) { + if (L.isFixedPoint(i)) { + auto fp = L.toFixedPoint(i); + int32_t raw = fp.raw(); + int integer = raw >> 12; + unsigned fraction = (raw < 0 ? -raw : raw) & 0xfff; + if (fraction == 0) { + len += snprintf(buf + len, sizeof(buf) - len, "%d", integer); + } else { + unsigned decimal = (fraction * 1000) >> 12; + if (raw < 0 && integer == 0) + len += snprintf(buf + len, sizeof(buf) - len, "-%d.%03u", integer, decimal); + else + len += snprintf(buf + len, sizeof(buf) - len, "%d.%03u", integer, decimal); + } + } else { + const char* s = L.toString(i); + if (s) { + int slen = 0; + while (s[slen]) slen++; + if (len + slen < (int)sizeof(buf)) { + for (int j = 0; j < slen; j++) buf[len++] = s[j]; + } + } + } + } + buf[len] = '\0'; + L.push(buf, len); + return 1; + }); + L.setField(-2, "__concat"); + } + L.pop(); } void psxsplash::Lua::Shutdown() { @@ -357,14 +475,18 @@ void psxsplash::Lua::RegisterGameObject(GameObject* go) { // Store the event mask directly in the GameObject go->eventMask = eventMask; - + L.pop(); // empty stack + // Note: onCreate is NOT fired here. Call FireAllOnCreate() after all objects + // are registered so that Entity.Find works across all objects in onCreate. +} - - // Fire onCreate event if this object handles it - if (eventMask & EVENT_ON_CREATE) { - onCreateMethodWrapper.callMethod(*this, go); +void psxsplash::Lua::FireAllOnCreate(GameObject** objects, size_t count) { + for (size_t i = 0; i < count; i++) { + if (objects[i] && (objects[i]->eventMask & EVENT_ON_CREATE)) { + onCreateMethodWrapper.callMethod(*this, objects[i]); + } } } diff --git a/src/lua.h b/src/lua.h index 632833d..a7c82ae 100644 --- a/src/lua.h +++ b/src/lua.h @@ -49,6 +49,7 @@ class Lua { void LoadLuaFile(const char* code, size_t len, int index); void RegisterSceneScripts(int index); void RegisterGameObject(GameObject* go); + void FireAllOnCreate(GameObject** objects, size_t count); void RelocateGameObjects(GameObject** objects, size_t count, intptr_t delta); // Get the underlying psyqo::Lua state for API registration diff --git a/src/luaapi.cpp b/src/luaapi.cpp index a5efc83..a36fff9 100644 --- a/src/luaapi.cpp +++ b/src/luaapi.cpp @@ -4,6 +4,7 @@ #include "controls.hh" #include "camera.hh" #include "cutscene.hh" +#include "animation.hh" #include "uisystem.hh" #include @@ -16,18 +17,20 @@ namespace psxsplash { // Static member SceneManager* LuaAPI::s_sceneManager = nullptr; CutscenePlayer* LuaAPI::s_cutscenePlayer = nullptr; +AnimationPlayer* LuaAPI::s_animationPlayer = nullptr; UISystem* LuaAPI::s_uiSystem = nullptr; // Scale factor: FixedPoint<12> stores 1.0 as raw 4096. // Lua scripts work in world-space units (1 = one unit), so we convert. static constexpr lua_Number kFixedScale = 4096; -static lua_Number fpToLua(psyqo::FixedPoint<12> fp) { - return static_cast(fp.raw()) / kFixedScale; -} - -static psyqo::FixedPoint<12> luaToFp(lua_Number val) { - return psyqo::FixedPoint<12>(static_cast(val * kFixedScale), psyqo::FixedPoint<12>::RAW); +// Read a FixedPoint<12> from the stack, accepting either a FixedPoint object +// or a plain integer (which gets scaled by 4096 to become fp12). +static psyqo::FixedPoint<12> readFP(psyqo::Lua& L, int idx) { + if (L.isFixedPoint(idx)) { + return L.toFixedPoint(idx); + } + return psyqo::FixedPoint<12>(static_cast(L.toNumber(idx) * kFixedScale), psyqo::FixedPoint<12>::RAW); } // Angle scale: psyqo::Angle is FixedPoint<10>, so 1.0_pi = raw 1024 @@ -38,9 +41,10 @@ static psyqo::Trig<> s_trig; // REGISTRATION // ============================================================================ -void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer, UISystem* uiSystem) { +void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer, AnimationPlayer* animationPlayer, UISystem* uiSystem) { s_sceneManager = scene; s_cutscenePlayer = cutscenePlayer; + s_animationPlayer = animationPlayer; s_uiSystem = uiSystem; // ======================================================================== @@ -285,6 +289,22 @@ void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cut L.setGlobal("Cutscene"); + // ======================================================================== + // ANIMATION API + // ======================================================================== + L.newTable(); + + L.push(Animation_Play); + L.setField(-2, "Play"); + + L.push(Animation_Stop); + L.setField(-2, "Stop"); + + L.push(Animation_IsPlaying); + L.setField(-2, "IsPlaying"); + + L.setGlobal("Animation"); + // ======================================================================== // CONTROLS API // ======================================================================== @@ -598,26 +618,28 @@ int LuaAPI::Entity_GetRotationY(lua_State* L) { angle.value = a; } - // Return in pi-units: 0.5 = π/2 = 90° - lua.pushNumber(static_cast(angle.value) / kAngleScale); + // Return as FixedPoint<12> (Angle is FixedPoint<10>, shift left 2 for fp12) + psyqo::FixedPoint<12> fp12; + fp12.value = angle.value << 2; + lua.push(fp12); return 1; } int LuaAPI::Entity_SetRotationY(lua_State* L) { psyqo::Lua lua(L); - + if (!lua.isTable(1)) return 0; - + lua.getField(1, "__cpp_ptr"); auto go = lua.toUserdata(-1); lua.pop(); - + if (!go) return 0; - - // Accept angle in pi-units (0.5 = π/2 = 90°) - lua_Number piUnits = lua.toNumber(2); + + // Accept FixedPoint or number, convert to Angle (FixedPoint<10>) + psyqo::FixedPoint<12> fp12 = readFP(lua, 2); psyqo::Angle angle; - angle.value = static_cast(piUnits * kAngleScale); + angle.value = fp12.value >> 2; go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig); return 0; } @@ -654,41 +676,41 @@ int LuaAPI::Entity_ForEach(lua_State* L) { // VEC3 API IMPLEMENTATION // ============================================================================ -void LuaAPI::PushVec3(psyqo::Lua& L, psyqo::FixedPoint<12> x, +void LuaAPI::PushVec3(psyqo::Lua& L, psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z) { L.newTable(); - L.pushNumber(fpToLua(x)); + L.push(x); L.setField(-2, "x"); - L.pushNumber(fpToLua(y)); + L.push(y); L.setField(-2, "y"); - L.pushNumber(fpToLua(z)); + L.push(z); L.setField(-2, "z"); } -void LuaAPI::ReadVec3(psyqo::Lua& L, int idx, - psyqo::FixedPoint<12>& x, - psyqo::FixedPoint<12>& y, +void LuaAPI::ReadVec3(psyqo::Lua& L, int idx, + psyqo::FixedPoint<12>& x, + psyqo::FixedPoint<12>& y, psyqo::FixedPoint<12>& z) { L.getField(idx, "x"); - x = luaToFp(L.toNumber(-1)); + x = readFP(L, -1); L.pop(); - + L.getField(idx, "y"); - y = luaToFp(L.toNumber(-1)); + y = readFP(L, -1); L.pop(); - + L.getField(idx, "z"); - z = luaToFp(L.toNumber(-1)); + z = readFP(L, -1); L.pop(); } int LuaAPI::Vec3_New(lua_State* L) { psyqo::Lua lua(L); - - psyqo::FixedPoint<12> x = luaToFp(lua.optNumber(1, 0)); - psyqo::FixedPoint<12> y = luaToFp(lua.optNumber(2, 0)); - psyqo::FixedPoint<12> z = luaToFp(lua.optNumber(3, 0)); - + + psyqo::FixedPoint<12> x = lua.isNoneOrNil(1) ? psyqo::FixedPoint<12>() : readFP(lua, 1); + psyqo::FixedPoint<12> y = lua.isNoneOrNil(2) ? psyqo::FixedPoint<12>() : readFP(lua, 2); + psyqo::FixedPoint<12> z = lua.isNoneOrNil(3) ? psyqo::FixedPoint<12>() : readFP(lua, 3); + PushVec3(lua, x, y, z); return 1; } @@ -740,7 +762,7 @@ int LuaAPI::Vec3_Mul(lua_State* L) { psyqo::FixedPoint<12> x, y, z; ReadVec3(lua, 1, x, y, z); - psyqo::FixedPoint<12> scalar = luaToFp(lua.toNumber(2)); + psyqo::FixedPoint<12> scalar = readFP(lua, 2); PushVec3(lua, x * scalar, y * scalar, z * scalar); return 1; @@ -761,7 +783,7 @@ int LuaAPI::Vec3_Dot(lua_State* L) { ReadVec3(lua, 2, bx, by, bz); auto dot = ax * bx + ay * by + az * bz; - lua.pushNumber(fpToLua(dot)); + lua.push(dot); return 1; } @@ -799,7 +821,7 @@ int LuaAPI::Vec3_LengthSq(lua_State* L) { ReadVec3(lua, 1, x, y, z); auto lengthSq = x * x + y * y + z * z; - lua.pushNumber(fpToLua(lengthSq)); + lua.push(lengthSq); return 1; } @@ -814,26 +836,32 @@ int LuaAPI::Vec3_Length(lua_State* L) { psyqo::FixedPoint<12> x, y, z; ReadVec3(lua, 1, x, y, z); - // Compute length in scaled world-space to avoid fp12×fp12 overflow issues. - // Convert to Lua-number domain, sqrt there, return directly. - lua_Number sx = fpToLua(x); - lua_Number sy = fpToLua(y); - lua_Number sz = fpToLua(z); - lua_Number sqVal = sx * sx + sy * sy + sz * sz; - - if (sqVal <= 0) { - lua.pushNumber(0); + // lengthSq in fp12: (x*x + y*y + z*z) is fp24 (two fp12 multiplied). + // We need sqrt(lengthSq) as fp12. + // lengthSq raw = sum of (raw*raw >> 12) values = fp12 result + auto lengthSq = x * x + y * y + z * z; + int32_t lsRaw = lengthSq.raw(); + + if (lsRaw <= 0) { + lua.push(psyqo::FixedPoint<12>()); return 1; } - - // Newton's method sqrt (integer-safe) - lua_Number guess = sqVal / 2; - if (guess == 0) guess = 1; - for (int i = 0; i < 12; i++) { - guess = (guess + sqVal / guess) / 2; + + // Integer sqrt of (lsRaw << 12) to get result in fp12 + // sqrt(fp12_value) = sqrt(raw/4096) = sqrt(raw)/64 + // So: result_raw = isqrt(raw * 4096) = isqrt(raw << 12) + // isqrt(lsRaw) gives integer sqrt. Multiply by 64 (sqrt(4096)) to get fp12. + // Newton's method in 32-bit: isqrt(n) + uint32_t n = (uint32_t)lsRaw; + uint32_t guess = n; + for (int i = 0; i < 16; i++) { + if (guess == 0) break; + guess = (guess + n / guess) / 2; } - - lua.pushNumber(guess); + // guess = isqrt(lsRaw). lsRaw is in fp12, so sqrt needs * sqrt(4096) = 64 + psyqo::FixedPoint<12> result; + result.value = (int32_t)(guess * 64); + lua.push(result); return 1; } @@ -848,26 +876,31 @@ int LuaAPI::Vec3_Normalize(lua_State* L) { psyqo::FixedPoint<12> x, y, z; ReadVec3(lua, 1, x, y, z); - // Work in Lua-number (world-space) domain for the sqrt - lua_Number sx = fpToLua(x); - lua_Number sy = fpToLua(y); - lua_Number sz = fpToLua(z); - lua_Number sLen = sx * sx + sy * sy + sz * sz; - - if (sLen <= 0) { - PushVec3(lua, psyqo::FixedPoint<12>(0), psyqo::FixedPoint<12>(0), psyqo::FixedPoint<12>(0)); + auto lengthSq = x * x + y * y + z * z; + int32_t lsRaw = lengthSq.raw(); + + if (lsRaw <= 0) { + PushVec3(lua, psyqo::FixedPoint<12>(), psyqo::FixedPoint<12>(), psyqo::FixedPoint<12>()); return 1; } - - // Newton's method sqrt - lua_Number guess = sLen / 2; - if (guess == 0) guess = 1; - for (int i = 0; i < 12; i++) { - guess = (guess + sLen / guess) / 2; + + // isqrt(lsRaw) * 64 = length in fp12 + uint32_t n = (uint32_t)lsRaw; + uint32_t guess = n; + for (int i = 0; i < 16; i++) { + if (guess == 0) break; + guess = (guess + n / guess) / 2; } - if (guess == 0) guess = 1; - - PushVec3(lua, luaToFp(sx / guess), luaToFp(sy / guess), luaToFp(sz / guess)); + int32_t len = (int32_t)(guess * 64); + if (len == 0) len = 1; + + // Divide each component by length: component / length in fp12 + // (x.raw * 4096) / len using 32-bit math (safe since raw values fit int16 range) + psyqo::FixedPoint<12> nx, ny, nz; + nx.value = (x.raw() * 4096) / len; + ny.value = (y.raw() * 4096) / len; + nz.value = (z.raw() * 4096) / len; + PushVec3(lua, nx, ny, nz); return 1; } @@ -890,7 +923,7 @@ int LuaAPI::Vec3_DistanceSq(lua_State* L) { auto dz = az - bz; auto distSq = dx * dx + dy * dy + dz * dz; - lua.pushNumber(fpToLua(distSq)); + lua.push(distSq); return 1; } @@ -908,24 +941,28 @@ int LuaAPI::Vec3_Distance(lua_State* L) { ReadVec3(lua, 1, ax, ay, az); ReadVec3(lua, 2, bx, by, bz); - lua_Number dx = fpToLua(ax) - fpToLua(bx); - lua_Number dy = fpToLua(ay) - fpToLua(by); - lua_Number dz = fpToLua(az) - fpToLua(bz); - - lua_Number sqVal = dx * dx + dy * dy + dz * dz; - - if (sqVal <= 0) { - lua.pushNumber(0); + auto dx = ax - bx; + auto dy = ay - by; + auto dz = az - bz; + + auto distSq = dx * dx + dy * dy + dz * dz; + int32_t dsRaw = distSq.raw(); + + if (dsRaw <= 0) { + lua.push(psyqo::FixedPoint<12>()); return 1; } - - lua_Number guess = sqVal / 2; - if (guess == 0) guess = 1; - for (int i = 0; i < 12; i++) { - guess = (guess + sqVal / guess) / 2; + + uint32_t n = (uint32_t)dsRaw; + uint32_t guess = n; + for (int i = 0; i < 16; i++) { + if (guess == 0) break; + guess = (guess + n / guess) / 2; } - - lua.pushNumber(guess); + + psyqo::FixedPoint<12> result; + result.value = (int32_t)(guess * 64); + lua.push(result); return 1; } @@ -943,7 +980,7 @@ int LuaAPI::Vec3_Lerp(lua_State* L) { ReadVec3(lua, 1, ax, ay, az); ReadVec3(lua, 2, bx, by, bz); - psyqo::FixedPoint<12> t = luaToFp(lua.toNumber(3)); + psyqo::FixedPoint<12> t = readFP(lua, 3); psyqo::FixedPoint<12> oneMinusT = psyqo::FixedPoint<12>(4096, psyqo::FixedPoint<12>::RAW) - t; psyqo::FixedPoint<12> rx = ax * oneMinusT + bx * t; @@ -1158,25 +1195,27 @@ int LuaAPI::Camera_LookAt(lua_State* L) { if (lua.isTable(1)) { ReadVec3(lua, 1, tx, ty, tz); } else { - tx = luaToFp(lua.optNumber(1, 0)); - ty = luaToFp(lua.optNumber(2, 0)); - tz = luaToFp(lua.optNumber(3, 0)); + tx = lua.isNoneOrNil(1) ? psyqo::FixedPoint<12>() : readFP(lua, 1); + ty = lua.isNoneOrNil(2) ? psyqo::FixedPoint<12>() : readFP(lua, 2); + tz = lua.isNoneOrNil(3) ? psyqo::FixedPoint<12>() : readFP(lua, 3); } auto& cam = s_sceneManager->getCamera(); auto& pos = cam.GetPosition(); // Compute direction vector from camera to target - lua_Number dx = fpToLua(tx) - fpToLua(pos.x); - lua_Number dy = fpToLua(ty) - fpToLua(pos.y); - lua_Number dz = fpToLua(tz) - fpToLua(pos.z); - + auto dx = tx - pos.x; + auto dy = ty - pos.y; + auto dz = tz - pos.z; + // Compute horizontal distance for pitch calculation - lua_Number horizDistSq = dx * dx + dz * dz; - lua_Number horizGuess = horizDistSq / 2; - if (horizGuess == 0) horizGuess = 1; - for (int i = 0; i < 12; i++) { - horizGuess = (horizGuess + horizDistSq / horizGuess) / 2; + auto horizDistSq = dx * dx + dz * dz; + int32_t hdsRaw = horizDistSq.raw(); + uint32_t hn = (uint32_t)(hdsRaw > 0 ? hdsRaw : 1); + uint32_t horizGuess = hn; + for (int i = 0; i < 16; i++) { + if (horizGuess == 0) break; + horizGuess = (horizGuess + hn / horizGuess) / 2; } // Yaw = atan2(dx, dz) — approximate with lookup or use psyqo trig @@ -1554,6 +1593,67 @@ int LuaAPI::Cutscene_IsPlaying(lua_State* L) { return 1; } +// ============================================================================ +// ANIMATION API IMPLEMENTATION +// ============================================================================ + +int LuaAPI::Animation_Play(lua_State* L) { + psyqo::Lua lua(L); + + if (!s_animationPlayer || !lua.isString(1)) { + return 0; + } + + const char* name = lua.toString(1); + bool loop = false; + int onCompleteRef = LUA_NOREF; + + if (lua.isTable(2)) { + lua.getField(2, "loop"); + if (lua.isBoolean(-1)) loop = lua.toBoolean(-1); + lua.pop(); + + lua.getField(2, "onComplete"); + if (lua.isFunction(-1)) { + onCompleteRef = lua.ref(); // pops and stores in registry + } else { + lua.pop(); + } + } + + s_animationPlayer->setLuaState(L); + s_animationPlayer->play(name, loop); + + if (onCompleteRef != LUA_NOREF) { + s_animationPlayer->setOnCompleteRef(name, onCompleteRef); + } + + return 0; +} + +int LuaAPI::Animation_Stop(lua_State* L) { + psyqo::Lua lua(L); + if (!s_animationPlayer) return 0; + + if (lua.isString(1)) { + s_animationPlayer->stop(lua.toString(1)); + } else { + s_animationPlayer->stopAll(); + } + return 0; +} + +int LuaAPI::Animation_IsPlaying(lua_State* L) { + psyqo::Lua lua(L); + + if (s_animationPlayer && lua.isString(1)) { + lua.push(s_animationPlayer->isPlaying(lua.toString(1))); + } else { + lua.push(false); + } + return 1; +} + // ============================================================================ // CONTROLS API IMPLEMENTATION // ============================================================================ diff --git a/src/luaapi.hh b/src/luaapi.hh index c0cb61e..b9c699d 100644 --- a/src/luaapi.hh +++ b/src/luaapi.hh @@ -8,6 +8,7 @@ namespace psxsplash { class SceneManager; // Forward declaration class CutscenePlayer; // Forward declaration +class AnimationPlayer; // Forward declaration class UISystem; // Forward declaration /** @@ -25,7 +26,7 @@ class UISystem; // Forward declaration class LuaAPI { public: // Initialize all API modules - static void RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer = nullptr, UISystem* uiSystem = nullptr); + static void RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer = nullptr, AnimationPlayer* animationPlayer = nullptr, UISystem* uiSystem = nullptr); // Called once per frame to advance the Lua frame counter static void IncrementFrameCount(); @@ -39,6 +40,9 @@ private: // Cutscene player pointer (set during RegisterAll) static CutscenePlayer* s_cutscenePlayer; + + // Animation player pointer (set during RegisterAll) + static AnimationPlayer* s_animationPlayer; // UI system pointer (set during RegisterAll) static UISystem* s_uiSystem; @@ -271,6 +275,19 @@ private: // Cutscene.IsPlaying() -> boolean static int Cutscene_IsPlaying(lua_State* L); + // ======================================================================== + // ANIMATION API - Multi-instance animation playback + // ======================================================================== + + // Animation.Play(name) or Animation.Play(name, {loop=bool, onComplete=fn}) + static int Animation_Play(lua_State* L); + + // Animation.Stop(name) -> nil + static int Animation_Stop(lua_State* L); + + // Animation.IsPlaying(name) -> boolean + static int Animation_IsPlaying(lua_State* L); + // Controls.SetEnabled(bool) - enable/disable all player input static int Controls_SetEnabled(lua_State* L); diff --git a/src/scenemanager.cpp b/src/scenemanager.cpp index ecc931c..7057c5c 100644 --- a/src/scenemanager.cpp +++ b/src/scenemanager.cpp @@ -44,7 +44,7 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc m_audio.init(); // Register the Lua API - LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_uiSystem); + LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_animationPlayer, &m_uiSystem); #ifdef PSXSPLASH_PROFILER debug::Profiler::getInstance().initialize(); @@ -113,6 +113,19 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc &m_uiSystem ); + // Copy animation data into scene manager storage + m_animationCount = sceneSetup.animationCount; + for (int i = 0; i < m_animationCount; i++) { + m_animations[i] = sceneSetup.loadedAnimations[i]; + } + + // Initialize animation player + m_animationPlayer.init( + m_animationCount > 0 ? m_animations : nullptr, + m_animationCount, + &m_uiSystem + ); + // Initialize UI system (v13+) if (sceneSetup.uiCanvasCount > 0 && sceneSetup.uiTableOffset != 0 && s_font != nullptr) { m_uiSystem.init(*s_font); @@ -157,6 +170,35 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc } } } + + // Resolve UI track handles for animation tracks (same logic) + for (int ai = 0; ai < m_animationCount; ai++) { + for (uint8_t ti = 0; ti < m_animations[ai].trackCount; ti++) { + auto& track = m_animations[ai].tracks[ti]; + bool isUI = static_cast(track.trackType) >= 5; + if (!isUI || track.target == nullptr) continue; + + const char* nameStr = reinterpret_cast(track.target); + track.target = nullptr; + + if (track.trackType == TrackType::UICanvasVisible) { + track.uiHandle = static_cast(m_uiSystem.findCanvas(nameStr)); + } else { + const char* sep = nameStr; + while (*sep && *sep != '/') sep++; + if (*sep == '/') { + char* mutableSep = const_cast(sep); + *mutableSep = '\0'; + int canvasIdx = m_uiSystem.findCanvas(nameStr); + *mutableSep = '/'; + if (canvasIdx >= 0) { + track.uiHandle = static_cast( + m_uiSystem.findElement(canvasIdx, sep + 1)); + } + } + } + } + } } else { Renderer::GetInstance().SetUISystem(nullptr); } @@ -242,7 +284,15 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc for (auto object : m_gameObjects) { L.RegisterGameObject(object); - } + } + + // Fire all onCreate events AFTER all objects are registered, + // so Entity.Find works across all objects in onCreate handlers. + if (!m_gameObjects.empty()) { + L.FireAllOnCreate( + reinterpret_cast(m_gameObjects.data()), + m_gameObjects.size()); + } m_controls.forceAnalogMode(); m_controls.Init(); @@ -262,7 +312,8 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { LuaAPI::IncrementFrameCount(); m_cutscenePlayer.tick(); - + m_animationPlayer.tick(); + { uint32_t now = gpu.now(); if (m_lastFrameTime != 0) { @@ -279,7 +330,7 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { int camRoom = -1; if (m_navRegions.isLoaded()) { - if (m_cutscenePlayer.isPlaying()) { + if (m_cutscenePlayer.isPlaying() && m_cutscenePlayer.hasCameraTracks()) { auto& camPos = m_currentCamera.GetPosition(); uint16_t camRegion = m_navRegions.findRegion(camPos.x.value, camPos.z.value); if (camRegion != NAV_NO_REGION) { @@ -478,7 +529,7 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { // (no nav regions / no PSXPlayer), the camera is driven entirely // by cutscenes and Lua. After a cutscene ends in free mode, the // camera stays at the last cutscene position. - if (m_cameraFollowsPlayer && !m_cutscenePlayer.isPlaying()) { + if (m_cameraFollowsPlayer && !(m_cutscenePlayer.isPlaying() && m_cutscenePlayer.hasCameraTracks())) { m_currentCamera.SetPosition(static_cast>(m_playerPosition.x), static_cast>(m_playerPosition.y), static_cast>(m_playerPosition.z)); @@ -647,7 +698,6 @@ void psxsplash::SceneManager::processEnableDisableEvents() { // ============================================================================ void psxsplash::SceneManager::requestSceneLoad(int sceneIndex) { - if (sceneIndex == m_currentSceneIndex) return; m_pendingSceneIndex = sceneIndex; } @@ -763,6 +813,8 @@ void psxsplash::SceneManager::clearScene() { m_cutsceneCount = 0; s_activePromptCanvas = -1; // Reset prompt tracking m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player + m_animationCount = 0; + m_animationPlayer.init(nullptr, 0); // Reset animation player // BVH and NavRegions will be overwritten by next load // Reset UI system (disconnect from renderer before splashpack data disappears) @@ -825,6 +877,16 @@ void psxsplash::SceneManager::shrinkBuffer() { } } + for (int ai = 0; ai < m_animationCount; ai++) { + auto& an = m_animations[ai]; + an.name = reloc(an.name); + for (uint8_t ti = 0; ti < an.trackCount; ti++) { + auto& track = an.tracks[ti]; + track.keyframes = reloc(track.keyframes); + if (track.target) track.target = reloc(track.target); + } + } + m_uiSystem.relocate(delta); if (!m_gameObjects.empty()) { diff --git a/src/scenemanager.hh b/src/scenemanager.hh index e907995..9b98d10 100644 --- a/src/scenemanager.hh +++ b/src/scenemanager.hh @@ -19,6 +19,7 @@ #include "luaapi.hh" #include "fileloader.hh" #include "cutscene.hh" +#include "animation.hh" #include "uisystem.hh" #ifdef PSXSPLASH_MEMOVERLAY #include "memoverlay.hh" @@ -146,6 +147,10 @@ class SceneManager { Cutscene m_cutscenes[MAX_CUTSCENES]; int m_cutsceneCount = 0; CutscenePlayer m_cutscenePlayer; + + Animation m_animations[MAX_ANIMATIONS]; + int m_animationCount = 0; + AnimationPlayer m_animationPlayer; UISystem m_uiSystem; #ifdef PSXSPLASH_MEMOVERLAY diff --git a/src/splashpack.cpp b/src/splashpack.cpp index b3c5be3..f164a5a 100644 --- a/src/splashpack.cpp +++ b/src/splashpack.cpp @@ -64,8 +64,11 @@ struct SPLASHPACKFileHeader { uint8_t uiPad5; uint32_t uiTableOffset; uint32_t pixelDataOffset; + uint16_t animationCount; + uint16_t animPad; + uint32_t animationTableOffset; }; -static_assert(sizeof(SPLASHPACKFileHeader) == 104, "SPLASHPACKFileHeader must be 104 bytes"); +static_assert(sizeof(SPLASHPACKFileHeader) == 112, "SPLASHPACKFileHeader must be 112 bytes"); struct SPLASHPACKTextureAtlas { uint32_t polygonsOffset; @@ -85,7 +88,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null"); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast(data); psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); - psyqo::Kernel::assert(header->version >= 16, "Splashpack version too old (need v16+): re-export from SplashEdit"); + psyqo::Kernel::assert(header->version >= 17, "Splashpack version too old (need v17+): re-export from SplashEdit"); setup.playerStartPosition = header->playerStartPos; setup.playerStartRotation = header->playerStartRot; @@ -364,6 +367,84 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup setup.uiTableOffset = header->uiTableOffset; } + // Animation loading (v17+) + if (header->animationCount > 0 && header->animationTableOffset != 0) { + setup.animationCount = 0; + uint8_t* tablePtr = data + header->animationTableOffset; + int anCount = header->animationCount; + if (anCount > MAX_ANIMATIONS) anCount = MAX_ANIMATIONS; + + for (int ai = 0; ai < anCount; ai++) { + // SPLASHPACKAnimationEntry: 12 bytes (same layout as cutscene entry) + uint32_t dataOffset = *reinterpret_cast(tablePtr); tablePtr += 4; + uint8_t nameLen = *tablePtr++; + tablePtr += 3; // pad + uint32_t nameOffset = *reinterpret_cast(tablePtr); tablePtr += 4; + + Animation& an = setup.loadedAnimations[ai]; + an.name = (nameLen > 0 && nameOffset != 0) + ? reinterpret_cast(data + nameOffset) + : nullptr; + + // SPLASHPACKAnimation: 8 bytes (no audio) + uint8_t* anPtr = data + dataOffset; + an.totalFrames = *reinterpret_cast(anPtr); anPtr += 2; + an.trackCount = *anPtr++; + an.pad = *anPtr++; + uint32_t tracksOff = *reinterpret_cast(anPtr); anPtr += 4; + + if (an.trackCount > MAX_ANIM_TRACKS) an.trackCount = MAX_ANIM_TRACKS; + + // Parse tracks (same format as cutscene tracks) + uint8_t* trackPtr = data + tracksOff; + for (uint8_t ti = 0; ti < an.trackCount; ti++) { + CutsceneTrack& track = an.tracks[ti]; + + track.trackType = static_cast(*trackPtr++); + track.keyframeCount = *trackPtr++; + uint8_t objNameLen = *trackPtr++; + trackPtr++; // pad + uint32_t objNameOff = *reinterpret_cast(trackPtr); trackPtr += 4; + uint32_t kfOff = *reinterpret_cast(trackPtr); trackPtr += 4; + + track.keyframes = (track.keyframeCount > 0 && kfOff != 0) + ? reinterpret_cast(data + kfOff) + : nullptr; + + track.target = nullptr; + track.uiHandle = -1; + if (objNameLen > 0 && objNameOff != 0) { + const char* objName = reinterpret_cast(data + objNameOff); + bool isUITrack = static_cast(track.trackType) >= 5; + if (isUITrack) { + track.target = reinterpret_cast(const_cast(objName)); + } else { + for (size_t oi = 0; oi < setup.objectNames.size(); oi++) { + if (setup.objectNames[oi] && + streq(setup.objectNames[oi], objName)) { + track.target = setup.objects[oi]; + break; + } + } + } + } + } + + // Zero unused track slots + for (uint8_t ti = an.trackCount; ti < MAX_ANIM_TRACKS; ti++) { + an.tracks[ti].keyframeCount = 0; + an.tracks[ti].keyframes = nullptr; + an.tracks[ti].target = nullptr; + an.tracks[ti].uiHandle = -1; + an.tracks[ti].initialValues[0] = 0; + an.tracks[ti].initialValues[1] = 0; + an.tracks[ti].initialValues[2] = 0; + } + + setup.animationCount++; + } + } + setup.liveDataSize = header->pixelDataOffset; } diff --git a/src/splashpack.hh b/src/splashpack.hh index 8772537..d68eafa 100644 --- a/src/splashpack.hh +++ b/src/splashpack.hh @@ -12,6 +12,7 @@ #include "audiomanager.hh" #include "interactable.hh" #include "cutscene.hh" +#include "animation.hh" #include "uisystem.hh" namespace psxsplash { @@ -94,6 +95,9 @@ struct SplashpackSceneSetup { Cutscene loadedCutscenes[MAX_CUTSCENES]; int cutsceneCount = 0; + Animation loadedAnimations[MAX_ANIMATIONS]; + int animationCount = 0; + uint16_t uiCanvasCount = 0; uint8_t uiFontCount = 0; uint32_t uiTableOffset = 0;