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,