home · contact · privacy
7c67024f39d6e7e4b17aa00912feb839c7597e9f
[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     # TODO: How to handle strings that contain ' or "?
123
124     def mapsetter(key):
125         def helper(id):
126             string = ""
127             if world_db["Things"][id][key]:
128                 memmap = world_db["Things"][id][key]
129                 length = world_db["MAP_LENGTH"]
130                 for i in range(world_db["MAP_LENGTH"]):
131                     string = string + key + " " + str(i) + " '" + \
132                              memmap[i * length:(i * length) + length].decode() \
133                              + "'\n"
134             return string
135         return helper
136
137     def memthing(id):
138         string = ""
139         for memthing in world_db["Things"][id]["T_MEMTHING"]:
140             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
141                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
142         return string
143
144     def helper(category, id_string, special_keys={}):
145         string = ""
146         for id in world_db[category]:
147             string = string + id_string + " " + str(id) + "\n"
148             for key in world_db[category][id]:
149                 if not key in special_keys:
150                     x = world_db[category][id][key]
151                     argument = "'" + x + "'" if str == type(x) else str(x)
152                     string = string + key + " " + argument + "\n"
153                 elif special_keys[key]:
154                     string = string + special_keys[key](id)
155         return string
156
157     string = ""
158     for key in world_db:
159         if dict != type(world_db[key]):
160             string = string + key + " " + str(world_db[key]) + "\n"
161     string = string + helper("ThingActions", "TA_ID")
162     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
163     for id in world_db["ThingTypes"]:
164         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
165                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
166     string = string + helper("Things", "T_ID",
167                              {"T_CARRIES": False, "carried": False,
168                               "T_MEMMAP": mapsetter("T_MEMMAP"),
169                               "T_MEMTHING": memthing,
170                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
171     for id in world_db["Things"]:
172         if [] != world_db["Things"][id]["T_CARRIES"]:
173             string = string + "T_ID " + str(id) + "\n"
174             for carried_id in world_db["Things"][id]["T_CARRIES"]:
175                 string = string + "T_CARRIES " + str(carried_id) + "\n"
176     atomic_write(io_db["path_save"], string)
177
178
179 def obey_lines_in_file(path, name, do_record=False):
180     """Call obey() on each line of path's file, use name in input prefix."""
181     file = open(path, "r")
182     line_n = 1
183     for line in file.readlines():
184         obey(line.rstrip(), name + "file line " + str(line_n),
185              do_record=do_record)
186         line_n = line_n + 1
187     file.close()
188
189
190 def parse_command_line_arguments():
191     """Return settings values read from command line arguments."""
192     parser = argparse.ArgumentParser()
193     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
194                         action='store')
195     opts, unknown = parser.parse_known_args()
196     return opts
197
198
199 def server_test():
200     """Ensure valid server out file belonging to current process.
201
202     This is done by comparing io_db["teststring"] to what's found at the start
203     of the current file at io_db["path_out"]. On failure, set
204     io_db["kicked_by_rival"] and raise SystemExit.
205     """
206     if not os.access(io_db["path_out"], os.F_OK):
207         raise SystemExit("Server output file has disappeared.")
208     file = open(io_db["path_out"], "r")
209     test = file.readline().rstrip("\n")
210     file.close()
211     if test != io_db["teststring"]:
212         io_db["kicked_by_rival"] = True
213         msg = "Server test string in server output file does not match. This" \
214               " indicates that the current server process has been " \
215               "superseded by another one."
216         raise SystemExit(msg)
217
218
219 def read_command():
220     """Return next newline-delimited command from server in file.
221
222     Keep building return string until a newline is encountered. Pause between
223     unsuccessful reads, and after too much waiting, run server_test().
224     """
225     wait_on_fail = 1
226     max_wait = 5
227     now = time.time()
228     command = ""
229     while True:
230         add = io_db["file_in"].readline()
231         if len(add) > 0:
232             command = command + add
233             if len(command) > 0 and "\n" == command[-1]:
234                 command = command[:-1]
235                 break
236         else:
237             time.sleep(wait_on_fail)
238             if now + max_wait < time.time():
239                 server_test()
240                 now = time.time()
241     return command
242
243
244 def replay_game():
245     """Replay game from record file.
246
247     Use opts.replay as breakpoint turn to which to replay automatically before
248     switching to manual input by non-meta commands in server input file
249     triggering further reads of record file. Ensure opts.replay is at least 1.
250     """
251     if opts.replay < 1:
252         opts.replay = 1
253     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
254           " (if so late a turn is to be found).")
255     if not os.access(io_db["path_record"], os.F_OK):
256         raise SystemExit("No record file found to replay.")
257     io_db["file_record"] = open(io_db["path_record"], "r")
258     io_db["file_record"].prefix = "record file line "
259     io_db["file_record"].line_n = 1
260     while world_db["TURN"] < opts.replay:
261         line = io_db["file_record"].readline()
262         if "" == line:
263             break
264         obey(line.rstrip(), io_db["file_record"].prefix
265              + str(io_db["file_record"].line_n))
266         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
267     while True:
268         obey(read_command(), "in file", replay=True)
269
270
271 def play_game():
272     """Play game by server input file commands. Before, load save file found.
273
274     If no save file is found, a new world is generated from the commands in the
275     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
276     command and all that follow via the server input file.
277     """
278     if os.access(io_db["path_save"], os.F_OK):
279         obey_lines_in_file(io_db["path_save"], "save")
280     else:
281         if not os.access(io_db["path_worldconf"], os.F_OK):
282             msg = "No world config file from which to start a new world."
283             raise SystemExit(msg)
284         obey_lines_in_file(io_db["path_worldconf"], "world config ",
285                            do_record=True)
286         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
287     while True:
288         obey(read_command(), "in file", do_record=True)
289
290
291 def remake_map():
292     # DUMMY.
293     print("I'd (re-)make the map now, if only I knew how.")
294
295
296 def set_world_inactive():
297     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
298     server_test()
299     if os.access(io_db["path_worldstate"], os.F_OK):
300         os.remove(io_db["path_worldstate"])
301     world_db["WORLD_ACTIVE"] = 0
302
303
304 def integer_test(val_string, min, max):
305     """Return val_string if possible integer >= min and <= max, else None."""
306     try:
307         val = int(val_string)
308         if val < min or val > max:
309             raise ValueError
310         return val
311     except ValueError:
312         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
313               str(max) + ".")
314         return None
315
316
317 def setter(category, key, min, max):
318     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
319     if category is None:
320         def f(val_string):
321             val = integer_test(val_string, min, max)
322             if None != val:
323                 world_db[key] = val
324     else:
325         if category == "Thing":
326             id_store = command_tid
327             decorator = test_Thing_id
328         elif category == "ThingType":
329             id_store = command_ttid
330             decorator = test_ThingType_id
331         elif category == "ThingAction":
332             id_store = command_taid
333             decorator = test_ThingAction_id
334
335         @decorator
336         def f(val_string):
337             val = integer_test(val_string, min, max)
338             if None != val:
339                 world_db[category + "s"][id_store.id][key] = val
340     return f
341
342
343 def command_ping():
344     """Send PONG line to server output file."""
345     io_db["file_out"].write("PONG\n")
346     io_db["file_out"].flush()
347
348
349 def command_quit():
350     """Abort server process."""
351     raise SystemExit("received QUIT command")
352
353
354 def command_seedmap(seed_string):
355     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
356     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
357     remake_map()
358
359
360 def command_makeworld(seed_string):
361     # DUMMY.
362     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
363     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
364     # TODO: Test for existence of player thing and 'wait' thing action?
365
366
367 def command_maplength(maplength_string):
368     # DUMMY.
369     set_world_inactive()
370     # TODO: remove things, map
371     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
372
373
374 def command_worldactive(worldactive_string):
375     # DUMMY.
376     val = integer_test(worldactive_string, 0, 1)
377     if val:
378         if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
379             set_world_inactive()
380         elif 0 == world_db["WORLD_ACTIVE"]:
381             wait_exists = False
382             player_exists = False
383             map_exists = False
384             # TODO: perform tests:
385             # Is there thing action of name 'wait'?
386             # Is there a player thing?
387             # Is there a map?
388             if wait_exists and player_exists and map_exists:
389                 # TODO: rebuild al things' FOVs, map memories
390                 world_db["WORLD_ACTIVE"] = 1
391
392
393 def id_setter(id_string, category, id_store, start_at_1=False):
394     """Set ID of object of category to manipulate ID unused? Create new one.
395
396     The ID is stored as id_store.id. If the integer of the input is valid (if
397     start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
398     start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
399     >= 1, and <= 255. None is always returned when no new object is created,
400     otherwise the new object's ID.
401     """
402     min = 0 if start_at_1 else -32768
403     max = 255 if start_at_1 else 32767
404     id = integer_test(id_string, min, max)
405     if None != id:
406         if id in world_db[category]:
407             id_store.id = id
408             return None
409         else:
410             if (start_at_1 and 0 == id) \
411                or ((not start_at_1) and (id < 0 or id > 255)):
412                 id = -1
413                 while 1:
414                     id = id + 1
415                     if id not in world_db[category]:
416                         break
417                 if id > 255:
418                     print("Ignoring: "
419                           "No unused ID available to add to ID list.")
420                     return None
421             id_store.id = id
422     return id
423
424
425 def test_for_id_maker(object, category):
426     """Return decorator testing for object having "id" attribute."""
427     def decorator(f):
428         def helper(*args):
429             if hasattr(object, "id"):
430                 f(*args)
431             else:
432                 print("Ignoring: No " + category +
433                       " defined to manipulate yet.")
434         return helper
435     return decorator
436
437
438 def command_tid(id_string):
439     """Set ID of Thing to manipulate. ID unused? Create new one.
440
441     Default new Thing's type to the first available ThingType, others: zero.
442     """
443     id = id_setter(id_string, "Things", command_tid)
444     if None != id:
445         if world_db["ThingTypes"] == {}:
446             print("Ignoring: No ThingType to settle new Thing in.")
447             return
448         world_db["Things"][id] = {
449             "T_LIFEPOINTS": 0,
450             "T_ARGUMENT": 0,
451             "T_PROGRESS": 0,
452             "T_SATIATION": 0,
453             "T_COMMAND": 0,
454             "T_TYPE": list(world_db["ThingTypes"].keys())[0],
455             "T_POSY": 0,
456             "T_POSX": 0,
457             "T_CARRIES": [],
458             "carried": False,
459             "T_MEMTHING": [],
460             "T_MEMMAP": False,
461             "T_MEMDEPTHMAP": False
462         }
463
464
465 test_Thing_id = test_for_id_maker(command_tid, "Thing")
466
467
468 @test_Thing_id
469 def command_tcommand(str_int):
470     """Set T_COMMAND of selected Thing."""
471     val = integer_test(str_int, 0, 255)
472     if None != val:
473         if 0 == val or val in world_db["ThingActions"]:
474             world_db["Things"][command_tid.id]["T_COMMAND"] = val
475         else:
476             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
477
478
479 @test_Thing_id
480 def command_ttype(str_int):
481     """Set T_TYPE of selected Thing."""
482     val = integer_test(str_int, 0, 255)
483     if None != val:
484         if val in world_db["ThingTypes"]:
485             world_db["Things"][command_tid.id]["T_TYPE"] = val
486         else:
487             print("Ignoring: ThingType ID belongs to no known ThingType.")
488
489
490 @test_Thing_id
491 def command_tcarries(str_int):
492     """Append int(str_int) to T_CARRIES of selected Thing.
493
494     The ID int(str_int) must not be of the selected Thing, and must belong to a
495     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
496     """
497     val = integer_test(str_int, 0, 255)
498     if None != val:
499         if val == command_tid.id:
500             print("Ignoring: Thing cannot carry itself.")
501         elif val in world_db["Things"] \
502              and not world_db["Things"][val]["carried"]:
503             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
504             world_db["Things"][val]["carried"] = True
505         else:
506             print("Ignoring: Thing not available for carrying.")
507     # Note that the whole carrying structure is different from the C version:
508     # Carried-ness is marked by a "carried" flag, not by Things containing
509     # Things internally.
510
511
512 @test_Thing_id
513 def command_tmemthing(str_t, str_y, str_x):
514     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
515
516     The type must fit to an existing ThingType, and the position into the map.
517     """
518     type = integer_test(str_t, 0, 255)
519     posy = integer_test(str_y, 0, 255)
520     posx = integer_test(str_x, 0, 255)
521     if None != type and None != posy and None != posx:
522         if type not in world_db["ThingTypes"] \
523            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
524             print("Ignoring: Illegal value for thing type or position.")
525         else:
526             memthing = (type, posy, posx)
527             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
528
529
530 def setter_map(maptype):
531     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
532
533     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
534     """
535     @test_Thing_id
536     def helper(str_int, mapline):
537         val = integer_test(str_int, 0, 255)
538         if None != val:
539             if val >= world_db["MAP_LENGTH"]:
540                 print("Illegal value for map line number.")
541             elif len(mapline) != world_db["MAP_LENGTH"]:
542                 print("Map line length is unequal map width.")
543             else:
544                 length = world_db["MAP_LENGTH"]
545                 rmap = None
546                 if not world_db["Things"][command_tid.id][maptype]:
547                     rmap = bytearray(b' ' * (length ** 2))
548                 else:
549                     rmap = world_db["Things"][command_tid.id][maptype]
550                 rmap[val * length:(val * length) + length] = mapline.encode()
551                 world_db["Things"][command_tid.id][maptype] = rmap
552     return helper
553
554
555 def setter_tpos(axis):
556     """Generate setter for T_POSX or  T_POSY of selected Thing."""
557     @test_Thing_id
558     def helper(str_int):
559         val = integer_test(str_int, 0, 255)
560         if None != val:
561             if val < world_db["MAP_LENGTH"]:
562                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
563                 # TODO: Delete Thing's FOV, and rebuild it if world is active.
564             else:
565                 print("Ignoring: Position is outside of map.")
566     return helper
567
568
569 def command_ttid(id_string):
570     """Set ID of ThingType to manipulate. ID unused? Create new one.
571
572     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
573     """
574     id = id_setter(id_string, "ThingTypes", command_ttid)
575     if None != id:
576         world_db["ThingTypes"][id] = {
577             "TT_NAME": "(none)",
578             "TT_CONSUMABLE": 0,
579             "TT_LIFEPOINTS": 0,
580             "TT_PROLIFERATE": 0,
581             "TT_START_NUMBER": 0,
582             "TT_SYMBOL": "?",
583             "TT_CORPSE_ID": id
584         }
585
586
587 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
588
589
590 @test_ThingType_id
591 def command_ttname(name):
592     """Set TT_NAME of selected ThingType."""
593     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
594
595
596 @test_ThingType_id
597 def command_ttsymbol(char):
598     """Set TT_SYMBOL of selected ThingType. """
599     if 1 == len(char):
600         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
601     else:
602         print("Ignoring: Argument must be single character.")
603
604
605 @test_ThingType_id
606 def command_ttcorpseid(str_int):
607     """Set TT_CORPSE_ID of selected ThingType."""
608     val = integer_test(str_int, 0, 255)
609     if None != val:
610         if val in world_db["ThingTypes"]:
611             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
612         else:
613             print("Ignoring: Corpse ID belongs to no known ThignType.")
614
615
616 def command_taid(id_string):
617     """Set ID of ThingAction to manipulate. ID unused? Create new one.
618
619     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
620     """
621     id = id_setter(id_string, "ThingActions", command_taid, True)
622     if None != id:
623         world_db["ThingActions"][id] = {
624             "TA_EFFORT": 1,
625             "TA_NAME": "wait"
626         }
627
628
629 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
630
631
632 @test_ThingAction_id
633 def command_taname(name):
634     """Set TA_NAME of selected ThingAction.
635
636     The name must match a valid thing action function. If after the name
637     setting no ThingAction with name "wait" remains, call set_world_inactive().
638     """
639     if name == "wait" or name == "move" or name == "use" or name == "drop" \
640        or name == "pick_up":
641         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
642         if 1 == world_db["WORLD_ACTIVE"]:
643             wait_defined = False
644             for id in world_db["ThingActions"]:
645                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
646                     wait_defined = True
647                     break
648             if not wait_defined:
649                 set_world_inactive()
650     else:
651         print("Ignoring: Invalid action name.")
652     # In contrast to the original,naming won't map a function to a ThingAction.
653
654
655 """Commands database.
656
657 Map command start tokens to ([0]) number of expected command arguments, ([1])
658 the command's meta-ness (i.e. is it to be written to the record file, is it to
659 be ignored in replay mode if read from server input file), and ([2]) a function
660 to be called on it.
661 """
662 commands_db = {
663     "QUIT": (0, True, command_quit),
664     "PING": (0, True, command_ping),
665     "MAKE_WORLD": (1, False, command_makeworld),
666     "SEED_MAP": (1, False, command_seedmap),
667     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
668                                          0, 4294967295)),
669     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
670     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
671     "MAP_LENGTH": (1, False, command_maplength),
672     "WORLD_ACTIVE": (1, False, command_worldactive),
673     "TA_ID": (1, False, command_taid),
674     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
675     "TA_NAME": (1, False, command_taname),
676     "TT_ID": (1, False, command_ttid),
677     "TT_NAME": (1, False, command_ttname),
678     "TT_SYMBOL": (1, False, command_ttsymbol),
679     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
680     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
681                                        0, 65535)),
682     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
683                                          0, 255)),
684     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
685                                         0, 255)),
686     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
687     "T_ID": (1, False, command_tid),
688     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
689     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
690     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
691     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
692     "T_COMMAND": (1, False, command_tcommand),
693     "T_TYPE": (1, False, command_ttype),
694     "T_CARRIES": (1, False, command_tcarries),
695     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
696     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
697     "T_MEMTHING": (3, False, command_tmemthing),
698     "T_POSY": (1, False, setter_tpos("Y")),
699     "T_POSX": (1, False, setter_tpos("X")),
700 }
701
702
703 """World state database. With sane default values."""
704 world_db = {
705     "TURN": 1,
706     "SEED_MAP": 0,
707     "SEED_RANDOMNESS": 0,
708     "PLAYER_TYPE": 0,
709     "MAP_LENGTH": 64,
710     "WORLD_ACTIVE": 0,
711     "ThingActions": {},
712     "ThingTypes": {},
713     "Things": {}
714 }
715
716
717 """File IO database."""
718 io_db = {
719     "path_save": "save",
720     "path_record": "record",
721     "path_worldconf": "confserver/world",
722     "path_server": "server/",
723     "path_in": "server/in",
724     "path_out": "server/out",
725     "path_worldstate": "server/worldstate",
726     "tmp_suffix": "_tmp",
727     "kicked_by_rival": False
728 }
729
730
731 try:
732     opts = parse_command_line_arguments()
733     setup_server_io()
734     # print("DUMMY: Run game.")
735     if None != opts.replay:
736         replay_game()
737     else:
738         play_game()
739 except SystemExit as exit:
740     print("ABORTING: " + exit.args[0])
741 except:
742     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
743     raise
744 finally:
745     cleanup_server_io()
746     # print("DUMMY: (Clean up C heap.)")