github / src / world.py @ master
1 |
# Copyright (c) 2016-2020, 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 |
self.loaded = False |
75 |
|
76 |
# World's access to general data
|
77 |
self.engine = None |
78 |
self.sharp_engine = None |
79 |
|
80 |
# World's configuration
|
81 |
self.aliases = []
|
82 |
self.channels = []
|
83 |
self.macros = []
|
84 |
self.triggers = []
|
85 |
self.notepad = None |
86 |
self.merging = MergingMethod.ignore
|
87 |
|
88 |
# Auto completion
|
89 |
self.words = {}
|
90 |
self.ac_choices = []
|
91 |
|
92 |
def __repr__(self): |
93 |
return "<World {} (hostname={}, port={})>".format( |
94 |
self.name, self.hostname, self.port) |
95 |
|
96 |
@property
|
97 |
def path(self): |
98 |
"""Return the path to the world."""
|
99 |
return os.path.join(self.engine.config_dir, "worlds", self.location) |
100 |
|
101 |
def load(self): |
102 |
"""Load the config.set script."""
|
103 |
if self.loaded: # The world has already been loaded, don't duplicate |
104 |
return
|
105 |
|
106 |
from game import Level |
107 |
level = self.engine.level
|
108 |
self.engine.level = Level.world
|
109 |
to_save = False
|
110 |
|
111 |
# Reset some of the world's configuration
|
112 |
self.aliases = []
|
113 |
self.channels = []
|
114 |
self.macros = []
|
115 |
self.triggers = []
|
116 |
|
117 |
path = self.path
|
118 |
path = os.path.join(path, "config.set")
|
119 |
if os.path.exists(path):
|
120 |
try:
|
121 |
with open(path, "r", encoding="utf-8") as file: |
122 |
content = file.read()
|
123 |
except UnicodeDecodeError: |
124 |
with open(path, "r", encoding="latin-1") as file: |
125 |
content = file.read()
|
126 |
to_save = True
|
127 |
|
128 |
# Execute the script
|
129 |
self.sharp_engine.execute(content, variables=False) |
130 |
|
131 |
# Put the engine level back
|
132 |
self.engine.level = level
|
133 |
self.loaded = True |
134 |
|
135 |
if to_save:
|
136 |
self.save()
|
137 |
|
138 |
def load_characters(self): |
139 |
"""Load the characters."""
|
140 |
location = self.path
|
141 |
for directory in os.listdir(location): |
142 |
if os.path.isdir(os.path.join(location, directory)) and \ |
143 |
os.path.exists(os.path.join(location, |
144 |
directory, ".passphrase")):
|
145 |
character = Character(self, directory)
|
146 |
logger.info("Loading the character {} from the world " \
|
147 |
"{}".format(directory, self.name)) |
148 |
character.load() |
149 |
self.characters[directory] = character
|
150 |
|
151 |
def save(self): |
152 |
"""Save the world in its configuration file."""
|
153 |
if not os.path.exists(self.path): |
154 |
os.mkdir(self.path)
|
155 |
|
156 |
spec = dedent("""
|
157 |
[connection]
|
158 |
name = "unknown"
|
159 |
hostname = "unknown.ext"
|
160 |
port = 0
|
161 |
protocol = "telnet"
|
162 |
""").strip("\n") |
163 |
|
164 |
if self.settings is None: |
165 |
try:
|
166 |
self.settings = ConfigObj(spec.split("\n"), encoding="utf-8") |
167 |
except (ParseError, UnicodeError): |
168 |
logger.warning("Error while parsing the config file, " \
|
169 |
"trying without encoding")
|
170 |
self.settings = ConfigObj(spec.split("\n")) |
171 |
self.settings.encoding = "utf-8" |
172 |
self.settings.write()
|
173 |
|
174 |
connection = self.settings["connection"] |
175 |
connection["name"] = self.name |
176 |
connection["hostname"] = self.hostname |
177 |
connection["port"] = self.port |
178 |
connection["protocol"] = self.protocol |
179 |
self.settings.filename = os.path.join(self.path, "options.conf") |
180 |
self.settings.write()
|
181 |
self.save_config()
|
182 |
|
183 |
def save_config(self): |
184 |
"""Save the 'config.set' script file."""
|
185 |
lines = [] |
186 |
|
187 |
# Aliases
|
188 |
for alias in self.aliases: |
189 |
lines.append(alias.sharp_script) |
190 |
|
191 |
# Channels
|
192 |
for channel in self.channels: |
193 |
lines.append("#channel {{{}}}".format(channel.name))
|
194 |
|
195 |
# Macros
|
196 |
for macro in self.macros: |
197 |
lines.append(macro.sharp_script) |
198 |
|
199 |
# Triggers
|
200 |
for trigger in self.triggers: |
201 |
lines.append(trigger.sharp_script) |
202 |
|
203 |
content = "\n".join(lines) + "\n" |
204 |
path = self.path
|
205 |
path = os.path.join(path, "config.set")
|
206 |
with open(path, "w", encoding="utf-8") as file: |
207 |
file.write(content)
|
208 |
|
209 |
def remove(self): |
210 |
"""Remove the world."""
|
211 |
shutil.rmtree(self.path)
|
212 |
|
213 |
def add_character(self, location, name=None): |
214 |
"""Add a new character in the world."""
|
215 |
name = name or location
|
216 |
character = Character(self, location)
|
217 |
character.name = name |
218 |
character.save() |
219 |
self.characters[name] = character
|
220 |
return character
|
221 |
|
222 |
def add_alias(self, alias): |
223 |
"""Add the alias to the world's configuration, handling conflicts.
|
224 |
|
225 |
If another alias with the same name exists, either replace
|
226 |
it or ignore the second one.
|
227 |
|
228 |
"""
|
229 |
for existing in self.aliases: |
230 |
if existing.alias == alias.alias:
|
231 |
# There's a conflict, look at the 'merging' setting
|
232 |
if self.merging == MergingMethod.ignore: |
233 |
return
|
234 |
elif self.merging == MergingMethod.replace: |
235 |
existing.action = alias.action |
236 |
existing.level = alias.level |
237 |
return
|
238 |
|
239 |
# Otherwise, just add it at the end
|
240 |
self.aliases.append(alias)
|
241 |
|
242 |
def add_channel(self, channel): |
243 |
"""Add a channel, handling conflicts."""
|
244 |
for existing in self.channels: |
245 |
if existing.name == channel.name:
|
246 |
return
|
247 |
|
248 |
# Otherwise, just add it at the end
|
249 |
self.channels.append(channel)
|
250 |
|
251 |
def add_macro(self, macro): |
252 |
"""Add the macro to the world's configuration, handling conflicts.
|
253 |
|
254 |
If another macro with the same shortcut exists, either replace
|
255 |
it or ignore the second one.
|
256 |
|
257 |
"""
|
258 |
for existing in self.macros: |
259 |
if existing.shortcut == macro.shortcut:
|
260 |
# There's a conflict, look at the 'merging' setting
|
261 |
if self.merging == MergingMethod.ignore: |
262 |
return
|
263 |
elif self.merging == MergingMethod.replace: |
264 |
existing.action = macro.action |
265 |
existing.level = macro.level |
266 |
return
|
267 |
|
268 |
# Otherwise, just add it at the end
|
269 |
self.macros.append(macro)
|
270 |
|
271 |
def add_trigger(self, trigger): |
272 |
"""Add the trigger to the world's configuration, handling conflicts.
|
273 |
|
274 |
If another trigger with the same reaction exists, either replace
|
275 |
it or ignore the second one.
|
276 |
|
277 |
"""
|
278 |
for existing in self.triggers: |
279 |
if existing.reaction == trigger.reaction:
|
280 |
# There's a conflict, look at the 'merging' setting
|
281 |
if self.merging == MergingMethod.ignore: |
282 |
return
|
283 |
elif self.merging == MergingMethod.replace: |
284 |
existing.action = trigger.action |
285 |
existing.mute = trigger.mute |
286 |
existing.level = trigger.level |
287 |
return
|
288 |
|
289 |
# Otherwise, just add it at the end
|
290 |
self.triggers.append(trigger)
|
291 |
|
292 |
def reset_autocompletion(self): |
293 |
"""Erase the list of possible choices in for the auto completion."""
|
294 |
self.ac_choices[:] = []
|
295 |
|
296 |
def feed_words(self, text): |
297 |
"""Add new words using the provided text.
|
298 |
|
299 |
Each word in this text will be added to the list of words for
|
300 |
a future auto-completion.
|
301 |
|
302 |
"""
|
303 |
for word in re.findall(r"(\w+)", text, re.UNICODE): |
304 |
word = word.lower() |
305 |
count = self.words.get(word, 0) |
306 |
count += 1
|
307 |
self.words[word] = count
|
308 |
|
309 |
def find_word(self, word, TTS=False): |
310 |
"""Find the most likely word for auto-completion."""
|
311 |
matches = {} |
312 |
word = word.lower() |
313 |
for potential, count in self.words.items(): |
314 |
if potential.startswith(word):
|
315 |
if potential not in self.ac_choices: |
316 |
matches[potential] = count |
317 |
|
318 |
# Sort through the most common
|
319 |
for potential, count in sorted(matches.items(), |
320 |
key=lambda tup: tup[1], reverse=True): |
321 |
self.ac_choices.append(potential)
|
322 |
if TTS:
|
323 |
ScreenReader.talk(potential) |
324 |
return potential
|
325 |
|
326 |
return None |
327 |
|
328 |
def create_session(self, client): |
329 |
"""Create a session attached to this world."""
|
330 |
session = Session(client, self)
|
331 |
session.engine = self.engine
|
332 |
return session
|
333 |
|
334 |
def open_notepad(self): |
335 |
"""Open and return the notepad associated to this world."""
|
336 |
if self.notepad: |
337 |
return self.notepad |
338 |
|
339 |
self.notepad = Notepad(self) |
340 |
empty_string = t("ui.message.notepad.world_empty", world=self.name) |
341 |
self.notepad.open(empty_string)
|
342 |
return self.notepad |
343 |
|
344 |
@classmethod
|
345 |
def get_infos(cls, configuration): |
346 |
"""Get the information in the configuration and return a dict."""
|
347 |
config = ConfigObj(StringIO(configuration), encoding="utf-8")
|
348 |
data = {} |
349 |
|
350 |
for key, value in config.items(): |
351 |
if key == "port": |
352 |
try:
|
353 |
value = int(value)
|
354 |
except ValueError: |
355 |
pass
|
356 |
|
357 |
data[key] = value |
358 |
|
359 |
return data
|
- « Previous
- 1
- …
- 21
- 22
- 23
- Next »