home · contact · privacy
Server/py: Implement dummy for THINGS_HERE command.
[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](*tokens[1:])
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_thingshere(y, x):
427     # DUMMY
428     print("Ignoring not-yet implemented THINGS_HERE command.")
429
430
431 def command_seedmap(seed_string):
432     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
433     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
434     remake_map()
435
436
437 def command_makeworld(seed_string):
438     # DUMMY.
439     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
440     player_will_be_generated = False
441     playertype = world_db["PLAYER_TYPE"]
442     for ThingType in world_db["ThingTypes"]:
443         if playertype == ThingType:
444             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
445                 player_will_be_generated = True
446             break
447     if not player_will_be_generated:
448         print("Ignoring beyond SEED_MAP: " +
449               "No player type with start number >0 defined.")
450         return
451     wait_action = False
452     for ThingAction in world_db["ThingActions"]:
453         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
454             wait_action = True
455     if not wait_action:
456         print("Ignoring beyond SEED_MAP: " +
457               "No thing action with name 'wait' defined.")
458         return
459     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
460     world_db["Things"] = {}
461     remake_map()
462     world_db["WORLD_ACTIVE"] = 1
463     world_db["TURN"] = 1
464     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
465         world_db["Things"][id_setter(-1, "Things")] = {
466             "T_LIFEPOINTS": world_db["ThingTypes"][playertype]["TT_LIFEPOINTS"],
467             "T_TYPE": playertype,
468             "T_POSY": 0, # randomize safely
469             "T_POSX": 0, # randomize safely
470             "T_ARGUMENT": 0,
471             "T_PROGRESS": 0,
472             "T_SATIATION": 0,
473             "T_COMMAND": 0,
474             "T_CARRIES": [],
475             "carried": False,
476             "T_MEMTHING": [],
477             "T_MEMMAP": False,
478             "T_MEMDEPTHMAP": False
479         }
480     # generate fov map?
481     # TODO: Generate things (player first, with updated memory)
482     atomic_write(io_db["path_out"], "NEW_WORLD\n", do_append=True)
483
484
485 def command_maplength(maplength_string):
486     # DUMMY.
487     set_world_inactive()
488     # TODO: remove map (is this necessary? no memory management trouble …)
489     world_db["Things"] = {}
490     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
491
492
493 def command_worldactive(worldactive_string):
494     # DUMMY.
495     val = integer_test(worldactive_string, 0, 1)
496     if val:
497         if 0 != world_db["WORLD_ACTIVE"]:
498             if 0 == val:
499                 set_world_inactive()
500             else:
501                 print("World already active.")
502         elif 0 == world_db["WORLD_ACTIVE"]:
503             wait_exists = False
504             for ThingAction in world_db["ThingActions"]:
505                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
506                     wait_exists = True
507                     break
508             player_exists = False
509             for Thing in world_db["Things"]:
510                 if 0 == Thing:
511                     player_exists = True
512                     break
513             map_exists = "MAP" in world_db
514             if wait_exists and player_exists and map_exists:
515                 # TODO: rebuild all things' FOVs, map memories
516                 world_db["WORLD_ACTIVE"] = 1
517
518
519 def test_for_id_maker(object, category):
520     """Return decorator testing for object having "id" attribute."""
521     def decorator(f):
522         def helper(*args):
523             if hasattr(object, "id"):
524                 f(*args)
525             else:
526                 print("Ignoring: No " + category +
527                       " defined to manipulate yet.")
528         return helper
529     return decorator
530
531
532 def command_tid(id_string):
533     """Set ID of Thing to manipulate. ID unused? Create new one.
534
535     Default new Thing's type to the first available ThingType, others: zero.
536     """
537     id = id_setter(id_string, "Things", command_tid)
538     if None != id:
539         if world_db["ThingTypes"] == {}:
540             print("Ignoring: No ThingType to settle new Thing in.")
541             return
542         world_db["Things"][id] = {
543             "T_LIFEPOINTS": 0,
544             "T_ARGUMENT": 0,
545             "T_PROGRESS": 0,
546             "T_SATIATION": 0,
547             "T_COMMAND": 0,
548             "T_TYPE": list(world_db["ThingTypes"].keys())[0],
549             "T_POSY": 0,
550             "T_POSX": 0,
551             "T_CARRIES": [],
552             "carried": False,
553             "T_MEMTHING": [],
554             "T_MEMMAP": False,
555             "T_MEMDEPTHMAP": False
556         }
557
558
559 test_Thing_id = test_for_id_maker(command_tid, "Thing")
560
561
562 @test_Thing_id
563 def command_tcommand(str_int):
564     """Set T_COMMAND of selected Thing."""
565     val = integer_test(str_int, 0, 255)
566     if None != val:
567         if 0 == val or val in world_db["ThingActions"]:
568             world_db["Things"][command_tid.id]["T_COMMAND"] = val
569         else:
570             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
571
572
573 @test_Thing_id
574 def command_ttype(str_int):
575     """Set T_TYPE of selected Thing."""
576     val = integer_test(str_int, 0, 255)
577     if None != val:
578         if val in world_db["ThingTypes"]:
579             world_db["Things"][command_tid.id]["T_TYPE"] = val
580         else:
581             print("Ignoring: ThingType ID belongs to no known ThingType.")
582
583
584 @test_Thing_id
585 def command_tcarries(str_int):
586     """Append int(str_int) to T_CARRIES of selected Thing.
587
588     The ID int(str_int) must not be of the selected Thing, and must belong to a
589     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
590     """
591     val = integer_test(str_int, 0, 255)
592     if None != val:
593         if val == command_tid.id:
594             print("Ignoring: Thing cannot carry itself.")
595         elif val in world_db["Things"] \
596              and not world_db["Things"][val]["carried"]:
597             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
598             world_db["Things"][val]["carried"] = True
599         else:
600             print("Ignoring: Thing not available for carrying.")
601     # Note that the whole carrying structure is different from the C version:
602     # Carried-ness is marked by a "carried" flag, not by Things containing
603     # Things internally.
604
605
606 @test_Thing_id
607 def command_tmemthing(str_t, str_y, str_x):
608     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
609
610     The type must fit to an existing ThingType, and the position into the map.
611     """
612     type = integer_test(str_t, 0, 255)
613     posy = integer_test(str_y, 0, 255)
614     posx = integer_test(str_x, 0, 255)
615     if None != type and None != posy and None != posx:
616         if type not in world_db["ThingTypes"] \
617            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
618             print("Ignoring: Illegal value for thing type or position.")
619         else:
620             memthing = (type, posy, posx)
621             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
622
623
624 def setter_map(maptype):
625     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
626
627     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
628     """
629     @test_Thing_id
630     def helper(str_int, mapline):
631         val = integer_test(str_int, 0, 255)
632         if None != val:
633             if val >= world_db["MAP_LENGTH"]:
634                 print("Illegal value for map line number.")
635             elif len(mapline) != world_db["MAP_LENGTH"]:
636                 print("Map line length is unequal map width.")
637             else:
638                 length = world_db["MAP_LENGTH"]
639                 rmap = None
640                 if not world_db["Things"][command_tid.id][maptype]:
641                     rmap = bytearray(b' ' * (length ** 2))
642                 else:
643                     rmap = world_db["Things"][command_tid.id][maptype]
644                 rmap[val * length:(val * length) + length] = mapline.encode()
645                 world_db["Things"][command_tid.id][maptype] = rmap
646     return helper
647
648
649 def setter_tpos(axis):
650     """Generate setter for T_POSX or  T_POSY of selected Thing."""
651     @test_Thing_id
652     def helper(str_int):
653         val = integer_test(str_int, 0, 255)
654         if None != val:
655             if val < world_db["MAP_LENGTH"]:
656                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
657                 # TODO: Delete Thing's FOV, and rebuild it if world is active.
658             else:
659                 print("Ignoring: Position is outside of map.")
660     return helper
661
662
663 def command_ttid(id_string):
664     """Set ID of ThingType to manipulate. ID unused? Create new one.
665
666     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
667     """
668     id = id_setter(id_string, "ThingTypes", command_ttid)
669     if None != id:
670         world_db["ThingTypes"][id] = {
671             "TT_NAME": "(none)",
672             "TT_CONSUMABLE": 0,
673             "TT_LIFEPOINTS": 0,
674             "TT_PROLIFERATE": 0,
675             "TT_START_NUMBER": 0,
676             "TT_SYMBOL": "?",
677             "TT_CORPSE_ID": id
678         }
679
680
681 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
682
683
684 @test_ThingType_id
685 def command_ttname(name):
686     """Set TT_NAME of selected ThingType."""
687     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
688
689
690 @test_ThingType_id
691 def command_ttsymbol(char):
692     """Set TT_SYMBOL of selected ThingType. """
693     if 1 == len(char):
694         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
695     else:
696         print("Ignoring: Argument must be single character.")
697
698
699 @test_ThingType_id
700 def command_ttcorpseid(str_int):
701     """Set TT_CORPSE_ID of selected ThingType."""
702     val = integer_test(str_int, 0, 255)
703     if None != val:
704         if val in world_db["ThingTypes"]:
705             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
706         else:
707             print("Ignoring: Corpse ID belongs to no known ThignType.")
708
709
710 def command_taid(id_string):
711     """Set ID of ThingAction to manipulate. ID unused? Create new one.
712
713     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
714     """
715     id = id_setter(id_string, "ThingActions", command_taid, True)
716     if None != id:
717         world_db["ThingActions"][id] = {
718             "TA_EFFORT": 1,
719             "TA_NAME": "wait"
720         }
721
722
723 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
724
725
726 @test_ThingAction_id
727 def command_taname(name):
728     """Set TA_NAME of selected ThingAction.
729
730     The name must match a valid thing action function. If after the name
731     setting no ThingAction with name "wait" remains, call set_world_inactive().
732     """
733     if name == "wait" or name == "move" or name == "use" or name == "drop" \
734        or name == "pick_up":
735         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
736         if 1 == world_db["WORLD_ACTIVE"]:
737             wait_defined = False
738             for id in world_db["ThingActions"]:
739                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
740                     wait_defined = True
741                     break
742             if not wait_defined:
743                 set_world_inactive()
744     else:
745         print("Ignoring: Invalid action name.")
746     # In contrast to the original,naming won't map a function to a ThingAction.
747
748
749 """Commands database.
750
751 Map command start tokens to ([0]) number of expected command arguments, ([1])
752 the command's meta-ness (i.e. is it to be written to the record file, is it to
753 be ignored in replay mode if read from server input file), and ([2]) a function
754 to be called on it.
755 """
756 commands_db = {
757     "QUIT": (0, True, command_quit),
758     "PING": (0, True, command_ping),
759     "THINGS_HERE": (2, True, command_thingshere),
760     "MAKE_WORLD": (1, False, command_makeworld),
761     "SEED_MAP": (1, False, command_seedmap),
762     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
763                                          0, 4294967295)),
764     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
765     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
766     "MAP_LENGTH": (1, False, command_maplength),
767     "WORLD_ACTIVE": (1, False, command_worldactive),
768     "TA_ID": (1, False, command_taid),
769     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
770     "TA_NAME": (1, False, command_taname),
771     "TT_ID": (1, False, command_ttid),
772     "TT_NAME": (1, False, command_ttname),
773     "TT_SYMBOL": (1, False, command_ttsymbol),
774     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
775     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
776                                        0, 65535)),
777     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
778                                          0, 255)),
779     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
780                                         0, 255)),
781     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
782     "T_ID": (1, False, command_tid),
783     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
784     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
785     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
786     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
787     "T_COMMAND": (1, False, command_tcommand),
788     "T_TYPE": (1, False, command_ttype),
789     "T_CARRIES": (1, False, command_tcarries),
790     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
791     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
792     "T_MEMTHING": (3, False, command_tmemthing),
793     "T_POSY": (1, False, setter_tpos("Y")),
794     "T_POSX": (1, False, setter_tpos("X")),
795 }
796
797
798 """World state database. With sane default values."""
799 world_db = {
800     "TURN": 1,
801     "SEED_MAP": 0,
802     "SEED_RANDOMNESS": 0,
803     "PLAYER_TYPE": 0,
804     "MAP_LENGTH": 64,
805     "WORLD_ACTIVE": 0,
806     "ThingActions": {},
807     "ThingTypes": {},
808     "Things": {}
809 }
810
811
812 """File IO database."""
813 io_db = {
814     "path_save": "save",
815     "path_record": "record",
816     "path_worldconf": "confserver/world",
817     "path_server": "server/",
818     "path_in": "server/in",
819     "path_out": "server/out",
820     "path_worldstate": "server/worldstate",
821     "tmp_suffix": "_tmp",
822     "kicked_by_rival": False,
823     "worldstate_updateable": False
824 }
825
826
827 try:
828     opts = parse_command_line_arguments()
829     setup_server_io()
830     # print("DUMMY: Run game.")
831     if None != opts.replay:
832         replay_game()
833     else:
834         play_game()
835 except SystemExit as exit:
836     print("ABORTING: " + exit.args[0])
837 except:
838     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
839     raise
840 finally:
841     cleanup_server_io()
842     # print("DUMMY: (Clean up C heap.)")