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:])
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().
122 atomic_write(io_db["path_save"],
123 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"]) + "\n" +
124 "MAP_LENGTH " + str(world_db["MAP_LENGTH"]) + "\n" +
125 "PLAYER_TYPE " + str(world_db["PLAYER_TYPE"]) + "\n" +
126 "TURN " + str(world_db["TURN"]) + "\n" +
127 "SEED_RANDOMNESS " + str(world_db["SEED_RANDOMNESS"]) + "\n" +
128 "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n")
129 # TODO: If all this ever does is just writing down what's in world_db, some
130 # loop over its entries should be all that's needed.
133 def obey_lines_in_file(path, name, do_record=False):
134 """Call obey() on each line of path's file, use name in input prefix."""
135 file = open(path, "r")
137 for line in file.readlines():
138 obey(line.rstrip(), name + "file line " + str(line_n),
144 def parse_command_line_arguments():
145 """Return settings values read from command line arguments."""
146 parser = argparse.ArgumentParser()
147 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
149 opts, unknown = parser.parse_known_args()
154 """Ensure valid server out file belonging to current process.
156 This is done by comparing io_db["teststring"] to what's found at the start
157 of the current file at io_db["path_out"]. On failure, set
158 io_db["kicked_by_rival"] and raise SystemExit.
160 if not os.access(io_db["path_out"], os.F_OK):
161 raise SystemExit("Server output file has disappeared.")
162 file = open(io_db["path_out"], "r")
163 test = file.readline().rstrip("\n")
165 if test != io_db["teststring"]:
166 io_db["kicked_by_rival"] = True
167 msg = "Server test string in server output file does not match. This" \
168 " indicates that the current server process has been " \
169 "superseded by another one."
170 raise SystemExit(msg)
174 """Return next newline-delimited command from server in file.
176 Keep building return string until a newline is encountered. Pause between
177 unsuccessful reads, and after too much waiting, run server_test().
184 add = io_db["file_in"].readline()
186 command = command + add
187 if len(command) > 0 and "\n" == command[-1]:
188 command = command[:-1]
191 time.sleep(wait_on_fail)
192 if now + max_wait < time.time():
199 """Replay game from record file.
201 Use opts.replay as breakpoint turn to which to replay automatically before
202 switching to manual input by non-meta commands in server input file
203 triggering further reads of record file. Ensure opts.replay is at least 1.
207 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
208 " (if so late a turn is to be found).")
209 if not os.access(io_db["path_record"], os.F_OK):
210 raise SystemExit("No record file found to replay.")
211 io_db["file_record"] = open(io_db["path_record"], "r")
212 io_db["file_record"].prefix = "record file line "
213 io_db["file_record"].line_n = 1
214 while world_db["TURN"] < opts.replay:
215 line = io_db["file_record"].readline()
218 obey(line.rstrip(), io_db["file_record"].prefix
219 + str(io_db["file_record"].line_n))
220 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
222 obey(read_command(), "in file", replay=True)
226 """Play game by server input file commands. Before, load save file found.
228 If no save file is found, a new world is generated from the commands in the
229 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
230 command and all that follow via the server input file.
232 if os.access(io_db["path_save"], os.F_OK):
233 obey_lines_in_file(io_db["path_save"], "save")
235 if not os.access(io_db["path_worldconf"], os.F_OK):
236 msg = "No world config file from which to start a new world."
237 raise SystemExit(msg)
238 obey_lines_in_file(io_db["path_worldconf"], "world config ",
240 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
242 obey(read_command(), "in file", do_record=True)
247 print("I'd (re-)make the map now, if only I knew how.")
250 def set_world_inactive():
251 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
253 if os.access(io_db["path_worldstate"], os.F_OK):
254 os.remove(io_db["path_worldstate"])
255 world_db["WORLD_ACTIVE"] = 0
258 def worlddb_value_setter(key, min, max):
259 """Generate: Set world_db[key] to int(val_string) if >= min and <= max."""
260 def func(val_string):
262 val = int(val_string)
263 if val < min or val > max:
267 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
273 """Send PONG line to server output file."""
274 io_db["file_out"].write("PONG\n")
275 io_db["file_out"].flush()
279 """Abort server process."""
280 raise SystemExit("received QUIT command")
283 def command_seedmap(seed_string):
284 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
285 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
289 def command_makeworld(seed_string):
291 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
292 worlddb_value_setter("SEED_RANDOMNESS", 0, 4294967295)(seed_string)
293 # TODO: Test for existence of player thing and 'wait' thing action?
296 def command_maplength(maplength_string):
299 # TODO: remove things, map
300 worlddb_value_setter("MAP_LENGTH", 1, 256)(maplength_string)
303 def command_worldactive(worldactive_string):
306 val = int(worldactive_string)
307 if not (0 == val or 1 == val):
310 print("Ignoring: Please use integer 0 or 1.")
312 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
314 elif 0 == world_db["WORLD_ACTIVE"]:
316 player_exists = False
318 # TODO: perform tests:
319 # Is there thing action of name 'wait'?
320 # Is there a player thing?
322 if wait_exists and player_exists and map_exists:
323 # TODO: rebuild al things' FOVs, map memories
324 world_db["WORLD_ACTIVE"] = 1
327 def command_taid(id_string):
330 """Add new ThingAction to world_db["thing_actions"]."""
334 if id not in world_db["thing actions"]:
337 print("Ignoring: No unused ID available to add to ID list.")
339 world_db["thing actions"][id] = { "TA_EFFORT": 1, "TA_NAME": "wait" }
344 if id < min or id > max:
347 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
350 if id in world_db["thing actions"]:
351 pass # TODO: Assign ID to work on in other TA_ commands …
357 """Commands database.
359 Map command start tokens to ([0]) number of expected command arguments, ([1])
360 the command's meta-ness (i.e. is it to be written to the record file, is it to
361 be ignored in replay mode if read from server input file), and ([2]) a function
365 "QUIT": (0, True, command_quit),
366 "PING": (0, True, command_ping),
367 "MAKE_WORLD": (1, False, command_makeworld),
368 "SEED_MAP": (1, False, command_seedmap),
369 "SEED_RANDOMNESS": (1, False, worlddb_value_setter("SEED_RANDOMNESS", 0,
371 "TURN": (1, False, worlddb_value_setter("TURN", 0, 65535)),
372 "PLAYER_TYPE": (1, False, worlddb_value_setter("PLAYER_TYPE", 0, 255)),
373 "MAP_LENGTH": (1, False, command_maplength),
374 "WORLD_ACTIVE": (1, False, command_worldactive),
375 "TA_ID": (1, False, command_taid)
379 """World state database. With sane default values."""
383 "SEED_RANDOMNESS": 0,
393 """File IO database."""
396 "path_record": "record",
397 "path_worldconf": "confserver/world",
398 "path_server": "server/",
399 "path_in": "server/in",
400 "path_out": "server/out",
401 "path_worldstate": "server/worldstate",
402 "tmp_suffix": "_tmp",
403 "kicked_by_rival": False
408 opts = parse_command_line_arguments()
410 # print("DUMMY: Run game.")
411 if None != opts.replay:
415 except SystemExit as exit:
416 print("ABORTING: " + exit.args[0])
418 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
422 # print("DUMMY: (Clean up C heap.)")