Skip to content

Commit 58449dc

Browse files
committed
implement pathfinding callbacks in python; update documentation for shootings callbacks
1 parent 3b46efd commit 58449dc

File tree

8 files changed

+192
-35
lines changed

8 files changed

+192
-35
lines changed

csharp/uwapi/events.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Runtime.InteropServices;
34

45
namespace Unnatural
@@ -16,12 +17,19 @@ public class ChatMessage
1617
public ChatTargetFLags Flags;
1718
}
1819

20+
public struct ShootingControlData
21+
{
22+
public Interop.UwShootingEventEnum type;
23+
public ushort count;
24+
}
25+
1926
public static class Events
2027
{
2128
public static event EventHandler<ConnectionStateEnum> ConnectionStateChanged;
2229
public static event EventHandler<GameStateEnum> GameStateChanged;
2330
public static event EventHandler<MapStateEnum> MapStateChanged;
2431
public static event EventHandler<bool> Updating;
32+
public static event EventHandler<IReadOnlyList<uint>> Shootings;
2533
public static event EventHandler<uint> ForceEliminated;
2634
public static event EventHandler<ChatMessage> ChatReceived;
2735

@@ -43,31 +51,51 @@ static void ExceptionCallback([MarshalAs(UnmanagedType.LPStr)] string message)
4351

4452
static void ConnectionStateCallback(ConnectionStateEnum state)
4553
{
46-
if (ConnectionStateChanged != null)
47-
ConnectionStateChanged(null, state);
54+
if (ConnectionStateChanged == null)
55+
return;
56+
ConnectionStateChanged(null, state);
4857
}
4958

5059
static void GameStateCallback(GameStateEnum state)
5160
{
52-
if (GameStateChanged != null)
53-
GameStateChanged(null, state);
61+
if (GameStateChanged == null)
62+
return;
63+
GameStateChanged(null, state);
5464
}
5565

5666
static void MapStateCallback(MapStateEnum state)
5767
{
58-
if (MapStateChanged != null)
59-
MapStateChanged(null, state);
68+
if (MapStateChanged == null)
69+
return;
70+
MapStateChanged(null, state);
6071
}
6172

6273
static void UpdateCallback(bool stepping)
6374
{
64-
if (Updating != null)
65-
Updating(null, stepping);
75+
if (Updating == null)
76+
return;
77+
Updating(null, stepping);
6678
}
6779

6880
static void ShootingsCallback(ref ShootingsArray data)
6981
{
70-
// todo decode shooting
82+
if (Shootings == null)
83+
return;
84+
uint[] tmp = new uint[data.count];
85+
if (data.count > 0)
86+
Marshal.Copy(data.data, (int[])(object)tmp, 0, (int)data.count);
87+
Shootings(null, tmp);
88+
}
89+
90+
public static ShootingControlData ShootingControlData(uint id)
91+
{
92+
ushort low = (ushort)(id & 0xFFFF);
93+
ushort high = (ushort)(id >> 16);
94+
return new ShootingControlData
95+
{
96+
type = (Interop.UwShootingEventEnum)low,
97+
count = high
98+
};
7199
}
72100

73101
static void ForceEliminatedCallback(uint force)
@@ -126,7 +154,7 @@ public static ulong InsertTask(Action a)
126154
}
127155

128156
static ulong index = 1;
129-
static System.Collections.Generic.Dictionary<ulong, Action> actions = new System.Collections.Generic.Dictionary<ulong, Action>();
157+
static Dictionary<ulong, Action> actions = new Dictionary<ulong, Action>();
130158
static readonly Interop.UwTaskCompletedCallbackType TaskCompletedDelegate = new Interop.UwTaskCompletedCallbackType(TaskCompleted);
131159

132160
static void TaskCompleted(ulong taskUserData, Interop.UwTaskTypeEnum type)

python/uwapi/events.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
from dataclasses import field
2-
from typing import Callable, List
2+
from tarfile import NUL
3+
from typing import Callable, List, Dict, Any
34
from .interop import *
45

56

7+
@dataclass
8+
class ShootingControlData:
9+
type: UwShootingEventEnum
10+
count: int
11+
12+
613
class Events:
714
_instance = None
815
_connection_state_listeners: List[Callable[[UwConnectionStateEnum], None]] = []
916
_game_state_listeners: List[Callable[[UwGameStateEnum], None]] = []
17+
_map_state_listeners: List[Callable[[UwMapStateEnum], None]] = []
1018
_update_listeners: List[Callable[[bool], None]] = []
11-
_shootings_listeners: List[Callable[[UwShootingsArray], None]] = []
19+
_shootings_listeners: List[Callable[[List[int]], None]] = []
1220
_force_eliminated_listeners: List[Callable[[int], None]] = []
1321
_chat_listeners: List[Callable[[str, int, UwChatTargetFlags], None]] = []
14-
_map_state_listeners: List[Callable[[UwMapStateEnum], None]] = []
22+
_tasks_index: int = 1
23+
_tasks_actions: Dict[int, Callable] = {}
1524

1625
def __new__(cls):
1726
if cls._instance is None:
@@ -20,15 +29,14 @@ def __new__(cls):
2029

2130
def initialize(self) -> None:
2231
uw_interop.uwSetExceptionCallback(self._exception_callback)
23-
# uw_interop.uwSetLogCallback(self._log_callback)
2432
uw_interop.uwSetConnectionStateCallback(self._connection_state_callback)
2533
uw_interop.uwSetGameStateCallback(self._game_state_callback)
34+
uw_interop.uwSetMapStateCallback(self._map_state_callback)
2635
uw_interop.uwSetUpdateCallback(self._update_callback)
2736
uw_interop.uwSetShootingsCallback(self._shootings_callback)
2837
uw_interop.uwSetForceEliminatedCallback(self._force_eliminated_callback)
2938
uw_interop.uwSetChatCallback(self._chat_callback)
3039
uw_interop.uwSetTaskCompletedCallback(self._task_completed_callback)
31-
uw_interop.uwSetMapStateCallback(self._map_state_callback)
3240

3341
# ---------------------
3442

@@ -40,10 +48,13 @@ def on_connection_state(
4048
def on_game_state(self, listener: Callable[[UwGameStateEnum], None]) -> None:
4149
self._game_state_listeners.append(listener)
4250

51+
def on_map_state(self, listener: Callable[[UwMapStateEnum], None]) -> None:
52+
self._map_state_listeners.append(listener)
53+
4354
def on_update(self, listener: Callable[[bool], None]) -> None:
4455
self._update_listeners.append(listener)
4556

46-
def on_shootings(self, listener: Callable[[UwShootingsArray], None]) -> None:
57+
def on_shootings(self, listener: Callable[[List[int]], None]) -> None:
4758
self._shootings_listeners.append(listener)
4859

4960
def on_force_eliminated(self, listener: Callable[[int], None]) -> None:
@@ -52,8 +63,10 @@ def on_force_eliminated(self, listener: Callable[[int], None]) -> None:
5263
def on_chat(self, listener: Callable[[str, int, UwChatTargetFlags], None]) -> None:
5364
self._chat_listeners.append(listener)
5465

55-
def on_map_state(self, listener: Callable[[UwMapStateEnum], None]) -> None:
56-
self._map_state_listeners.append(listener)
66+
def shooting_control_data(self, id: int) -> ShootingControlData:
67+
low = id & 0xFFFF
68+
high = (id >> 16) & 0xFFFF
69+
return ShootingControlData(UwShootingEventEnum(low), high)
5770

5871
# ---------------------
5972

@@ -69,13 +82,17 @@ def _game_state_callback(self, state: UwGameStateEnum) -> None:
6982
for listener in self._game_state_listeners:
7083
listener(state)
7184

85+
def _map_state_callback(self, state: UwMapStateEnum) -> None:
86+
for listener in self._map_state_listeners:
87+
listener(state)
88+
7289
def _update_callback(self, stepping: bool) -> None:
7390
for listener in self._update_listeners:
7491
listener(stepping)
7592

7693
def _shootings_callback(self, data: UwShootingsArray) -> None:
7794
for listener in self._shootings_listeners:
78-
listener(data)
95+
listener(data.data)
7996

8097
def _force_eliminated_callback(self, force: int) -> None:
8198
for listener in self._force_eliminated_listeners:
@@ -90,11 +107,17 @@ def _chat_callback(
90107
def _task_completed_callback(
91108
self, task_user_data: int, type: UwTaskTypeEnum
92109
) -> None:
93-
pass # todo
94-
95-
def _map_state_callback(self, state: UwMapStateEnum) -> None:
96-
for listener in self._map_state_listeners:
97-
listener(state)
110+
if type != UwTaskTypeEnum.Nothing:
111+
a = self._tasks_actions.get(task_user_data)
112+
if a is not None:
113+
a()
114+
self._tasks_actions.pop(task_user_data)
115+
116+
def _insert_task(self, a: Callable) -> int:
117+
i = self._tasks_index
118+
self._tasks_index += 1
119+
self._tasks_actions[i] = a
120+
return i
98121

99122

100123
uw_events = Events()

python/uwapi/map.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,24 @@ def clusters_neighbors_all(self) -> List[List[int]]:
134134
def clusters_neighbors(self, cluster: int) -> List[int]:
135135
return self._clusters_neighbors[cluster]
136136

137+
def clusters_distances(
138+
self,
139+
callback: Callable,
140+
starting_cluster: int,
141+
unit_prototype: int,
142+
allow_impassable_terrain: bool = False,
143+
) -> None:
144+
def fin():
145+
callback(uw_interop.uwRetrieveClustersDistances())
146+
147+
q = UwClustersDistancesQuery(
148+
uw_events._insert_task(fin),
149+
starting_cluster,
150+
unit_prototype,
151+
allow_impassable_terrain,
152+
)
153+
uw_interop.uwStartClustersDistances(q)
154+
137155
def _reset(self) -> None:
138156
self._name: str = ""
139157
self._guid: str = ""

python/uwapi/world.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ def overview_flags(self, position: int) -> UwOverviewFlags:
7373
def overview_entities(self, position: int) -> list[int]:
7474
return uw_interop.uwOverviewIds(position).ids
7575

76+
def unit_pathfinding(
77+
self,
78+
callback: Callable,
79+
starting_position: int,
80+
goal_position: int,
81+
unit_prototype: int,
82+
allow_nearby_position: bool = False,
83+
max_iterations: int = 0,
84+
) -> None:
85+
def fin():
86+
callback(uw_interop.uwRetrieveUnitPathfinding())
87+
88+
q = UwUnitPathfindingQuery(
89+
uw_events._insert_task(fin),
90+
starting_position,
91+
goal_position,
92+
unit_prototype,
93+
max_iterations,
94+
allow_nearby_position,
95+
)
96+
uw_interop.uwStartUnitPathfinding(q)
97+
7698
def entities(self) -> dict[int, Entity]:
7799
return self._entities
78100

sphinx/source/bots/setup.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ This will show you additional information about any single thing you select in t
5252
For example: unit id, its state, position (both index and 3D coordinates), etc.
5353
It shows at the bottom of the left panel.
5454

55+
Enable *Detailed tooltips* too.

sphinx/source/concepts/callbacks.rst

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,86 @@ This callback is the primary driving point for your program.
1818
Use ``>=`` when waiting for number of ticks to pass.
1919

2020
Shootings Callback
21-
-----------------
21+
------------------
2222
Shootings events are synchronized *asynchronously and unreliably*.
2323
This event is useful for measuring threat.
2424

25+
The data encode multiple events, possibly of different types, mixed together.
26+
Make sure to carefully decode them, as shown in the example below.
27+
2528
.. warning::
2629
The entity id may be expired.
2730

28-
Deaths Callback
29-
---------------
30-
Deaths events are synchronized *asynchronously and unreliably* - some events may be lost, or delivered much later.
31-
Do *not* depend on deaths events for important decisions.
32-
It is useful for measuring threat.
31+
.. tab-set::
32+
:sync-group: language
33+
34+
.. tab-item:: Python
35+
:sync: python
36+
37+
.. code-block:: python
38+
39+
def shootings_callback(data: List[int]) -> None:
40+
index = 0
41+
while index < len(data):
42+
control = uw_events.shooting_control_data(data[index])
43+
if control.type == UwShootingEventEnum.Shooting:
44+
shooter_id = data[index + 1]
45+
for i in range(1, control.count):
46+
target_id = data[index + i + 1]
47+
# handle shooting event
48+
pass
49+
index += control.count + 1
50+
51+
.. tab-item:: C#
52+
:sync: csharp
53+
54+
.. code-block:: csharp
55+
56+
void ShootingsCallback(IReadOnlyList<uint> data)
57+
{
58+
int index = 0;
59+
while (index < data.Count)
60+
{
61+
var control = Events.ShootingControlData(data[index]);
62+
if (control.type == UwShootingEventEnum.Shooting)
63+
{
64+
uint shooterId = data[index + 1];
65+
for (uint i = 1; i < control.count; i++)
66+
{
67+
uint targetId = data[index + i + 1];
68+
// handle data event
69+
}
70+
}
71+
index += control.count + 1;
72+
}
73+
}
74+
75+
.. tab-item:: C++
76+
:sync: cpp
77+
78+
.. code-block:: cpp
79+
80+
extern "C"
81+
void uwShootingsCallback(const UwShootingsArray *data)
82+
{
83+
const auto shooting = uw::makeVector(data);
84+
uint32 index = 0;
85+
while (index < shooting.size())
86+
{
87+
const auto control = uw::shootingControlData(shooting[index]);
88+
if (control.type == UwShootingEventEnum_Shooting)
89+
{
90+
const uint32 shooterId = shooting[index + 1];
91+
for (uint32 i = 1; i < control.count; i++)
92+
{
93+
const uint32 targetId = shooting[index + i + 1];
94+
// handle shooting event
95+
}
96+
}
97+
index += control.count + 1;
98+
}
99+
}
33100
34-
.. warning::
35-
The entity id may be expired.
36101
37102
Task Completed Callback
38103
-----------------------

sphinx/source/mods/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Mods
33
Guide on making mods for Unnatural Worlds.
44

55
.. warning::
6-
Making custom mods is not yet fully supported.
6+
Making mods is not yet fully supported.
77

88
A mod is a collection of assets that can modify the look and/or behavior of any moddable map.
99
It may contain :ref:`prototypes <concepts/prototypes:Prototypes>`, :ref:`scripts <scripts/index:Scripts>`, models, sounds, etc.

sphinx/source/scripts/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Scripts
22
=======
3-
Guide on making scripts for custom maps for Unnatural Worlds.
3+
Guide on making scripts for custom maps or mods for Unnatural Worlds.
44

55
.. warning::
6-
Making custom scripts is not yet fully supported.
6+
Making scripts is not yet fully supported.
77

88
Scripts are executed within the server.
99
They have access to all data within the game, and may modify all data.

0 commit comments

Comments
 (0)