25 Commits

Author SHA1 Message Date
0f2ec7de29 docs: audit de reprise (revue 5 casquettes) avant reprise du projet
Ajoute AUDIT.md : revue archi / securite / backend .NET / DCS-Lua / DevOps-DBA
du fork de reprise. Constats majeurs P0 : injection SQL via donnees joueur
(DatabaseController.cs) et listener TCP non authentifie expose (TCPController.cs).
Feuille de route priorisee P0->P3 + plan de reprise inclus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:31:50 +02:00
Szymon Porwolik
28fba57604 Update README to reflect project archival status
Added a notice indicating that the repository is no longer actively maintained and archived.
2026-03-29 01:23:14 +01:00
ags
da26e9b37f Merge pull request #76 from VEAF/nil-exception-protection
Nil exception protection
2024-08-22 23:42:54 +01:00
ags
9ddf139767 Merge pull request #75 from VEAF/make-record-chat-message-optional
Make recording of chat messages optional
2024-08-22 23:42:42 +01:00
ags
af310ba403 Merge pull request #77 from VEAF/stop-after-sqlerror-1366
Stop after sqlerror 1366
2024-08-22 23:42:27 +01:00
ags
661d05526d Merge pull request #84 from szporwolik/dependabot/nuget/02_Windows_App/Perun_v1/BouncyCastle-1.8.9
Bump BouncyCastle from 1.8.3.1 to 1.8.9 in /02_Windows_App/Perun_v1
2024-08-22 23:42:00 +01:00
ags
2ce3836130 Update README.md 2024-07-13 16:10:04 +01:00
dependabot[bot]
7fd4a1e95c Bump BouncyCastle from 1.8.3.1 to 1.8.9 in /02_Windows_App/Perun_v1
Bumps BouncyCastle from 1.8.3.1 to 1.8.9.

---
updated-dependencies:
- dependency-name: BouncyCastle
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-30 19:15:03 +00:00
ags
b26b5b115b Merge pull request #82 from szporwolik/dependabot/nuget/02_Windows_App/Perun_v1/Newtonsoft.Json-13.0.1
Bump Newtonsoft.Json from 12.0.2 to 13.0.1 in /02_Windows_App/Perun_v1
2022-06-29 01:02:10 +02:00
ags
0fd2633d5b Merge pull request #81 from szporwolik/dependabot/nuget/02_Windows_App/Perun_v1/00_Config/Newtonsoft.Json-13.0.1
Bump Newtonsoft.Json from 12.0.1 to 13.0.1 in /02_Windows_App/Perun_v1/00_Config
2022-06-29 01:01:49 +02:00
dependabot[bot]
d239f63a56 Bump Newtonsoft.Json from 12.0.2 to 13.0.1 in /02_Windows_App/Perun_v1
Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 12.0.2 to 13.0.1.
- [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases)
- [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/12.0.2...13.0.1)

---
updated-dependencies:
- dependency-name: Newtonsoft.Json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-22 21:54:15 +00:00
dependabot[bot]
25dcbb818b Bump Newtonsoft.Json in /02_Windows_App/Perun_v1/00_Config
Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 12.0.1 to 13.0.1.
- [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases)
- [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/12.0.1...13.0.1)

---
updated-dependencies:
- dependency-name: Newtonsoft.Json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-22 20:42:38 +00:00
ags
7074503039 Merge pull request #79 from szporwolik/dependabot/nuget/02_Windows_App/Perun_v1/Google.Protobuf-3.15.0
Bump Google.Protobuf from 3.6.1 to 3.15.0 in /02_Windows_App/Perun_v1
2022-06-02 01:12:59 +01:00
ags
d0620a3dca Merge pull request #78 from szporwolik/dependabot/nuget/02_Windows_App/Perun_v1/00_Config/Google.Protobuf-3.15.0
Bump Google.Protobuf from 3.5.1 to 3.15.0 in /02_Windows_App/Perun_v1/00_Config
2022-06-02 01:12:18 +01:00
dependabot[bot]
8ba30d31cd Bump Google.Protobuf from 3.6.1 to 3.15.0 in /02_Windows_App/Perun_v1
Bumps [Google.Protobuf](https://github.com/protocolbuffers/protobuf) from 3.6.1 to 3.15.0.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.6.1...v3.15.0)

---
updated-dependencies:
- dependency-name: Google.Protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 23:51:05 +00:00
dependabot[bot]
009117d3c5 Bump Google.Protobuf in /02_Windows_App/Perun_v1/00_Config
Bumps [Google.Protobuf](https://github.com/protocolbuffers/protobuf) from 3.5.1 to 3.15.0.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.5.1...v3.15.0)

---
updated-dependencies:
- dependency-name: Google.Protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 23:50:43 +00:00
ags
483b15276e Merge pull request #80 from szporwolik/dependabot/nuget/02_Windows_App/Perun_v1/SSH.NET-2020.0.2
Bump SSH.NET from 2016.1.0 to 2020.0.2 in /02_Windows_App/Perun_v1
2022-06-02 00:50:03 +01:00
dependabot[bot]
07f8b14611 Bump SSH.NET from 2016.1.0 to 2020.0.2 in /02_Windows_App/Perun_v1
Bumps [SSH.NET](https://github.com/sshnet/SSH.NET) from 2016.1.0 to 2020.0.2.
- [Release notes](https://github.com/sshnet/SSH.NET/releases)
- [Commits](https://github.com/sshnet/SSH.NET/compare/2016.1.0...2020.0.2)

---
updated-dependencies:
- dependency-name: SSH.NET
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 23:25:17 +00:00
Andrzej Grzesik
6648d5cad3 Update app.config
Bumped default port to what's used in the docs
2021-12-26 00:51:38 +00:00
ags
4bbb2ad98a WinApp: use utf8mb4 for sql connection 2021-12-20 17:23:19 +00:00
ags
98fc7a799f default tables to utf8mb4 2021-12-20 17:11:05 +00:00
David Pierron
a88109b85c If a packet generates a SQL error 1366 (invalid encoding), it'll be discarded from the queue and never sent to the database again. 2021-06-28 13:14:55 +02:00
David Pierron
c9f55992ad Protected calls to Perun.LogEvent in case of a nil value in the message elements 2021-06-28 13:06:22 +02:00
David Pierron
3cbbcd023f Made chat message recording optional (PerunConfig.RecordChatMessages in the configuration file) 2021-06-28 13:05:02 +02:00
David Pierron
4bea05e7a2 Merge pull request #1 from szporwolik/master
Merged origin/master to our master
2021-04-24 09:47:34 +02:00
34 changed files with 614 additions and 1281 deletions

4
.gitignore vendored
View File

@@ -261,6 +261,4 @@ __pycache__/
*.pyc *.pyc
# cmake folders # cmake folders
cmake-build-debug cmake-build-debug
cmake-build-release

View File

@@ -0,0 +1,27 @@
-- 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

View File

@@ -1,33 +0,0 @@
-- 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

View File

@@ -13,18 +13,18 @@ package.cpath = package.cpath..';'.. lfs.writedir()..'/Mods/services/Perun/bin/'
Perun.DLL = require('perun') Perun.DLL = require('perun')
-- Load config file -- Load config file
local config = require "perun_config" local PerunConfig = require "perun_config"
Perun.RefreshStatus = PerunConfig.RefreshStatus
Perun.RefreshStatus = config.RefreshStatus Perun.TCPTargetPort = PerunConfig.TCPTargetPort
Perun.TCPTargetPort = config.port Perun.TCPPerunHost = PerunConfig.TCPPerunHost
Perun.TCPPerunHost = config.host Perun.Instance = PerunConfig.Instance
Perun.Instance = config.Instance Perun.MissionStartNoDeathWindow = PerunConfig.MissionStartNoDeathWindow
Perun.MissionStartNoDeathWindow = config.MissionStartNoDeathWindow Perun.DebugMode = PerunConfig.DebugMode
Perun.DebugMode = config.DebugMode Perun.MOTD_L1 = PerunConfig.MOTD_L1
Perun.MOTD_L1 = config.MOTD_L1 Perun.MOTD_L2 = PerunConfig.MOTD_L2
Perun.MOTD_L2 = config.MOTD_L2 Perun.ConnectionError = PerunConfig.ConnectionError_L1
Perun.ConnectionError = config.ConnectionError_L1 Perun.BroadcastPerunErrors = PerunConfig.BroadcastPerunErrors
Perun.BroadcastPerunErrors = config.BroadcastPerunErrors Perun.RecordChatMessages = PerunConfig.RecordChatMessages
-- Variable init -- Variable init
Perun.Version = "v0.12.1" Perun.Version = "v0.12.1"
@@ -48,22 +48,13 @@ Perun.lastFrameTime = 0;
Perun.ReconnectTimeout = 30; Perun.ReconnectTimeout = 30;
Perun.RefreshKeepAlive = 3 Perun.RefreshKeepAlive = 3
Perun.State = {} Perun.PTS = function(data) -- ProtectedToString
Perun.State.connected = false; if data == nil then
return "[nil]"
-- to avoid declaring every time a function is run else
Perun.Globals = {} return tostring(data)
Perun.Globals.Sides = { end
[0] = 'SPECTATOR', end
[1] = 'RED',
[2] = 'BLUE',
[3] = 'NEUTRAL', -- TBD check once this is released in DCS
}
Perun.Mission = {}
Perun.Mission.Coalitions = {}
-- ################################ Helper function definitions ################################ -- ################################ Helper function definitions ################################
Perun.GetCategory = function(id) Perun.GetCategory = function(id)
@@ -93,8 +84,14 @@ end
Perun.SideID2Name = function(id) Perun.SideID2Name = function(id)
-- Helper function returns side name per side (coalition) id -- Helper function returns side name per side (coalition) id
if id > 0 and id <= 3 then local _sides = {
return Perun.Globals.Sides[id] [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]
else else
return "?" return "?"
end end
@@ -103,11 +100,11 @@ end
Perun.AddLog = function(text,LogLevel) Perun.AddLog = function(text,LogLevel)
-- Adds logs to DCS.log file -- Adds logs to DCS.log file
if Perun.DebugMode >= LogLevel then if Perun.DebugMode >= LogLevel then
net.log("[Perun] ", text) net.log("[Perun] ".. text)
end end
end end
Perun.generate_mission_hash = function() Perun.GenerateMissionHash = function()
-- Generates unique simulation mission hash -- Generates unique simulation mission hash
return DCS.getMissionName( ).."@".. Perun.Instance .. "@" .. Perun.Version .. "@".. os.date('%Y%m%d_%H%M%S') return DCS.getMissionName( ).."@".. Perun.Instance .. "@" .. Perun.Version .. "@".. os.date('%Y%m%d_%H%M%S')
end end
@@ -251,7 +248,6 @@ Perun.ConnectToPerun = function ()
Perun.DLL.tcpConnect(Perun.TCPPerunHost, Perun.TCPTargetPort) Perun.DLL.tcpConnect(Perun.TCPPerunHost, Perun.TCPTargetPort)
Perun.AddLog(string.format("Connecting to TCP server %s:%i", Perun.TCPPerunHost, Perun.TCPTargetPort), 2) Perun.AddLog(string.format("Connecting to TCP server %s:%i", Perun.TCPPerunHost, Perun.TCPTargetPort), 2)
Perun.State.connected = true;
end end
Perun.SendToPerun = function(data_id, data_package) Perun.SendToPerun = function(data_id, data_package)
@@ -266,7 +262,7 @@ Perun.SendToPerun = function(data_id, data_package)
_payload = net.lua2json(data_package); _payload = net.lua2json(data_package);
end end
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 _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 _tcpMessage= table.concat( _TempData ) local _tcpMessage= table.concat( _TempData )
-- TCP Part - sending -- TCP Part - sending
@@ -633,8 +629,8 @@ end
Perun.onSimulationStart = function() Perun.onSimulationStart = function()
-- Simulation was started -- Simulation was started
Perun.MissionHash=Perun.generate_mission_hash() Perun.MissionHash=Perun.GenerateMissionHash()
Perun.LogEvent("SimStart","Mission " .. Perun.MissionHash .. " started",nil,nil); Perun.LogEvent("SimStart","Mission " .. Perun.PTS(Perun.MissionHash) .. " started",nil,nil);
Perun.StatData = {} Perun.StatData = {}
Perun.StatDataLastType = {} Perun.StatDataLastType = {}
Perun.PlayersTableCache = {} Perun.PlayersTableCache = {}
@@ -645,9 +641,9 @@ end
Perun.onSimulationStop = function() Perun.onSimulationStop = function()
-- Simulation was stopped -- Simulation was stopped
Perun.LogEvent("SimStop","Mission " .. Perun.MissionHash .. " finished",nil,nil); Perun.LogEvent("SimStop","Mission " .. Perun.PTS(Perun.MissionHash) .. " finished",nil,nil);
Perun.LogAllStats() Perun.LogAllStats()
Perun.MissionHash=Perun.generate_mission_hash(); Perun.MissionHash=Perun.GenerateMissionHash();
Perun.StatData = {} Perun.StatData = {}
Perun.MissionData = {} Perun.MissionData = {}
Perun.StatDataLastType = {} Perun.StatDataLastType = {}
@@ -657,14 +653,14 @@ end
Perun.onPlayerDisconnect = function(id, err_code) Perun.onPlayerDisconnect = function(id, err_code)
-- Player disconnected -- Player disconnected
Perun.LogEvent("disconnect", "Player " .. id .. " disconnected.",nil,nil); Perun.LogEvent("disconnect", "Player " .. Perun.PTS(id) .. " disconnected.",nil,nil);
return return
end end
Perun.onPlayerStop = function (id) Perun.onPlayerStop = function (id)
-- Player left the simulation (happens right before a disconnect, if player exited by desire) -- Player left the simulation (happens right before a disconnect, if player exited by desire)
Perun.LogEvent("quit", "Player " .. id .. " quit the server.",nil,nil); Perun.LogEvent("quit", "Player " .. Perun.PTS(id) .. " quit the server.",nil,nil);
return return
end end
@@ -719,7 +715,7 @@ end
Perun.onPlayerTrySendChat = function (playerID, msg, all) Perun.onPlayerTrySendChat = function (playerID, msg, all)
-- Somebody tries to send chat message -- Somebody tries to send chat message
if msg~=Perun.MOTD_L1 and msg~=Perun.MOTD_L2 and msg~=Perun.ConnectionError then if Perun.RecordChatMessages and msg~=Perun.MOTD_L1 and msg~=Perun.MOTD_L2 and msg~=Perun.ConnectionError then
Perun.LogChat(playerID,msg,all) Perun.LogChat(playerID,msg,all)
end end
@@ -729,24 +725,6 @@ end
Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7) Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
-- Game event has occured -- Game event has occured
local _now = DCS.getRealTime() 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) Perun.AddLog("Event handler for ".. eventName .. " started",2)
if eventName == "friendly_fire" then if eventName == "friendly_fire" then
@@ -755,11 +733,11 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
arg2 = "Cannon" arg2 = "Cannon"
end end
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); 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);
elseif eventName == "mission_end" then elseif eventName == "mission_end" then
--"mission_end", winner, msg --"mission_end", winner, msg
Perun.LogEvent(eventName,"Mission finished, winner " .. arg1 .. " message: " .. arg2,nil,nil); Perun.LogEvent(Perun.PTS(eventName),"Mission finished, winner " .. Perun.PTS(arg1) .. " message: " .. Perun.PTS(arg2),nil,nil);
elseif eventName == "kill" then elseif eventName == "kill" then
--"kill", killerPlayerID, killerUnitType, killerSide, victimPlayerID, victimUnitType, victimSide, weaponName --"kill", killerPlayerID, killerUnitType, killerSide, victimPlayerID, victimUnitType, victimSide, weaponName
@@ -822,12 +800,12 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
victim_vehicle = "?" victim_vehicle = "?"
end end
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)); 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));
elseif eventName == "self_kill" then elseif eventName == "self_kill" then
--"self_kill", playerID --"self_kill", playerID
Perun.LogStats(arg1); Perun.LogStats(arg1);
Perun.LogEvent(eventName,net.get_player_info(arg1, "name") .. " killed himself",nil,nil); Perun.LogEvent(Perun.PTS(eventName),Perun.PTS(net.get_player_info(arg1, "name")) .. " killed himself",nil,nil);
elseif eventName == "change_slot" then elseif eventName == "change_slot" then
--"change_slot", playerID, slotID, prevSide --"change_slot", playerID, slotID, prevSide
@@ -838,7 +816,7 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
else else
_sub_slot =" (" .. _sub_slot .. ") " _sub_slot =" (" .. _sub_slot .. ") "
end end
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.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.LogStats(arg1); Perun.LogStats(arg1);
Perun.LogStatsCount(arg1,"init") Perun.LogStatsCount(arg1,"init")
@@ -847,23 +825,23 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
elseif eventName == "connect" then elseif eventName == "connect" then
--"connect", playerID, name --"connect", playerID, name
Perun.LogLogin(arg1); Perun.LogLogin(arg1);
Perun.LogEvent(eventName,"Player "..net.get_player_info(arg1, "name") .. " connected",nil,nil); Perun.LogEvent(Perun.PTS(eventName),"Player "..Perun.PTS(net.get_player_info(arg1, "name")) .. " connected",nil,nil);
Perun.PlayersTableCache["p"..arg1]=net.get_player_info(arg1); Perun.PlayersTableCache["p"..arg1]=net.get_player_info(arg1);
elseif eventName == "disconnect" then elseif eventName == "disconnect" then
--"disconnect", playerID, name, playerSide, reason_code --"disconnect", playerID, name, playerSide, reason_code
Perun.LogEvent(eventName,"Player " .. arg2 .. " disconnected (".. arg4 .. ")." ,arg4,nil); Perun.LogEvent(Perun.PTS(eventName),"Player " .. Perun.PTS(arg2) .. " disconnected (".. Perun.PTS(arg4) .. ")." ,arg4,nil);
Perun.LogStats(arg1); Perun.LogStats(arg1);
elseif eventName == "crash" then elseif eventName == "crash" then
--"crash", playerID, unit_missionID --"crash", playerID, unit_missionID
Perun.LogStatsCountCrew (arg1,"crash") Perun.LogStatsCountCrew (arg1,"crash")
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " crashed in " .. DCS.getUnitType(arg2),nil,nil); 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);
elseif eventName == "eject" then elseif eventName == "eject" then
--"eject", playerID, unit_missionID --"eject", playerID, unit_missionID
Perun.LogStatsCountCrew (arg1,"eject") -- TBD crew or initiator only? Perun.LogStatsCountCrew (arg1,"eject") -- TBD crew or initiator only?
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " ejected " .. DCS.getUnitType(arg2),nil,nil); 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);
elseif eventName == "takeoff" then elseif eventName == "takeoff" then
--"takeoff", playerID, unit_missionID, airdromeName --"takeoff", playerID, unit_missionID, airdromeName
@@ -874,7 +852,7 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
end end
Perun.LogStatsCountCrew (arg1,Perun.GetTakeOffLandingEvent(true,arg3)) Perun.LogStatsCountCrew (arg1,Perun.GetTakeOffLandingEvent(true,arg3))
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); 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);
elseif eventName == "landing" then elseif eventName == "landing" then
--"landing", playerID, unit_missionID, airdromeName --"landing", playerID, unit_missionID, airdromeName
@@ -885,15 +863,15 @@ Perun.onGameEvent = function (eventName,arg1,arg2,arg3,arg4,arg5,arg6,arg7)
end end
Perun.LogStatsCountCrew (arg1,Perun.GetTakeOffLandingEvent(false,arg3)) Perun.LogStatsCountCrew (arg1,Perun.GetTakeOffLandingEvent(false,arg3))
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); 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);
elseif eventName == "pilot_death" then elseif eventName == "pilot_death" then
--"pilot_death", playerID, unit_missionID --"pilot_death", playerID, unit_missionID
Perun.LogStatsCountCrew (arg1,"pilot_death") -- TBD crew or initiator only? Perun.LogStatsCountCrew (arg1,"pilot_death") -- TBD crew or initiator only?
Perun.LogEvent(eventName, Perun.SideID2Name( net.get_player_info(arg1, "side")) .. " player(s) " .. Perun.GetMulticrewCrewNames(arg1) .. " in " .. DCS.getUnitType(arg2) .. " died",nil,nil); 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);
else else
Perun.LogEvent(eventName,"Unknown event type",nil,nil); Perun.LogEvent(Perun.PTS(eventName),"Unknown event type",nil,nil);
end end
local _delay = (DCS.getRealTime() - _now) * 1000000 local _delay = (DCS.getRealTime() - _now) * 1000000
@@ -904,7 +882,7 @@ end
if DCS.isServer() then if DCS.isServer() then
-- If this game instance is hosting multiplayer game, start Perun -- If this game instance is hosting multiplayer game, start Perun
Perun.DLL.StartOfApp() -- Start the main Perun dll Perun.DLL.StartOfApp() -- Start the main Perun dll
Perun.MissionHash=Perun.generate_mission_hash() -- Generate initial missionhash Perun.MissionHash=Perun.GenerateMissionHash() -- Generate initial missionhash
DCS.setUserCallbacks(Perun) -- Set user callbacs, map DCS event handlers with functions defined above 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.AddLog("Loaded - Perun for DCS World - version: " .. Perun.Version,0) -- Display perun information in log
Perun.ConnectToPerun() -- Connect to Perun server Perun.ConnectToPerun() -- Connect to Perun server

View File

@@ -1,99 +0,0 @@
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

View File

@@ -1,497 +0,0 @@
-- 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

View File

@@ -1,55 +0,0 @@
|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.

View File

@@ -1,38 +0,0 @@
@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

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Google.Protobuf" version="3.5.1" targetFramework="net461" /> <package id="Google.Protobuf" version="3.15.0" targetFramework="net461" />
<package id="MySql.Data" version="8.0.15" targetFramework="net461" /> <package id="MySql.Data" version="8.0.15" targetFramework="net461" />
<package id="Newtonsoft.Json" version="12.0.1" targetFramework="net461" /> <package id="Newtonsoft.Json" version="13.0.1" targetFramework="net461" />
</packages> </packages>

View File

@@ -261,6 +261,14 @@ public class DatabaseController
DatabaseStatus = true; // True as connection is not broken DatabaseStatus = true; // True as connection is not broken
ReturnValue = 1; // Return a value to remove this query from quae ReturnValue = 1; // Return a value to remove this query from quae
break; 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 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 - 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 - query: {SQLQueryTxt}", 1, 1, TCPFrameType);

View File

@@ -231,7 +231,7 @@ namespace Perun_v1
trayIconMain.Text = this.Text; // Set notification icon text trayIconMain.Text = this.Text; // Set notification icon text
// Prepare MySQL connection string // 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}"; 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";
// Start listening // Start listening
PerunHelper.LogInfo(ref Globals.AppLogHistory, "Opening connections", 0, 1); PerunHelper.LogInfo(ref Globals.AppLogHistory, "Opening connections", 0, 1);

View File

@@ -35,7 +35,7 @@
<value>3306</value> <value>3306</value>
</setting> </setting>
<setting name="DCS_Server_Port" serializeAs="String"> <setting name="DCS_Server_Port" serializeAs="String">
<value>48620</value> <value>48621</value>
</setting> </setting>
<setting name="DCS_Instance" serializeAs="String"> <setting name="DCS_Instance" serializeAs="String">
<value>1</value> <value>1</value>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="BouncyCastle" version="1.8.3.1" targetFramework="net48" /> <package id="BouncyCastle" version="1.8.9" targetFramework="net48" />
<package id="Google.Protobuf" version="3.6.1" targetFramework="net48" /> <package id="Google.Protobuf" version="3.15.0" targetFramework="net48" />
<package id="MySql.Data" version="8.0.17" targetFramework="net48" /> <package id="MySql.Data" version="8.0.17" targetFramework="net48" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net48" /> <package id="Newtonsoft.Json" version="13.0.1" targetFramework="net48" />
<package id="SSH.NET" version="2016.1.0" targetFramework="net48" /> <package id="SSH.NET" version="2020.0.2" targetFramework="net48" />
</packages> </packages>

View File

@@ -1,18 +1,12 @@
# CMakeList.txt : CMake project for perun_dll, include source and define # CMakeList.txt : CMake project for perun_dll, include source and define
# project specific logic here. # project specific logic here.
# #
cmake_minimum_required(VERSION 3.17) cmake_minimum_required (VERSION 3.8)
project ("parent") project ("parent")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set_property(GLOBAL PROPERTY USE_FOLDERS ON) 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(lua-5.1.5)
add_subdirectory(pierog) add_subdirectory(perun)
add_subdirectory(experimental)

View File

@@ -1,21 +0,0 @@
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")

View File

@@ -28,7 +28,7 @@ set (LUA_RUNTIME_SOURCES
"src/lua.h" "src/lua.h"
"src/lualib.h" "src/lualib.h"
"src/lzio.c" "src/lzio.h" "src/lzio.c" "src/lzio.h"
../experimental/src/main.cpp) )
add_library( lua-5.1.5 ${LUA_RUNTIME_SOURCES} ) add_library( lua-5.1.5 ${LUA_RUNTIME_SOURCES} )

View File

@@ -44,7 +44,7 @@ void luaK_nil (FuncState *fs, int from, int n) {
if (GET_OPCODE(*previous) == OP_LOADNIL) { if (GET_OPCODE(*previous) == OP_LOADNIL) {
int pfrom = GETARG_A(*previous); int pfrom = GETARG_A(*previous);
int pto = GETARG_B(*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) if (from+n-1 > pto)
SETARG_B(*previous, from+n-1); SETARG_B(*previous, from+n-1);
return; return;

View File

@@ -91,7 +91,7 @@
".\\?.lua;" LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \ ".\\?.lua;" LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \
LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua" LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua"
#define LUA_CPATH_DEFAULT \ #define LUA_CPATH_DEFAULT \
".\\?.bin;" LUA_CDIR"?.bin;" LUA_CDIR"loadall.bin" ".\\?.dll;" LUA_CDIR"?.dll;" LUA_CDIR"loadall.dll"
#else #else
#define LUA_ROOT "/usr/local/" #define LUA_ROOT "/usr/local/"

View File

@@ -0,0 +1,23 @@
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")

View File

@@ -0,0 +1,48 @@
#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

View File

@@ -0,0 +1,124 @@
#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);
}
}

View File

@@ -0,0 +1,55 @@
#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;
}

View File

@@ -12,5 +12,6 @@ extern "C" {
#include <fstream> #include <fstream>
#include <chrono> #include <chrono>
#include <queue> #include <queue>
#include "Connection.h"
#endif //PERUN_LIBRARY_H #endif //PERUN_LIBRARY_H

View File

@@ -1,25 +0,0 @@
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})

View File

@@ -1,102 +0,0 @@
#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();
}

View File

@@ -1,40 +0,0 @@
#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

View File

@@ -1,41 +0,0 @@
#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();
}

View File

@@ -1,27 +0,0 @@
#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

View File

@@ -1,56 +0,0 @@
#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;
}

View File

@@ -1,38 +0,0 @@
#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

View File

@@ -1,99 +0,0 @@
#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;
}

View File

@@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS `pe_Config` (
`pe_Config_id` int(11) NOT NULL, `pe_Config_id` int(11) NOT NULL,
`pe_Config_payload` varchar(255) DEFAULT NULL, `pe_Config_payload` varchar(255) DEFAULT NULL,
PRIMARY KEY (`pe_Config_id`) PRIMARY KEY (`pe_Config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
INSERT INTO `pe_Config` (`pe_Config_id`, `pe_Config_payload`) VALUES INSERT INTO `pe_Config` (`pe_Config_id`, `pe_Config_payload`) VALUES
(1, 'v0.12.1'); (1, 'v0.12.1');
@@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS `pe_DataMissionHashes` (
PRIMARY KEY (`pe_DataMissionHashes_id`), PRIMARY KEY (`pe_DataMissionHashes_id`),
UNIQUE KEY `UNIQUE_hash` (`pe_DataMissionHashes_hash`), UNIQUE KEY `UNIQUE_hash` (`pe_DataMissionHashes_hash`),
KEY `pe_DataMissionHashes_instance` (`pe_DataMissionHashes_instance`) KEY `pe_DataMissionHashes_instance` (`pe_DataMissionHashes_instance`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_DataPlayers`; DROP TABLE IF EXISTS `pe_DataPlayers`;
CREATE TABLE IF NOT 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, `pe_DataPlayers_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`pe_DataPlayers_id`), PRIMARY KEY (`pe_DataPlayers_id`),
UNIQUE KEY `UNIQUE_UCID` (`pe_DataPlayers_ucid`) UNIQUE KEY `UNIQUE_UCID` (`pe_DataPlayers_ucid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_DataRaw`; DROP TABLE IF EXISTS `pe_DataRaw`;
CREATE TABLE IF NOT 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`), PRIMARY KEY (`pe_dataraw_type`,`pe_dataraw_instance`),
KEY `pe_dataraw_type_pe_dataraw_instance` (`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`) KEY `pe_dataraw_updated` (`pe_dataraw_updated`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_DataTypes`; DROP TABLE IF EXISTS `pe_DataTypes`;
CREATE TABLE IF NOT 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, `pe_DataTypes_update` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`pe_DataTypes_id`), PRIMARY KEY (`pe_DataTypes_id`),
UNIQUE KEY `UNIQUE_TYPE_NAME` (`pe_DataTypes_name`) UNIQUE KEY `UNIQUE_TYPE_NAME` (`pe_DataTypes_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_LogChat`; DROP TABLE IF EXISTS `pe_LogChat`;
CREATE TABLE IF NOT EXISTS `pe_LogChat` ( CREATE TABLE IF NOT EXISTS `pe_LogChat` (
`pe_LogChat_id` bigint NOT NULL AUTO_INCREMENT, `pe_LogChat_id` bigint(20) NOT NULL AUTO_INCREMENT,
`pe_LogChat_datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `pe_LogChat_datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`pe_LogChat_missionhash_id` bigint DEFAULT NULL, `pe_LogChat_missionhash_id` bigint(20) DEFAULT NULL,
`pe_LogChat_playerid` bigint NOT NULL, `pe_LogChat_playerid` varchar(100) NOT NULL,
`pe_LogChat_msg` text NOT NULL, `pe_LogChat_msg` text NOT NULL,
`pe_LogChat_all` varchar(10) NOT NULL, `pe_LogChat_all` varchar(10) NOT NULL,
PRIMARY KEY (`pe_LogChat_id`), 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_playerid` (`pe_LogChat_playerid`),
KEY `pe_LogChat_datetime` (`pe_LogChat_datetime`), KEY `pe_LogChat_datetime` (`pe_LogChat_datetime`),
KEY `pe_LogChat_all` (`pe_LogChat_all`) KEY `pe_LogChat_all` (`pe_LogChat_all`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_LogEvent`; DROP TABLE IF EXISTS `pe_LogEvent`;
CREATE TABLE IF NOT 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_missionhash_id` (`pe_LogEvent_missionhash_id`),
KEY `pe_LogEvent_datetime` (`pe_LogEvent_datetime`), KEY `pe_LogEvent_datetime` (`pe_LogEvent_datetime`),
KEY `pe_LogEvent_type_2` (`pe_LogEvent_type`) KEY `pe_LogEvent_type_2` (`pe_LogEvent_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_LogLogins`; DROP TABLE IF EXISTS `pe_LogLogins`;
CREATE TABLE IF NOT 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_playerid` (`pe_LogLogins_playerid`),
KEY `pe_LogLogins_datetime` (`pe_LogLogins_datetime`), KEY `pe_LogLogins_datetime` (`pe_LogLogins_datetime`),
KEY `pe_LogLogins_instance` (`pe_LogLogins_instance`) KEY `pe_LogLogins_instance` (`pe_LogLogins_instance`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_LogStats`; DROP TABLE IF EXISTS `pe_LogStats`;
CREATE TABLE IF NOT 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_masterslot` (`pe_LogStats_masterslot`),
KEY `pe_LogStats_mstatus` (`pe_LogStats_mstatus`), KEY `pe_LogStats_mstatus` (`pe_LogStats_mstatus`),
KEY `pe_LogStats_seat` (`pe_LogStats_seat`) KEY `pe_LogStats_seat` (`pe_LogStats_seat`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TRIGGER IF EXISTS `pe_LogStats_UPDATE`; DROP TRIGGER IF EXISTS `pe_LogStats_UPDATE`;
DELIMITER $$ DELIMITER $$
CREATE TRIGGER `pe_LogStats_UPDATE` BEFORE UPDATE ON `pe_LogStats` FOR EACH ROW BEGIN 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_slot` varchar(255) DEFAULT NULL,
`pe_OnlinePlayers_ucid` varchar(255) DEFAULT NULL, `pe_OnlinePlayers_ucid` varchar(255) DEFAULT NULL,
`pe_OnlinePlayers_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP `pe_OnlinePlayers_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `pe_OnlineStatus`; DROP TABLE IF EXISTS `pe_OnlineStatus`;
CREATE TABLE IF NOT 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_winapp` varchar(255) DEFAULT NULL,
`pe_OnlineStatus_perunversion_dcshook` 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 `pe_OnlineStatus_updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `pe_DataMissionHashes` ADD FULLTEXT KEY `pe_DataMissionHashes_hash` (`pe_DataMissionHashes_hash`); ALTER TABLE `pe_DataMissionHashes` ADD FULLTEXT KEY `pe_DataMissionHashes_hash` (`pe_DataMissionHashes_hash`);

238
AUDIT.md Normal file
View File

@@ -0,0 +1,238 @@
# 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.

View File

@@ -1,3 +1,10 @@
> [!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.
![Discord](https://img.shields.io/discord/641759751780302866) ![Discord](https://img.shields.io/discord/641759751780302866)
![alt text](https://img.shields.io/github/license/szporwolik/perun.svg "MIT") ![alt text](https://img.shields.io/github/license/szporwolik/perun.svg "MIT")
![alt text](https://img.shields.io/github/release-pre/szporwolik/perun.svg "Latest release") ![alt text](https://img.shields.io/github/release-pre/szporwolik/perun.svg "Latest release")
@@ -76,6 +83,9 @@ That probably means that your database does not support JSON functions, you shal
### I keep getting 1364 MySQL error ### 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 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 ### 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. 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.