Project

Profile

Help

Download (11 KB) Statistics View on GitHub Reload from mirrored respository
| Branch: | Tag: | Revision:

github / src / world.py @ 96664dff

1 1df43fe1 Vincent Le Goff
# 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 6b36580d Vincent Le Goff
from enum import Enum
32 7382248f Vincent Le Goff
import shutil
33 6298df9a Vincent Le Goff
import os
34 c0055a5f Vincent Le Goff
import re
35 6b36580d Vincent Le Goff
from StringIO import StringIO
36 6298df9a Vincent Le Goff
from textwrap import dedent
37 28046bf5 Vincent Le Goff
from threading import RLock
38 e9cc2ac8 Vincent Le Goff
39 6298df9a Vincent Le Goff
from configobj import ConfigObj
40 6d5cf9ef Vincent Le Goff
from ytranslate import t
41 6298df9a Vincent Le Goff
42 c63ff6dd Vincent Le Goff
from character import Character
43 20ef9d49 Vincent Le Goff
from log import sharp as logger
44 6d5cf9ef Vincent Le Goff
from notepad import Notepad
45 adaa56c9 Vincent Le Goff
from screenreader import ScreenReader
46 e791b66f Vincent Le Goff
from session import Session
47 adaa56c9 Vincent Le Goff
48 6b36580d Vincent Le Goff
class MergingMethod(Enum):
49
50
    """Enumeration to represent merging methods."""
51
52
    ignore = 1
53
    replace = 2
54
55
56 1df43fe1 Vincent Le Goff
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 6298df9a Vincent Le Goff
        self.name = ""
68
        self.hostname = ""
69
        self.port = 4000
70 5e6fddad Vincent Le Goff
        self.protocol = "telnet"
71 1df43fe1 Vincent Le Goff
        self.characters = {}
72 a5c338e8 Vincent Le Goff
        self.settings = None
73 28046bf5 Vincent Le Goff
        self.lock = RLock()
74 a5c338e8 Vincent Le Goff
75
        # World's access to general data
76
        self.engine = None
77
        self.sharp_engine = None
78
79
        # World's configuration
80 13e86181 Vincent Le Goff
        self.aliases = []
81 c86e4e07 Vincent Le Goff
        self.channels = []
82 a5c338e8 Vincent Le Goff
        self.macros = []
83
        self.triggers = []
84 6d5cf9ef Vincent Le Goff
        self.notepad = None
85 6b36580d Vincent Le Goff
        self.merging = MergingMethod.ignore
86 01f37b6a Vincent Le Goff
87
        # Auto completion
88 c0055a5f Vincent Le Goff
        self.words = {}
89 01f37b6a Vincent Le Goff
        self.ac_choices = []
90 1df43fe1 Vincent Le Goff
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 6d5cf9ef Vincent Le Goff
        """Return the path to the world."""
98
        return os.path.join("worlds", self.location)
99 6298df9a Vincent Le Goff
100 a5c338e8 Vincent Le Goff
    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 78732c03 Vincent Le Goff
106
        # Reset some of the world's configuration
107
        self.aliases = []
108 c86e4e07 Vincent Le Goff
        self.channels = []
109 78732c03 Vincent Le Goff
        self.macros = []
110
        self.triggers = []
111
112 a5c338e8 Vincent Le Goff
        path = self.path
113
        path = os.path.join(path, "config.set")
114
        if os.path.exists(path):
115 20ef9d49 Vincent Le Goff
            file = open(path, "r")
116 ce0953c6 Vincent Le Goff
            content = file.read()
117 a5c338e8 Vincent Le Goff
            file.close()
118
119 ce0953c6 Vincent Le Goff
            # Convert the content to unicode
120
            content = content.decode("latin-1", errors="replace")
121
122 a5c338e8 Vincent Le Goff
            # Execute the script
123 20ef9d49 Vincent Le Goff
            self.sharp_engine.execute(content, variables=False)
124 a5c338e8 Vincent Le Goff
125
        # Put the engine level back
126
        self.engine.level = level
127
128 c63ff6dd Vincent Le Goff
    def load_characters(self):
129
        """Load the characters."""
130
        location = self.path
131
        for directory in os.listdir(location):
132
            if os.path.isdir(os.path.join(location, directory)) and \
133
                    os.path.exists(os.path.join(location,
134
                    directory, ".passphrase")):
135
                character = Character(self, directory)
136
                logger.info("Loading the character {} from the world " \
137
                        "{}".format(directory, self.name))
138
                character.load()
139
                self.characters[directory] = character
140
141 6298df9a Vincent Le Goff
    def save(self):
142 a5c338e8 Vincent Le Goff
        """Save the world in its configuration file."""
143 6298df9a Vincent Le Goff
        if not os.path.exists(self.path):
144
            os.mkdir(self.path)
145
146
        spec = dedent("""
147
            [connection]
148
                name = "unknown"
149
                hostname = "unknown.ext"
150
                port = 0
151 5e6fddad Vincent Le Goff
                protocol = "telnet"
152 6298df9a Vincent Le Goff
        """).strip("\n")
153
154
        if self.settings is None:
155 96664dff Vincent Le Goff
            self.settings = ConfigObj(spec.split("\n"), encoding="latin-1")
156 6298df9a Vincent Le Goff
157
        connection = self.settings["connection"]
158
        connection["name"] = self.name
159
        connection["hostname"] = self.hostname
160
        connection["port"] = self.port
161 5e6fddad Vincent Le Goff
        connection["protocol"] = self.protocol
162 6298df9a Vincent Le Goff
        self.settings.filename = os.path.join(self.path, "options.conf")
163
        self.settings.write()
164 866c4bd1 Vincent Le Goff
        self.save_config()
165 8b4a5b19 Vincent Le Goff
166 866c4bd1 Vincent Le Goff
    def save_config(self):
167
        """Save the 'config.set' script file."""
168 8b4a5b19 Vincent Le Goff
        lines = []
169
170
        # Aliases
171
        for alias in self.aliases:
172
            lines.append(alias.sharp_script)
173
174 3ff3ab7c Vincent Le Goff
        # Channels
175
        for channel in self.channels:
176
            lines.append("#channel {{{}}}".format(channel.name))
177
178 8b4a5b19 Vincent Le Goff
        # Macros
179
        for macro in self.macros:
180
            lines.append(macro.sharp_script)
181
182
        # Triggers
183
        for trigger in self.triggers:
184
            lines.append(trigger.sharp_script)
185
186 866c4bd1 Vincent Le Goff
        content = "\n".join(lines) + "\n"
187 8b4a5b19 Vincent Le Goff
        path = self.path
188
        path = os.path.join(path, "config.set")
189 20ef9d49 Vincent Le Goff
        file = open(path, "w")
190 9aff6288 Vincent Le Goff
        content = content.encode("latin-1")
191 8b4a5b19 Vincent Le Goff
        file.write(content)
192
        file.close()
193 7382248f Vincent Le Goff
194
    def remove(self):
195
        """Remove the world."""
196
        shutil.rmtree(self.path)
197 c0055a5f Vincent Le Goff
198 c63ff6dd Vincent Le Goff
    def add_character(self, location, name=None):
199
        """Add a new character in the world."""
200
        name = name or location
201
        character = Character(self, location)
202
        character.name = name
203
        character.save()
204
        self.characters[name] = character
205
        return character
206
207 95d2c123 Vincent Le Goff
    def add_alias(self, alias):
208
        """Add the alias to the world's configuration, handling conflicts.
209

210
        If another alias with the same name exists, either replace
211
        it or ignore the second one.
212

213
        """
214
        for existing in self.aliases:
215
            if existing.alias == alias.alias:
216
                # There's a conflict, look at the 'merging' setting
217
                if self.merging == MergingMethod.ignore:
218
                    return
219
                elif self.merging == MergingMethod.replace:
220
                    existing.action = alias.action
221
                    existing.level = alias.level
222
                    return
223
224
        # Otherwise, just add it at the end
225
        self.aliases.append(alias)
226
227
    def add_channel(self, channel):
228
        """Add a channel, handling conflicts."""
229
        for existing in self.channels:
230
            if existing.name == channel.name:
231
                return
232
233
        # Otherwise, just add it at the end
234
        self.channels.append(channel)
235
236
    def add_macro(self, macro):
237
        """Add the macro to the world's configuration, handling conflicts.
238

239
        If another macro with the same shortcut exists, either replace
240
        it or ignore the second one.
241

242
        """
243
        for existing in self.macros:
244
            if existing.shortcut == macro.shortcut:
245
                # There's a conflict, look at the 'merging' setting
246
                if self.merging == MergingMethod.ignore:
247
                    return
248
                elif self.merging == MergingMethod.replace:
249
                    existing.action = macro.action
250
                    existing.level = macro.level
251
                    return
252
253
        # Otherwise, just add it at the end
254
        self.macros.append(macro)
255
256
    def add_trigger(self, trigger):
257
        """Add the trigger to the world's configuration, handling conflicts.
258

259
        If another trigger with the same reaction exists, either replace
260
        it or ignore the second one.
261

262
        """
263
        for existing in self.triggers:
264
            if existing.reaction == trigger.reaction:
265
                # There's a conflict, look at the 'merging' setting
266
                if self.merging == MergingMethod.ignore:
267
                    return
268
                elif self.merging == MergingMethod.replace:
269
                    existing.action = trigger.action
270
                    existing.mute = trigger.mute
271
                    existing.level = trigger.level
272
                    return
273
274
        # Otherwise, just add it at the end
275
        self.triggers.append(trigger)
276
277 01f37b6a Vincent Le Goff
    def reset_autocompletion(self):
278
        """Erase the list of possible choices in for the auto completion."""
279
        self.ac_choices[:] = []
280
281 c0055a5f Vincent Le Goff
    def feed_words(self, text):
282
        """Add new words using the provided text.
283

284
        Each word in this text will be added to the list of words for
285
        a future auto-completion.
286

287
        """
288
        for word in re.findall(r"(\w+)", text, re.UNICODE):
289
            word = word.lower()
290
            count = self.words.get(word, 0)
291
            count += 1
292
            self.words[word] = count
293
294 adaa56c9 Vincent Le Goff
    def find_word(self, word, TTS=False):
295 c0055a5f Vincent Le Goff
        """Find the most likely word for auto-completion."""
296
        matches = {}
297
        word = word.lower()
298
        for potential, count in self.words.items():
299
            if potential.startswith(word):
300 01f37b6a Vincent Le Goff
                if potential not in self.ac_choices:
301
                    matches[potential] = count
302 c0055a5f Vincent Le Goff
303
        # Sort through the most common
304
        for potential, count in sorted(matches.items(),
305
                key=lambda tup: tup[1], reverse=True):
306 01f37b6a Vincent Le Goff
            self.ac_choices.append(potential)
307 adaa56c9 Vincent Le Goff
            if TTS:
308
                ScreenReader.talk(potential)
309 c0055a5f Vincent Le Goff
            return potential
310
311
        return None
312 e791b66f Vincent Le Goff
313
    def create_session(self, client):
314
        """Create a session attached to this world."""
315
        session = Session(client, self)
316
        session.engine = self.engine
317
        session.sharp_engine = self.sharp_engine
318
        return session
319 6b36580d Vincent Le Goff
320 6d5cf9ef Vincent Le Goff
    def open_notepad(self):
321
        """Open and return the notepad associated to this world."""
322
        if self.notepad:
323
            return self.notepad
324
325
        self.notepad = Notepad(self)
326
        empty_string = t("ui.message.notepad.world_empty", world=self.name)
327
        self.notepad.open(empty_string)
328
        return self.notepad
329
330 6b36580d Vincent Le Goff
    @classmethod
331
    def get_infos(cls, configuration):
332
        """Get the information in the configuration and return a dict."""
333 96664dff Vincent Le Goff
        config = ConfigObj(StringIO(configuration), encoding="latin-1")
334 6b36580d Vincent Le Goff
        data = {}
335
336
        for key, value in config.items():
337
            if key == "port":
338
                try:
339
                    value = int(value)
340
                except ValueError:
341
                    pass
342
343
            data[key] = value
344
345
        return data