diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index 8e36bc9bb6..740cc28723 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3624,6 +3624,7 @@ STR_6516 :One or more objects added require RCT1 linked for proper display. F STR_6517 :One or more objects in this park require RCT1 linked for proper display. Fallback images will be used. STR_6518 :{BLACK}Hover over a scenario to view its description and objective. Click it to start playing. STR_6519 :Extras +STR_6520 :Asset Packs ############# # Scenarios # diff --git a/resources/g2/icons/arrow_down.png b/resources/g2/icons/arrow_down.png new file mode 100644 index 0000000000..5dd30e5a03 Binary files /dev/null and b/resources/g2/icons/arrow_down.png differ diff --git a/resources/g2/icons/arrow_up.png b/resources/g2/icons/arrow_up.png new file mode 100644 index 0000000000..8840bc9943 Binary files /dev/null and b/resources/g2/icons/arrow_up.png differ diff --git a/resources/g2/sprites.json b/resources/g2/sprites.json index cbc615147a..b184d9938a 100644 --- a/resources/g2/sprites.json +++ b/resources/g2/sprites.json @@ -540,6 +540,16 @@ { "path": "sideways-tab-active.png" }, + { + "path": "icons/arrow_up.png", + "x_offset": 5, + "y_offset": 5 + }, + { + "path": "icons/arrow_down.png", + "x_offset": 5, + "y_offset": 5 + }, { "path": "font/latin/ae-uc-small.png", "y_offset": 0, diff --git a/src/openrct2-ui/WindowManager.cpp b/src/openrct2-ui/WindowManager.cpp index f020ce6f96..22b02b54de 100644 --- a/src/openrct2-ui/WindowManager.cpp +++ b/src/openrct2-ui/WindowManager.cpp @@ -136,6 +136,8 @@ public: return WindowWaterOpen(); case WindowClass::Transparency: return WindowTransparencyOpen(); + case WindowClass::AssetPacks: + return WindowAssetPacksOpen(); default: Console::Error::WriteLine("Unhandled window class (%d)", wc); return nullptr; diff --git a/src/openrct2-ui/interface/Theme.cpp b/src/openrct2-ui/interface/Theme.cpp index aa8a3ab567..872cfdbdfe 100644 --- a/src/openrct2-ui/interface/Theme.cpp +++ b/src/openrct2-ui/interface/Theme.cpp @@ -129,6 +129,7 @@ static constexpr const WindowThemeDesc WindowThemeDescriptors[] = { WindowClass::Scenery, "WC_SCENERY", STR_THEMES_WINDOW_SCENERY, COLOURS_3(COLOUR_DARK_BROWN, COLOUR_DARK_GREEN, COLOUR_DARK_GREEN ) }, { WindowClass::SceneryScatter, "WC_SCENERY_SCATTER", STR_THEMES_WINDOW_SCENERY_SCATTER, COLOURS_3(COLOUR_DARK_BROWN, COLOUR_DARK_GREEN, COLOUR_DARK_GREEN ) }, { WindowClass::Options, "WC_OPTIONS", STR_THEMES_WINDOW_OPTIONS, COLOURS_3(COLOUR_GREY, COLOUR_LIGHT_BLUE, COLOUR_LIGHT_BLUE ) }, + { WindowClass::AssetPacks, "WC_ASSET_PACKS", STR_ASSET_PACKS, COLOURS_3(COLOUR_LIGHT_BLUE, COLOUR_LIGHT_BLUE, COLOUR_LIGHT_BLUE ) }, { WindowClass::Footpath, "WC_FOOTPATH", STR_THEMES_WINDOW_FOOTPATH, COLOURS_3(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN ) }, { WindowClass::Land, "WC_LAND", STR_THEMES_WINDOW_LAND, COLOURS_3(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN ) }, { WindowClass::Water, "WC_WATER", STR_THEMES_WINDOW_WATER, COLOURS_3(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN ) }, @@ -206,6 +207,7 @@ static constexpr const UIThemeWindowEntry PredefinedThemeRCT1_Entries[] = { WindowClass::TitleOptions, COLOURS_RCT1(TRANSLUCENT(COLOUR_GREY), TRANSLUCENT(COLOUR_GREY), TRANSLUCENT(COLOUR_GREY), COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, { WindowClass::Staff, COLOURS_RCT1(COLOUR_DARK_GREEN, COLOUR_LIGHT_PURPLE, COLOUR_LIGHT_PURPLE, COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, { WindowClass::Options, COLOURS_RCT1(COLOUR_GREY, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, + { WindowClass::AssetPacks, COLOURS_RCT1(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, { WindowClass::KeyboardShortcutList, COLOURS_RCT1(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, { WindowClass::ChangeKeyboardShortcut, COLOURS_RCT1(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, { WindowClass::TrackDesignList, COLOURS_RCT1(COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_DARK_BROWN, COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK) }, diff --git a/src/openrct2-ui/libopenrct2ui.vcxproj b/src/openrct2-ui/libopenrct2ui.vcxproj index c9d29b78a3..aebee18021 100644 --- a/src/openrct2-ui/libopenrct2ui.vcxproj +++ b/src/openrct2-ui/libopenrct2ui.vcxproj @@ -136,6 +136,7 @@ + diff --git a/src/openrct2-ui/windows/AssetPacks.cpp b/src/openrct2-ui/windows/AssetPacks.cpp new file mode 100644 index 0000000000..679c15a190 --- /dev/null +++ b/src/openrct2-ui/windows/AssetPacks.cpp @@ -0,0 +1,298 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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 +#include +#include +#include +#include +#include +#include +#include + +using namespace OpenRCT2; + +static constexpr const StringId WINDOW_TITLE = STR_ASSET_PACKS; +static constexpr const int32_t WW = 400; +static constexpr const int32_t WH = 200; + +// clang-format off +enum WindowAssetPacksWidgetIdx { + WIDX_BACKGROUND, + WIDX_TITLE, + WIDX_CLOSE, + WIDX_LIST, + WIDX_TOGGLE, + WIDX_MOVE_UP, + WIDX_MOVE_DOWN, + WIDX_APPLY, +}; + +static rct_widget WindowAssetPacksWidgets[] = { + WINDOW_SHIM(WINDOW_TITLE, WW, WH), + MakeWidget({ 0, 0 }, { 0, 0 }, WindowWidgetType::Scroll, WindowColour::Secondary, SCROLL_VERTICAL), + MakeWidget({ 0, 0 }, { 0, 0 }, WindowWidgetType::FlatBtn, WindowColour::Secondary, SPR_OPEN, STR_NONE), + MakeWidget({ 0, 0 }, { 0, 0 }, WindowWidgetType::FlatBtn, WindowColour::Secondary, SPR_G2_ARROW_UP, STR_NONE), + MakeWidget({ 0, 0 }, { 0, 0 }, WindowWidgetType::FlatBtn, WindowColour::Secondary, SPR_G2_ARROW_DOWN, STR_NONE), + MakeWidget({ 0, 0 }, { 0, 0 }, WindowWidgetType::FlatBtn, WindowColour::Secondary, SPR_ROTATE_ARROW, STR_NONE), + WIDGETS_END, +}; +// clang-format on + +class AssetPacksWindow final : public Window +{ +private: + std::optional _highlightedIndex; + std::optional _selectedIndex; + +public: + void OnOpen() override + { + widgets = WindowAssetPacksWidgets; + WindowInitScrollWidgets(*this); + } + + void OnClose() override + { + Apply(); + } + + void OnMouseUp(WidgetIndex widgetIndex) override + { + switch (widgetIndex) + { + case WIDX_CLOSE: + Close(); + break; + case WIDX_TOGGLE: + ToggleSelectedAssetPack(); + break; + case WIDX_MOVE_UP: + ReorderSelectedAssetPack(-1); + break; + case WIDX_MOVE_DOWN: + ReorderSelectedAssetPack(1); + break; + case WIDX_APPLY: + Apply(); + break; + } + } + + ScreenSize OnScrollGetSize(int32_t scrollIndex) override + { + ScreenSize result; + auto assetPackManager = GetContext()->GetAssetPackManager(); + if (assetPackManager != nullptr) + { + auto numAssetPacks = assetPackManager->GetCount(); + result.height = static_cast(numAssetPacks * SCROLLABLE_ROW_HEIGHT); + } + + if (_highlightedIndex) + { + _highlightedIndex = {}; + Invalidate(); + } + + return result; + } + + void OnScrollMouseDown(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override + { + const auto index = screenCoords.y / SCROLLABLE_ROW_HEIGHT; + if (index < 0 || static_cast(index) >= GetNumAssetPacks()) + return; + + if (_selectedIndex != index) + { + _selectedIndex = index; + Invalidate(); + } + } + + void OnScrollMouseOver(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override + { + const auto index = screenCoords.y / SCROLLABLE_ROW_HEIGHT; + if (index < 0 || static_cast(index) >= GetNumAssetPacks()) + return; + + if (_highlightedIndex != index) + { + _highlightedIndex = index; + Invalidate(); + } + } + + void OnPrepareDraw() override + { + widgets[WIDX_BACKGROUND].right = width - 1; + widgets[WIDX_BACKGROUND].bottom = height - 1; + widgets[WIDX_TITLE].right = width - 2; + widgets[WIDX_CLOSE].left = width - 13; + widgets[WIDX_CLOSE].right = width - 3; + + widgets[WIDX_LIST].left = 6; + widgets[WIDX_LIST].top = 20; + widgets[WIDX_LIST].right = width - 2 - 24 - 1; + widgets[WIDX_LIST].bottom = height - 6; + + auto toolstripY = 20; + auto toolstripRight = width - 2; + auto toolstripLeft = toolstripRight - 24; + auto disabled = !_selectedIndex.has_value(); + for (WidgetIndex i = WIDX_TOGGLE; i <= WIDX_APPLY; i++) + { + SetWidgetDisabled(i, disabled); + widgets[i].top = toolstripY; + widgets[i].bottom = toolstripY + 24; + widgets[i].left = toolstripLeft; + widgets[i].right = toolstripRight; + toolstripY += 24; + } + + auto isEnabled = IsSelectedAssetPackEnabled(); + widgets[WIDX_TOGGLE].image = isEnabled ? SPR_OPEN : SPR_CLOSED; + SetWidgetPressed(WIDX_TOGGLE, isEnabled); + + SetWidgetDisabled(WIDX_APPLY, false); + widgets[WIDX_APPLY].bottom = widgets[WIDX_LIST].bottom; + widgets[WIDX_APPLY].top = widgets[WIDX_APPLY].bottom - 24; + } + + void OnDraw(rct_drawpixelinfo& dpi) override + { + DrawWidgets(dpi); + } + + void OnScrollDraw(int32_t scrollIndex, rct_drawpixelinfo& dpi) override + { + auto dpiCoords = ScreenCoordsXY{ dpi.x, dpi.y }; + gfx_fill_rect( + &dpi, { dpiCoords, dpiCoords + ScreenCoordsXY{ dpi.width - 1, dpi.height - 1 } }, ColourMapA[colours[1]].mid_light); + + auto listWidth = dpi.width - 1; + auto y = 0; + + auto assetPackManager = GetContext()->GetAssetPackManager(); + if (assetPackManager == nullptr) + return; + + auto numAssetPacks = assetPackManager->GetCount(); + for (size_t i = 0; i < numAssetPacks; i++) + { + if (y > dpi.y + dpi.height) + break; + if (y + 11 < dpi.y) + continue; + + auto assetPack = assetPackManager->GetAssetPack(i); + if (assetPack != nullptr) + { + auto stringId = STR_BLACK_STRING; + auto fillRectangle = ScreenRect{ { 0, y }, { listWidth, y + SCROLLABLE_ROW_HEIGHT - 1 } }; + if (i == _selectedIndex) + { + gfx_fill_rect(&dpi, fillRectangle, ColourMapA[colours[1]].mid_dark); + stringId = STR_WINDOW_COLOUR_2_STRINGID; + } + else if (i == _highlightedIndex) + { + gfx_fill_rect(&dpi, fillRectangle, ColourMapA[colours[1]].mid_dark); + } + + auto ft = Formatter(); + ft.Add(assetPack->IsEnabled() ? STR_TOGGLE_OPTION_CHECKED : STR_TOGGLE_OPTION); + ft.Add(STR_STRING); + ft.Add(assetPack->Name.c_str()); + DrawTextEllipsised(&dpi, { 0, y }, listWidth, stringId, ft); + } + + y += SCROLLABLE_ROW_HEIGHT; + } + } + +private: + size_t GetNumAssetPacks() const + { + auto assetPackManager = GetContext()->GetAssetPackManager(); + if (assetPackManager == nullptr) + return 0; + return assetPackManager->GetCount(); + } + + bool IsSelectedAssetPackEnabled() const + { + if (_selectedIndex) + { + auto assetPackManager = GetContext()->GetAssetPackManager(); + if (assetPackManager != nullptr) + { + auto assetPack = assetPackManager->GetAssetPack(*_selectedIndex); + if (assetPack != nullptr) + { + return assetPack->IsEnabled(); + } + } + } + return false; + } + + void ToggleSelectedAssetPack() + { + if (_selectedIndex) + { + auto assetPackManager = GetContext()->GetAssetPackManager(); + if (assetPackManager != nullptr) + { + auto assetPack = assetPackManager->GetAssetPack(*_selectedIndex); + if (assetPack != nullptr) + { + assetPack->SetEnabled(!assetPack->IsEnabled()); + Invalidate(); + } + } + } + } + + void ReorderSelectedAssetPack(int32_t direction) + { + if (!_selectedIndex) + return; + + auto assetPackManager = GetContext()->GetAssetPackManager(); + if (assetPackManager == nullptr) + return; + + if (direction < 0 && *_selectedIndex > 0) + { + assetPackManager->Swap(*_selectedIndex, *_selectedIndex - 1); + (*_selectedIndex)--; + Invalidate(); + } + else if (*_selectedIndex < assetPackManager->GetCount() - 1) + { + assetPackManager->Swap(*_selectedIndex, *_selectedIndex + 1); + (*_selectedIndex)++; + Invalidate(); + } + } + + void Apply() + { + auto& objectManager = GetContext()->GetObjectManager(); + objectManager.ResetObjects(); + } +}; + +rct_window* WindowAssetPacksOpen() +{ + auto flags = WF_AUTO_POSITION | WF_CENTRE_SCREEN; + return WindowFocusOrCreate(WindowClass::AssetPacks, WW, WH, flags); +} diff --git a/src/openrct2-ui/windows/Options.cpp b/src/openrct2-ui/windows/Options.cpp index cf748f911c..255fdbbe5c 100644 --- a/src/openrct2-ui/windows/Options.cpp +++ b/src/openrct2-ui/windows/Options.cpp @@ -210,6 +210,7 @@ enum WindowOptionsWidgetIdx { WIDX_PATH_TO_RCT1_TEXT, WIDX_PATH_TO_RCT1_BUTTON, WIDX_PATH_TO_RCT1_CLEAR, + WIDX_ASSET_PACKS, }; static constexpr const StringId WINDOW_TITLE = STR_OPTIONS_TITLE; @@ -395,6 +396,7 @@ static rct_widget window_options_advanced_widgets[] = { MakeWidget ({ 23, 169}, {276, 12}, WindowWidgetType::Label, WindowColour::Secondary, STR_PATH_TO_RCT1, STR_PATH_TO_RCT1_TIP ), // RCT 1 path text MakeWidget ({ 24, 184}, {266, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_NONE, STR_STRING_TOOLTIP ), // RCT 1 path button MakeWidget ({289, 184}, { 11, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_CLOSE_X, STR_PATH_TO_RCT1_CLEAR_TIP ), // RCT 1 path clear button + MakeWidget ({ 24, 200}, {140, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_ASSET_PACKS, STR_NONE ), // Asset packs WIDGETS_END, }; @@ -1940,6 +1942,9 @@ private: } Invalidate(); break; + case WIDX_ASSET_PACKS: + context_open_window(WindowClass::AssetPacks); + break; } } diff --git a/src/openrct2-ui/windows/Themes.cpp b/src/openrct2-ui/windows/Themes.cpp index 01ee5778a5..a3ae54fb12 100644 --- a/src/openrct2-ui/windows/Themes.cpp +++ b/src/openrct2-ui/windows/Themes.cpp @@ -200,6 +200,7 @@ static WindowClass window_themes_tab_6_classes[] = { WindowClass::Options, WindowClass::KeyboardShortcutList, WindowClass::ChangeKeyboardShortcut, + WindowClass::AssetPacks, WindowClass::Loadsave, WindowClass::About, WindowClass::Changelog, diff --git a/src/openrct2-ui/windows/Window.h b/src/openrct2-ui/windows/Window.h index 91cec013cc..068afc7bd3 100644 --- a/src/openrct2-ui/windows/Window.h +++ b/src/openrct2-ui/windows/Window.h @@ -74,6 +74,7 @@ rct_window* WindowViewportOpen(); rct_window* WindowWaterOpen(); rct_window* WindowViewClippingOpen(); rct_window* WindowTransparencyOpen(); +rct_window* WindowAssetPacksOpen(); // WC_FINANCES rct_window* WindowFinancesOpen(); diff --git a/src/openrct2/AssetPack.cpp b/src/openrct2/AssetPack.cpp new file mode 100644 index 0000000000..ae74776a73 --- /dev/null +++ b/src/openrct2/AssetPack.cpp @@ -0,0 +1,189 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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 "AssetPack.h" + +#include "Context.h" +#include "core/Json.hpp" +#include "core/Path.hpp" +#include "core/Zip.h" +#include "drawing/Image.h" +#include "localisation/LocalisationService.h" +#include "object/Object.h" + +#include + +using namespace OpenRCT2; + +constexpr std::string_view ManifestFileName = "manifest.json"; + +AssetPack::AssetPack(const fs::path& path) + : Path(path) +{ +} + +AssetPack::~AssetPack() +{ +} + +bool AssetPack::IsEnabled() const +{ + return _enabled; +} + +void AssetPack::SetEnabled(bool value) +{ + _enabled = value; +} + +void AssetPack::Fetch() +{ + auto archive = Zip::Open(Path.u8string(), ZIP_ACCESS::READ); + if (!archive->Exists(ManifestFileName)) + { + throw std::runtime_error("Manifest does not exist."); + } + + auto manifestJson = archive->GetFileData(ManifestFileName); + auto jManifest = Json::FromVector(manifestJson); + Id = jManifest["id"].get(); + Version = jManifest["version"].get(); + + // TODO use a better string table class that can be used for objects, park files and asset packs + auto& localisationService = GetContext()->GetLocalisationService(); + auto locale = std::string(localisationService.GetCurrentLanguageLocale()); + Name = GetString(jManifest, "name", locale); + Description = GetString(jManifest, "description", locale); +} + +std::string AssetPack::GetString(json_t& jManifest, const std::string& key, const std::string& locale) +{ + if (jManifest.contains("strings")) + { + auto& jStrings = jManifest["strings"]; + if (jStrings.contains(key)) + { + auto& jKey = jStrings[key]; + if (jKey.contains(locale)) + { + return jKey[locale].get(); + } + if (jKey.contains("en-GB")) + { + return jKey["en-GB"].get(); + } + if (jKey.contains("en-US")) + { + return jKey["en-US"].get(); + } + } + } + return {}; +} + +bool AssetPack::ContainsObject(std::string_view id) const +{ + auto it = std::find_if(_entries.begin(), _entries.end(), [id](const Entry& entry) { return entry.ObjectId == id; }); + return it != _entries.end(); +} + +void AssetPack::LoadSamplesForObject(std::string_view id, AudioSampleTable& objectTable) +{ + auto it = std::find_if(_entries.begin(), _entries.end(), [id](const Entry& entry) { return entry.ObjectId == id; }); + if (it != _entries.end()) + { + objectTable.LoadFrom(_sampleTable, it->TableIndex, it->TableLength); + } +} + +class AssetPackLoadContext : public IReadObjectContext +{ +private: + std::string _zipPath; + IZipArchive* _zipArchive; + +public: + AssetPackLoadContext(std::string_view zipPath, IZipArchive* zipArchive) + : _zipPath(zipPath) + , _zipArchive(zipArchive) + { + } + + virtual ~AssetPackLoadContext() override + { + } + + std::string_view GetObjectIdentifier() override + { + throw std::runtime_error("Not implemented"); + } + + IObjectRepository& GetObjectRepository() override + { + throw std::runtime_error("Not implemented"); + } + + bool ShouldLoadImages() override + { + return true; + } + + std::vector GetData(std::string_view path) override + { + return _zipArchive->GetFileData(path); + } + + ObjectAsset GetAsset(std::string_view path) override + { + return ObjectAsset(_zipPath, path); + } + + void LogVerbose(ObjectError code, const utf8* text) override + { + } + + void LogWarning(ObjectError code, const utf8* text) override + { + } + + void LogError(ObjectError code, const utf8* text) override + { + } +}; + +void AssetPack::Load() +{ + auto path = Path.u8string(); + auto archive = Zip::Open(path, ZIP_ACCESS::READ); + if (!archive->Exists(ManifestFileName)) + { + throw std::runtime_error("Manifest does not exist."); + } + + AssetPackLoadContext loadContext(path, archive.get()); + + auto manifestJson = archive->GetFileData(ManifestFileName); + auto jManifest = Json::FromVector(manifestJson); + auto& jObjects = jManifest["objects"]; + + _entries.clear(); + for (auto& jObject : jObjects) + { + Entry entry; + entry.ObjectId = jObject["id"].get(); + + if (jObject.contains("samples")) + { + entry.TableIndex = _sampleTable.GetCount(); + _sampleTable.ReadFromJson(&loadContext, jObject); + entry.TableLength = _sampleTable.GetCount() - entry.TableIndex; + } + _entries.push_back(entry); + } +} diff --git a/src/openrct2/AssetPack.h b/src/openrct2/AssetPack.h new file mode 100644 index 0000000000..38c89b28af --- /dev/null +++ b/src/openrct2/AssetPack.h @@ -0,0 +1,58 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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. + *****************************************************************************/ + +#pragma once + +#include "core/FileSystem.hpp" +#include "core/JsonFwd.hpp" +#include "object/AudioSampleTable.h" + +#include +#include +#include + +namespace OpenRCT2 +{ + class AssetPack + { + private: + struct Entry + { + std::string ObjectId; + size_t TableIndex{}; + size_t TableLength{}; + }; + + AudioSampleTable _sampleTable; + std::vector _entries; + bool _enabled = true; + + public: + fs::path Path; + std::string Id; + std::string Version; + std::string Name; + std::string Description; + + AssetPack(const fs::path& path); + AssetPack(const AssetPack&) = delete; + ~AssetPack(); + + bool IsEnabled() const; + void SetEnabled(bool value); + + void Fetch(); + void Load(); + bool ContainsObject(std::string_view id) const; + void LoadSamplesForObject(std::string_view id, AudioSampleTable& objectTable); + + private: + static std::string GetString(json_t& jManifest, const std::string& key, const std::string& locale); + }; +} // namespace OpenRCT2 diff --git a/src/openrct2/AssetPackManager.cpp b/src/openrct2/AssetPackManager.cpp new file mode 100644 index 0000000000..c3eac93282 --- /dev/null +++ b/src/openrct2/AssetPackManager.cpp @@ -0,0 +1,111 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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 "AssetPackManager.h" + +#include "AssetPack.h" +#include "Context.h" +#include "PlatformEnvironment.h" +#include "core/Console.hpp" +#include "core/FileSystem.hpp" +#include "core/String.hpp" +#include "object/AudioSampleTable.h" + +#include + +using namespace OpenRCT2; + +AssetPackManager::AssetPackManager() +{ +} + +AssetPackManager::~AssetPackManager() +{ +} + +size_t AssetPackManager::GetCount() const +{ + return _assetPacks.size(); +} + +AssetPack* AssetPackManager::GetAssetPack(size_t index) +{ + if (index >= _assetPacks.size()) + return nullptr; + return _assetPacks[index].get(); +} + +void AssetPackManager::Scan() +{ + ClearAssetPacks(); + + auto context = GetContext(); + auto env = context->GetPlatformEnvironment(); + auto assetPackDirectory = fs::u8path(env->GetDirectoryPath(DIRBASE::USER, DIRID::ASSET_PACK)); + for (const fs::directory_entry& entry : fs::recursive_directory_iterator(assetPackDirectory)) + { + if (!entry.is_directory()) + { + auto path = entry.path().u8string(); + if (String::EndsWith(path, ".parkap", true)) + { + AddAssetPack(path); + } + } + } +} + +void AssetPackManager::Reload() +{ + for (auto& assetPack : _assetPacks) + { + assetPack->Load(); + } +} + +void AssetPackManager::Swap(size_t index, size_t otherIndex) +{ + if (index < _assetPacks.size() && otherIndex < _assetPacks.size() && index != otherIndex) + { + std::swap(_assetPacks[index], _assetPacks[otherIndex]); + } +} + +void AssetPackManager::LoadSamplesForObject(std::string_view id, AudioSampleTable& objectTable) +{ + for (auto& assetPack : _assetPacks) + { + if (assetPack->IsEnabled() && assetPack->ContainsObject(id)) + { + return assetPack->LoadSamplesForObject(id, objectTable); + } + } +} + +void AssetPackManager::ClearAssetPacks() +{ + _assetPacks.clear(); +} + +void AssetPackManager::AddAssetPack(const fs::path& path) +{ + auto szPath = path.u8string(); + log_verbose("Scanning asset pack: %s", szPath.c_str()); + try + { + auto ap = std::make_unique(path); + ap->Fetch(); + _assetPacks.push_back(std::move(ap)); + } + catch (const std::exception& e) + { + auto fileName = path.filename().u8string(); + Console::Error::WriteFormat("Unable to load asset pack: %s (%s)", fileName.c_str(), e.what()); + } +} diff --git a/src/openrct2/AssetPackManager.h b/src/openrct2/AssetPackManager.h new file mode 100644 index 0000000000..0b3ef120b0 --- /dev/null +++ b/src/openrct2/AssetPackManager.h @@ -0,0 +1,47 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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. + *****************************************************************************/ + +#pragma once + +#include "core/FileSystem.hpp" +#include "drawing/ImageId.hpp" + +#include +#include + +class AudioSampleTable; + +namespace OpenRCT2 +{ + class AssetPack; + + class AssetPackManager + { + private: + std::vector> _assetPacks; + + public: + AssetPackManager(); + ~AssetPackManager(); + + size_t GetCount() const; + AssetPack* GetAssetPack(size_t index); + + void Scan(); + void Reload(); + void Swap(size_t index, size_t otherIndex); + + void LoadSamplesForObject(std::string_view id, AudioSampleTable& objectTable); + + private: + void ClearAssetPacks(); + void AddAssetPack(const fs::path& path); + }; + +} // namespace OpenRCT2 diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 25f5990dcf..0d45df3300 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -11,6 +11,7 @@ # include #endif // __EMSCRIPTEN__ +#include "AssetPackManager.h" #include "Context.h" #include "Editor.h" #include "FileClassifier.h" @@ -107,6 +108,7 @@ namespace OpenRCT2 std::unique_ptr _scenarioRepository; std::unique_ptr _replayManager; std::unique_ptr _gameStateSnapshots; + std::unique_ptr _assetPackManager; #ifdef __ENABLE_DISCORD__ std::unique_ptr _discordService; #endif @@ -264,6 +266,11 @@ namespace OpenRCT2 return _gameStateSnapshots.get(); } + AssetPackManager* GetAssetPackManager() override + { + return _assetPackManager.get(); + } + DrawingEngine GetDrawingEngineType() override { return _drawingEngineType; @@ -383,6 +390,10 @@ namespace OpenRCT2 _scenarioRepository = CreateScenarioRepository(_env); _replayManager = CreateReplayManager(); _gameStateSnapshots = CreateGameStateSnapshots(); + if (!gOpenRCT2Headless) + { + _assetPackManager = std::make_unique(); + } #ifdef __ENABLE_DISCORD__ if (!gOpenRCT2Headless) { @@ -428,6 +439,12 @@ namespace OpenRCT2 // of the object cache. _objectRepository->LoadOrConstruct(_localisationService->GetCurrentLanguage()); + if (!gOpenRCT2Headless) + { + _assetPackManager->Scan(); + _assetPackManager->Reload(); + } + // TODO Like objects, this can take a while if there are a lot of track designs // its also really something really we might want to do in the background // as its not required until the player wants to place a new ride. diff --git a/src/openrct2/Context.h b/src/openrct2/Context.h index 9b4812c8b1..5611e66962 100644 --- a/src/openrct2/Context.h +++ b/src/openrct2/Context.h @@ -81,6 +81,7 @@ class NetworkBase; namespace OpenRCT2 { + class AssetPackManager; class GameState; struct IPlatformEnvironment; @@ -136,6 +137,7 @@ namespace OpenRCT2 virtual ITrackDesignRepository* GetTrackDesignRepository() abstract; virtual IScenarioRepository* GetScenarioRepository() abstract; virtual IReplayManager* GetReplayManager() abstract; + virtual AssetPackManager* GetAssetPackManager() abstract; virtual IGameStateSnapshots* GetGameStateSnapshots() abstract; virtual DrawingEngine GetDrawingEngineType() abstract; virtual Drawing::IDrawingEngine* GetDrawingEngine() abstract; diff --git a/src/openrct2/PlatformEnvironment.cpp b/src/openrct2/PlatformEnvironment.cpp index c02da0fdee..19384b3e89 100644 --- a/src/openrct2/PlatformEnvironment.cpp +++ b/src/openrct2/PlatformEnvironment.cpp @@ -268,6 +268,7 @@ const u8string PlatformEnvironment::DirectoryNamesOpenRCT2[] = { u8"replay", // REPLAY u8"desyncs", // DESYNCS u8"crash", // CRASH + u8"assetpack", // ASSET_PACK }; const u8string PlatformEnvironment::FileNames[] = { diff --git a/src/openrct2/PlatformEnvironment.h b/src/openrct2/PlatformEnvironment.h index c76282bc6a..fbdbfbf69b 100644 --- a/src/openrct2/PlatformEnvironment.h +++ b/src/openrct2/PlatformEnvironment.h @@ -51,6 +51,7 @@ namespace OpenRCT2 REPLAY, // Contains recorded replays. LOG_DESYNCS, // Contains desync reports. CRASH, // Contains crash dumps. + ASSET_PACK, // Contains asset packs. }; enum class PATHID diff --git a/src/openrct2/core/Range.hpp b/src/openrct2/core/Range.hpp new file mode 100644 index 0000000000..9a2732894f --- /dev/null +++ b/src/openrct2/core/Range.hpp @@ -0,0 +1,109 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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. + *****************************************************************************/ + +#pragma once + +#include +#include + +template struct Range +{ + static_assert(std::is_integral(), "type must be integral"); + + class Iterator + { + friend Range; + + private: + T Lower{}; + T Upper{}; + T Value{}; + T Change{}; + + private: + Iterator(const Range& range, T initialValue) + : Lower(range.Lower) + , Upper(range.Upper) + , Value(initialValue) + , Change(range.Upper >= range.Lower ? 1 : -1) + { + } + + public: + Iterator& operator++() + { + Value += Change; + return *this; + } + + Iterator operator++(int) + { + auto result = *this; + ++(*this); + return result; + } + + bool operator==(Iterator other) const + { + return Value == other.Value; + } + + bool operator!=(Iterator other) const + { + return !(*this == other); + } + + const T& operator*() + { + return Value; + } + }; + + T Lower{}; + T Upper{}; + + Range() = default; + + Range(T single) + : Lower(single) + , Upper(single) + { + } + + Range(T lower, T upper) + : Lower(lower) + , Upper(upper) + { + } + + size_t size() const + { + return std::abs(Upper - Lower); + } + + Iterator begin() + { + return Iterator(*this, Lower); + } + + Iterator end() + { + return Iterator(*this, Upper + 1); + } + + Iterator begin() const + { + return Iterator(*this, Lower); + } + + Iterator end() const + { + return Iterator(*this, Upper + 1); + } +}; diff --git a/src/openrct2/interface/WindowClasses.h b/src/openrct2/interface/WindowClasses.h index d8b8019bf3..1a707523b2 100644 --- a/src/openrct2/interface/WindowClasses.h +++ b/src/openrct2/interface/WindowClasses.h @@ -86,6 +86,7 @@ enum class WindowClass : uint8_t ObjectLoadError = 132, PatrolArea = 133, Transparency = 134, + AssetPacks = 135, // Only used for colour schemes Staff = 220, diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 18e745f8cf..585de5ad8c 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -146,6 +146,8 @@ + + @@ -300,11 +302,13 @@ + + @@ -642,6 +646,8 @@ + + @@ -791,6 +797,7 @@ + @@ -1004,4 +1011,4 @@ - + \ No newline at end of file diff --git a/src/openrct2/localisation/LocalisationService.cpp b/src/openrct2/localisation/LocalisationService.cpp index 8f3c673dc2..3cb87cdd20 100644 --- a/src/openrct2/localisation/LocalisationService.cpp +++ b/src/openrct2/localisation/LocalisationService.cpp @@ -81,6 +81,15 @@ std::string LocalisationService::GetLanguagePath(uint32_t languageId) const return languagePath; } +std::string_view LocalisationService::GetCurrentLanguageLocale() const +{ + if (_currentLanguage >= 0 && static_cast(_currentLanguage) < std::size(LanguagesDescriptors)) + { + return LanguagesDescriptors[_currentLanguage].locale; + } + return {}; +} + void LocalisationService::OpenLanguage(int32_t id) { CloseLanguages(); diff --git a/src/openrct2/localisation/LocalisationService.h b/src/openrct2/localisation/LocalisationService.h index afd4799172..a38e017927 100644 --- a/src/openrct2/localisation/LocalisationService.h +++ b/src/openrct2/localisation/LocalisationService.h @@ -44,6 +44,7 @@ namespace OpenRCT2::Localisation { return _currentLanguage; } + std::string_view GetCurrentLanguageLocale() const; bool UseTrueTypeFont() const { return _useTrueTypeFont; diff --git a/src/openrct2/localisation/StringIds.h b/src/openrct2/localisation/StringIds.h index ba845d0589..5d84e522de 100644 --- a/src/openrct2/localisation/StringIds.h +++ b/src/openrct2/localisation/StringIds.h @@ -3907,6 +3907,8 @@ enum : uint16_t STR_SCENARIO_CATEGORY_EXTRAS_PARKS = 6519, + STR_ASSET_PACKS = 6520, + // Have to include resource strings (from scenarios and objects) for the time being now that language is partially working /* MAX_STR_COUNT = 32768 */ // MAX_STR_COUNT - upper limit for number of strings, not the current count strings }; diff --git a/src/openrct2/object/AudioObject.cpp b/src/openrct2/object/AudioObject.cpp index 6f3d807076..c4c219f541 100644 --- a/src/openrct2/object/AudioObject.cpp +++ b/src/openrct2/object/AudioObject.cpp @@ -9,6 +9,7 @@ #include "AudioObject.h" +#include "../AssetPackManager.h" #include "../Context.h" #include "../PlatformEnvironment.h" #include "../audio/AudioContext.h" @@ -20,27 +21,38 @@ using namespace OpenRCT2::Audio; void AudioObject::Load() { - _sampleTable.Load(); + // 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); + } + + _loadedSampleTable.Load(); } void AudioObject::Unload() { - _sampleTable.Unload(); + _loadedSampleTable.Unload(); } void AudioObject::ReadJson(IReadObjectContext* context, json_t& root) { - Guard::Assert(root.is_object(), "BannerObject::ReadJson expects parameter root to be object"); + Guard::Assert(root.is_object(), "AudioObject::ReadJson expects parameter root to be object"); _sampleTable.ReadFromJson(context, root); PopulateTablesFromJson(context, root); } IAudioSource* AudioObject::GetSample(uint32_t index) const { - return _sampleTable.GetSample(index); + return _loadedSampleTable.GetSample(index); } int32_t AudioObject::GetSampleModifier(uint32_t index) const { - return _sampleTable.GetSampleModifier(index); + return _loadedSampleTable.GetSampleModifier(index); } diff --git a/src/openrct2/object/AudioObject.h b/src/openrct2/object/AudioObject.h index 4b6e2caa0f..422a6ed78c 100644 --- a/src/openrct2/object/AudioObject.h +++ b/src/openrct2/object/AudioObject.h @@ -20,6 +20,7 @@ class AudioObject final : public Object { private: AudioSampleTable _sampleTable; + AudioSampleTable _loadedSampleTable; public: void ReadJson(IReadObjectContext* context, json_t& root) override; diff --git a/src/openrct2/object/AudioSampleTable.cpp b/src/openrct2/object/AudioSampleTable.cpp index 3f238543a8..2b80f8c448 100644 --- a/src/openrct2/object/AudioSampleTable.cpp +++ b/src/openrct2/object/AudioSampleTable.cpp @@ -15,89 +15,14 @@ #include "../core/File.h" #include "../core/Json.hpp" #include "../core/Path.hpp" +#include "Object.h" using namespace OpenRCT2; using namespace OpenRCT2::Audio; -struct SourceInfo +std::vector& AudioSampleTable::GetEntries() { - std::string Path; - std::vector Range{}; -}; - -static std::vector ParseRange(std::string_view s) -{ - // Currently only supports [###] or [###..###] - std::vector result = {}; - if (s.length() >= 3 && s[0] == '[' && s[s.length() - 1] == ']') - { - s = s.substr(1, s.length() - 2); - auto parts = String::Split(s, ".."); - if (parts.size() == 1) - { - result.push_back(std::stoi(parts[0])); - } - else - { - auto left = std::stoi(parts[0]); - auto right = std::stoi(parts[1]); - if (left <= right) - { - for (auto i = left; i <= right; i++) - { - result.push_back(i); - } - } - else - { - for (auto i = right; i >= left; i--) - { - result.push_back(i); - } - } - } - } - return result; -} - -static SourceInfo ParseSource(std::string_view source) -{ - SourceInfo info; - if (String::StartsWith(source, "$RCT1:DATA/")) - { - auto name = source.substr(11); - auto rangeStart = name.find('['); - if (rangeStart != std::string::npos) - { - info.Range = ParseRange(name.substr(rangeStart)); - name = name.substr(0, rangeStart); - } - - auto env = GetContext()->GetPlatformEnvironment(); - info.Path = env->FindFile(DIRBASE::RCT1, DIRID::DATA, name); - } - else if (String::StartsWith(source, "$RCT2:DATA/")) - { - auto name = source.substr(11); - auto rangeStart = name.find('['); - if (rangeStart != std::string::npos) - { - info.Range = ParseRange(name.substr(rangeStart)); - name = name.substr(0, rangeStart); - } - - auto env = GetContext()->GetPlatformEnvironment(); - info.Path = env->FindFile(DIRBASE::RCT2, DIRID::DATA, name); - } - else if (String::StartsWith(source, "$[")) - { - info.Range = ParseRange(source.substr(1)); - } - else - { - info.Path = source; - } - return info; + return _entries; } void AudioSampleTable::ReadFromJson(IReadObjectContext* context, const json_t& root) @@ -123,7 +48,7 @@ void AudioSampleTable::ReadFromJson(IReadObjectContext* context, const json_t& r } auto asset = context->GetAsset(sourceInfo.Path); - if (sourceInfo.Range.empty()) + if (!sourceInfo.SourceRange) { auto& entry = _entries.emplace_back(); entry.Asset = asset; @@ -131,7 +56,8 @@ void AudioSampleTable::ReadFromJson(IReadObjectContext* context, const json_t& r } else { - for (auto index : sourceInfo.Range) + Range r(1, 5); + for (auto index : *sourceInfo.SourceRange) { auto& entry = _entries.emplace_back(); entry.Asset = asset; @@ -143,38 +69,31 @@ void AudioSampleTable::ReadFromJson(IReadObjectContext* context, const json_t& r } } -void AudioSampleTable::LoadFrom(const AudioSampleTable& table, size_t index, size_t length) +void AudioSampleTable::LoadFrom(const AudioSampleTable& table, size_t sourceStartIndex, size_t length) { - auto audioContext = GetContext()->GetAudioContext(); - auto numEntries = std::min(_entries.size(), length); - for (size_t i = 0; i < numEntries; i++) + // Ensure we stay in bounds of source table + if (sourceStartIndex >= table._entries.size()) + return; + length = std::min(length, table._entries.size() - sourceStartIndex); + + // Asset packs may allocate more images for an object that original, or original object may + // not allocate any images at all. + if (_entries.size() < length) { - auto& entry = _entries[i]; - if (entry.Source != nullptr) - { - continue; - } + _entries.resize(length); + } - auto sourceIndex = index + i; - if (sourceIndex >= table._entries.size()) - { - continue; - } - - const auto& sourceEntry = table._entries[sourceIndex]; + for (size_t i = 0; i < length; i++) + { + const auto& sourceEntry = table._entries[sourceStartIndex + i]; if (sourceEntry.Asset) { auto stream = sourceEntry.Asset->GetStream(); if (stream != nullptr) { - if (sourceEntry.PathIndex) - { - entry.Source = audioContext->CreateStreamFromCSS(std::move(stream), *sourceEntry.PathIndex); - } - else - { - entry.Source = audioContext->CreateStreamFromWAV(std::move(stream)); - } + auto& entry = _entries[i]; + entry.Asset = sourceEntry.Asset; + entry.PathIndex = sourceEntry.PathIndex; entry.Modifier = sourceEntry.Modifier; } } @@ -183,7 +102,15 @@ void AudioSampleTable::LoadFrom(const AudioSampleTable& table, size_t index, siz void AudioSampleTable::Load() { - LoadFrom(*this, 0, _entries.size()); + auto audioContext = GetContext()->GetAudioContext(); + for (size_t i = 0; i < _entries.size(); i++) + { + auto& entry = _entries[i]; + if (entry.Source == nullptr) + { + entry.Source = LoadSample(static_cast(i)); + } + } } void AudioSampleTable::Unload() @@ -212,6 +139,32 @@ IAudioSource* AudioSampleTable::GetSample(uint32_t index) const return nullptr; } +IAudioSource* AudioSampleTable::LoadSample(uint32_t index) const +{ + IAudioSource* result{}; + if (index < _entries.size()) + { + auto& entry = _entries[index]; + if (entry.Asset) + { + auto stream = entry.Asset->GetStream(); + if (stream != nullptr) + { + auto audioContext = GetContext()->GetAudioContext(); + if (entry.PathIndex) + { + result = audioContext->CreateStreamFromCSS(std::move(stream), *entry.PathIndex); + } + else + { + result = audioContext->CreateStreamFromWAV(std::move(stream)); + } + } + } + } + return result; +} + int32_t AudioSampleTable::GetSampleModifier(uint32_t index) const { if (index < _entries.size()) diff --git a/src/openrct2/object/AudioSampleTable.h b/src/openrct2/object/AudioSampleTable.h index e80f626d4f..611eb838b6 100644 --- a/src/openrct2/object/AudioSampleTable.h +++ b/src/openrct2/object/AudioSampleTable.h @@ -12,12 +12,14 @@ #include "../audio/AudioSource.h" #include "../core/JsonFwd.hpp" #include "Object.h" +#include "ObjectAsset.h" +#include "ResourceTable.h" #include struct IReadObjectContext; -class AudioSampleTable +class AudioSampleTable : public ResourceTable { private: struct Entry @@ -31,6 +33,8 @@ private: std::vector _entries; public: + std::vector& GetEntries(); + /** * Read the entries from the given JSON into the table, but do not load anything. */ @@ -39,7 +43,7 @@ public: /** * Load all available entries from the given table. */ - void LoadFrom(const AudioSampleTable& table, size_t index, size_t length); + void LoadFrom(const AudioSampleTable& table, size_t sourceIndex, size_t length); /** * Load all available entries. @@ -53,5 +57,6 @@ public: size_t GetCount() const; OpenRCT2::Audio::IAudioSource* GetSample(uint32_t index) const; + OpenRCT2::Audio::IAudioSource* LoadSample(uint32_t index) const; int32_t GetSampleModifier(uint32_t index) const; }; diff --git a/src/openrct2/object/MusicObject.cpp b/src/openrct2/object/MusicObject.cpp index 59b29cc87e..eec2442ec5 100644 --- a/src/openrct2/object/MusicObject.cpp +++ b/src/openrct2/object/MusicObject.cpp @@ -9,6 +9,7 @@ #include "MusicObject.h" +#include "../AssetPackManager.h" #include "../Context.h" #include "../OpenRCT2.h" #include "../PlatformEnvironment.h" @@ -24,6 +25,7 @@ #include using namespace OpenRCT2; +using namespace OpenRCT2::Audio; constexpr size_t DEFAULT_BYTES_PER_TICK = 1378; @@ -32,6 +34,18 @@ void MusicObject::Load() GetStringTable().Sort(); NameStringId = language_allocate_object_string(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) { @@ -129,6 +143,7 @@ void MusicObject::ParseRideTypes(const json_t& jRideTypes) void MusicObject::ParseTracks(IReadObjectContext& context, json_t& jTracks) { + auto& entries = _sampleTable.GetEntries(); for (auto& jTrack : jTracks) { if (jTrack.is_object()) @@ -143,7 +158,12 @@ void MusicObject::ParseTracks(IReadObjectContext& context, json_t& jTracks) } else { - track.Asset = GetAsset(context, source); + auto asset = GetAsset(context, source); + + auto& entry = entries.emplace_back(); + entry.Asset = asset; + + track.Asset = asset; _tracks.push_back(std::move(track)); } } @@ -181,6 +201,11 @@ const MusicObjectTrack* MusicObject::GetTrack(size_t trackIndex) const 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) diff --git a/src/openrct2/object/MusicObject.h b/src/openrct2/object/MusicObject.h index 92f123d9d2..9aaf0cd620 100644 --- a/src/openrct2/object/MusicObject.h +++ b/src/openrct2/object/MusicObject.h @@ -9,6 +9,7 @@ #pragma once +#include "AudioSampleTable.h" #include "Object.h" #include @@ -46,6 +47,8 @@ private: std::vector _tracks; std::optional _originalStyleId; MusicNiceFactor _niceFactor; + AudioSampleTable _sampleTable; + AudioSampleTable _loadedSampleTable; public: StringId NameStringId{}; @@ -60,6 +63,7 @@ public: bool SupportsRideType(ride_type_t rideType); size_t GetTrackCount() const; const MusicObjectTrack* GetTrack(size_t trackIndex) const; + OpenRCT2::Audio::IAudioSource* GetTrackSample(size_t trackIndex) const; constexpr MusicNiceFactor GetNiceFactor() const { return _niceFactor; diff --git a/src/openrct2/object/Object.h b/src/openrct2/object/Object.h index 80c7efd5b2..ca0457c900 100644 --- a/src/openrct2/object/Object.h +++ b/src/openrct2/object/Object.h @@ -14,6 +14,7 @@ #include "../core/String.hpp" #include "../util/Util.h" #include "ImageTable.h" +#include "ObjectAsset.h" #include "StringTable.h" #include @@ -225,30 +226,6 @@ enum class ObjectError : uint32_t UnexpectedEOF, }; -class ObjectAsset -{ -private: - std::string _zipPath; - std::string _path; - -public: - ObjectAsset() = default; - ObjectAsset(std::string_view path) - : _path(path) - { - } - ObjectAsset(std::string_view zipPath, std::string_view path) - : _zipPath(zipPath) - , _path(path) - { - } - - [[nodiscard]] bool IsAvailable() const; - [[nodiscard]] uint64_t GetSize() const; - [[nodiscard]] std::vector GetData() const; - [[nodiscard]] std::unique_ptr GetStream() const; -}; - struct IReadObjectContext { virtual ~IReadObjectContext() = default; diff --git a/src/openrct2/object/ObjectAsset.h b/src/openrct2/object/ObjectAsset.h new file mode 100644 index 0000000000..dcbfab67dd --- /dev/null +++ b/src/openrct2/object/ObjectAsset.h @@ -0,0 +1,57 @@ +/***************************************************************************** + * Copyright (c) 2014-2021 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. + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +namespace OpenRCT2 +{ + struct IStream; +} + +class ObjectAsset +{ +private: + std::string _zipPath; + std::string _path; + +public: + ObjectAsset() = default; + ObjectAsset(std::string_view path) + : _path(path) + { + } + ObjectAsset(std::string_view zipPath, std::string_view path) + : _zipPath(zipPath) + , _path(path) + { + } + + [[nodiscard]] bool IsAvailable() const; + [[nodiscard]] uint64_t GetSize() const; + [[nodiscard]] std::vector GetData() const; + [[nodiscard]] std::unique_ptr GetStream() const; + const std::string& GetZipPath() const; + const std::string& GetPath() const; + size_t GetHash() const; + + friend bool operator==(const ObjectAsset& l, const ObjectAsset& r); +}; + +template<> struct std::hash +{ + std::size_t operator()(const ObjectAsset& asset) const noexcept + { + return asset.GetHash(); + } +}; diff --git a/src/openrct2/object/ObjectManager.cpp b/src/openrct2/object/ObjectManager.cpp index 51fcc2d5e9..959810e081 100644 --- a/src/openrct2/object/ObjectManager.cpp +++ b/src/openrct2/object/ObjectManager.cpp @@ -16,6 +16,7 @@ #include "../core/Memory.hpp" #include "../localisation/StringIds.h" #include "../ride/Ride.h" +#include "../ride/RideAudio.h" #include "../util/Util.h" #include "FootpathItemObject.h" #include "LargeSceneryObject.h" @@ -243,6 +244,7 @@ public: // We will need to replay the title music if the title music object got reloaded OpenRCT2::Audio::StopTitleMusic(); OpenRCT2::Audio::PlayTitleMusic(); + OpenRCT2::RideAudio::StopAllChannels(); } std::vector GetPackableObjects() override diff --git a/src/openrct2/object/ResourceTable.cpp b/src/openrct2/object/ResourceTable.cpp new file mode 100644 index 0000000000..92f6307836 --- /dev/null +++ b/src/openrct2/object/ResourceTable.cpp @@ -0,0 +1,116 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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 "ResourceTable.h" + +#include "../Context.h" +#include "../PlatformEnvironment.h" +#include "../core/Path.hpp" +#include "../core/String.hpp" + +using namespace OpenRCT2; + +Range ResourceTable::ParseRange(std::string_view s) +{ + // Currently only supports [###] or [###..###] + Range result = {}; + if (s.length() >= 3 && s[0] == '[' && s[s.length() - 1] == ']') + { + s = s.substr(1, s.length() - 2); + auto parts = String::Split(s, ".."); + if (parts.size() == 1) + { + result = Range(std::stoi(parts[0])); + } + else + { + auto left = std::stoi(parts[0]); + auto right = std::stoi(parts[1]); + if (left <= right) + { + result = Range(left, right); + } + else + { + result = Range(right, left); + } + } + } + return result; +} + +ResourceTable::SourceInfo ResourceTable::ParseSource(std::string_view source) +{ + SourceInfo info; + auto base = source; + auto rangeStart = source.find('['); + if (rangeStart != std::string::npos) + { + base = source.substr(0, rangeStart); + info.SourceRange = ParseRange(source.substr(rangeStart)); + } + + auto fileName = base; + auto fileNameStart = base.find('/'); + if (fileNameStart != std::string::npos) + { + fileName = base.substr(fileNameStart + 1); + } + else + { + fileNameStart = base.find(':'); + if (fileNameStart != std::string::npos) + { + fileName = base.substr(fileNameStart + 1); + } + } + + if (String::StartsWith(base, "$LGX:")) + { + info.Kind = SourceKind::Gx; + info.Path = fileName; + } + else if (String::StartsWith(base, "$G1")) + { + auto env = GetContext()->GetPlatformEnvironment(); + auto dataPath = env->GetDirectoryPath(DIRBASE::RCT2, DIRID::DATA); + info.Kind = SourceKind::G1; + // info.Path = env->FindFile(DIRBASE::RCT2, DIRID::DATA, "g1.dat"); + } + else if (String::StartsWith(base, "$CSG")) + { + auto env = GetContext()->GetPlatformEnvironment(); + auto dataPath = env->GetDirectoryPath(DIRBASE::RCT2, DIRID::DATA); + info.Kind = SourceKind::Csg; + // info.Path = env->FindFile(DIRBASE::RCT2, DIRID::DATA, "g1.dat"); + } + else if (String::StartsWith(base, "$RCT1:DATA/")) + { + auto env = GetContext()->GetPlatformEnvironment(); + info.Kind = SourceKind::Data; + info.Path = env->FindFile(DIRBASE::RCT1, DIRID::DATA, fileName); + } + else if (String::StartsWith(base, "$RCT2:DATA/")) + { + auto env = GetContext()->GetPlatformEnvironment(); + info.Kind = SourceKind::Data; + info.Path = env->FindFile(DIRBASE::RCT2, DIRID::DATA, fileName); + } + else if (String::StartsWith(base, "$RCT2:OBJDATA/")) + { + auto env = GetContext()->GetPlatformEnvironment(); + info.Kind = SourceKind::ObjData; + info.Path = env->FindFile(DIRBASE::RCT2, DIRID::OBJECT, fileName); + } + else if (!String::StartsWith(base, "$")) + { + info.Path = base; + } + return info; +} diff --git a/src/openrct2/object/ResourceTable.h b/src/openrct2/object/ResourceTable.h new file mode 100644 index 0000000000..f22a85cd4e --- /dev/null +++ b/src/openrct2/object/ResourceTable.h @@ -0,0 +1,41 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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. + *****************************************************************************/ + +#pragma once + +#include "../core/Range.hpp" + +#include +#include +#include + +class ResourceTable +{ +protected: + enum class SourceKind + { + None, + Data, + ObjData, + Gx, + G1, + Csg, + Png, + }; + + struct SourceInfo + { + SourceKind Kind{}; + std::string Path; + std::optional> SourceRange; + }; + + static Range ParseRange(std::string_view s); + static SourceInfo ParseSource(std::string_view source); +}; diff --git a/src/openrct2/ride/RideAudio.cpp b/src/openrct2/ride/RideAudio.cpp index 0c5bc302c7..cf6630f1da 100644 --- a/src/openrct2/ride/RideAudio.cpp +++ b/src/openrct2/ride/RideAudio.cpp @@ -179,23 +179,14 @@ namespace OpenRCT2::RideAudio auto musicObj = static_cast(objManager.GetLoadedObject(ObjectType::Music, ride->music)); if (musicObj != nullptr) { - auto track = musicObj->GetTrack(instance.TrackIndex); - if (track != nullptr) + auto shouldLoop = musicObj->GetTrackCount() == 1; + auto source = musicObj->GetTrackSample(instance.TrackIndex); + if (source != nullptr) { - auto stream = track->Asset.GetStream(); - if (stream != nullptr) + auto channel = CreateAudioChannel(source, MixerGroup::RideMusic, shouldLoop, 0); + if (channel != nullptr) { - auto audioContext = GetContext()->GetAudioContext(); - auto source = audioContext->CreateStreamFromWAV(std::move(stream)); - if (source != nullptr) - { - auto shouldLoop = musicObj->GetTrackCount() == 1; - auto channel = CreateAudioChannel(source, MixerGroup::RideMusic, shouldLoop, 0); - if (channel != nullptr) - { - _musicChannels.emplace_back(instance, channel, source); - } - } + _musicChannels.emplace_back(instance, channel, source); } } } diff --git a/src/openrct2/scripting/bindings/game/ScConfiguration.hpp b/src/openrct2/scripting/bindings/game/ScConfiguration.hpp index 305f810fca..b58278220d 100644 --- a/src/openrct2/scripting/bindings/game/ScConfiguration.hpp +++ b/src/openrct2/scripting/bindings/game/ScConfiguration.hpp @@ -199,13 +199,8 @@ namespace OpenRCT2::Scripting if (key == "general.language") { auto& localisationService = GetContext()->GetLocalisationService(); - auto language = localisationService.GetCurrentLanguage(); - auto locale = ""; - if (language >= 0 && static_cast(language) < std::size(LanguagesDescriptors)) - { - locale = LanguagesDescriptors[language].locale; - } - duk_push_string(ctx, locale); + auto locale = localisationService.GetCurrentLanguageLocale(); + duk_push_lstring(ctx, locale.data(), locale.size()); return DukValue::take_from_stack(ctx); } if (key == "general.showFps") diff --git a/src/openrct2/sprites.h b/src/openrct2/sprites.h index 2ef579c3e0..010c3a7a33 100644 --- a/src/openrct2/sprites.h +++ b/src/openrct2/sprites.h @@ -900,7 +900,10 @@ enum SPR_G2_SIDEWAYS_TAB = SPR_G2_BEGIN + 150, SPR_G2_SIDEWAYS_TAB_ACTIVE = SPR_G2_BEGIN + 151, - SPR_G2_CHAR_BEGIN = SPR_G2_BEGIN + 152, + SPR_G2_ARROW_UP = SPR_G2_BEGIN + 152, + SPR_G2_ARROW_DOWN = SPR_G2_BEGIN + 153, + + SPR_G2_CHAR_BEGIN = SPR_G2_BEGIN + 154, SPR_G2_AE_UPPER = SPR_G2_CHAR_BEGIN, SPR_G2_AE_LOWER = SPR_G2_CHAR_BEGIN + 1,