mirror of
https://github.com/DaKerboul/perun.git
synced 2026-08-09 17:45:38 +02:00
Compare commits
6 Commits
master
...
event_rewo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d02b6d4e78 | ||
|
|
1bc0f142bd | ||
|
|
545298f26d | ||
|
|
0a0da5b5a9 | ||
|
|
957dc393bd | ||
|
|
b87276cfe3 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -261,4 +261,6 @@ __pycache__/
|
||||
*.pyc
|
||||
|
||||
# cmake folders
|
||||
cmake-build-debug
|
||||
cmake-build-debug
|
||||
cmake-build-release
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
-- Perun for DCS World https://github.com/szporwolik/perun -> DCS Hook config component
|
||||
local PerunConfig = {}
|
||||
|
||||
-- ###################### SETTINGS - DO NOT MODIFY OUTSIDE THIS SECTION #############################
|
||||
-- Connection
|
||||
PerunConfig.TCPPerunHost = "localhost" -- (string) [default: "localhost"] IP adress of the Perun instance or "localhost"
|
||||
PerunConfig.TCPTargetPort = 48621 -- (int) [default: 48621] TCP port to send data to
|
||||
PerunConfig.Instance = 1 -- (int) [default: 1] Id number of instance (if multiple DCS instances are to run at the same PC)
|
||||
PerunConfig.RefreshStatus = 60 -- (int) [default: 60] Base refresh rate in seconds to send status update
|
||||
|
||||
-- Mission Configuration
|
||||
PerunConfig.MissionStartNoDeathWindow = 300 -- (int) [default: 300] Number of secounds after mission start when death of the pilot will not go to statistics, shall avoid death penalty during spawning DCS bugs
|
||||
|
||||
-- Localisation
|
||||
PerunConfig.MOTD_L1 = "[Perun] Welcome to our server !" -- (string) Message send to players connecting the server - Line 1
|
||||
PerunConfig.MOTD_L2 = "[Perun] Stats and event data integrated with Perun for DCS World" -- (string) Message send to players connecting the server - Line 2
|
||||
PerunConfig.ConnectionError_L1 = "[Perun] ERROR: Connection broken - contact server admin!" -- (string) Information to send to players when Perun connection is broken
|
||||
|
||||
-- Misc
|
||||
PerunConfig.BroadcastPerunErrors = 1 -- (int) [0 (default),1] Value greater than 0 will broadcast chat message about missing connection to Perun
|
||||
PerunConfig.RecordChatMessages = 1
|
||||
|
||||
-- Debug
|
||||
PerunConfig.DebugMode = 1 -- (int) [0 (default),1,2] Value greater than 0 will display Perun information in DCS log file, values: 1 - minimal verbose, 2 - all log information will be logged
|
||||
|
||||
-- ###################### END OF SETTINGS - DO NOT MODIFY OUTSIDE THIS SECTION ######################
|
||||
return PerunConfig
|
||||
33
01_DCS/Mods/services/Perun/lua/pierog_config.lua
Normal file
33
01_DCS/Mods/services/Perun/lua/pierog_config.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Perun for DCS World https://github.com/szporwolik/perun -> DCS Hook config component
|
||||
local config = {}
|
||||
|
||||
-- ###################### SETTINGS - DO NOT MODIFY OUTSIDE THIS SECTION #############################
|
||||
-- Connection
|
||||
config.host = "localhost" -- (string) [default: "localhost"] IP adress of the Perun instance or "localhost"
|
||||
config.port = 48621 -- (int) [default: 48621] TCP port to send data to
|
||||
config.delimiters = {"<SOT>", "<EOT>"}
|
||||
config.Instance = 1 -- (int) [default: 1] Id number of instance (if multiple DCS instances run on the same machine)
|
||||
config.keepAliveSeconds = 5 --
|
||||
config.logPath = "c:\\temp" -- path for communication logs
|
||||
|
||||
config.bigUpdatesSeconds = 100 -- big updates (mission, unit IDs, slots) have to wait between being sent
|
||||
-- deprecated, decrease greatly
|
||||
config.refreshSlotsSeconds = 1200 -- delay between slot updates are sent
|
||||
config.refreshMissionSeconds = 1200 -- delay between mission update is sent
|
||||
|
||||
-- Localisation
|
||||
-- Server MOTD, sent to ALL players when entering cockpit
|
||||
config.MOTD_line1 = "[Pierog] Welcome to our server!";
|
||||
config.MOTD_line2 = "[Pierog] Stats and event data integrated with Pierog for DCS World";
|
||||
-- (string) Information to send to players when Perun connection is broken
|
||||
config.ConnectionError = "[Pierog] ERROR: Connection broken - contact server admin!"
|
||||
|
||||
-- Misc
|
||||
config.BroadcastPierogErrors = 1 -- (int) [0 (default),1] Value greater than 0 will broadcast chat message about missing connection to Perun
|
||||
config.BroadcastErrorEachSeconds = 600
|
||||
|
||||
-- Debug
|
||||
config.DebugMode = 2 -- (int) [0 (default),1,2] Value greater than 0 will display Perun information in DCS log file, values: 1 - minimal verbose, 2 - all log information will be logged
|
||||
|
||||
-- ###################### END OF SETTINGS - DO NOT MODIFY OUTSIDE THIS SECTION ######################
|
||||
return config
|
||||
@@ -13,18 +13,18 @@ package.cpath = package.cpath..';'.. lfs.writedir()..'/Mods/services/Perun/bin/'
|
||||
Perun.DLL = require('perun')
|
||||
|
||||
-- Load config file
|
||||
local PerunConfig = require "perun_config"
|
||||
Perun.RefreshStatus = PerunConfig.RefreshStatus
|
||||
Perun.TCPTargetPort = PerunConfig.TCPTargetPort
|
||||
Perun.TCPPerunHost = PerunConfig.TCPPerunHost
|
||||
Perun.Instance = PerunConfig.Instance
|
||||
Perun.MissionStartNoDeathWindow = PerunConfig.MissionStartNoDeathWindow
|
||||
Perun.DebugMode = PerunConfig.DebugMode
|
||||
Perun.MOTD_L1 = PerunConfig.MOTD_L1
|
||||
Perun.MOTD_L2 = PerunConfig.MOTD_L2
|
||||
Perun.ConnectionError = PerunConfig.ConnectionError_L1
|
||||
Perun.BroadcastPerunErrors = PerunConfig.BroadcastPerunErrors
|
||||
Perun.RecordChatMessages = PerunConfig.RecordChatMessages
|
||||
local config = require "perun_config"
|
||||
|
||||
Perun.RefreshStatus = config.RefreshStatus
|
||||
Perun.TCPTargetPort = config.port
|
||||
Perun.TCPPerunHost = config.host
|
||||
Perun.Instance = config.Instance
|
||||
Perun.MissionStartNoDeathWindow = config.MissionStartNoDeathWindow
|
||||
Perun.DebugMode = config.DebugMode
|
||||
Perun.MOTD_L1 = config.MOTD_L1
|
||||
Perun.MOTD_L2 = config.MOTD_L2
|
||||
Perun.ConnectionError = config.ConnectionError_L1
|
||||
Perun.BroadcastPerunErrors = config.BroadcastPerunErrors
|
||||
|
||||
-- Variable init
|
||||
Perun.Version = "v0.12.1"
|
||||
@@ -48,13 +48,22 @@ Perun.lastFrameTime = 0;
|
||||
Perun.ReconnectTimeout = 30;
|
||||
Perun.RefreshKeepAlive = 3
|
||||
|
||||
Perun.PTS = function(data) -- ProtectedToString
|
||||
if data == nil then
|
||||
return "[nil]"
|
||||
else
|
||||
return tostring(data)
|
||||
end
|
||||
end
|
||||
Perun.State = {}
|
||||
Perun.State.connected = false;
|
||||
|
||||
-- to avoid declaring every time a function is run
|
||||
Perun.Globals = {}
|
||||
Perun.Globals.Sides = {
|
||||
[0] = 'SPECTATOR',
|
||||
[1] = 'RED',
|
||||
[2] = 'BLUE',
|
||||
[3] = 'NEUTRAL', -- TBD check once this is released in DCS
|
||||
}
|
||||
|
||||
Perun.Mission = {}
|
||||
Perun.Mission.Coalitions = {}
|
||||
|
||||
|
||||
|
||||
-- ################################ Helper function definitions ################################
|
||||
Perun.GetCategory = function(id)
|
||||
@@ -84,14 +93,8 @@ end
|
||||
|
||||
Perun.SideID2Name = function(id)
|
||||
-- Helper function returns side name per side (coalition) id
|
||||
local _sides = {
|
||||
[0] = 'SPECTATOR',
|
||||
[1] = 'RED',
|
||||
[2] = 'BLUE',
|
||||
[3] = 'NEUTRAL', -- TBD check once this is released in DCS
|
||||
}
|
||||
if id and id > 0 and id <= 3 then
|
||||
return _sides[id]
|
||||
if id > 0 and id <= 3 then
|
||||
return Perun.Globals.Sides[id]
|
||||
else
|
||||
return "?"
|
||||
end
|
||||
@@ -100,11 +103,11 @@ end
|
||||
Perun.AddLog = function(text,LogLevel)
|
||||
-- Adds logs to DCS.log file
|
||||
if Perun.DebugMode >= LogLevel then
|
||||
net.log("[Perun] ".. text)
|
||||
net.log("[Perun] ", text)
|
||||
end
|
||||
end
|
||||
|
||||
Perun.GenerateMissionHash = function()
|
||||
Perun.generate_mission_hash = function()
|
||||
-- Generates unique simulation mission hash
|
||||
return DCS.getMissionName( ).."@".. Perun.Instance .. "@" .. Perun.Version .. "@".. os.date('%Y%m%d_%H%M%S')
|
||||
end
|
||||
@@ -248,6 +251,7 @@ Perun.ConnectToPerun = function ()
|
||||
Perun.DLL.tcpConnect(Perun.TCPPerunHost, Perun.TCPTargetPort)
|
||||
|
||||
Perun.AddLog(string.format("Connecting to TCP server %s:%i", Perun.TCPPerunHost, Perun.TCPTargetPort), 2)
|
||||
Perun.State.connected = true;
|
||||
end
|
||||
|
||||
Perun.SendToPerun = function(data_id, data_package)
|
||||
@@ -262,7 +266,7 @@ Perun.SendToPerun = function(data_id, data_package)
|
||||
_payload = net.lua2json(data_package);
|
||||
end
|
||||
|
||||
local _TempData={"<SOT>","{\"dcs_current_frame_delay\":",((DCS.getRealTime() - Perun.lastFrameStart) * 1000000),",\"type\":",data_id,",\"dcs_frame_time\":",(Perun.lastFrameTime * 1000000),",\"instance\":",(Perun.Instance),",\"timestamp\":\"",(os.date('%Y-%m-%d %H:%M:%S')),"\",\"payload\":",(_payload),"}<EOT>"}
|
||||
local _TempData={"<SOT>","{\"dcs_current_frame_delay\":",((DCS.getRealTime() - Perun.lastFrameStart) * 1000),",\"type\":",data_id,",\"dcs_frame_time\":",(Perun.lastFrameTime * 1000),",\"instance\":",(Perun.Instance),",\"timestamp\":\"",(os.date('%Y-%m-%d %H:%M:%S')),"\",\"payload\":",(_payload),"}<EOT>"}
|
||||
local _tcpMessage= table.concat( _TempData )
|
||||
|
||||
-- TCP Part - sending
|
||||
@@ -629,8 +633,8 @@ end
|
||||
|
||||
Perun.onSimulationStart = function()
|
||||
-- Simulation was started
|
||||
Perun.MissionHash=Perun.GenerateMissionHash()
|
||||
Perun.LogEvent("SimStart","Mission " .. Perun.PTS(Perun.MissionHash) .. " started",nil,nil);
|
||||
Perun.MissionHash=Perun.generate_mission_hash()
|
||||
Perun.LogEvent("SimStart","Mission " .. Perun.MissionHash .. " started",nil,nil);
|
||||
Perun.StatData = {}
|
||||
Perun.StatDataLastType = {}
|
||||
Perun.PlayersTableCache = {}
|
||||
@@ -641,9 +645,9 @@ end
|
||||
|
||||
Perun.onSimulationStop = function()
|
||||
-- Simulation was stopped
|
||||
Perun.LogEvent("SimStop","Mission " .. Perun.PTS(Perun.MissionHash) .. " finished",nil,nil);
|
||||
Perun.LogEvent("SimStop","Mission " .. Perun.MissionHash .. " finished",nil,nil);
|
||||
Perun.LogAllStats()
|
||||
Perun.MissionHash=Perun.GenerateMissionHash();
|
||||
Perun.MissionHash=Perun.generate_mission_hash();
|
||||
Perun.StatData = {}
|
||||
Perun.MissionData = {}
|
||||
Perun.StatDataLastType = {}
|
||||
@@ -653,14 +657,14 @@ end
|
||||
|
||||
Perun.onPlayerDisconnect = function(id, err_code)
|
||||
-- Player disconnected
|
||||
Perun.LogEvent("disconnect", "Player " .. Perun.PTS(id) .. " disconnected.",nil,nil);
|
||||
Perun.LogEvent("disconnect", "Player " .. id .. " disconnected.",nil,nil);
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
Perun.onPlayerStop = function (id)
|
||||
-- Player left the simulation (happens right before a disconnect, if player exited by desire)
|
||||
Perun.LogEvent("quit", "Player " .. Perun.PTS(id) .. " quit the server.",nil,nil);
|
||||
Perun.LogEvent("quit", "Player " .. id .. " quit the server.",nil,nil);
|
||||
return
|
||||
end
|
||||
|
||||
@@ -715,7 +719,7 @@ end
|
||||
|
||||
Perun.onPlayerTrySendChat = function (playerID, msg, all)
|
||||
-- Somebody tries to send chat message
|
||||
if Perun.RecordChatMessages and msg~=Perun.MOTD_L1 and msg~=Perun.MOTD_L2 and msg~=Perun.ConnectionError then
|
||||
if msg~=Perun.MOTD_L1 and msg~=Perun.MOTD_L2 and msg~=Perun.ConnectionError then
|
||||
Perun.LogChat(playerID,msg,all)
|
||||
end
|
||||
|
||||
@@ -725,6 +729,24 @@ end
|
||||
Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
-- Game event has occured
|
||||
local _now = DCS.getRealTime()
|
||||
|
||||
local _payload = net.lua2json({eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7})
|
||||
Perun.DLL.tcpSend("<SOT>" .. _payload .. "<EOT>")
|
||||
|
||||
for i, value in pairs({ "red", "blue" }) do
|
||||
-- table { type, unit_missionID }
|
||||
local slots = DCS.getAvailableSlots(value)
|
||||
for slot in slots do
|
||||
local idx = slot[1];
|
||||
Perun.Mission.Coalitions[value][idx] = {}
|
||||
Perun.Mission.Coalitions[value][idx]["type"] = slot[2];
|
||||
Perun.DLL.tcpSend("<SOT>" .. net.lua2json(DCS.getUnitType(idx)) .. "<EOT>")
|
||||
end
|
||||
end
|
||||
|
||||
local _payload = net.lua2json(Perun.Mission.Coalitions)
|
||||
Perun.DLL.tcpSend("<SOT> SLOTS " .. _payload .. "<EOT>")
|
||||
|
||||
Perun.AddLog("Event handler for ".. eventName .. " started",2)
|
||||
|
||||
if eventName == "friendly_fire" then
|
||||
@@ -733,11 +755,11 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
arg2 = "Cannon"
|
||||
end
|
||||
|
||||
Perun.LogEvent(Perun.PTS(eventName),Perun.SideID2Name( Perun.PTS(net.get_player_info(arg1, "side"))) .. " player(s) " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg1)) .." killed friendly " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg3)) .. " using " .. Perun.PTS(arg2),nil,nil);
|
||||
Perun.LogEvent(eventName,Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .." killed friendly " .. Perun.GetMulticrewCrewNames(arg3) .. " using " .. arg2,nil,nil);
|
||||
|
||||
elseif eventName == "mission_end" then
|
||||
--"mission_end", winner, msg
|
||||
Perun.LogEvent(Perun.PTS(eventName),"Mission finished, winner " .. Perun.PTS(arg1) .. " message: " .. Perun.PTS(arg2),nil,nil);
|
||||
Perun.LogEvent(eventName,"Mission finished, winner " .. arg1 .. " message: " .. arg2,nil,nil);
|
||||
|
||||
elseif eventName == "kill" then
|
||||
--"kill", killerPlayerID, killerUnitType, killerSide, victimPlayerID, victimUnitType, victimSide, weaponName
|
||||
@@ -800,12 +822,12 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
victim_vehicle = "?"
|
||||
end
|
||||
|
||||
Perun.LogEvent(Perun.PTS(eventName),Perun.PTS(Perun.SideID2Name(arg3)) .. _temp_killers .. " in " .. Perun.PTS(arg2) .. " killed " .. Perun.PTS(Perun.SideID2Name(arg6)) .. Perun.PTS(_temp_victims) .. " in " .. Perun.PTS(victim_vehicle) .. " using " .. Perun.PTS(arg7) .. " [".. Perun.PTS(Perun.GetCategory(arg5)).."]",arg7,Perun.GetCategory(arg5));
|
||||
Perun.LogEvent(eventName,Perun.SideID2Name(arg3) .. _temp_killers .. " in " .. arg2 .. " killed " .. Perun.SideID2Name(arg6) .. _temp_victims .. " in " .. victim_vehicle .. " using " .. arg7 .. " [".. Perun.GetCategory(arg5).."]",arg7,Perun.GetCategory(arg5));
|
||||
|
||||
elseif eventName == "self_kill" then
|
||||
--"self_kill", playerID
|
||||
Perun.LogStats(arg1);
|
||||
Perun.LogEvent(Perun.PTS(eventName),Perun.PTS(net.get_player_info(arg1, "name")) .. " killed himself",nil,nil);
|
||||
Perun.LogEvent(eventName,net.get_player_info(arg1, "name") .. " killed himself",nil,nil);
|
||||
|
||||
elseif eventName == "change_slot" then
|
||||
--"change_slot", playerID, slotID, prevSide
|
||||
@@ -816,7 +838,7 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
else
|
||||
_sub_slot =" (" .. _sub_slot .. ") "
|
||||
end
|
||||
Perun.LogEvent(Perun.PTS(eventName),Perun.PTS(Perun.SideID2Name( net.get_player_info(arg1, "side"))) .. " player " .. Perun.PTS(net.get_player_info(arg1, "name")) .. " changed slot to " .. Perun.PTS(_master_type) .. " " .. Perun.PTS(_sub_slot),nil,nil);
|
||||
Perun.LogEvent(eventName,Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player " .. net.get_player_info(arg1, "name") .. " changed slot to " .. _master_type .. " " .. _sub_slot,nil,nil);
|
||||
|
||||
Perun.LogStats(arg1);
|
||||
Perun.LogStatsCount(arg1,"init")
|
||||
@@ -825,23 +847,23 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
elseif eventName == "connect" then
|
||||
--"connect", playerID, name
|
||||
Perun.LogLogin(arg1);
|
||||
Perun.LogEvent(Perun.PTS(eventName),"Player "..Perun.PTS(net.get_player_info(arg1, "name")) .. " connected",nil,nil);
|
||||
Perun.LogEvent(eventName,"Player "..net.get_player_info(arg1, "name") .. " connected",nil,nil);
|
||||
Perun.PlayersTableCache["p"..arg1]=net.get_player_info(arg1);
|
||||
|
||||
elseif eventName == "disconnect" then
|
||||
--"disconnect", playerID, name, playerSide, reason_code
|
||||
Perun.LogEvent(Perun.PTS(eventName),"Player " .. Perun.PTS(arg2) .. " disconnected (".. Perun.PTS(arg4) .. ")." ,arg4,nil);
|
||||
Perun.LogEvent(eventName,"Player " .. arg2 .. " disconnected (".. arg4 .. ")." ,arg4,nil);
|
||||
Perun.LogStats(arg1);
|
||||
|
||||
elseif eventName == "crash" then
|
||||
--"crash", playerID, unit_missionID
|
||||
Perun.LogStatsCountCrew (arg1,"crash")
|
||||
Perun.LogEvent(Perun.PTS(eventName), Perun.PTS(Perun.SideID2Name( net.get_player_info(arg1, "side"))) .. " player(s) " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg1)) .. " crashed in " .. Perun.PTS(DCS.getUnitType(arg2)),nil,nil);
|
||||
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " crashed in " .. DCS.getUnitType(arg2),nil,nil);
|
||||
|
||||
elseif eventName == "eject" then
|
||||
--"eject", playerID, unit_missionID
|
||||
Perun.LogStatsCountCrew (arg1,"eject") -- TBD crew or initiator only?
|
||||
Perun.LogEvent(Perun.PTS(eventName), Perun.PTS(Perun.SideID2Name( net.get_player_info(arg1, "side"))) .. " player(s) " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg1)) .. " ejected " .. Perun.PTS(DCS.getUnitType(arg2)),nil,nil);
|
||||
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " ejected " .. DCS.getUnitType(arg2),nil,nil);
|
||||
|
||||
elseif eventName == "takeoff" then
|
||||
--"takeoff", playerID, unit_missionID, airdromeName
|
||||
@@ -852,7 +874,7 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
end
|
||||
|
||||
Perun.LogStatsCountCrew (arg1,Perun.GetTakeOffLandingEvent(true,arg3))
|
||||
Perun.LogEvent(Perun.PTS(eventName), Perun.PTS(Perun.SideID2Name( net.get_player_info(arg1, "side"))) .. " player(s) " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg1)) .. " took off in ".. Perun.PTS(DCS.getUnitType(arg2)) .. Perun.PTS(_temp_airfield),arg3,nil);
|
||||
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " took off in ".. DCS.getUnitType(arg2) .. _temp_airfield,arg3,nil);
|
||||
|
||||
elseif eventName == "landing" then
|
||||
--"landing", playerID, unit_missionID, airdromeName
|
||||
@@ -863,15 +885,15 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
|
||||
end
|
||||
|
||||
Perun.LogStatsCountCrew (arg1,Perun.GetTakeOffLandingEvent(false,arg3))
|
||||
Perun.LogEvent(Perun.PTS(eventName), Perun.PTS(Perun.SideID2Name( net.get_player_info(arg1, "side"))) .. " player(s) " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg1)) .. " landed in " .. Perun.PTS(DCS.getUnitType(arg2)).. Perun.PTS(_temp_airfield),arg3,nil);
|
||||
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " landed in " .. DCS.getUnitType(arg2).. _temp_airfield,arg3,nil);
|
||||
|
||||
elseif eventName == "pilot_death" then
|
||||
--"pilot_death", playerID, unit_missionID
|
||||
Perun.LogStatsCountCrew (arg1,"pilot_death") -- TBD crew or initiator only?
|
||||
Perun.LogEvent(Perun.PTS(eventName), Perun.PTS(Perun.SideID2Name( net.get_player_info(arg1, "side"))) .. " player(s) " .. Perun.PTS(Perun.GetMulticrewCrewNames(arg1)) .. " in " .. Perun.PTS(DCS.getUnitType(arg2)) .. " died",nil,nil);
|
||||
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " in " .. DCS.getUnitType(arg2) .. " died",nil,nil);
|
||||
|
||||
else
|
||||
Perun.LogEvent(Perun.PTS(eventName),"Unknown event type",nil,nil);
|
||||
Perun.LogEvent(eventName,"Unknown event type",nil,nil);
|
||||
|
||||
end
|
||||
local _delay = (DCS.getRealTime() - _now) * 1000000
|
||||
@@ -882,7 +904,7 @@ end
|
||||
if DCS.isServer() then
|
||||
-- If this game instance is hosting multiplayer game, start Perun
|
||||
Perun.DLL.StartOfApp() -- Start the main Perun dll
|
||||
Perun.MissionHash=Perun.GenerateMissionHash() -- Generate initial missionhash
|
||||
Perun.MissionHash=Perun.generate_mission_hash() -- Generate initial missionhash
|
||||
DCS.setUserCallbacks(Perun) -- Set user callbacs, map DCS event handlers with functions defined above
|
||||
Perun.AddLog("Loaded - Perun for DCS World - version: " .. Perun.Version,0) -- Display perun information in log
|
||||
Perun.ConnectToPerun() -- Connect to Perun server
|
||||
|
||||
99
01_DCS/Scripts/Hooks/lifecycle.lua
Normal file
99
01_DCS/Scripts/Hooks/lifecycle.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
local explorer = {}
|
||||
|
||||
|
||||
explorer.onMissionLoadBegin = function()
|
||||
net.log("[Explorer]", "on mission load begin");
|
||||
end
|
||||
explorer.onMissionLoadProgress = function(progress, message)
|
||||
net.log("[Explorer]", "on mission load progress", progress, message);
|
||||
end
|
||||
explorer.onMissionLoadEnd = function()
|
||||
net.log("[Explorer]", "on mission load end");
|
||||
end
|
||||
explorer.onSimulationStart = function()
|
||||
net.log("[Explorer]", "on simulation start");
|
||||
end
|
||||
explorer.onSimulationStop = function()
|
||||
net.log("[Explorer]", "on simulation stop");
|
||||
end
|
||||
explorer.onSimulationFrame = function() end
|
||||
explorer.onSimulationPause= function()
|
||||
net.log("[Explorer]", "on simulation pause");
|
||||
end
|
||||
explorer.onSimulationResume= function()
|
||||
net.log("[Explorer]", "on simulation resume");
|
||||
end
|
||||
|
||||
explorer.onNetConnect= function()
|
||||
et.log("[Explorer]", "onNetConnect");
|
||||
end
|
||||
explorer.onNetMissionChanged= function()
|
||||
net.log("[Explorer]", "onNetMissionChanged");
|
||||
end
|
||||
explorer.onNetConnect= function()
|
||||
net.log("[Explorer]", "onNetConnect");
|
||||
end
|
||||
explorer.onNetDisconnect= function()
|
||||
net.log("[Explorer]", "onNetDisconnect");
|
||||
end
|
||||
explorer.onPlayerConnect= function()
|
||||
net.log("[Explorer]", "onPlayerConnect");
|
||||
end
|
||||
explorer.onPlayerDisconnect= function()
|
||||
net.log("[Explorer]", "onPlayerDisconnect");
|
||||
end
|
||||
explorer.onPlayerStart= function()
|
||||
net.log("[Explorer]", "onPlayerStart");
|
||||
end
|
||||
explorer.onPlayerStop= function()
|
||||
net.log("[Explorer]", "onPlayerStop");
|
||||
end
|
||||
explorer.onPlayerChangeSlot= function()
|
||||
net.log("[Explorer]", "onPlayerChangeSlot");
|
||||
end
|
||||
explorer.onPlayerTryConnect= function(address, name, ucid, id)
|
||||
net.log("[Explorer]", "onPlayerTryConnect", address, name, ucid, id);
|
||||
end
|
||||
explorer.onPlayerTrySendChat= function()
|
||||
net.log("[Explorer]", "onPlayerTrySendChat");
|
||||
end
|
||||
explorer.onPlayerTryChangeSlot= function()
|
||||
net.log("[Explorer]", "onPlayerTryChangeSlot");
|
||||
end
|
||||
|
||||
explorer.onGameEvent = function(eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
|
||||
|
||||
local payload = {}
|
||||
if(argCount == 1) then
|
||||
payload = handler(arg1);
|
||||
elseif argCount == 2 then
|
||||
payload = handler(arg1, arg2);
|
||||
elseif argCount == 3 then
|
||||
payload = handler(arg1, arg2, arg3);
|
||||
elseif argCount == 4 then
|
||||
payload = handler(arg1, arg2, arg3, arg4);
|
||||
elseif argCount == 7 then
|
||||
payload = handler(arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
||||
end
|
||||
|
||||
local json = net.lua2json(payload);
|
||||
net.log("[Explorer]", json);
|
||||
|
||||
--net.log("[Pierog] json", json)
|
||||
|
||||
--net.log("[Pierog]", "get_player_list", net.lua2json(net.get_player_list()))
|
||||
--net.log("[Pierog]", "mission_data", net.lua2json(DCS.getCurrentMission()))
|
||||
|
||||
--net.log("[Pierog]", "DCS.getAvailableCoalitions()", net.lua2json(DCS.getAvailableCoalitions()))
|
||||
--net.log("[Pierog]", "DCS.getAvailableSlots(0)", DCS.getAvailableSlots(0))
|
||||
--net.log("[Pierog]", "DCS.getAvailableSlots(3)", DCS.getAvailableSlots(3))
|
||||
--net.log("[Pierog]", "DCS.getMissionName()", DCS.getMissionName())
|
||||
--net.log("[Pierog]", "DCS.getModelTime()", DCS.getModelTime())
|
||||
--net.log("[Pierog]", "DCS.getRealTime()", DCS.getRealTime())
|
||||
end
|
||||
|
||||
|
||||
if DCS.isServer() then
|
||||
net.log("[Explorer]", "Initialising API explorer");
|
||||
DCS.setUserCallbacks(explorer);
|
||||
end
|
||||
497
01_DCS/Scripts/Hooks/pierog_light_hook.lua
Normal file
497
01_DCS/Scripts/Hooks/pierog_light_hook.lua
Normal file
@@ -0,0 +1,497 @@
|
||||
-- Pierog for DCS World (evolution of Perun, name needs deconfliction for parallel runs)
|
||||
-- https://github.com/szporwolik/perun -> DCS Hook component
|
||||
net.log("[Pierog] Loading Pierog")
|
||||
|
||||
-- Initial init
|
||||
local Pierog = {}
|
||||
|
||||
-- Load Luas
|
||||
package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua"..";"..lfs.writedir() .. "/Mods/services/Pierog/lua/?.lua"
|
||||
package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll"
|
||||
|
||||
-- Load Dlls
|
||||
package.cpath = package.cpath..';'.. lfs.writedir()..'/Mods/services/Pierog/bin/' ..'?.dll;'
|
||||
net.log("[Pierog]", "Loading DLL")
|
||||
Pierog.DLL = require('pierog')
|
||||
|
||||
-- Load config file
|
||||
net.log("[Pierog]", "Loading config")
|
||||
local config = require "pierog_config"
|
||||
|
||||
Pierog.host = config.host;
|
||||
Pierog.port = config.port;
|
||||
Pierog.delimiters = config.delimiters;
|
||||
Pierog.Instance = config.Instance;
|
||||
Pierog.DebugMode = config.DebugMode;
|
||||
Pierog.logPath = config.logPath;
|
||||
|
||||
Pierog.MOTD_line1 = config.MOTD_line1
|
||||
Pierog.MOTD_line2 = config.MOTD_line2
|
||||
Pierog.ConnectionError = config.ConnectionError
|
||||
Pierog.BroadcastPierogErrors = config.BroadcastPierogErrors
|
||||
Pierog.BroadcastErrorEachSeconds = config.BroadcastErrorEachSeconds or 600
|
||||
|
||||
Pierog.keepAliveSeconds = config.keepAliveSeconds
|
||||
Pierog.bigUpdatesSeconds = config.bigUpdatesSeconds
|
||||
Pierog.refreshSlotsSeconds = config.refreshSlotsSeconds
|
||||
Pierog.refreshMissionSeconds = config.refreshMissionSeconds
|
||||
|
||||
net.log("[Pierog]", "Config loaded")
|
||||
|
||||
Pierog.Globals = {}
|
||||
Pierog.Globals.version = "v0.21.12"
|
||||
|
||||
net.log("[Pierog]", "Global variables initialised")
|
||||
|
||||
Pierog.ReconnectTimeout = 30;
|
||||
Pierog.RefreshKeepAlive = 5
|
||||
|
||||
Pierog.State = {}
|
||||
Pierog.State.connected = false;
|
||||
|
||||
net.log("[Pierog]", "Initialising handlers")
|
||||
|
||||
Pierog.HandlerArgCounts = {}
|
||||
|
||||
Pierog.HandlerArgCounts["self_kill"] = 1
|
||||
Pierog.HandlerArgCounts["crash"] = 2
|
||||
Pierog.HandlerArgCounts["eject"] = 2
|
||||
Pierog.HandlerArgCounts["connect"] = 2
|
||||
Pierog.HandlerArgCounts["mission_end"] = 2
|
||||
Pierog.HandlerArgCounts["pilot_death"] = 2
|
||||
Pierog.HandlerArgCounts["takeoff"] = 3
|
||||
Pierog.HandlerArgCounts["landing"] = 3
|
||||
Pierog.HandlerArgCounts["change_slot"] = 3
|
||||
Pierog.HandlerArgCounts["friendly_fire"] = 3
|
||||
Pierog.HandlerArgCounts["disconnect"] = 4
|
||||
Pierog.HandlerArgCounts["kill"] = 7
|
||||
|
||||
Pierog.Stats = {}
|
||||
Pierog.Stats.LastConnectionError = 0
|
||||
|
||||
Pierog.func = {};
|
||||
|
||||
Pierog.func.initialiseMissionData = function()
|
||||
Pierog.Mission = {}
|
||||
Pierog.Mission.Hash= {}
|
||||
Pierog.Mission.Slots = {}
|
||||
Pierog.Mission.Players = {}
|
||||
Pierog.Mission.Units = {}
|
||||
Pierog.Mission.slotsInitialised = false;
|
||||
Pierog.Mission.lastSent = 0;
|
||||
|
||||
Pierog.Mission.LastSentMission = 0;
|
||||
Pierog.Mission.LastSentSlots = 0;
|
||||
Pierog.Mission.LastBigUpdate = 0;
|
||||
end
|
||||
|
||||
Pierog.func.ensureSlots = function()
|
||||
if(not Pierog.Mission.slotsInitialised) then
|
||||
|
||||
local count = 0;
|
||||
local coalitions = DCS.getAvailableCoalitions();
|
||||
for name, _ in pairs(coalitions) do
|
||||
local slots = DCS.getAvailableSlots(name);
|
||||
Pierog.Mission.Slots[name] = {}
|
||||
for k, unit in pairs(slots) do
|
||||
Pierog.Mission.Slots[name][unit["unitId"]] = unit
|
||||
count = count + 1;
|
||||
end
|
||||
end
|
||||
|
||||
if(count > 0) then
|
||||
net.log("[Pierog]", "initialised ".. count .. " slots")
|
||||
Pierog.Mission.slotsInitialised = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Pierog.func.ensureUnitId = function(unitId)
|
||||
net.log("[Pierog]", "Ensuring unit id: ", unitId)
|
||||
if(not Pierog.Mission.Units[unitId]) then
|
||||
local unitType = DCS.getUnitType(unitId);
|
||||
Pierog.Mission.Units[unitId] = unitType;
|
||||
|
||||
--net.log("[Pierog]", unitId, "category:", DCS.getUnitTypeAttribute(unitType, "category"));
|
||||
--net.log("[Pierog]", unitId, "category via type -> Prop:", DCS.getUnitProperty(unitType, DCS.UNIT_CATEGORY));
|
||||
--net.log("[Pierog]", unitId, "category via id -> Prop:", DCS.getUnitProperty(unitId, DCS.UNIT_CATEGORY));
|
||||
--net.log("[Pierog]", unitId, "wing span:", DCS.getUnitTypeAttribute(unitType, "WingSpan"));
|
||||
--net.log("[Pierog]", unitId, "deck level", DCS.getUnitTypeAttribute(unitType, "DeckLevel"));
|
||||
--
|
||||
local payload = Pierog.Handlers["event"]("unit_definition");
|
||||
payload["unitId"] = unitId;
|
||||
payload["unitType"] = unitType;
|
||||
payload["unitCategory"] = DCS.getUnitTypeAttribute(unitId, "category");
|
||||
payload["wingSpan"] = DCS.getUnitTypeAttribute(unitType, "WingSpan");
|
||||
payload["deckLevel"] = DCS.getUnitTypeAttribute(unitType, "DeckLevel");
|
||||
--
|
||||
Pierog.func.sendData(net.lua2json(payload))
|
||||
end
|
||||
end
|
||||
|
||||
Pierog.func.sendData = function(payload)
|
||||
|
||||
local now = DCS.getRealTime();
|
||||
|
||||
local connected = Pierog.DLL.tcpSend(payload);
|
||||
net.log("[Pierog]", "Sending", payload)
|
||||
|
||||
if(connected < 1) and (now > Pierog.Stats.LastConnectionError + Pierog.ReconnectTimeout) then
|
||||
Pierog.Stats.LastConnectionError = now;
|
||||
Pierog.AddLog("ERROR - TCP connection not available",0)
|
||||
|
||||
if Pierog.BroadcastPierogErrors > 0 then
|
||||
local _all_players = net.get_player_list()
|
||||
for i, playerId in ipairs(_all_players) do
|
||||
net.send_chat_to(Pierog.ConnectionError , playerId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Pierog.func.sendMissionUpdate = function()
|
||||
local payload = Pierog.Handlers["event"]("mission")
|
||||
payload["mission"] = DCS.getCurrentMission()["mission"]
|
||||
payload["missionName"] = DCS.getMissionName()
|
||||
|
||||
local json = net.lua2json(payload);
|
||||
Pierog.func.sendData(json)
|
||||
end
|
||||
|
||||
Pierog.func.sendSlotUpdate = function()
|
||||
local payload = Pierog.Handlers["event"]("slots")
|
||||
payload["slots"] = Pierog.Mission.Slots;
|
||||
|
||||
local json = net.lua2json(payload)
|
||||
Pierog.func.sendData(json)
|
||||
end
|
||||
|
||||
Pierog.Handlers = {}
|
||||
Pierog.Handlers["event"] = function(eventName)
|
||||
local data = {};
|
||||
data["event"] = eventName;
|
||||
data["missionHash"] = Pierog.Mission.Hash
|
||||
data['datetime']=os.date('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["friendly_fire"] = function(playerID, weaponName, victimPlayerID)
|
||||
local data = Pierog.Handlers["event"]("friendly_fire")
|
||||
|
||||
data["killer"] = playerID;
|
||||
data["victim"] = victimPlayerID;
|
||||
data["weapon"] = weaponName;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["mission_end"] = function(winner, msg)
|
||||
local data = Pierog.Handlers["event"]("mission_end")
|
||||
|
||||
data["winner"] = winner;
|
||||
data["message"] = msg;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["kill"] = function(killerPlayerID, killerUnitType, killerSide, victimPlayerID, victimUnitType, victimSide, weaponName)
|
||||
local data = Pierog.Handlers["event"]("kill")
|
||||
|
||||
data["killer"] = {id = killerPlayerID, side = killerSide, type = killerUnitType, ucid = net.get_player_info(killerPlayerID)["ucid"]}
|
||||
data["victim"] = {id = victimPlayerID, side = victimSide, type = victimUnitType, ucid = net.get_player_info(victimPlayerID)["ucid"]}
|
||||
data["weapon"] = weaponName;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["self_kill"] = function(playerID)
|
||||
local data = Pierog.Handlers["event"]("self_kill")
|
||||
data["who"] = playerID;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["change_slot"] = function(playerID, slotID, prevSide)
|
||||
local newDetails = net.get_player_info(playerID);
|
||||
local oldDetails = Pierog.Mission.Players[playerID];
|
||||
|
||||
Pierog.Mission.Players[playerID] = newDetails;
|
||||
|
||||
local data = Pierog.Handlers["event"]("change_slot")
|
||||
data["previousSlot"] = oldDetails["slot"]
|
||||
data["previousSide"] = oldDetails["side"]
|
||||
data["side"] = newDetails["side"]
|
||||
data["slot"] = newDetails["slot"]
|
||||
data["ucid"] = newDetails["ucid"]
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["connect"] = function(playerID, name)
|
||||
net.log("[Pierog]", "Getting player details")
|
||||
local details = net.get_player_info(playerID)
|
||||
Pierog.Mission.Players[playerID] = details
|
||||
|
||||
local data = Pierog.Handlers["event"]("connect")
|
||||
data["who"] = playerID
|
||||
data["details"] = details;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
Pierog.Handlers["disconnect"] = function(playerID, name, playerSide, reason_code)
|
||||
local data = Pierog.Handlers["event"]("disconnect")
|
||||
data["who"] = Pierog.Mission.Players[playerID]
|
||||
data["reasonCode"] = reason_code;
|
||||
|
||||
Pierog.Mission.Players[playerID] = nil
|
||||
|
||||
return data;
|
||||
end
|
||||
--"crash", playerID, unit_missionID
|
||||
Pierog.Handlers["crash"] = function(playerID, unit_missionID)
|
||||
Pierog.func.ensureUnitId(unit_missionID)
|
||||
|
||||
local data = Pierog.Handlers["event"]("crash")
|
||||
data["who"] = playerID;
|
||||
data["unitId"] = unit_missionID;
|
||||
|
||||
return data;
|
||||
end
|
||||
--"eject", playerID, unit_missionID
|
||||
Pierog.Handlers["eject"] = function(playerID, unit_missionID)
|
||||
Pierog.func.ensureUnitId(unit_missionID)
|
||||
|
||||
local data = Pierog.Handlers["event"]("eject")
|
||||
data["who"] = playerID;
|
||||
data["unitId"] = unit_missionID;
|
||||
|
||||
return data;
|
||||
end
|
||||
--"takeoff", playerID, unit_missionID, airdromeName
|
||||
Pierog.Handlers["takeoff"] = function(playerID, unit_missionID, airdromeName)
|
||||
Pierog.func.ensureUnitId(unit_missionID)
|
||||
|
||||
local data = Pierog.Handlers["event"]("takeoff")
|
||||
data["who"] = playerID;
|
||||
data["unitId"] = unit_missionID;
|
||||
data["from"] = airdromeName;
|
||||
|
||||
return data;
|
||||
end
|
||||
--"landing", playerID, unit_missionID, airdromeName
|
||||
Pierog.Handlers["landing"] = function(playerID, unit_missionID, airdromeName)
|
||||
Pierog.func.ensureUnitId(unit_missionID)
|
||||
|
||||
local data = Pierog.Handlers["event"]("landing")
|
||||
data["who"] = playerID;
|
||||
data["unitId"] = unit_missionID;
|
||||
data["at"] = airdromeName;
|
||||
|
||||
return data;
|
||||
end
|
||||
--"pilot_death", playerID, unit_missionID
|
||||
Pierog.Handlers["pilot_death"] = function(playerID, unit_missionID)
|
||||
Pierog.func.ensureUnitId(unit_missionID)
|
||||
|
||||
local data = Pierog.Handlers["event"]("pilot_death")
|
||||
data["who"] = playerID;
|
||||
data["unitId"] = unit_missionID;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
net.log("[Pierog] Handlers defined")
|
||||
|
||||
--- ################################ Helper function definitions ################################
|
||||
|
||||
Pierog.AddLog = function(text, logLevel)
|
||||
-- Adds logs to DCS.log file
|
||||
if(logLevel == nil) then
|
||||
logLevel = -1
|
||||
end
|
||||
|
||||
if Pierog.DebugMode >= logLevel then
|
||||
net.log("[Pierog]", text)
|
||||
end
|
||||
end
|
||||
|
||||
Pierog.generate_mission_hash = function()
|
||||
-- Generates unique simulation mission hash
|
||||
return DCS.getMissionName( ).."@".. Pierog.Instance .. "@" .. Pierog.Globals.version .. "@".. os.date('%Y%m%d_%H%M%S');
|
||||
end
|
||||
|
||||
--- ################################ Log functions ################################
|
||||
|
||||
Pierog.LogChat = function(playerID, msg, all)
|
||||
|
||||
local data = Pierog.Handlers["event"]("chat");
|
||||
|
||||
data['who']= Pierog.Mission.Players[playerID]["ucid"]
|
||||
data['msg']=msg
|
||||
data['messageToAll']=all
|
||||
data['missionhash']=Pierog.Mission.Hash
|
||||
|
||||
Pierog.func.sendData(net.lua2json(data));
|
||||
end
|
||||
|
||||
Pierog.LogEvent = function(log_type, log_content, log_arg1, log_arg2)
|
||||
-- Logs events messages
|
||||
local data = Pierog.Handlers["event"]("customLog");
|
||||
data['log_type']= log_type
|
||||
data['log_arg_1']= log_arg1
|
||||
data['log_arg_2']= log_arg2
|
||||
data['log_content']=log_content
|
||||
data['log_missionhash']=Pierog.Mission.Hash
|
||||
|
||||
Pierog.func.sendData(net.lua2json(data));
|
||||
|
||||
Pierog.AddLog("Sending event data, event: " .. log_type .. ", arg1:" .. log_arg1 .. ", arg2:" .. log_arg2 .. ", content: " .. log_content,1)
|
||||
end
|
||||
|
||||
Pierog.onMissionLoadEnd = function()
|
||||
Pierog.func.initialiseMissionData();
|
||||
Pierog.Mission.Hash=Pierog.generate_mission_hash();
|
||||
Pierog.DLL.markMissionStart(Pierog.Mission.Hash);
|
||||
|
||||
net.log("[Pierog]", "Mission loaded", Pierog.Mission.Hash);
|
||||
end
|
||||
|
||||
--- ################################ Event callbacks ################################
|
||||
|
||||
Pierog.onSimulationStart = function()
|
||||
Pierog.func.initialiseMissionData();
|
||||
Pierog.Mission.Hash=Pierog.generate_mission_hash();
|
||||
Pierog.DLL.markMissionStart(Pierog.Mission.Hash);
|
||||
|
||||
net.log("[Pierog]", "markMissionStart", Pierog.Mission.Hash);
|
||||
|
||||
Pierog.LogEvent("SimStart","Mission " .. Pierog.Mission.Hash .. " started",nil,nil);
|
||||
end
|
||||
|
||||
Pierog.onPlayerStop = function (id)
|
||||
-- Player left the simulation (happens right before a disconnect, if player exited by desire)
|
||||
|
||||
Pierog.LogEvent("quit", "Player " .. id .. " quit the server.",nil,nil);
|
||||
return
|
||||
end
|
||||
|
||||
Pierog.onSimulationFrame = function()
|
||||
|
||||
if(DCS.getMissionName() == "") then
|
||||
-- no mission loaded
|
||||
return;
|
||||
end
|
||||
|
||||
local now = DCS.getRealTime()
|
||||
|
||||
if(Pierog.Mission.LastBigUpdate < 1 or now > Pierog.Mission.LastBigUpdate + Pierog.bigUpdatesSeconds) then
|
||||
--net.log("[Pierog]", "Big update evaluation at", now, ", Pierog.Mission.LastSentSlots", Pierog.Mission.LastSentSlots, ", Pierog.Stats.LastBigUpdate", Pierog.Stats.LastBigUpdate)
|
||||
--net.log("[Pierog]", "Pierog.refreshSlotsSeconds: ".. Pierog.refreshSlotsSeconds)
|
||||
--net.log("[Pierog]", "name: >"..DCS.getMissionName().."<");
|
||||
--net.log("[Pierog]", "mission"..net.lua2json(DCS.getCurrentMission()))
|
||||
|
||||
if(not Pierog.Mission.slotsInitialised) then
|
||||
local foo = Pierog.func.ensureSlots;
|
||||
|
||||
local status, error = pcall(foo);
|
||||
if(error) then
|
||||
Pierog.AddLog( "error getting slot information" .. error);
|
||||
else
|
||||
Pierog.func.sendSlotUpdate();
|
||||
Pierog.Mission.LastSentSlots = now;
|
||||
Pierog.Mission.LastBigUpdate = now;
|
||||
end
|
||||
elseif(now > Pierog.Mission.LastSentSlots + Pierog.refreshSlotsSeconds) then
|
||||
--net.log("[Pierog]", "Slots")
|
||||
|
||||
Pierog.func.sendSlotUpdate();
|
||||
Pierog.Mission.LastSentSlots = now;
|
||||
Pierog.Mission.LastBigUpdate = now;
|
||||
end
|
||||
|
||||
if (now > Pierog.Mission.LastSentMission) then
|
||||
--net.log("[Pierog]", "Mission")
|
||||
|
||||
Pierog.func.sendMissionUpdate()
|
||||
Pierog.Mission.LastSentMission = now
|
||||
Pierog.Mission.LastBigUpdate = now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Pierog.onPlayerStart = function (playerId)
|
||||
-- Player entered cocpit
|
||||
net.send_chat_to(Pierog.MOTD_line1, playerId);
|
||||
net.send_chat_to(Pierog.MOTD_line2, playerId);
|
||||
end
|
||||
|
||||
Pierog.onPlayerTrySendChat = function (playerID, msg, all)
|
||||
-- Somebody tries to send chat message
|
||||
if (msg == Pierog.ConnectionError or msg == Pierog.MOTD_line1 or msg == Pierog.MOTD_line2) then
|
||||
return msg;
|
||||
end
|
||||
|
||||
Pierog.LogChat(playerID, msg, all)
|
||||
return msg;
|
||||
end
|
||||
|
||||
Pierog.onPlayerTryConnect= function(address, name, ucid, id)
|
||||
net.log("[Pierog]", "onPlayerTryConnect", address, name, ucid, id);
|
||||
return true;
|
||||
end
|
||||
|
||||
Pierog.onGameEvent = function(eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
|
||||
local argCount = Pierog.HandlerArgCounts[eventName];
|
||||
local handler = Pierog.Handlers[eventName];
|
||||
|
||||
--net.log("[Pierog]", 'Event', eventName);
|
||||
|
||||
if(Pierog.Handlers[eventName] == nil) then
|
||||
-- no handler found
|
||||
net.log("[Pierog]", 'Event type not found'..eventName)
|
||||
return;
|
||||
end
|
||||
|
||||
local payload = {}
|
||||
if(argCount == 1) then
|
||||
payload = handler(arg1)
|
||||
elseif argCount == 2 then
|
||||
payload = handler(arg1, arg2)
|
||||
elseif argCount == 3 then
|
||||
payload = handler(arg1, arg2, arg3)
|
||||
elseif argCount == 4 then
|
||||
payload = handler(arg1, arg2, arg3, arg4)
|
||||
elseif argCount == 7 then
|
||||
payload = handler(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
|
||||
end
|
||||
|
||||
local json = net.lua2json(payload)
|
||||
Pierog.func.sendData(json)
|
||||
|
||||
net.log("[Pierog] json", json)
|
||||
|
||||
--net.log("[Pierog]", "get_player_list", net.lua2json(net.get_player_list()))
|
||||
--net.log("[Pierog]", "mission_data", net.lua2json(DCS.getCurrentMission()))
|
||||
|
||||
--net.log("[Pierog]", "DCS.getAvailableCoalitions()", net.lua2json(DCS.getAvailableCoalitions()))
|
||||
--net.log("[Pierog]", "DCS.getAvailableSlots(0)", DCS.getAvailableSlots(0))
|
||||
--net.log("[Pierog]", "DCS.getAvailableSlots(3)", DCS.getAvailableSlots(3))
|
||||
--net.log("[Pierog]", "DCS.getMissionName()", DCS.getMissionName())
|
||||
--net.log("[Pierog]", "DCS.getModelTime()", DCS.getModelTime())
|
||||
--net.log("[Pierog]", "DCS.getRealTime()", DCS.getRealTime())
|
||||
end
|
||||
|
||||
-- ########### Finalize and set callbacks ###########
|
||||
if DCS.isServer() then
|
||||
-- If this game instance is hosting multiplayer game, start Pierog
|
||||
net.log("[Pierog]", "In a server, setting DLL", Pierog.logPath, Pierog.host, Pierog.port)
|
||||
Pierog.DLL.delimiters(Pierog.delimiters[1], Pierog.delimiters[2])
|
||||
Pierog.DLL.StartOfApp(Pierog.logPath, Pierog.host, Pierog.port);
|
||||
net.log("[Pierog]", "DLL started")
|
||||
DCS.setUserCallbacks(Pierog); -- Set user callbacs, map DCS event handlers with functions defined above
|
||||
Pierog.AddLog("Loaded - Pierog for DCS World - version: " .. Pierog.Globals.version,0) -- Display Pierog information in log
|
||||
end
|
||||
|
||||
|
||||
55
01_DCS/Scripts/notes.md
Normal file
55
01_DCS/Scripts/notes.md
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
|
||||
|call |return | meaning |
|
||||
|-------- |---- |---- |
|
||||
|`DCS.getAvailableSlots("blue")`|TBD|slots available for a side|
|
||||
|`net.get_player_list()`|[1, 2]|players in the mission. As in, occupied player slots in the server|
|
||||
|||
|
||||
|||
|
||||
|`DCS.getRealTime()` | `37.5899104` | seconds.micros after mission was started |
|
||||
|`DCS.getModelTime()`|`37.413`|
|
||||
|
||||
|
||||
|
||||
Callbacks:
|
||||
|
||||
|callback|event||
|
||||
|----|----|----|
|
||||
|onMissionLoadBegin|||
|
||||
|onMissionLoadProgress|||
|
||||
|onMissionLoadEnd, |||
|
||||
|onSimulationStart, |||
|
||||
|onSimulationStop, |||
|
||||
|onSimulationFrame, |||
|
||||
|onSimulationPause, |||
|
||||
|onSimulationResume, |||
|
||||
|onGameEvent, |||
|
||||
|onNetConnect, |||
|
||||
|onNetMissionChanged,|||
|
||||
|onNetConnect, |||
|
||||
|onNetDisconnect, |||
|
||||
|onPlayerTryConnect, |||
|
||||
|onPlayerConnect, |_connect||
|
||||
|onPlayerDisconnect, |disconnect||
|
||||
|onPlayerStart, |||
|
||||
|onPlayerStop, |||
|
||||
|onPlayerTrySendChat, |||
|
||||
|onPlayerTryChangeSlot|||
|
||||
|onPlayerChangeSlot,|change_slot||
|
||||
|
||||
|
||||
# Q's:
|
||||
Is unit ID tracking needed?
|
||||
|
||||
# ToDos:
|
||||
* file per mission hash
|
||||
* start of mission cpp callback?
|
||||
* hash as argument
|
||||
* end as callback
|
||||
* investigate DCS namespace (for pairs?)
|
||||
|
||||
* file creation and writing to file should only happen when there is something to be written
|
||||
|
||||
* mission update should happen every X seconds, only
|
||||
* connection should only affect message to players, nothing else!
|
||||
* detect monotonic time flow - back in time? new mission.
|
||||
38
01_DCS/dcs_engine_events.puml
Normal file
38
01_DCS/dcs_engine_events.puml
Normal file
@@ -0,0 +1,38 @@
|
||||
@startuml
|
||||
'https://plantuml.com/sequence-diagram
|
||||
|
||||
autonumber
|
||||
|
||||
participant UI
|
||||
participant Server
|
||||
participant Mission
|
||||
participant Simulation
|
||||
participant Paused
|
||||
participant Active
|
||||
|
||||
UI -> Server: onNetMissionChange
|
||||
note left: start server
|
||||
Server -> Mission: onMissionLoadBegin
|
||||
Server -> Mission: onMissionLoadEnd
|
||||
|
||||
Mission -> Paused: onSimulationStart
|
||||
note right: if starting configured, will unpause automatically
|
||||
Paused -> Active: onSimulationResume
|
||||
|
||||
|
||||
Active -> Paused: onSimulationPaused
|
||||
note left: when paused in the UI
|
||||
Paused -> Active: onSimulationResume
|
||||
note right: when simulation is unpaused
|
||||
|
||||
Simulation -> Mission: onSimulationStop
|
||||
note right: when simulation is stopped
|
||||
|
||||
|
||||
UI -> Server: onNetMissionChange
|
||||
note left: change mission
|
||||
Server -> Mission: onMissionLoadBegin
|
||||
|
||||
@enduml
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Google.Protobuf" version="3.15.0" targetFramework="net461" />
|
||||
<package id="Google.Protobuf" version="3.5.1" targetFramework="net461" />
|
||||
<package id="MySql.Data" version="8.0.15" targetFramework="net461" />
|
||||
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net461" />
|
||||
<package id="Newtonsoft.Json" version="12.0.1" targetFramework="net461" />
|
||||
</packages>
|
||||
@@ -261,14 +261,6 @@ public class DatabaseController
|
||||
DatabaseStatus = true; // True as connection is not broken
|
||||
ReturnValue = 1; // Return a value to remove this query from quae
|
||||
break;
|
||||
case 1366: // Invalid character
|
||||
PerunHelper.LogError(ref Globals.AppLogHistory, $"ERROR MySQL - error id: {m_ex.Number}", 1, 1, TCPFrameType);
|
||||
PerunHelper.LogError(ref Globals.AppLogHistory, $"ERROR MySQL - query: {SQLQueryTxt}", 1, 1, TCPFrameType);
|
||||
PerunHelper.LogError(ref Globals.AppLogHistory, $"ERROR MySQL - error: {m_ex.Message}", 1, 1, TCPFrameType);
|
||||
PerunHelper.LogError(ref Globals.AppLogHistory, $"ERROR MySQL - frame skipped, it will not be saved to the database", 1, 1, TCPFrameType);
|
||||
DatabaseStatus = true; // True as connection is not broken
|
||||
ReturnValue = 1; // Return a value to remove this query from queue
|
||||
break;
|
||||
default: // Default error handler
|
||||
PerunHelper.LogError(ref Globals.AppLogHistory, $"ERROR MySQL - error id: {m_ex.Number}", 1, 1, TCPFrameType);
|
||||
PerunHelper.LogError(ref Globals.AppLogHistory, $"ERROR MySQL - query: {SQLQueryTxt}", 1, 1, TCPFrameType);
|
||||
|
||||
@@ -231,7 +231,7 @@ namespace Perun_v1
|
||||
trayIconMain.Text = this.Text; // Set notification icon text
|
||||
|
||||
// Prepare MySQL connection string
|
||||
Globals.DatabaseConnection.DatabaseConnectionString = $"server={con_txt_mysql_server.Text};user={con_txt_mysql_username.Text};database={con_txt_mysql_database.Text};port={con_txt_mysql_port.Text};password={con_txt_mysql_password.Text};CharSet=utf8mb4";
|
||||
Globals.DatabaseConnection.DatabaseConnectionString = $"server={con_txt_mysql_server.Text};user={con_txt_mysql_username.Text};database={con_txt_mysql_database.Text};port={con_txt_mysql_port.Text};password={con_txt_mysql_password.Text}";
|
||||
|
||||
// Start listening
|
||||
PerunHelper.LogInfo(ref Globals.AppLogHistory, "Opening connections", 0, 1);
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<value>3306</value>
|
||||
</setting>
|
||||
<setting name="DCS_Server_Port" serializeAs="String">
|
||||
<value>48621</value>
|
||||
<value>48620</value>
|
||||
</setting>
|
||||
<setting name="DCS_Instance" serializeAs="String">
|
||||
<value>1</value>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="BouncyCastle" version="1.8.9" targetFramework="net48" />
|
||||
<package id="Google.Protobuf" version="3.15.0" targetFramework="net48" />
|
||||
<package id="BouncyCastle" version="1.8.3.1" targetFramework="net48" />
|
||||
<package id="Google.Protobuf" version="3.6.1" targetFramework="net48" />
|
||||
<package id="MySql.Data" version="8.0.17" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net48" />
|
||||
<package id="SSH.NET" version="2020.0.2" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net48" />
|
||||
<package id="SSH.NET" version="2016.1.0" targetFramework="net48" />
|
||||
</packages>
|
||||
@@ -1,12 +1,18 @@
|
||||
# CMakeList.txt : CMake project for perun_dll, include source and define
|
||||
# project specific logic here.
|
||||
#
|
||||
cmake_minimum_required (VERSION 3.8)
|
||||
cmake_minimum_required(VERSION 3.17)
|
||||
|
||||
project ("parent")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.12.25835)
|
||||
set(CMAKE_CXX20_STANDARD_COMPILE_OPTION "-std:c++latest")
|
||||
set(CMAKE_CXX20_EXTENSION_COMPILE_OPTION "-std:c++latest")
|
||||
endif()
|
||||
|
||||
add_subdirectory(lua-5.1.5)
|
||||
add_subdirectory(perun)
|
||||
add_subdirectory(pierog)
|
||||
add_subdirectory(experimental)
|
||||
21
03_Perun_Lua_Wrapper/experimental/CMakeLists.txt
Normal file
21
03_Perun_Lua_Wrapper/experimental/CMakeLists.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
cmake_minimum_required(VERSION 3.17)
|
||||
project(experimental)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
set (EXPERIMENTAL_SOURCES
|
||||
"src/main.cpp"
|
||||
)
|
||||
|
||||
add_executable(experimental ${EXPERIMENTAL_SOURCES})
|
||||
|
||||
include_directories(${pierog_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(
|
||||
experimental
|
||||
ws2_32
|
||||
pierog
|
||||
)
|
||||
|
||||
target_include_directories (pierog PUBLIC pierog)
|
||||
#set_target_properties(main PROPERTIES OUTPUT_NAME "pierog")
|
||||
@@ -28,7 +28,7 @@ set (LUA_RUNTIME_SOURCES
|
||||
"src/lua.h"
|
||||
"src/lualib.h"
|
||||
"src/lzio.c" "src/lzio.h"
|
||||
)
|
||||
../experimental/src/main.cpp)
|
||||
|
||||
|
||||
add_library( lua-5.1.5 ${LUA_RUNTIME_SOURCES} )
|
||||
|
||||
@@ -44,7 +44,7 @@ void luaK_nil (FuncState *fs, int from, int n) {
|
||||
if (GET_OPCODE(*previous) == OP_LOADNIL) {
|
||||
int pfrom = GETARG_A(*previous);
|
||||
int pto = GETARG_B(*previous);
|
||||
if (pfrom <= from && from <= pto+1) { /* can connect both? */
|
||||
if (pfrom <= from && from <= pto+1) { /* can _connect both? */
|
||||
if (from+n-1 > pto)
|
||||
SETARG_B(*previous, from+n-1);
|
||||
return;
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
".\\?.lua;" LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \
|
||||
LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua"
|
||||
#define LUA_CPATH_DEFAULT \
|
||||
".\\?.dll;" LUA_CDIR"?.dll;" LUA_CDIR"loadall.dll"
|
||||
".\\?.bin;" LUA_CDIR"?.bin;" LUA_CDIR"loadall.bin"
|
||||
|
||||
#else
|
||||
#define LUA_ROOT "/usr/local/"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.17)
|
||||
project(perun)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
set (PERUN_DLL_SOURCES
|
||||
"src/library.h"
|
||||
"src/library.cpp"
|
||||
"src/Connection.h"
|
||||
"src/Connecton.cpp"
|
||||
)
|
||||
|
||||
include (GenerateExportHeader)
|
||||
add_library(perun SHARED ${PERUN_DLL_SOURCES})
|
||||
|
||||
target_link_libraries(
|
||||
perun
|
||||
lua-5.1.5
|
||||
ws2_32
|
||||
)
|
||||
|
||||
target_include_directories (perun PUBLIC)
|
||||
#set_target_properties(main PROPERTIES OUTPUT_NAME "perun")
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifndef PERUN_CONNECTION_H
|
||||
#define PERUN_CONNECTION_H
|
||||
|
||||
#include "winsock.h"
|
||||
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
|
||||
#include "library.h"
|
||||
|
||||
enum enumConnectionState {
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
};
|
||||
|
||||
class SocketWrapper {
|
||||
public:
|
||||
SocketWrapper();
|
||||
~SocketWrapper();
|
||||
|
||||
void disconnect();
|
||||
void createConnection(std::string* host, const int* port);
|
||||
void enqueueForSending(std::string* payload);
|
||||
|
||||
int getAndResetReconnected();
|
||||
int getFlagConnected();
|
||||
|
||||
private:
|
||||
SOCKET tcpSocket;
|
||||
std::string* tcpHost;
|
||||
int tcpPort;
|
||||
|
||||
int flagReconnected = 0;
|
||||
volatile enumConnectionState connectionState = DISCONNECTED;
|
||||
|
||||
std::queue<std::string*> dataBuffer;
|
||||
std::deque<std::string*> sendQueue;
|
||||
|
||||
std::mutex mutexLock;
|
||||
|
||||
void reconnect();
|
||||
void tcpConnect();
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
@@ -1,124 +0,0 @@
|
||||
#include "Connection.h"
|
||||
|
||||
SocketWrapper::SocketWrapper() {
|
||||
tcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
}
|
||||
|
||||
SocketWrapper::~SocketWrapper() {
|
||||
}
|
||||
|
||||
int SocketWrapper::getAndResetReconnected() {
|
||||
int result = this->flagReconnected;
|
||||
|
||||
this->flagReconnected = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int SocketWrapper::getFlagConnected() {
|
||||
return this->connectionState;
|
||||
}
|
||||
|
||||
void SocketWrapper::tcpConnect() {
|
||||
// Create socket address object from TCP port and host
|
||||
SOCKADDR_IN socketAddress;
|
||||
socketAddress.sin_family = AF_INET;
|
||||
socketAddress.sin_port = htons(u_short(this->tcpPort));
|
||||
socketAddress.sin_addr.s_addr = *((unsigned long*)gethostbyname(this->tcpHost->c_str())->h_addr);
|
||||
|
||||
if (connect(tcpSocket, (sockaddr*)&socketAddress, sizeof(SOCKADDR_IN)) == 0) {
|
||||
this->connectionState = CONNECTED;
|
||||
this->flagReconnected = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void SocketWrapper::createConnection(std::string* host, const int* port) {
|
||||
// TCP connection - ConnectTo
|
||||
this->tcpHost = host;
|
||||
this->tcpPort = *port;
|
||||
|
||||
tcpConnect();
|
||||
|
||||
// Create new thread
|
||||
std::thread thread_object([this]() {
|
||||
// TCP sending loop
|
||||
bool nothingToSend = false;
|
||||
while (true) {
|
||||
// Endless loop (will run after main dll thread is active)
|
||||
if (connectionState == CONNECTED && mutexLock.try_lock()) {
|
||||
if (sendQueue.empty()) {
|
||||
nothingToSend = true;
|
||||
} else {
|
||||
// Payload in queue
|
||||
auto payload = sendQueue.front();
|
||||
int length = payload->length();
|
||||
int bytesSent = send(tcpSocket, payload->c_str(), payload->length(), 0);
|
||||
|
||||
if (bytesSent == payload->length()) {
|
||||
// All payload was sent
|
||||
sendQueue.pop_front();
|
||||
delete payload;
|
||||
} else {
|
||||
// Remaining paylad
|
||||
if (bytesSent > 0) {
|
||||
// Send remaining bytes
|
||||
auto shortened = payload->substr(bytesSent, length - bytesSent);
|
||||
sendQueue.pop_front();
|
||||
sendQueue.push_front(&shortened);
|
||||
delete payload;
|
||||
} else {
|
||||
// Payload was not sent - handle error
|
||||
switch (WSAGetLastError()) {
|
||||
// Connection was reset
|
||||
case WSAECONNRESET:
|
||||
// Connection aborted
|
||||
case WSAECONNABORTED:
|
||||
// Connection was closed
|
||||
case WSAESHUTDOWN:
|
||||
connectionState = DISCONNECTED;
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutexLock.unlock();
|
||||
} else {
|
||||
// Not connected
|
||||
reconnect();
|
||||
}
|
||||
// sleep longer if nothing to send
|
||||
if (nothingToSend) { Sleep(100); } else { Sleep(10); }
|
||||
}
|
||||
});
|
||||
thread_object.detach(); // Detach TCP thread from main thread
|
||||
}
|
||||
|
||||
void SocketWrapper::reconnect() {
|
||||
if (connectionState == DISCONNECTED) {
|
||||
disconnect();
|
||||
tcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // Reset socket
|
||||
}
|
||||
|
||||
tcpConnect();
|
||||
}
|
||||
|
||||
void SocketWrapper::disconnect() {
|
||||
// TCP connection - Disconnect
|
||||
closesocket(tcpSocket);
|
||||
connectionState = DISCONNECTED;
|
||||
}
|
||||
|
||||
void SocketWrapper::enqueueForSending(std::string* payload) {
|
||||
if (mutexLock.try_lock()) {
|
||||
while (!dataBuffer.empty()) {
|
||||
// Shift buffer to queue
|
||||
sendQueue.push_back(dataBuffer.front());
|
||||
dataBuffer.pop();
|
||||
}
|
||||
sendQueue.push_back(payload);
|
||||
mutexLock.unlock();
|
||||
}
|
||||
else {
|
||||
dataBuffer.push(payload);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#include "library.h"
|
||||
|
||||
static SocketWrapper tcpConnection;
|
||||
|
||||
static int appStartHook(lua_State* luaState) {
|
||||
// Starting the app - prepare
|
||||
|
||||
lua_pushinteger(luaState, 1); // First return value: confirmation that app was started
|
||||
|
||||
return (1); // Set return to number of arguments returned
|
||||
}
|
||||
|
||||
static int appEndHook(lua_State* luaState) {
|
||||
// Closing the app - clean up
|
||||
|
||||
tcpConnection.disconnect();
|
||||
|
||||
return (0); // No return values
|
||||
}
|
||||
|
||||
static int tcpConnect(lua_State* luaState) {
|
||||
// Connect to the TCP socket
|
||||
|
||||
auto *host = new std::string(lua_tolstring(luaState, 1, 0));
|
||||
const int *port = new int(lua_tointeger(luaState, 2));
|
||||
tcpConnection.createConnection(host, port);
|
||||
|
||||
return (0); // No return values
|
||||
}
|
||||
|
||||
static int tcpSend(lua_State* luaState) {
|
||||
// Send frame over TCP socket
|
||||
|
||||
tcpConnection.enqueueForSending(new std::string(lua_tolstring(luaState, 1, 0)));
|
||||
|
||||
lua_pushinteger(luaState, tcpConnection.getFlagConnected()); // First return value: information if there is TCP connection
|
||||
lua_pushinteger(luaState, tcpConnection.getAndResetReconnected()); // Secound return value: information if there was recent reconnection to TCP server
|
||||
|
||||
return (2); // Set return to number of arguments returned
|
||||
}
|
||||
|
||||
extern "C" int __declspec(dllexport) luaopen_perun(lua_State * L) {
|
||||
static const luaL_Reg Map[] = {
|
||||
{"StartOfApp", appStartHook}, // Called at the begining of the session
|
||||
{"EndOfApp", appEndHook }, // Called at the end of the session out of LuaExportStop
|
||||
{"tcpSend", tcpSend}, // Called to send data from lua over TCP
|
||||
{"tcpConnect", tcpConnect}, // Create connection
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
// Register the list of functions for lua
|
||||
luaL_register(L, "perun", Map);
|
||||
|
||||
return 1;
|
||||
}
|
||||
25
03_Perun_Lua_Wrapper/pierog/CmakeLists.txt
Normal file
25
03_Perun_Lua_Wrapper/pierog/CmakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.17)
|
||||
project(pierog)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
set(PIEROG_DLL_SOURCES
|
||||
"src/library.h"
|
||||
"src/library.cpp"
|
||||
src/RotatingFileOutput.cpp
|
||||
src/SocketOutput.cpp
|
||||
src/DataDistributor.cpp
|
||||
src/RotatingFileOutput.h
|
||||
src/SocketOutput.h
|
||||
src/DataDistributor.h)
|
||||
|
||||
include(GenerateExportHeader)
|
||||
add_library(pierog SHARED ${PIEROG_DLL_SOURCES})
|
||||
|
||||
target_link_libraries(
|
||||
pierog
|
||||
lua-5.1.5
|
||||
ws2_32
|
||||
)
|
||||
|
||||
target_include_directories(pierog PUBLIC ${PIEROG_DLL_SOURCES})
|
||||
102
03_Perun_Lua_Wrapper/pierog/src/DataDistributor.cpp
Normal file
102
03_Perun_Lua_Wrapper/pierog/src/DataDistributor.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "DataDistributor.h"
|
||||
|
||||
#include <utility>
|
||||
#include <ostream>
|
||||
|
||||
DataDistributor::DataDistributor(std::string logPath,
|
||||
std::string host,
|
||||
int port) {
|
||||
|
||||
fileOutput = new RotatingFileOutput(std::move(logPath));
|
||||
socketOutput = new SocketOutput(std::move(host), port);
|
||||
}
|
||||
|
||||
DataDistributor::~DataDistributor() = default;
|
||||
|
||||
void DataDistributor::start() {
|
||||
if(running) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread thread_object([this]() {
|
||||
long KEEP_ALIVE = 1000;
|
||||
bool nothingToSend = false;
|
||||
|
||||
while(shouldRun) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
|
||||
if(lock.try_lock()) {
|
||||
if(sendQueue.empty()) {
|
||||
auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastSent);
|
||||
if(delay.count() > KEEP_ALIVE) {
|
||||
sendQueue.push_front(new std::string(" "));
|
||||
} else {
|
||||
nothingToSend = true;
|
||||
}
|
||||
} else {
|
||||
auto payload = sendQueue.front();
|
||||
int sent = socketOutput->write(payload);
|
||||
lastSent = std::chrono::system_clock::now();
|
||||
|
||||
if(sent > 0) {
|
||||
everSentViaSocket = true;
|
||||
sendQueue.pop_front();
|
||||
if(sent == payload->length()) {
|
||||
delete payload;
|
||||
} else {
|
||||
auto shortened = payload->substr(sent, payload->length() - sent);
|
||||
sendQueue.push_front(&shortened);
|
||||
delete payload;
|
||||
}
|
||||
} else {
|
||||
// failed to send
|
||||
}
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
if(nothingToSend) {
|
||||
Sleep(100);
|
||||
}
|
||||
}
|
||||
});
|
||||
thread_object.detach();
|
||||
|
||||
shouldRun = true;
|
||||
running = true;
|
||||
}
|
||||
|
||||
void DataDistributor::stop() {
|
||||
shouldRun = false;
|
||||
}
|
||||
|
||||
void DataDistributor::enqueueForSending(std::string *payload) {
|
||||
fileOutput->write(payload);
|
||||
if (lock.try_lock()) {
|
||||
while (!dataBuffer.empty()) {
|
||||
// Shift buffer to queue
|
||||
sendQueue.push_back(dataBuffer.front());
|
||||
dataBuffer.pop_front();
|
||||
}
|
||||
sendQueue.push_back(payload);
|
||||
lock.unlock();
|
||||
}
|
||||
else {
|
||||
dataBuffer.push_back(payload);
|
||||
}
|
||||
}
|
||||
|
||||
void DataDistributor::markNewRecording() {
|
||||
fileOutput->markNewRecording();
|
||||
if(!everSentViaSocket) {
|
||||
lock.lock();
|
||||
sendQueue.clear();
|
||||
dataBuffer.clear();
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
int DataDistributor::isConnected() {
|
||||
return socketOutput->isConnected();
|
||||
}
|
||||
40
03_Perun_Lua_Wrapper/pierog/src/DataDistributor.h
Normal file
40
03_Perun_Lua_Wrapper/pierog/src/DataDistributor.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef PARENT_DATADISTRIBUTOR_H
|
||||
#define PARENT_DATADISTRIBUTOR_H
|
||||
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include "SocketOutput.h"
|
||||
#include "RotatingFileOutput.h"
|
||||
|
||||
class DataDistributor {
|
||||
public:
|
||||
DataDistributor(std::string logPath,
|
||||
std::string host,
|
||||
int port);
|
||||
|
||||
virtual ~DataDistributor();
|
||||
|
||||
void enqueueForSending(std::string* payload);
|
||||
void markNewRecording();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
int isConnected();
|
||||
|
||||
private:
|
||||
std::atomic<boolean> shouldRun = true;
|
||||
std::atomic<boolean> running = false;
|
||||
std::atomic<boolean> everSentViaSocket = false;
|
||||
std::deque<std::string*> dataBuffer;
|
||||
std::deque<std::string*> sendQueue;
|
||||
std::mutex lock;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> lastSent = std::chrono::system_clock::now();
|
||||
|
||||
SocketOutput* socketOutput;
|
||||
RotatingFileOutput* fileOutput;
|
||||
};
|
||||
|
||||
|
||||
#endif //PARENT_DATADISTRIBUTOR_H
|
||||
41
03_Perun_Lua_Wrapper/pierog/src/RotatingFileOutput.cpp
Normal file
41
03_Perun_Lua_Wrapper/pierog/src/RotatingFileOutput.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "RotatingFileOutput.h"
|
||||
|
||||
RotatingFileOutput::~RotatingFileOutput() = default;
|
||||
|
||||
RotatingFileOutput::RotatingFileOutput(std::string outputPath): path(std::move(outputPath)) {
|
||||
|
||||
}
|
||||
|
||||
void RotatingFileOutput::markNewRecording() {
|
||||
if(outputFile != nullptr && outputFile->good()) {
|
||||
outputFile->flush();
|
||||
outputFile->close();
|
||||
delete outputFile;
|
||||
outputFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void RotatingFileOutput::write(std::string *payload) {
|
||||
if (outputFile == nullptr) {
|
||||
std::string fileName = generateFileName();
|
||||
outputFile = new std::ofstream(fileName, std::ofstream::app | std::ios::out);
|
||||
}
|
||||
|
||||
outputFile->write(payload->c_str(), payload->length());
|
||||
bytesWritten += (long) payload->length();
|
||||
if (payload->length() > 50) {
|
||||
outputFile->flush();
|
||||
}
|
||||
}
|
||||
|
||||
std::string RotatingFileOutput::generateFileName() {
|
||||
auto const now = std::chrono::system_clock::now();
|
||||
auto const gmt = std::chrono::locate_zone("Etc/GMT");
|
||||
auto const filename = std::format("pierog.{:%FT_%H%M%S}.log",
|
||||
std::chrono::zoned_time{gmt, floor<std::chrono::milliseconds>(now)});
|
||||
|
||||
std::filesystem::path dir(this->path);
|
||||
std::filesystem::path file(filename);
|
||||
return (dir / file).string();
|
||||
}
|
||||
|
||||
27
03_Perun_Lua_Wrapper/pierog/src/RotatingFileOutput.h
Normal file
27
03_Perun_Lua_Wrapper/pierog/src/RotatingFileOutput.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef PARENT_ROTATINGFILEOUTPUT_H
|
||||
#define PARENT_ROTATINGFILEOUTPUT_H
|
||||
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
class RotatingFileOutput {
|
||||
public:
|
||||
RotatingFileOutput(std::string outputPath);
|
||||
virtual ~RotatingFileOutput();
|
||||
|
||||
void markNewRecording();
|
||||
void write(std::string* payload);
|
||||
|
||||
private:
|
||||
const std::string path;
|
||||
|
||||
std::atomic<long long> bytesWritten;
|
||||
std::ofstream * outputFile = nullptr;
|
||||
|
||||
std::string generateFileName();
|
||||
};
|
||||
|
||||
|
||||
#endif //PARENT_ROTATINGFILEOUTPUT_H
|
||||
56
03_Perun_Lua_Wrapper/pierog/src/SocketOutput.cpp
Normal file
56
03_Perun_Lua_Wrapper/pierog/src/SocketOutput.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "SocketOutput.h"
|
||||
|
||||
SocketOutput::SocketOutput(std::string host,
|
||||
const int port): tcpHost(std::move(host)), tcpPort(port) {
|
||||
|
||||
address = new SOCKADDR_IN;
|
||||
address->sin_family = AF_INET;
|
||||
address->sin_port = htons(u_short(this->tcpPort));
|
||||
address->sin_addr.s_addr = *((unsigned long*)gethostbyname(this->tcpHost.c_str())->h_addr);
|
||||
}
|
||||
|
||||
SocketOutput::~SocketOutput() = default;
|
||||
|
||||
int SocketOutput::write(std::string *payload) {
|
||||
|
||||
if(!(isConnected() || _connect())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bytesSent = send(tcpSocket, payload->c_str(), payload->length(), 0);
|
||||
if(bytesSent > 0) {
|
||||
return bytesSent;
|
||||
} else {
|
||||
switch (WSAGetLastError()) {
|
||||
case WSAECONNRESET: // Connection reset
|
||||
case WSAECONNABORTED: // Connection aborted
|
||||
case WSAESHUTDOWN: // Connection closed
|
||||
disconnect();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool SocketOutput::isConnected() {
|
||||
return connectionState == CONNECTED;
|
||||
}
|
||||
|
||||
bool SocketOutput::_connect() {
|
||||
if(tcpSocket == INVALID_SOCKET) {
|
||||
tcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
}
|
||||
if(connectionState != CONNECTED) {
|
||||
if (connect(tcpSocket, (sockaddr *) address, sizeof(SOCKADDR_IN)) == 0) {
|
||||
connectionState = CONNECTED;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SocketOutput::disconnect() {
|
||||
closesocket(tcpSocket);
|
||||
tcpSocket = INVALID_SOCKET;
|
||||
connectionState = DISCONNECTED;
|
||||
}
|
||||
38
03_Perun_Lua_Wrapper/pierog/src/SocketOutput.h
Normal file
38
03_Perun_Lua_Wrapper/pierog/src/SocketOutput.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef PARENT_SOCKETOUTPUT_H
|
||||
#define PARENT_SOCKETOUTPUT_H
|
||||
|
||||
#include "winsock.h"
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
|
||||
enum enumConnectionState {
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
NEVER_CONNECTED
|
||||
};
|
||||
|
||||
class SocketOutput {
|
||||
public:
|
||||
SocketOutput(std::string host,
|
||||
int port);
|
||||
virtual ~SocketOutput();
|
||||
|
||||
int write(std::string* payload);
|
||||
|
||||
bool isConnected();
|
||||
private:
|
||||
SOCKET tcpSocket = INVALID_SOCKET;
|
||||
SOCKADDR_IN* address = nullptr;
|
||||
|
||||
const std::string tcpHost = "localhost";
|
||||
const int tcpPort = 0;
|
||||
|
||||
volatile enumConnectionState connectionState = NEVER_CONNECTED;
|
||||
|
||||
bool _connect();
|
||||
// void reconnect();
|
||||
void disconnect();
|
||||
};
|
||||
|
||||
|
||||
#endif //PARENT_SOCKETOUTPUT_H
|
||||
99
03_Perun_Lua_Wrapper/pierog/src/library.cpp
Normal file
99
03_Perun_Lua_Wrapper/pierog/src/library.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "library.h"
|
||||
#include "DataDistributor.h"
|
||||
#include <format>
|
||||
#include <filesystem>
|
||||
|
||||
static DataDistributor * dataDistributor = nullptr;
|
||||
|
||||
static std::string *startingDelimiter = nullptr;
|
||||
static std::string *endingDelimiter = nullptr;
|
||||
static std::string lastObservedHash = std::string("");
|
||||
|
||||
/* this method exists for comments */
|
||||
static int valuesToReturn(int input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
static int appStartHook(lua_State* luaState) {
|
||||
// Starting the app - prepare
|
||||
if(dataDistributor == nullptr) {
|
||||
|
||||
int i = 1;
|
||||
const std::string path = std::string(lua_tolstring(luaState, i++, 0));
|
||||
const std::string host = std::string(lua_tolstring(luaState, i++, 0));
|
||||
const int port = (int) lua_tointeger(luaState, i++);
|
||||
|
||||
dataDistributor = new DataDistributor(path, host, port);
|
||||
dataDistributor->start();
|
||||
}
|
||||
|
||||
lua_pushinteger(luaState, 1); // First return value: confirmation that app was started
|
||||
|
||||
return valuesToReturn(1);
|
||||
}
|
||||
|
||||
static int appEndHook(lua_State* luaState) {
|
||||
// Closing the app - clean up
|
||||
|
||||
if(dataDistributor != nullptr) {
|
||||
dataDistributor->stop();
|
||||
}
|
||||
|
||||
return valuesToReturn(0);
|
||||
}
|
||||
|
||||
static int setDelimiters(lua_State* luaState) {
|
||||
startingDelimiter = new std::string(lua_tolstring(luaState, 1, 0));
|
||||
endingDelimiter = new std::string(lua_tolstring(luaState, 2, 0));
|
||||
|
||||
return valuesToReturn(0);
|
||||
}
|
||||
|
||||
static int markMissionStart(lua_State* luaState) {
|
||||
|
||||
std::string missionHash = std::string(lua_tolstring(luaState, 1, 0));
|
||||
|
||||
if(dataDistributor != nullptr && missionHash != lastObservedHash) {
|
||||
dataDistributor->markNewRecording();
|
||||
lastObservedHash = missionHash;
|
||||
}
|
||||
|
||||
return valuesToReturn(0);
|
||||
}
|
||||
|
||||
static int tcpSend(lua_State* luaState) {
|
||||
// Send frame over TCP socket
|
||||
|
||||
if(dataDistributor != nullptr) {
|
||||
if(startingDelimiter != nullptr) {
|
||||
dataDistributor->enqueueForSending(new std::string(*startingDelimiter));
|
||||
}
|
||||
dataDistributor->enqueueForSending(new std::string(lua_tolstring(luaState, 1, 0)));
|
||||
if(endingDelimiter != nullptr) {
|
||||
dataDistributor->enqueueForSending(new std::string(*endingDelimiter));
|
||||
}
|
||||
|
||||
lua_pushinteger(luaState,
|
||||
dataDistributor->isConnected()); // First return value: information if there is TCP connection
|
||||
} else {
|
||||
lua_pushinteger(luaState, 0);
|
||||
}
|
||||
|
||||
return valuesToReturn(1);
|
||||
}
|
||||
|
||||
extern "C" int __declspec(dllexport) luaopen_pierog(lua_State * L) {
|
||||
static const luaL_Reg Map[] = {
|
||||
{"StartOfApp", appStartHook}, // Called at the begining of the session
|
||||
{"EndOfApp", appEndHook }, // Called at the end of the session out of LuaExportStop
|
||||
{"tcpSend", tcpSend}, // Called to send data from lua over TCP
|
||||
{"delimiters", setDelimiters },
|
||||
{"markMissionStart", markMissionStart },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
// Register the list of functions for lua
|
||||
luaL_register(L, "pierog", Map);
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -12,6 +12,5 @@ extern "C" {
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <queue>
|
||||
#include "Connection.h"
|
||||
|
||||
#endif //PERUN_LIBRARY_H
|
||||
@@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS `pe_Config` (
|
||||
`pe_Config_id` int(11) NOT NULL,
|
||||
`pe_Config_payload` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`pe_Config_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `pe_Config` (`pe_Config_id`, `pe_Config_payload`) VALUES
|
||||
(1, 'v0.12.1');
|
||||
@@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS `pe_DataMissionHashes` (
|
||||
PRIMARY KEY (`pe_DataMissionHashes_id`),
|
||||
UNIQUE KEY `UNIQUE_hash` (`pe_DataMissionHashes_hash`),
|
||||
KEY `pe_DataMissionHashes_instance` (`pe_DataMissionHashes_instance`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_DataPlayers`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_DataPlayers` (
|
||||
@@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `pe_DataPlayers` (
|
||||
`pe_DataPlayers_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`pe_DataPlayers_id`),
|
||||
UNIQUE KEY `UNIQUE_UCID` (`pe_DataPlayers_ucid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_DataRaw`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_DataRaw` (
|
||||
@@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS `pe_DataRaw` (
|
||||
PRIMARY KEY (`pe_dataraw_type`,`pe_dataraw_instance`),
|
||||
KEY `pe_dataraw_type_pe_dataraw_instance` (`pe_dataraw_type`,`pe_dataraw_instance`),
|
||||
KEY `pe_dataraw_updated` (`pe_dataraw_updated`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_DataTypes`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_DataTypes` (
|
||||
@@ -59,14 +59,14 @@ CREATE TABLE IF NOT EXISTS `pe_DataTypes` (
|
||||
`pe_DataTypes_update` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`pe_DataTypes_id`),
|
||||
UNIQUE KEY `UNIQUE_TYPE_NAME` (`pe_DataTypes_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_LogChat`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_LogChat` (
|
||||
`pe_LogChat_id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`pe_LogChat_id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`pe_LogChat_datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`pe_LogChat_missionhash_id` bigint(20) DEFAULT NULL,
|
||||
`pe_LogChat_playerid` varchar(100) NOT NULL,
|
||||
`pe_LogChat_missionhash_id` bigint DEFAULT NULL,
|
||||
`pe_LogChat_playerid` bigint NOT NULL,
|
||||
`pe_LogChat_msg` text NOT NULL,
|
||||
`pe_LogChat_all` varchar(10) NOT NULL,
|
||||
PRIMARY KEY (`pe_LogChat_id`),
|
||||
@@ -74,7 +74,7 @@ CREATE TABLE IF NOT EXISTS `pe_LogChat` (
|
||||
KEY `pe_LogChat_playerid` (`pe_LogChat_playerid`),
|
||||
KEY `pe_LogChat_datetime` (`pe_LogChat_datetime`),
|
||||
KEY `pe_LogChat_all` (`pe_LogChat_all`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_LogEvent`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_LogEvent` (
|
||||
@@ -90,7 +90,7 @@ CREATE TABLE IF NOT EXISTS `pe_LogEvent` (
|
||||
KEY `pe_LogEvent_missionhash_id` (`pe_LogEvent_missionhash_id`),
|
||||
KEY `pe_LogEvent_datetime` (`pe_LogEvent_datetime`),
|
||||
KEY `pe_LogEvent_type_2` (`pe_LogEvent_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_LogLogins`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_LogLogins` (
|
||||
@@ -104,7 +104,7 @@ CREATE TABLE IF NOT EXISTS `pe_LogLogins` (
|
||||
KEY `pe_LogLogins_playerid` (`pe_LogLogins_playerid`),
|
||||
KEY `pe_LogLogins_datetime` (`pe_LogLogins_datetime`),
|
||||
KEY `pe_LogLogins_instance` (`pe_LogLogins_instance`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_LogStats`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_LogStats` (
|
||||
@@ -149,7 +149,7 @@ CREATE TABLE IF NOT EXISTS `pe_LogStats` (
|
||||
KEY `pe_LogStats_masterslot` (`pe_LogStats_masterslot`),
|
||||
KEY `pe_LogStats_mstatus` (`pe_LogStats_mstatus`),
|
||||
KEY `pe_LogStats_seat` (`pe_LogStats_seat`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
DROP TRIGGER IF EXISTS `pe_LogStats_UPDATE`;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER `pe_LogStats_UPDATE` BEFORE UPDATE ON `pe_LogStats` FOR EACH ROW BEGIN
|
||||
@@ -182,7 +182,7 @@ CREATE TABLE IF NOT EXISTS `pe_OnlinePlayers` (
|
||||
`pe_OnlinePlayers_slot` varchar(255) DEFAULT NULL,
|
||||
`pe_OnlinePlayers_ucid` varchar(255) DEFAULT NULL,
|
||||
`pe_OnlinePlayers_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `pe_OnlineStatus`;
|
||||
CREATE TABLE IF NOT EXISTS `pe_OnlineStatus` (
|
||||
@@ -197,7 +197,7 @@ CREATE TABLE IF NOT EXISTS `pe_OnlineStatus` (
|
||||
`pe_OnlineStatus_perunversion_winapp` varchar(255) DEFAULT NULL,
|
||||
`pe_OnlineStatus_perunversion_dcshook` varchar(255) DEFAULT NULL,
|
||||
`pe_OnlineStatus_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
ALTER TABLE `pe_DataMissionHashes` ADD FULLTEXT KEY `pe_DataMissionHashes_hash` (`pe_DataMissionHashes_hash`);
|
||||
|
||||
238
AUDIT.md
238
AUDIT.md
@@ -1,238 +0,0 @@
|
||||
# Audit de reprise — Perun for DCS World
|
||||
|
||||
> Revue menée pour décider d'une reprise du projet (upstream `szporwolik/perun`,
|
||||
> **archivé le 2026-03-29**, licence MIT). Fork de travail : `DaKerboul/perun`,
|
||||
> miroir Gitea : `git.kerboul.me/kerboul/perun`.
|
||||
>
|
||||
> **Méthode.** Revue à 5 casquettes (Architecte, Sécurité, Backend .NET, DCS/Lua,
|
||||
> DevOps/DBA). **Périmètre audité** : le code réellement écrit par le projet —
|
||||
> hook Lua (`01_DCS`), app C# WinForms (`02_Windows_App`), wrapper C++ (`03_*`,
|
||||
> hors arbre Lua 5.1.5 vendoré), schéma MySQL (`04_MySQL`), exemple PHP
|
||||
> (`05_Misc`). Les ~35 `.c`/`.h` de `lua-5.1.5/` sont du Lua amont vendoré et **ne
|
||||
> sont pas audités**.
|
||||
> Date : 2026-06-11.
|
||||
|
||||
---
|
||||
|
||||
## Synthèse — verdict
|
||||
|
||||
Projet **fonctionnellement riche et bien pensé sur le fond** (modèle de données
|
||||
propre, couverture événementielle DCS complète, multi-instances, intégrations
|
||||
SRS/LotATC). Mais **dette de sécurité critique** et code applicatif daté
|
||||
(monolithes, threading artisanal, zéro test, zéro CI, Windows/.NET Framework
|
||||
uniquement). C'est une **bonne base à reprendre**, à condition de traiter les
|
||||
points P0 **avant** toute remise en production.
|
||||
|
||||
| # | Domaine | Gravité | Sévérité |
|
||||
|---|---------|---------|----------|
|
||||
| S1 | Injection SQL via données joueur (app C#) | **Critique** | 🔴 P0 |
|
||||
| S2 | Port TCP sans authentification, bind `0.0.0.0` | **Critique** | 🔴 P0 |
|
||||
| S3 | XSS stocké dans l'exemple PHP | Élevé | 🟠 P1 |
|
||||
| S4 | Mot de passe MySQL stocké en clair | Élevé | 🟠 P1 |
|
||||
| S5 | PII (IP, UCID) sans rétention ni consentement (RGPD) | Élevé | 🟠 P1 |
|
||||
| B1 | Threading non synchronisé (buffer partagé) | Élevé | 🟠 P1 |
|
||||
| B2 | `ExecuteReader` pour des INSERT/UPDATE, 1 connexion/frame | Moyen | 🟡 P2 |
|
||||
| A1 | Monolithes, `dynamic`, aucun test, aucune CI | Moyen | 🟡 P2 |
|
||||
| L1 | Fuite de variables globales dans le hook DCS | Moyen | 🟡 P2 |
|
||||
| D1 | Lua 5.1.5 vendoré dans le repo, pas de build reproductible | Moyen | 🟡 P2 |
|
||||
| D2 | Pas de FK, dépendance à `STRICT_TRANS_TABLES` off, pas de migrations | Faible | 🟢 P3 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecte / Lead — structure & dette
|
||||
|
||||
**Points forts**
|
||||
- Découpage fonctionnel clair en 5 dossiers numérotés, lisible d'emblée.
|
||||
- Séparation nette des responsabilités : collecte (Lua) → transport (TCP/DLL) →
|
||||
persistance (C#) → restitution (PHP). Le protocole de trames (IDs 1/2/3/50…101)
|
||||
est documenté dans le README.
|
||||
- Multi-instances supporté de bout en bout (champ `instance` partout).
|
||||
|
||||
**Points faibles**
|
||||
- **Monolithes.** `DatabaseController.SendToMySql` fait ~290 lignes avec un `switch`
|
||||
géant mêlant construction SQL, mapping métier et logging
|
||||
(`01_Classes/DatabaseController.cs:12-304`). Idem `TCPController.StartListen`
|
||||
(boucles imbriquées sur ~170 lignes).
|
||||
- **`dynamic` partout** pour le JSON entrant (`DatabaseController.cs:25`,
|
||||
`TCPController.cs:121`) : aucune validation de schéma, accès `TCPFrame.payload.x`
|
||||
qui lèvent au moindre champ manquant → exceptions au lieu d'un rejet propre.
|
||||
- **Plateforme verrouillée** : VS2017, .NET Framework 4.8, WinForms → Windows
|
||||
uniquement, fin de vie. Or le serveur DCS est Windows, mais l'app de
|
||||
persistance n'a aucune raison d'y être clouée (elle ne parle que TCP + MySQL).
|
||||
- **TODO laissé en dur** (commentaire polonais) :
|
||||
`DatabaseController.cs:92` « TUTAJ DODAC CATCH TBD » → gestion d'erreur inachevée.
|
||||
- **Aucun test, aucune CI**, dérive de version (cf. §4 L-version).
|
||||
|
||||
**À faire**
|
||||
- [ ] Extraire la construction SQL dans une couche dédiée (un handler par type de
|
||||
trame) + DTO typés à la place de `dynamic`.
|
||||
- [ ] Cibler **.NET 8** + un worker headless multiplateforme ; garder l'UI WinForms
|
||||
en option (ou la remplacer par un petit panneau web/CLI).
|
||||
- [ ] Introduire des tests unitaires (parsing de trames, génération SQL) et un
|
||||
pipeline CI.
|
||||
|
||||
## 2. Sécurité (AppSec) — **bloquant pour la prod**
|
||||
|
||||
**S1 — Injection SQL via données contrôlées par le joueur. 🔴**
|
||||
L'app paramètre *certaines* valeurs (`@PAR_*`) mais **en concatène des dizaines
|
||||
d'autres** directement dans le SQL, dont des champs que n'importe quel joueur
|
||||
maîtrise (UCID, nom, hash de mission, IP, datetime, tous les compteurs `ps_*`) :
|
||||
- `DatabaseController.cs:128-132` (chat) : `ucid`, `missionhash`, `all`, `datetime`.
|
||||
- `DatabaseController.cs:159-165` (stats) : `stat_ucid`, `stat_missionhash` et
|
||||
~30 valeurs `stat_data_perun.ps_*` injectées telles quelles.
|
||||
- `DatabaseController.cs:175-177` (login) : `login_ucid`, `login_ipaddr`, `login_datetime`.
|
||||
- `DatabaseController.cs:50-51, 77-89, 205-206` : `instance`/`type` concaténés.
|
||||
|
||||
Un UCID/nom forgé (ou un module client modifié) permet l'exfiltration ou la
|
||||
destruction de la base. C'est le **défaut n°1 à corriger**.
|
||||
→ **Tout** passer en requêtes paramétrées (`MySqlParameter`), sans exception.
|
||||
|
||||
**S2 — Listener TCP non authentifié, exposé. 🔴**
|
||||
`new TcpListener(IPAddress.Any, intListenPort)` (`TCPController.cs:51`) écoute sur
|
||||
**toutes** les interfaces, **sans authentification ni allowlist**. Couplé à S1,
|
||||
n'importe quel hôte joignant le port (48621 par défaut) injecte des trames
|
||||
arbitraires → compromission complète de la base.
|
||||
→ Par défaut **bind `127.0.0.1`** (hook et app sont quasi toujours sur la même
|
||||
machine), + secret partagé/HMAC sur les trames, + allowlist d'IP.
|
||||
|
||||
**S3 — XSS stocké (PHP). 🟠**
|
||||
`05_Misc/05_PHP_Example/index.php` réinjecte en HTML des données joueur **sans
|
||||
échappement** : message de chat (`:110`), nom (`:92, :109, :143`),
|
||||
contenu d'événement (`:127`). Un joueur dont le nom vaut `<script>…</script>`
|
||||
exécute du JS dans le navigateur de l'admin. Les requêtes elles-mêmes sont
|
||||
statiques (pas de SQLi côté PHP), le risque est l'**XSS**.
|
||||
→ `htmlspecialchars()` systématique sur toute sortie ; corriger aussi le HTML
|
||||
invalide (`<h1>` fermé par `</h2>` ligne 44).
|
||||
|
||||
**S4 — Identifiants MySQL en clair. 🟠**
|
||||
Le mot de passe est stocké en `String` dans les user settings .NET
|
||||
(`02_Forms/form_Main.cs:119`, clé `MYSQL_Password` de `app.config`) → écrit en
|
||||
clair dans `user.config`.
|
||||
→ Chiffrer via **DPAPI** (`ProtectedData`) ou déléguer à un gestionnaire de
|
||||
secrets ; a minima ne jamais journaliser la chaîne de connexion.
|
||||
|
||||
**S5 — Données personnelles (RGPD). 🟠**
|
||||
Le hook collecte et stocke **adresses IP** et **UCID** des joueurs
|
||||
(`Perun-hook.lua:364-366` → `pe_DataPlayers_lastip`, `pe_LogLogins_ip`), sans
|
||||
politique de rétention ni information des joueurs.
|
||||
→ Définir une durée de rétention + purge, anonymiser/hacher l'IP si non
|
||||
nécessaire, documenter (mention serveur + Discord).
|
||||
|
||||
## 3. Backend / .NET — qualité & robustesse
|
||||
|
||||
**B1 — Threading artisanal non synchronisé. 🟠**
|
||||
Le thread TCP écrit dans `Globals.arrMySQLSendBuffer` (tableau fixe) pendant que
|
||||
le thread d'envoi le lit, **sans verrou** (`TCPController.cs:130-142`). Buffer
|
||||
plein = paquets **silencieusement perdus** (`:139-141`), scan linéaire O(n) par
|
||||
paquet. Accès concurrents → corruption/race.
|
||||
→ Remplacer par une `BlockingCollection<T>`/`Channel<T>` thread-safe et bornée.
|
||||
|
||||
**B2 — Accès base inefficace. 🟡**
|
||||
- `ExecuteReader()` utilisé pour exécuter des lots d'INSERT/UPDATE
|
||||
(`DatabaseController.cs:217`) : devrait être `ExecuteNonQuery()`.
|
||||
- **Une nouvelle `MySqlConnection` ouverte/fermée par trame** (`:34, :292`) :
|
||||
pas de réutilisation du pool, surcoût réseau par paquet.
|
||||
- Tout est **synchrone bloquant** (pas d'`async`/`await`).
|
||||
→ Connexion/pool réutilisé, requêtes asynchrones, `ExecuteNonQueryAsync`.
|
||||
|
||||
**Autres**
|
||||
- Dépendance `Newtonsoft.Json` → migrable vers `System.Text.Json`.
|
||||
- Gestion d'erreurs par numéros MySQL en dur (`:246-279`) : utile mais fragile,
|
||||
à compléter (le catch manquant signalé en `:92`).
|
||||
|
||||
## 4. DCS / Lua / Intégration
|
||||
|
||||
**Points forts**
|
||||
- Couverture événementielle DCS **très complète** : kill (avec catégorisation
|
||||
PvP/AI), friendly fire, crash, eject, takeoff/landing (airfield/ship/FARP),
|
||||
multicrew, change_slot, connect/disconnect, chat, MOTD. C'est le vrai actif du
|
||||
projet (`Perun-hook.lua:725-879`).
|
||||
- Comptage de stats maison car les stats natives DCS sont peu fiables (choix
|
||||
assumé et pertinent).
|
||||
|
||||
**Points faibles**
|
||||
- **L1 — Fuite de variables globales 🟡** : plusieurs variables sont assignées
|
||||
sans `local` dans l'environnement *hook* (privilégié et partagé) — ex.
|
||||
`_temp_killers`/`_temp_event_type` (`:754-756`), `_master_type`/`_master_slot`/
|
||||
`_sub_slot` (`:813`), `_temp_airfield` (`:849`). Risque de collision avec
|
||||
d'autres hooks installés sur le serveur.
|
||||
- **L-version — dérive de version** : la version du hook est codée en dur
|
||||
`"v0.12.1"` (`:30`) et vit séparément de la version de l'app (`Globals.VersionPerun`).
|
||||
→ source unique de vérité (tag git → injecté au build).
|
||||
- **Coût par frame** : `onSimulationFrame` fait du `table.concat`/JSON à chaque
|
||||
frame ; sur serveur chargé, surveiller le budget temps (déjà mesuré en µs dans
|
||||
les logs — bon réflexe à conserver/exposer).
|
||||
- **Dépendance à une DLL compilée** (`perun.dll`, issue de `03_Perun_Lua_Wrapper`)
|
||||
livrée hors repo : reproductibilité du build à fiabiliser (cf. D1).
|
||||
- TCP **en clair**, pas de TLS (acceptable en loopback, à revoir si distant).
|
||||
|
||||
**À faire**
|
||||
- [ ] `local` sur toutes les temporaires ; passe `luacheck`.
|
||||
- [ ] Versionner hook + app depuis le tag git.
|
||||
- [ ] Documenter/reproduire le build de `perun.dll` (CMake déjà présent).
|
||||
|
||||
## 5. DevOps / DBA / Release
|
||||
|
||||
**Base de données — plutôt saine.**
|
||||
- InnoDB + `utf8mb4`/`unicode_ci`, PK/`AUTO_INCREMENT`, **clés UNIQUE**
|
||||
pertinentes (UCID, hash, type, stats par mission+ucid+type) et index sur les
|
||||
colonnes de tri (`datetime`, `instance`, `type`) — `04_MySQL/m1081_perun.sql`.
|
||||
- **D2 🟢** : pas de **`FOREIGN KEY`** (intégrité référentielle non garantie),
|
||||
dépendance documentée à **`STRICT_TRANS_TABLES` désactivé** (README:42,84) —
|
||||
c'est-à-dire qu'on s'appuie sur la coercition/troncature silencieuse de MySQL,
|
||||
ce qui masque des bugs. Nom de fichier cryptique (`m1081_perun.sql`).
|
||||
- Pas d'outil de **migration** (un seul dump). → introduire des migrations
|
||||
versionnées (Flyway/dbmate/sqitch) et, à terme, des FK + un mode strict assumé.
|
||||
|
||||
**Build / release / repo**
|
||||
- **D1 🟡** : arbre **Lua 5.1.5 complet vendoré** dans `03_Perun_Lua_Wrapper/lua-5.1.5/`
|
||||
→ gonfle le repo et l'audit. Le passer en sous-module / téléchargement au build.
|
||||
- Build **manuel VS2017**, pas de CI, pas d'artefacts reproductibles.
|
||||
- Contributions historiquement attendues sur la branche `dev` (README:135).
|
||||
- Pas de `.gitignore` racine (un seul dans le wrapper).
|
||||
|
||||
**À faire**
|
||||
- [ ] CI (build C# + `luacheck` + lint PHP + validation du schéma SQL).
|
||||
- [ ] Migrations DB versionnées ; activer les FK progressivement.
|
||||
- [ ] Sortir Lua amont du repo ; pipeline de build de la DLL.
|
||||
- [ ] Releases taguées avec binaires (`perun.dll` + app) attachés.
|
||||
|
||||
---
|
||||
|
||||
## Feuille de route priorisée
|
||||
|
||||
**P0 — Sécurité bloquante (avant toute prod)**
|
||||
1. Paramétrer **100 %** des requêtes SQL (S1).
|
||||
2. Bind loopback par défaut + auth/allowlist sur le listener TCP (S2).
|
||||
|
||||
**P1 — Durcissement & conformité**
|
||||
3. Échappement HTML de l'exemple PHP (S3).
|
||||
4. Chiffrer le mot de passe MySQL — DPAPI (S4).
|
||||
5. Rétention/anonymisation IP & UCID, doc RGPD (S5).
|
||||
6. File thread-safe bornée à la place du buffer tableau (B1).
|
||||
|
||||
**P2 — Modernisation**
|
||||
7. Découper les monolithes, DTO typés au lieu de `dynamic` (A1).
|
||||
8. Connexion poolée + accès DB async + `ExecuteNonQuery` (B2).
|
||||
9. `local` + `luacheck` sur le hook, version unifiée depuis git (L1).
|
||||
10. Cibler .NET 8 / worker multiplateforme.
|
||||
11. Tests + CI.
|
||||
|
||||
**P3 — Hygiène long terme**
|
||||
12. Migrations DB, FK, mode SQL strict assumé (D2).
|
||||
13. Désvendoriser Lua, build reproductible de la DLL (D1).
|
||||
14. Releases taguées + artefacts.
|
||||
|
||||
## Plan de reprise suggéré
|
||||
|
||||
1. **Geler le comportement** : quelques tests de caractérisation sur le parsing de
|
||||
trames et la génération SQL, pour refactorer sans régresser.
|
||||
2. **Sprint sécu (P0)** sur une branche `security/sql-and-tcp`, puis tag
|
||||
`v0.13.0-rc` testé en loopback.
|
||||
3. **P1** en incréments livrables.
|
||||
4. Décider de la cible app (garder WinForms vs worker .NET 8) **avant** d'attaquer
|
||||
P2 — ça oriente tout le refactor.
|
||||
|
||||
> Adapté à ton contexte : tu fais déjà tourner Gitea + CI sur le cluster ; un
|
||||
> pipeline build/lint Perun s'y intègre directement, et la communauté Commus DCS
|
||||
> est un terrain de test naturel pour les stats.
|
||||
10
README.md
10
README.md
@@ -1,10 +1,3 @@
|
||||
> [!IMPORTANT]
|
||||
> This repository is no longer actively maintained.
|
||||
>
|
||||
> I have not hosted games or played DCS for a long time, so I decided to archive this project.
|
||||
>
|
||||
> The code will remain available for reference. Anyone interested is welcome to fork the repository and continue maintaining or improving it.
|
||||
|
||||

|
||||

|
||||

|
||||
@@ -83,9 +76,6 @@ That probably means that your database does not support JSON functions, you shal
|
||||
### I keep getting 1364 MySQL error
|
||||
Make sure that STRICT_TRANS_TABLES is disabled at your MySQL server, see: https://stackoverflow.com/questions/37964325/how-to-find-and-disable-mysql-strict-mode
|
||||
|
||||
### Dynamic slots (DCS 2.9.6 onwards) are not tracked
|
||||
Not yet handled. (Existing server-side events do not support this yet).
|
||||
|
||||
### Carrier landing are not tracked correctly
|
||||
DCS API does not track carrier or FARP operations natively, so there is a trick to achive that. At this point of time to detect FARP operations, the FARPs shall have "FARP" string in the group name (set in mission editor). For carrier operations the string "SHIP" is required within group name.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user