10 """Fill IO files DB with proper file( path)s. Write process IO test string.
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"].
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):
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"])
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):
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()
54 def obey(command, prefix, replay=False, do_record=False):
55 """Call function from commands_db mapped to command's first token.
57 The command string is tokenized by shlex.split(comments=True). If replay is
58 set, a non-meta command from the commands_db merely triggers obey() on the
59 next command from the records file. If not, and do do_record is set,
60 non-meta commands are recorded via record(), and save_world() is called.
61 The prefix string is inserted into the server's input message between its
62 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
65 print("input " + prefix + ": " + command)
67 tokens = shlex.split(command, comments=True)
68 except ValueError as err:
69 print("Can't tokenize command string: " + str(err) + ".")
71 if len(tokens) > 0 and tokens[0] in commands_db \
72 and len(tokens) == commands_db[tokens[0]][0] + 1:
73 if commands_db[tokens[0]][1]:
74 commands_db[tokens[0]][2]()
76 print("Due to replay mode, reading command as 'go on in record'.")
77 line = io_db["file_record"].readline()
79 obey(line.rstrip(), io_db["file_record"].prefix
80 + str(io_db["file_record"].line_n))
81 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
83 print("Reached end of record file.")
85 commands_db[tokens[0]][2](*tokens[1:])
89 elif 0 != len(tokens):
90 print("Invalid command/argument, or bad number of tokens.")
93 def atomic_write(path, text, do_append=False):
94 """Atomic write of text to file at path, appended if do_append is set."""
95 path_tmp = path + io_db["tmp_suffix"]
99 if os.access(path, os.F_OK):
100 shutil.copyfile(path, path_tmp)
101 file = open(path_tmp, mode)
104 os.fsync(file.fileno())
106 if os.access(path, os.F_OK):
108 os.rename(path_tmp, path)
112 """Append command string plus newline to record file. (Atomic.)"""
113 # This misses some optimizations from the original record(), namely only
114 # finishing the atomic write with expensive flush() and fsync() every 15
115 # seconds unless explicitely forced. Implement as needed.
116 atomic_write(io_db["path_record"], command + "\n", do_append=True)
120 """Save all commands needed to reconstruct current world state."""
121 # TODO: Misses same optimizations as record() from the original record().
122 # TODO: How to handle strings that contain ' or "?
127 if world_db["Things"][id][key]:
128 memmap = world_db["Things"][id][key]
129 length = world_db["MAP_LENGTH"]
130 for i in range(world_db["MAP_LENGTH"]):
131 string = string + key + " " + str(i) + " '" + \
132 memmap[i * length:(i * length) + length].decode() \
139 for memthing in world_db["Things"][id]["T_MEMTHING"]:
140 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
141 str(memthing[1]) + " " + str(memthing[2]) + "\n"
144 def helper(category, id_string, special_keys={}):
146 for id in world_db[category]:
147 string = string + id_string + " " + str(id) + "\n"
148 for key in world_db[category][id]:
149 if not key in special_keys:
150 x = world_db[category][id][key]
151 argument = "'" + x + "'" if str == type(x) else str(x)
152 string = string + key + " " + argument + "\n"
153 elif special_keys[key]:
154 string = string + special_keys[key](id)
159 if dict != type(world_db[key]):
160 string = string + key + " " + str(world_db[key]) + "\n"
161 string = string + helper("ThingActions", "TA_ID")
162 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
163 for id in world_db["ThingTypes"]:
164 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
165 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
166 string = string + helper("Things", "T_ID",
167 {"T_CARRIES": False, "carried": False,
168 "T_MEMMAP": mapsetter("T_MEMMAP"),
169 "T_MEMTHING": memthing,
170 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
171 for id in world_db["Things"]:
172 if [] != world_db["Things"][id]["T_CARRIES"]:
173 string = string + "T_ID " + str(id) + "\n"
174 for carried_id in world_db["Things"][id]["T_CARRIES"]:
175 string = string + "T_CARRIES " + str(carried_id) + "\n"
176 atomic_write(io_db["path_save"], string)
179 def obey_lines_in_file(path, name, do_record=False):
180 """Call obey() on each line of path's file, use name in input prefix."""
181 file = open(path, "r")
183 for line in file.readlines():
184 obey(line.rstrip(), name + "file line " + str(line_n),
190 def parse_command_line_arguments():
191 """Return settings values read from command line arguments."""
192 parser = argparse.ArgumentParser()
193 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
195 opts, unknown = parser.parse_known_args()
200 """Ensure valid server out file belonging to current process.
202 This is done by comparing io_db["teststring"] to what's found at the start
203 of the current file at io_db["path_out"]. On failure, set
204 io_db["kicked_by_rival"] and raise SystemExit.
206 if not os.access(io_db["path_out"], os.F_OK):
207 raise SystemExit("Server output file has disappeared.")
208 file = open(io_db["path_out"], "r")
209 test = file.readline().rstrip("\n")
211 if test != io_db["teststring"]:
212 io_db["kicked_by_rival"] = True
213 msg = "Server test string in server output file does not match. This" \
214 " indicates that the current server process has been " \
215 "superseded by another one."
216 raise SystemExit(msg)
220 """Return next newline-delimited command from server in file.
222 Keep building return string until a newline is encountered. Pause between
223 unsuccessful reads, and after too much waiting, run server_test().
230 add = io_db["file_in"].readline()
232 command = command + add
233 if len(command) > 0 and "\n" == command[-1]:
234 command = command[:-1]
237 time.sleep(wait_on_fail)
238 if now + max_wait < time.time():
245 """Replay game from record file.
247 Use opts.replay as breakpoint turn to which to replay automatically before
248 switching to manual input by non-meta commands in server input file
249 triggering further reads of record file. Ensure opts.replay is at least 1.
253 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
254 " (if so late a turn is to be found).")
255 if not os.access(io_db["path_record"], os.F_OK):
256 raise SystemExit("No record file found to replay.")
257 io_db["file_record"] = open(io_db["path_record"], "r")
258 io_db["file_record"].prefix = "record file line "
259 io_db["file_record"].line_n = 1
260 while world_db["TURN"] < opts.replay:
261 line = io_db["file_record"].readline()
264 obey(line.rstrip(), io_db["file_record"].prefix
265 + str(io_db["file_record"].line_n))
266 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
268 obey(read_command(), "in file", replay=True)
272 """Play game by server input file commands. Before, load save file found.
274 If no save file is found, a new world is generated from the commands in the
275 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
276 command and all that follow via the server input file.
278 if os.access(io_db["path_save"], os.F_OK):
279 obey_lines_in_file(io_db["path_save"], "save")
281 if not os.access(io_db["path_worldconf"], os.F_OK):
282 msg = "No world config file from which to start a new world."
283 raise SystemExit(msg)
284 obey_lines_in_file(io_db["path_worldconf"], "world config ",
286 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
288 obey(read_command(), "in file", do_record=True)
293 print("I'd (re-)make the map now, if only I knew how.")
296 def set_world_inactive():
297 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
299 if os.access(io_db["path_worldstate"], os.F_OK):
300 os.remove(io_db["path_worldstate"])
301 world_db["WORLD_ACTIVE"] = 0
304 def integer_test(val_string, min, max):
305 """Return val_string if possible integer >= min and <= max, else None."""
307 val = int(val_string)
308 if val < min or val > max:
312 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
317 def setter(category, key, min, max):
318 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
321 val = integer_test(val_string, min, max)
325 if category == "Thing":
326 id_store = command_tid
327 decorator = test_Thing_id
328 elif category == "ThingType":
329 id_store = command_ttid
330 decorator = test_ThingType_id
331 elif category == "ThingAction":
332 id_store = command_taid
333 decorator = test_ThingAction_id
337 val = integer_test(val_string, min, max)
339 world_db[category + "s"][id_store.id][key] = val
344 """Send PONG line to server output file."""
345 io_db["file_out"].write("PONG\n")
346 io_db["file_out"].flush()
350 """Abort server process."""
351 raise SystemExit("received QUIT command")
354 def command_seedmap(seed_string):
355 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
356 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
360 def command_makeworld(seed_string):
362 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
363 setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
364 # TODO: Test for existence of player thing and 'wait' thing action?
367 def command_maplength(maplength_string):
370 # TODO: remove things, map
371 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
374 def command_worldactive(worldactive_string):
376 val = integer_test(worldactive_string, 0, 1)
378 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
380 elif 0 == world_db["WORLD_ACTIVE"]:
382 player_exists = False
384 # TODO: perform tests:
385 # Is there thing action of name 'wait'?
386 # Is there a player thing?
388 if wait_exists and player_exists and map_exists:
389 # TODO: rebuild al things' FOVs, map memories
390 world_db["WORLD_ACTIVE"] = 1
393 def id_setter(id_string, category, id_store, start_at_1=False):
394 """Set ID of object of category to manipulate ID unused? Create new one.
396 The ID is stored as id_store.id. If the integer of the input is valid (if
397 start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
398 start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
399 >= 1, and <= 255. None is always returned when no new object is created,
400 otherwise the new object's ID.
402 min = 0 if start_at_1 else -32768
403 max = 255 if start_at_1 else 32767
404 id = integer_test(id_string, min, max)
406 if id in world_db[category]:
410 if (start_at_1 and 0 == id) \
411 or ((not start_at_1) and (id < 0 or id > 255)):
415 if id not in world_db[category]:
419 "No unused ID available to add to ID list.")
425 def test_for_id_maker(object, category):
426 """Return decorator testing for object having "id" attribute."""
429 if hasattr(object, "id"):
432 print("Ignoring: No " + category +
433 " defined to manipulate yet.")
438 def command_tid(id_string):
439 """Set ID of Thing to manipulate. ID unused? Create new one.
441 Default new Thing's type to the first available ThingType, others: zero.
443 id = id_setter(id_string, "Things", command_tid)
445 if world_db["ThingTypes"] == {}:
446 print("Ignoring: No ThingType to settle new Thing in.")
448 world_db["Things"][id] = {
454 "T_TYPE": list(world_db["ThingTypes"].keys())[0],
461 "T_MEMDEPTHMAP": False
465 test_Thing_id = test_for_id_maker(command_tid, "Thing")
469 def command_tcommand(str_int):
470 """Set T_COMMAND of selected Thing."""
471 val = integer_test(str_int, 0, 255)
473 if 0 == val or val in world_db["ThingActions"]:
474 world_db["Things"][command_tid.id]["T_COMMAND"] = val
476 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
480 def command_ttype(str_int):
481 """Set T_TYPE of selected Thing."""
482 val = integer_test(str_int, 0, 255)
484 if val in world_db["ThingTypes"]:
485 world_db["Things"][command_tid.id]["T_TYPE"] = val
487 print("Ignoring: ThingType ID belongs to no known ThingType.")
491 def command_tcarries(str_int):
492 """Append int(str_int) to T_CARRIES of selected Thing.
494 The ID int(str_int) must not be of the selected Thing, and must belong to a
495 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
497 val = integer_test(str_int, 0, 255)
499 if val == command_tid.id:
500 print("Ignoring: Thing cannot carry itself.")
501 elif val in world_db["Things"] \
502 and not world_db["Things"][val]["carried"]:
503 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
504 world_db["Things"][val]["carried"] = True
506 print("Ignoring: Thing not available for carrying.")
507 # Note that the whole carrying structure is different from the C version:
508 # Carried-ness is marked by a "carried" flag, not by Things containing
513 def command_tmemthing(str_t, str_y, str_x):
514 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
516 The type must fit to an existing ThingType, and the position into the map.
518 type = integer_test(str_t, 0, 255)
519 posy = integer_test(str_y, 0, 255)
520 posx = integer_test(str_x, 0, 255)
521 if None != type and None != posy and None != posx:
522 if type not in world_db["ThingTypes"] \
523 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
524 print("Ignoring: Illegal value for thing type or position.")
526 memthing = (type, posy, posx)
527 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
530 def setter_map(maptype):
531 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
533 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
536 def helper(str_int, mapline):
537 val = integer_test(str_int, 0, 255)
539 if val >= world_db["MAP_LENGTH"]:
540 print("Illegal value for map line number.")
541 elif len(mapline) != world_db["MAP_LENGTH"]:
542 print("Map line length is unequal map width.")
544 length = world_db["MAP_LENGTH"]
546 if not world_db["Things"][command_tid.id][maptype]:
547 rmap = bytearray(b' ' * (length ** 2))
549 rmap = world_db["Things"][command_tid.id][maptype]
550 rmap[val * length:(val * length) + length] = mapline.encode()
551 world_db["Things"][command_tid.id][maptype] = rmap
555 def setter_tpos(axis):
556 """Generate setter for T_POSX or T_POSY of selected Thing."""
559 val = integer_test(str_int, 0, 255)
561 if val < world_db["MAP_LENGTH"]:
562 world_db["Things"][command_tid.id]["T_POS" + axis] = val
563 # TODO: Delete Thing's FOV, and rebuild it if world is active.
565 print("Ignoring: Position is outside of map.")
569 def command_ttid(id_string):
570 """Set ID of ThingType to manipulate. ID unused? Create new one.
572 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
574 id = id_setter(id_string, "ThingTypes", command_ttid)
576 world_db["ThingTypes"][id] = {
581 "TT_START_NUMBER": 0,
587 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
591 def command_ttname(name):
592 """Set TT_NAME of selected ThingType."""
593 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
597 def command_ttsymbol(char):
598 """Set TT_SYMBOL of selected ThingType. """
600 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
602 print("Ignoring: Argument must be single character.")
606 def command_ttcorpseid(str_int):
607 """Set TT_CORPSE_ID of selected ThingType."""
608 val = integer_test(str_int, 0, 255)
610 if val in world_db["ThingTypes"]:
611 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
613 print("Ignoring: Corpse ID belongs to no known ThignType.")
616 def command_taid(id_string):
617 """Set ID of ThingAction to manipulate. ID unused? Create new one.
619 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
621 id = id_setter(id_string, "ThingActions", command_taid, True)
623 world_db["ThingActions"][id] = {
629 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
633 def command_taname(name):
634 """Set TA_NAME of selected ThingAction.
636 The name must match a valid thing action function. If after the name
637 setting no ThingAction with name "wait" remains, call set_world_inactive().
639 if name == "wait" or name == "move" or name == "use" or name == "drop" \
640 or name == "pick_up":
641 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
642 if 1 == world_db["WORLD_ACTIVE"]:
644 for id in world_db["ThingActions"]:
645 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
651 print("Ignoring: Invalid action name.")
652 # In contrast to the original,naming won't map a function to a ThingAction.
655 """Commands database.
657 Map command start tokens to ([0]) number of expected command arguments, ([1])
658 the command's meta-ness (i.e. is it to be written to the record file, is it to
659 be ignored in replay mode if read from server input file), and ([2]) a function
663 "QUIT": (0, True, command_quit),
664 "PING": (0, True, command_ping),
665 "MAKE_WORLD": (1, False, command_makeworld),
666 "SEED_MAP": (1, False, command_seedmap),
667 "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
669 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
670 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
671 "MAP_LENGTH": (1, False, command_maplength),
672 "WORLD_ACTIVE": (1, False, command_worldactive),
673 "TA_ID": (1, False, command_taid),
674 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
675 "TA_NAME": (1, False, command_taname),
676 "TT_ID": (1, False, command_ttid),
677 "TT_NAME": (1, False, command_ttname),
678 "TT_SYMBOL": (1, False, command_ttsymbol),
679 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
680 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
682 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
684 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
686 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
687 "T_ID": (1, False, command_tid),
688 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
689 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
690 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
691 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
692 "T_COMMAND": (1, False, command_tcommand),
693 "T_TYPE": (1, False, command_ttype),
694 "T_CARRIES": (1, False, command_tcarries),
695 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
696 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
697 "T_MEMTHING": (3, False, command_tmemthing),
698 "T_POSY": (1, False, setter_tpos("Y")),
699 "T_POSX": (1, False, setter_tpos("X")),
703 """World state database. With sane default values."""
707 "SEED_RANDOMNESS": 0,
717 """File IO database."""
720 "path_record": "record",
721 "path_worldconf": "confserver/world",
722 "path_server": "server/",
723 "path_in": "server/in",
724 "path_out": "server/out",
725 "path_worldstate": "server/worldstate",
726 "tmp_suffix": "_tmp",
727 "kicked_by_rival": False
732 opts = parse_command_line_arguments()
734 # print("DUMMY: Run game.")
735 if None != opts.replay:
739 except SystemExit as exit:
740 print("ABORTING: " + exit.args[0])
742 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
746 # print("DUMMY: (Clean up C heap.)")