12#include "../saveload/saveload.h"
14#include "../script/squirrel_class.hpp"
15#include "../script/squirrel_std.hpp"
22#include "api/script_controller.hpp"
23#include "api/script_error.hpp"
24#include "api/script_event.hpp"
25#include "api/script_log.hpp"
27#include "../company_type.h"
28#include "../fileio_func.h"
29#include "../goal_type.h"
30#include "../league_type.h"
31#include "../signs_type.h"
32#include "../story_type.h"
33#include "../misc/endian_buffer.hpp"
35#include "../safeguards.h"
37ScriptStorage::ScriptStorage() =
default;
38ScriptStorage::~ScriptStorage() =
default;
45static void PrintFunc(
bool error_msg, std::string_view message)
48 ScriptController::Print(error_msg, std::string{message});
45static void PrintFunc(
bool error_msg, std::string_view message) {
…}
53 this->
storage = std::make_unique<ScriptStorage>();
54 this->
engine = std::make_unique<Squirrel>(api_name);
60 ScriptObject::ActiveInstance active(*
this);
62 this->
controller = std::make_unique<ScriptController>(company);
73 ScriptObject::DisableDoCommandScope disabler{};
75 if (main_script ==
"%_dummy") {
77 }
else if (!this->
engine->LoadScript(main_script) || this->engine->IsSuspended()) {
78 if (this->
engine->IsSuspended()) ScriptLog::Error(
"This script took too long to load script. AI is not started.");
84 this->
instance = std::make_unique<SQObject>();
85 if (!this->
engine->CreateClassInstance(instance_name, this->controller.get(), this->instance.get())) {
95 this->
engine->ResumeError();
107 std::string script_name = fmt::format(
"compat_{}.nut",
api_version);
110 std::string buf = FioGetDirectory(sp, dir);
114 if (this->
engine->LoadScript(buf))
return true;
116 ScriptLog::Error(fmt::format(
"Failed to load API compatibility script for {}",
api_version));
117 Debug(script, 0,
"Error compiling / running API compatibility script: {}", buf);
121 ScriptLog::Warning(fmt::format(
"API compatibility script for {} not found",
api_version));
128 if (this->
api_version == api_versions.back())
return true;
130 ScriptLog::Info(fmt::format(
"Downgrading API to be compatible with version {}", this->
api_version));
132 HSQUIRRELVM vm = this->
engine->GetVM();
133 sq_pushroottable(vm);
134 sq_pushstring(vm,
"CompatScriptRootTable");
135 sq_pushroottable(vm);
136 sq_newslot(vm, -3, SQFalse);
141 for (
auto it = std::rbegin(api_versions) + 1; it != std::rend(api_versions); ++it) {
147 sq_pushroottable(vm);
148 sq_pushstring(vm,
"CompatScriptRootTable");
149 sq_deleteslot(vm, -2, SQFalse);
155ScriptInstance::~ScriptInstance()
157 ScriptObject::ActiveInstance active(*
this);
174 Debug(script, 0,
"The script died unexpectedly.");
187 ScriptObject::ActiveInstance active(*
this);
189 if (this->
IsDead())
return;
190 if (this->
engine->HasScriptCrashed()) {
200 if (--this->
suspend > 0)
return;
207 sq_poptop(this->
engine->GetVM());
226 ScriptObject::DisableDoCommandScope disabler{};
228 if (this->
engine->MethodExists(*this->instance,
"constructor")) {
230 if (this->
engine->IsSuspended()) ScriptLog::Error(
"This script took too long to initialize. Script is not started.");
236 if (this->
engine->IsSuspended()) ScriptLog::Error(
"This script took too long in the Load function. Script is not started.");
248 this->
engine->ThrowError(e.GetErrorMessage());
249 this->
engine->ResumeError();
257 sq_poptop(this->
engine->GetVM());
269 this->
engine->ThrowError(e.GetErrorMessage());
270 this->
engine->ResumeError();
278 ScriptObject::ActiveInstance active(*
this);
279 this->
engine->CollectGarbage();
285 instance.engine->InsertResult(ScriptObject::GetLastCommandRes());
290 instance.engine->InsertResult(EndianBufferReader::ToValue<VehicleID>(ScriptObject::GetLastCommandResData()));
295 instance.engine->InsertResult(EndianBufferReader::ToValue<SignID>(ScriptObject::GetLastCommandResData()));
300 instance.engine->InsertResult(EndianBufferReader::ToValue<GroupID>(ScriptObject::GetLastCommandResData()));
305 instance.engine->InsertResult(EndianBufferReader::ToValue<GoalID>(ScriptObject::GetLastCommandResData()));
310 instance.engine->InsertResult(EndianBufferReader::ToValue<StoryPageID>(ScriptObject::GetLastCommandResData()));
315 instance.engine->InsertResult(EndianBufferReader::ToValue<StoryPageElementID>(ScriptObject::GetLastCommandResData()));
320 instance.engine->InsertResult(EndianBufferReader::ToValue<LeagueTableElementID>(ScriptObject::GetLastCommandResData()));
325 instance.engine->InsertResult(EndianBufferReader::ToValue<LeagueTableID>(ScriptObject::GetLastCommandResData()));
331 assert(this->
storage !=
nullptr);
337 ScriptObject::ActiveInstance active(*
this);
339 return ScriptObject::GetLogData();
372 if (max_depth == 0) {
373 ScriptLog::Error(
"Savedata can only be nested to 25 deep. No data saved.");
377 switch (sq_gettype(vm, index)) {
384 sq_getinteger(vm, index, &res);
386 int64_t value = (int64_t)res;
387 SlCopy(&value, 1, SLE_INT64);
397 std::string_view view;
398 sq_getstring(vm, index, view);
399 size_t len = view.size() + 1;
401 ScriptLog::Error(
"Maximum string length is 254 chars. No data saved.");
407 SlCopy(
const_cast<char *
>(view.data()), len, SLE_CHAR);
418 while (SQ_SUCCEEDED(sq_next(vm, index - 1))) {
420 bool res =
SaveObject(vm, -1, max_depth - 1, test);
441 while (SQ_SUCCEEDED(sq_next(vm, index - 1))) {
464 sq_getbool(vm, index, &res);
485 SQInteger top = sq_gettop(vm);
488 if (!obj->SaveObject(vm))
throw std::exception();
489 if (sq_gettop(vm) != top + 2)
throw std::exception();
490 if (sq_gettype(vm, -2) != OT_STRING || !
SaveObject(vm, -2, max_depth - 1, test))
throw std::exception();
491 if (!
SaveObject(vm, -1, max_depth - 1, test))
throw std::exception();
495 ScriptLog::Error(
"You tried to save an unsupported type. No data saved.");
502 ScriptLog::Error(
"You tried to save an unsupported type. No data saved.");
515 ScriptObject::ActiveInstance active(*
this);
518 if (this->
engine ==
nullptr || this->
engine->HasScriptCrashed()) {
523 HSQUIRRELVM vm = this->
engine->GetVM();
532 }
else if (this->
engine->MethodExists(*this->instance,
"Save")) {
536 ScriptObject::DisableDoCommandScope disabler{};
537 if (!this->
engine->CallMethod(*this->instance,
"Save", &savedata,
MAX_SL_OPS)) {
541 this->
engine->CrashOccurred();
549 this->
engine->ResumeError();
554 this->
engine->CrashOccurred();
558 if (!sq_istable(savedata)) {
559 ScriptLog::Error(this->
GetOpsTillSuspend() <= 0 ?
"This script took too long to Save." :
"Save function should return a table.");
561 this->
engine->CrashOccurred();
564 sq_pushobject(vm, savedata);
572 this->
engine->CrashOccurred();
575 ScriptLog::Warning(
"Save function is not implemented");
584 HSQUIRRELVM vm = this->
engine->GetVM();
607 if (data !=
nullptr) data->push_back(
static_cast<SQInteger
>(value));
613 static char buf[std::numeric_limits<
decltype(
_script_sl_byte)>::max()];
628 if (data !=
nullptr) data->push_back(
static_cast<SQBool
>(
_script_sl_byte != 0));
653 ScriptDataVariant value = data->front();
660 bool operator()(
const SQInteger &value) { sq_pushinteger(this->vm, value);
return true; }
661 bool operator()(
const std::string &value) { sq_pushstring(this->vm, value);
return true; }
662 bool operator()(
const SQBool &value) { sq_pushbool(this->vm, value);
return true; }
663 bool operator()(
const SQSaveLoadType &type)
667 sq_newarray(this->vm, 0);
668 while (LoadObjects(this->vm, this->data)) {
669 sq_arrayappend(this->vm, -2);
675 sq_newtable(this->vm);
676 while (LoadObjects(this->vm, this->data)) {
677 LoadObjects(this->vm, this->data);
678 sq_rawset(this->vm, -3);
684 sq_pushnull(this->vm);
687 case SQSL_INSTANCE: {
688 SQInteger top = sq_gettop(this->vm);
689 LoadObjects(this->vm, this->data);
690 std::string_view view;
691 sq_getstring(this->vm, -1, view);
693 std::string class_name = fmt::format(
"{}{}", engine->
GetAPIName(), view);
694 sq_pushroottable(this->vm);
695 sq_pushstring(this->vm, class_name);
696 if (SQ_FAILED(sq_get(this->vm, -2)))
throw Script_FatalError(fmt::format(
"'{}' doesn't exist", class_name));
697 sq_pushroottable(vm);
698 if (SQ_FAILED(sq_call(this->vm, 1, SQTrue, SQFalse)))
throw Script_FatalError(fmt::format(
"Failed to instantiate '{}'", class_name));
700 sq_getstackobj(vm, -1, &res);
702 sq_settop(this->vm, top);
703 sq_pushobject(vm, res);
704 sq_release(vm, &res);
706 LoadObjects(this->vm, this->data);
707 if (!obj->LoadObject(vm))
throw Script_FatalError(fmt::format(
"Failed to load '{}'", class_name));
712 case SQSL_ARRAY_TABLE_END:
715 default: NOT_REACHED();
720 return std::visit(visitor{vm, data}, value);
743 ScriptData *data =
new ScriptData();
744 data->push_back((SQInteger)version);
751 ScriptObject::ActiveInstance active(*
this);
753 if (this->
IsDead() || data ==
nullptr)
return;
755 HSQUIRRELVM vm = this->engine->
GetVM();
757 ScriptDataVariant version = data->front();
759 SQInteger top = sq_gettop(vm);
761 sq_pushinteger(vm, std::get<SQInteger>(version));
765 ScriptLog::Warning(fmt::format(
"Loading failed: {}", e.
GetErrorMessage()));
773 HSQUIRRELVM vm = this->engine->
GetVM();
779 if (!this->engine->
MethodExists(*this->instance,
"Load")) {
780 ScriptLog::Warning(
"Loading failed: there was data for the script to load, but the script does not have a Load() function.");
790 sq_pushstring(vm,
"Load");
801 if (SQ_FAILED(sq_call(vm, 3, SQFalse, SQTrue,
MAX_SL_OPS)))
return false;
815 ScriptObject::ActiveInstance active(*
this);
817 if (!ScriptObject::CheckLastCommand(data, cmd)) {
818 Debug(script, 1,
"DoCommandCallback terminating a script, last command does not match expected command");
822 ScriptObject::SetLastCommandRes(result.
Succeeded());
823 ScriptObject::SetLastCommandResData(std::move(result_data));
826 ScriptObject::SetLastError(ScriptError::StringToError(result.
GetErrorMessage()));
828 ScriptObject::IncreaseDoCommandCosts(result.
GetCost());
829 ScriptObject::SetLastCost(result.
GetCost());
832 ScriptObject::SetLastCommand({},
CMD_END);
839 ScriptObject::ActiveInstance active(*
this);
841 ScriptEventController::InsertEvent(event);
844size_t ScriptInstance::GetAllocatedMemory()
const
Common return value for all commands.
bool Succeeded() const
Did this command succeed?
Money GetCost() const
The costs as made up to this moment.
bool Failed() const
Did this command fail?
StringID GetErrorMessage() const
Returns the error message of a command.
Runtime information about a script like a pointer to the squirrel vm and the current state.
bool LoadCompatibilityScripts(Subdirectory dir, std::span< const std::string_view > api_versions)
Load squirrel scripts to emulate an older API.
static ScriptData * Load(int version)
Load data from a savegame.
virtual void RegisterAPI()
Register all API functions to the VM.
void InsertEvent(class ScriptEvent *event)
Insert an event for this script.
void Unpause()
Resume execution of the script.
static void DoCommandReturnGroupID(ScriptInstance &instance)
Return a GroupID reply for a DoCommand.
bool IsPaused()
Checks if the script is paused.
virtual void Died()
Tell the script it died.
static bool LoadObjects(ScriptData *data)
Load all objects from a savegame.
void ReleaseSQObject(HSQOBJECT *obj)
Decrease the ref count of a squirrel object.
static void DoCommandReturnVehicleID(ScriptInstance &instance)
Return a VehicleID reply for a DoCommand.
static void DoCommandReturnSignID(ScriptInstance &instance)
Return a SignID reply for a DoCommand.
std::unique_ptr< class Squirrel > engine
A wrapper around the squirrel vm.
void CollectGarbage()
Let the VM collect any garbage.
static void DoCommandReturn(ScriptInstance &instance)
Return a true/false reply for a DoCommand.
SQSaveLoadType
The type of the data that follows in the savegame.
@ SQSL_BOOL
The following data is a boolean.
@ SQSL_STRING
The following data is an string.
@ SQSL_TABLE
The following data is an table.
@ SQSL_ARRAY_TABLE_END
Marks the end of an array or table, no data follows.
@ SQSL_ARRAY
The following data is an array.
@ SQSL_NULL
A null variable.
@ SQSL_INT
The following data is an integer.
@ SQSL_INSTANCE
The following data is an instance.
static void DoCommandReturnGoalID(ScriptInstance &instance)
Return a GoalID reply for a DoCommand.
static void SaveEmpty()
Don't save any data in the savegame.
static void DoCommandReturnLeagueTableElementID(ScriptInstance &instance)
Return a LeagueTableElementID reply for a DoCommand.
void LoadOnStack(ScriptData *data)
Store loaded data on the stack.
void Save()
Call the script Save function and save all data in the savegame.
bool is_save_data_on_stack
Is the save data still on the squirrel stack?
bool in_shutdown
Is this instance currently being destructed?
virtual void LoadDummyScript()=0
Load the dummy script.
std::string api_version
Current API used by this script.
bool DoCommandCallback(const CommandCost &result, const CommandDataBuffer &data, CommandDataBuffer result_data, Commands cmd)
DoCommand callback function for all commands executed by scripts.
ScriptLogTypes::LogData & GetLogData()
Get the log pointer of this script.
void Pause()
Suspends the script for the current tick and then pause the execution of script.
ScriptInstance(std::string_view api_name)
Create a new script.
Script_SuspendCallbackProc * callback
Callback that should be called in the next tick the script runs.
void Initialize(const std::string &main_script, const std::string &instance_name, CompanyID company)
Initialize the script and prepare it for its first run.
static bool SaveObject(HSQUIRRELVM vm, SQInteger index, int max_depth, bool test)
Save one object (int / string / array / table) to the savegame.
bool CallLoad()
Call the script Load function if it exists and data was loaded from a savegame.
bool is_dead
True if the script has been stopped.
std::unique_ptr< class ScriptController > controller
The script main class.
bool IsDead() const
Return the "this script died" value.
size_t last_allocated_memory
Last known allocated memory value (for display for crashed scripts)
void Continue()
A script in multiplayer waits for the server to handle its DoCommand.
static void LoadEmpty()
Load and discard data from a savegame.
void GameLoop()
Run the GameLoop of a script.
static void DoCommandReturnStoryPageElementID(ScriptInstance &instance)
Return a StoryPageElementID reply for a DoCommand.
int suspend
The amount of ticks to suspend this script before it's allowed to continue.
SQInteger GetOpsTillSuspend()
Get the number of operations the script can execute before being suspended.
std::unique_ptr< class ScriptStorage > storage
Some global information for each running script.
class ScriptStorage & GetStorage()
Get the storage of this script.
bool is_started
Is the scripts constructor executed?
static void DoCommandReturnLeagueTableID(ScriptInstance &instance)
Return a LeagueTableID reply for a DoCommand.
static void DoCommandReturnStoryPageID(ScriptInstance &instance)
Return a StoryPageID reply for a DoCommand.
bool LoadCompatibilityScript(std::string_view api_version, Subdirectory dir)
Load squirrel script for a specific version to emulate an older API.
bool is_paused
Is the script paused? (a paused script will not be executed until unpaused)
std::unique_ptr< SQObject > instance
Squirrel-pointer to the script main class.
The storage for each script.
A throw-class that is given when the script made a fatal error.
const std::string & GetErrorMessage() const
The error message associated with the fatal error.
A throw-class that is given when the script wants to suspend.
int GetSuspendTime()
Get the amount of ticks the script should be suspended.
Script_SuspendCallbackProc * GetSuspendCallback()
Get the callback to call when the script can run again.
static SQUserPointer GetRealInstance(HSQUIRRELVM vm, int index, std::string_view tag)
Get the real-instance pointer.
SQInteger GetOpsTillSuspend()
How many operations can we execute till suspension?
void ReleaseObject(HSQOBJECT *ptr)
Release a SQ object.
size_t GetAllocatedMemory() const noexcept
Get number of bytes allocated by this VM.
static void DecreaseOps(HSQUIRRELVM vm, int amount)
Tell the VM to remove amount ops from the number of ops till suspend.
HSQUIRRELVM GetVM()
Get the squirrel VM.
std::string_view GetAPIName()
Get the API name.
bool MethodExists(HSQOBJECT instance, std::string_view method_name)
Check if a method exists in an instance.
std::vector< uint8_t > CommandDataBuffer
Storage buffer for serialized command data.
Commands
List of commands.
@ CMD_END
Must ALWAYS be on the end of this list!! (period)
CompanyID _current_company
Company currently doing an action.
#define Debug(category, level, format_string,...)
Output a line of debugging information.
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Searchpath
Types of searchpaths OpenTTD might use.
Subdirectory
The different kinds of subdirectories OpenTTD uses.
void SlCopy(void *object, size_t length, VarType conv)
Copy a list of SL_VARs to/from a savegame.
void SlErrorCorrupt(const std::string &msg)
Error handler for corrupt savegames.
void SlObject(void *object, const SaveLoadTable &slt)
Main SaveLoad function.
#define SLEG_VAR(name, variable, type)
Storage of a global variable in every savegame version.
bool IsSavegameVersionBefore(SaveLoadVersion major, uint8_t minor=0)
Checks whether the savegame is below major.
@ SLV_SCRIPT_INT64
296 PR#9415 SQInteger is 64bit but was saved as 32bit.
The definition of Script_FatalError.
ScriptInfo keeps track of all information of a script, like Author, Description, ....
static const int MAX_SL_OPS
The maximum number of operations for saving or loading the data of a script.
static const int MAX_CONSTRUCTOR_OPS
The maximum number of operations for initial start of a script.
static void PrintFunc(bool error_msg, std::string_view message)
Callback called by squirrel when a script uses "print" and for error messages.
static uint8_t _script_sl_byte
Used as source/target by the script saveload code to store/load a single byte.
static const SaveLoad _script_byte[]
SaveLoad array that saves/loads exactly one byte.
The ScriptInstance tracks a script.
static const uint SQUIRREL_MAX_DEPTH
The maximum recursive depth for items stored in the savegame.
Defines ScriptStorage and includes all files required for it.
GameSettings _settings_game
Game settings of a running game or the scenario editor.
void squirrel_register_std(Squirrel &engine)
Register all standard functions we want to give to a script.
static void StrMakeValid(Builder &builder, StringConsumer &consumer, StringValidationSettings settings)
Copies the valid (UTF-8) characters from consumer to the builder.
ScriptSettings script
settings for scripts
uint32_t script_max_opcode_till_suspend
max opcode calls till scripts will suspend