1 # This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
2 # or any later version. For details on its copyright, license, and warranties,
3 # see the file NOTICE in the root directory of the PlomRogue source package.
9 from server.config.world_data import world_db
10 from server.config.io import io_db
14 """Ensure valid server out file belonging to current process.
16 This is done by comparing io_db["teststring"] to what's found at the start
17 of the current file at io_db["path_out"]. On failure, set
18 io_db["kicked_by_rival"] and raise SystemExit.
20 if not os.access(io_db["path_out"], os.F_OK):
21 raise SystemExit("Server output file has disappeared.")
22 file = open(io_db["path_out"], "r")
23 test = file.readline().rstrip("\n")
25 if test != io_db["teststring"]:
26 io_db["kicked_by_rival"] = True
27 msg = "Server test string in server output file does not match. This" \
28 " indicates that the current server process has been " \
29 "superseded by another one."
33 def safely_remove_worldstate_file():
34 from server.io import server_test
35 if os.access(io_db["path_worldstate"], os.F_OK):
37 os.remove(io_db["path_worldstate"])
40 def atomic_write(path, text, do_append=False, delete=True):
41 """Atomic write of text to file at path, appended if do_append is set."""
42 path_tmp = path + io_db["tmp_suffix"]
46 if os.access(path, os.F_OK):
47 from shutil import copyfile
48 copyfile(path, path_tmp)
49 file = open(path_tmp, mode)
50 strong_write(file, text)
52 if delete and os.access(path, os.F_OK):
54 os.rename(path_tmp, path)
57 def strong_write(file, string):
58 """Apply write(string), then flush()."""
63 def setup_server_io():
64 """Fill IO files DB with proper file( path)s. Write process IO test string.
66 Ensure IO files directory at server/. Remove any old input file if found.
67 Set up new input file for reading, and new output file for appending. Start
68 output file with process hash line of format PID + " " + floated UNIX time
69 (io_db["teststring"]). Raise SystemExit if file is found at path of either
70 record or save file plus io_db["tmp_suffix"].
72 def detect_atomic_leftover(path, tmp_suffix):
73 path_tmp = path + tmp_suffix
74 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
75 "aborted previous attempt to write '" + path + "'. Aborting " \
76 "until matter is resolved by removing it from its current path."
77 if os.access(path_tmp, os.F_OK):
79 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
80 io_db["save_wait_start"] = 0
81 io_db["verbose"] = False
82 io_db["record_chunk"] = ""
83 os.makedirs(io_db["path_server"], exist_ok=True)
84 io_db["file_out"] = open(io_db["path_out"], "a")
85 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
86 if os.access(io_db["path_in"], os.F_OK):
87 os.remove(io_db["path_in"])
88 io_db["file_in"] = open(io_db["path_in"], "w")
89 io_db["file_in"].close()
90 io_db["file_in"] = open(io_db["path_in"], "r")
91 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
92 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
95 def cleanup_server_io():
96 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
97 def helper(file_key, path_key):
99 io_db[file_key].close()
100 if not io_db["kicked_by_rival"] \
101 and os.access(io_db[path_key], os.F_OK):
102 os.remove(io_db[path_key])
103 helper("file_in", "path_in")
104 helper("file_out", "path_out")
105 helper("file_worldstate", "path_worldstate")
106 if "file_record" in io_db:
107 io_db["file_record"].close()
110 """Return next newline-delimited command from server in file.
112 Keep building return string until a newline is encountered. Pause between
113 unsuccessful reads, and after too much waiting, run server_test().
115 wait_on_fail = io_db["wait_on_read_fail"]
116 max_wait = io_db["max_wait_on_read_fail"]
120 add = io_db["file_in"].readline()
122 command = command + add
123 if len(command) > 0 and "\n" == command[-1]:
124 command = command[:-1]
127 time.sleep(wait_on_fail)
128 if now + max_wait < time.time():
135 """Send "msg" to log."""
136 strong_write(io_db["file_out"], "LOG " + msg + "\n")
140 """Save all commands needed to reconstruct current world state."""
141 from server.utils import rand
143 def quote_escape(string):
144 string = string.replace("\u005C", '\u005C\u005C')
145 return '"' + string.replace('"', '\u005C"') + '"'
150 if key == "MAP" or world_db["Things"][id][key]:
151 map = world_db["MAP"] if key == "MAP" \
152 else world_db["Things"][id][key]
153 length = world_db["MAP_LENGTH"]
154 for i in range(length):
155 line = map[i * length:(i * length) + length].decode()
156 string = string + key + " " + str(i) + " " + \
157 quote_escape(line) + "\n"
163 for memthing in world_db["Things"][id]["T_MEMTHING"]:
164 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
165 str(memthing[1]) + " " + str(memthing[2]) + "\n"
168 def helper(category, id_string, special_keys={}):
170 for _id in sorted(world_db[category].keys()):
171 string = string + id_string + " " + str(_id) + "\n"
172 for key in sorted(world_db[category][_id].keys()):
173 if not key in special_keys:
174 x = world_db[category][_id][key]
175 argument = quote_escape(x) if str == type(x) else str(x)
176 string = string + key + " " + argument + "\n"
177 elif special_keys[key]:
178 string = string + special_keys[key](_id)
183 memmap = mapsetter("T_MEMMAP")
184 memdepthmap = mapsetter("T_MEMDEPTHMAP")
185 for tid in sorted(world_db["Things"].keys()):
186 string += "T_ID " + str(tid) + "\n"
187 t = world_db["Things"][tid]
188 for key in sorted(t.keys()):
189 if key not in {"T_CARRIES", "carried", "fovmap", "T_MEMMAP",
190 "T_MEMTHING", "T_MEMDEPTHMAP"}:
192 string += key + " " + (quote_escape(argument) if \
193 str == type(argument) else str(argument)) + "\n"
194 string += memthing(tid) + memmap(tid) + memdepthmap(tid)
197 # ALTERNATIVE TO helper_things, but not more efficient despite listcomp
201 # return key + " " + (quote_escape(argument) if \
202 # str == type(argument) else str(argument)) + "\n"
204 # memmap = mapsetter("T_MEMMAP")
205 # memdepthmap = mapsetter("T_MEMDEPTHMAP")
206 # for tid in sorted(world_db["Things"].keys()):
207 # string += "T_ID " + str(tid) + "\n"
208 # t = world_db["Things"][tid]
209 # lines = [foo(t, key) for key in sorted(t.keys())
210 # if key not in {"T_CARRIES", "carried", "fovmap",
211 # "T_MEMMAP", "T_MEMTHING", "T_MEMDEPTHMAP"}]
212 # string += "".join(lines)
213 # string += memthing(tid) + memmap(tid) + memdepthmap(tid)
217 for plugin in world_db["PLUGIN"]:
218 string = string + "PLUGIN " + plugin + "\n"
219 for key in sorted(world_db.keys()):
220 if (not isinstance(world_db[key], dict) and
221 not isinstance(world_db[key], list)) and key != "MAP" and \
222 key != "WORLD_ACTIVE" and key[0].isupper():
223 string = string + key + " " + str(world_db[key]) + "\n"
224 string = string + mapsetter("MAP")()
225 string = string + helper("ThingActions", "TA_ID")
226 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
227 for id in sorted(world_db["ThingTypes"].keys()):
228 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
229 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
230 string += helper_things()
231 for tid in sorted(world_db["Things"].keys()):
232 if [] != world_db["Things"][tid]["T_CARRIES"]:
233 string = string + "T_ID " + str(tid) + "\n"
234 for carried in sorted(world_db["Things"][tid]["T_CARRIES"]):
235 string = string + "T_CARRIES " + str(carried) + "\n"
236 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
237 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
238 atomic_write(io_db["path_save"], string)
241 def obey(command, prefix, replay=False, do_record=False):
242 """Call function from commands_db mapped to command's first token.
244 Tokenize command string with shlex.split(comments=True). If replay is set,
245 a non-meta command from the commands_db merely triggers obey() on the next
246 command from the records file. If not, non-meta commands set
247 io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
248 do_record is set, are recorded to io_db["record_chunk"], and save_world()
249 is called (and io_db["record_chunk"] written) if io_db["save_wait"] seconds
250 have passed since the last time it was called. The prefix string is
251 inserted into the server's input message between its beginning 'input ' and
252 ':'. All activity is preceded by a server_test() call. Commands that start
253 with a lowercase letter are ignored when world_db["WORLD_ACTIVE"] is
257 from server.config.commands import commands_db
260 print("input " + prefix + ": " + command)
262 tokens = shlex.split(command, comments=True)
263 except ValueError as err:
264 print("Can't tokenize command string: " + str(err) + ".")
266 if len(tokens) > 0 and tokens[0] in commands_db \
267 and len(tokens) == commands_db[tokens[0]][0] + 1:
268 if commands_db[tokens[0]][1]:
269 commands_db[tokens[0]][2](*tokens[1:])
270 elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
271 print("Ignoring lowercase-starting commands when world inactive.")
273 print("Due to replay mode, reading command as 'go on in record'.")
274 line = io_db["file_record"].readline()
276 obey(line.rstrip(), io_db["file_record"].prefix
277 + str(io_db["file_record"].line_n))
278 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
280 print("Reached end of record file.")
282 commands_db[tokens[0]][2](*tokens[1:])
284 io_db["record_chunk"] += command + "\n"
285 if time.time() > io_db["save_wait_start"] + io_db["save_wait"]:
286 atomic_write(io_db["path_record"], io_db["record_chunk"],
288 if world_db["WORLD_ACTIVE"]:
290 io_db["record_chunk"] = ""
291 io_db["save_wait"] = time.time()
292 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
293 elif 0 != len(tokens):
294 print("Invalid command/argument, or bad number of tokens: " + command)
297 def obey_lines_in_file(path, name, do_record=False):
298 """Call obey() on each line of path's file, use name in input prefix."""
299 file = open(path, "r")
301 for line in file.readlines():
302 obey(line.rstrip(), name + "file line " + str(line_n),
308 def try_worldstate_update():
309 """Write worldstate file if io_db["worldstate_updateable"] is set."""
310 if io_db["worldstate_updateable"]:
312 for entry in io_db["worldstate_write_order"]:
313 if entry[1] == "world_int":
314 string += str(world_db[entry[0]]) + "\n"
315 elif entry[1] == "player_int":
316 string += str(world_db["Things"][0][entry[0]]) + "\n"
317 elif entry[1] == "func":
319 atomic_write(io_db["path_worldstate"], string, delete=False)
320 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
321 io_db["worldstate_updateable"] = False