Project

Profile

Help

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

github / src / world.py @ 05b173a7

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