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) or broadcast_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

  1. All container types in an event are stored as pointers.

  2. 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 called fromconnid, which stores the sender’s connection ID (on the client side, this is always 0, 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 as getEventScheme, hasEventScheme, getFieldsCount (for argument count), getFieldOffset (for field offset), getFieldName (for field name), findFieldIndex (for field index), and getEventFieldValue<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 (the eid receiving the event must be the player’s hero or player eid).

    • 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 to possessed_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:

  1. Define the enum in daScript where needed (preferably in a separate file for easy parsing during code generation).

  2. Explicitly mark the enum with the [export_enum] annotation.

  3. Add the file path to genDasevents.bat with the --module scripts/file_with_enum.das argument.

  4. Run genDasevents.bat.

  5. 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) ▸ WindowECSEvents db.

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.