home · contact · privacy
Change plugin naming convention.
[plomrogue] / server / commands.py
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.
4
5
6 from server.config.world_data import world_db
7 from server.config.io import io_db
8 from server.io import log, strong_write 
9 from server.utils import integer_test, id_setter
10 from server.world import build_fov_map, update_map_memory, set_world_inactive,\
11         turn_over
12
13
14 def command_plugin(str_plugin):
15     """Run code in plugins/[str_plugin]."""
16     import os
17     if (str_plugin.replace("_", "").isalnum()
18         and os.access("plugins/server/" + str_plugin + ".py", os.F_OK)):
19         exec(open("plugins/server/" + str_plugin + ".py").read())
20         world_db["PLUGIN"] += [str_plugin]
21         return
22     print("Bad plugin name:", str_plugin)
23
24
25 def command_ping():
26     """Send PONG line to server output file."""
27     strong_write(io_db["file_out"], "PONG\n")
28
29
30 def command_quit():
31     """Abort server process."""
32     from server.io import save_world, atomic_write
33     from server.utils import opts
34     if None == opts.replay:
35         if world_db["WORLD_ACTIVE"]:
36             save_world()
37         atomic_write(io_db["path_record"], io_db["record_chunk"],
38             do_append=True)
39     raise SystemExit("received QUIT command")
40
41
42 def command_thingshere(str_y, str_x):
43     """Write to out file list of Things known to player at coordinate y, x."""
44     if world_db["WORLD_ACTIVE"]:
45         y = integer_test(str_y, 0, 255)
46         x = integer_test(str_x, 0, 255)
47         length = world_db["MAP_LENGTH"]
48         if None != y and None != x and y < length and x < length:
49             pos = (y * world_db["MAP_LENGTH"]) + x
50             strong_write(io_db["file_out"], "THINGS_HERE START\n")
51             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
52                 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
53                               for id in world_db["Things"]
54                               if not world_db["Things"][id]["carried"]
55                               if world_db["Things"][id]["T_TYPE"] == tid
56                               if y == world_db["Things"][id]["T_POSY"]
57                               if x == world_db["Things"][id]["T_POSX"]]:
58                     type = world_db["Things"][id]["T_TYPE"]
59                     name = world_db["ThingTypes"][type]["TT_NAME"]
60                     strong_write(io_db["file_out"], name + "\n")
61             else:
62                 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
63                               for mt in world_db["Things"][0]["T_MEMTHING"]
64                               if mt[0] == tid if y == mt[1] if x == mt[2]]:
65                     name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
66                     strong_write(io_db["file_out"], name + "\n")
67             strong_write(io_db["file_out"], "THINGS_HERE END\n")
68         else:
69             print("Ignoring: Invalid map coordinates.")
70     else:
71         print("Ignoring: Command only works on existing worlds.")
72
73
74 def command_seedrandomness(seed_string):
75     """Set rand seed to int(seed_string)."""
76     from server.utils import rand
77     val = integer_test(seed_string, 0, 4294967295)
78     if None != val:
79         rand.seed = val
80
81
82 def command_makeworld(seed_string):
83     """Call make_world()."""
84     val = integer_test(seed_string, 0, 4294967295)
85     if None != val:
86         from server.world import make_world
87         make_world(val)
88
89
90 def command_maplength(maplength_string):
91     """Redefine map length. Invalidate map, therefore lose all things on it."""
92     val = integer_test(maplength_string, 1, 256)
93     if None != val:
94         from server.utils import libpr
95         world_db["MAP_LENGTH"] = val
96         world_db["MAP"] = False
97         set_world_inactive()
98         world_db["Things"] = {}
99         libpr.set_maplength(val)
100
101
102 def command_worldactive(worldactive_string):
103     """Toggle world_db["WORLD_ACTIVE"] if possible.
104
105     An active world can always be set inactive. An inactive world can only be
106     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
107     map. On activation, rebuild all Things' FOVs, and the player's map memory.
108     """
109     val = integer_test(worldactive_string, 0, 1)
110     if None != val:
111         if 0 != world_db["WORLD_ACTIVE"]:
112             if 0 == val:
113                 set_world_inactive()
114             else:
115                 print("World already active.")
116         elif 0 == world_db["WORLD_ACTIVE"]:
117             wait_exists = False
118             for ThingAction in world_db["ThingActions"]:
119                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
120                     wait_exists = True
121                     break
122             player_exists = False
123             for Thing in world_db["Things"]:
124                 if 0 == Thing:
125                     player_exists = True
126                     break
127             if wait_exists and player_exists and world_db["MAP"]:
128                 for id in world_db["Things"]:
129                     if world_db["Things"][id]["T_LIFEPOINTS"]:
130                         build_fov_map(world_db["Things"][id])
131                         if 0 == id:
132                             update_map_memory(world_db["Things"][id], False)
133                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
134                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
135                     world_db["Things"][0]["fovmap"] = empty_fovmap
136                 world_db["WORLD_ACTIVE"] = 1
137             else:
138                 print("Ignoring: Not all conditions for world activation met.")
139
140
141 def command_tid(id_string):
142     """Set ID of Thing to manipulate. ID unused? Create new one.
143
144     Default new Thing's type to the first available ThingType, others: zero.
145     """
146     id = id_setter(id_string, "Things", command_tid)
147     if None != id:
148         if world_db["ThingTypes"] == {}:
149             print("Ignoring: No ThingType to settle new Thing in.")
150             return
151         type = list(world_db["ThingTypes"].keys())[0]
152         from server.world import new_Thing
153         world_db["Things"][id] = new_Thing(type)
154
155
156 def command_ttid(id_string):
157     """Set ID of ThingType to manipulate. ID unused? Create new one.
158
159     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
160     "", others: 0. 
161     """
162     id = id_setter(id_string, "ThingTypes", command_ttid)
163     if None != id:
164         world_db["ThingTypes"][id] = {
165             "TT_NAME": "(none)",
166             "TT_TOOLPOWER": 0,
167             "TT_LIFEPOINTS": 0,
168             "TT_PROLIFERATE": 0,
169             "TT_START_NUMBER": 0,
170             "TT_SYMBOL": "?",
171             "TT_CORPSE_ID": id,
172             "TT_TOOL": ""
173         }
174
175
176 def command_taid(id_string):
177     """Set ID of ThingAction to manipulate. ID unused? Create new one.
178
179     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
180     """
181     id = id_setter(id_string, "ThingActions", command_taid, True)
182     if None != id:
183         world_db["ThingActions"][id] = {
184             "TA_EFFORT": 1,
185             "TA_NAME": "wait"
186         }
187
188
189 def test_for_id_maker(object, category):
190     """Return decorator testing for object having "id" attribute."""
191     def decorator(f):
192         def helper(*args):
193             if hasattr(object, "id"):
194                 f(*args)
195             else:
196                 print("Ignoring: No " + category +
197                       " defined to manipulate yet.")
198         return helper
199     return decorator
200
201
202 test_Thing_id = test_for_id_maker(command_tid, "Thing")
203 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
204 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
205
206
207 @test_Thing_id
208 def command_tcommand(str_int):
209     """Set T_COMMAND of selected Thing."""
210     val = integer_test(str_int, 0)
211     if None != val:
212         if 0 == val or val in world_db["ThingActions"]:
213             world_db["Things"][command_tid.id]["T_COMMAND"] = val
214         else:
215             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
216
217
218 @test_Thing_id
219 def command_ttype(str_int):
220     """Set T_TYPE of selected Thing."""
221     val = integer_test(str_int, 0)
222     if None != val:
223         if val in world_db["ThingTypes"]:
224             world_db["Things"][command_tid.id]["T_TYPE"] = val
225         else:
226             print("Ignoring: ThingType ID belongs to no known ThingType.")
227
228
229 @test_Thing_id
230 def command_tcarries(str_int):
231     """Append int(str_int) to T_CARRIES of selected Thing.
232
233     The ID int(str_int) must not be of the selected Thing, and must belong to a
234     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
235     """
236     val = integer_test(str_int, 0)
237     if None != val:
238         if val == command_tid.id:
239             print("Ignoring: Thing cannot carry itself.")
240         elif val in world_db["Things"] \
241                 and not world_db["Things"][val]["carried"]:
242             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
243             world_db["Things"][val]["carried"] = True
244         else:
245             print("Ignoring: Thing not available for carrying.")
246     # Note that the whole carrying structure is different from the C version:
247     # Carried-ness is marked by a "carried" flag, not by Things containing
248     # Things internally.
249
250
251 @test_Thing_id
252 def command_tmemthing(str_t, str_y, str_x):
253     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
254
255     The type must fit to an existing ThingType, and the position into the map.
256     """
257     type = integer_test(str_t, 0)
258     posy = integer_test(str_y, 0, 255)
259     posx = integer_test(str_x, 0, 255)
260     if None != type and None != posy and None != posx:
261         if type not in world_db["ThingTypes"] \
262            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
263             print("Ignoring: Illegal value for thing type or position.")
264         else:
265             memthing = (type, posy, posx)
266             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
267
268
269 @test_ThingType_id
270 def command_ttname(name):
271     """Set TT_NAME of selected ThingType."""
272     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
273
274
275 @test_ThingType_id
276 def command_tttool(name):
277     """Set TT_TOOL of selected ThingType."""
278     world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
279
280
281 @test_ThingType_id
282 def command_ttsymbol(char):
283     """Set TT_SYMBOL of selected ThingType. """
284     if 1 == len(char):
285         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
286     else:
287         print("Ignoring: Argument must be single character.")
288
289
290 @test_ThingType_id
291 def command_ttcorpseid(str_int):
292     """Set TT_CORPSE_ID of selected ThingType."""
293     val = integer_test(str_int, 0)
294     if None != val:
295         if val in world_db["ThingTypes"]:
296             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
297         else:
298             print("Ignoring: Corpse ID belongs to no known ThignType.")
299
300
301 @test_ThingAction_id
302 def command_taname(name):
303     """Set TA_NAME of selected ThingAction.
304
305     The name must match a valid thing action function. If after the name
306     setting no ThingAction with name "wait" remains, call set_world_inactive().
307     """
308     if name == "wait" or name == "move" or name == "use" or name == "drop" \
309        or name == "pickup":
310         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
311         if 1 == world_db["WORLD_ACTIVE"]:
312             wait_defined = False
313             for id in world_db["ThingActions"]:
314                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
315                     wait_defined = True
316                     break
317             if not wait_defined:
318                 set_world_inactive()
319     else:
320         print("Ignoring: Invalid action name.")
321     # In contrast to the original,naming won't map a function to a ThingAction.
322
323
324 def setter(category, key, min, max=None):
325     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
326     if category is None:
327         def f(val_string):
328             val = integer_test(val_string, min, max)
329             if None != val:
330                 world_db[key] = val
331     else:
332         if category == "Thing":
333             id_store = command_tid
334             decorator = test_Thing_id
335         elif category == "ThingType":
336             id_store = command_ttid
337             decorator = test_ThingType_id
338         elif category == "ThingAction":
339             id_store = command_taid
340             decorator = test_ThingAction_id
341
342         @decorator
343         def f(val_string):
344             val = integer_test(val_string, min, max)
345             if None != val:
346                 world_db[category + "s"][id_store.id][key] = val
347     return f
348
349
350 def setter_map(maptype):
351     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
352
353     If no map of maptype exists yet, initialize it with ' ' bytes first.
354     """
355
356     def valid_map_line(str_int, mapline):
357         val = integer_test(str_int, 0, 255)
358         if None != val:
359             if val >= world_db["MAP_LENGTH"]:
360                 print("Illegal value for map line number.")
361             elif len(mapline) != world_db["MAP_LENGTH"]:
362                 print("Map line length is unequal map width.")
363             else:
364                 return val
365         return None
366
367     def nonThingMap_helper(str_int, mapline):
368         val = valid_map_line(str_int, mapline)
369         if None != val:
370             length = world_db["MAP_LENGTH"]
371             if not world_db["MAP"]:
372                 map = bytearray(b' ' * (length ** 2))
373             else:
374                 map = world_db["MAP"]
375             map[val * length:(val * length) + length] = mapline.encode()
376             if not world_db["MAP"]:
377                 world_db["MAP"] = map
378
379     @test_Thing_id
380     def ThingMap_helper(str_int, mapline):
381         val = valid_map_line(str_int, mapline)
382         if None != val:
383             length = world_db["MAP_LENGTH"]
384             if not world_db["Things"][command_tid.id][maptype]:
385                 map = bytearray(b' ' * (length ** 2))
386             else:
387                 map = world_db["Things"][command_tid.id][maptype]
388             map[val * length:(val * length) + length] = mapline.encode()
389             if not world_db["Things"][command_tid.id][maptype]:
390                 world_db["Things"][command_tid.id][maptype] = map
391
392     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
393
394
395
396 def setter_tpos(axis):
397     """Generate setter for T_POSX or  T_POSY of selected Thing.
398
399     If world is active, rebuilds animate things' fovmap, player's memory map.
400     """
401     @test_Thing_id
402     def helper(str_int):
403         val = integer_test(str_int, 0, 255)
404         if None != val:
405             if val < world_db["MAP_LENGTH"]:
406                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
407                 if world_db["WORLD_ACTIVE"] \
408                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
409                     build_fov_map(world_db["Things"][command_tid.id])
410                     if 0 == command_tid.id:
411                         update_map_memory(world_db["Things"][command_tid.id])
412             else:
413                 print("Ignoring: Position is outside of map.")
414     return helper
415
416
417 def set_command(action):
418     """Set player's T_COMMAND, then call turn_over()."""
419     id = [x for x in world_db["ThingActions"]
420           if world_db["ThingActions"][x]["TA_NAME"] == action][0]
421     world_db["Things"][0]["T_COMMAND"] = id
422     turn_over()
423
424
425 def play_wait():
426     """Try "wait" as player's T_COMMAND."""
427     set_command("wait")
428
429
430 def action_exists(action):
431     matching_actions = [x for x in world_db["ThingActions"]
432                         if world_db["ThingActions"][x]["TA_NAME"] == action]
433     if len(matching_actions) >= 1:
434         return True
435     print("No appropriate ThingAction defined.")
436     return False
437
438
439 def play_pickup():
440     """Try "pickup" as player's T_COMMAND"."""
441     if action_exists("pickup"):
442         t = world_db["Things"][0]
443         ids = [id for id in world_db["Things"] if id
444                if not world_db["Things"][id]["carried"]
445                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
446                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
447         if not len(ids):
448              log("NOTHING to pick up.")
449         else:
450             set_command("pickup")
451
452
453 def play_drop(str_arg):
454     """Try "drop" as player's T_COMMAND, int(str_arg) as T_ARGUMENT / slot."""
455     if action_exists("drop"):
456         t = world_db["Things"][0]
457         if 0 == len(t["T_CARRIES"]):
458             log("You have NOTHING to drop in your inventory.")
459         else:
460             val = integer_test(str_arg, 0, 255)
461             if None != val and val < len(t["T_CARRIES"]):
462                 world_db["Things"][0]["T_ARGUMENT"] = val
463                 set_command("drop")
464             else:
465                 print("Illegal inventory index.")
466
467
468 def play_use(str_arg):
469     """Try "use" as player's T_COMMAND, int(str_arg) as T_ARGUMENT / slot."""
470     if action_exists("use"):
471         t = world_db["Things"][0]
472         if 0 == len(t["T_CARRIES"]):
473             log("You have NOTHING to use in your inventory.")
474         else:
475             val = integer_test(str_arg, 0, 255)
476             if None != val and val < len(t["T_CARRIES"]):
477                 id = t["T_CARRIES"][val]
478                 type = world_db["Things"][id]["T_TYPE"]
479                 if not world_db["ThingTypes"][type]["TT_TOOL"] == "food":
480                     log("You CAN'T consume this thing.")
481                     return
482                 world_db["Things"][0]["T_ARGUMENT"] = val
483                 set_command("use")
484             else:
485                 print("Illegal inventory index.")
486
487
488 def play_move(str_arg):
489     """Try "move" as player's T_COMMAND, str_arg as T_ARGUMENT / direction."""
490     if action_exists("move"):
491         from server.config.world_data import directions_db
492         t = world_db["Things"][0]
493         if not str_arg in directions_db:
494             print("Illegal move direction string.")
495             return
496         dir = ord(directions_db[str_arg])
497         from server.utils import mv_yx_in_dir_legal
498         move_result = mv_yx_in_dir_legal(chr(dir), t["T_POSY"], t["T_POSX"])
499         if 1 == move_result[0]:
500             pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
501             if ord(".") == world_db["MAP"][pos]:
502                 world_db["Things"][0]["T_ARGUMENT"] = dir
503                 set_command("move")
504                 return
505         log("You CAN'T move there.")
506
507
508 def command_ai():
509     """Call ai() on player Thing, then turn_over()."""
510     from server.ai import ai
511     ai(world_db["Things"][0])
512     turn_over()