"Logic" changes (ILogic.Logic, ITask.Logic, TaskManager)
Along the theme of the previous post, "logic" based systems have gone a refresh as well. The main motivation behind these changes is to present a more intuitive system that helps devs avoid ambiguous mechanics while giving them the flexibility and power needed to do cool things.
ILogic is now ILogicHandler.
Code:
/// <summary>
/// An interface for an object that can handle dynamic logic implementations.
/// </summary>
public interface ILogicHandler
{
/// <summary>
/// Implements the ability to handle a logic passed through the system.
/// </summary>
/// <param name="logic">The logic to be processed.</param>
/// <returns>A LogicResult that describes the result..</returns>
Task<LogicResult> Logic(Logic logic);
}
The LogicResult enum has two states:
Code:
/// <summary>
/// Return value type for ILogic.Logic.
/// </summary>
public enum LogicResult
{
/// <summary>The logic was provided.</summary>
Provided,
/// <summary>The logic was not provided.</summary>
Unprovided,
}
The idea behind the new Logic function is the same as the new Message function - more intuitive designs to help users avoid ambiguous or hidden behaviors. Logic that provides executed code should return
LogicResult.Provided now rather than true, and LogicResult.Unprovided rather than false.
Old AutoLogin code
Code:
public async Task<bool> Logic(string type, params dynamic[] param)
{
if (type == "login_screen_hook")
{
return await login_screen(type, param);
}
if (type == "character_selection_hook")
{
return await character_selection(type, param);
}
return false;
}
New AutoLogin code
Code:
public async Task<LogicResult> Logic(Logic logic)
{
if (type == "hook_login_screen")
{
if (await login_screen(logic))
{
return LogicResult.Provided;
}
return LogicResult.Unprovided;
}
if (type == "hook_character_selection")
{
if (await character_selection(logic))
{
return LogicResult.Provided;
}
return LogicResult.Unprovided;
}
return LogicResult.Unprovided;
}
Where the coroutines 'login_screen' and 'character_selection' are separate tasks that return true/false based on their logic which is processed by the caller.
The intended use of Logic is to allow code to insert a placeholder call for external logic to be executed. Typically, this pattern is used for "hooks", but "non-hook" logic is also acceptable as long as it's not "event" or "message" based logic (in which case those should use the Message function instead).
For example, in addition to the character selection and login screen hooks shown above, the "combat" message that is handled in any IRoutine is triggered from a combat task. The whole task is just calling Logic on the current routine, and letting the routine have full control over what happens. This pattern can me applied to a lot of other things as well for similar dynamic systems.
These changes result in the proper handling of messages that have been incorrectly using Logic in the current system.
"plugin_coroutine_event" - In current bot bases, this is called before the TaskManager polls its tasks.
Code:
foreach (var plugin in PluginManager.EnabledPlugins)
{
await plugin.Logic("plugin_coroutine_event");
}
The primary use of "plugin_coroutine_event" right now is in "CommonEvents", which performs a BroadcastEventLogic for various events, such as "core_player_died_event", "core_player_leveled_event", "core_area_changed_event". The original intention with this design was that when such an event happened, it'd be possible to run async code right away, in a hook-like fashion, except all handlers get called.
The problem with the design is that it's never practical to execute a sequence of unrelated coroutines in such a manner, because it can result in unintended behaviors as soon as one of the handlers performs complex logic, like changing areas. While there are no known problems so far, it's a design flaw that needs to be addressed to improve there coherency of these core systems. In addition, it's extra overhead for no other reason than to support CommonEvents, which can easily be implemented in Tick and use messages!
"plugin_coroutine_event" has been removed completely.
As a result: "core_area_changed_event", "core_player_died_event", "core_player_leveled_event" are now no longer "Logic" messages, but rather Messages for IMessageHandlers, so all code that used those events in Logic will need to migrate the code to the new "Message" function and return the appropriate "MessageResult" value.
This change will hopefully make things a bit more intuitive. Currently, it makes no sense you have to handle "core_player_died_event" in "Logic", and "Execute" doesn't get notified. Having to choose between coroutine and non-coroutine variants for "events" shouldn't be a thing, so now such "events" are treated as what they really are, "messages".
* "core_exploration_complete_event" is also affected, but should have been named "ogb_exploration_complete_event", so that change is now in place.
The following Logic messages have been renamed for consistency:
"combat" -> "hook_combat"
"login_screen_hook" -> "hook_login_screen"
"character_selection_hook" -> "hook_character_selection"
"post_combat_hook" -> "hook_post_combat"
Additional hooks will be added to improve customization as part of other 3.0 updates.
ITask no longer implements ILogic. Originally, the intended design was for them to share the same interface to cover overlap, but that system has caused a lot of confusion over time, and contributes to a non-intuitive design.
"task_execute" - In current bot bases that make use of tasks, this is called on the TaskManager instance to transition logic into that provided by tasks.
Current bot base code
Code:
// What the bot does now is up to the registered tasks.
await _taskManager.Logic("task_execute");
The idea behind this setup was to pass execution to the list of tasks managed by the task manager. This design has proved very valuable and served as the core of how logic was implemented in bots the past few years.
The new code for that is as follows (name might change soon too):
Code:
// What the bot does now is up to the registered tasks.
var logic = new Logic("task_execute", this);
await _taskManager.RunTasks(TaskGroup.Enabled, RunBehavior.UntilHandled, logic);
TaskManager is getting a few improvements to reflect how tasks are primary used now.
TaskManager/Base.Execute is now "MessageTasks". This is to reflect changes made to ILogic.Execute, which is now IMessageHandlers.Message as talked about in the previous post.
TaskManager/Base.Logic is now "RunTasks". This is to avoid confusion with calls to ILogic.Logic and better reflects what the coroutine does. The name "Execute" is not being used to avoid confusion with the previous ILogic function, and the past coroutine based Execute that preceded Logic.
RunTasks now returns a RunTasksResult, which has values of "NoTasksRan" and "TasksRan" to report to calling logic whether or not tasks ran to handle the logic.
In addition, RunTasks now takes a TaskGroup parameter at the start to tell the logic if all tasks should be run (All), enabled tasks should be run (Enabled), or if only disabled tasks should be run (Disabled). This change was done to provide more transparency as to what the coroutine is actually doing, as before only enabled tasks were ran.
Finally, hidden mechanics of the old TaskManager.Run function have been removed. Previously, if the logic type ended with "_event", the function would discard return values and simply process all tasks. Otherwise, the task looping would stop as soon as a task returned true.
Utility.BroadcastEventLogic was used to implement the now removed idea of using "events" via Logic, so that function has now been removed. Logic using BroadcastEventLogic needs to switch to the new Message systems instead, as that is the correct way to handle "events".
The following events were changed as a result, and have been updated to the Message system:
oldgrindbot_local_area_changed_event
item_looted_event [EXtensions]
player_resurrected_event [EXtensions]
item_stashed_event [EXtensions]
items_sold_event [EXtensions]
explorer_local_transition_entered_event [EXtensions]
map_trial_entered_event [EXtensions]
That about covers the main logic based changes in place so far. It's important these core systems get the necessary updates, so new logic for 3.0 in our bots, plugins, and routines can properly take advantage of them. These breaking changes will result in quite a bit of code updates, but the updates themselves are really simple - moving code to different places and fixing return values from true/false to the appropriate enum.