Project

Profile

Help

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

github / src / world.py @ 96664dff

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 StringIO import StringIO
36
from textwrap import dedent
37
from threading import RLock
38

    
39
from configobj import ConfigObj
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("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

    
106
        # Reset some of the world's configuration
107
        self.aliases = []
108
        self.channels = []
109
        self.macros = []
110
        self.triggers = []
111

    
112
        path = self.path
113
        path = os.path.join(path, "config.set")
114
        if os.path.exists(path):
115
            file = open(path, "r")
116
            content = file.read()
117
            file.close()
118

    
119
            # Convert the content to unicode
120
            content = content.decode("latin-1", errors="replace")
121

    
122
            # Execute the script
123
            self.sharp_engine.execute(content, variables=False)
124

    
125
        # Put the engine level back
126
        self.engine.level = level
127

    
128
    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
    def save(self):
142
        """Save the world in its configuration file."""
143
        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
                protocol = "telnet"
152
        """).strip("\n")
153

    
154
        if self.settings is None:
155
            self.settings = ConfigObj(spec.split("\n"), encoding="latin-1")
156

    
157
        connection = self.settings["connection"]
158
        connection["name"] = self.name
159
        connection["hostname"] = self.hostname
160
        connection["port"] = self.port
161
        connection["protocol"] = self.protocol
162
        self.settings.filename = os.path.join(self.path, "options.conf")
163
        self.settings.write()
164
        self.save_config()
165

    
166
    def save_config(self):
167
        """Save the 'config.set' script file."""
168
        lines = []
169

    
170
        # Aliases
171
        for alias in self.aliases:
172
            lines.append(alias.sharp_script)
173

    
174
        # Channels
175
        for channel in self.channels:
176
            lines.append("#channel {{{}}}".format(channel.name))
177

    
178
        # 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
        content = "\n".join(lines) + "\n"
187
        path = self.path
188
        path = os.path.join(path, "config.set")
189
        file = open(path, "w")
190
        content = content.encode("latin-1")
191
        file.write(content)
192
        file.close()
193

    
194
    def remove(self):
195
        """Remove the world."""
196
        shutil.rmtree(self.path)
197

    
198
    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
    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
    def reset_autocompletion(self):
278
        """Erase the list of possible choices in for the auto completion."""
279
        self.ac_choices[:] = []
280

    
281
    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
    def find_word(self, word, TTS=False):
295
        """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
                if potential not in self.ac_choices:
301
                    matches[potential] = count
302

    
303
        # Sort through the most common
304
        for potential, count in sorted(matches.items(),
305
                key=lambda tup: tup[1], reverse=True):
306
            self.ac_choices.append(potential)
307
            if TTS:
308
                ScreenReader.talk(potential)
309
            return potential
310

    
311
        return None
312

    
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

    
320
    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
    @classmethod
331
    def get_infos(cls, configuration):
332
        """Get the information in the configuration and return a dict."""
333
        config = ConfigObj(StringIO(configuration), encoding="latin-1")
334
        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
(19-19/19)