Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addons/sys_headless/$PBOPREFIX$
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
idi\acre\addons\sys_headless
17 changes: 17 additions & 0 deletions addons/sys_headless/CfgEventHandlers.hpp
Original file line number Diff line number Diff line change
@@ -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));
};
};
4 changes: 4 additions & 0 deletions addons/sys_headless/XEH_PREP.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PREP(addRadio);
PREP(handleGetHeadlessID);
PREP(start);
PREP(stop);
19 changes: 19 additions & 0 deletions addons/sys_headless/XEH_postInit.sqf
Original file line number Diff line number Diff line change
@@ -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;
};
9 changes: 9 additions & 0 deletions addons/sys_headless/XEH_preInit.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include "script_component.hpp"

ADDON = false;

PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;

ADDON = true;
3 changes: 3 additions & 0 deletions addons/sys_headless/XEH_preStart.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "script_component.hpp"

#include "XEH_PREP.hpp"
16 changes: 16 additions & 0 deletions addons/sys_headless/config.cpp
Original file line number Diff line number Diff line change
@@ -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"
58 changes: 58 additions & 0 deletions addons/sys_headless/fnc_addRadio.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "script_component.hpp"
/*
* Author: ACRE2Team
* Adds a unique radio to a AI unit
*
* Arguments:
* 0: Unit <OBJECT>
* 1: Radio Base class <STRING>
* 2: Preset (optional) <STRING>
*
* Return Value:
* Added <BOOL>
*
* 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;
30 changes: 30 additions & 0 deletions addons/sys_headless/fnc_handleGetHeadlessID.sqf
Original file line number Diff line number Diff line change
@@ -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 <BOOL>
*
* 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];
96 changes: 96 additions & 0 deletions addons/sys_headless/fnc_start.sqf
Original file line number Diff line number Diff line change
@@ -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 <OBJECT>
* 1: TS Display Name (name of a TS source inside the ACRE channel) <STRING>
* 2: Language Index <NUMBER>
* 3: Speaking Type (0 direct, 1 radio,...) <NUMBER>
* 4: Radio ID <STRING>
*
* Return Value:
* Started <BOOL>
*
* 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 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of 'Arma 3' 2.18, the nil check could be simplified:

Suggested change
if (!isNil {_unit getVariable [QGVAR(keepRunning), nil]}) exitWith { ERROR_1("unit is already active",_this); false };
if !(_unit isNil QGVAR(keepRunning)) 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
25 changes: 25 additions & 0 deletions addons/sys_headless/fnc_stop.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "script_component.hpp"
/*
* Author: ACRE2Team
* Stops a unit speaking from a headless TS client
*
* Arguments:
* 0: Unit <OBJECT>
*
* 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); };
Copy link
Contributor

@rautamiekka rautamiekka Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise this could be simplified:

Suggested change
if (isNil {_unit getVariable [QGVAR(keepRunning), nil]}) exitWith { ERROR_1("unit is not active",_this); };
if (_unit isNil QGVAR(keepRunning)) exitWith { ERROR_1("unit is not active",_this); };


// Won't have immediete effects, will shutdown at next PFEH interval
_unit setVariable [QGVAR(keepRunning), false];
9 changes: 9 additions & 0 deletions addons/sys_headless/script_component.hpp
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions extensions/src/ACRE2Core/Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "setSelectableVoiceCurve.h"
#include "setSetting.h"
#include "setTs3ChannelDetails.h"
#include "getHeadlessID.h"
#include <shlobj.h>

acre::Result CEngine::initialize(IClient *client, IServer *externalServer, std::string fromPipeName, std::string toPipeName) {
Expand Down Expand Up @@ -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();
Expand Down
29 changes: 29 additions & 0 deletions extensions/src/ACRE2Core/getHeadlessID.h
Original file line number Diff line number Diff line change
@@ -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<const char *const>(vMessage->getParameter(0))};
const std::string targetName{reinterpret_cast<const char *const>(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;
};
2 changes: 2 additions & 0 deletions extensions/src/ACRE2Shared/IClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
26 changes: 26 additions & 0 deletions extensions/src/ACRE2TS/TS3Client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char, TS3_MAX_SIZE_CLIENT_NICKNAME_NONSDK> 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;
}
9 changes: 9 additions & 0 deletions extensions/src/ACRE2TS/TS3Client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down