home · contact · privacy
Server/py: Fix out file writing bug.
[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         io_db["file_out"].write("WORLD_UPDATED\n")
274         io_db["file_out"].flush()
275         os.fsync(io_db["file_out"])
276         io_db["worldstate_updateable"] = False
277
278
279 def replay_game():
280     """Replay game from record file.
281
282     Use opts.replay as breakpoint turn to which to replay automatically before
283     switching to manual input by non-meta commands in server input file
284     triggering further reads of record file. Ensure opts.replay is at least 1.
285     Run try_worldstate_update() before each interactive obey()/read_command().
286     """
287     if opts.replay < 1:
288         opts.replay = 1
289     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
290           " (if so late a turn is to be found).")
291     if not os.access(io_db["path_record"], os.F_OK):
292         raise SystemExit("No record file found to replay.")
293     io_db["file_record"] = open(io_db["path_record"], "r")
294     io_db["file_record"].prefix = "record file line "
295     io_db["file_record"].line_n = 1
296     while world_db["TURN"] < opts.replay:
297         line = io_db["file_record"].readline()
298         if "" == line:
299             break
300         obey(line.rstrip(), io_db["file_record"].prefix
301              + str(io_db["file_record"].line_n))
302         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
303     while True:
304         try_worldstate_update()
305         obey(read_command(), "in file", replay=True)
306
307
308 def play_game():
309     """Play game by server input file commands. Before, load save file found.
310
311     If no save file is found, a new world is generated from the commands in the
312     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
313     command and all that follow via the server input file. Run
314     try_worldstate_update() before each interactive obey()/read_command().
315     """
316     if os.access(io_db["path_save"], os.F_OK):
317         obey_lines_in_file(io_db["path_save"], "save")
318     else:
319         if not os.access(io_db["path_worldconf"], os.F_OK):
320             msg = "No world config file from which to start a new world."
321             raise SystemExit(msg)
322         obey_lines_in_file(io_db["path_worldconf"], "world config ",
323                            do_record=True)
324         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
325     while True:
326         try_worldstate_update()
327         obey(read_command(), "in file", do_record=True)
328
329
330 def remake_map():
331     # DUMMY map creator.
332     world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
333
334
335 def set_world_inactive():
336     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
337     server_test()
338     if os.access(io_db["path_worldstate"], os.F_OK):
339         os.remove(io_db["path_worldstate"])
340     world_db["WORLD_ACTIVE"] = 0
341
342
343 def integer_test(val_string, min, max):
344     """Return val_string if possible integer >= min and <= max, else None."""
345     try:
346         val = int(val_string)
347         if val < min or val > max:
348             raise ValueError
349         return val
350     except ValueError:
351         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
352               str(max) + ".")
353         return None
354
355
356 def setter(category, key, min, max):
357     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
358     if category is None:
359         def f(val_string):
360             val = integer_test(val_string, min, max)
361             if None != val:
362                 world_db[key] = val
363     else:
364         if category == "Thing":
365             id_store = command_tid
366             decorator = test_Thing_id
367         elif category == "ThingType":
368             id_store = command_ttid
369             decorator = test_ThingType_id
370         elif category == "ThingAction":
371             id_store = command_taid
372             decorator = test_ThingAction_id
373
374         @decorator
375         def f(val_string):
376             val = integer_test(val_string, min, max)
377             if None != val:
378                 world_db[category + "s"][id_store.id][key] = val
379     return f
380
381
382 def id_setter(id, category, id_store=False, start_at_1=False):
383     """Set ID of object of category to manipulate ID unused? Create new one.
384
385     The ID is stored as id_store.id (if id_store is set). If the integer of the
386     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
387     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
388     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
389     new object is created, otherwise the new object's ID.
390     """
391     min = 0 if start_at_1 else -32768
392     max = 255 if start_at_1 else 32767
393     if str == type(id):
394         id = integer_test(id, min, max)
395     if None != id:
396         if id in world_db[category]:
397             if id_store:
398                 id_store.id = id
399             return None
400         else:
401             if (start_at_1 and 0 == id) \
402                or ((not start_at_1) and (id < 0 or id > 255)):
403                 id = -1
404                 while 1:
405                     id = id + 1
406                     if id not in world_db[category]:
407                         break
408                 if id > 255:
409                     print("Ignoring: "
410                           "No unused ID available to add to ID list.")
411                     return None
412             if id_store:
413                 id_store.id = id
414     return id
415
416
417 def command_ping():
418     """Send PONG line to server output file."""
419     io_db["file_out"].write("PONG\n")
420     io_db["file_out"].flush()
421     os.fsync(io_db["file_out"].fileno())
422
423
424 def command_quit():
425     """Abort server process."""
426     raise SystemExit("received QUIT command")
427
428
429 def command_thingshere(y, x):
430     # DUMMY
431     print("Ignoring not-yet implemented THINGS_HERE command.")
432
433
434 def command_seedmap(seed_string):
435     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
436     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
437     remake_map()
438
439
440 def command_makeworld(seed_string):
441     # DUMMY.
442     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
443     player_will_be_generated = False
444     playertype = world_db["PLAYER_TYPE"]
445     for ThingType in world_db["ThingTypes"]:
446         if playertype == ThingType:
447             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
448                 player_will_be_generated = True
449             break
450     if not player_will_be_generated:
451         print("Ignoring beyond SEED_MAP: " +
452               "No player type with start number >0 defined.")
453         return
454     wait_action = False
455     for ThingAction in world_db["ThingActions"]:
456         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
457             wait_action = True
458     if not wait_action:
459         print("Ignoring beyond SEED_MAP: " +
460               "No thing action with name 'wait' defined.")
461         return
462     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
463     world_db["Things"] = {}
464     remake_map()
465     world_db["WORLD_ACTIVE"] = 1
466     world_db["TURN"] = 1
467     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
468         world_db["Things"][id_setter(-1, "Things")] = {
469             "T_LIFEPOINTS": world_db["ThingTypes"][playertype]["TT_LIFEPOINTS"],
470             "T_TYPE": playertype,
471             "T_POSY": 0, # randomize safely
472             "T_POSX": 0, # randomize safely
473             "T_ARGUMENT": 0,
474             "T_PROGRESS": 0,
475             "T_SATIATION": 0,
476             "T_COMMAND": 0,
477             "T_CARRIES": [],
478             "carried": False,
479             "T_MEMTHING": [],
480             "T_MEMMAP": False,
481             "T_MEMDEPTHMAP": False
482         }
483     # generate fov map?
484     # TODO: Generate things (player first, with updated memory)
485     atomic_write(io_db["path_out"], "NEW_WORLD\n", do_append=True)
486
487
488 def command_maplength(maplength_string):
489     # DUMMY.
490     set_world_inactive()
491     # TODO: remove map (is this necessary? no memory management trouble …)
492     world_db["Things"] = {}
493     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
494
495
496 def command_worldactive(worldactive_string):
497     # DUMMY.
498     val = integer_test(worldactive_string, 0, 1)
499     if val:
500         if 0 != world_db["WORLD_ACTIVE"]:
501             if 0 == val:
502                 set_world_inactive()
503             else:
504                 print("World already active.")
505         elif 0 == world_db["WORLD_ACTIVE"]:
506             wait_exists = False
507             for ThingAction in world_db["ThingActions"]:
508                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
509                     wait_exists = True
510                     break
511             player_exists = False
512             for Thing in world_db["Things"]:
513                 if 0 == Thing:
514                     player_exists = True
515                     break
516             map_exists = "MAP" in world_db
517             if wait_exists and player_exists and map_exists:
518                 # TODO: rebuild all things' FOVs, map memories
519                 world_db["WORLD_ACTIVE"] = 1
520
521
522 def test_for_id_maker(object, category):
523     """Return decorator testing for object having "id" attribute."""
524     def decorator(f):
525         def helper(*args):
526             if hasattr(object, "id"):
527                 f(*args)
528             else:
529                 print("Ignoring: No " + category +
530                       " defined to manipulate yet.")
531         return helper
532     return decorator
533
534
535 def command_tid(id_string):
536     """Set ID of Thing to manipulate. ID unused? Create new one.
537
538     Default new Thing's type to the first available ThingType, others: zero.
539     """
540     id = id_setter(id_string, "Things", command_tid)
541     if None != id:
542         if world_db["ThingTypes"] == {}:
543             print("Ignoring: No ThingType to settle new Thing in.")
544             return
545         world_db["Things"][id] = {
546             "T_LIFEPOINTS": 0,
547             "T_ARGUMENT": 0,
548             "T_PROGRESS": 0,
549             "T_SATIATION": 0,
550             "T_COMMAND": 0,
551             "T_TYPE": list(world_db["ThingTypes"].keys())[0],
552             "T_POSY": 0,
553             "T_POSX": 0,
554             "T_CARRIES": [],
555             "carried": False,
556             "T_MEMTHING": [],
557             "T_MEMMAP": False,
558             "T_MEMDEPTHMAP": False
559         }
560
561
562 test_Thing_id = test_for_id_maker(command_tid, "Thing")
563
564
565 @test_Thing_id
566 def command_tcommand(str_int):
567     """Set T_COMMAND of selected Thing."""
568     val = integer_test(str_int, 0, 255)
569     if None != val:
570         if 0 == val or val in world_db["ThingActions"]:
571             world_db["Things"][command_tid.id]["T_COMMAND"] = val
572         else:
573             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
574
575
576 @test_Thing_id
577 def command_ttype(str_int):
578     """Set T_TYPE of selected Thing."""
579     val = integer_test(str_int, 0, 255)
580     if None != val:
581         if val in world_db["ThingTypes"]:
582             world_db["Things"][command_tid.id]["T_TYPE"] = val
583         else:
584             print("Ignoring: ThingType ID belongs to no known ThingType.")
585
586
587 @test_Thing_id
588 def command_tcarries(str_int):
589     """Append int(str_int) to T_CARRIES of selected Thing.
590
591     The ID int(str_int) must not be of the selected Thing, and must belong to a
592     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
593     """
594     val = integer_test(str_int, 0, 255)
595     if None != val:
596         if val == command_tid.id:
597             print("Ignoring: Thing cannot carry itself.")
598         elif val in world_db["Things"] \
599              and not world_db["Things"][val]["carried"]:
600             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
601             world_db["Things"][val]["carried"] = True
602         else:
603             print("Ignoring: Thing not available for carrying.")
604     # Note that the whole carrying structure is different from the C version:
605     # Carried-ness is marked by a "carried" flag, not by Things containing
606     # Things internally.
607
608
609 @test_Thing_id
610 def command_tmemthing(str_t, str_y, str_x):
611     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
612
613     The type must fit to an existing ThingType, and the position into the map.
614     """
615     type = integer_test(str_t, 0, 255)
616     posy = integer_test(str_y, 0, 255)
617     posx = integer_test(str_x, 0, 255)
618     if None != type and None != posy and None != posx:
619         if type not in world_db["ThingTypes"] \
620            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
621             print("Ignoring: Illegal value for thing type or position.")
622         else:
623             memthing = (type, posy, posx)
624             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
625
626
627 def setter_map(maptype):
628     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
629
630     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
631     """
632     @test_Thing_id
633     def helper(str_int, mapline):
634         val = integer_test(str_int, 0, 255)
635         if None != val:
636             if val >= world_db["MAP_LENGTH"]:
637                 print("Illegal value for map line number.")
638             elif len(mapline) != world_db["MAP_LENGTH"]:
639                 print("Map line length is unequal map width.")
640             else:
641                 length = world_db["MAP_LENGTH"]
642                 rmap = None
643                 if not world_db["Things"][command_tid.id][maptype]:
644                     rmap = bytearray(b' ' * (length ** 2))
645                 else:
646                     rmap = world_db["Things"][command_tid.id][maptype]
647                 rmap[val * length:(val * length) + length] = mapline.encode()
648                 world_db["Things"][command_tid.id][maptype] = rmap
649     return helper
650
651
652 def setter_tpos(axis):
653     """Generate setter for T_POSX or  T_POSY of selected Thing."""
654     @test_Thing_id
655     def helper(str_int):
656         val = integer_test(str_int, 0, 255)
657         if None != val:
658             if val < world_db["MAP_LENGTH"]:
659                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
660                 # TODO: Delete Thing's FOV, and rebuild it if world is active.
661             else:
662                 print("Ignoring: Position is outside of map.")
663     return helper
664
665
666 def command_ttid(id_string):
667     """Set ID of ThingType to manipulate. ID unused? Create new one.
668
669     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
670     """
671     id = id_setter(id_string, "ThingTypes", command_ttid)
672     if None != id:
673         world_db["ThingTypes"][id] = {
674             "TT_NAME": "(none)",
675             "TT_CONSUMABLE": 0,
676             "TT_LIFEPOINTS": 0,
677             "TT_PROLIFERATE": 0,
678             "TT_START_NUMBER": 0,
679             "TT_SYMBOL": "?",
680             "TT_CORPSE_ID": id
681         }
682
683
684 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
685
686
687 @test_ThingType_id
688 def command_ttname(name):
689     """Set TT_NAME of selected ThingType."""
690     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
691
692
693 @test_ThingType_id
694 def command_ttsymbol(char):
695     """Set TT_SYMBOL of selected ThingType. """
696     if 1 == len(char):
697         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
698     else:
699         print("Ignoring: Argument must be single character.")
700
701
702 @test_ThingType_id
703 def command_ttcorpseid(str_int):
704     """Set TT_CORPSE_ID of selected ThingType."""
705     val = integer_test(str_int, 0, 255)
706     if None != val:
707         if val in world_db["ThingTypes"]:
708             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
709         else:
710             print("Ignoring: Corpse ID belongs to no known ThignType.")
711
712
713 def command_taid(id_string):
714     """Set ID of ThingAction to manipulate. ID unused? Create new one.
715
716     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
717     """
718     id = id_setter(id_string, "ThingActions", command_taid, True)
719     if None != id:
720         world_db["ThingActions"][id] = {
721             "TA_EFFORT": 1,
722             "TA_NAME": "wait"
723         }
724
725
726 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
727
728
729 @test_ThingAction_id
730 def command_taname(name):
731     """Set TA_NAME of selected ThingAction.
732
733     The name must match a valid thing action function. If after the name
734     setting no ThingAction with name "wait" remains, call set_world_inactive().
735     """
736     if name == "wait" or name == "move" or name == "use" or name == "drop" \
737        or name == "pick_up":
738         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
739         if 1 == world_db["WORLD_ACTIVE"]:
740             wait_defined = False
741             for id in world_db["ThingActions"]:
742                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
743                     wait_defined = True
744                     break
745             if not wait_defined:
746                 set_world_inactive()
747     else:
748         print("Ignoring: Invalid action name.")
749     # In contrast to the original,naming won't map a function to a ThingAction.
750
751
752 """Commands database.
753
754 Map command start tokens to ([0]) number of expected command arguments, ([1])
755 the command's meta-ness (i.e. is it to be written to the record file, is it to
756 be ignored in replay mode if read from server input file), and ([2]) a function
757 to be called on it.
758 """
759 commands_db = {
760     "QUIT": (0, True, command_quit),
761     "PING": (0, True, command_ping),
762     "THINGS_HERE": (2, True, command_thingshere),
763     "MAKE_WORLD": (1, False, command_makeworld),
764     "SEED_MAP": (1, False, command_seedmap),
765     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
766                                          0, 4294967295)),
767     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
768     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
769     "MAP_LENGTH": (1, False, command_maplength),
770     "WORLD_ACTIVE": (1, False, command_worldactive),
771     "TA_ID": (1, False, command_taid),
772     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
773     "TA_NAME": (1, False, command_taname),
774     "TT_ID": (1, False, command_ttid),
775     "TT_NAME": (1, False, command_ttname),
776     "TT_SYMBOL": (1, False, command_ttsymbol),
777     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
778     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
779                                        0, 65535)),
780     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
781                                          0, 255)),
782     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
783                                         0, 255)),
784     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
785     "T_ID": (1, False, command_tid),
786     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
787     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
788     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
789     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
790     "T_COMMAND": (1, False, command_tcommand),
791     "T_TYPE": (1, False, command_ttype),
792     "T_CARRIES": (1, False, command_tcarries),
793     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
794     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
795     "T_MEMTHING": (3, False, command_tmemthing),
796     "T_POSY": (1, False, setter_tpos("Y")),
797     "T_POSX": (1, False, setter_tpos("X")),
798 }
799
800
801 """World state database. With sane default values."""
802 world_db = {
803     "TURN": 1,
804     "SEED_MAP": 0,
805     "SEED_RANDOMNESS": 0,
806     "PLAYER_TYPE": 0,
807     "MAP_LENGTH": 64,
808     "WORLD_ACTIVE": 0,
809     "ThingActions": {},
810     "ThingTypes": {},
811     "Things": {}
812 }
813
814
815 """File IO database."""
816 io_db = {
817     "path_save": "save",
818     "path_record": "record",
819     "path_worldconf": "confserver/world",
820     "path_server": "server/",
821     "path_in": "server/in",
822     "path_out": "server/out",
823     "path_worldstate": "server/worldstate",
824     "tmp_suffix": "_tmp",
825     "kicked_by_rival": False,
826     "worldstate_updateable": False
827 }
828
829
830 try:
831     opts = parse_command_line_arguments()
832     setup_server_io()
833     # print("DUMMY: Run game.")
834     if None != opts.replay:
835         replay_game()
836     else:
837         play_game()
838 except SystemExit as exit:
839     print("ABORTING: " + exit.args[0])
840 except:
841     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
842     raise
843 finally:
844     cleanup_server_io()
845     # print("DUMMY: (Clean up C heap.)")