lzrtag-base 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,80 @@
|
|
1
|
+
|
2
|
+
require 'json'
|
3
|
+
require_relative 'base_handler.rb'
|
4
|
+
|
5
|
+
module LZRTag
|
6
|
+
module Handler
|
7
|
+
# Hit arbitration handling class.
|
8
|
+
# This class extends the Handler::Base class, adding important features
|
9
|
+
# to ensure that each player shot is only counted once.
|
10
|
+
# Additionally, hooks can manipulate this behavior and prevent friendly-fire,
|
11
|
+
# or enable multiple hits in the case of a shotgun!
|
12
|
+
#
|
13
|
+
# Shot arbitration is performed by listening to "Lasertag/Game/Events",
|
14
|
+
# waiting for a 'type: "hit"'
|
15
|
+
# If such a JSON payload is found, hit and source players will be
|
16
|
+
# determined, and every available hook's "process_raw_hit" function
|
17
|
+
# is called. If this function returns false, the hit will be "vetoed" and
|
18
|
+
# does not count at all. However, a hook can raise {LZRTag::Handler::NoArbitration},
|
19
|
+
# preventing this shot from being logged and thusly enabling multiple hits
|
20
|
+
# @see Base
|
21
|
+
class HitArb < Base
|
22
|
+
def initialize(*data, **options)
|
23
|
+
super(*data, **options);
|
24
|
+
|
25
|
+
@mqtt.subscribe_to "Lasertag/Game/Events" do |data|
|
26
|
+
begin
|
27
|
+
data = JSON.parse(data, symbolize_names: true);
|
28
|
+
|
29
|
+
if(data[:type] == "hit")
|
30
|
+
_handle_hitArb(data);
|
31
|
+
end
|
32
|
+
rescue JSON::ParserError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_raw_hit(*)
|
38
|
+
return true;
|
39
|
+
end
|
40
|
+
|
41
|
+
def _handle_hitArb(data)
|
42
|
+
unless (hitPlayer = get_player(data[:target])) and
|
43
|
+
(sourcePlayer = get_player(data[:shooterID])) and
|
44
|
+
(arbCode = data[:arbCode])
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
return if (sourcePlayer.hitIDTimetable[arbCode] + 1) > Time.now();
|
49
|
+
|
50
|
+
veto = false;
|
51
|
+
arbitrateShot = true;
|
52
|
+
|
53
|
+
hookList = Array.new();
|
54
|
+
hookList << @hooks;
|
55
|
+
if(@currentGame)
|
56
|
+
hookList << @currentGame.hookList
|
57
|
+
end
|
58
|
+
hookList.flatten
|
59
|
+
|
60
|
+
hookList.each do |h|
|
61
|
+
begin
|
62
|
+
veto |= !(h.process_raw_hit(hitPlayer, sourcePlayer));
|
63
|
+
rescue NoArbitration
|
64
|
+
arbitrateShot = false;
|
65
|
+
end
|
66
|
+
end
|
67
|
+
return if veto;
|
68
|
+
|
69
|
+
if arbitrateShot
|
70
|
+
sourcePlayer.hitIDTimetable[arbCode] = Time.now();
|
71
|
+
end
|
72
|
+
|
73
|
+
send_event(:playerHit, hitPlayer, sourcePlayer);
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class NoArbitration < Exception
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
|
2
|
+
module LZRTag
|
3
|
+
module Hook
|
4
|
+
=begin
|
5
|
+
Base class for all game hooks, implements DSL.
|
6
|
+
This class shall be used as a base class for any DIY game hooks.
|
7
|
+
The purpose of any hook is to implement a specific element of a game,
|
8
|
+
such as damaging players, regenerating them, handing out teams, etc.
|
9
|
+
|
10
|
+
This hook base class implements a DSL that makes it very easy for the application
|
11
|
+
to implement their own behavior in a modular fashion.
|
12
|
+
|
13
|
+
@example
|
14
|
+
class MyHook < LZRTag::Base::Hook
|
15
|
+
# Hooks can register which parameters are configurable, and how
|
16
|
+
# This will be used to reconfigure them on the fly, but it is not mandatory
|
17
|
+
# to register options.
|
18
|
+
describe_option :myValue, "A description of my Value";
|
19
|
+
|
20
|
+
def initialize(handler, **options)
|
21
|
+
super(handler);
|
22
|
+
|
23
|
+
@myValue = options[:myValue] || "default";
|
24
|
+
end
|
25
|
+
|
26
|
+
# The "on" DSL makes it easy to perform tasks on
|
27
|
+
# any arbitrary event
|
28
|
+
on :playerKilled do |player|
|
29
|
+
puts "Player #{player.name} was killed :C";
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class MyGame < LZRTag::Game::Base
|
34
|
+
# A game can register that it wants to use this hook, and
|
35
|
+
# even which options to use for it.
|
36
|
+
hook :aHook, MyHook, {myValue: "Not a default!"};
|
37
|
+
end
|
38
|
+
|
39
|
+
# Alternatively, the hook can be added to the game directly
|
40
|
+
handler.add_hook(MyHook);
|
41
|
+
=end
|
42
|
+
class Base
|
43
|
+
def self.getCBs()
|
44
|
+
@globalCBList ||= Hash.new();
|
45
|
+
return @globalCBList;
|
46
|
+
end
|
47
|
+
def self.getOptionDescriptions()
|
48
|
+
@globalOptionDescriptions ||= Hash.new();
|
49
|
+
return @globalOptionDescriptions
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(handler)
|
53
|
+
@localCBList = Hash.new();
|
54
|
+
|
55
|
+
@handler = handler
|
56
|
+
end
|
57
|
+
|
58
|
+
# DSL function to describe an option of this hook.
|
59
|
+
# The application can use this DSL to describe a given option,
|
60
|
+
# identified by optionSymbol. The extraDetails hash is optional,
|
61
|
+
# but in the future will allow this hook to be reconfigured remotely
|
62
|
+
# via MQTT!
|
63
|
+
# @param optionSymbol [Symbol] The Symbol used for this option
|
64
|
+
# @param descString [String] String description of this option.
|
65
|
+
# @param extraDetails [Hash] Optional hash to provide further details
|
66
|
+
# of this option, such as "min", "max", "type", etc.
|
67
|
+
def self.describe_option(optionSymbol, descString, extraDetails = {})
|
68
|
+
raise ArgumentError, "Option shall be a symbol!" unless optionSymbol.is_a? Symbol
|
69
|
+
raise ArgumentError, "Description should be a string!" unless descString.is_a? String
|
70
|
+
getOptionDescriptions()[optionSymbol] = extraDetails;
|
71
|
+
getOptionDescriptions()[optionSymbol][:desc] = descString;
|
72
|
+
end
|
73
|
+
|
74
|
+
# DSL function to add a block on an event.
|
75
|
+
# The application can provide a block to this function that will be executed
|
76
|
+
# for every "evtName" game event
|
77
|
+
# @param evtName [Symbol] Event name to trigger this block on
|
78
|
+
def self.on(evtName, &block)
|
79
|
+
raise ArgumentError, "Block needs to be given!" unless block_given?
|
80
|
+
|
81
|
+
evtName = [evtName].flatten
|
82
|
+
evtName.each do |evt|
|
83
|
+
unless (evt.is_a? Symbol)
|
84
|
+
raise ArgumentError, "Event needs to be a symbol or array of symbols!"
|
85
|
+
end
|
86
|
+
getCBs()[evt] ||= Array.new();
|
87
|
+
getCBs()[evt] << block;
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Function to add hooks to the already instantiated hook.
|
92
|
+
# This function works exactly like {self.on}, except that it
|
93
|
+
# acts on an instance of hook, and allows the app to extend a standard
|
94
|
+
# hook by extending it afterwards.
|
95
|
+
def on(evtName, &block)
|
96
|
+
raise ArgumentError, "Block needs to be given!" unless block_given?
|
97
|
+
|
98
|
+
evtName = [evtName].flatten
|
99
|
+
evtName.each do |evt|
|
100
|
+
unless (evt.is_a? Symbol)
|
101
|
+
raise ArgumentError, "Event needs to be a symbol or array of symbols!"
|
102
|
+
end
|
103
|
+
@localCBList[evt] ||= Array.new();
|
104
|
+
@localCBList[evt] << block;
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @private
|
109
|
+
def consume_event(evtName, data)
|
110
|
+
if(cbList = self.class.getCBs()[evtName])
|
111
|
+
cbList.each do |cb|
|
112
|
+
begin
|
113
|
+
instance_exec(*data, &cb);
|
114
|
+
rescue StandardError => e
|
115
|
+
puts e.message
|
116
|
+
puts e.backtrace.inspect
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
if(cbList = @localCBList[evtName]) then
|
121
|
+
cbList.each do |cb|
|
122
|
+
begin
|
123
|
+
cb.call(*data);
|
124
|
+
rescue StandardError => e
|
125
|
+
puts e.message
|
126
|
+
puts e.backtrace.inspect
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_hookin(handler)
|
133
|
+
@handler = handler;
|
134
|
+
end
|
135
|
+
def on_hookout()
|
136
|
+
end
|
137
|
+
|
138
|
+
def process_raw_hit(*)
|
139
|
+
return true;
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
|
2
|
+
require_relative 'base_hook.rb'
|
3
|
+
|
4
|
+
module LZRTag
|
5
|
+
module Hook
|
6
|
+
class Debug < Base
|
7
|
+
attr_accessor :eventWhitelist
|
8
|
+
attr_accessor :eventBlacklist
|
9
|
+
|
10
|
+
def initialize()
|
11
|
+
super();
|
12
|
+
|
13
|
+
@eventWhitelist = Array.new();
|
14
|
+
@eventBlacklist = Array.new();
|
15
|
+
end
|
16
|
+
|
17
|
+
def consume_event(evtName, data)
|
18
|
+
super(evtName, data);
|
19
|
+
|
20
|
+
return if @eventBlacklist.include? evtName
|
21
|
+
unless(@eventWhitelist.empty?)
|
22
|
+
return unless @eventWhitelist.include? evtName
|
23
|
+
end
|
24
|
+
|
25
|
+
puts "Caught event: #{evtName} with data: #{data}";
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class TeamSelector < Base
|
30
|
+
describe_option :possibleTeams, "List of teams that can be selected", {
|
31
|
+
type: Array
|
32
|
+
}
|
33
|
+
|
34
|
+
def initialize(handler, possibleTeams: [1, 2, 3, 4])
|
35
|
+
super(handler);
|
36
|
+
|
37
|
+
@possibleTeams = possibleTeams;
|
38
|
+
end
|
39
|
+
|
40
|
+
on :gamePhaseEnds do |oldPhase, nextPhase|
|
41
|
+
if((oldPhase == :teamSelect) && (nextPhase != :idle))
|
42
|
+
puts "Selecting active players!"
|
43
|
+
|
44
|
+
@handler.gamePlayers = Array.new();
|
45
|
+
@handler.each do |pl|
|
46
|
+
if(pl.brightness == :active)
|
47
|
+
@handler.gamePlayers << pl;
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
puts "Game players are: #{@handler.gamePlayers}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
on :gamePhaseStarts do |nextPhase, oldPhase|
|
56
|
+
case(nextPhase)
|
57
|
+
when :teamSelect
|
58
|
+
@handler.each do |pl|
|
59
|
+
pl.brightness = :idle;
|
60
|
+
|
61
|
+
if(!@possibleTeams.include?(pl.team))
|
62
|
+
pl.team = @possibleTeams.sample();
|
63
|
+
end
|
64
|
+
end
|
65
|
+
when :idle
|
66
|
+
@handler.each do |pl|
|
67
|
+
pl.brightness = :idle;
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
on :poseChanged do |pl, nPose|
|
73
|
+
next if(@handler.gamePhase != :teamSelect)
|
74
|
+
next if(pl.brightness == :active)
|
75
|
+
|
76
|
+
pl.brightness = (pl.gyroPose == :laidDown) ? :idle : :teamSelect;
|
77
|
+
end
|
78
|
+
|
79
|
+
on :navSwitchPressed do |player, dir|
|
80
|
+
next if(@handler.gamePhase != :teamSelect)
|
81
|
+
next unless [:teamSelect, :idle].include? player.brightness
|
82
|
+
|
83
|
+
newTeam = @possibleTeams.find_index(player.team) || 0;
|
84
|
+
|
85
|
+
newTeam += 1 if(dir == 2)
|
86
|
+
newTeam -= 1 if(dir == 3)
|
87
|
+
|
88
|
+
player.team = @possibleTeams[newTeam % @possibleTeams.length]
|
89
|
+
if(dir == 1)
|
90
|
+
player.brightness = :active
|
91
|
+
else
|
92
|
+
player.brightness = :teamSelect
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Regenerator < Base
|
98
|
+
|
99
|
+
describe_option :regRate, "Regeneraton rate, HP per second"
|
100
|
+
describe_option :regDelay, "Healing delay, in s, after a player was hit"
|
101
|
+
describe_option :healDead, "Whether or not to heal dead players"
|
102
|
+
|
103
|
+
describe_option :autoReviveThreshold, "The HP a player needs before he is revived"
|
104
|
+
|
105
|
+
describe_option :teamFilter, "Which teams this regenerator belongs to"
|
106
|
+
describe_option :phaseFilter, "During which phases this hook should be active"
|
107
|
+
|
108
|
+
def initialize(handler, **options)
|
109
|
+
super(handler);
|
110
|
+
|
111
|
+
@regRate = options[:regRate] || 1;
|
112
|
+
@regDelay = options[:regDelay] || 10;
|
113
|
+
|
114
|
+
@healDead = options[:healDead] || false;
|
115
|
+
@autoReviveThreshold = options[:autoReviveThreshold] || 30;
|
116
|
+
|
117
|
+
@teamFilter = options[:teamFilter] || (0..7).to_a
|
118
|
+
@phaseFilter = options[:phaseFilter] || [:running]
|
119
|
+
end
|
120
|
+
|
121
|
+
on :gameTick do |dT|
|
122
|
+
next unless @phaseFilter.include? @handler.gamePhase
|
123
|
+
|
124
|
+
@handler.each_participating do |pl|
|
125
|
+
next unless @teamFilter.include? pl.team
|
126
|
+
|
127
|
+
if((Time.now() - pl.lastDamageTime) >= @regDelay)
|
128
|
+
pl.regenerate(dT * @regRate);
|
129
|
+
end
|
130
|
+
|
131
|
+
if(pl.dead and pl.life >= @autoReviveThreshold)
|
132
|
+
pl.dead = false;
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Damager < Base
|
139
|
+
describe_option :dmgPerShot, "Base damage per shot"
|
140
|
+
describe_option :useDamageMultiplier, "Shall shots be adjusted per-gun?"
|
141
|
+
describe_option :friendlyFire, "Shall friendly-fire be enabled"
|
142
|
+
describe_option :hitThreshold, "Limit below dead players will not be hit"
|
143
|
+
|
144
|
+
def initialize(handler, **options)
|
145
|
+
super(handler);
|
146
|
+
|
147
|
+
@dmgPerShot = options[:dmgPerShot] || 40;
|
148
|
+
@useDamageMultiplier = options[:useDamageMultiplier] || true;
|
149
|
+
@friendlyFire = options[:friendlyFire] || false;
|
150
|
+
@hitThreshold = options[:hitThreshold] || 10;
|
151
|
+
end
|
152
|
+
|
153
|
+
def process_raw_hit(hitPlayer, sourcePlayer)
|
154
|
+
unless(@friendlyFire)
|
155
|
+
return false if hitPlayer.team == sourcePlayer.team
|
156
|
+
end
|
157
|
+
return false if(hitPlayer.dead && (hitPlayer.life < @hitThreshold));
|
158
|
+
|
159
|
+
return true;
|
160
|
+
end
|
161
|
+
|
162
|
+
on :playerHit do |hitPlayer, sourcePlayer|
|
163
|
+
shotMultiplier = 1;
|
164
|
+
|
165
|
+
if((@useDamageMultiplier) && (!sourcePlayer.nil?))
|
166
|
+
shotMultiplier = sourcePlayer.gunDamage();
|
167
|
+
end
|
168
|
+
|
169
|
+
hitPlayer.damage_by(@dmgPerShot * shotMultiplier, sourcePlayer);
|
170
|
+
hitPlayer.hit();
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
require_relative 'map_zone.rb'
|
3
|
+
require_relative 'myMaps_parser.rb'
|
4
|
+
|
5
|
+
module LZRTag
|
6
|
+
module Map
|
7
|
+
class Set
|
8
|
+
attr_reader :zones
|
9
|
+
|
10
|
+
attr_accessor :centerpoint
|
11
|
+
|
12
|
+
def initialize(mqtt, zones = Array.new())
|
13
|
+
@mqtt = mqtt;
|
14
|
+
@zones = zones;
|
15
|
+
|
16
|
+
@centerpoint = Array.new();
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_h()
|
20
|
+
outData = Hash.new();
|
21
|
+
|
22
|
+
if(@centerpoint.length != 3)
|
23
|
+
raise ArgumentError, "Center point needs to be set!"
|
24
|
+
end
|
25
|
+
|
26
|
+
outData[:centerpoint] = @centerpoint
|
27
|
+
|
28
|
+
outData[:zones] = Array.new();
|
29
|
+
@zones.each do |z|
|
30
|
+
outData[:zones] << z.to_h;
|
31
|
+
end
|
32
|
+
|
33
|
+
return outData;
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_json()
|
37
|
+
return self.to_h().to_json();
|
38
|
+
end
|
39
|
+
|
40
|
+
def publish()
|
41
|
+
@mqtt.publish_to "Lasertag/Zones", self.to_json, qos: 1, retain: true;
|
42
|
+
end
|
43
|
+
def clear()
|
44
|
+
@mqtt.publish_to "Lasertag/Game/Zones", "", retain: true;
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
|
2
|
+
module LZRTag
|
3
|
+
module Map
|
4
|
+
class Zone
|
5
|
+
attr_accessor :tag
|
6
|
+
|
7
|
+
attr_accessor :centerPoint, :radius
|
8
|
+
attr_accessor :polygon
|
9
|
+
attr_accessor :coordinatesAsGPS
|
10
|
+
|
11
|
+
attr_accessor :teamMask
|
12
|
+
|
13
|
+
attr_accessor :data
|
14
|
+
|
15
|
+
attr_accessor :style
|
16
|
+
|
17
|
+
def initialize()
|
18
|
+
@centerPoint = [0, 0];
|
19
|
+
@radius = 0;
|
20
|
+
@polygon = Array.new();
|
21
|
+
@coordinatesAsGPS = false;
|
22
|
+
|
23
|
+
@teamMask = 255;
|
24
|
+
|
25
|
+
@style = {
|
26
|
+
color: "transparent",
|
27
|
+
borderColor: "black"
|
28
|
+
}
|
29
|
+
|
30
|
+
@data = Hash.new();
|
31
|
+
end
|
32
|
+
|
33
|
+
def affects_teams(teamList)
|
34
|
+
@teamMask = 0;
|
35
|
+
[teamList].flatten.each do |t|
|
36
|
+
@teamMask += (2<<t);
|
37
|
+
end
|
38
|
+
end
|
39
|
+
def ignores_teams(teamList)
|
40
|
+
@teamMask = 255;
|
41
|
+
[teamList].flatten.each do |t|
|
42
|
+
@teamMask -= (2<<t);
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.from_raw_zone(rawZone)
|
47
|
+
outZone = Zone.new();
|
48
|
+
|
49
|
+
outZone.tag = rawZone[:arguments]["tag"] || rawZone[:name];
|
50
|
+
|
51
|
+
outZone.polygon = rawZone[:polygon];
|
52
|
+
outZone.coordinatesAsGPS = true;
|
53
|
+
|
54
|
+
if(rawZone[:style])
|
55
|
+
outZone.style = rawZone[:style];
|
56
|
+
end
|
57
|
+
|
58
|
+
if(tMask = rawZone[:arguments]["teamMask"])
|
59
|
+
outZone.teamMask = tMask.to_i;
|
60
|
+
rawZone[:arguments].delete "teamMask"
|
61
|
+
end
|
62
|
+
|
63
|
+
outZone.data = rawZone[:arguments];
|
64
|
+
|
65
|
+
return outZone
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_h()
|
69
|
+
outHash = Hash.new();
|
70
|
+
|
71
|
+
raise ArgumentError, "Tag needs to be set!" if(@tag.nil?);
|
72
|
+
outHash[:tag] = @tag;
|
73
|
+
outHash[:teamMask] = @teamMask;
|
74
|
+
|
75
|
+
if(@radius > 0.1)
|
76
|
+
outHash[:centerPoint] = @centerPoint;
|
77
|
+
outHash[:radius] = @radius;
|
78
|
+
else
|
79
|
+
outHash[:polygon] = @polygon;
|
80
|
+
end
|
81
|
+
outHash[:coordinatesAsGPS] = @coordinatesAsGPS
|
82
|
+
|
83
|
+
outHash[:style] = @style;
|
84
|
+
|
85
|
+
outHash[:data] = @data;
|
86
|
+
|
87
|
+
return outHash;
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect()
|
91
|
+
return "#<Zone: #{to_h}>";
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|