diff --git a/CMakeLists.txt b/CMakeLists.txt index 742a1e4..29ddccd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,6 @@ add_library(ISXMr SHARED ${SOURCE_DIR}/ISXMr.cpp src/isxeq2/Actor.cpp src/isxeq2/Actor.h src/isxeq2/LSObject.h - src/WriteUIFileToDisk.cpp src/isxeq2/Point3f.h src/isxeq2/Ability.cpp src/isxeq2/Ability.h @@ -78,12 +77,12 @@ add_library(ISXMr SHARED ${SOURCE_DIR}/ISXMr.cpp src/isxeq2/CharacterClass.h src/isxeq2/AbilityEffect.h src/Logger.h - src/Commands/ExecutableCommand.h - src/Commands/ExportCommand.cpp - src/Commands/ExportCommand.h + src/Tasks/ExecutableTask.h + src/Tasks/ExportAbilitiesTask.cpp + src/Tasks/ExportAbilitiesTask.h src/isxeq2/ExtensionTLOs.h - src/Commands/CommandExecutor.cpp - src/Commands/CommandExecutor.h + src/Tasks/TaskExecutor.cpp + src/Tasks/TaskExecutor.h src/BotSettings/ExportedAbility.h includes/argh/argh.h src/isxeq2/EQ2.h @@ -94,6 +93,10 @@ add_library(ISXMr SHARED ${SOURCE_DIR}/ISXMr.cpp src/Api/MrBotApi.h includes/json_struct/json_struct.h includes/json_struct/json_struct_diff.h + src/Tasks/BotTask.cpp + src/Tasks/BotTask.h + src/ScopedEnumBitwiseOperators.h + lgui2/UpdateUIPackageFile.h ) IF (WIN32) diff --git a/lgui2/bot_cast_stack.json b/lgui2/bot_cast_stack.json new file mode 100644 index 0000000..ec14a4f --- /dev/null +++ b/lgui2/bot_cast_stack.json @@ -0,0 +1,687 @@ +{ + "$schema": "http://www.lavishsoft.com/schema/lgui2Package.json", + "skin": "MRSkin", + "templates": { + "settings.abilityListEntry": { + "jsonTemplate": "listboxitem", + "padding": 2, + "content": { + "type": "stackpanel", + "orientation": "vertical", + "-contentContainer": { + "jsonTemplate": "listbox.contentContainerFitWidth" + }, + "children": [ + { + "type": "textblock", + "horizontalAlignment": "stretch", + "textBinding": { + "pullFormat": "${_CONTEXTITEMDATA_.Get[name]}" + } + } + ] + } + }, + "settings.castStack": { + "orientation": "vertical", + "children": [ + { + "type": "panel", + "visibility": "hidden", + "name": "CastStackController.events" + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "heightFactor": 0.9, + "widthFactor": 1, + "horizontalAlignment": "left", + "children": [ + { + "type": "stackpanel", + "orientation": "vertical", + "widthFactor": 0.3, + "margin": [ + 0, + 0, + 5, + 0 + ], + "children": [ + { + "type": "textblock", + "text": "Ability List", + "widthFactor": 1, + "horizontalAlignment": "left" + }, + { + "type": "listbox", + "name": "abilityList", + "heightFactor": 0.9, + "itemsBinding": { + "pullFormat": "${CastStackController.abilityListItems}", + "pullOnce": true + }, + "itemViewGenerators": { + "default": { + "type": "template", + "template": "settings.abilityListEntry" + } + }, + "selectedItemBinding": { + "pullFormat": "${CastStackController.currentSelectedAvailableAbilityIndex}", + "pushFormat": [ + "CastStackController:SetCurrentAbility[\"", + "\"]" + ] + }, + "-contentContainer": { + "jsonTemplate": "listbox.contentContainerFitWidth" + } + }, + { + "type": "button", + "name": "castStack.addAbility", + "content": "Add Ability", + "horizontalAlignment": "stretch", + "eventHandlers": { + "onRelease": [ + "method", + "CastStackController", + "AddAbility" + ] + } + } + ] + }, + { + "type": "stackpanel", + "orientation": "vertical", + "widthFactor": 0.3, + "heightFactor": 1, + "verticalAlignment": "top", + "margin": [ + 0, + 0, + 5, + 0 + ], + "padding": [ + 0, + 16, + 0, + 0 + ], + "children": [ + { + "type": "stackpanel", + "orientation": "horizontal", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "Type", + "width": 60 + }, + { + "type": "combobox", + "name": "castStack.ability.type", + "horizontalAlignment": "stretch", + "items": [ + "Combat", + "CA", + "NamedCA", + "Heal", + "PowerHeal", + "Debuff", + "NamedDebuff", + "NonCombatBuff", + "Cure", + "Buff" + ], + "selectedItemBinding": { + "pullFormat": "${CastStackController.newCastStackItem.Get[type]}", + "autoPull": false, + "pullHook": { + "elementName": "CastStackController.events", + "flags": "global", + "event": "onNewCastStackItemChanged" + } + }, + "eventHandlers": { + "onSelectionChanged": { + "type": "method", + "object": "CastStackController", + "method": "OnCastStackAbilityComboChange" + } + } + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "Target", + "width": 60 + }, + { + "type": "combobox", + "name": "castStack.ability.target", + "horizontalAlignment": "stretch", + "itemsBinding": { + "pullFormat": "${CastStackController.GetTargetOptions}", + "pullOnce": true + }, + "selectedItemBinding": { + "pullFormat": "${CastStackController.newCastStackItem.Get[target]}", + "autoPull": false, + "pullHook": { + "elementName": "CastStackController.events", + "flags": "global", + "event": "onNewCastStackItemChanged" + } + }, + "eventHandlers": { + "onSelectionChanged": { + "type": "method", + "object": "CastStackController", + "method": "OnCastStackAbilityComboChange" + } + } + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "# targets", + "width": 60 + }, + { + "type": "textbox", + "name": "castStack.ability.targetCount", + "horizontalAlignment": "stretch", + "textBinding": { + "pullFormat": "${CastStackController.SafeGetNewCastStackItemProperty[targetCount]}", + "autoPull": false, + "pullHook": { + "elementName": "CastStackController.events", + "flags": "global", + "event": "onNewCastStackItemChanged" + }, + "pushFormat": [ + "CastStackController:SafeSetNewCastStackItemProperty[\"targetCount\",\"", + "\"]" + ], + "autoPush": false + }, + "hooks": { + "onLostFocus": { + "flags": "self", + "event": "lostKeyboardFocus", + "eventHandler": { + "type": "forward", + "event": "pushTextBinding", + "flags": "self" + } + }, + "onLostMouseFocus": { + "flags": "self", + "event": "lostMouseFocus", + "eventHandler": { + "type": "forward", + "event": "pushTextBinding", + "flags": "self" + } + } + } + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "children": [ + { + "type": "textblock", + "text": "HP/MP %", + "width": 60 + }, + { + "type": "textbox", + "name": "castStack.percent", + "horizontalAlignment": "stretch", + "textBinding": { + "pullFormat": "${CastStackController.SafeGetNewCastStackItemProperty[percent]}", + "autoPull": false, + "pullHook": { + "elementName": "CastStackController.events", + "flags": "global", + "event": "onNewCastStackItemChanged" + }, + "pushFormat": [ + "CastStackController:SafeSetNewCastStackItemProperty[\"percent\",\"", + "\"]" + ], + "autoPush": false + }, + "hooks": { + "onLostFocus": { + "flags": "self", + "event": "lostKeyboardFocus", + "eventHandler": { + "type": "forward", + "event": "pushTextBinding", + "flags": "self" + } + }, + "onLostMouseFocus": { + "flags": "self", + "event": "lostMouseFocus", + "eventHandler": { + "type": "forward", + "event": "pushTextBinding", + "flags": "self" + } + } + } + } + ] + }, + { + "type": "checkbox", + "name": "castStack.ignoreDuration", + "content": "Ignore Duration", + "horizontalAlignment": "stretch", + "margin": [ + 2, + 10, + 0, + 0 + ] + }, + { + "type": "checkbox", + "name": "castStack.ignoreEncounterNukes", + "content": "Ignore Encounter Nukes", + "horizontalAlignment": "stretch" + }, + { + "type": "checkbox", + "name": "castStack.ignoreAENukes", + "content": "Ignore AE Nukes", + "horizontalAlignment": "stretch" + }, + { + "type": "checkbox", + "name": "castStack.maxIncrements", + "content": "Max Increments", + "horizontalAlignment": "stretch" + }, + { + "type": "checkbox", + "name": "castStack.namedOnly", + "content": "Named Only", + "horizontalAlignment": "stretch" + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "horizontalAlignment": "stretch", + "margin": [ + 0, + 10, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "Fervor", + "width": 70 + }, + { + "type": "textbox", + "name": "castStack.fervorRangeMin", + "width": 30 + }, + { + "type": "textblock", + "text": "-", + "font": { + "bold": true, + "height": 24 + }, + "margin": [ + 5, + 0, + 0, + 0 + ], + "width": 10 + }, + { + "type": "textbox", + "name": "castStack.fervorRangeMax", + "width": 30 + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "horizontalAlignment": "stretch", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "Dissonance", + "width": 70 + }, + { + "type": "textbox", + "name": "castStack.dissonanceRangeMin", + "width": 30 + }, + { + "type": "textblock", + "text": "-", + "font": { + "bold": true, + "height": 24 + }, + "margin": [ + 5, + 0, + 0, + 0 + ], + "width": 10 + }, + { + "type": "textbox", + "name": "castStack.dissonanceRangeMax", + "width": 30 + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "horizontalAlignment": "stretch", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "My HP", + "width": 70 + }, + { + "type": "textbox", + "name": "castStack.myHpRangeMin", + "width": 30 + }, + { + "type": "textblock", + "text": "-", + "font": { + "bold": true, + "height": 24 + }, + "margin": [ + 5, + 0, + 0, + 0 + ], + "width": 10 + }, + { + "type": "textbox", + "name": "castStack.myHpRangeMax", + "width": 30 + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "horizontalAlignment": "stretch", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "My Power", + "width": 70 + }, + { + "type": "textbox", + "name": "castStack.myPowerRangeMin", + "width": 30 + }, + { + "type": "textblock", + "text": "-", + "font": { + "bold": true, + "height": 24 + }, + "margin": [ + 5, + 0, + 0, + 0 + ], + "width": 10 + }, + { + "type": "textbox", + "name": "castStack.myPowerRangeMax", + "width": 30 + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "horizontalAlignment": "stretch", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "NPC HP", + "width": 70 + }, + { + "type": "textbox", + "name": "castStack.npcHpRangeMin", + "width": 30 + }, + { + "type": "textblock", + "text": "-", + "font": { + "bold": true, + "height": 24 + }, + "margin": [ + 5, + 0, + 0, + 0 + ], + "width": 10 + }, + { + "type": "textbox", + "name": "castStack.npcHpRangeMax", + "width": 30 + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "horizontalAlignment": "stretch", + "margin": [ + 0, + 0, + 0, + 5 + ], + "children": [ + { + "type": "textblock", + "text": "Aggro", + "width": 70 + }, + { + "type": "textbox", + "name": "castStack.aggroRangeMin", + "width": 30 + }, + { + "type": "textblock", + "text": "-", + "font": { + "bold": true, + "height": 24 + }, + "margin": [ + 5, + 0, + 0, + 0 + ], + "width": 10 + }, + { + "type": "textbox", + "name": "castStack.aggroRangeMax", + "width": 30 + } + ] + } + ] + }, + { + "type": "stackpanel", + "orientation": "vertical", + "widthFactor": 1, + "heightFactor": 1, + "children": [ + { + "type": "textblock", + "text": "Cast Order", + "widthFactor": 1, + "horizontalAlignment": "left" + }, + { + "type": "listbox", + "name": "castStack.castOrder", + "horizontalAlignment": "stretch", + "heightFactor": 0.9, + "itemsBinding": { + "pullFormat": "${CastStackController.profile.Get[castStack].Keys}" + } + }, + { + "type": "button", + "name": "castStack.castOrder.edit", + "content": "Edit Entry", + "horizontalAlignment": "stretch", + "eventHandlers": { + "onRelease": [ + "method", + "CastStackController", + "EditEntry" + ] + } + } + ] + } + ] + }, + { + "type": "stackpanel", + "orientation": "horizontal", + "verticalAlignment": "stretch", + "horizontalAlignment": "stretch", + "children": [ + { + "type": "stackpanel", + "orientation": "vertical", + "verticalAlignment": "stretch", + "horizontalAlignment": "left", + "children": [ + { + "type": "button", + "name": "castStack.loadProfile", + "content": "Load Profile", + "horizontalAlignment": "left" + }, + { + "type": "combobox", + "name": "castStack.profileList", + "horizontalAlignment": "left", + "itemsBinding": { + "pullFormat": "${CastStackController.ProfileList}", + "autoPull": true, + "pullHook": { + "elementName": "CastStackController.events", + "flags": "global", + "event": "onProfileListChanged" + } + } + } + ] + } + ] + } + ] + } + } +} diff --git a/lgui2/bot_window.json b/lgui2/bot_window.json new file mode 100644 index 0000000..eae3736 --- /dev/null +++ b/lgui2/bot_window.json @@ -0,0 +1,174 @@ +{ + "$schema": "http://www.lavishsoft.com/schema/lgui2Package.json", + "includes": [ + "bot_cast_stack.json" + ], + "skin": { + "name": "MRSkin", + "brushes": { + "window.titleBar.backgroundBrush": { + "color": "#211C18" + } + }, + "templates": { + "window.title": { + "verticalAlignment": "center", + "margin": [ + 2, + 0, + 0, + 0 + ] + }, + "button": { + "jsonTemplate": "default:button", + "margin": [ + 2, + 2, + 2, + 2 + ], + "color": "#f4f3ee" + }, + "checkbox": { + "jsonTemplate": "default:checkbox", + "margin": [ + 2, + 2, + 2, + 2 + ] + }, + "window": { + "jsonTemplate": "default:window", + "backgroundBrush": { + "color": "#463f3a" + }, + "color": "#f4f3ee", + "font": { + "face": "Segoe UI", + "height": 16 + } + }, + "listbox.contentContainerFitWidth": { + "jsonTemplate": "listbox.contentContainer", + "horizontalScroll": "fit" + } + } + }, + "elements": [ + { + "type": "window", + "skin": "MRSkin", + "title": "MR Bot", + "name": "mr.bot.miniwindow", + "borderThickness": 2, + "hideOnClose": false, + "minSize": { + "width": 100, + "height": 50 + }, + "maxSize": { + "width": 150, + "height": 125 + }, + "eventHandlers": { + "onCloseButtonClick": [ + "method", + "BotController", + "OnClose" + ] + }, + "content": { + "type": "stackpanel", + "uniform": true, + "heightFactor": 1, + "children": [ + { + "type": "button", + "content": "${BotController.StartButtonText}", + "horizontalAlignment": "stretch", + "eventHandlers": { + "onRelease": [ + "method", + "BotController", + "ToggleBot" + ] + } + }, + { + "type": "button", + "content": "${BotController.SettingsButtonText}", + "horizontalAlignment": "stretch", + "eventHandlers": { + "onRelease": [ + "method", + "BotController", + "ToggleSettings" + ] + } + } + ] + } + }, + { + "type": "window", + "skin": "MRSkin", + "title": "MR Bot Settings", + "name": "mr.bot.settings", + "borderThickness": 2, + "hideOnClose": true, + "visibility": "hidden", + "minSize": { + "width": 450, + "height": 200 + }, + "maxSize": { + "height": 600, + "width": 800 + }, + "eventHandlers": { + "onCloseButtonClick": [ + "method", + "BotController", + "OnCloseSettings" + ] + }, + "content": { + "type": "tabcontrol", + "heightFactor": 1, + "horizontalAlignment": "stretch", + "verticalAlignment": "stretch", + "tabs": [ + { + "type": "tab", + "header": "Cast Stack", + "name": "mr.bot.settings.castStack", + "content": { + "jsonTemplate": "settings.castStack", + "type": "stackpanel" + } + }, + { + "type": "tab", + "header": "General", + "content": { + "type": "dockpanel", + "_dock": "top", + "padding": 2, + "horizontalAlignment": "stretch", + "children": [ + { + "type": "textblock", + "text": "General Settings", + "horizontalAlignment": "center", + "verticalAlignment": "center" + } + ] + } + } + ] + } + } + ] +} diff --git a/scripts/bot_controller.iss b/scripts/bot_controller.iss new file mode 100644 index 0000000..b619814 --- /dev/null +++ b/scripts/bot_controller.iss @@ -0,0 +1,29 @@ +objectdef MRBotController +{ + variable string test = "just a test" + method Initialize() + { + LGUI2:LoadPackageFile["${LavishScript.HomeDirectory}/scripts/mr/ui/bot_window.json"] + } + + method Shutdown() + { + LGUI2:UnloadPackageFile["${LavishScript.HomeDirectory}/scripts/mr/ui/bot_window.json"] + } + + method OnClose() + { + Event[MRBot_OnCloseButtonClicked]:Execute + } +} + +variable(global) MRBotController BotController +; variable(global) MRSettingsController SettingsController + +function main() +{ + while 1 + { + wait 5 + } +} \ No newline at end of file diff --git a/scripts/cast_stack_controller.iss b/scripts/cast_stack_controller.iss new file mode 100644 index 0000000..9ed34d3 --- /dev/null +++ b/scripts/cast_stack_controller.iss @@ -0,0 +1,25 @@ +objectdef MRCastStackController +{ + method Initialize() + { + } + + method Shutdown() + { + } + + method OnClose() + { + Event[OnCloseButtonClicked]:Execute + } +} + +variable(global) MRCastStackController CastStackController + +function main() +{ + while 1 + { + wait 5 + } +} \ No newline at end of file diff --git a/src/BotSettings/ExportedAbility.h b/src/BotSettings/ExportedAbility.h index 3b33171..40da69e 100644 --- a/src/BotSettings/ExportedAbility.h +++ b/src/BotSettings/ExportedAbility.h @@ -3,15 +3,13 @@ #include #include #include - -//#include -//#include #include -using namespace std; -//using json = nlohmann::json; +#include "ScopedEnumBitwiseOperators.h" -enum AbilityTypeFlags { +using namespace std; + +enum class AbilityTypeFlags { Debuff = 1, Buff = 2, AE = 4, @@ -32,13 +30,16 @@ enum AbilityTypeFlags { Self = 131072, }; -enum AbilityRequirementsFlags { +enum class AbilityRequirementsFlags { NoEpic = 1, Flanking = 2, Stealth = 4, Ranged = 8, }; +JS_ENUM_DECLARE_VALUE_PARSER(AbilityTypeFlags); +JS_ENUM_DECLARE_VALUE_PARSER(AbilityRequirementsFlags); + struct ExportedAbility { unsigned long Id; string Name; @@ -75,8 +76,8 @@ struct ExportedAbility { float MaxRange; unsigned int Damage; vector Effects; - unsigned long TypeFlags; - unsigned short RequirementsFlags; + AbilityTypeFlags TypeFlags; + AbilityRequirementsFlags RequirementsFlags; JS_OBJ(Id, Name, Description, Tier, Level, HealthCost, PowerCost, DissonanceCost, SavageryCost, ConcentrationCost, MainIconID, HOIconID, CastingTime, RecoveryTime, RecastTime, MaxDuration, NumClasses, NumEffects, diff --git a/src/Commands.cpp b/src/Commands.cpp index e8632de..f9d2df6 100644 --- a/src/Commands.cpp +++ b/src/Commands.cpp @@ -4,35 +4,46 @@ #include "ISXMr.h" #include "Logger.h" -#include "Commands/ExportCommand.h" +#include "Tasks/BotTask.h" +#include "Tasks/ExportAbilitiesTask.h" -enum CommandType { - Export, - Test, - NotDefined -}; -CommandType GetCommandType(const std::string &command) { +TaskTypeEnum GetTaskType(const std::string &command) { if (command == "export" || command == "e") { - return CommandType::Export; - } else if (command == "test" || command == "t") { - return CommandType::Test; + return TaskTypeEnum::Export; } - return CommandType::NotDefined; + if (command == "test" || command == "t") { + return TaskTypeEnum::Test; + } + if (command == "bot" || command == "b") { + return TaskTypeEnum::Bot; + } + return TaskTypeEnum::NotDefined; } int CMD_Mr(int argc, char *argv[]) { const argh::parser cmdl(argv); - switch (const auto commandType = GetCommandType(cmdl[1])) { - case CommandType::Export: - executor.AddTask(std::make_shared()); + const auto taskType = GetTaskType(cmdl[1]); + + if (executor.IsTaskTypeRunning(taskType)) { + logw << "Task of type " << cmdl[1] << " is already running" << std::endl; + return 0; + } + + switch (taskType) { + case TaskTypeEnum::Export: + executor.AddTask(std::make_shared()); break; - case CommandType::Test: + case TaskTypeEnum::Bot: + executor.AddTask(BotTask::Instance()); + break; + case TaskTypeEnum::Test: log << LogLevel::Info << "Test command" << std::endl; break; default: - logw << "USAGE: mr [e|export]: Export abilities" << std::endl; + logw << "USAGE: mr [e|export]: Export abilities\n" + << " mr [b|bot]: Run the combat bot" << std::endl; break; } diff --git a/src/Commands/CommandExecutor.h b/src/Commands/CommandExecutor.h deleted file mode 100644 index 3cbe457..0000000 --- a/src/Commands/CommandExecutor.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef COMMANDEXECUTOR_H -#define COMMANDEXECUTOR_H -#include -#include -#include -#include - -#include "ExecutableCommand.h" - - -class CommandExecutor { -public: - CommandExecutor(): stop(false) { - } - - void Shutdown(); - - void AddTask(std::shared_ptr command); - - void RemoveFinishedTasks(); - -private: - struct Task { - std::shared_ptr command; - std::future future; - }; - - std::vector tasks; - mutable std::mutex queueMutex; - std::atomic stop; -}; - - -#endif //COMMANDEXECUTOR_H diff --git a/src/Commands/ExecutableCommand.h b/src/Commands/ExecutableCommand.h deleted file mode 100644 index ba5a616..0000000 --- a/src/Commands/ExecutableCommand.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef BASECOMMAND_H -#define BASECOMMAND_H - -class ExecutableCommand { -public: - ExecutableCommand() : finished(false) { - } - - virtual ~ExecutableCommand() = default; - - virtual void Execute() = 0; - - void RequestStop() { - stopRequested = true; - } - - bool IsStopRequested() const { - return stopRequested; - } - - bool IsFinished() const { - return finished; - } - -protected: - void MarkFinished() { - finished = true; - } - - bool stopRequested = false; - -private: - bool finished; -}; - -#endif //BASECOMMAND_H diff --git a/src/Commands/ExportCommand.h b/src/Commands/ExportCommand.h deleted file mode 100644 index db8677e..0000000 --- a/src/Commands/ExportCommand.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// Created by marob on 12/27/2023. -// - -#ifndef EXPORT_H -#define EXPORT_H -#include "ExecutableCommand.h" - - -class ExportCommand final : public ExecutableCommand { -public: - void Execute() override; -}; - - -#endif //EXPORT_H diff --git a/src/ISXMr.cpp b/src/ISXMr.cpp index c33da88..9bfc032 100644 --- a/src/ISXMr.cpp +++ b/src/ISXMr.cpp @@ -22,7 +22,7 @@ #include "isxeq2/Character.h" #include "../lgui2/test.json.h" #include "../scripts/bot.iss.h" -#include "Commands/CommandExecutor.h" +#include "Tasks/TaskExecutor.h" #pragma comment(lib,"isxdk.lib") // The mandatory pre-setup function. Our name is "ISXMr", and the class is ISXMr. // This sets up a "ModulePath" variable which contains the path to this module in case we want it, @@ -85,23 +85,23 @@ bool ISXMr::Initialize(ISInterface *p_ISInterface) { * Most of the functionality in Initialize is completely optional and could be removed or * changed if so desired. The defaults are simply a suggestion that can be easily followed. */ - constexpr size_t innerspacePathBufferLength = 255; - char innerspacePathBuffer[innerspacePathBufferLength]; - const std::filesystem::path innerspacePath = p_ISInterface->GetInnerSpacePath( - innerspacePathBuffer, innerspacePathBufferLength); - std::filesystem::path fullPath = innerspacePath / R"(scripts\mr\ui\test.json)"; - - if (fullPath.has_parent_path() && !std::filesystem::exists(fullPath.parent_path())) { - std::filesystem::create_directories(fullPath.parent_path()); - } - - std::ofstream file(fullPath, std::ios::binary); - if (!file) { - std::cerr << "Error opening file for writing: " << fullPath << std::endl; - } - - file.write(reinterpret_cast(test_json), test_json_len); - file.close(); + // constexpr size_t innerspacePathBufferLength = 255; + // char innerspacePathBuffer[innerspacePathBufferLength]; + // const std::filesystem::path innerspacePath = p_ISInterface->GetInnerSpacePath( + // innerspacePathBuffer, innerspacePathBufferLength); + // std::filesystem::path fullPath = innerspacePath / R"(scripts\mr\ui\test.json)"; + // + // if (fullPath.has_parent_path() && !std::filesystem::exists(fullPath.parent_path())) { + // std::filesystem::create_directories(fullPath.parent_path()); + // } + // + // std::ofstream file(fullPath, std::ios::binary); + // if (!file) { + // std::cerr << "Error opening file for writing: " << fullPath << std::endl; + // } + // + // file.write(reinterpret_cast(test_json), test_json_len); + // file.close(); //__try // exception handling. See __except below. @@ -141,7 +141,7 @@ bool ISXMr::Initialize(ISInterface *p_ISInterface) { // Register any text triggers built into ISXMr RegisterTriggers(); - pISInterface->RunScriptFromBuffer("mrbot", reinterpret_cast(bot_iss), bot_iss_len); + // pISInterface->RunScriptFromBuffer("mrbot", reinterpret_cast(bot_iss), bot_iss_len); printf("ISXMr version %s Loaded", Mr_Version); return true; } @@ -315,11 +315,11 @@ void __cdecl OnCloseButtonClicked(int argc, char *argv[], PLSOBJECT lsObj) { } void ISXMr::RegisterEvents() { - onGetTargetEventId = pISInterface->RegisterEvent("OnGetTarget"); - pISInterface->AttachEventTarget(onGetTargetEventId, OnGetTargetEvent); - - onCloseButtonClickedEventId = pISInterface->RegisterEvent("OnCloseButtonClicked"); - pISInterface->AttachEventTarget(onCloseButtonClickedEventId, OnCloseButtonClicked); + // onGetTargetEventId = pISInterface->RegisterEvent("OnGetTarget"); + // pISInterface->AttachEventTarget(onGetTargetEventId, OnGetTargetEvent); + // + // onCloseButtonClickedEventId = pISInterface->RegisterEvent("OnCloseButtonClicked"); + // pISInterface->AttachEventTarget(onCloseButtonClickedEventId, OnCloseButtonClicked); } void ISXMr::DisconnectServices() { @@ -382,14 +382,14 @@ void ISXMr::UnRegisterServices() { } void ISXMr::UnRegisterEvents() { - pISInterface->DetachEventTarget(onGetTargetEventId, OnGetTargetEvent); - pISInterface->UnregisterEvent(onGetTargetEventId); - - pISInterface->DetachEventTarget(onCloseButtonClickedEventId, OnCloseButtonClicked); - pISInterface->UnregisterEvent(onCloseButtonClickedEventId); + // pISInterface->DetachEventTarget(onGetTargetEventId, OnGetTargetEvent); + // pISInterface->UnregisterEvent(onGetTargetEventId); + // + // pISInterface->DetachEventTarget(onCloseButtonClickedEventId, OnCloseButtonClicked); + // pISInterface->UnregisterEvent(onCloseButtonClickedEventId); } -CommandExecutor executor; +TaskExecutor executor; std::chrono::milliseconds interval(100);; std::chrono::steady_clock::time_point nextCleanup = std::chrono::steady_clock::now() + interval; diff --git a/src/ISXMr.h b/src/ISXMr.h index a7ef5e2..15ebfe6 100644 --- a/src/ISXMr.h +++ b/src/ISXMr.h @@ -2,7 +2,7 @@ #include #include -#include "Commands/CommandExecutor.h" +#include "Tasks/TaskExecutor.h" class ISXMr : @@ -56,7 +56,7 @@ extern HISXSERVICE hHTTPService; extern HISXSERVICE hTriggerService; extern HISXSERVICE hSystemService; -extern CommandExecutor executor;; +extern TaskExecutor executor;; extern ISXMr *pExtension; #define printf pISInterface->Printf diff --git a/src/Logger.h b/src/Logger.h index 72e6d7d..532a7f0 100644 --- a/src/Logger.h +++ b/src/Logger.h @@ -75,7 +75,7 @@ private: const auto now = std::chrono::system_clock::now(); const auto now_c = std::chrono::system_clock::to_time_t(now); - std::tm now_tm; + std::tm now_tm = {}; localtime_s(&now_tm, &now_c); // Thread-safe on Windows std::ostringstream oss; diff --git a/src/ScopedEnumBitwiseOperators.h b/src/ScopedEnumBitwiseOperators.h new file mode 100644 index 0000000..06bff57 --- /dev/null +++ b/src/ScopedEnumBitwiseOperators.h @@ -0,0 +1,59 @@ + +#ifndef SCOPEDENUMBITWISEOPERATORS_H +#define SCOPEDENUMBITWISEOPERATORS_H +#include + + +template +std::enable_if_t, Enum> +operator^(Enum lhs, Enum rhs) { + using underlying = std::underlying_type_t; + return static_cast(static_cast(lhs) ^ static_cast(rhs)); +} + +template +std::enable_if_t, Enum> +operator|(Enum lhs, Enum rhs) { + using underlying = std::underlying_type_t; + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +template +std::enable_if_t, Enum> +operator&(Enum lhs, Enum rhs) { + using underlying = std::underlying_type_t; + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +template +std::enable_if_t, Enum> +operator~(Enum rhs) { + using underlying = std::underlying_type_t; + return static_cast(~static_cast(rhs)); +} + +template +std::enable_if_t, Enum> +operator|=(Enum &lhs, Enum rhs) { + using underlying = std::underlying_type_t; + lhs = static_cast(static_cast(lhs) | static_cast(rhs)); + return lhs; +} + +template +std::enable_if_t, Enum> +operator&=(Enum &lhs, Enum rhs) { + using underlying = std::underlying_type_t; + lhs = static_cast(static_cast(lhs) & static_cast(rhs)); + return lhs; +} + +template +std::enable_if_t, Enum> +operator^=(Enum &lhs, Enum rhs) { + using underlying = std::underlying_type_t; + lhs = static_cast(static_cast(lhs) ^ static_cast(rhs)); + return lhs; +} + +#endif //SCOPEDENUMBITWISEOPERATORS_H diff --git a/src/Tasks/BotTask.cpp b/src/Tasks/BotTask.cpp new file mode 100644 index 0000000..8f61b36 --- /dev/null +++ b/src/Tasks/BotTask.cpp @@ -0,0 +1,77 @@ +#include "BotTask.h" +#include "ISXMr.h" + +#include +#include + +#include "../../lgui2/UpdateUIPackageFile.h" +#include "../../lgui2/bot_window.json.h" +#include "../../lgui2/bot_cast_stack.json.h" +#include "../../scripts/bot_controller.iss.h" + + +std::weak_ptr BotTask::instance; + +std::shared_ptr BotTask::Instance() { + auto existingInstance = instance.lock(); + if (!existingInstance) { + existingInstance = std::shared_ptr(new BotTask()); + instance = existingInstance; + } + + return existingInstance; +} + +BotTask::BotTask() { + UpdateUIPackageFile( + "bot_window.json", + reinterpret_cast(bot_window_json), + bot_window_json_len, + bot_window_json_last_modified + ); + + UpdateUIPackageFile( + "bot_cast_stack.json", + reinterpret_cast(bot_cast_stack_json), + bot_cast_stack_json_len, + bot_cast_stack_json_last_modified + ); + + pISInterface->RunScriptFromBuffer( + ScriptName.c_str(), + reinterpret_cast(bot_controller_iss), + bot_controller_iss_len + ); + + botController = make_shared(LSObject::FromDataParse("${BotController}")); + this->onCloseButtonClickedEventId = pISInterface->RegisterEvent(OnClosedEventName.c_str()); + pISInterface->AttachEventTarget(this->onCloseButtonClickedEventId, + &BotTask::OnCloseEventHandler); +} + +void __cdecl BotTask::OnCloseEventHandler(int argc, char *argv[], PLSOBJECT plsObject) { + Instance()->Close(); +} + + +BotTask::~BotTask() { + const std::string bufferScriptName = "Buffer:" + ScriptName; + pISInterface->EndScript(bufferScriptName.c_str()); + pISInterface->DetachEventTarget(this->onCloseButtonClickedEventId, &BotTask::OnCloseEventHandler); + pISInterface->UnregisterEvent(this->onCloseButtonClickedEventId); +} + +void BotTask::Execute() { + while (!this->stopRequested && !this->IsFinished()) { + // sleep for 100 ms + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +void BotTask::Close() { + this->MarkFinished(); +} + +TaskTypeEnum BotTask::TaskType() const { + return TaskTypeEnum::Bot; +} diff --git a/src/Tasks/BotTask.h b/src/Tasks/BotTask.h new file mode 100644 index 0000000..5419da4 --- /dev/null +++ b/src/Tasks/BotTask.h @@ -0,0 +1,37 @@ +#ifndef BOTTASK_H +#define BOTTASK_H +#include +#include + +#include "ExecutableTask.h" +#include "isxeq2/LSObject.h" + +class BotTask final : public ExecutableTask, public std::enable_shared_from_this { +public: + static std::shared_ptr Instance(); + + ~BotTask() override; + + void Execute() override; + + void Close(); + + [[nodiscard]] TaskTypeEnum TaskType() const override; + +private: + BotTask(); + + static std::weak_ptr instance; + + const std::string ScriptName = "MRBot"; + const std::string OnClosedEventName = "MRBot_OnCloseButtonClicked"; + + std::shared_ptr botController; + std::shared_ptr settingsController; + + u_int onCloseButtonClickedEventId = 0; + + static void __cdecl OnCloseEventHandler(int argc, char *argv[], PLSOBJECT plsObject); +}; + +#endif //BOTTASK_H diff --git a/src/Tasks/ExecutableTask.h b/src/Tasks/ExecutableTask.h new file mode 100644 index 0000000..7d45e54 --- /dev/null +++ b/src/Tasks/ExecutableTask.h @@ -0,0 +1,46 @@ +#ifndef BASETASK_H +#define BASETASK_H + +enum class TaskTypeEnum { + Export, + Bot, + Test, + NotDefined +}; + +class ExecutableTask { +public: + ExecutableTask() : finished(false) { + } + + virtual ~ExecutableTask() = default; + + virtual void Execute() = 0; + + [[nodiscard]] + virtual TaskTypeEnum TaskType() const = 0; + + void RequestStop() { + stopRequested = true; + } + + [[nodiscard]] bool IsStopRequested() const { + return stopRequested; + } + + [[nodiscard]] bool IsFinished() const { + return finished; + } + +protected: + void MarkFinished() { + finished = true; + } + + bool stopRequested = false; + +private: + bool finished; +}; + +#endif //BASETASK_H diff --git a/src/Commands/ExportCommand.cpp b/src/Tasks/ExportAbilitiesTask.cpp similarity index 97% rename from src/Commands/ExportCommand.cpp rename to src/Tasks/ExportAbilitiesTask.cpp index 24b3a73..397389d 100644 --- a/src/Commands/ExportCommand.cpp +++ b/src/Tasks/ExportAbilitiesTask.cpp @@ -5,7 +5,7 @@ #include #include "ISXMr.h" -#include "ExportCommand.h" +#include "ExportAbilitiesTask.h" #include @@ -23,7 +23,7 @@ AbilityInfo GetAbilityInfo(const int idx) { return ability.GetAbilityInfo(); } -void ExportCommand::Execute() { +void ExportAbilitiesTask::Execute() { try { log << "Exporting abilities" << endl; const auto numAbilities = ExtensionTLOs::Me().NumAbilities(); diff --git a/src/Tasks/ExportAbilitiesTask.h b/src/Tasks/ExportAbilitiesTask.h new file mode 100644 index 0000000..827c3e7 --- /dev/null +++ b/src/Tasks/ExportAbilitiesTask.h @@ -0,0 +1,15 @@ +#ifndef EXPORTABILITIESTASK_H +#define EXPORTABILITIESTASK_H +#include "ExecutableTask.h" + +class ExportAbilitiesTask final : public ExecutableTask { +public: + void Execute() override; + + [[nodiscard]] TaskTypeEnum TaskType() const override { + return TaskTypeEnum::Export; + } +}; + + +#endif //EXPORTABILITIESTASK_H diff --git a/src/Commands/CommandExecutor.cpp b/src/Tasks/TaskExecutor.cpp similarity index 80% rename from src/Commands/CommandExecutor.cpp rename to src/Tasks/TaskExecutor.cpp index 5e2345a..a1f441d 100644 --- a/src/Commands/CommandExecutor.cpp +++ b/src/Tasks/TaskExecutor.cpp @@ -1,9 +1,9 @@ -#include "CommandExecutor.h" +#include "TaskExecutor.h" #include "Logger.h" -void CommandExecutor::Shutdown() { +void TaskExecutor::Shutdown() { stop = true; for (auto &[command, future]: tasks) { if (future.valid()) { @@ -22,7 +22,7 @@ void CommandExecutor::Shutdown() { } -void CommandExecutor::AddTask(std::shared_ptr command) { +void TaskExecutor::AddTask(std::shared_ptr command) { std::lock_guard lock(queueMutex); std::future future = std::async(std::launch::async, [command]() { try { @@ -34,7 +34,15 @@ void CommandExecutor::AddTask(std::shared_ptr command) { tasks.push_back({command, std::move(future)}); } -void CommandExecutor::RemoveFinishedTasks() { +bool TaskExecutor::IsTaskTypeRunning(TaskTypeEnum taskType) const { + std::lock_guard lock(queueMutex); + return std::ranges::any_of(tasks, [taskType](const Task &task) { + return task.task->TaskType() == taskType; + }); +} + + +void TaskExecutor::RemoveFinishedTasks() { std::lock_guard lock(queueMutex); std::erase_if(tasks, [](Task &task) { if (task.future.valid() && task.future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { diff --git a/src/Tasks/TaskExecutor.h b/src/Tasks/TaskExecutor.h new file mode 100644 index 0000000..7fb5cc1 --- /dev/null +++ b/src/Tasks/TaskExecutor.h @@ -0,0 +1,42 @@ +#ifndef TASKEXECUTOR_H +#define TASKEXECUTOR_H +#include +#include +#include +#include + +#include "ExecutableTask.h" + +enum TaskType { + Export, + Bot, + Test, + NotDefined +}; + +class TaskExecutor { +public: + TaskExecutor(): stop(false) { + } + + void Shutdown(); + + void AddTask(std::shared_ptr command); + + bool IsTaskTypeRunning(TaskTypeEnum taskType) const; + + void RemoveFinishedTasks(); + +private: + struct Task { + std::shared_ptr task; + std::future future; + }; + + std::vector tasks; + mutable std::mutex queueMutex; + std::atomic stop; +}; + + +#endif //TASKEXECUTOR_H diff --git a/src/WriteUIFileToDisk.cpp b/src/WriteUIFileToDisk.cpp deleted file mode 100644 index 5363797..0000000 --- a/src/WriteUIFileToDisk.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include -#include -#include -#include - -std::chrono::system_clock::time_point ParseEmbeddedTimestamp(const std::string& timestamp) { - std::tm tm = {}; - std::istringstream ss(timestamp); - ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); - return std::chrono::system_clock::from_time_t(std::mktime(&tm)); -} - -void WriteUIFileToDiskIfChanged( - const std::string& filePath, - const unsigned char* data, - const size_t length, - const std::string& lastModified) { - - auto embeddedTimestamp = ParseEmbeddedTimestamp(lastModified); - - if (std::filesystem::exists(filePath)) { - const auto lastWriteTime = std::filesystem::last_write_time(filePath); - - // Convert file_time_type to system_clock::time_point - const auto lastWriteTimeTp = std::chrono::time_point_cast( - lastWriteTime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now() - ); - - if (lastWriteTimeTp < embeddedTimestamp) { - std::ofstream file(filePath, std::ios::binary); - file.write(reinterpret_cast(data), length); - } - } else { - std::ofstream file(filePath, std::ios::binary); - file.write(reinterpret_cast(data), length); - } -} diff --git a/src/isxeq2/Ability.cpp b/src/isxeq2/Ability.cpp index 3a983d7..a2c71f9 100644 --- a/src/isxeq2/Ability.cpp +++ b/src/isxeq2/Ability.cpp @@ -76,23 +76,23 @@ ExportedAbility Ability::ToExportedAbility() const { } if (RequiresStealth(exportedAbility.Effects)) { - exportedAbility.RequirementsFlags |= Stealth; + exportedAbility.RequirementsFlags |= AbilityRequirementsFlags::Stealth; } if (RequiresFlanking(exportedAbility.Effects)) { - exportedAbility.RequirementsFlags |= Flanking; + exportedAbility.RequirementsFlags |= AbilityRequirementsFlags::Flanking; } if (RequiresNonEpic(exportedAbility.Effects)) { - exportedAbility.RequirementsFlags |= NoEpic; + exportedAbility.RequirementsFlags |= AbilityRequirementsFlags::NoEpic; } if (IsStun(exportedAbility.Effects)) { - exportedAbility.TypeFlags |= Stun; + exportedAbility.TypeFlags |= AbilityTypeFlags::Stun; } if (IsInterrupt(exportedAbility.Effects)) { - exportedAbility.TypeFlags |= Interrupt; + exportedAbility.TypeFlags |= AbilityTypeFlags::Interrupt; } diff --git a/src/isxeq2/LSObject.h b/src/isxeq2/LSObject.h index d1cda49..973b0ca 100644 --- a/src/isxeq2/LSObject.h +++ b/src/isxeq2/LSObject.h @@ -36,7 +36,7 @@ public: // Use a fold expression to convert each argument to a string const std::vector arguments = {toString(args)...}; - const int argc = arguments.size(); + const size_t argc = arguments.size(); std::vector argv; for (auto &arg: arguments) { @@ -49,7 +49,7 @@ public: if (const auto result = this->lsObject->Type->GetMemberEx( this->lsObject->GetObjectData(), const_cast(memberName.c_str()), - argc, + static_cast(argc), argv.data(), response ); !result) { @@ -75,7 +75,7 @@ public: // Use a fold expression to convert each argument to a string const std::vector arguments = {toString(args)...}; - const int argc = arguments.size(); + const size_t argc = arguments.size(); std::vector argv; for (auto &arg: arguments) { @@ -85,14 +85,22 @@ public: this->lsObject->Type->GetMethodEx( this->lsObject->GetObjectData(), const_cast(methodName.c_str()), - argc, + static_cast(argc), argv.data()); } - bool IsValid() const { + [[nodiscard]] bool IsValid() const { return this->lsObject.has_value(); } + static LSObject FromDataParse(const std::string &dataToParse) { + if (LSOBJECT rawObject; pISInterface->DataParse(dataToParse.c_str(), rawObject)) { + return LSObject(rawObject); + } + + return LSObject(nullopt); + } + private: optional lsObject; };