-
Notifications
You must be signed in to change notification settings - Fork 0
/
autopilot.tcl
executable file
·457 lines (401 loc) · 16.8 KB
/
autopilot.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
#!/bin/sh
# Start Tcl \
exec tclsh $0 $@
# Copyright 2006 Brian Ronald. All rights reserved.
# Autopilot for use on OpenTTD dedicated server console.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# sanity check - do not run as root
if {$tcl_platform(user) == "root"} {
puts {WARNING}
puts {please do not run ap+ as user 'root'}
exit 1
}
# sanity check - require tcl 8.4
if {[package vcompare [info tclversion] 8.4] < 0} {
puts {WARNING}
puts "ap+ requires at least tcl version 8.4"
puts "You are trying to use tcl [info patchlevel]"
exit 1
}
# sanity check - require unix platform
if {$tcl_platform(platform) != {unix}} {
puts {WARNING}
puts "ap+ does not support your operating system ($tcl_platform(platform))"
exit 1
}
package require msgcat
package require Expect
log_user 0
set pidfile autopilot.pid
exec echo [ pid ] > $pidfile
# Decide which config file we're using; either set by environment, or default.
set inifilename openttd.cfg
if [info exists env(OTTD_CONFIG)] {
set inifilename $env(OTTD_CONFIG)
}
namespace eval mainloop {
# Do nothing; just make the namespace
}
# Fetch in our library of functions
source autopilot/libs/main.tcl
# Read in values from openttd.cfg
# namespace apconfig contains only configuration lists
::ap::config::load $inifilename
# load language definitions
::msgcat::mclocale [::ap::config::getLanguage en]
::msgcat::mcload autopilot/lang
# Our version - if you modify and redistribute, please change this
# string to reflect the fact that this autopilot isn't the original
# autopilot by Brian Ronald.
set version [::ap::func::getApVersion]
if {![info exists ::ap::config::autopilot]} {
error [::msgcat::mc dbg_autopilot_no_config $inifilename]
exit 1
}
# Check the config, and include support for extra features
if {[::ap::config::isEnabled autopilot use_irc]} {
source autopilot/libs/irc.tcl
}
if {[::ap::config::isEnabled autopilot use_mysql]} {
source autopilot/libs/mysql.tcl
}
if {[::ap::config::isEnabled autopilot use_signals]} {
source autopilot/libs/signals.tcl
}
# Three ways to start the game - new, load default, load specified
# Construct the command we plan to spawn.
if {[set openttd [::ap::config::get autopilot command]] == {} } {
set openttd {./openttd}
}
::ap::game::output [::msgcat::mc autopilot_engaged]
set arg1 [ lindex $argv 0 ]
set arg2 [ lindex $argv 1 ]
if { [ string equal "$arg1" "load" ] } {
if { [ string length $arg2 ] > 0 } {
set commandline "$openttd -c $inifilename -D -g $arg2 [::ap::config::get autopilot command_endargs]"
::ap::game::output [::msgcat::mc game_start_save [::ap::config::get network server_name]]
} else {
set commandline "$openttd -c $inifilename -D -g save/game.sav [::ap::config::get autopilot command_endargs]"
::ap::game::output [::msgcat::mc game_start_default [::ap::config::get network server_name]]
}
} else {
set commandline "$openttd -c $inifilename -D [::ap::config::get autopilot command_endargs]"
::ap::game::output [::msgcat::mc game_start_new [::ap::config::get network server_name]]
::ap::game::output [::msgcat::mc game_spec_landscape [::ap::config::get game_creation landscape]]
if {[::ap::config::get game_creation map_y] != {}} {
::ap::game::output [::msgcat::mc game_spec_dimensions [expr (pow(2,[::ap::config::get game_creation map_y]))] [expr (pow(2,[::ap::config::get game_creation map_x]))]]
} else {
::ap::game::output [::msgcat::mc game_spec_dimensions [expr (pow(2,[::ap::config::get patches map_y]))] [expr (pow(2,[::ap::config::get patches map_x]))]]
}
if {[::ap::config::get game_creation starting_year] != {}} {
::ap::game::output [::msgcat::mc game_spec_start_year [::ap::config::get game_creation starting_year]]
} else {
if {[::ap::config::get patches starting_year] != {}} {
::ap::game::output [::msgcat::mc game_spec_start_year [::ap::config::get patches starting_year]]
} else {
::ap::game::output [::msgcat::mc game_spec_start_year [::ap::config::get patches starting_date]]
}
}
if {[namespace exists ::mod_db]} {
::mod_db::newgame [::ap::config::get network server_name]
}
}
# Get the version
set ottd_version [::ap::game::version $openttd]
# Start openttd in dedicated mode
set ds [::ap::game::start $commandline]
set ::pause_level [::ap::config::get autopilot pause_level]
# Create a list of passwords if that feature is enabled, and trigger
# the recurring password randomizer
if {[::ap::config::isEnabled autopilot randomize_password]} {
if { $::pause_level > 0 } {
set ::pw_pause $::pause_level
} else {
set ::pw_pause [::ap::config::get network min_active_clients]
}
set ::players $::pw_pause
set wordfile [open [::ap::config::get autopilot password_list] "r"]
set worddata [read -nonewline $wordfile]
close $wordfile
set passwords [split $worddata "\n"]
set numpasswords [llength $passwords]
::ap::func::every [::ap::config::get autopilot password_frequency] {
if { $::players >= $::pw_pause } {
set ::password [::ap::func::lrandom $::passwords]
::ap::game::console "server_pw $::password\r"
::ap::callback::execute {} ::ap::game::say 0 [list {[callback] on_game_serverpw} $::password] {autopilot/scripts/callback/} {on_game_serverpw.tcl}
}
}
} else {
set ::password [::ap::config::get network server_password]
}
if {[namespace exists ::mod_db]} {
::mod_db::set_password $::password
}
# Set some expect variables
set spawn_id $ds
# Initialize the OpenTTD Console with useful aliases for ap
::ap::game::initConsole
# Send some one-off commands to the server
# set the debug level according to the autopilot config setting!
::ap::game::console "debug_level \"[::ap::config::get autopilot debug_level]\"\r"
# only pause a new game if 'pause_on_newgame' is enabled
if {[::ap::config::isEnabled gui pause_on_newgame] || [::ap::config::isEnabled patches pause_on_newgame]} {
::ap::game::pause
}
# Initialize other variables
set name [::ap::config::get network client_name]
if {[::ap::config::isEnabled autopilot use_console]} {
set standard_delay [expr ([::ap::config::get autopilot responsiveness] * 1000 + 500)]
} else {
set standard_delay 1000
}
# Getting started by sending a couple of commands and reading the output.
# We want to know the maximum number of companies, players and spectators,
# and details of which companies already exist, if any.
::ap::game::console "server_info\r"
# I *really* want these variables setting.
set timeout 3600
expect {
-re "Current/maximum clients: *\[ 0-9\]*/\[ 0-9\]{2}" {
scan $expect_out(0,string) "Current/maximum clients: %2d/%2d" players max_clients
exp_continue
}
-re "Current/maximum companies: *\[ 0-9\]*/\[ 0-9\]{2}" {
scan $expect_out(0,string) "Current/maximum companies: %2d/%2d" companies max_companies
exp_continue
}
-re "Current/maximum spectators: *\[ 0-9\]*/\[ 0-9\]{2}" {
scan $expect_out(0,string) "Current/maximum spectators: %2d/%2d" - max_spectators
}
}
# Now set the timeout for the main loop's expect
set timeout [::ap::config::get autopilot responsiveness]
# This is it - the main Expect loop. Wrapped in a namespace
# to avoid accidental pollution. It's monolithic, and
# unashamedly so.
namespace eval mainloop {
# Array for players
array set player {}
# map player names to id's
array set nick2id {}
# company array
array set company {}
# Whether to enable the console for commands
set use_console [::ap::config::get autopilot use_console]
if $use_console {log_user 1}
# Start a background periodic task to recount players and
# companies - just in case the game "forgets" to inform us
# and we lose count, only needed when ap controls pause.
if { $::pause_level >= 0 } {
::ap::func::every [::ap::config::get autopilot recount_frequency] ::ap::count::players
} else {
::ap::count::players
}
while true {
expect {
-re ".*\n" {
# This is a greedy regex, so it might *contain* more \n
set out_buffer $expect_out(0,string)
# The regex matches one or more lines. Separate them.
foreach linestr [split [string map {"\r" {} } $out_buffer] "\n"] {
# You'll get at least one empty from the split
if {$linestr != {} } {
regexp -nocase {^(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\])\s+(.*)} $linestr line timestamp linestr
set line [split $linestr]
# Get this far, and we have exactly one line of output from the server.
# Now we have fun with ifs and cases!
if {[string first {dbg: } $linestr] == 0} {
# this is debug output, just output the info to the console
puts $linestr
} elseif {[string first {openttd: } $linestr] == 0 && [::ap::config::isEnabled autopilot catch_assert]} {
# this looks like an assertion... just make sure it is one
if {[string first {Assertion} $linestr] > -1} {
# output it to irc, as the app will soon die
::ap::say::fromGame [string replace $linestr [string first {/} $linestr] [string first {/src/} $linestr]]
}
} elseif {[string first {[All] } $linestr] == 1 || [string first {[Private] } $linestr] == 1} {
set chat [regexp -inline -- {\[(All|Private)\] (.+?): (.*)} $linestr]
set nick [lindex $chat 2]
set lineafternick [lindex $chat 3]
set private 0
if {[lindex $chat 1] == "Private"} {
set private 1
}
if {$nick == $::name} {
# dont handle what we ourselves say!
} elseif {$private && [::ap::func::getClientId $nick] == 0} {
# if we say in private To somebody, the cought nick is prefixed with "To " and we get a 0 back as Id
# ignore commands the server might say in private
} elseif {[string first {!} $lineafternick] == 0} {
# this is a bang_command...
set bang_command [split [string range $lineafternick 1 end]]
switch -- $bang_command {
{version} {
ap::game::say::reply $private $nick $::version
}
{default} {
variable filename "[lindex $bang_command 0].tcl"
if {![::ap::callback::execute $nick ::ap::game::say $private [lrange $bang_command 0 end] {autopilot/scripts/game/} "$filename"]} {
if {![::ap::callback::execute $nick ::ap::game::say $private [lrange $bang_command 0 end] {autopilot/scripts/global/} "$filename"]} {
::ap::debug [namespace current] [::msgcat::mc dbg_callback_not_found [lindex $bang_command 0]]
}
}
}
}
} elseif {!$private} {
if {[string first {/me } $lineafternick] == 0} {
::ap::say::fromGame "* $nick [lrange $lineafternick 1 end]"
} else {
::ap::say::fromGame "<$nick> $lineafternick"
}
}
} elseif {[string first "Company Name" $linestr] > 1} {
# Output from players command, populate companies
# First pull out the name, which can contain quotes
set c_name [join [lrange [split $linestr '] 1 end-1] ']
set ncline [string map "{'$c_name'} discarded" $linestr]
# then scan everything else, which is far more predictable
scan $ncline "#:%d(%\[^)\]) Company Name: discarded Year Founded: %d Money: %d Loan: %d Value: %d (T:%\[^,\], R:%\[^,\], P:%\[^,\], S:%\[^,)])" c_number c_color c_founded c_money c_loan c_value c_trains c_roadvehicles c_planes c_ships
set company($c_number) "{$c_color} {$c_name} $c_founded $c_money $c_loan $c_value $c_trains $c_roadvehicles $c_planes $c_ships"
}
if {[string first "*** " $linestr] == 1} {
# Somebody joined, left or was renamed, or company changes occured
switch -regexp -- $linestr {
{\*{3} .* has joined the game .*$} {
# Joined the game. Greet, announce and increment count.
set nick [lrange [split $linestr] 1 end-6]
# We used to increment and decrement, but this also
# populates the player array.
::ap::count::players
after $::standard_delay [string map "NICK {$nick}" {::ap::callback::execute {NICK} ::ap::game::say 1 [list {[callback] on_game_join}] {autopilot/scripts/callback/} {on_game_join.tcl}}]
# Unpause if there are enough players.
if {[::ap::config::isEnabled autopilot save_on_join]} {
::ap::game::save "join_[format %x [clock seconds]]"
}
if {$::players > $::pause_level && $::pause_level >= 0} {
::ap::game::unpause
}
}
{\*{3} .* has left the game .*$} {
# Left the game. Announce and decrement count.
::ap::say::fromGame "*** [join [lrange $line 1 end]]"
# We used to increment and decrement, but this also
# populates the player array.
::ap::count::players
# Pause if there are too few players.
if {$::players <= $::pause_level && $::pause_level >= 0} {
::ap::game::pause
::ap::game::save
}
}
{\*{3} .* has changed his/her name to .*$} -
{\*{3} .* has joined (:?company #\d+|spectators)$} -
{\*{3} .* has started a new company .*$} -
{\*{3} Game (un)*paused \(manual\)$} -
{\*{3} Game (un)*paused \(number of players\)$} -
{\*{3} Game still paused \((manual, )*number of players\)$} -
{\*{3} Game (un)*paused \((not )*enough players\)$} {
::ap::say::fromGame "*** [lrange $line 1 end]"
::ap::count::players
}
}
}
if {[string first "Current/maximum companies: " $linestr] == 0} {
scan $linestr "Current/maximum companies: %2d/%2d" ::companies ::max_companies
}
if {[string first "'rcon_pw' changed to: " $linestr] == 0} {
set newentry "rcon_password [lindex $line 4]"
set location [lsearch [set ::ap::config::network] rcon_password*]
set ::ap::config::network "[lreplace [set ::ap::config::network] $location $location $newentry]"
}
if {[string first "'server_pw' changed to: " $linestr] == 0} {
set ::password [lindex $line 4]
if {[namespace exists ::mod_db]} {
::mod_db::set_password $::password
}
}
if {[regexp "^Client.*unique-id: '\[0-9,a-f\]*'\$" $linestr]} {
# We're discarding output from status
} elseif {[string first "Client" $linestr] == 0} {
# Output from clients command, populate players
# First pull out the name, which can contain quotes
set p_name [join [lrange [split $linestr '] 1 end-1] ']
set npline [string map "{'$p_name'} discarded" $linestr]
# then scan everything else, which is far more predictable
scan $npline "Client #%d name: discarded company: %d IP: %s" p_number p_company p_IP
# Ignore client #1 (the server)
if {$p_number > 1} {
set pl_number [array size player]
set nick2id($p_name) $p_number
if {$p_company > $::max_companies} {
set player([expr $pl_number + 1]) "{$p_name} $p_company $p_IP {[lindex $company(255) 0]} $p_number"
} else {
set player([expr $pl_number + 1]) "{$p_name} $p_company $p_IP {[lindex $company($p_company) 0]} $p_number"
}
}
}
if {[string match doneclientcount $linestr]} {
set ::players [array size player]
}
if {[string first {Map sucessfully saved to} $linestr] == 0} {
::ap::say::everywhere [::msgcat::mc game_saved]
}
}
}
}
eof {
# wait for the client process to quit
exp_wait
::ap::game::output [::msgcat::mc game_quit_message]
if {[namespace exists ::mod_db]} {
::mod_db::network::quit [::msgcat::mc game_quit_by_admin]
}
exec echo {} > $pidfile
break;
}
}
# Respond to player commands from console, if enabled
if $use_console {
expect_user {
"quit\n" {
::ap::say::everywhere [::msgcat::mc game_quit_by_admin]
::ap::game::quit
}
"exit\n" {
::ap::say::everywhere [::msgcat::mc game_quit_by_admin]
::ap::say::everywhere [::msgcat::mc game_saving]
::ap::game::save
::ap::game::quit
}
"save\n" {
::ap::say::everywhere [::msgcat::mc game_saving]
::ap::game::save
}
"version\n" {
puts $::version
}
"license\n" {
puts [::msgcat::mc autopilot_license]
}
-re "(.*)\n" {
::ap::game::console "$expect_out(1,string)\r"
}
}
}
}
# End of ::mainloop namespace
}