/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "MusicObject.h" #include "../AssetPackManager.h" #include "../Context.h" #include "../OpenRCT2.h" #include "../PlatformEnvironment.h" #include "../audio/AudioContext.h" #include "../audio/AudioSource.h" #include "../core/IStream.hpp" #include "../core/Json.hpp" #include "../core/Path.hpp" #include "../drawing/Image.h" #include "../localisation/StringIds.h" #include "../ride/Ride.h" #include "RideObject.h" #include using namespace OpenRCT2; using namespace OpenRCT2::Audio; constexpr size_t kDefaultBytesPerTick = 1378; void MusicObject::Load() { GetStringTable().Sort(); NameStringId = LanguageAllocateObjectString(GetName()); // Start with base images _loadedSampleTable.LoadFrom(_sampleTable, 0, _sampleTable.GetCount()); // Override samples from asset packs auto context = GetContext(); auto assetManager = context->GetAssetPackManager(); if (assetManager != nullptr) { assetManager->LoadSamplesForObject(GetIdentifier(), _loadedSampleTable); } // Load metadata of samples auto& audioContext = GetContext()->GetAudioContext(); for (auto& track : _tracks) { auto stream = track.Asset.GetStream(); if (stream != nullptr) { auto source = audioContext.CreateStreamFromWAV(std::move(stream)); if (source != nullptr) { track.BytesPerTick = source->GetBytesPerSecond() / 40; track.Size = source->GetLength(); source->Release(); } else { track.BytesPerTick = kDefaultBytesPerTick; track.Size = track.Asset.GetSize(); } } else { track.BytesPerTick = kDefaultBytesPerTick; track.Size = track.Asset.GetSize(); } } _hasPreview = !!GetImageTable().GetCount(); _previewImageId = LoadImages(); } void MusicObject::Unload() { LanguageFreeObjectString(NameStringId); UnloadImages(); _hasPreview = false; _previewImageId = 0; NameStringId = 0; } void MusicObject::DrawPreview(RenderTarget& rt, int32_t width, int32_t height) const { // Write (no image) int32_t x = width / 2; int32_t y = height / 2; if (_hasPreview) GfxDrawSprite(rt, ImageId(_previewImageId), { 0, 0 }); else DrawTextBasic(rt, { x, y }, STR_WINDOW_NO_IMAGE, {}, { TextAlignment::CENTRE }); } bool MusicObject::HasPreview() const { return _hasPreview; } void MusicObject::ReadJson(IReadObjectContext* context, json_t& root) { _originalStyleId = {}; _rideTypes.clear(); _tracks.clear(); _niceFactor = MusicNiceFactor::Neutral; auto& properties = root["properties"]; if (properties != nullptr) { const auto& originalStyleId = properties["originalStyleId"]; if (originalStyleId.is_number_integer()) { _originalStyleId = originalStyleId.get(); } const auto& niceFactor = properties["niceFactor"]; if (niceFactor.is_number_integer()) { _niceFactor = static_cast(std::clamp(niceFactor.get(), -1, 1)); } const auto& jRideTypes = properties["rideTypes"]; if (jRideTypes.is_array()) { ParseRideTypes(jRideTypes); } auto& jTracks = properties["tracks"]; if (jTracks.is_array()) { ParseTracks(*context, jTracks); } } PopulateTablesFromJson(context, root); } void MusicObject::ParseRideTypes(const json_t& jRideTypes) { for (const auto& jRideType : jRideTypes) { auto szRideType = Json::GetString(jRideType); if (!szRideType.empty()) { auto rideType = RideObject::ParseRideType(szRideType); if (rideType != kRideTypeNull) { _rideTypes.push_back(rideType); } } } } void MusicObject::ParseTracks(IReadObjectContext& context, json_t& jTracks) { auto& entries = _sampleTable.GetEntries(); for (auto& jTrack : jTracks) { if (jTrack.is_object()) { MusicObjectTrack track; track.Name = Json::GetString(jTrack["name"]); track.Composer = Json::GetString(jTrack["composer"]); auto source = Json::GetString(jTrack["source"]); if (source.empty()) { context.LogError(ObjectError::InvalidProperty, "Invalid audio track definition."); } else { auto asset = GetAsset(context, source); auto& entry = entries.emplace_back(); entry.Asset = asset; track.Asset = asset; _tracks.push_back(std::move(track)); } } } } std::optional MusicObject::GetOriginalStyleId() const { return _originalStyleId; } bool MusicObject::SupportsRideType(ride_type_t rideType, bool onlyAllowIfExplicitlyListed) { if (_rideTypes.size() == 0) { // Some rides, like the merry-go-round, need music objects to explicitly list them. return !onlyAllowIfExplicitlyListed; } auto it = std::find(_rideTypes.begin(), _rideTypes.end(), rideType); return it != _rideTypes.end(); } size_t MusicObject::GetTrackCount() const { return _tracks.size(); } const MusicObjectTrack* MusicObject::GetTrack(size_t trackIndex) const { if (_tracks.size() > trackIndex) { return &_tracks[trackIndex]; } return {}; } IAudioSource* MusicObject::GetTrackSample(size_t trackIndex) const { return _loadedSampleTable.LoadSample(static_cast(trackIndex)); } ObjectAsset MusicObject::GetAsset(IReadObjectContext& context, std::string_view path) { if (path.find("$RCT2:DATA/") == 0) { auto& env = GetContext()->GetPlatformEnvironment(); auto path2 = env.FindFile(DirBase::rct2, DirId::data, path.substr(11)); return ObjectAsset(path2); } return context.GetAsset(path); }