Skip to content

Commit d07c20e

Browse files
committed
Add W3CEvents for creating events, uses W3CData internally
1 parent b76e644 commit d07c20e

File tree

3 files changed

+291
-9
lines changed

3 files changed

+291
-9
lines changed

src/lua/w3cChecksum.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ for i = 0, 255 do
1313
CRC32_TABLE[i] = crc
1414
end
1515

16+
---@class W3CChecksum
1617
local W3CChecksum = {}
1718
W3CChecksum.__index = W3CChecksum
1819

src/lua/w3cEvents.lua

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
local W3CData = require("src.lua.w3cdata")
2+
local W3CChecksum = require("src.lua.w3cChecksum")
3+
4+
local MAX_PAYLOAD_SIZE_BYTES = 180
5+
local CHECKSUM_INTERVAL_SECS = 30
6+
local PLAYER_INDEX_TO_FLUSH = 0
7+
8+
-- This needs to be "WC" for W3Champions to be able to automatically parse events.
9+
-- Prefixes larger than 2 characters may cayse latency issues. See below post, although it's specific to the other
10+
-- Sync<>, it would make sense that it works the same way for SyncData.
11+
-- https://www.hiveworkshop.com/pastebin/1ce4fe042832e6bd7d06697a43055373.5801
12+
local SYNC_DATA_PREFIX = "WC"
13+
14+
---@alias Event PayloadValue
15+
16+
---@class ChecksumConfig
17+
---@field enabled boolean
18+
---@field update_checksum? function
19+
---@field get_checksum? function
20+
---@field interval? integer
21+
22+
---@class EventBaseSchemaConfig
23+
---@field enabled boolean
24+
---@field set_base_event_data? function
25+
26+
---@class BooleanConfig
27+
---@field enabled boolean
28+
29+
---@class W3CEventsConfig
30+
---@field checksum ChecksumConfig
31+
---@field base_schema EventBaseSchemaConfig
32+
---@field logging BooleanConfig
33+
34+
---@class W3CEvents
35+
---@field event_buffer table<Payload>
36+
---@field trackers table<timer>
37+
---@field config W3CEventsConfig
38+
local W3CEvents = {
39+
event_buffer = {},
40+
trackers = {},
41+
config = {
42+
checksum = { enabled = true },
43+
base_schema = { enabled = true },
44+
logging = { enabled = false },
45+
},
46+
}
47+
48+
local event_buffer_size = 0
49+
50+
---@type W3CChecksum
51+
local checksum = nil
52+
53+
---@type Schema
54+
local base_schema = {
55+
version = 1,
56+
name = "base",
57+
fields = {
58+
{ name = "player", bits = 5 }, -- Up to 32 player ids
59+
{ name = "time", bits = 13 }, -- Up to ~2 hours 16 minutes
60+
},
61+
}
62+
63+
---Flushes the current `W3CEvents.event_buffer`, sending all events to `BlzSendSyncData` using the configured `W3CEvents.config.prefix`
64+
---Events are only sent by a single player.
65+
local function flush()
66+
-- Only want to send events from the first player to avoid spam. Checksums are used to detect if there's any
67+
-- manipulation of event data being sent.
68+
if GetLocalPlayer() == Player(PLAYER_INDEX_TO_FLUSH) then
69+
local payloads = W3CData:encode_payload(W3CEvents.event_buffer, MAX_PAYLOAD_SIZE_BYTES)
70+
local timer = CreateTimer()
71+
local index = 1
72+
73+
-- Iterate through all payloads on a timer, sending every 0.2 seconds until
74+
-- all payloads are sent. To prevent us sending huge amounts of SyncData all at
75+
-- once causing latency issues
76+
-- 0.2 seconds results in sending a maximum of 1275 bytes/second, a little over 1kb, if
77+
-- all payloads use all 255 bytes possible
78+
-- WC3 has a max bandwidth of 4kb/s before having issues
79+
TimerStart(timer, 0.2, true, function()
80+
if index <= #payloads then
81+
local payload = payloads[index]
82+
index = index + 1
83+
BlzSendSyncData(SYNC_DATA_PREFIX, payload)
84+
else
85+
PauseTimer(timer)
86+
DestroyTimer(timer)
87+
end
88+
end)
89+
end
90+
91+
W3CEvents.event_buffer = {}
92+
end
93+
94+
-- Monotonic clock to get time since game started
95+
---@type timer
96+
local clock = nil
97+
98+
---@type timer
99+
local checksum_clock = nil
100+
101+
local function now()
102+
return math.floor(TimerGetElapsed(clock))
103+
end
104+
105+
---Utility function that attempts to estimate the byte size of an event based on it's schema and values
106+
---@param schema_name string
107+
---@param event Event
108+
local function estimate_event_size(schema_name, event)
109+
local schema = W3CData:get_schema(schema_name)
110+
local event_size_bytes = 0
111+
local event_size_bits = 0
112+
113+
for _, field in ipairs(schema.fields) do
114+
if field.type ~= "string" then
115+
event_size_bits = event_size_bits + field.bits
116+
else
117+
local string_value = event[field.name]
118+
event_size_bytes = event_buffer_size + #string_value
119+
end
120+
end
121+
122+
event_size_bytes = event_size_bytes + (math.ceil(event_size_bits / 8))
123+
return event_size_bytes
124+
end
125+
126+
--- Used to set base schema fields on events. Done like this to allow it to
127+
--- be overridden in `W3CEvents.config.base_schema`
128+
---@param event Event
129+
local function add_base_schema_data(event)
130+
event["time"] = now()
131+
event["player"] = GetPlayerId(GetLocalPlayer())
132+
end
133+
134+
---Sends a checksum payload using configured function to get the checksum value.
135+
local function send_checksum()
136+
if W3CEvents.config.checksum.enabled then
137+
local checksum_value = W3CEvents.config.checksum.get_checksum()
138+
local payload = W3CData:generate_checksum_payload(checksum_value)
139+
BlzSendSyncData(SYNC_DATA_PREFIX, payload)
140+
end
141+
end
142+
143+
--- Set and Update checksum functions used for checksum payloads. Done like this
144+
--- to allow them to be overridden in `W3CEvents.config.checksum`
145+
local function get_checksum()
146+
return checksum:finalize()
147+
end
148+
149+
local function update_checksum(payload)
150+
W3CChecksum:update(payload)
151+
end
152+
153+
---Setup monotonic clock to get game time and checksum clock to send checksum packets
154+
---on a set interval
155+
local function setup_timers()
156+
if not clock then
157+
clock = CreateTimer()
158+
TimerStart(clock, 1e9, false, nil)
159+
end
160+
161+
if not checksum_clock and W3CEvents.config.checksum.enabled then
162+
checksum_clock = CreateTimer()
163+
TimerStart(checksum_clock, W3CEvents.config.checksum.interval, true, send_checksum)
164+
end
165+
end
166+
167+
---Registers a base schema that will be included in all other events, if those events
168+
---also have `use_base` enabled and `W3CEvents.config.base_schema.enabled = true`
169+
---@param schema Schema Base schema to register.
170+
---@param setter function Function used to set values on events for the base schema
171+
function W3CEvents:register_base_schema(schema, setter)
172+
if schema.name:lower() ~= "base" then
173+
error("Base schemas need to have the name 'base'")
174+
end
175+
176+
if type(setter) ~= "function" then
177+
error("Setter needs to be a function")
178+
end
179+
180+
W3CData:register_schema(schema)
181+
self.set_base_event_data = setter
182+
end
183+
184+
---Initializes W3CEvents. Call this before anything else.
185+
---@param config W3CEventsConfig
186+
function W3CEvents.init(config)
187+
W3CData.init()
188+
189+
W3CEvents.config = config or W3CEvents.config
190+
191+
if W3CEvents.config.base_schema.enabled then
192+
W3CEvents:register_base_schema(base_schema, add_base_schema_data)
193+
end
194+
195+
if W3CEvents.config.checksum.enabled then
196+
checksum = W3CChecksum.new()
197+
198+
W3CEvents.config.checksum.update_checksum = W3CEvents.config.checksum.update_checksum or update_checksum
199+
W3CEvents.config.checksum.get_checksum = W3CEvents.config.checksum.get_checksum or get_checksum
200+
W3CEvents.config.checksum.interval = W3CEvents.config.checksum.interval or CHECKSUM_INTERVAL_SECS
201+
end
202+
203+
setup_timers()
204+
end
205+
206+
---Creates events for `name` on a set interval, calling the `getter` to get the value used for the event.
207+
---@param name string Name of the event. Must match the name of a schema that has been registered with `W3CEvents.register`
208+
---@param getter function Getter function that provides the event data
209+
---@param interval integer How frequently to create this event
210+
---@return function stop_function Function that can be called to stop and clean up the tracking event. All events that were created
211+
---before calling this function will still be created and sent.
212+
function W3CEvents:track(name, getter, interval)
213+
local timer = CreateTimer()
214+
self.trackers[timer] = true
215+
216+
TimerStart(timer, interval, true, function()
217+
local val = nil
218+
if type(getter) == "function" then
219+
val = getter()
220+
else
221+
return
222+
end
223+
self:event(name, val)
224+
end)
225+
226+
return function()
227+
if not self.trackers[timer] then
228+
return
229+
end
230+
PauseTimer(timer)
231+
DestroyTimer(timer)
232+
self.trackers[timer] = nil
233+
end
234+
end
235+
236+
---@param name string Name of the event. Must match the name of a schema that has been registered with `W3CEvents.register`
237+
---@param event Event Event to create and send. Fields and their values must match the fields configured in the matching schema
238+
function W3CEvents:event(name, event)
239+
if not W3CData:has_schema(name) then
240+
error("Schema [" .. name .. "] is not registered but an event is being created.")
241+
end
242+
243+
if W3CData:should_use_base(name) and self.set_base_event_data then
244+
self.set_base_event_data(event)
245+
end
246+
247+
-- Updates checksum with raw event data, not packed, as it doesn't really matter which we use and
248+
-- this avoids having to pack every event just to update the checksum
249+
checksum:update(table.concat(event))
250+
251+
local size_estimate = estimate_event_size(name, event)
252+
if event_buffer_size + size_estimate > MAX_PAYLOAD_SIZE_BYTES then
253+
flush()
254+
else
255+
event_buffer_size = event_buffer_size + size_estimate
256+
end
257+
258+
table.insert(self.event_buffer, { name, event })
259+
end
260+
261+
---Register a schema to use when create events. A schema must be registered before creating events or errors will occur
262+
---when attempting to call `W3CEvents.event` or `W3CEvents.track`
263+
---@param schema Schema Schema to register.
264+
function W3CEvents:register(schema)
265+
W3CData:register_schema(schema)
266+
end
267+
268+
return W3CEvents

src/lua/w3cdata.lua

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,17 @@ Below is a table showing bits and the numbers they allow up to 24 bits / 3 bytes
119119
120120
--]]
121121

122-
-- TODO: Still need to test chunked data
123122
-- TODO: Still need to update the event library to use this library and test that it all works inside WC3
124123

125124
require("src.lua.libDeflate")
126-
LibDeflate.InitCompressor()
127-
128-
local json = require("dkjson")
129125

130126
---@alias FieldType "bool" | "byte" | "short" | "int" | "float" | "string"
131-
127+
---@alias FieldName string
132128
---@alias SchemaId integer
133129

130+
---@alias PayloadFieldValue string | number | boolean
131+
---@alias PayloadValue table<FieldName, PayloadFieldValue>
132+
134133
---@class Field
135134
---@field name string
136135
---@field type? FieldType
@@ -145,9 +144,7 @@ local json = require("dkjson")
145144

146145
---@class Payload
147146
---@field schema_name string
148-
---@field payload table
149-
150-
---@class SchemaType<string, SchemaId>: { [string]: SchemaId }
147+
---@field payload table<PayloadValue>
151148

152149
---@class BaseSchemaConfig
153150
---@field enabled boolean
@@ -226,6 +223,12 @@ local function setup_bits_for_field_types(schema)
226223
end
227224
end
228225

226+
---@param config? W3CDataConfig
227+
function W3CData.init(config)
228+
LibDeflate.InitCompressor()
229+
W3CData.config = config or { base_schema = { enabled = true } }
230+
end
231+
229232
--- Register a schema to be used for compression and decompression.
230233
--- Schemas with the name "base" will be combined with all other schemas if `config.base_schema.enabled = true` and
231234
--- the `schema.use_base = true`
@@ -268,6 +271,16 @@ function W3CData:get_schema(schema_name)
268271
return self:get_schema_by_id(id)
269272
end
270273

274+
function W3CData:has_schema(schema_name)
275+
return self:get_schema(schema_name) and true or false
276+
end
277+
278+
function W3CData:should_use_base(schema_name)
279+
local schema = self:get_schema(schema_name)
280+
281+
return self.config.base_schema.enabled and schema.use_base
282+
end
283+
271284
--- COBS encodes a string to remove null bytes so that it can be safely sent using BlzSendSyncData.
272285
---@param input string Bytes to do COBS encoding on
273286
---@return string output COBS encoded bytes
@@ -798,7 +811,7 @@ end
798811
---@see W3CData.chunk_payload
799812
---@param events table<Payload> Table containing all payload events to encode with their associated schema names
800813
---@param max_size integer Maximum size for a single data packet
801-
---@return string | table<string>, boolean encoded_payload
814+
---@return table<string>, boolean encoded_payload
802815
function W3CData:encode_payload(events, max_size)
803816
local result = {}
804817
local packed = self:pack_batch_with_name(events)

0 commit comments

Comments
 (0)