diff --git a/distribution/changelog.txt b/distribution/changelog.txt
index b198b4b220..97a4303cdf 100644
--- a/distribution/changelog.txt
+++ b/distribution/changelog.txt
@@ -1,6 +1,7 @@
0.3.0+ (in development)
------------------------------------------------------------------------
- Feature: [#10807] Add 2x and 4x zoom levels (currently limited to OpenGL).
+- Feature: [#12703] Add scenario plugin APIs.
- Feature: [#12708] Add plugin-accessible names to all game actions.
- Feature: [#12712] Add TCP / socket plugin APIs.
- Feature: [#12840] Add Park.entranceFee to the plugin API.
diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts
index ad25517878..88274c024e 100644
--- a/distribution/openrct2.d.ts
+++ b/distribution/openrct2.d.ts
@@ -35,6 +35,8 @@ declare global {
var network: Network;
/** APIs for the park and management of it. */
var park: Park;
+ /** APIs for the current scenario. */
+ var scenario: Scenario;
/**
* APIs for controlling the user interface.
* These will only be available to servers and clients that are not running headless mode.
@@ -1470,6 +1472,109 @@ declare global {
postMessage(message: ParkMessageDesc): void;
}
+ type ScenarioObjectiveType =
+ "none" |
+ "guestsBy" |
+ "parkValueBy" |
+ "haveFun" |
+ "buildTheBest" |
+ "10Rollercoasters" |
+ "guestsAndRating" |
+ "monthlyRideIncome" |
+ "10RollercoastersLength" |
+ "finish5Rollercoasters" |
+ "replayLoanAndParkValue" |
+ "monthlyFoodIncome";
+
+ interface ScenarioObjective {
+ /**
+ * The objective type.
+ */
+ type: ScenarioObjective;
+
+ /**
+ * The required number of guests.
+ */
+ guests: number;
+
+ /**
+ * The year the objective must be completed by the end of.
+ */
+ year: number;
+
+ /**
+ * The minimum length required for each rollercoaster.
+ */
+ length: number;
+
+ /**
+ * The minimum excitement rating required for each rollercoaster.
+ */
+ excitement: number;
+
+ /**
+ * The minimum park value required.
+ */
+ parkValue: number;
+
+ /**
+ * The minimum monthly income from rides / food.
+ */
+ monthlyIncome: number;
+ }
+
+ type ScenarioStatus = "inProgress" | "completed" | "failed";
+
+ interface Scenario {
+ /**
+ * The name of the scenario. This is not necessarily the name of the park.
+ */
+ name: string;
+
+ /**
+ * The description of the scenario, shown above the scenario objective.
+ */
+ details: string;
+
+ /**
+ * The entered player name if the scenario is complete.
+ */
+ completedBy: string;
+
+ /**
+ * The filename of the scenario that is being played. Used to match the
+ * completion score with the scenario file.
+ */
+ filename: string;
+
+ /**
+ * The criteria required to complete the scenario.
+ */
+ objective: ScenarioObjective;
+
+ /**
+ * The number of consecutive days the park rating has been under the threshold for.
+ * This is reset when the park rating rises above the threshold again.
+ * Also used to post warning messages.
+ */
+ parkRatingWarningDays: number;
+
+ /**
+ * The company value when the scenario was completed.
+ */
+ completedCompanyValue?: number;
+
+ /**
+ * The current status of the scenario.
+ */
+ status: ScenarioStatus;
+
+ /**
+ * The current highest recorded company value.
+ */
+ companyValueRecord: number;
+ }
+
interface Cheats {
allowArbitraryRideTypeChanges: boolean;
allowTrackPlaceInvalidHeights: boolean;
diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj
index d079a1dc3f..dd2b045f3a 100644
--- a/src/openrct2/libopenrct2.vcxproj
+++ b/src/openrct2/libopenrct2.vcxproj
@@ -410,6 +410,7 @@
+
diff --git a/src/openrct2/scripting/ScScenario.hpp b/src/openrct2/scripting/ScScenario.hpp
new file mode 100644
index 0000000000..33e1998abb
--- /dev/null
+++ b/src/openrct2/scripting/ScScenario.hpp
@@ -0,0 +1,310 @@
+/*****************************************************************************
+ * Copyright (c) 2014-2020 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
+
+#ifdef ENABLE_SCRIPTING
+
+# include "../Context.h"
+# include "../GameState.h"
+# include "../common.h"
+# include "../core/String.hpp"
+# include "../scenario/Scenario.h"
+# include "../world/Park.h"
+# include "Duktape.hpp"
+# include "ScriptEngine.h"
+
+# include
+
+namespace OpenRCT2::Scripting
+{
+ static const DukEnumMap ScenarioObjectiveTypeMap({
+ { "none", OBJECTIVE_NONE },
+ { "guestsBy", OBJECTIVE_GUESTS_BY },
+ { "parkValueBy", OBJECTIVE_PARK_VALUE_BY },
+ { "haveFun", OBJECTIVE_HAVE_FUN },
+ { "buildTheBest", OBJECTIVE_BUILD_THE_BEST },
+ { "10Rollercoasters", OBJECTIVE_10_ROLLERCOASTERS },
+ { "guestsAndRating", OBJECTIVE_GUESTS_AND_RATING },
+ { "monthlyRideIncome", OBJECTIVE_MONTHLY_RIDE_INCOME },
+ { "10RollercoastersLength", OBJECTIVE_10_ROLLERCOASTERS_LENGTH },
+ { "finish5Rollercoasters", OBJECTIVE_FINISH_5_ROLLERCOASTERS },
+ { "replayLoanAndParkValue", OBJECTIVE_REPLAY_LOAN_AND_PARK_VALUE },
+ { "monthlyFoodIncome", OBJECTIVE_MONTHLY_FOOD_INCOME },
+ });
+
+ class ScScenarioObjective
+ {
+ private:
+ std::string type_get()
+ {
+ return std::string(ScenarioObjectiveTypeMap[gScenarioObjective.Type]);
+ }
+
+ void type_set(const std::string& value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioObjective.Type = ScenarioObjectiveTypeMap[value];
+ }
+
+ uint16_t guests_get()
+ {
+ if (gScenarioObjective.Type == OBJECTIVE_GUESTS_BY || gScenarioObjective.Type == OBJECTIVE_GUESTS_AND_RATING)
+ {
+ return gScenarioObjective.NumGuests;
+ }
+ return 0;
+ }
+
+ void guests_set(uint16_t value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (gScenarioObjective.Type == OBJECTIVE_GUESTS_BY || gScenarioObjective.Type == OBJECTIVE_GUESTS_AND_RATING)
+ {
+ gScenarioObjective.NumGuests = value;
+ }
+ }
+
+ uint8_t year_get()
+ {
+ if (gScenarioObjective.Type == OBJECTIVE_GUESTS_BY || gScenarioObjective.Type == OBJECTIVE_PARK_VALUE_BY)
+ {
+ return gScenarioObjective.Year;
+ }
+ return 0;
+ }
+
+ void year_set(uint8_t value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (gScenarioObjective.Type == OBJECTIVE_GUESTS_BY || gScenarioObjective.Type == OBJECTIVE_PARK_VALUE_BY)
+ {
+ gScenarioObjective.Year = value;
+ }
+ }
+
+ uint16_t length_get()
+ {
+ if (gScenarioObjective.Type == OBJECTIVE_10_ROLLERCOASTERS_LENGTH)
+ {
+ return gScenarioObjective.NumGuests;
+ }
+ return 0;
+ }
+
+ void length_set(uint16_t value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (gScenarioObjective.Type == OBJECTIVE_10_ROLLERCOASTERS_LENGTH)
+ {
+ gScenarioObjective.NumGuests = value;
+ }
+ }
+
+ money32 excitement_get()
+ {
+ if (gScenarioObjective.Type == OBJECTIVE_FINISH_5_ROLLERCOASTERS)
+ {
+ return gScenarioObjective.Currency;
+ }
+ return 0;
+ }
+
+ void excitement_set(money32 value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (gScenarioObjective.Type == OBJECTIVE_FINISH_5_ROLLERCOASTERS)
+ {
+ gScenarioObjective.Currency = value;
+ }
+ }
+
+ money32 parkValue_get()
+ {
+ if (gScenarioObjective.Type == OBJECTIVE_PARK_VALUE_BY
+ || gScenarioObjective.Type == OBJECTIVE_REPLAY_LOAN_AND_PARK_VALUE)
+ {
+ return gScenarioObjective.Currency;
+ }
+ return 0;
+ }
+
+ void parkValue_set(money32 value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (gScenarioObjective.Type == OBJECTIVE_PARK_VALUE_BY
+ || gScenarioObjective.Type == OBJECTIVE_REPLAY_LOAN_AND_PARK_VALUE)
+ {
+ gScenarioObjective.Currency = value;
+ }
+ }
+
+ money32 monthlyIncome_get()
+ {
+ if (gScenarioObjective.Type == OBJECTIVE_MONTHLY_RIDE_INCOME
+ || gScenarioObjective.Type == OBJECTIVE_MONTHLY_FOOD_INCOME)
+ {
+ return gScenarioObjective.Currency;
+ }
+ return 0;
+ }
+
+ void monthlyIncome_set(money32 value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (gScenarioObjective.Type == OBJECTIVE_PARK_VALUE_BY
+ || gScenarioObjective.Type == OBJECTIVE_REPLAY_LOAN_AND_PARK_VALUE)
+ {
+ gScenarioObjective.Currency = value;
+ }
+ }
+
+ public:
+ static void Register(duk_context* ctx)
+ {
+ dukglue_register_property(ctx, &ScScenarioObjective::type_get, &ScScenarioObjective::type_set, "type");
+ dukglue_register_property(ctx, &ScScenarioObjective::guests_get, &ScScenarioObjective::guests_set, "guests");
+ dukglue_register_property(ctx, &ScScenarioObjective::year_get, &ScScenarioObjective::year_set, "year");
+ dukglue_register_property(
+ ctx, &ScScenarioObjective::excitement_get, &ScScenarioObjective::excitement_set, "excitement");
+ dukglue_register_property(
+ ctx, &ScScenarioObjective::monthlyIncome_get, &ScScenarioObjective::monthlyIncome_set, "monthlyIncome");
+ dukglue_register_property(
+ ctx, &ScScenarioObjective::parkValue_get, &ScScenarioObjective::parkValue_set, "parkValue");
+ }
+ };
+
+ class ScScenario
+ {
+ public:
+ std::string name_get()
+ {
+ return gScenarioName;
+ }
+
+ void name_set(const std::string& value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioName = value;
+ }
+
+ std::string details_get()
+ {
+ return gScenarioDetails;
+ }
+
+ void details_set(const std::string& value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioDetails = value;
+ }
+
+ std::string completedBy_get()
+ {
+ return gScenarioCompletedBy;
+ }
+
+ void completedBy_set(const std::string& value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioCompletedBy = value;
+ }
+
+ std::string filename_get()
+ {
+ return gScenarioFileName;
+ }
+
+ void filename_set(const std::string& value)
+ {
+ ThrowIfGameStateNotMutable();
+ String::Set(gScenarioFileName, std::size(gScenarioFileName), value.c_str());
+ }
+
+ std::shared_ptr objective_get() const
+ {
+ return std::make_shared();
+ }
+
+ uint16_t parkRatingWarningDays_get() const
+ {
+ return gScenarioParkRatingWarningDays;
+ }
+
+ void parkRatingWarningDays_set(uint16_t value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioParkRatingWarningDays = value;
+ }
+
+ DukValue completedCompanyValue_get() const
+ {
+ auto ctx = GetContext()->GetScriptEngine().GetContext();
+ if (gScenarioCompletedCompanyValue == MONEY32_UNDEFINED
+ || gScenarioCompletedCompanyValue == COMPANY_VALUE_ON_FAILED_OBJECTIVE)
+ {
+ return ToDuk(ctx, nullptr);
+ }
+ return ToDuk(ctx, gScenarioCompletedCompanyValue);
+ }
+ void completedCompanyValue_set(int32_t value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioCompletedCompanyValue = value;
+ }
+
+ std::string status_get() const
+ {
+ if (gScenarioCompletedCompanyValue == MONEY32_UNDEFINED)
+ return "inProgress";
+ else if (gScenarioCompletedCompanyValue == COMPANY_VALUE_ON_FAILED_OBJECTIVE)
+ return "failed";
+ return "completed";
+ }
+ void status_set(const std::string& value)
+ {
+ ThrowIfGameStateNotMutable();
+ if (value == "inProgress")
+ gScenarioCompletedCompanyValue = MONEY32_UNDEFINED;
+ else if (value == "failed")
+ gScenarioCompletedCompanyValue = COMPANY_VALUE_ON_FAILED_OBJECTIVE;
+ else if (value == "completed")
+ gScenarioCompletedCompanyValue = gCompanyValue;
+ }
+
+ money32 companyValueRecord_get() const
+ {
+ return gScenarioCompanyValueRecord;
+ }
+ void companyValueRecord_set(money32 value)
+ {
+ ThrowIfGameStateNotMutable();
+ gScenarioCompanyValueRecord = value;
+ }
+
+ public:
+ static void Register(duk_context* ctx)
+ {
+ dukglue_register_property(ctx, &ScScenario::name_get, &ScScenario::name_set, "name");
+ dukglue_register_property(ctx, &ScScenario::details_get, &ScScenario::details_set, "details");
+ dukglue_register_property(ctx, &ScScenario::completedBy_get, &ScScenario::completedBy_set, "completedBy");
+ dukglue_register_property(ctx, &ScScenario::filename_get, &ScScenario::filename_set, "filename");
+ dukglue_register_property(
+ ctx, &ScScenario::parkRatingWarningDays_get, &ScScenario::parkRatingWarningDays_set, "parkRatingWarningDays");
+ dukglue_register_property(ctx, &ScScenario::objective_get, nullptr, "objective");
+ dukglue_register_property(ctx, &ScScenario::status_get, &ScScenario::status_set, "status");
+ dukglue_register_property(
+ ctx, &ScScenario::completedCompanyValue_get, &ScScenario::completedCompanyValue_set, "completedCompanyValue");
+ dukglue_register_property(
+ ctx, &ScScenario::companyValueRecord_get, &ScScenario::companyValueRecord_set, "companyValueRecord");
+ }
+ };
+} // namespace OpenRCT2::Scripting
+
+#endif
diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp
index b7cd51a4d6..0a67294fb6 100644
--- a/src/openrct2/scripting/ScriptEngine.cpp
+++ b/src/openrct2/scripting/ScriptEngine.cpp
@@ -33,6 +33,7 @@
# include "ScObject.hpp"
# include "ScPark.hpp"
# include "ScRide.hpp"
+# include "ScScenario.hpp"
# include "ScSocket.hpp"
# include "ScTile.hpp"
@@ -398,6 +399,8 @@ void ScriptEngine::Initialise()
ScSocket::Register(ctx);
ScListener::Register(ctx);
# endif
+ ScScenario::Register(ctx);
+ ScScenarioObjective::Register(ctx);
ScStaff::Register(ctx);
dukglue_register_global(ctx, std::make_shared(), "cheats");
@@ -407,6 +410,7 @@ void ScriptEngine::Initialise()
dukglue_register_global(ctx, std::make_shared(ctx), "map");
dukglue_register_global(ctx, std::make_shared(ctx), "network");
dukglue_register_global(ctx, std::make_shared(), "park");
+ dukglue_register_global(ctx, std::make_shared(), "scenario");
_initialised = true;
_pluginsLoaded = false;