Skip to content

Commit 0602caa

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

File tree

3 files changed

+272
-9
lines changed

3 files changed

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