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