Project

Profile

Help

How to connect?
Download (11.6 KB) Statistics View on GitHub Reload from mirrored respository
| Branch: | Tag: | Revision:

github / src / world.py @ 1b421251

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
        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
(22-22/22)