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 # Dummy for saving all commands to reconstruct current world state.
121 # Misses same optimizations as record() from the original record().
124 if dict != type(world_db[key]):
125 string = string + key + " " + str(world_db[key]) + "\n"
126 for id in world_db["ThingActions"]:
127 string = string + "TA_ID " + str(id) + "\n"
128 for key in world_db["ThingActions"][id]:
129 val = world_db["ThingActions"][id][key]
130 argument = "'" + val + "'" if str == type(val) else str(val)
131 string = string + key + " " + argument + "\n"
132 for id in world_db["ThingTypes"]:
133 string = string + "TT_ID " + str(id) + "\n"
134 for key in world_db["ThingTypes"][id]:
135 if not "TT_CORPSE_ID" == key:
136 val = world_db["ThingTypes"][id][key]
137 argument = "'" + val + "'" if str == type(val) else str(val)
138 string = string + key + " " + argument + "\n"
139 for id in world_db["ThingTypes"]:
140 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
141 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
142 atomic_write(io_db["path_save"], string)
145 def obey_lines_in_file(path, name, do_record=False):
146 """Call obey() on each line of path's file, use name in input prefix."""
147 file = open(path, "r")
149 for line in file.readlines():
150 obey(line.rstrip(), name + "file line " + str(line_n),
156 def parse_command_line_arguments():
157 """Return settings values read from command line arguments."""
158 parser = argparse.ArgumentParser()
159 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
161 opts, unknown = parser.parse_known_args()
166 """Ensure valid server out file belonging to current process.
168 This is done by comparing io_db["teststring"] to what's found at the start
169 of the current file at io_db["path_out"]. On failure, set
170 io_db["kicked_by_rival"] and raise SystemExit.
172 if not os.access(io_db["path_out"], os.F_OK):
173 raise SystemExit("Server output file has disappeared.")
174 file = open(io_db["path_out"], "r")
175 test = file.readline().rstrip("\n")
177 if test != io_db["teststring"]:
178 io_db["kicked_by_rival"] = True
179 msg = "Server test string in server output file does not match. This" \
180 " indicates that the current server process has been " \
181 "superseded by another one."
182 raise SystemExit(msg)
186 """Return next newline-delimited command from server in file.
188 Keep building return string until a newline is encountered. Pause between
189 unsuccessful reads, and after too much waiting, run server_test().
196 add = io_db["file_in"].readline()
198 command = command + add
199 if len(command) > 0 and "\n" == command[-1]:
200 command = command[:-1]
203 time.sleep(wait_on_fail)
204 if now + max_wait < time.time():
211 """Replay game from record file.
213 Use opts.replay as breakpoint turn to which to replay automatically before
214 switching to manual input by non-meta commands in server input file
215 triggering further reads of record file. Ensure opts.replay is at least 1.
219 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
220 " (if so late a turn is to be found).")
221 if not os.access(io_db["path_record"], os.F_OK):
222 raise SystemExit("No record file found to replay.")
223 io_db["file_record"] = open(io_db["path_record"], "r")
224 io_db["file_record"].prefix = "record file line "
225 io_db["file_record"].line_n = 1
226 while world_db["TURN"] < opts.replay:
227 line = io_db["file_record"].readline()
230 obey(line.rstrip(), io_db["file_record"].prefix
231 + str(io_db["file_record"].line_n))
232 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
234 obey(read_command(), "in file", replay=True)
238 """Play game by server input file commands. Before, load save file found.
240 If no save file is found, a new world is generated from the commands in the
241 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
242 command and all that follow via the server input file.
244 if os.access(io_db["path_save"], os.F_OK):
245 obey_lines_in_file(io_db["path_save"], "save")
247 if not os.access(io_db["path_worldconf"], os.F_OK):
248 msg = "No world config file from which to start a new world."
249 raise SystemExit(msg)
250 obey_lines_in_file(io_db["path_worldconf"], "world config ",
252 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
254 obey(read_command(), "in file", do_record=True)
259 print("I'd (re-)make the map now, if only I knew how.")
262 def set_world_inactive():
263 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
265 if os.access(io_db["path_worldstate"], os.F_OK):
266 os.remove(io_db["path_worldstate"])
267 world_db["WORLD_ACTIVE"] = 0
270 def integer_test(val_string, min, max):
271 """Return val_string if possible integer >= min and <= max, else None."""
273 val = int(val_string)
274 if val < min or val > max:
278 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
283 def worlddb_value_setter(key, min, max):
284 """Generate: Set world_db[key] to int(val_string) if >= min and <= max."""
285 def func(val_string):
286 val = integer_test(val_string, min, max)
293 """Send PONG line to server output file."""
294 io_db["file_out"].write("PONG\n")
295 io_db["file_out"].flush()
299 """Abort server process."""
300 raise SystemExit("received QUIT command")
303 def command_seedmap(seed_string):
304 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
305 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
309 def command_makeworld(seed_string):
311 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
312 worlddb_value_setter("SEED_RANDOMNESS", 0, 4294967295)(seed_string)
313 # TODO: Test for existence of player thing and 'wait' thing action?
316 def command_maplength(maplength_string):
319 # TODO: remove things, map
320 worlddb_value_setter("MAP_LENGTH", 1, 256)(maplength_string)
323 def command_worldactive(worldactive_string):
325 val = integer_test(worldactive_string, 0, 1)
327 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
329 elif 0 == world_db["WORLD_ACTIVE"]:
331 player_exists = False
333 # TODO: perform tests:
334 # Is there thing action of name 'wait'?
335 # Is there a player thing?
337 if wait_exists and player_exists and map_exists:
338 # TODO: rebuild al things' FOVs, map memories
339 world_db["WORLD_ACTIVE"] = 1
342 def command_ttid(id_string):
343 """Set ID of ThingType to manipulate. ID unused? Create new ThingType.
345 The ID of the ThingType to manipulate is stored as command_ttid.id. If
346 the integer of the input value is valid (>= -32768 and <= 32767), but <0 or
347 >255, a new ID is calculated: the lowest unused ID >=0 and <= 255.
349 id = integer_test(id_string, -32768, 32767)
351 if id in world_db["ThingTypes"]:
354 if id < 0 or id > 255:
358 if id not in world_db["ThingTypes"]:
362 "No unused ID available to add to ID list.")
365 world_db["ThingTypes"][id] = {
370 "TT_START_NUMBER": 0,
376 def test_for_id_maker(object, category):
377 """Return decorator testing for object having "id" attribute."""
380 if hasattr(object, "id"):
383 print("Ignoring: No " + category +
384 " defined to manipulate yet.")
389 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
392 def ThingType_value_setter(key, min, max):
393 """Build: Set selected ThingType's [key] to int(val_string) >=min/<=max."""
396 val = integer_test(val_string, min, max)
398 world_db["ThingTypes"][command_ttid.id][key] = val
403 def command_ttname(name):
404 """Set to name TT_NAME of selected ThingType."""
405 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
409 def command_ttsymbol(char):
410 """Set to char TT_SYMBOL of selected ThingType. """
412 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
414 print("Ignoring: Argument must be single character.")
418 def command_ttcorpseid(str_int):
419 """Set to int(str_int) TT_CORPSE_ID of selected ThingType."""
420 val = integer_test(str_int, 0, 255)
422 if val in world_db["ThingTypes"]:
423 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
425 print("Corpse ID belongs to no known object type.")
428 def command_taid(id_string):
429 """Set ID of ThingAction to manipulate. ID unused? Create new ThingAction.
431 The ID of the ThingAction to manipulate is stored as command_taid.id. If
432 the integer of the input value is valid (>= 0 and <= 255), but 0, a new ID
433 is calculated: The lowest unused ID >0 and <= 255.
435 id = integer_test(id_string, 0, 255)
437 if id in world_db["ThingActions"]:
443 if id not in world_db["ThingActions"]:
447 "No unused ID available to add to ID list.")
450 world_db["ThingActions"][id] = {
456 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
460 def command_taeffort(str_int):
461 """Set to int(str_int) TA_EFFORT of selected ThingAction."""
462 val = integer_test(str_int, 0, 255)
464 world_db["ThingActions"][command_taid.id]["TA_EFFORT"] = val
468 def command_taname(name):
469 """Set to name TA_NAME of selected ThingAction.
471 The name must match a valid thing action function. If after the name
472 setting no ThingAction with name "wait" remains, call set_world_inactive().
474 if name == "wait" or name == "move" or name == "use" or name == "drop" \
475 or name == "pick_up":
476 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
477 if 1 == world_db["WORLD_ACTIVE"]:
479 for id in world_db["ThingActions"]:
480 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
486 print("Ignoring: Invalid action name.")
487 # In contrast to the original,naming won't map a function to a ThingAction.
490 """Commands database.
492 Map command start tokens to ([0]) number of expected command arguments, ([1])
493 the command's meta-ness (i.e. is it to be written to the record file, is it to
494 be ignored in replay mode if read from server input file), and ([2]) a function
498 "QUIT": (0, True, command_quit),
499 "PING": (0, True, command_ping),
500 "MAKE_WORLD": (1, False, command_makeworld),
501 "SEED_MAP": (1, False, command_seedmap),
502 "SEED_RANDOMNESS": (1, False, worlddb_value_setter("SEED_RANDOMNESS",
504 "TURN": (1, False, worlddb_value_setter("TURN", 0, 65535)),
505 "PLAYER_TYPE": (1, False, worlddb_value_setter("PLAYER_TYPE", 0, 255)),
506 "MAP_LENGTH": (1, False, command_maplength),
507 "WORLD_ACTIVE": (1, False, command_worldactive),
508 "TA_ID": (1, False, command_taid),
509 "TA_EFFORT": (1, False, command_taeffort),
510 "TA_NAME": (1, False, command_taname),
511 "TT_ID": (1, False, command_ttid),
512 "TT_NAME": (1, False, command_ttname),
513 "TT_SYMBOL": (1, False, command_ttsymbol),
514 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
515 "TT_CONSUMABLE": (1, False, ThingType_value_setter("TT_CONSUMABLE",
517 "TT_START_NUMBER": (1, False, ThingType_value_setter("TT_START_NUMBER",
519 "TT_PROLIFERATE": (1, False, ThingType_value_setter("TT_PROLIFERATE",
521 "TT_LIFEPOINTS": (1, False, ThingType_value_setter("TT_LIFEPOINTS",
526 """World state database. With sane default values."""
530 "SEED_RANDOMNESS": 0,
540 """File IO database."""
543 "path_record": "record",
544 "path_worldconf": "confserver/world",
545 "path_server": "server/",
546 "path_in": "server/in",
547 "path_out": "server/out",
548 "path_worldstate": "server/worldstate",
549 "tmp_suffix": "_tmp",
550 "kicked_by_rival": False
555 opts = parse_command_line_arguments()
557 # print("DUMMY: Run game.")
558 if None != opts.replay:
562 except SystemExit as exit:
563 print("ABORTING: " + exit.args[0])
565 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
569 # print("DUMMY: (Clean up C heap.)")