From 893392d98732e081faadfa62ee79f9a1d9794f17 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sun, 5 May 2024 23:09:41 +0200 Subject: [PATCH] Scripting: allow precise and safe control of peep animations --- distribution/changelog.txt | 1 + distribution/openrct2.d.ts | 98 +++++++++ src/openrct2/entity/Peep.cpp | 17 +- src/openrct2/entity/Peep.h | 1 + src/openrct2/scripting/ScriptEngine.h | 2 +- .../scripting/bindings/entity/ScGuest.cpp | 160 +++++++++++++++ .../scripting/bindings/entity/ScGuest.hpp | 10 + .../scripting/bindings/entity/ScStaff.cpp | 194 ++++++++++++++++++ .../scripting/bindings/entity/ScStaff.hpp | 12 ++ 9 files changed, 492 insertions(+), 3 deletions(-) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index a6fac3a6d7..dd8f84db51 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -1,6 +1,7 @@ 0.4.12 (in development) ------------------------------------------------------------------------ - Feature: [#21714] [Plugin] Costume assignment is now tailored to each staff type. +- Feature: [#21913] [Plugin] Allow precise and safe control of peep animations. 0.4.11 (2024-05-05) ------------------------------------------------------------------------ diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 473648a89c..b764a2faf8 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -2682,6 +2682,34 @@ declare global { */ type PeepType = "guest" | "staff"; + type GuestAnimation = + "walking" | + "checkTime" | + "watchRide" | + "eatFood" | + "shakeHead" | + "emptyPockets" | + "holdMat" | + "sittingIdle" | + "sittingEatFood" | + "sittingLookAroundLeft" | + "sittingLookAroundRight" | + "hanging" | + "wow" | + "throwUp" | + "jump" | + "drowning" | + "joy" | + "readMap" | + "wave" | + "wave2" | + "takePhoto" | + "clap" | + "disgust" | + "drawPicture" | + "beingWatched" | + "withdrawMoney"; + /** * Represents a guest. */ @@ -2819,6 +2847,31 @@ declare global { * Removes all items from the guest's possession. */ removeAllItems(): void; + + /** + * The animations available to this guest. + */ + readonly availableAnimations: GuestAnimation[]; + + /** + * Gets an array of sprite ids representing a particular guest animation. + */ + getAnimationSpriteIds(animation: GuestAnimation, rotation: number): number[]; + + /** + * The animation the guest is currently exhibiting. + */ + animation: GuestAnimation; + + /** + * The frame offset in the current animation. + */ + animationOffset: number; + + /** + * The total number of frames in the current animation. + */ + readonly animationLength: number; } /** @@ -3113,6 +3166,26 @@ declare global { "sheriff" | "pirate"; + type StaffAnimation = + "walking" | + "watchRide" | + "wave" | + "hanging" | + "staffMower" | + "staffSweep" | + "drowning" | + "staffAnswerCall" | + "staffAnswerCall2" | + "staffCheckBoard" | + "staffFix" | + "staffFix2" | + "staffFixGround" | + "staffFix3" | + "staffWatering" | + "joy" | + "staffEmptyBin" | + "wave2"; + /** * Represents a staff member. */ @@ -3146,6 +3219,31 @@ declare global { * Gets the patrol area for the staff member. */ readonly patrolArea: PatrolArea; + + /** + * The animations available to this staff member. + */ + readonly availableAnimations: StaffAnimation[]; + + /** + * Gets an array of sprite ids representing a particular staff animation. + */ + getAnimationSpriteIds(animation: StaffAnimation, rotation: number): number[]; + + /** + * The animation the staff member is currently exhibiting. + */ + animation: StaffAnimation; + + /** + * The frame offset in the current animation. + */ + animationOffset: number; + + /** + * The total number of frames in the current animation. + */ + readonly animationLength: number; } type StaffType = "handyman" | "mechanic" | "security" | "entertainer"; diff --git a/src/openrct2/entity/Peep.cpp b/src/openrct2/entity/Peep.cpp index 18bb860f1d..f8ecc38abd 100644 --- a/src/openrct2/entity/Peep.cpp +++ b/src/openrct2/entity/Peep.cpp @@ -344,9 +344,15 @@ void Peep::UpdateCurrentActionSpriteType() return; } - Invalidate(); ActionSpriteType = newActionSpriteType; + UpdateSpriteBoundingBox(); +} + +void Peep::UpdateSpriteBoundingBox() +{ + Invalidate(); + const SpriteBounds* spriteBounds = &GetSpriteBounds(SpriteType, ActionSpriteType); SpriteData.Width = spriteBounds->sprite_width; SpriteData.HeightMin = spriteBounds->sprite_height_negative; @@ -2813,7 +2819,14 @@ void Peep::Paint(PaintSession& session, int32_t imageDirection) const // In the following 4 calls to PaintAddImageAsParent/PaintAddImageAsChild, we add 5 (instead of 3) to the // bound_box_offset_z to make sure peeps are drawn on top of railways - uint32_t baseImageId = (imageDirection >> 3) + GetPeepAnimation(SpriteType, actionSpriteType).base_image + imageOffset * 4; + uint32_t baseImageId = GetPeepAnimation(SpriteType, actionSpriteType).base_image; + + // Offset frame onto the base image, using rotation except for the 'picked up' state + if (actionSpriteType != PeepActionSpriteType::Ui) + baseImageId += (imageDirection >> 3) + imageOffset * 4; + else + baseImageId += imageOffset; + auto imageId = ImageId(baseImageId, TshirtColour, TrousersColour); auto bb = BoundBoxXYZ{ { 0, 0, z + 5 }, { 1, 1, 11 } }; diff --git a/src/openrct2/entity/Peep.h b/src/openrct2/entity/Peep.h index 23e1eda2af..ae934c8522 100644 --- a/src/openrct2/entity/Peep.h +++ b/src/openrct2/entity/Peep.h @@ -380,6 +380,7 @@ public: // Peep void SetState(PeepState new_state); void Remove(); void UpdateCurrentActionSpriteType(); + void UpdateSpriteBoundingBox(); void SwitchToSpecialSprite(uint8_t special_sprite_id); void StateReset(); [[nodiscard]] uint8_t GetNextDirection() const; diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 381044d17d..cc3b892a0d 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -47,7 +47,7 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { - static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 86; + static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 87; // Versions marking breaking changes. static constexpr int32_t API_VERSION_33_PEEP_DEPRECATION = 33; diff --git a/src/openrct2/scripting/bindings/entity/ScGuest.cpp b/src/openrct2/scripting/bindings/entity/ScGuest.cpp index f6072f989d..3f650c504d 100644 --- a/src/openrct2/scripting/bindings/entity/ScGuest.cpp +++ b/src/openrct2/scripting/bindings/entity/ScGuest.cpp @@ -13,6 +13,7 @@ # include "../../../entity/Guest.h" # include "../../../localisation/Localisation.h" +# include "../../../peep/PeepAnimationData.h" namespace OpenRCT2::Scripting { @@ -144,6 +145,35 @@ namespace OpenRCT2::Scripting { "here_we_are", PeepThoughtType::HereWeAre }, }); + static const DukEnumMap availableGuestAnimations({ + { "walking", PeepActionSpriteType::None }, + { "checkTime", PeepActionSpriteType::CheckTime }, + { "watchRide", PeepActionSpriteType::WatchRide }, + { "eatFood", PeepActionSpriteType::EatFood }, + { "shakeHead", PeepActionSpriteType::ShakeHead }, + { "emptyPockets", PeepActionSpriteType::EmptyPockets }, + { "holdMat", PeepActionSpriteType::HoldMat }, + { "sittingIdle", PeepActionSpriteType::SittingIdle }, + { "sittingEatFood", PeepActionSpriteType::SittingEatFood }, + { "sittingLookAroundLeft", PeepActionSpriteType::SittingLookAroundLeft }, + { "sittingLookAroundRight", PeepActionSpriteType::SittingLookAroundRight }, + { "hanging", PeepActionSpriteType::Ui }, + { "wow", PeepActionSpriteType::Wow }, + { "throwUp", PeepActionSpriteType::ThrowUp }, + { "jump", PeepActionSpriteType::Jump }, + { "drowning", PeepActionSpriteType::Drowning }, + { "joy", PeepActionSpriteType::Joy }, + { "readMap", PeepActionSpriteType::ReadMap }, + { "wave", PeepActionSpriteType::Wave }, + { "wave2", PeepActionSpriteType::Wave2 }, + { "takePhoto", PeepActionSpriteType::TakePhoto }, + { "clap", PeepActionSpriteType::Clap }, + { "disgust", PeepActionSpriteType::Disgust }, + { "drawPicture", PeepActionSpriteType::DrawPicture }, + { "beingWatched", PeepActionSpriteType::BeingWatched }, + { "withdrawMoney", PeepActionSpriteType::WithdrawMoney }, + }); + ScGuest::ScGuest(EntityId id) : ScPeep(id) { @@ -174,6 +204,11 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScGuest::lostCountdown_get, &ScGuest::lostCountdown_set, "lostCountdown"); dukglue_register_property(ctx, &ScGuest::thoughts_get, nullptr, "thoughts"); dukglue_register_property(ctx, &ScGuest::items_get, nullptr, "items"); + dukglue_register_property(ctx, &ScGuest::availableAnimations_get, nullptr, "availableAnimations"); + dukglue_register_property(ctx, &ScGuest::animation_get, &ScGuest::animation_set, "animation"); + dukglue_register_property(ctx, &ScGuest::animationOffset_get, &ScGuest::animationOffset_set, "animationOffset"); + dukglue_register_property(ctx, &ScGuest::animationLength_get, nullptr, "animationLength"); + dukglue_register_method(ctx, &ScGuest::getAnimationSpriteIds, "getAnimationSpriteIds"); dukglue_register_method(ctx, &ScGuest::has_item, "hasItem"); dukglue_register_method(ctx, &ScGuest::give_item, "giveItem"); dukglue_register_method(ctx, &ScGuest::remove_item, "removeItem"); @@ -791,6 +826,131 @@ namespace OpenRCT2::Scripting } } + std::vector ScGuest::availableAnimations_get() const + { + std::vector availableAnimations{}; + for (auto& animation : availableGuestAnimations) + { + availableAnimations.push_back(std::string(animation.first)); + } + return availableAnimations; + } + + std::vector ScGuest::getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const + { + std::vector spriteIds{}; + + auto animationType = availableGuestAnimations.TryGet(groupKey); + if (animationType == std::nullopt) + { + return spriteIds; + } + + auto peep = GetPeep(); + if (peep != nullptr) + { + auto& animationGroup = GetPeepAnimation(peep->SpriteType, *animationType); + for (auto frameOffset : animationGroup.frame_offsets) + { + auto imageId = animationGroup.base_image; + if (animationType != PeepActionSpriteType::Ui) + imageId += rotation + frameOffset * 4; + else + imageId += frameOffset; + + spriteIds.push_back(imageId); + } + } + return spriteIds; + } + + std::string ScGuest::animation_get() const + { + auto* peep = GetGuest(); + if (peep == nullptr) + { + return nullptr; + } + + std::string_view action = availableGuestAnimations[peep->ActionSpriteType]; + + // Special consideration for sitting peeps + // TODO: something funky going on in the state machine + if (peep->ActionSpriteType == PeepActionSpriteType::None && peep->State == PeepState::Sitting) + action = availableGuestAnimations[PeepActionSpriteType::SittingIdle]; + + return std::string(action); + } + + void ScGuest::animation_set(std::string groupKey) + { + ThrowIfGameStateNotMutable(); + + auto newType = availableGuestAnimations.TryGet(groupKey); + if (newType == std::nullopt) + { + throw DukException() << "Invalid animation for this guest (" << groupKey << ")"; + } + + auto* peep = GetGuest(); + peep->ActionSpriteType = peep->NextActionSpriteType = *newType; + + auto offset = 0; + if (peep->IsActionWalking()) + peep->WalkingFrameNum = offset; + else + peep->ActionFrame = offset; + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType); + peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset]; + peep->UpdateSpriteBoundingBox(); + } + + uint8_t ScGuest::animationOffset_get() const + { + auto* peep = GetGuest(); + if (peep == nullptr) + { + return 0; + } + + if (peep->IsActionWalking()) + return peep->WalkingFrameNum; + else + return peep->ActionFrame; + } + + void ScGuest::animationOffset_set(uint8_t offset) + { + ThrowIfGameStateNotMutable(); + + auto* peep = GetGuest(); + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType); + auto length = animationGroup.frame_offsets.size(); + offset %= length; + + if (peep->IsActionWalking()) + peep->WalkingFrameNum = offset; + else + peep->ActionFrame = offset; + + peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset]; + peep->UpdateSpriteBoundingBox(); + } + + uint8_t ScGuest::animationLength_get() const + { + auto* peep = GetGuest(); + if (peep == nullptr) + { + return 0; + } + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType); + return static_cast(animationGroup.frame_offsets.size()); + } + ScThought::ScThought(PeepThought backing) : _backing(backing) { diff --git a/src/openrct2/scripting/bindings/entity/ScGuest.hpp b/src/openrct2/scripting/bindings/entity/ScGuest.hpp index 4aa6f48285..90e1dac810 100644 --- a/src/openrct2/scripting/bindings/entity/ScGuest.hpp +++ b/src/openrct2/scripting/bindings/entity/ScGuest.hpp @@ -14,6 +14,8 @@ # include "../../../entity/Guest.h" # include "ScPeep.hpp" +enum class PeepActionSpriteType : uint8_t; + namespace OpenRCT2::Scripting { static const DukEnumMap ShopItemMap({ @@ -172,6 +174,14 @@ namespace OpenRCT2::Scripting void give_item(const DukValue& item) const; void remove_item(const DukValue& item) const; void remove_all_items() const; + + std::vector availableAnimations_get() const; + std::vector getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const; + std::string animation_get() const; + void animation_set(std::string groupKey); + uint8_t animationOffset_get() const; + void animationOffset_set(uint8_t offset); + uint8_t animationLength_get() const; }; } // namespace OpenRCT2::Scripting diff --git a/src/openrct2/scripting/bindings/entity/ScStaff.cpp b/src/openrct2/scripting/bindings/entity/ScStaff.cpp index dbc31aa15f..995223acf1 100644 --- a/src/openrct2/scripting/bindings/entity/ScStaff.cpp +++ b/src/openrct2/scripting/bindings/entity/ScStaff.cpp @@ -13,9 +13,51 @@ # include "../../../entity/PatrolArea.h" # include "../../../entity/Staff.h" +# include "../../../peep/PeepAnimationData.h" namespace OpenRCT2::Scripting { + static const DukEnumMap availableHandymanAnimations({ + { "walking", PeepActionSpriteType::None }, + { "watchRide", PeepActionSpriteType::WatchRide }, + { "hanging", PeepActionSpriteType::Ui }, + { "staffMower", PeepActionSpriteType::StaffMower }, + { "staffSweep", PeepActionSpriteType::StaffSweep }, + { "drowning", PeepActionSpriteType::Drowning }, + { "staffWatering", PeepActionSpriteType::StaffWatering }, + { "staffEmptyBin", PeepActionSpriteType::StaffEmptyBin }, + }); + + static const DukEnumMap availableMechanicAnimations({ + { "walking", PeepActionSpriteType::None }, + { "watchRide", PeepActionSpriteType::WatchRide }, + { "hanging", PeepActionSpriteType::Ui }, + { "staffAnswerCall", PeepActionSpriteType::StaffAnswerCall }, + { "staffAnswerCall2", PeepActionSpriteType::StaffAnswerCall2 }, + { "staffCheckBoard", PeepActionSpriteType::StaffCheckboard }, + { "staffFix", PeepActionSpriteType::StaffFix }, + { "staffFix2", PeepActionSpriteType::StaffFix2 }, + { "staffFixGround", PeepActionSpriteType::StaffFixGround }, + { "staffFix3", PeepActionSpriteType::StaffFix3 }, + }); + + static const DukEnumMap availableSecurityAnimations({ + { "walking", PeepActionSpriteType::None }, + { "watchRide", PeepActionSpriteType::WatchRide }, + { "hanging", PeepActionSpriteType::Ui }, + { "drowning", PeepActionSpriteType::Drowning }, + }); + + static const DukEnumMap availableEntertainerAnimations({ + { "walking", PeepActionSpriteType::None }, + { "watchRide", PeepActionSpriteType::WatchRide }, + { "wave", PeepActionSpriteType::EatFood }, // NB: this not a typo + { "hanging", PeepActionSpriteType::Ui }, + { "drowning", PeepActionSpriteType::Drowning }, + { "joy", PeepActionSpriteType::Joy }, + { "wave2", PeepActionSpriteType::Wave2 }, + }); + ScStaff::ScStaff(EntityId Id) : ScPeep(Id) { @@ -30,6 +72,11 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScStaff::costume_get, &ScStaff::costume_set, "costume"); dukglue_register_property(ctx, &ScStaff::patrolArea_get, nullptr, "patrolArea"); dukglue_register_property(ctx, &ScStaff::orders_get, &ScStaff::orders_set, "orders"); + dukglue_register_property(ctx, &ScStaff::availableAnimations_get, nullptr, "availableAnimations"); + dukglue_register_property(ctx, &ScStaff::animation_get, &ScStaff::animation_set, "animation"); + dukglue_register_property(ctx, &ScStaff::animationOffset_get, &ScStaff::animationOffset_set, "animationOffset"); + dukglue_register_property(ctx, &ScStaff::animationLength_get, nullptr, "animationLength"); + dukglue_register_method(ctx, &ScStaff::getAnimationSpriteIds, "getAnimationSpriteIds"); } Staff* ScStaff::GetStaff() const @@ -243,6 +290,153 @@ namespace OpenRCT2::Scripting } } + const DukEnumMap& ScStaff::animationsByStaffType(StaffType staffType) const + { + switch (staffType) + { + case StaffType::Handyman: + return availableHandymanAnimations; + case StaffType::Mechanic: + return availableMechanicAnimations; + case StaffType::Security: + return availableSecurityAnimations; + case StaffType::Entertainer: + default: + return availableEntertainerAnimations; + } + } + + std::vector ScStaff::availableAnimations_get() const + { + std::vector availableAnimations{}; + + auto* peep = GetStaff(); + if (peep != nullptr) + { + for (auto& animation : animationsByStaffType(peep->AssignedStaffType)) + { + availableAnimations.push_back(std::string(animation.first)); + } + } + + return availableAnimations; + } + + std::vector ScStaff::getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const + { + std::vector spriteIds{}; + + auto* peep = GetStaff(); + if (peep == nullptr) + { + return spriteIds; + } + + auto& animationGroups = animationsByStaffType(peep->AssignedStaffType); + auto animationType = animationGroups.TryGet(groupKey); + if (animationType == std::nullopt) + { + return spriteIds; + } + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, *animationType); + for (auto frameOffset : animationGroup.frame_offsets) + { + auto imageId = animationGroup.base_image; + if (animationType != PeepActionSpriteType::Ui) + imageId += rotation + frameOffset * 4; + else + imageId += frameOffset; + + spriteIds.push_back(imageId); + } + + return spriteIds; + } + + std::string ScStaff::animation_get() const + { + auto* peep = GetStaff(); + if (peep == nullptr) + { + return nullptr; + } + + auto& animationGroups = animationsByStaffType(peep->AssignedStaffType); + std::string_view action = animationGroups[peep->ActionSpriteType]; + return std::string(action); + } + + void ScStaff::animation_set(std::string groupKey) + { + ThrowIfGameStateNotMutable(); + + auto* peep = GetStaff(); + auto& animationGroups = animationsByStaffType(peep->AssignedStaffType); + auto newType = animationGroups.TryGet(groupKey); + if (newType == std::nullopt) + { + throw DukException() << "Invalid animation for this staff member (" << groupKey << ")"; + } + + peep->ActionSpriteType = peep->NextActionSpriteType = *newType; + + auto offset = 0; + if (peep->IsActionWalking()) + peep->WalkingFrameNum = offset; + else + peep->ActionFrame = offset; + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType); + peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset]; + peep->UpdateSpriteBoundingBox(); + } + + uint8_t ScStaff::animationOffset_get() const + { + auto* peep = GetStaff(); + if (peep == nullptr) + { + return 0; + } + + if (peep->IsActionWalking()) + return peep->WalkingFrameNum; + else + return peep->ActionFrame; + } + + void ScStaff::animationOffset_set(uint8_t offset) + { + ThrowIfGameStateNotMutable(); + + auto* peep = GetStaff(); + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType); + auto length = animationGroup.frame_offsets.size(); + offset %= length; + + if (peep->IsActionWalking()) + peep->WalkingFrameNum = offset; + else + peep->ActionFrame = offset; + + peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset]; + peep->UpdateSpriteBoundingBox(); + } + + uint8_t ScStaff::animationLength_get() const + { + auto* peep = GetStaff(); + if (peep == nullptr) + { + return 0; + } + + auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType); + return static_cast(animationGroup.frame_offsets.size()); + } + ScPatrolArea::ScPatrolArea(EntityId id) : _staffId(id) { diff --git a/src/openrct2/scripting/bindings/entity/ScStaff.hpp b/src/openrct2/scripting/bindings/entity/ScStaff.hpp index a6f79c3523..3e4a94aa41 100644 --- a/src/openrct2/scripting/bindings/entity/ScStaff.hpp +++ b/src/openrct2/scripting/bindings/entity/ScStaff.hpp @@ -15,6 +15,9 @@ # include +enum class PeepActionSpriteType : uint8_t; +enum class StaffType : uint8_t; + namespace OpenRCT2::Scripting { class ScPatrolArea @@ -64,6 +67,15 @@ namespace OpenRCT2::Scripting uint8_t orders_get() const; void orders_set(uint8_t value); + + const DukEnumMap& animationsByStaffType(StaffType staffType) const; + std::vector getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const; + std::vector availableAnimations_get() const; + std::string animation_get() const; + void animation_set(std::string groupKey); + uint8_t animationOffset_get() const; + void animationOffset_set(uint8_t offset); + uint8_t animationLength_get() const; }; } // namespace OpenRCT2::Scripting