Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Headless broadcast #1105

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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 };

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); };

// 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