home · contact · privacy
Server/py: Extend command_makeworld().
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7
8
9 def setup_server_io():
10     """Fill IO files DB with proper file( path)s. Write process IO test string.
11
12     Ensure IO files directory at server/. Remove any old input file if found.
13     Set up new input file for reading, and new output file for writing. Start
14     output file with process hash line of format PID + " " + floated UNIX time
15     (io_db["teststring"]). Raise SystemExit if file is found at path of either
16     record or save file plus io_db["tmp_suffix"].
17     """
18     def detect_atomic_leftover(path, tmp_suffix):
19         path_tmp = path + tmp_suffix
20         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
21               "aborted previous attempt to write '" + path + "'. Aborting " \
22              "until matter is resolved by removing it from its current path."
23         if os.access(path_tmp, os.F_OK):
24             raise SystemExit(msg)
25     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
26     os.makedirs(io_db["path_server"], exist_ok=True)
27     io_db["file_out"] = open(io_db["path_out"], "w")
28     io_db["file_out"].write(io_db["teststring"] + "\n")
29     io_db["file_out"].flush()
30     if os.access(io_db["path_in"], os.F_OK):
31         os.remove(io_db["path_in"])
32     io_db["file_in"] = open(io_db["path_in"], "w")
33     io_db["file_in"].close()
34     io_db["file_in"] = open(io_db["path_in"], "r")
35     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
36     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
37
38
39 def cleanup_server_io():
40     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
41     def helper(file_key, path_key):
42         if file_key in io_db:
43             io_db[file_key].close()
44             if not io_db["kicked_by_rival"] \
45                and os.access(io_db[path_key], os.F_OK):
46                 os.remove(io_db[path_key])
47     helper("file_out", "path_out")
48     helper("file_in", "path_in")
49     helper("file_worldstate", "path_worldstate")
50     if "file_record" in io_db:
51         io_db["file_record"].close()
52
53
54 def obey(command, prefix, replay=False, do_record=False):
55     """Call function from commands_db mapped to command's first token.
56
57     The command string is tokenized by shlex.split(comments=True). If replay is
58     set, a non-meta command from the commands_db merely triggers obey() on the
59     next command from the records file. If not, and do do_record is set,
60     non-meta commands are recorded via record(), and save_world() is called.
61     The prefix string is inserted into the server's input message between its
62     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
63     """
64     server_test()
65     print("input " + prefix + ": " + command)
66     try:
67         tokens = shlex.split(command, comments=True)
68     except ValueError as err:
69         print("Can't tokenize command string: " + str(err) + ".")
70         return
71     if len(tokens) > 0 and tokens[0] in commands_db \
72        and len(tokens) == commands_db[tokens[0]][0] + 1:
73         if commands_db[tokens[0]][1]:
74             commands_db[tokens[0]][2]()
75         elif replay:
76             print("Due to replay mode, reading command as 'go on in record'.")
77             line = io_db["file_record"].readline()
78             if len(line) > 0:
79                 obey(line.rstrip(), io_db["file_record"].prefix
80                      + str(io_db["file_record"].line_n))
81                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
82             else:
83                 print("Reached end of record file.")
84         else:
85             commands_db[tokens[0]][2](*tokens[1:])
86             if do_record:
87                 record(command)
88                 save_world()
89     elif 0 != len(tokens):
90         print("Invalid command/argument, or bad number of tokens.")
91
92
93 def atomic_write(path, text, do_append=False):
94     """Atomic write of text to file at path, appended if do_append is set."""
95     path_tmp = path + io_db["tmp_suffix"]
96     mode = "w"
97     if do_append:
98         mode = "a"
99         if os.access(path, os.F_OK):
100             shutil.copyfile(path, path_tmp)
101     file = open(path_tmp, mode)
102     file.write(text)
103     file.flush()
104     os.fsync(file.fileno())
105     file.close()
106     if os.access(path, os.F_OK):
107         os.remove(path)
108     os.rename(path_tmp, path)
109
110
111 def record(command):
112     """Append command string plus newline to record file. (Atomic.)"""
113     # This misses some optimizations from the original record(), namely only
114     # finishing the atomic write with expensive flush() and fsync() every 15
115     # seconds unless explicitely forced. Implement as needed.
116     atomic_write(io_db["path_record"], command + "\n", do_append=True)
117
118
119 def save_world():
120     """Save all commands needed to reconstruct current world state."""
121     # TODO: Misses same optimizations as record() from the original record().
122
123     def quote(string):
124         string = string.replace("\u005C", '\u005C\u005C')
125         return '"' + string.replace('"', '\u005C"') + '"'
126
127     def mapsetter(key):
128         def helper(id):
129             string = ""
130             if world_db["Things"][id][key]:
131                 rmap = world_db["Things"][id][key]
132                 length = world_db["MAP_LENGTH"]
133                 for i in range(world_db["MAP_LENGTH"]):
134                     line = rmap[i * length:(i * length) + length].decode()
135                     string = string + key + " " + str(i) + quote(line) + "\n"
136             return string
137         return helper
138
139     def memthing(id):
140         string = ""
141         for memthing in world_db["Things"][id]["T_MEMTHING"]:
142             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
143                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
144         return string
145
146     def helper(category, id_string, special_keys={}):
147         string = ""
148         for id in world_db[category]:
149             string = string + id_string + " " + str(id) + "\n"
150             for key in world_db[category][id]:
151                 if not key in special_keys:
152                     x = world_db[category][id][key]
153                     argument = quote(x) if str == type(x) else str(x)
154                     string = string + key + " " + argument + "\n"
155                 elif special_keys[key]:
156                     string = string + special_keys[key](id)
157         return string
158
159     string = ""
160     for key in world_db:
161         if dict != type(world_db[key]):
162             string = string + key + " " + str(world_db[key]) + "\n"
163     string = string + helper("ThingActions", "TA_ID")
164     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
165     for id in world_db["ThingTypes"]:
166         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
167                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
168     string = string + helper("Things", "T_ID",
169                              {"T_CARRIES": False, "carried": False,
170                               "T_MEMMAP": mapsetter("T_MEMMAP"),
171                               "T_MEMTHING": memthing,
172                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
173     for id in world_db["Things"]:
174         if [] != world_db["Things"][id]["T_CARRIES"]:
175             string = string + "T_ID " + str(id) + "\n"
176             for carried_id in world_db["Things"][id]["T_CARRIES"]:
177                 string = string + "T_CARRIES " + str(carried_id) + "\n"
178     atomic_write(io_db["path_save"], string)
179
180
181 def obey_lines_in_file(path, name, do_record=False):
182     """Call obey() on each line of path's file, use name in input prefix."""
183     file = open(path, "r")
184     line_n = 1
185     for line in file.readlines():
186         obey(line.rstrip(), name + "file line " + str(line_n),
187              do_record=do_record)
188         line_n = line_n + 1
189     file.close()
190
191
192 def parse_command_line_arguments():
193     """Return settings values read from command line arguments."""
194     parser = argparse.ArgumentParser()
195     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
196                         action='store')
197     opts, unknown = parser.parse_known_args()
198     return opts
199
200
201 def server_test():
202     """Ensure valid server out file belonging to current process.
203
204     This is done by comparing io_db["teststring"] to what's found at the start
205     of the current file at io_db["path_out"]. On failure, set
206     io_db["kicked_by_rival"] and raise SystemExit.
207     """
208     if not os.access(io_db["path_out"], os.F_OK):
209         raise SystemExit("Server output file has disappeared.")
210     file = open(io_db["path_out"], "r")
211     test = file.readline().rstrip("\n")
212     file.close()
213     if test != io_db["teststring"]:
214         io_db["kicked_by_rival"] = True
215         msg = "Server test string in server output file does not match. This" \
216               " indicates that the current server process has been " \
217               "superseded by another one."
218         raise SystemExit(msg)
219
220
221 def read_command():
222     """Return next newline-delimited command from server in file.
223
224     Keep building return string until a newline is encountered. Pause between
225     unsuccessful reads, and after too much waiting, run server_test().
226     """
227     wait_on_fail = 1
228     max_wait = 5
229     now = time.time()
230     command = ""
231     while True:
232         add = io_db["file_in"].readline()
233         if len(add) > 0:
234             command = command + add
235             if len(command) > 0 and "\n" == command[-1]:
236                 command = command[:-1]
237                 break
238         else:
239             time.sleep(wait_on_fail)
240             if now + max_wait < time.time():
241                 server_test()
242                 now = time.time()
243     return command
244
245
246 def replay_game():
247     """Replay game from record file.
248
249     Use opts.replay as breakpoint turn to which to replay automatically before
250     switching to manual input by non-meta commands in server input file
251     triggering further reads of record file. Ensure opts.replay is at least 1.
252     """
253     if opts.replay < 1:
254         opts.replay = 1
255     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
256           " (if so late a turn is to be found).")
257     if not os.access(io_db["path_record"], os.F_OK):
258         raise SystemExit("No record file found to replay.")
259     io_db["file_record"] = open(io_db["path_record"], "r")
260     io_db["file_record"].prefix = "record file line "
261     io_db["file_record"].line_n = 1
262     while world_db["TURN"] < opts.replay:
263         line = io_db["file_record"].readline()
264         if "" == line:
265             break
266         obey(line.rstrip(), io_db["file_record"].prefix
267              + str(io_db["file_record"].line_n))
268         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
269     while True:
270         obey(read_command(), "in file", replay=True)
271
272
273 def play_game():
274     """Play game by server input file commands. Before, load save file found.
275
276     If no save file is found, a new world is generated from the commands in the
277     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
278     command and all that follow via the server input file.
279     """
280     if os.access(io_db["path_save"], os.F_OK):
281         obey_lines_in_file(io_db["path_save"], "save")
282     else:
283         if not os.access(io_db["path_worldconf"], os.F_OK):
284             msg = "No world config file from which to start a new world."
285             raise SystemExit(msg)
286         obey_lines_in_file(io_db["path_worldconf"], "world config ",
287                            do_record=True)
288         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
289     while True:
290         obey(read_command(), "in file", do_record=True)
291
292
293 def remake_map():
294     # DUMMY map creator.
295     world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
296
297
298 def set_world_inactive():
299     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
300     server_test()
301     if os.access(io_db["path_worldstate"], os.F_OK):
302         os.remove(io_db["path_worldstate"])
303     world_db["WORLD_ACTIVE"] = 0
304
305
306 def integer_test(val_string, min, max):
307     """Return val_string if possible integer >= min and <= max, else None."""
308     try:
309         val = int(val_string)
310         if val < min or val > max:
311             raise ValueError
312         return val
313     except ValueError:
314         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
315               str(max) + ".")
316         return None
317
318
319 def setter(category, key, min, max):
320     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
321     if category is None:
322         def f(val_string):
323             val = integer_test(val_string, min, max)
324             if None != val:
325                 world_db[key] = val
326     else:
327         if category == "Thing":
328             id_store = command_tid
329             decorator = test_Thing_id
330         elif category == "ThingType":
331             id_store = command_ttid
332             decorator = test_ThingType_id
333         elif category == "ThingAction":
334             id_store = command_taid
335             decorator = test_ThingAction_id
336
337         @decorator
338         def f(val_string):
339             val = integer_test(val_string, min, max)
340             if None != val:
341                 world_db[category + "s"][id_store.id][key] = val
342     return f
343
344
345 def command_ping():
346     """Send PONG line to server output file."""
347     io_db["file_out"].write("PONG\n")
348     io_db["file_out"].flush()
349
350
351 def command_quit():
352     """Abort server process."""
353     raise SystemExit("received QUIT command")
354
355
356 def command_seedmap(seed_string):
357     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
358     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
359     remake_map()
360
361
362 def command_makeworld(seed_string):
363     # DUMMY.
364     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
365     player_will_be_generated = False
366     for ThingType in world_db["ThingTypes"]:
367         if 0 == ThingType:
368             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
369                 player_will_be_generated = True
370             break
371     if not player_will_be_generated:
372         print("Ignoring beyond SEED_MAP: " +
373               "No player type with start number >0 defined.")
374         return
375     wait_action = False
376     for ThingAction in world_db["ThingActions"]:
377         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
378             wait_action = True
379     if not wait_action:
380         print("Ignoring beyond SEED_MAP: " +
381               "No thing action with name 'wait' defined.")
382         return
383     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
384     world_db["Things"] = {}
385     remake_map()
386     world_db["WORLD_ACTIVE"] = 1
387     world_db["TURN"] = 1
388     # TODO: Generate things (player first, with updated memory)
389     atomic_write(io_db["path_out"], "NEW_WORLD\n", do_append=True)
390
391
392 def command_maplength(maplength_string):
393     # DUMMY.
394     set_world_inactive()
395     # TODO: remove map (is this necessary? no memory management trouble …)
396     world_db["Things"] = {}
397     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
398
399
400 def command_worldactive(worldactive_string):
401     # DUMMY.
402     val = integer_test(worldactive_string, 0, 1)
403     if val:
404         if 0 != world_db["WORLD_ACTIVE"]:
405             if 0 == val:
406                 set_world_inactive()
407             else:
408                 print("World already active.")
409         elif 0 == world_db["WORLD_ACTIVE"]:
410             wait_exists = False
411             for ThingAction in world_db["ThingActions"]:
412                 if "wait" == ThingAction["TA_NAME"]:
413                     wait_exists = True
414                     break
415             player_exists = False
416             for Thing in world_db["Things"]:
417                 if 0 == ThingAction["T_ID"]:
418                     player_exists = True
419                     break
420             map_exists = "MAP" in world_db
421             if wait_exists and player_exists and map_exists:
422                 # TODO: rebuild all things' FOVs, map memories
423                 world_db["WORLD_ACTIVE"] = 1
424
425
426 def id_setter(id_string, category, id_store, start_at_1=False):
427     """Set ID of object of category to manipulate ID unused? Create new one.
428
429     The ID is stored as id_store.id. If the integer of the input is valid (if
430     start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
431     start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
432     >= 1, and <= 255. None is always returned when no new object is created,
433     otherwise the new object's ID.
434     """
435     min = 0 if start_at_1 else -32768
436     max = 255 if start_at_1 else 32767
437     id = integer_test(id_string, min, max)
438     if None != id:
439         if id in world_db[category]:
440             id_store.id = id
441             return None
442         else:
443             if (start_at_1 and 0 == id) \
444                or ((not start_at_1) and (id < 0 or id > 255)):
445                 id = -1
446                 while 1:
447                     id = id + 1
448                     if id not in world_db[category]:
449                         break
450                 if id > 255:
451                     print("Ignoring: "
452                           "No unused ID available to add to ID list.")
453                     return None
454             id_store.id = id
455     return id
456
457
458 def test_for_id_maker(object, category):
459     """Return decorator testing for object having "id" attribute."""
460     def decorator(f):
461         def helper(*args):
462             if hasattr(object, "id"):
463                 f(*args)
464             else:
465                 print("Ignoring: No " + category +
466                       " defined to manipulate yet.")
467         return helper
468     return decorator
469
470
471 def command_tid(id_string):
472     """Set ID of Thing to manipulate. ID unused? Create new one.
473
474     Default new Thing's type to the first available ThingType, others: zero.
475     """
476     id = id_setter(id_string, "Things", command_tid)
477     if None != id:
478         if world_db["ThingTypes"] == {}:
479             print("Ignoring: No ThingType to settle new Thing in.")
480             return
481         world_db["Things"][id] = {
482             "T_LIFEPOINTS": 0,
483             "T_ARGUMENT": 0,
484             "T_PROGRESS": 0,
485             "T_SATIATION": 0,
486             "T_COMMAND": 0,
487             "T_TYPE": list(world_db["ThingTypes"].keys())[0],
488             "T_POSY": 0,
489             "T_POSX": 0,
490             "T_CARRIES": [],
491             "carried": False,
492             "T_MEMTHING": [],
493             "T_MEMMAP": False,
494             "T_MEMDEPTHMAP": False
495         }
496
497
498 test_Thing_id = test_for_id_maker(command_tid, "Thing")
499
500
501 @test_Thing_id
502 def command_tcommand(str_int):
503     """Set T_COMMAND of selected Thing."""
504     val = integer_test(str_int, 0, 255)
505     if None != val:
506         if 0 == val or val in world_db["ThingActions"]:
507             world_db["Things"][command_tid.id]["T_COMMAND"] = val
508         else:
509             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
510
511
512 @test_Thing_id
513 def command_ttype(str_int):
514     """Set T_TYPE of selected Thing."""
515     val = integer_test(str_int, 0, 255)
516     if None != val:
517         if val in world_db["ThingTypes"]:
518             world_db["Things"][command_tid.id]["T_TYPE"] = val
519         else:
520             print("Ignoring: ThingType ID belongs to no known ThingType.")
521
522
523 @test_Thing_id
524 def command_tcarries(str_int):
525     """Append int(str_int) to T_CARRIES of selected Thing.
526
527     The ID int(str_int) must not be of the selected Thing, and must belong to a
528     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
529     """
530     val = integer_test(str_int, 0, 255)
531     if None != val:
532         if val == command_tid.id:
533             print("Ignoring: Thing cannot carry itself.")
534         elif val in world_db["Things"] \
535              and not world_db["Things"][val]["carried"]:
536             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
537             world_db["Things"][val]["carried"] = True
538         else:
539             print("Ignoring: Thing not available for carrying.")
540     # Note that the whole carrying structure is different from the C version:
541     # Carried-ness is marked by a "carried" flag, not by Things containing
542     # Things internally.
543
544
545 @test_Thing_id
546 def command_tmemthing(str_t, str_y, str_x):
547     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
548
549     The type must fit to an existing ThingType, and the position into the map.
550     """
551     type = integer_test(str_t, 0, 255)
552     posy = integer_test(str_y, 0, 255)
553     posx = integer_test(str_x, 0, 255)
554     if None != type and None != posy and None != posx:
555         if type not in world_db["ThingTypes"] \
556            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
557             print("Ignoring: Illegal value for thing type or position.")
558         else:
559             memthing = (type, posy, posx)
560             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
561
562
563 def setter_map(maptype):
564     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
565
566     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
567     """
568     @test_Thing_id
569     def helper(str_int, mapline):
570         val = integer_test(str_int, 0, 255)
571         if None != val:
572             if val >= world_db["MAP_LENGTH"]:
573                 print("Illegal value for map line number.")
574             elif len(mapline) != world_db["MAP_LENGTH"]:
575                 print("Map line length is unequal map width.")
576             else:
577                 length = world_db["MAP_LENGTH"]
578                 rmap = None
579                 if not world_db["Things"][command_tid.id][maptype]:
580                     rmap = bytearray(b' ' * (length ** 2))
581                 else:
582                     rmap = world_db["Things"][command_tid.id][maptype]
583                 rmap[val * length:(val * length) + length] = mapline.encode()
584                 world_db["Things"][command_tid.id][maptype] = rmap
585     return helper
586
587
588 def setter_tpos(axis):
589     """Generate setter for T_POSX or  T_POSY of selected Thing."""
590     @test_Thing_id
591     def helper(str_int):
592         val = integer_test(str_int, 0, 255)
593         if None != val:
594             if val < world_db["MAP_LENGTH"]:
595                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
596                 # TODO: Delete Thing's FOV, and rebuild it if world is active.
597             else:
598                 print("Ignoring: Position is outside of map.")
599     return helper
600
601
602 def command_ttid(id_string):
603     """Set ID of ThingType to manipulate. ID unused? Create new one.
604
605     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
606     """
607     id = id_setter(id_string, "ThingTypes", command_ttid)
608     if None != id:
609         world_db["ThingTypes"][id] = {
610             "TT_NAME": "(none)",
611             "TT_CONSUMABLE": 0,
612             "TT_LIFEPOINTS": 0,
613             "TT_PROLIFERATE": 0,
614             "TT_START_NUMBER": 0,
615             "TT_SYMBOL": "?",
616             "TT_CORPSE_ID": id
617         }
618
619
620 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
621
622
623 @test_ThingType_id
624 def command_ttname(name):
625     """Set TT_NAME of selected ThingType."""
626     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
627
628
629 @test_ThingType_id
630 def command_ttsymbol(char):
631     """Set TT_SYMBOL of selected ThingType. """
632     if 1 == len(char):
633         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
634     else:
635         print("Ignoring: Argument must be single character.")
636
637
638 @test_ThingType_id
639 def command_ttcorpseid(str_int):
640     """Set TT_CORPSE_ID of selected ThingType."""
641     val = integer_test(str_int, 0, 255)
642     if None != val:
643         if val in world_db["ThingTypes"]:
644             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
645         else:
646             print("Ignoring: Corpse ID belongs to no known ThignType.")
647
648
649 def command_taid(id_string):
650     """Set ID of ThingAction to manipulate. ID unused? Create new one.
651
652     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
653     """
654     id = id_setter(id_string, "ThingActions", command_taid, True)
655     if None != id:
656         world_db["ThingActions"][id] = {
657             "TA_EFFORT": 1,
658             "TA_NAME": "wait"
659         }
660
661
662 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
663
664
665 @test_ThingAction_id
666 def command_taname(name):
667     """Set TA_NAME of selected ThingAction.
668
669     The name must match a valid thing action function. If after the name
670     setting no ThingAction with name "wait" remains, call set_world_inactive().
671     """
672     if name == "wait" or name == "move" or name == "use" or name == "drop" \
673        or name == "pick_up":
674         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
675         if 1 == world_db["WORLD_ACTIVE"]:
676             wait_defined = False
677             for id in world_db["ThingActions"]:
678                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
679                     wait_defined = True
680                     break
681             if not wait_defined:
682                 set_world_inactive()
683     else:
684         print("Ignoring: Invalid action name.")
685     # In contrast to the original,naming won't map a function to a ThingAction.
686
687
688 """Commands database.
689
690 Map command start tokens to ([0]) number of expected command arguments, ([1])
691 the command's meta-ness (i.e. is it to be written to the record file, is it to
692 be ignored in replay mode if read from server input file), and ([2]) a function
693 to be called on it.
694 """
695 commands_db = {
696     "QUIT": (0, True, command_quit),
697     "PING": (0, True, command_ping),
698     "MAKE_WORLD": (1, False, command_makeworld),
699     "SEED_MAP": (1, False, command_seedmap),
700     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
701                                          0, 4294967295)),
702     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
703     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
704     "MAP_LENGTH": (1, False, command_maplength),
705     "WORLD_ACTIVE": (1, False, command_worldactive),
706     "TA_ID": (1, False, command_taid),
707     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
708     "TA_NAME": (1, False, command_taname),
709     "TT_ID": (1, False, command_ttid),
710     "TT_NAME": (1, False, command_ttname),
711     "TT_SYMBOL": (1, False, command_ttsymbol),
712     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
713     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
714                                        0, 65535)),
715     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
716                                          0, 255)),
717     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
718                                         0, 255)),
719     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
720     "T_ID": (1, False, command_tid),
721     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
722     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
723     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
724     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
725     "T_COMMAND": (1, False, command_tcommand),
726     "T_TYPE": (1, False, command_ttype),
727     "T_CARRIES": (1, False, command_tcarries),
728     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
729     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
730     "T_MEMTHING": (3, False, command_tmemthing),
731     "T_POSY": (1, False, setter_tpos("Y")),
732     "T_POSX": (1, False, setter_tpos("X")),
733 }
734
735
736 """World state database. With sane default values."""
737 world_db = {
738     "TURN": 1,
739     "SEED_MAP": 0,
740     "SEED_RANDOMNESS": 0,
741     "PLAYER_TYPE": 0,
742     "MAP_LENGTH": 64,
743     "WORLD_ACTIVE": 0,
744     "ThingActions": {},
745     "ThingTypes": {},
746     "Things": {}
747 }
748
749
750 """File IO database."""
751 io_db = {
752     "path_save": "save",
753     "path_record": "record",
754     "path_worldconf": "confserver/world",
755     "path_server": "server/",
756     "path_in": "server/in",
757     "path_out": "server/out",
758     "path_worldstate": "server/worldstate",
759     "tmp_suffix": "_tmp",
760     "kicked_by_rival": False
761 }
762
763
764 try:
765     opts = parse_command_line_arguments()
766     setup_server_io()
767     # print("DUMMY: Run game.")
768     if None != opts.replay:
769         replay_game()
770     else:
771         play_game()
772 except SystemExit as exit:
773     print("ABORTING: " + exit.args[0])
774 except:
775     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
776     raise
777 finally:
778     cleanup_server_io()
779     # print("DUMMY: (Clean up C heap.)")