lzrtag-base 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +98 -0
- data/docs/EventSymbols.md +117 -0
- data/docs/MQTTBranches.md +74 -0
- data/lib/lzrtag.rb +13 -0
- data/lib/lzrtag/game/base_game.rb +220 -0
- data/lib/lzrtag/handler/base_handler.rb +169 -0
- data/lib/lzrtag/handler/count_handler.rb +55 -0
- data/lib/lzrtag/handler/game_handler.rb +258 -0
- data/lib/lzrtag/handler/hitArb_handler.rb +80 -0
- data/lib/lzrtag/hooks/base_hook.rb +143 -0
- data/lib/lzrtag/hooks/standard_hooks.rb +174 -0
- data/lib/lzrtag/map/map_set.rb +48 -0
- data/lib/lzrtag/map/map_zone.rb +95 -0
- data/lib/lzrtag/map/myMaps_parser.rb +136 -0
- data/lib/lzrtag/player/base_player.rb +104 -0
- data/lib/lzrtag/player/effects_player.rb +96 -0
- data/lib/lzrtag/player/hardware_player.rb +251 -0
- data/lib/lzrtag/player/life_player.rb +116 -0
- metadata +131 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
|
2
|
+
require 'mqtt/sub_handler'
|
3
|
+
|
4
|
+
require_relative '../hooks/base_hook.rb'
|
5
|
+
|
6
|
+
require_relative '../player/life_player.rb'
|
7
|
+
|
8
|
+
module LZRTag
|
9
|
+
module Handler
|
10
|
+
# The base handler class.
|
11
|
+
# This class code deals with the most rudimentary systems:
|
12
|
+
# - It handles the MQTT connection
|
13
|
+
# - It registers new players and distributes MQTT data to the respective class
|
14
|
+
# - It hands out Player IDs to connected players
|
15
|
+
# - It runs the event loop system and manages hooks
|
16
|
+
#
|
17
|
+
# In it's simplest form it can be instantiated with
|
18
|
+
# just a MQTT handler:
|
19
|
+
# @example
|
20
|
+
# # Using LZRTag.Handler instead of LZRTag::Handler::Base to fetch the latest handler
|
21
|
+
# handler = LZRTag.Handler.new(mqttConn);
|
22
|
+
class Base
|
23
|
+
# Returns the MQTT connection
|
24
|
+
attr_reader :mqtt
|
25
|
+
|
26
|
+
# Returns the ID-Table, a Hash of Players and their matched IDs
|
27
|
+
attr_reader :idTable
|
28
|
+
|
29
|
+
def initialize(mqtt, playerClass = Player::Life, clean_on_exit: true)
|
30
|
+
@mqtt = mqtt;
|
31
|
+
|
32
|
+
@playerClass = playerClass;
|
33
|
+
|
34
|
+
@players = Hash.new();
|
35
|
+
@idTable = Hash.new();
|
36
|
+
|
37
|
+
@playerSynchMutex = Mutex.new();
|
38
|
+
|
39
|
+
@hooks = [self];
|
40
|
+
@eventQueue = Queue.new();
|
41
|
+
|
42
|
+
@eventThread = Thread.new do
|
43
|
+
loop do
|
44
|
+
nextData = @eventQueue.pop;
|
45
|
+
@hooks.each do |h|
|
46
|
+
h.consume_event(nextData[0], nextData[1]);
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
@eventThread.abort_on_exception = true;
|
51
|
+
|
52
|
+
@mqtt.subscribe_to "Lasertag/Players/#" do |data, topic|
|
53
|
+
dID = topic[0];
|
54
|
+
if(not @players.key? dID)
|
55
|
+
@playerSynchMutex.synchronize {
|
56
|
+
@players[dID] = @playerClass.new(dID, self);
|
57
|
+
}
|
58
|
+
send_event(:playerRegistered, @players[dID]);
|
59
|
+
end
|
60
|
+
|
61
|
+
@players[dID].on_mqtt_data(data, topic);
|
62
|
+
end
|
63
|
+
|
64
|
+
if(clean_on_exit)
|
65
|
+
at_exit {
|
66
|
+
@playerSynchMutex.synchronize {
|
67
|
+
@players.each do |id, player|
|
68
|
+
player.clear_all_topics();
|
69
|
+
sleep 0.1;
|
70
|
+
end
|
71
|
+
}
|
72
|
+
sleep 0.5;
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
puts "I LZR::Handler init finished".green
|
77
|
+
end
|
78
|
+
|
79
|
+
# Send an event into the event loop.
|
80
|
+
# Any events will be queued and will be executed in-order by a separate
|
81
|
+
# thread. The provided data will be passed along to the hooks
|
82
|
+
# @param evtName [Symbol] Name of the event
|
83
|
+
# @param *data Any additional data to send along with the event
|
84
|
+
def send_event(evtName, *data)
|
85
|
+
raise ArgumentError, "Event needs to be a symbol!" unless evtName.is_a? Symbol;
|
86
|
+
@eventQueue << [evtName, data];
|
87
|
+
end
|
88
|
+
|
89
|
+
# @private
|
90
|
+
def consume_event(evtName, data)
|
91
|
+
case evtName
|
92
|
+
when :playerConnected
|
93
|
+
player = data[0];
|
94
|
+
i = 1;
|
95
|
+
while(@idTable[i]) do i+=1; end
|
96
|
+
@idTable[i] = player;
|
97
|
+
player.id = i;
|
98
|
+
when :playerDisconnected
|
99
|
+
player = data[0];
|
100
|
+
@idTable[player.id] = nil;
|
101
|
+
player.id = nil;
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Add or instantiate a new hook.
|
106
|
+
# This function will take either a Class of Hook::Base or an
|
107
|
+
# instance of it, and add it to the current list of hooks, thusly
|
108
|
+
# including it in the event processing
|
109
|
+
# @param hook [LZRTag::Hook::Base] The hook to instantiate and add
|
110
|
+
# @return The added hook
|
111
|
+
def add_hook(hook)
|
112
|
+
hook = hook.new(self) if hook.is_a? Class and hook <= LZRTag::Hook::Base;
|
113
|
+
|
114
|
+
unless(hook.is_a? LZRTag::Hook::Base)
|
115
|
+
raise ArgumentError, "Hook needs to be a Lasertag::EventHook!"
|
116
|
+
end
|
117
|
+
|
118
|
+
return if(@hooks.include? hook);
|
119
|
+
hook.on_hookin(self);
|
120
|
+
@hooks << hook;
|
121
|
+
|
122
|
+
return hook;
|
123
|
+
end
|
124
|
+
# Remove an existing hook from the system.
|
125
|
+
# This will remove the provided hook instance from the event handling
|
126
|
+
# @param hook [LZRTag::Hook::Base] The hook to remove
|
127
|
+
def remove_hook(hook)
|
128
|
+
unless(hook.is_a? Lasertag::EventHook)
|
129
|
+
raise ArgumentError, "Hook needs to be a Lasertag::EventHook!"
|
130
|
+
end
|
131
|
+
|
132
|
+
return unless @hooks.include? hook
|
133
|
+
hook.on_hookout();
|
134
|
+
@hooks.delete(hook);
|
135
|
+
end
|
136
|
+
|
137
|
+
# Return a player either by their ID or their DeviceID
|
138
|
+
# @return LZRTag::Player::Base
|
139
|
+
def [](c)
|
140
|
+
return @players[c] if c.is_a? String
|
141
|
+
return @idTable[c] if c.is_a? Integer
|
142
|
+
|
143
|
+
raise ArgumentError, "Unknown identifier for the player id!"
|
144
|
+
end
|
145
|
+
alias get_player []
|
146
|
+
|
147
|
+
# Run the provided block on each registered player.
|
148
|
+
# @param connected Only yield for connected players
|
149
|
+
# @yield [player] Yields for every played. With connected = true,
|
150
|
+
# only yields connected players
|
151
|
+
def each(connected: false)
|
152
|
+
@playerSynchMutex.synchronize {
|
153
|
+
@players.each do |_, player|
|
154
|
+
yield(player) if(player.connected? | !connected);
|
155
|
+
end
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the number of currently connected players
|
160
|
+
def num_connected()
|
161
|
+
n = 0;
|
162
|
+
self.each_connected do
|
163
|
+
n += 1;
|
164
|
+
end
|
165
|
+
return n;
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
require_relative 'hitArb_handler.rb'
|
3
|
+
require_relative '../player/hardware_player.rb'
|
4
|
+
|
5
|
+
module LZRTag
|
6
|
+
module Handler
|
7
|
+
# This class provides useful statistics about the current game and situation.
|
8
|
+
#
|
9
|
+
# The various readable attributes contain various information on the game, such as
|
10
|
+
# current kill count, damage done, team and brightness composition, etc.
|
11
|
+
# Most of this information can be used to determine game progress
|
12
|
+
class Count < HitArb
|
13
|
+
# Returns a Hash with keys 0..7, describing which teams have
|
14
|
+
# how many players
|
15
|
+
attr_reader :teamCount
|
16
|
+
# Returns a Hash with keys equal to player's brightnesses, describing
|
17
|
+
# how many players have which brightness
|
18
|
+
attr_reader :brightnessCount
|
19
|
+
|
20
|
+
def initialize(*args, **argHash)
|
21
|
+
super(*args, **argHash);
|
22
|
+
|
23
|
+
@teamCount = Hash.new();
|
24
|
+
7.times do |i|
|
25
|
+
@teamCount[i] = 0;
|
26
|
+
end
|
27
|
+
|
28
|
+
@brightnessCount = Hash.new();
|
29
|
+
Player::Hardware.getBrightnessKeys().each do |bKey|
|
30
|
+
@brightnessCount[bKey] = 0;
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def consume_event(evtName, data)
|
36
|
+
super(evtName, data);
|
37
|
+
|
38
|
+
case evtName
|
39
|
+
when :playerRegistered
|
40
|
+
@teamCount[data[0].team] += 1;
|
41
|
+
@brightnessCount[data[0].brightness] += 1;
|
42
|
+
when :playerUnregistered
|
43
|
+
@teamCount[data[0].team] -= 1;
|
44
|
+
@brightnessCount[data[0].brightness] -= 1;
|
45
|
+
when :playerTeamChanged
|
46
|
+
@teamCount[data[1]] -= 1;
|
47
|
+
@teamCount[data[0].team] += 1;
|
48
|
+
when :playerBrightnessChanged
|
49
|
+
@brightnessCount[data[1]] -= 1;
|
50
|
+
@brightnessCount[data[0].brightness] += 1;
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
|
2
|
+
require_relative 'count_handler.rb'
|
3
|
+
|
4
|
+
module LZRTag
|
5
|
+
module Handler
|
6
|
+
=begin
|
7
|
+
Game handler class, managing game registration and game ticks.
|
8
|
+
This class manages the lifecycle of any lasertag game started in it.
|
9
|
+
This includes sending a gameTick, managing phase and game state transitions,
|
10
|
+
sending out but also receiving information on the game state via MQTT, etc.
|
11
|
+
|
12
|
+
@example
|
13
|
+
handler = LZRTag::Handler::Game.new(mqtt);
|
14
|
+
|
15
|
+
handler.register_game("My Game", SomeGame);
|
16
|
+
handler.register_game("Other game!", SomeOtherGame);
|
17
|
+
|
18
|
+
handler.start_game(SomeGame); # This will fetch the registered name and publish to MQTT
|
19
|
+
# Alternatively, sending to Lasertag/Game/Controls/SetGame with
|
20
|
+
# the string name will switch to the game.
|
21
|
+
|
22
|
+
# Game and phase switching is possible at any time, and will be handled asynchronously
|
23
|
+
sleep 5;
|
24
|
+
handler.set_phase(:somePhase);
|
25
|
+
=end
|
26
|
+
class Game < Count
|
27
|
+
# Returns the instance of the currently active game, or nil if none is present
|
28
|
+
attr_reader :currentGame
|
29
|
+
|
30
|
+
# currently active game phase
|
31
|
+
# When set it will start a phase change, sending :gamePhaseEnds and
|
32
|
+
# :gamePhaseStarts events
|
33
|
+
# @see set_phase
|
34
|
+
# @return Symbol
|
35
|
+
attr_reader :gamePhase
|
36
|
+
|
37
|
+
# List of in-game players.
|
38
|
+
# This list is mainly to keep track of which players the game is acting
|
39
|
+
# upon, but updating it will also send a list of player ID Strings to
|
40
|
+
# MQTT (Lasertag/Game/ParticipatingPlayers), and will send :playerEnteredGame
|
41
|
+
# and :playerLeftGame events
|
42
|
+
# @return Array<LZRTag::Player::Base>
|
43
|
+
attr_reader :gamePlayers
|
44
|
+
|
45
|
+
def initialize(*data, **argHash)
|
46
|
+
super(*data, **argHash)
|
47
|
+
|
48
|
+
@lastTick = Time.now();
|
49
|
+
|
50
|
+
@lastGame = nil;
|
51
|
+
@currentGame = nil;
|
52
|
+
@nextGame = nil;
|
53
|
+
|
54
|
+
@gamePhase = :idle;
|
55
|
+
|
56
|
+
@gamePlayers = Array.new();
|
57
|
+
|
58
|
+
@knownGames = Hash.new();
|
59
|
+
|
60
|
+
_start_game_thread();
|
61
|
+
|
62
|
+
@mqtt.subscribe_to "Lasertag/Game/Controls/+" do |data, topics|
|
63
|
+
case topics[0]
|
64
|
+
when "SetPhase"
|
65
|
+
phase = data.to_sym;
|
66
|
+
if(get_allowed_phases().include? phase)
|
67
|
+
set_phase(phase);
|
68
|
+
end
|
69
|
+
when "SetGame"
|
70
|
+
if(@knownGames[data])
|
71
|
+
start_game(@knownGames[data])
|
72
|
+
elsif(data == "STOP")
|
73
|
+
stop_game();
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
clean_game_topics();
|
79
|
+
at_exit {
|
80
|
+
clean_game_topics();
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def clean_game_topics()
|
85
|
+
@mqtt.publish_to "Lasertag/Game/ParticipatingPlayers", [].to_json(), retain: true
|
86
|
+
@mqtt.publish_to "Lasertag/Game/KnownGames", [].to_json, retain: true;
|
87
|
+
@mqtt.publish_to "Lasertag/Game/CurrentGame", "", retain: true
|
88
|
+
end
|
89
|
+
private :clean_game_topics;
|
90
|
+
|
91
|
+
def _start_game_thread()
|
92
|
+
@gameTickThread = Thread.new() do
|
93
|
+
loop do
|
94
|
+
Thread.stop() until(@nextGame.is_a? LZRTag::Game::Base);
|
95
|
+
|
96
|
+
@currentGame = @nextGame;
|
97
|
+
set_phase(:starting);
|
98
|
+
|
99
|
+
@lastTick = Time.now();
|
100
|
+
while(@currentGame == @nextGame)
|
101
|
+
sleep @currentGame.tickTime
|
102
|
+
dT = Time.now() - @lastTick;
|
103
|
+
@lastTick = Time.now();
|
104
|
+
|
105
|
+
send_event(:gameTick, dT);
|
106
|
+
end
|
107
|
+
|
108
|
+
puts "Stopping current game.".green
|
109
|
+
set_phase(:idle);
|
110
|
+
sleep 1;
|
111
|
+
@currentGame = nil;
|
112
|
+
end
|
113
|
+
end
|
114
|
+
@gameTickThread.abort_on_exception = true;
|
115
|
+
end
|
116
|
+
private :_start_game_thread;
|
117
|
+
|
118
|
+
# Register a game by a tag.
|
119
|
+
# This function will register a given LZRTag::Game::Base class under a given
|
120
|
+
# string tag. This tag can then be used to, via MQTT, start the game, and is
|
121
|
+
# also used to give players a cleartext game name.
|
122
|
+
# A list of games is published to Lasertag/Game/KnownGames
|
123
|
+
# @param gameTag [String] Cleartext name of the game
|
124
|
+
# @param game [LZRTag::Game::Base] The game class to register
|
125
|
+
def register_game(gameTag, game)
|
126
|
+
raise ArgumentError, "Game Tag must be a string!" unless gameTag.is_a? String
|
127
|
+
raise ArgumentError, "Game must be a LZRTag::Game class" unless game <= LZRTag::Game::Base
|
128
|
+
|
129
|
+
@knownGames[gameTag] = game;
|
130
|
+
|
131
|
+
@mqtt.publish_to "Lasertag/Game/KnownGames", @knownGames.keys.to_json, retain: true;
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
def consume_event(event, data)
|
136
|
+
super(event, data)
|
137
|
+
|
138
|
+
return unless @currentGame
|
139
|
+
@currentGame.consume_event(event, data);
|
140
|
+
end
|
141
|
+
|
142
|
+
# Starts a given new game (or the last one).
|
143
|
+
# This function will take either a String (as registered with register_game),
|
144
|
+
# or a LZRTag::Game::Base class, instantiate it, and start it.
|
145
|
+
# If no fitting game was found, the game is instead stopped.
|
146
|
+
# @param game [String,LZRTag::Game::Base] The game, or game name, to start
|
147
|
+
def start_game(game = @lastGame)
|
148
|
+
@lastGame = game;
|
149
|
+
|
150
|
+
if(game.is_a? String and gClass = @knownGames[game])
|
151
|
+
game = gClass;
|
152
|
+
elsif(game.is_a? String)
|
153
|
+
stop_game();
|
154
|
+
return;
|
155
|
+
end
|
156
|
+
|
157
|
+
if(gKey = @knownGames.key(game))
|
158
|
+
@mqtt.publish_to "Lasertag/Game/CurrentGame", gKey, retain: true
|
159
|
+
puts "Starting game #{gKey}!".green
|
160
|
+
else
|
161
|
+
@mqtt.publish_to "Lasertag/Game/CurrentGame", "", retain: true
|
162
|
+
end
|
163
|
+
|
164
|
+
game = game.new(self) if game.is_a? Class and game <= LZRTag::Game::Base;
|
165
|
+
unless(game.is_a? LZRTag::Game::Base)
|
166
|
+
raise ArgumentError, "Game class needs to be specified!"
|
167
|
+
end
|
168
|
+
@nextGame = game;
|
169
|
+
send_event(:gameStarting, @nextGame);
|
170
|
+
|
171
|
+
@gameTickThread.run();
|
172
|
+
@mqtt.publish_to "Lasertag/Game/Phase/Valid",
|
173
|
+
get_allowed_phases.to_json(), retain: true
|
174
|
+
end
|
175
|
+
|
176
|
+
# Stops the currently running game.
|
177
|
+
def stop_game()
|
178
|
+
@nextGame = nil;
|
179
|
+
@mqtt.publish_to "Lasertag/Game/CurrentGame", "", retain: true
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns an Array<Symbol> of the currently allowed phases of this game.
|
183
|
+
# This list can also be retrieved via MQTT, under Lasertag/Game/CurrentGame
|
184
|
+
def get_allowed_phases()
|
185
|
+
allowedPhases = [:idle]
|
186
|
+
if(@currentGame)
|
187
|
+
allowedPhases = [allowedPhases, @currentGame.phases].flatten
|
188
|
+
end
|
189
|
+
|
190
|
+
return allowedPhases;
|
191
|
+
end
|
192
|
+
|
193
|
+
# Tries to change the current phase.
|
194
|
+
# This function will set the current phase to nextPhase, if it is an allowed
|
195
|
+
# one. However, if nextPhase does not belong to the list of allowed phases,
|
196
|
+
# and error is raised. The :gamePhaseEnds and :gamePhaseStarts events are
|
197
|
+
# triggered properly. This function can be called from any context, not just
|
198
|
+
# inside the game code itself.
|
199
|
+
# @see get_allowed_phases
|
200
|
+
def set_phase(nextPhase)
|
201
|
+
allowedPhases = get_allowed_phases();
|
202
|
+
|
203
|
+
raise ArgumentError, "Phase must be valid!" unless allowedPhases.include? nextPhase
|
204
|
+
|
205
|
+
puts "Phase started: #{nextPhase}!".green;
|
206
|
+
|
207
|
+
oldPhase = @gamePhase
|
208
|
+
send_event(:gamePhaseEnds, oldPhase, nextPhase)
|
209
|
+
|
210
|
+
@mqtt.publish_to "Lasertag/Game/Phase/Current", @gamePhase.to_s, retain: true
|
211
|
+
@gamePhase = nextPhase;
|
212
|
+
send_event(:gamePhaseStarts, nextPhase, oldPhase);
|
213
|
+
end
|
214
|
+
# Alias for set_phase
|
215
|
+
# @see set_phase
|
216
|
+
def gamePhase=(nextPhase)
|
217
|
+
set_phase(nextPhase)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Update the list of in-game players.
|
221
|
+
# @param [Array<LZRTag::Player::Base] Array of active players
|
222
|
+
def gamePlayers=(newPlayers)
|
223
|
+
raise ArgumentError, "Game player list shall be an array!" unless newPlayers.is_a? Array
|
224
|
+
@gamePlayers = newPlayers.dup;
|
225
|
+
|
226
|
+
@playerNames = Array.new();
|
227
|
+
plNameArray = Array.new();
|
228
|
+
@gamePlayers.each do |pl|
|
229
|
+
plNameArray << pl.deviceID();
|
230
|
+
end
|
231
|
+
|
232
|
+
newPlayers = @gamePlayers - @oldGamePlayers;
|
233
|
+
newPlayers.each do |pl|
|
234
|
+
send_event :playerEnteredGame, pl;
|
235
|
+
end
|
236
|
+
oldPlayers = @oldGamePlayers - @gamePlayers;
|
237
|
+
oldPlayers.each do |pl|
|
238
|
+
send_event :playerLeftGame, pl;
|
239
|
+
end
|
240
|
+
|
241
|
+
@oldGamePlayers = @gamePlayers.dup;
|
242
|
+
@mqtt.publish_to "Lasertag/Game/ParticipatingPlayers", plNameArray.to_json(), retain: true
|
243
|
+
end
|
244
|
+
|
245
|
+
# Yield for each currently in-game player
|
246
|
+
def each_participating()
|
247
|
+
@gamePlayers.each do |pl|
|
248
|
+
yield(pl)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Check if a player is currently in game
|
253
|
+
def in_game?(player)
|
254
|
+
return @gamePlayers.include? player
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|