github / src / world.py @ 0ce7e35f
1 |
# Copyright (c) 2016, LE GOFF Vincent
|
---|---|
2 |
# All rights reserved.
|
3 |
|
4 |
# Redistribution and use in source and binary forms, with or without
|
5 |
# modification, are permitted provided that the following conditions are met:
|
6 |
|
7 |
# * Redistributions of source code must retain the above copyright notice, this
|
8 |
# list of conditions and the following disclaimer.
|
9 |
|
10 |
# * Redistributions in binary form must reproduce the above copyright notice,
|
11 |
# this list of conditions and the following disclaimer in the documentation
|
12 |
# and/or other materials provided with the distribution.
|
13 |
|
14 |
# * Neither the name of ytranslate nor the names of its
|
15 |
# contributors may be used to endorse or promote products derived from
|
16 |
# this software without specific prior written permission.
|
17 |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22 |
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28 |
|
29 |
"""This file contains the World class."""
|
30 |
|
31 |
from enum import Enum |
32 |
import shutil |
33 |
import os |
34 |
import re |
35 |
from io import StringIO |
36 |
from textwrap import dedent |
37 |
from threading import RLock |
38 |
|
39 |
from configobj import ConfigObj, ParseError |
40 |
from ytranslate import t |
41 |
|
42 |
from character import Character |
43 |
from log import sharp as logger |
44 |
from notepad import Notepad |
45 |
from screenreader import ScreenReader |
46 |
from session import Session |
47 |
|
48 |
class MergingMethod(Enum): |
49 |
|
50 |
"""Enumeration to represent merging methods."""
|
51 |
|
52 |
ignore = 1
|
53 |
replace = 2
|
54 |
|
55 |
|
56 |
class World: |
57 |
|
58 |
"""A class representing a World object.
|
59 |
|
60 |
A world is a game (a server). It conatins a hostname and a port
|
61 |
and optionally characters.
|
62 |
|
63 |
"""
|
64 |
|
65 |
def __init__(self, location): |
66 |
self.location = location
|
67 |
self.name = "" |
68 |
self.hostname = "" |
69 |
self.port = 4000 |
70 |
self.protocol = "telnet" |
71 |
self.characters = {}
|
72 |
self.settings = None |
73 |
self.lock = RLock()
|
74 |
|
75 |
# World's access to general data
|
76 |
self.engine = None |
77 |
self.sharp_engine = None |
78 |
|
79 |
# World's configuration
|
80 |
self.aliases = []
|
81 |
self.channels = []
|
82 |
self.macros = []
|
83 |
self.triggers = []
|
84 |
self.notepad = None |
85 |
self.merging = MergingMethod.ignore
|
86 |
|
87 |
# Auto completion
|
88 |
self.words = {}
|
89 |
self.ac_choices = []
|
90 |
|
91 |
def __repr__(self): |
92 |
return "<World {} (hostname={}, port={})>".format( |
93 |
self.name, self.hostname, self.port) |
94 |
|
95 |
@property
|
96 |
def path(self): |
97 |
"""Return the path to the world."""
|
98 |
return os.path.join(self.engine.config_dir, "worlds", self.location) |
99 |
|
100 |
def load(self): |
101 |
"""Load the config.set script."""
|
102 |
from game import Level |
103 |
level = self.engine.level
|
104 |
self.engine.level = Level.world
|
105 |
to_save = False
|
106 |
|
107 |
# Reset some of the world's configuration
|
108 |
self.aliases = []
|
109 |
self.channels = []
|
110 |
self.macros = []
|
111 |
self.triggers = []
|
112 |
|
113 |
path = self.path
|
114 |
path = os.path.join(path, "config.set")
|
115 |
if os.path.exists(path):
|
116 |
file = open(path, "r", encoding="utf-8") |
117 |
content = file.read()
|
118 |
file.close()
|
119 |
|
120 |
# Execute the script
|
121 |
self.sharp_engine.execute(content, variables=False) |
122 |
|
123 |
# Put the engine level back
|
124 |
self.engine.level = level
|
125 |
|
126 |
if to_save:
|
127 |
self.save()
|
128 |
|
129 |
def load_characters(self): |
130 |
"""Load the characters."""
|
131 |
location = self.path
|
132 |
for directory in os.listdir(location): |
133 |
if os.path.isdir(os.path.join(location, directory)) and \ |
134 |
os.path.exists(os.path.join(location, |
135 |
directory, ".passphrase")):
|
136 |
character = Character(self, directory)
|
137 |
logger.info("Loading the character {} from the world " \
|
138 |
"{}".format(directory, self.name)) |
139 |
character.load() |
140 |
self.characters[directory] = character
|
141 |
|
142 |
def save(self): |
143 |
"""Save the world in its configuration file."""
|
144 |
if not os.path.exists(self.path): |
145 |
os.mkdir(self.path)
|
146 |
|
147 |
spec = dedent("""
|
148 |
[connection]
|
149 |
name = "unknown"
|
150 |
hostname = "unknown.ext"
|
151 |
port = 0
|
152 |
protocol = "telnet"
|
153 |
""").strip("\n") |
154 |
|
155 |
if self.settings is None: |
156 |
try:
|
157 |
self.settings = ConfigObj(spec.split("\n"), encoding="utf-8") |
158 |
except (ParseError, UnicodeError): |
159 |
logger.warning("Error while parsing the config file, " \
|
160 |
"trying without encoding")
|
161 |
self.settings = ConfigObj(spec.split("\n")) |
162 |
self.settings.encoding = "utf-8" |
163 |
self.settings.write()
|
164 |
|
165 |
connection = self.settings["connection"] |
166 |
connection["name"] = self.name |
167 |
connection["hostname"] = self.hostname |
168 |
connection["port"] = self.port |
169 |
connection["protocol"] = self.protocol |
170 |
self.settings.filename = os.path.join(self.path, "options.conf") |
171 |
self.settings.write()
|
172 |
self.save_config()
|
173 |
|
174 |
def save_config(self): |
175 |
"""Save the 'config.set' script file."""
|
176 |
lines = [] |
177 |
|
178 |
# Aliases
|
179 |
for alias in self.aliases: |
180 |
lines.append(alias.sharp_script) |
181 |
|
182 |
# Channels
|
183 |
for channel in self.channels: |
184 |
lines.append("#channel {{{}}}".format(channel.name))
|
185 |
|
186 |
# Macros
|
187 |
for macro in self.macros: |
188 |
lines.append(macro.sharp_script) |
189 |
|
190 |
# Triggers
|
191 |
for trigger in self.triggers: |
192 |
lines.append(trigger.sharp_script) |
193 |
|
194 |
content = "\n".join(lines) + "\n" |
195 |
path = self.path
|
196 |
path = os.path.join(path, "config.set")
|
197 |
file = open(path, "w") |
198 |
file.write(content)
|
199 |
file.close()
|
200 |
|
201 |
def remove(self): |
202 |
"""Remove the world."""
|
203 |
shutil.rmtree(self.path)
|
204 |
|
205 |
def add_character(self, location, name=None): |
206 |
"""Add a new character in the world."""
|
207 |
name = name or location
|
208 |
character = Character(self, location)
|
209 |
character.name = name |
210 |
character.save() |
211 |
self.characters[name] = character
|
212 |
return character
|
213 |
|
214 |
def add_alias(self, alias): |
215 |
"""Add the alias to the world's configuration, handling conflicts.
|
216 |
|
217 |
If another alias with the same name exists, either replace
|
218 |
it or ignore the second one.
|
219 |
|
220 |
"""
|
221 |
for existing in self.aliases: |
222 |
if existing.alias == alias.alias:
|
223 |
# There's a conflict, look at the 'merging' setting
|
224 |
if self.merging == MergingMethod.ignore: |
225 |
return
|
226 |
elif self.merging == MergingMethod.replace: |
227 |
existing.action = alias.action |
228 |
existing.level = alias.level |
229 |
return
|
230 |
|
231 |
# Otherwise, just add it at the end
|
232 |
self.aliases.append(alias)
|
233 |
|
234 |
def add_channel(self, channel): |
235 |
"""Add a channel, handling conflicts."""
|
236 |
for existing in self.channels: |
237 |
if existing.name == channel.name:
|
238 |
return
|
239 |
|
240 |
# Otherwise, just add it at the end
|
241 |
self.channels.append(channel)
|
242 |
|
243 |
def add_macro(self, macro): |
244 |
"""Add the macro to the world's configuration, handling conflicts.
|
245 |
|
246 |
If another macro with the same shortcut exists, either replace
|
247 |
it or ignore the second one.
|
248 |
|
249 |
"""
|
250 |
for existing in self.macros: |
251 |
if existing.shortcut == macro.shortcut:
|
252 |
# There's a conflict, look at the 'merging' setting
|
253 |
if self.merging == MergingMethod.ignore: |
254 |
return
|
255 |
elif self.merging == MergingMethod.replace: |
256 |
existing.action = macro.action |
257 |
existing.level = macro.level |
258 |
return
|
259 |
|
260 |
# Otherwise, just add it at the end
|
261 |
self.macros.append(macro)
|
262 |
|
263 |
def add_trigger(self, trigger): |
264 |
"""Add the trigger to the world's configuration, handling conflicts.
|
265 |
|
266 |
If another trigger with the same reaction exists, either replace
|
267 |
it or ignore the second one.
|
268 |
|
269 |
"""
|
270 |
for existing in self.triggers: |
271 |
if existing.reaction == trigger.reaction:
|
272 |
# There's a conflict, look at the 'merging' setting
|
273 |
if self.merging == MergingMethod.ignore: |
274 |
return
|
275 |
elif self.merging == MergingMethod.replace: |
276 |
existing.action = trigger.action |
277 |
existing.mute = trigger.mute |
278 |
existing.level = trigger.level |
279 |
return
|
280 |
|
281 |
# Otherwise, just add it at the end
|
282 |
self.triggers.append(trigger)
|
283 |
|
284 |
def reset_autocompletion(self): |
285 |
"""Erase the list of possible choices in for the auto completion."""
|
286 |
self.ac_choices[:] = []
|
287 |
|
288 |
def feed_words(self, text): |
289 |
"""Add new words using the provided text.
|
290 |
|
291 |
Each word in this text will be added to the list of words for
|
292 |
a future auto-completion.
|
293 |
|
294 |
"""
|
295 |
for word in re.findall(r"(\w+)", text, re.UNICODE): |
296 |
word = word.lower() |
297 |
count = self.words.get(word, 0) |
298 |
count += 1
|
299 |
self.words[word] = count
|
300 |
|
301 |
def find_word(self, word, TTS=False): |
302 |
"""Find the most likely word for auto-completion."""
|
303 |
matches = {} |
304 |
word = word.lower() |
305 |
for potential, count in self.words.items(): |
306 |
if potential.startswith(word):
|
307 |
if potential not in self.ac_choices: |
308 |
matches[potential] = count |
309 |
|
310 |
# Sort through the most common
|
311 |
for potential, count in sorted(matches.items(), |
312 |
key=lambda tup: tup[1], reverse=True): |
313 |
self.ac_choices.append(potential)
|
314 |
if TTS:
|
315 |
ScreenReader.talk(potential) |
316 |
return potential
|
317 |
|
318 |
return None |
319 |
|
320 |
def create_session(self, client): |
321 |
"""Create a session attached to this world."""
|
322 |
session = Session(client, self)
|
323 |
session.engine = self.engine
|
324 |
session.sharp_engine = self.sharp_engine
|
325 |
return session
|
326 |
|
327 |
def open_notepad(self): |
328 |
"""Open and return the notepad associated to this world."""
|
329 |
if self.notepad: |
330 |
return self.notepad |
331 |
|
332 |
self.notepad = Notepad(self) |
333 |
empty_string = t("ui.message.notepad.world_empty", world=self.name) |
334 |
self.notepad.open(empty_string)
|
335 |
return self.notepad |
336 |
|
337 |
@classmethod
|
338 |
def get_infos(cls, configuration): |
339 |
"""Get the information in the configuration and return a dict."""
|
340 |
config = ConfigObj(StringIO(configuration), encoding="utf-8")
|
341 |
data = {} |
342 |
|
343 |
for key, value in config.items(): |
344 |
if key == "port": |
345 |
try:
|
346 |
value = int(value)
|
347 |
except ValueError: |
348 |
pass
|
349 |
|
350 |
data[key] = value |
351 |
|
352 |
return data
|
- « Previous
- 1
- …
- 18
- 19
- 20
- Next »