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().
124 string = string.replace("\u005C", '\u005C\u005C')
125 return '"' + string.replace('"', '\u005C"') + '"'
130 if world_db["Things"][id][key]:
131 rmap = world_db["Things"][id][key]
132 length = world_db["MAP_LENGTH"]
133 for i in range(world_db["MAP_LENGTH"]):
134 line = rmap[i * length:(i * length) + length].decode()
135 string = string + key + " " + str(i) + quote(line) + "\n"
141 for memthing in world_db["Things"][id]["T_MEMTHING"]:
142 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
143 str(memthing[1]) + " " + str(memthing[2]) + "\n"
146 def helper(category, id_string, special_keys={}):
148 for id in world_db[category]:
149 string = string + id_string + " " + str(id) + "\n"
150 for key in world_db[category][id]:
151 if not key in special_keys:
152 x = world_db[category][id][key]
153 argument = quote(x) if str == type(x) else str(x)
154 string = string + key + " " + argument + "\n"
155 elif special_keys[key]:
156 string = string + special_keys[key](id)
161 if dict != type(world_db[key]):
162 string = string + key + " " + str(world_db[key]) + "\n"
163 string = string + helper("ThingActions", "TA_ID")
164 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
165 for id in world_db["ThingTypes"]:
166 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
167 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
168 string = string + helper("Things", "T_ID",
169 {"T_CARRIES": False, "carried": False,
170 "T_MEMMAP": mapsetter("T_MEMMAP"),
171 "T_MEMTHING": memthing,
172 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
173 for id in world_db["Things"]:
174 if [] != world_db["Things"][id]["T_CARRIES"]:
175 string = string + "T_ID " + str(id) + "\n"
176 for carried_id in world_db["Things"][id]["T_CARRIES"]:
177 string = string + "T_CARRIES " + str(carried_id) + "\n"
178 atomic_write(io_db["path_save"], string)
181 def obey_lines_in_file(path, name, do_record=False):
182 """Call obey() on each line of path's file, use name in input prefix."""
183 file = open(path, "r")
185 for line in file.readlines():
186 obey(line.rstrip(), name + "file line " + str(line_n),
192 def parse_command_line_arguments():
193 """Return settings values read from command line arguments."""
194 parser = argparse.ArgumentParser()
195 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
197 opts, unknown = parser.parse_known_args()
202 """Ensure valid server out file belonging to current process.
204 This is done by comparing io_db["teststring"] to what's found at the start
205 of the current file at io_db["path_out"]. On failure, set
206 io_db["kicked_by_rival"] and raise SystemExit.
208 if not os.access(io_db["path_out"], os.F_OK):
209 raise SystemExit("Server output file has disappeared.")
210 file = open(io_db["path_out"], "r")
211 test = file.readline().rstrip("\n")
213 if test != io_db["teststring"]:
214 io_db["kicked_by_rival"] = True
215 msg = "Server test string in server output file does not match. This" \
216 " indicates that the current server process has been " \
217 "superseded by another one."
218 raise SystemExit(msg)
222 """Return next newline-delimited command from server in file.
224 Keep building return string until a newline is encountered. Pause between
225 unsuccessful reads, and after too much waiting, run server_test().
232 add = io_db["file_in"].readline()
234 command = command + add
235 if len(command) > 0 and "\n" == command[-1]:
236 command = command[:-1]
239 time.sleep(wait_on_fail)
240 if now + max_wait < time.time():
247 """Replay game from record file.
249 Use opts.replay as breakpoint turn to which to replay automatically before
250 switching to manual input by non-meta commands in server input file
251 triggering further reads of record file. Ensure opts.replay is at least 1.
255 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
256 " (if so late a turn is to be found).")
257 if not os.access(io_db["path_record"], os.F_OK):
258 raise SystemExit("No record file found to replay.")
259 io_db["file_record"] = open(io_db["path_record"], "r")
260 io_db["file_record"].prefix = "record file line "
261 io_db["file_record"].line_n = 1
262 while world_db["TURN"] < opts.replay:
263 line = io_db["file_record"].readline()
266 obey(line.rstrip(), io_db["file_record"].prefix
267 + str(io_db["file_record"].line_n))
268 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
270 obey(read_command(), "in file", replay=True)
274 """Play game by server input file commands. Before, load save file found.
276 If no save file is found, a new world is generated from the commands in the
277 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
278 command and all that follow via the server input file.
280 if os.access(io_db["path_save"], os.F_OK):
281 obey_lines_in_file(io_db["path_save"], "save")
283 if not os.access(io_db["path_worldconf"], os.F_OK):
284 msg = "No world config file from which to start a new world."
285 raise SystemExit(msg)
286 obey_lines_in_file(io_db["path_worldconf"], "world config ",
288 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
290 obey(read_command(), "in file", do_record=True)
295 world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
298 def set_world_inactive():
299 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
301 if os.access(io_db["path_worldstate"], os.F_OK):
302 os.remove(io_db["path_worldstate"])
303 world_db["WORLD_ACTIVE"] = 0
306 def integer_test(val_string, min, max):
307 """Return val_string if possible integer >= min and <= max, else None."""
309 val = int(val_string)
310 if val < min or val > max:
314 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
319 def setter(category, key, min, max):
320 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
323 val = integer_test(val_string, min, max)
327 if category == "Thing":
328 id_store = command_tid
329 decorator = test_Thing_id
330 elif category == "ThingType":
331 id_store = command_ttid
332 decorator = test_ThingType_id
333 elif category == "ThingAction":
334 id_store = command_taid
335 decorator = test_ThingAction_id
339 val = integer_test(val_string, min, max)
341 world_db[category + "s"][id_store.id][key] = val
346 """Send PONG line to server output file."""
347 io_db["file_out"].write("PONG\n")
348 io_db["file_out"].flush()
352 """Abort server process."""
353 raise SystemExit("received QUIT command")
356 def command_seedmap(seed_string):
357 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
358 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
362 def command_makeworld(seed_string):
364 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
365 setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
366 # TODO: Test for existence of player thing and 'wait' thing action?
369 def command_maplength(maplength_string):
373 world_db["Things"] = {}
374 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
377 def command_worldactive(worldactive_string):
379 val = integer_test(worldactive_string, 0, 1)
381 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
383 elif 0 == world_db["WORLD_ACTIVE"]:
385 for ThingAction in world_db["ThingActions"]:
386 if "wait" == ThingAction["TA_NAME"]:
389 player_exists = False
390 for Thing in world_db["Things"]:
391 if 0 == ThingAction["T_ID"]:
394 map_exists = "MAP" in world_db
395 if wait_exists and player_exists and map_exists:
396 # TODO: rebuild all things' FOVs, map memories
397 world_db["WORLD_ACTIVE"] = 1
400 def id_setter(id_string, category, id_store, start_at_1=False):
401 """Set ID of object of category to manipulate ID unused? Create new one.
403 The ID is stored as id_store.id. If the integer of the input is valid (if
404 start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
405 start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
406 >= 1, and <= 255. None is always returned when no new object is created,
407 otherwise the new object's ID.
409 min = 0 if start_at_1 else -32768
410 max = 255 if start_at_1 else 32767
411 id = integer_test(id_string, min, max)
413 if id in world_db[category]:
417 if (start_at_1 and 0 == id) \
418 or ((not start_at_1) and (id < 0 or id > 255)):
422 if id not in world_db[category]:
426 "No unused ID available to add to ID list.")
432 def test_for_id_maker(object, category):
433 """Return decorator testing for object having "id" attribute."""
436 if hasattr(object, "id"):
439 print("Ignoring: No " + category +
440 " defined to manipulate yet.")
445 def command_tid(id_string):
446 """Set ID of Thing to manipulate. ID unused? Create new one.
448 Default new Thing's type to the first available ThingType, others: zero.
450 id = id_setter(id_string, "Things", command_tid)
452 if world_db["ThingTypes"] == {}:
453 print("Ignoring: No ThingType to settle new Thing in.")
455 world_db["Things"][id] = {
461 "T_TYPE": list(world_db["ThingTypes"].keys())[0],
468 "T_MEMDEPTHMAP": False
472 test_Thing_id = test_for_id_maker(command_tid, "Thing")
476 def command_tcommand(str_int):
477 """Set T_COMMAND of selected Thing."""
478 val = integer_test(str_int, 0, 255)
480 if 0 == val or val in world_db["ThingActions"]:
481 world_db["Things"][command_tid.id]["T_COMMAND"] = val
483 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
487 def command_ttype(str_int):
488 """Set T_TYPE of selected Thing."""
489 val = integer_test(str_int, 0, 255)
491 if val in world_db["ThingTypes"]:
492 world_db["Things"][command_tid.id]["T_TYPE"] = val
494 print("Ignoring: ThingType ID belongs to no known ThingType.")
498 def command_tcarries(str_int):
499 """Append int(str_int) to T_CARRIES of selected Thing.
501 The ID int(str_int) must not be of the selected Thing, and must belong to a
502 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
504 val = integer_test(str_int, 0, 255)
506 if val == command_tid.id:
507 print("Ignoring: Thing cannot carry itself.")
508 elif val in world_db["Things"] \
509 and not world_db["Things"][val]["carried"]:
510 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
511 world_db["Things"][val]["carried"] = True
513 print("Ignoring: Thing not available for carrying.")
514 # Note that the whole carrying structure is different from the C version:
515 # Carried-ness is marked by a "carried" flag, not by Things containing
520 def command_tmemthing(str_t, str_y, str_x):
521 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
523 The type must fit to an existing ThingType, and the position into the map.
525 type = integer_test(str_t, 0, 255)
526 posy = integer_test(str_y, 0, 255)
527 posx = integer_test(str_x, 0, 255)
528 if None != type and None != posy and None != posx:
529 if type not in world_db["ThingTypes"] \
530 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
531 print("Ignoring: Illegal value for thing type or position.")
533 memthing = (type, posy, posx)
534 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
537 def setter_map(maptype):
538 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
540 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
543 def helper(str_int, mapline):
544 val = integer_test(str_int, 0, 255)
546 if val >= world_db["MAP_LENGTH"]:
547 print("Illegal value for map line number.")
548 elif len(mapline) != world_db["MAP_LENGTH"]:
549 print("Map line length is unequal map width.")
551 length = world_db["MAP_LENGTH"]
553 if not world_db["Things"][command_tid.id][maptype]:
554 rmap = bytearray(b' ' * (length ** 2))
556 rmap = world_db["Things"][command_tid.id][maptype]
557 rmap[val * length:(val * length) + length] = mapline.encode()
558 world_db["Things"][command_tid.id][maptype] = rmap
562 def setter_tpos(axis):
563 """Generate setter for T_POSX or T_POSY of selected Thing."""
566 val = integer_test(str_int, 0, 255)
568 if val < world_db["MAP_LENGTH"]:
569 world_db["Things"][command_tid.id]["T_POS" + axis] = val
570 # TODO: Delete Thing's FOV, and rebuild it if world is active.
572 print("Ignoring: Position is outside of map.")
576 def command_ttid(id_string):
577 """Set ID of ThingType to manipulate. ID unused? Create new one.
579 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
581 id = id_setter(id_string, "ThingTypes", command_ttid)
583 world_db["ThingTypes"][id] = {
588 "TT_START_NUMBER": 0,
594 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
598 def command_ttname(name):
599 """Set TT_NAME of selected ThingType."""
600 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
604 def command_ttsymbol(char):
605 """Set TT_SYMBOL of selected ThingType. """
607 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
609 print("Ignoring: Argument must be single character.")
613 def command_ttcorpseid(str_int):
614 """Set TT_CORPSE_ID of selected ThingType."""
615 val = integer_test(str_int, 0, 255)
617 if val in world_db["ThingTypes"]:
618 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
620 print("Ignoring: Corpse ID belongs to no known ThignType.")
623 def command_taid(id_string):
624 """Set ID of ThingAction to manipulate. ID unused? Create new one.
626 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
628 id = id_setter(id_string, "ThingActions", command_taid, True)
630 world_db["ThingActions"][id] = {
636 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
640 def command_taname(name):
641 """Set TA_NAME of selected ThingAction.
643 The name must match a valid thing action function. If after the name
644 setting no ThingAction with name "wait" remains, call set_world_inactive().
646 if name == "wait" or name == "move" or name == "use" or name == "drop" \
647 or name == "pick_up":
648 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
649 if 1 == world_db["WORLD_ACTIVE"]:
651 for id in world_db["ThingActions"]:
652 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
658 print("Ignoring: Invalid action name.")
659 # In contrast to the original,naming won't map a function to a ThingAction.
662 """Commands database.
664 Map command start tokens to ([0]) number of expected command arguments, ([1])
665 the command's meta-ness (i.e. is it to be written to the record file, is it to
666 be ignored in replay mode if read from server input file), and ([2]) a function
670 "QUIT": (0, True, command_quit),
671 "PING": (0, True, command_ping),
672 "MAKE_WORLD": (1, False, command_makeworld),
673 "SEED_MAP": (1, False, command_seedmap),
674 "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
676 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
677 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
678 "MAP_LENGTH": (1, False, command_maplength),
679 "WORLD_ACTIVE": (1, False, command_worldactive),
680 "TA_ID": (1, False, command_taid),
681 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
682 "TA_NAME": (1, False, command_taname),
683 "TT_ID": (1, False, command_ttid),
684 "TT_NAME": (1, False, command_ttname),
685 "TT_SYMBOL": (1, False, command_ttsymbol),
686 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
687 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
689 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
691 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
693 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
694 "T_ID": (1, False, command_tid),
695 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
696 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
697 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
698 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
699 "T_COMMAND": (1, False, command_tcommand),
700 "T_TYPE": (1, False, command_ttype),
701 "T_CARRIES": (1, False, command_tcarries),
702 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
703 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
704 "T_MEMTHING": (3, False, command_tmemthing),
705 "T_POSY": (1, False, setter_tpos("Y")),
706 "T_POSX": (1, False, setter_tpos("X")),
710 """World state database. With sane default values."""
714 "SEED_RANDOMNESS": 0,
724 """File IO database."""
727 "path_record": "record",
728 "path_worldconf": "confserver/world",
729 "path_server": "server/",
730 "path_in": "server/in",
731 "path_out": "server/out",
732 "path_worldstate": "server/worldstate",
733 "tmp_suffix": "_tmp",
734 "kicked_by_rival": False
739 opts = parse_command_line_arguments()
741 # print("DUMMY: Run game.")
742 if None != opts.replay:
746 except SystemExit as exit:
747 print("ABORTING: " + exit.args[0])
749 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
753 # print("DUMMY: (Clean up C heap.)")