Package horizons :: Package scenario :: Module scenarioeventhandler
[hide private]
[frames] | no frames]

Source Code for Module horizons.scenario.scenarioeventhandler

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import copy 
 23  import json 
 24   
 25  import yaml 
 26   
 27  from horizons.i18n import gettext as T 
 28  from horizons.scheduler import Scheduler 
 29  from horizons.util.living import LivingObject 
 30  from horizons.util.python.callback import Callback 
 31  from horizons.util.yamlcache import YamlCache 
 32   
 33  from .actions import ACTIONS 
 34  from .conditions import CONDITIONS 
35 36 37 -class InvalidScenarioFileFormat(Exception):
38 - def __init__(self, msg=None):
39 if msg is None: 40 msg = "Invalid scenario file." 41 super().__init__(msg)
42
43 44 -class ScenarioEventHandler(LivingObject):
45 """Handles event, that make up a scenario. See wiki. 46 An instance of this class is bound to a set of events. On a new scenario, you need a new instance. 47 48 Scenarios consist of condition-action events. 49 When all conditions of an event become true, the action is executed and the event is 50 removed from the scenario. All events only happen once. 51 52 Whenever the game state changes in a way, that can change the truth value of a condition, 53 the event handler must be notified. It will then check all relevant events. 54 It is imperative for this notification to always be triggered, else the scenario gets stuck. 55 For conditions, where this approach doesn't make sense (e.g. too frequent changes), 56 a periodic check can be used. 57 58 Save/load works by dumping all info into a yaml string in the savegame, 59 which is loaded just like normal scenarios are loaded. 60 """ 61 62 CHECK_CONDITIONS_INTERVAL = 3 # seconds 63 64 PICKLE_PROTOCOL = 2 65
66 - def __init__(self, session, scenariofile=None):
67 """ 68 @param session: Session instance 69 @param scenariofile: yaml file that describes the scenario 70 @throws InvalidScenarioFileFormat on yaml parse error 71 """ 72 self.inited = False 73 self.session = session 74 self._events = [] 75 self._data = {} 76 # map: condition types -> events 77 self._event_conditions = {} 78 self._scenario_variables = {} # variables for set_var, var_eq ... 79 for cond in CONDITIONS.registry.keys(): 80 self._event_conditions[cond] = set() 81 if scenariofile: 82 self._apply_data(self._parse_yaml_file(scenariofile)) 83 84 self.sleep_ticks_remaining = 0
85
86 - def start(self):
87 # Add the check_events method to the scheduler to be checked every few seconds 88 self.check_events("game_started") 89 Scheduler().add_new_object(self._scheduled_check, self, 90 run_in=Scheduler().get_ticks(self.CHECK_CONDITIONS_INTERVAL), 91 loops=-1)
92
93 - def sleep(self, ticks):
94 """Sleep the ScenarioEventHandler for number of ticks. This delays all 95 callbacks by the specific amount""" 96 callbacks = Scheduler().get_classinst_calls(self) 97 for callback in callbacks: 98 Scheduler().rem_object(callback) 99 callback.run_in = callback.run_in + ticks 100 Scheduler().add_object(callback) 101 self.sleep_ticks_remaining = ticks 102 Scheduler().add_new_object(self._reduce_sleep, self, loops=ticks)
103
104 - def _reduce_sleep(self):
105 self.sleep_ticks_remaining -= 1
106
107 - def end(self):
108 Scheduler().rem_all_classinst_calls(self) 109 self.session = None 110 self._events = None 111 self._data = None
112
113 - def save(self, db):
114 if self.inited: # only save in case we have data applied 115 db("INSERT INTO metadata(name, value) VALUES(?, ?)", "scenario_events", self.to_yaml()) 116 for key, value in self._scenario_variables.items(): 117 db("INSERT INTO scenario_variables(key, value) VALUES(?, ?)", key, 118 json.dumps(value))
119
120 - def load(self, db):
121 for key, value in db("SELECT key, value FROM scenario_variables"): 122 self._scenario_variables[key] = json.loads(value) 123 data = db("SELECT value FROM metadata WHERE name = ?", "scenario_events") 124 if not data: 125 return # nothing to load 126 self._apply_data(self._parse_yaml(data[0][0]))
127
128 - def schedule_check(self, condition):
129 """Let check_events run in one tick for condition. Useful for lag prevetion if time is a 130 critical factor, e.g. when the user has to wait for a function to return..""" 131 if self.session.world.inited: # don't check while loading 132 Scheduler().add_new_object(Callback(self.check_events, condition), self, run_in=self.sleep_ticks_remaining)
133
134 - def schedule_action(self, action):
135 if self.sleep_ticks_remaining > 0: 136 Scheduler().add_new_object(Callback(action, self.session), self, run_in=self.sleep_ticks_remaining) 137 else: 138 action(self.session)
139
140 - def check_events(self, condition):
141 """Checks whether an event happened. 142 @param condition: condition from enum conditions that changed""" 143 if not self.session.world.inited: # don't check while loading 144 return 145 events_to_remove = [] 146 for event in self._event_conditions[condition]: 147 event_executed = event.check(self) 148 if event_executed: 149 events_to_remove.append(event) 150 for event in events_to_remove: 151 self._remove_event(event)
152
153 - def get_map_file(self):
154 try: 155 return self._data['metadata']['mapfile'] 156 except KeyError: 157 # Old scenario format 158 return self._data['mapfile']
159 160 @classmethod
161 - def get_metadata_from_file(cls, filename):
162 """Returns metadata dictionary from a yaml scenario file. 163 164 Dictionary contains "unknown" for all of these fields if not specified 165 in the scenario file: 166 - difficulty 167 - author 168 - description 169 170 @throws InvalidScenarioFileFormat on yaml parse error 171 """ 172 fallback = T('unknown') 173 metadata = cls._parse_yaml_file(filename).get('metadata', {}) 174 for required_key in ('author', 'difficulty', 'description'): 175 metadata.setdefault(required_key, fallback) 176 return metadata
177
178 - def drop_events(self):
179 """Removes all events. Useful when player lost.""" 180 while self._events: 181 self._remove_event(self._events[0])
182 183 @staticmethod
184 - def _parse_yaml(string_or_stream):
185 try: 186 return YamlCache.load_yaml_data(string_or_stream) 187 except Exception as e: # catch anything yaml or functions that yaml calls might throw 188 raise InvalidScenarioFileFormat(str(e))
189 190 @classmethod
191 - def _parse_yaml_file(cls, filename):
193
194 - def _apply_data(self, data):
195 """Apply data to self loaded via from yaml 196 @param data: return value of yaml.load or _parse_yaml resp. 197 """ 198 self._data = data 199 for event_dict in self._data['events']: 200 event = _Event(self.session, event_dict) 201 self._events.append(event) 202 for cond in event.conditions: 203 self._event_conditions[cond.cond_type].add(event) 204 self.inited = True
205
206 - def _scheduled_check(self):
207 """Check conditions that can only be checked periodically""" 208 for cond_type in CONDITIONS.check_periodically: 209 self.check_events(cond_type)
210
211 - def _remove_event(self, event):
212 assert isinstance(event, _Event) 213 for cond in event.conditions: 214 # we have to use discard here, since cond.cond_type might be the same 215 # for multiple conditions of event 216 self._event_conditions[cond.cond_type].discard(event) 217 self._events.remove(event)
218
219 - def to_yaml(self):
220 """Returns yaml representation of current state of self. 221 Another object of this type, constructed with the return value of this function, has 222 to result in the very same object.""" 223 # every data except events are static, so reuse old data 224 data = copy.deepcopy(self._data) 225 del data['events'] 226 yaml_code = dump_dict_to_yaml(data) 227 # remove last } so we can add stuff 228 yaml_code = yaml_code.rsplit('}\n', 1)[0] 229 yaml_code += ", events: [ {} ] }}".format(', '.join(event.to_yaml() for event in self._events)) 230 return yaml_code
231
232 233 ### 234 # Simple utility classes 235 236 -def assert_type(var, expected_type, name):
237 if not isinstance(var, expected_type): 238 raise InvalidScenarioFileFormat('{} should be a {}, but is: {}'.format( 239 name, expected_type.__name__, str(var)))
240
241 242 -class _Event:
243 """Internal data structure representing an event."""
244 - def __init__(self, session, event_dict):
245 self.session = session 246 self.actions = [] 247 self.conditions = [] 248 assert_type(event_dict['actions'], list, "actions") 249 for action_dict in event_dict['actions']: 250 self.actions.append(_Action(action_dict)) 251 assert_type(event_dict['conditions'], list, "conditions") 252 for cond_dict in event_dict['conditions']: 253 self.conditions.append(_Condition(session, cond_dict))
254
255 - def check(self, scenarioeventhandler):
256 for cond in self.conditions: 257 if not cond(): 258 return False 259 for action in self.actions: 260 scenarioeventhandler.schedule_action(action) 261 return True
262
263 - def to_yaml(self):
264 """Returns yaml representation of self""" 265 return '{{ actions: [ {} ] , conditions: [ {} ] }}'.format( 266 ', '.join(action.to_yaml() for action in self.actions), 267 ', '.join(cond.to_yaml() for cond in self.conditions))
268
269 270 -class _Action:
271 """Internal data structure representing an ingame scenario action"""
272 - def __init__(self, action_dict):
273 assert_type(action_dict, dict, "action specification") 274 275 try: 276 self.action_type = action_dict['type'] 277 except KeyError: 278 raise InvalidScenarioFileFormat('Encountered action without type\n{}'.format(str(action_dict))) 279 try: 280 self.callback = ACTIONS.get(self.action_type) 281 except KeyError: 282 raise InvalidScenarioFileFormat('Found invalid action type: {}'.format(self.action_type)) 283 284 self.arguments = action_dict.get('arguments', [])
285
286 - def __call__(self, session):
287 """Executes action.""" 288 self.callback(session, *self.arguments)
289
290 - def to_yaml(self):
291 """Returns yaml representation of self""" 292 arguments_yaml = dump_dict_to_yaml(self.arguments) 293 return "{{ arguments: {}, type: {}}}".format(arguments_yaml, self.action_type)
294
295 296 -class _Condition:
297 """Internal data structure representing a condition""" 298
299 - def __init__(self, session, cond_dict):
300 self.session = session 301 assert_type(cond_dict, dict, "condition specification") 302 303 try: 304 self.cond_type = cond_dict['type'] 305 except KeyError: 306 raise InvalidScenarioFileFormat("Encountered condition without type\n{}".format(str(cond_dict))) 307 try: 308 self.callback = CONDITIONS.get(self.cond_type) 309 except KeyError: 310 raise InvalidScenarioFileFormat('Found invalid condition type: {}'.format(self.cond_type)) 311 312 self.arguments = cond_dict.get('arguments', [])
313
314 - def __call__(self):
315 """Check for condition. 316 @return: bool""" 317 return self.callback(self.session, *self.arguments)
318
319 - def to_yaml(self):
320 """Returns yaml representation of self""" 321 arguments_yaml = dump_dict_to_yaml(self.arguments) 322 return '{{arguments: {}, type: "{}"}}'.format(arguments_yaml, self.cond_type)
323
324 325 -def dump_dict_to_yaml(data):
326 """Wrapper for dumping yaml data using common parameters""" 327 # NOTE: the line below used to end with this: .replace('\n', '') 328 # which broke formatting of logbook messages, of course. Revert in case of problems. 329 330 # default_flow_style: makes use of short list notation without newlines (required here) 331 return yaml.safe_dump(data, line_break='\n', default_flow_style=True)
332