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