Schemeful Events (daScript, Quirrel, C++, Net)
Advantages of Schemeful Events
Events are universally compatible across scripting languages (daScript, Quirrel).
They can be transmitted both locally and over the network.
They can be modified in real-time without restarting the game.
They have a strict, validated structure, making all fields visible in Quirrel and accessible as an instance, e.g.,
evt.someField
.Full runtime information on the event structure (reflection) is available.
C++ API support is provided for handling these events, if required.
Declaring an Event
Each game directory contains an event declaration file, named in the format
events_<game_name>.das
(e.g., events_cuisine_royale.das
). The event
declaration consists of an annotation and a description of the event structure.
The annotation specifies whether the event is unicast
or broadcast
, and
network routing, if needed, which is covered below.
Example: events_<game>.das
[event(unicast)]
struct CmdCreateMapPoint
x: float
z: float
Note
All events in the file events_<game_name>.das
are loaded before Quirrel,
enabling them to be accessed within it. Therefore, events declared for Quirrel
should be placed in this file. Although not mandatory for other events, it is
recommended for consistency.
Creating an Event
daScript: An event is created like any regular instance structure, e.g.,
[[RqUseAbility ability_type=ability_type]]
.Quirrel: Here, strict validation ensures no typographical errors or extraneous fields are included,
RqUseAbility({ability_type="ultimate"})
.
Subscribing to an Event
daScript: Use
on_event=RqUseAbility
, or explicitly set the type of the first argument in the system (e.g.,evt: RqUseAbility...
).Quirrel: Use
local {OnAbilityCanceled} = require("dasevents")... ::ecs.register_es("ability_canceling_es", { [OnAbilityCanceled] = @( evt, eid, comp) ::dlog(evt.ability_type)
.C++: Events can be listened to by subscribing to the event name, e.g.,
ECS_ON_EVENT(eastl::integral_constant<ecs::event_type_t, str_hash_fnv1("EventOnSeatOwnersChanged")>)
.
Sending Events (Server-to-Server, Client-to-Client)
daScript: Use the standard
sendEvent
,broadcastEvent
.Quirrel: Similarly, use
::ecs.g_entity_mgr.sendEvent
,::ecs.g_entity_mgr.broadcastEvent
.
Sending Events Over the Network
When declaring an event, specify the routing to determine its network path,
e.g., [event(unicast, routing=ROUTING_SERVER_TO_CLIENT)]
,
ROUTING_CLIENT_TO_SERVER
, or ROUTING_CLIENT_CONTROLLED_ENTITY_TO_SERVER
.
daScript: Use
require net ... send_net_event(eid, evt)
orbroadcast_net_event(evt)
.Quirrel: Use
local {CmdBlinkMarker, sendNetEvent, broadcastNetEvent} = require("dasevents") ... sendNetEvent(eid, CmdBlinkMarker()) ... broadcastNetEvent(CmdBlinkMarker(...))
.
Network Protocol Version
All declared network events contribute to the protocol version. If the server
and client versions do not match, the client will disconnect from the session.
For script events ([event]
), you can control this behavior by excluding
certain events from protocol calculations. In cases of a mismatch, this may
result in either an on-screen error or no notification.
event(... net_liable=strict ...)
– The event participates in protocol versioning; any mismatch triggers a disconnect (default behavior).event(... net_liable=logerr ...)
– The event does not affect protocol versioning; a log error is recorded if a mismatch occurs.event(... net_liable=ignore ...)
– The event does not affect protocol versioning; a log warning (logwarn
) is recorded if a mismatch occurs.
C++ events follow a similar logic but use the NET_PROTO_VERSION
constant and
the count of network C++ events, without exceptions.
Event Version
An explicit version can be assigned to an event. By default, all events are set
to version 0
. When working with BitStream
, the version is required and will
assist in adapting the protocol if the stream content changes significantly.
Example: code.das
[event(broadcast, version=1)]
struct TestEvent {}
Sending Containers (Offline and Online)
Dynamic arrays/containers can be sent along with events. Currently, the
supported types are ecs::Object
, ecs::IntList
, ecs::FloatList
,
ecs::Point3List
, and ecs::Point4List
.
Here’s an example of sending such an event from daScript:
Example: code.das
[event(broadcast)]
struct TestEvent
str : string
i : int
obj : ecs::Object const?
...
using() <| $(var obj : Object)
obj |> set("foo", 1)
broadcastEvent([[TestEvent str="test event", i = 42, obj=ecs_addr(obj)]])
Important
All container types in an event are stored as pointers.
When sending a container in an event, use the helper function
ecs_addr(container)
.
Sending events from Quirrel follows a similar process:
Example: code.nut
let {CompObject} = require("ecs")
let {TestEvent, broadcastNetEvent} = require("dasevents")
...
let obj = CompObject()
obj["foo"] = 1
broadcastNetEvent(TestEvent({str="test event", i=42, obj=obj}))
Important
Any
ecs::Object
within an event will automatically include a field calledfromconnid
, which stores the sender’s connection ID (on the client side, this is always0
, indicating the server; on the server side, it holds the actual connection number).If the container contents undergo substantial changes, it is advisable to specify an event version (e.g.,
[event(... version=1)]
). This will ensure that clients or servers with outdated versions will no longer support the event.
Sending BitStream
Similar to containers, a raw data stream (BitStream
) can also be sent in an
event. When sending a BitStream
, specifying an event version is mandatory.
Reflection
Events possess an exact schema, accessible at runtime and retrievable from any script or C++ code.
C++: All event structure information is stored in
ecs::EventsDB
, which provides various methods such asgetEventScheme
,hasEventScheme
,getFieldsCount
(for argument count),getFieldOffset
(for field offset),getFieldName
(for field name),findFieldIndex
(for field index), andgetEventFieldValue<T>
(for direct access to parameter values).daScript: All of functions for C++ are also available in the
ecs
module for daScript (e.g.,events_db_getFieldsCount
). For example, the Events DB window in ImGui uses this API, see<project_name>/prog/scripts/game/es/imgui/ecs_events_db.das
.Quirrel: A detailed event printout is available when calling
::log(evt)
, which outputs all event fields. An API with reflection support is also provided, as demonstrated below:
Example: describe_event.nut
local function describeEvent(evt) {
if (evt == null) {
::dlog("null event")
return
}
local eventType = evt.getType()
local eventId = ::ecs.g_entity_mgr.getEventsDB().findEvent(eventType)
local hasScheme = ::ecs.g_entity_mgr.getEventsDB().hasEventScheme(eventId)
if (!hasScheme) {
::dlog($"event without scheme #{eventType}")
return
}
local fieldsCount = ::ecs.g_entity_mgr.getEventsDB().getFieldsCount(eventId)
::dlog($"Event {eventType} fields count #{fieldsCount}")
for (local i = 0; i < fieldsCount; i++)
{
local name = ::ecs.g_entity_mgr.getEventsDB().getFieldName(eventId, i)
local type = ::ecs.g_entity_mgr.getEventsDB().getFieldType(eventId, i)
local offset = ::ecs.g_entity_mgr.getEventsDB().getFieldOffset(eventId, i)
local value = ::ecs.g_entity_mgr.getEventsDB().getEventFieldValue(evt, eventId, i)
::dlog($"field #{i} {name} <{type}> offset={offset} = '{value}'")
}
}
C++ Event (cpp_event)
In addition to dynamic events, it is possible to declare C++ events, for which C++ code and SQ bindings will be generated. In Quirrel, handling these events is identical to working with standard events, as is the case in daScript.
When declaring a C++ event, the with_scheme
argument is required. This is
necessary because some events cannot be converted into schemeful events due to
restrictions (fields must be basic ECS types or compatible containers only).
Example: events_<game>.das
[cpp_event(unicast, with_scheme)]
struct EventOnPlayerDash
from: float3
to: float3
The utility <game>/scripts/genDasevents.bat
will generate a .h
and .cpp
file for this event (currently located at prog/game/dasEvents.h/cpp
).
Quirrel Stubs / C++ Code Generation
To generate the Quirrel stubs and C++ code automatically, run the batch file
<game>/scripts/genDasevents.bat
. If the batch file does not work, build the
daScript compiler manually once by running jam -sPlatform=win64 -sCheckedContainers=yes
in <project_name>/prog/aot
.
Filters
You can manage the list of recipients for server-side das-events using filters.
This is helpful for targeting specific groups, such as only the player or the
player’s team. When sending an event, specify the filter as an additional
argument. For instance, send_net_event(eid, [[EnableSpectator]], target_entity_conn(eid))
. The following filters are currently supported:
broadcast
(default) – Sends to all recipients.equivalent in C++:
&net::broadcast_rcptf
.
target_entity_conn
– Sends the event only to the player (theeid
receiving the event must be the player’s hero or playereid
).equivalent in C++:
&rcptf::entity_ctrl_conn<SomeNetMsg, rcptf::TARGET_ENTITY>
.
entity_team
– Sends the event to the player’s hero and team.equivalent in C++:
&rcptf::entity_team<SomeNetMsg, rcptf::TARGET_ENTITY>
.
possessed_and_spectated
– Sends the event to the player and any spectators watching them.equivalent in C++:
&rcptf::possessed_and_spectated
.
possessed_and_spectated_player
– Similar topossessed_and_spectated
but targets the player instead of the hero.equivalent in C++:
&rcptf::possessed_and_spectated_player
.
In daScript, a filter is a function returning an array<net::IConnection>
,
which is referred to as a “filter” for consistency with C++ terminology.
Filters in Squirrel
In Squirrel, as in daScript, event-sending methods have an optional parameter
where you can pass an array of connection IDs (i.e., an array of int
). Below
is an example filter implemented in Squirrel:
Example: sq_filter.nut
local filtered_by_team_query = ecs.SqQuery("filtered_by_team_query", {comps_ro=[["team", ecs.TYPE_INT], ["connid",ecs.TYPE_INT]], comps_rq=["player"], comps_no=["playerIsBot"]})
local function filter_connids_by_team(team){
local connids = []
filtered_by_team_query.perform(function(eid, comp){
connids.append(comp["connid"])
},"and(ne(connid,{0}), eq(team,{1}))".subst(INVALID_CONNECTION_ID, team))
return connids
}
And here is an example of sending an event using this filter:
Example: sq_send_event.nut
sendNetEvent(eid, RequestNextRespawnEntity({memberEid=eid}), filter_connids_by_team(target_team))
Filters in cpp_event
When annotating a cpp_event
with the filter=
parameter and one of the
filters listed above, the code generation process will produce C++ code that
includes the specified filter as described above in parentheses.
Event Delivery Reliability
By default, all events are sent with a reliability level of RELIABLE_ORDERED
.
This can be modified using the reliability
argument. Available reliability
levels include:
UNRELIABLE
UNRELIABLE_SEQUENCED
RELIABLE_ORDERED
RELIABLE_UNORDERED
Enums
If you need an enumerated type available in both scripts, there’s no need to
write it in C++ and bind it separately for each language. The genDasevents.bat
utility now supports generating Squirrel code with enums directly from daScript.
Follow these steps:
Define the enum in daScript where needed (preferably in a separate file for easy parsing during code generation).
Explicitly mark the enum with the
[export_enum]
annotation.Add the file path to
genDasevents.bat
with the--module scripts/file_with_enum.das
argument.Run
genDasevents.bat
.Constructors for all enums will be available in
<game_prog>/sq_globals/dasenums.nut
.
Utilities
ecs.dump_events
– This console command prints all events, their schemas, and schema hashes. If there are mismatches between client and server events, this command can be run on both to compare outputs (the log will already contain all necessary information for analysis).Additionally, there’s an in-game window with detailed event information: open the ImGui menu (
F2
) ▸ Window ▸ ECS ▸ Events db.
FAQ
I have a C++ network event and want to move its declaration to daScript while keeping the event in C++. (Example: ECS_REGISTER_NET_EVENT(EventUserMarkDisabled, net::Er::Unicast, net::ROUTING_SERVER_TO_CLIENT, (&rcptf::entity_ctrl_conn<EventUserMarkDisabledNetMsg, rcptf::TARGET_ENTITY>));
Define the event in daScript with the [cpp_event(unicast, with_scheme, routing=ROUTING_SERVER_TO_CLIENT, filter=direct_connection)]
annotation, then
run genDasevents.bat
. This will generate stubs, and the event will appear or
update in the .h
and .cpp
files. (cpp_event + with_scheme
activates code
generation).
I have a C++ network event and want to move it entirely to scripts (no need for it in C++).
Follow the same steps as above, but use [event(unicast, routing=ROUTING_SERVER_TO_CLIENT)]
. Replace all sendEvent
calls with
send_net_event/sendNetEvent
, passing a filter function call like
target_entity_conn(eid)
as the final argument. Running genDasevents.bat
will
still be necessary to generate the stubs.
I have a script-based event and need to migrate it to C++.
Simply change the event annotation from event
to cpp_event
. Then, replace
all send_net_event
calls with standard sendEvent
calls. Run
genDasevents.bat
to generate the stubs and C++ code.
I added an event, but I see the following error in Squirrel: [E] daRg: the index 'CmdHeroSpeech' does not exist
.
Make sure the event is registered in the system before Quirrel loads. Each game
has an initialization script (e.g., <game>_init.das
). Load the script
containing the event in this initialization script to resolve the error.
genDasevents.bat
shows compilation errors and won’t run.
Rebuild the compiler.
See also
For more information, see daScript plugin for VSCode.