diff --git a/addons/sys_headless/$PBOPREFIX$ b/addons/sys_headless/$PBOPREFIX$ new file mode 100644 index 000000000..e65a24e0c --- /dev/null +++ b/addons/sys_headless/$PBOPREFIX$ @@ -0,0 +1 @@ +idi\acre\addons\sys_headless diff --git a/addons/sys_headless/CfgEventHandlers.hpp b/addons/sys_headless/CfgEventHandlers.hpp new file mode 100644 index 000000000..0d3301d6e --- /dev/null +++ b/addons/sys_headless/CfgEventHandlers.hpp @@ -0,0 +1,17 @@ +class Extended_PreStart_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_FILE(XEH_preStart)); + }; +}; + +class Extended_PreInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_FILE(XEH_preInit)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_FILE(XEH_postInit)); + }; +}; diff --git a/addons/sys_headless/XEH_PREP.hpp b/addons/sys_headless/XEH_PREP.hpp new file mode 100644 index 000000000..a6251df70 --- /dev/null +++ b/addons/sys_headless/XEH_PREP.hpp @@ -0,0 +1,4 @@ +PREP(addRadio); +PREP(handleGetHeadlessID); +PREP(start); +PREP(stop); diff --git a/addons/sys_headless/XEH_postInit.sqf b/addons/sys_headless/XEH_postInit.sqf new file mode 100644 index 000000000..17728c027 --- /dev/null +++ b/addons/sys_headless/XEH_postInit.sqf @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +["handleGetHeadlessID", { call FUNC(handleGetHeadlessID) }] call EFUNC(sys_rpc,addProcedure); +[QEGVAR(sys_radio,returnRadioId), { call FUNC(handleReturnRadioId) }] call CBA_fnc_addEventHandler; + +if (hasInterface) then { + [ + {!isNull findDisplay 46}, + { + (findDisplay 46) displayAddEventHandler ["Unload", { + // Cleanup all headless ID's at mission end (or they will keep playing next mission, lol) + { + TRACE_1("RPC STOP: mission end",_x); + ["ext_remoteStopSpeaking", format ["%1,", _x]] call EFUNC(sys_rpc,callRemoteProcedure); + } forEach (missionNamespace getVariable [QGVAR(idsToCleanup), []]) + }]; + } + ] call CBA_fnc_waitUntilAndExecute; +}; diff --git a/addons/sys_headless/XEH_preInit.sqf b/addons/sys_headless/XEH_preInit.sqf new file mode 100644 index 000000000..b47cf6628 --- /dev/null +++ b/addons/sys_headless/XEH_preInit.sqf @@ -0,0 +1,9 @@ +#include "script_component.hpp" + +ADDON = false; + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +ADDON = true; diff --git a/addons/sys_headless/XEH_preStart.sqf b/addons/sys_headless/XEH_preStart.sqf new file mode 100644 index 000000000..022888575 --- /dev/null +++ b/addons/sys_headless/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/addons/sys_headless/config.cpp b/addons/sys_headless/config.cpp new file mode 100644 index 000000000..dcd3e5018 --- /dev/null +++ b/addons/sys_headless/config.cpp @@ -0,0 +1,16 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + name = COMPONENT_NAME; + units[] = {}; + weapons[] = {}; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = {"acre_sys_core"}; + author = ECSTRING(main,Author); + url = ECSTRING(main,URL); + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/addons/sys_headless/fnc_addRadio.sqf b/addons/sys_headless/fnc_addRadio.sqf new file mode 100644 index 000000000..6ed56974a --- /dev/null +++ b/addons/sys_headless/fnc_addRadio.sqf @@ -0,0 +1,58 @@ +#include "script_component.hpp" +/* + * Author: ACRE2Team + * Adds a unique radio to a AI unit + * + * Arguments: + * 0: Unit + * 1: Radio Base class + * 2: Preset (optional) + * + * Return Value: + * Added + * + * Example: + * [q2, "acre_prc343"] call acre_sys_headless_fnc_addRadio + * + * Public: No + */ +[{ + params ["_unit", "_radioBaseClass", ["_preset", "", [""]]]; + TRACE_3("addRadio",_unit,_radioBaseClass,_preset); + + if (!isServer) exitWith { ERROR_1("only use on server - %1",_this); false }; + if (!alive _unit) exitWith { ERROR_1("bad unit - %1",_this); false }; + if (!([_radioBaseClass] call EFUNC(api,isBaseRadio))) exitWith { ERROR_1("bad radio - %1",_this); false }; + if (!([_unit, _radioBaseClass] call CBA_fnc_canAddItem)) exitWith { ERROR_1("cannot add radio - %1",_this); false }; + + GVAR(newUniqueRadio) = ""; // Because this is all running on the server, this gvar will be set by the EH in-line + + private _tempEH = [QEGVAR(sys_radio,returnRadioId), { + params ["_unit", "_class"]; + TRACE_2("returnRadioId tempEH",_unit,_class); + GVAR(newUniqueRadio) = _class; + }] call CBA_fnc_addEventHandler; + + ["acre_getRadioId", [_unit, _radioBaseClass, QEGVAR(sys_radio,returnRadioId)]] call CBA_fnc_serverEvent; + + TRACE_2("after getRadioID",_tempEH,GVAR(newUniqueRadio)); + [QEGVAR(sys_radio,returnRadioId), _tempEH] call CBA_fnc_removeEventHandler; + + if (GVAR(newUniqueRadio) == "") exitWith { + ERROR_1("failed to get radio ID - %1",_this); + false + }; + + _unit addItem GVAR(newUniqueRadio); + + // initialize the new radio + if (_preset == "") then { + _preset = [_radioBaseClass] call EFUNC(sys_data,getRadioPresetName); + TRACE_2("got default preset",_radioBaseClass,_preset); + }; + [GVAR(newUniqueRadio), _preset] call EFUNC(sys_radio,initDefaultRadio); + + ["acre_acknowledgeId", [GVAR(newUniqueRadio), _unit]] call CBA_fnc_serverEvent; + + true +}, _this] call CBA_fnc_directCall; diff --git a/addons/sys_headless/fnc_handleGetHeadlessID.sqf b/addons/sys_headless/fnc_handleGetHeadlessID.sqf new file mode 100644 index 000000000..cf5744ccb --- /dev/null +++ b/addons/sys_headless/fnc_handleGetHeadlessID.sqf @@ -0,0 +1,30 @@ +#include "script_component.hpp" +/* + * Author: ACRE2Team + * Get the alive player units matching the current channel. If global chat, all units are targeted. + * + * Arguments: + * None + * + * Return Value: + * Handled + * + * Example: + * ["1:2", "Botty", 499] call acre_sys_headless_fnc_handleGetHeadlessID + * + * Public: No + */ + +params [["_netId","",[""]],["_targetName","",[""]],["_targetID","0",[""]]]; +TRACE_3("handleGetHeadlessID",_netId,_targetName,_targetID); + +private _unit = objectFromNetId _netId; +if (isNull _unit) exitWith { + WARNING_1("null unit %1",_this); +}; +_targetID = parseNumber _targetID; +if (_targetID == 0) then { + WARNING_2("Cannot find TSID - Headless Unit [%1] DisplayName [%2]",_unit,_tsDisplayName); +}; + +_unit setVariable [QGVAR(virtualID), _targetID]; diff --git a/addons/sys_headless/fnc_start.sqf b/addons/sys_headless/fnc_start.sqf new file mode 100644 index 000000000..e847a1723 --- /dev/null +++ b/addons/sys_headless/fnc_start.sqf @@ -0,0 +1,96 @@ +#include "script_component.hpp" +/* + * Author: ACRE2Team + * Starts a unit speaking from a headless TS client + * Must be called on all clients + * + * Arguments: + * 0: Unit + * 1: TS Display Name (name of a TS source inside the ACRE channel) + * 2: Language Index + * 3: Speaking Type (0 direct, 1 radio,...) + * 4: Radio ID + * + * Return Value: + * Started + * + * Example: + * [q2, "Botty", 0, 0, ""] call acre_sys_headless_fnc_start + * [q2, "Botty", 0, 1, "ACRE_PRC343_ID_2"] call acre_sys_headless_fnc_start + * + * Public: No + */ + +if (!hasInterface) exitWith {}; + +params ["_unit", "_tsDisplayName", "_languageId", "_speakingType", "_radioId"]; +TRACE_4("start",_unit,_tsName,_languageId,_speakingType,_radioId); + +if (!alive _unit) exitWith { ERROR_1("bad unit",_this); false }; +if (!isNil {_unit getVariable [QGVAR(keepRunning), nil]}) exitWith { ERROR_1("unit is already active",_this); false }; + +if (isNil QGVAR(idsToCleanup)) then { GVAR(idsToCleanup) = []; }; + +private _netId = netId _unit; + +// start getting tsId, won't have result until next frame+ +private _lastKnownId = 0; +_unit setVariable [QGVAR(virtualID), _lastKnownId]; +["getHeadlessID", [_netId, _tsDisplayName]] call EFUNC(sys_rpc,callRemoteProcedure); + +_unit setVariable [QGVAR(keepRunning), true]; + +[{ + params ["_args", "_pfid"]; + _args params ["_unit", "_tsDisplayName", "_lastKnownId", "_languageId", "_netId", "_speakingType", "_radioID"]; + private _currentId = _unit getVariable [QGVAR(virtualID), 0]; + private _isSpeaking = [_unit] call EFUNC(api,isSpeaking); + private _keepRunning = (_unit getVariable [QGVAR(keepRunning), false]) && {alive _unit}; + TRACE_5("tick",_unit,_lastKnownId,_currentId,_isSpeaking,_keepRunning); + + if ((_lastKnownId == 0) && {_currentId != 0}) then { + // When we first get a new valid ID, Ensure bot is not currently in plugin's speakingList (just in case it was never cleared) + TRACE_1("RPC STOP: reset on new ID",_currentId); + [ + "ext_remoteStopSpeaking", + format ["%1,", _currentId] + ] call EFUNC(sys_rpc,callRemoteProcedure); + }; + if (_isSpeaking) then { + // Handle client TS closed / pipe error - Need to manually do sqf-stopspeaking to remove from speakers list + if (!EGVAR(sys_io,serverStarted)) then { + TRACE_2("manual sqf stopspeaking: plugin problems",_currentId,_keepRunning); + _currentId = 0; + [str _lastKnownId, _netId] call EFUNC(sys_core,remoteStopSpeaking); + }; + // Normal shutdown or we lost tsID (bot disconnect) + if ((!_keepRunning) || {_currentId == 0}) then { + TRACE_2("RPC STOP: shutdown or bad ID",_keepRunning,_currentId); + [ + "ext_remoteStopSpeaking", + format ["%1,", _lastKnownId] + ] call EFUNC(sys_rpc,callRemoteProcedure); + }; + } else { + if (_keepRunning && {_currentId != 0}) then { + TRACE_1("RPC START: normal",_currentId); + [ + "ext_remoteStartSpeaking", + format ["%1,%2,%3,%4,%5,%6,", _currentId, _languageId, _netId, _speakingType, _radioId, 1] + ] call EFUNC(sys_rpc,callRemoteProcedure); + }; + }; + _args set [2, _currentId]; + if (_currentId !=0) then { GVAR(idsToCleanup) pushBackUnique _currentId; }; + + if (_keepRunning) then { + // Keep checking ID, this handles a bot disconnecting and reconnecting under a different ID + ["getHeadlessID", [_netId, _tsDisplayName]] call EFUNC(sys_rpc,callRemoteProcedure); + } else { + TRACE_1("pfeh stop",_this); + [_pfid] call CBA_fnc_removePerFrameHandler; + _unit setVariable [QGVAR(keepRunning), nil]; + }; +}, 0.5, [_unit, _tsDisplayName, _lastKnownId, _languageId, _netID, _speakingType, _radioId]] call CBA_fnc_addPerFrameHandler; + +true diff --git a/addons/sys_headless/fnc_stop.sqf b/addons/sys_headless/fnc_stop.sqf new file mode 100644 index 000000000..54a2e0e72 --- /dev/null +++ b/addons/sys_headless/fnc_stop.sqf @@ -0,0 +1,25 @@ +#include "script_component.hpp" +/* + * Author: ACRE2Team + * Stops a unit speaking from a headless TS client + * + * Arguments: + * 0: Unit + * + * Return Value: + * None + * + * Example: + * [q2] call acre_sys_headless_fnc_stop + * + * Public: No + */ +if (!hasInterface) exitWith {}; + +params ["_unit"]; +TRACE_1("stop"_unit); + +if (isNil {_unit getVariable [QGVAR(keepRunning), nil]}) exitWith { ERROR_1("unit is not active",_this); }; + +// Won't have immediete effects, will shutdown at next PFEH interval +_unit setVariable [QGVAR(keepRunning), false]; diff --git a/addons/sys_headless/script_component.hpp b/addons/sys_headless/script_component.hpp new file mode 100644 index 000000000..6614af297 --- /dev/null +++ b/addons/sys_headless/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT sys_headless +#define COMPONENT_BEAUTIFIED Headless +#include "\idi\acre\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\idi\acre\addons\main\script_macros.hpp" diff --git a/extensions/src/ACRE2Core/Engine.cpp b/extensions/src/ACRE2Core/Engine.cpp index 34f168426..21e4f7c0c 100644 --- a/extensions/src/ACRE2Core/Engine.cpp +++ b/extensions/src/ACRE2Core/Engine.cpp @@ -29,6 +29,7 @@ #include "setSelectableVoiceCurve.h" #include "setSetting.h" #include "setTs3ChannelDetails.h" +#include "getHeadlessID.h" #include acre::Result CEngine::initialize(IClient *client, IServer *externalServer, std::string fromPipeName, std::string toPipeName) { @@ -84,6 +85,7 @@ acre::Result CEngine::initialize(IClient *client, IServer *externalServer, std:: this->getRpcEngine()->addProcedure(new setSelectableVoiceCurve()); this->getRpcEngine()->addProcedure(new setSetting()); this->getRpcEngine()->addProcedure(new setTs3ChannelDetails()); + this->getRpcEngine()->addProcedure(new getHeadlessID()); // Initialize the client, because it never was derp this->getClient()->initialize(); diff --git a/extensions/src/ACRE2Core/getHeadlessID.h b/extensions/src/ACRE2Core/getHeadlessID.h new file mode 100644 index 000000000..952c32203 --- /dev/null +++ b/extensions/src/ACRE2Core/getHeadlessID.h @@ -0,0 +1,29 @@ +#pragma once +#include "Engine.h" +#include "IRpcFunction.h" +#include "IServer.h" +#include "Log.h" +#include "TextMessage.h" + +RPC_FUNCTION(getHeadlessID) { + const std::string netId{reinterpret_cast(vMessage->getParameter(0))}; + const std::string targetName{reinterpret_cast(vMessage->getParameter(1))}; + + // try to get ID from the displayName by searching the clientList (0 indicates not found) + const acre::id_t targetID = CEngine::getInstance()->getClient()->getClientIDByName(targetName); + + vServer->sendMessage(CTextMessage::formatNewMessage("handleGetHeadlessID", "%s,%s,%d,", netId.c_str(), targetName.c_str(), targetID)); + return acre::Result::ok; +} + +public: +inline void setName(const char *const value) final { + m_Name = value; +} +inline const char *getName() const final { + return m_Name; +} + +protected: +const char *m_Name; +}; diff --git a/extensions/src/ACRE2Shared/IClient.h b/extensions/src/ACRE2Shared/IClient.h index ceecf2418..72b29540b 100644 --- a/extensions/src/ACRE2Shared/IClient.h +++ b/extensions/src/ACRE2Shared/IClient.h @@ -29,6 +29,8 @@ class IClient { virtual acre::Result localStartSpeaking(const acre::Speaking speakingType, const std::string radioId) = 0; virtual acre::Result localStopSpeaking(const acre::Speaking speakingType) = 0; + virtual acre::id_t getClientIDByName(const std::string &targetClientName_) = 0; + virtual std::string getTempFilePath( void ) = 0; virtual std::string getConfigFilePath(void) = 0; diff --git a/extensions/src/ACRE2TS/TS3Client.cpp b/extensions/src/ACRE2TS/TS3Client.cpp index fa8ffb717..895850ca2 100644 --- a/extensions/src/ACRE2TS/TS3Client.cpp +++ b/extensions/src/ACRE2TS/TS3Client.cpp @@ -675,3 +675,29 @@ acre::Result CTS3Client::updateShouldSwitchTS3Channel(const bool state_) { bool CTS3Client::shouldSwitchTS3Channel() { return getShouldSwitchTS3Channel(); } + +acre::id_t CTS3Client::getClientIDByName(const std::string &targetClientName_) { + acre::id_t targetID{0}; // cliendID of 0 is considered invalid and indicates that target was not found + + anyID clientId; + anyID *clientList; + uint32_t res = ts3Functions.getClientID(ts3Functions.getCurrentServerConnectionHandlerID(), &clientId); + if (res == ERROR_ok) { + res = ts3Functions.getClientList(ts3Functions.getCurrentServerConnectionHandlerID(), &clientList); + if (res == ERROR_ok) { + std::array clientName{""}; + size_t index = 0; + while ((targetID == 0) && (clientList[index] != 0)) { // "null terminated array of client ids like {10, 30, …, 0}" + res = ts3Functions.getClientDisplayName(ts3Functions.getCurrentServerConnectionHandlerID(), clientList[index], clientName.data(), clientName.size()); + if (res == ERROR_ok) { + if (targetClientName_ == std::string(clientName.data())) { + targetID = clientList[index]; + } + } + index++; + } + ts3Functions.freeMemory(clientList); // "caller must free the array" + } + } + return targetID; +} diff --git a/extensions/src/ACRE2TS/TS3Client.h b/extensions/src/ACRE2TS/TS3Client.h index 5a2cce9c2..898644497 100644 --- a/extensions/src/ACRE2TS/TS3Client.h +++ b/extensions/src/ACRE2TS/TS3Client.h @@ -60,6 +60,15 @@ class CTS3Client: public IClient { */ acre::Result localStopSpeaking(const acre::Speaking speakingType_ ); + /*! + * \brief Gets client id of a headless client + * + * \param[in] targetClientName_ Target's DisplayName in TS + * + * \return acre::id_t + */ + acre::id_t getClientIDByName(const std::string &targetClientName_); + std::string getTempFilePath( void ); std::string getConfigFilePath(void);