1
0
Fork 0

Add: [Console] schedule command to execute a script file later

The change in https://github.com/OpenTTD/OpenTTD/pull/10655, released as part
of OpenTTD 14, changed how auto saving works from every X amount of game time,
to every X amount of real time. This is an improvement for normal play, but
there are cases where saving every X amount of game time is useful. For example
when developing and comparing AIs, or using OpenTTD to conduct experiments such
as via https://github.com/michalc/OpenTTDLab which extracts data from savegame
files.

This change adds a general "schedule" console command that makes it possible
for a console script to be scheduled for the future, where that script can, for
example, contain a "save" command to save the game. That script can also then
reschedule itself or another script, and thus making it possible to re-create
the pre-OpenTTD 14 behaviour of saving the game every X amount of game time.
(Possibly with a whole "suite" of game scripts that call each other in a chain,
each of them hard coded with a different filename to allow all the savegames to
be saved with different names, but that's a doable detail)

The current change only allows scheduling "on-next-calendar-month", since that
is my specific use case, but the syntax is hopefully friendly to this being
extended in the future. And by scheduling any console script, it is much more
general and so hopefully useful than just for saving games.

There was some discussion on what this should be called at
https://discord.com/channels/142724111502802944/1008473233844097104/1247278039927361546
(as well as some issues with an alternatively proposed solution using game
scripts at https://github.com/OpenTTD/OpenTTD/pull/12750). Instead of
"schedule", "trigger" was suggested for something to only be called once, but
after writing code and output to the user, it didn't make it immediately clear
that "trigger" means "once at some point in the future", and seemed too close
to "exec". Also I think "schedule" can be used _both_ for one-off or repeated
events in the future (in English it's common to say to "schedule an
appointment", even if it's just the one).

Or code-wise:

schedule on-next-calendar-month my.scr (the only current behaviour)
schedule every-calendar-month my.scr (possible future behaviour?)
schedule "* 1 * *" my.scr (possible future cron-like behaviour?)

Possible future extensions could include some mechanism for cancelling
something that has been scheduled, and I have no reason to think that mechanism
would be different for something scheduled to repeat or just as a one-off, so I
think also it makes sense to that we would have the one command for both.

The new "schedule" command:

- Only allows a single script to be scheduled - any (correct) usage of the
  "schedule" command will overwrite the previous, but hopefully the console
  messages make this clear
- Only allows a script to be scheduled once at the start of next month (but
  this could be extended if useful).
- Does not offer a way to unschedule, but hopefully since the script can only
  be scheduled to run once in the future, and relatively close in the future
  (at the start of next month), this shouldn't be too much of an annoying missing
  feature.

Co-authored-by: Patric Stout <github@truebrain.nl>
pull/12761/head
Michal Charemza 2024-06-06 21:25:47 +01:00
parent 8c7cf3bc75
commit dc26e40704
No known key found for this signature in database
GPG Key ID: B25024AF16535FB5
1 changed files with 49 additions and 1 deletions

View File

@ -29,7 +29,7 @@
#include "strings_func.h" #include "strings_func.h"
#include "viewport_func.h" #include "viewport_func.h"
#include "window_func.h" #include "window_func.h"
#include "timer/timer_game_calendar.h" #include "timer/timer.h"
#include "company_func.h" #include "company_func.h"
#include "gamelog.h" #include "gamelog.h"
#include "ai/ai.hpp" #include "ai/ai.hpp"
@ -53,6 +53,24 @@
/* scriptfile handling */ /* scriptfile handling */
static uint _script_current_depth; ///< Depth of scripts running (used to abort execution when #ConReturn is encountered). static uint _script_current_depth; ///< Depth of scripts running (used to abort execution when #ConReturn is encountered).
/* Scheduled execution handling. */
static std::string _scheduled_monthly_script; ///< Script scheduled to execute by the 'schedule' console command (empty if no script is scheduled).
/** Timer that runs every month of game time for the 'schedule' console command. */
static IntervalTimer<TimerGameCalendar> _scheduled_monthly_timer = {{TimerGameCalendar::MONTH, TimerGameCalendar::Priority::NONE}, [](auto) {
if (_scheduled_monthly_script.empty()) {
return;
}
/* Clear the schedule before rather than after the script to allow the script to itself call
* schedule without it getting immediately cleared. */
const std::string filename = _scheduled_monthly_script;
_scheduled_monthly_script.clear();
IConsolePrint(CC_DEFAULT, "Executing scheduled script file '{}'...", filename);
IConsoleCmdExec(std::string("exec") + " " + filename);
}};
/** File list storage for the console, for caching the last 'ls' command. */ /** File list storage for the console, for caching the last 'ls' command. */
class ConsoleFileList : public FileList { class ConsoleFileList : public FileList {
public: public:
@ -1154,6 +1172,35 @@ DEF_CONSOLE_CMD(ConExec)
return true; return true;
} }
DEF_CONSOLE_CMD(ConSchedule)
{
if (argc < 3 || std::string_view(argv[1]) != "on-next-calendar-month") {
IConsolePrint(CC_HELP, "Schedule a local script to execute later. Usage: 'schedule on-next-calendar-month <script>'.");
return true;
}
/* Check if the file exists. It might still go away later, but helpful to show an error now. */
if (!FioCheckFileExists(argv[2], BASE_DIR)) {
IConsolePrint(CC_ERROR, "Script file '{}' not found.", argv[2]);
return true;
}
/* We only support a single script scheduled, so we tell the user what's happening if there was already one. */
const std::string_view filename = std::string_view(argv[2]);
if (!_scheduled_monthly_script.empty() && filename == _scheduled_monthly_script) {
IConsolePrint(CC_INFO, "Script file '{}' was already scheduled to execute at the start of next calendar month.", filename);
} else if (!_scheduled_monthly_script.empty() && filename != _scheduled_monthly_script) {
IConsolePrint(CC_INFO, "Script file '{}' scheduled to execute at the start of next calendar month, replacing the previously scheduled script file '{}'.", filename, _scheduled_monthly_script);
} else {
IConsolePrint(CC_INFO, "Script file '{}' scheduled to execute at the start of next calendar month.", filename);
}
/* Store the filename to be used by _schedule_timer on the start of next calendar month. */
_scheduled_monthly_script = filename;
return true;
}
DEF_CONSOLE_CMD(ConReturn) DEF_CONSOLE_CMD(ConReturn)
{ {
if (argc == 0) { if (argc == 0) {
@ -2721,6 +2768,7 @@ void IConsoleStdLibRegister()
IConsole::CmdRegister("echo", ConEcho); IConsole::CmdRegister("echo", ConEcho);
IConsole::CmdRegister("echoc", ConEchoC); IConsole::CmdRegister("echoc", ConEchoC);
IConsole::CmdRegister("exec", ConExec); IConsole::CmdRegister("exec", ConExec);
IConsole::CmdRegister("schedule", ConSchedule);
IConsole::CmdRegister("exit", ConExit); IConsole::CmdRegister("exit", ConExit);
IConsole::CmdRegister("part", ConPart); IConsole::CmdRegister("part", ConPart);
IConsole::CmdRegister("help", ConHelp); IConsole::CmdRegister("help", ConHelp);