commit c1ced4ff33c12fc4be2709afd499307a8a8ae83f Author: Jerry Aldrich Date: Mon Jan 20 19:52:47 2020 -0800 Initial commit diff --git a/start_server.sh b/start_server.sh new file mode 100644 index 0000000..6e5736e --- /dev/null +++ b/start_server.sh @@ -0,0 +1 @@ +docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4002:4002 --rm -v $PWD:/usr/src/game --user $UID:$GID evennia/evennia diff --git a/taoshengshi/.gitignore b/taoshengshi/.gitignore new file mode 100644 index 0000000..f5fb664 --- /dev/null +++ b/taoshengshi/.gitignore @@ -0,0 +1,52 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Other +*.swp +*.log +*.pid +*.restart +*.db3 + +# Installation-specific. +# For group efforts, comment out some or all of these. +server/conf/secret_settings.py +server/logs/*.log.* +web/static/* +web/media/* + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# PyCharm config +.idea diff --git a/taoshengshi/.pylintrc b/taoshengshi/.pylintrc new file mode 100644 index 0000000..098a8da --- /dev/null +++ b/taoshengshi/.pylintrc @@ -0,0 +1,8 @@ +[MESSAGE CONTROL] + +disable=C0103, E0401 + +[VARIABLES] + +additional-builtins=caller + diff --git a/taoshengshi/IDEAS.md b/taoshengshi/IDEAS.md new file mode 100644 index 0000000..e106f4e --- /dev/null +++ b/taoshengshi/IDEAS.md @@ -0,0 +1,2 @@ +Exit inside item, can set to any room while inside it then teleport to that room + diff --git a/taoshengshi/README.md b/taoshengshi/README.md new file mode 100644 index 0000000..fe62bd3 --- /dev/null +++ b/taoshengshi/README.md @@ -0,0 +1,40 @@ +# Welcome to Evennia! + +This is your game directory, set up to let you start with +your new game right away. An overview of this directory is found here: +https://github.com/evennia/evennia/wiki/Directory-Overview#the-game-directory + +You can delete this readme file when you've read it and you can +re-arrange things in this game-directory to suit your own sense of +organisation (the only exception is the directory structure of the +`server/` directory, which Evennia expects). If you change the structure +you must however also edit/add to your settings file to tell Evennia +where to look for things. + +Your game's main configuration file is found in +`server/conf/settings.py` (but you don't need to change it to get +started). If you just created this directory (which means you'll already +have a `virtualenv` running if you followed the default instructions), +`cd` to this directory then initialize a new database using + + evennia migrate + +To start the server, stand in this directory and run + + evennia start + +This will start the server, logging output to the console. Make +sure to create a superuser when asked. By default you can now connect +to your new game using a MUD client on `localhost`, port `4000`. You can +also log into the web client by pointing a browser to +`http://localhost:4001`. + +# Getting started + +From here on you might want to look at one of the beginner tutorials: +http://github.com/evennia/evennia/wiki/Tutorials. + +Evennia's documentation is here: +https://github.com/evennia/evennia/wiki. + +Enjoy! diff --git a/taoshengshi/__init__.py b/taoshengshi/__init__.py new file mode 100644 index 0000000..6e3dbee --- /dev/null +++ b/taoshengshi/__init__.py @@ -0,0 +1,6 @@ +""" +This sub-package holds the template for creating a new game folder. +The new game folder (when running evennia --init) is a copy of this +folder. + +""" diff --git a/taoshengshi/commands/README.md b/taoshengshi/commands/README.md new file mode 100644 index 0000000..0425ce6 --- /dev/null +++ b/taoshengshi/commands/README.md @@ -0,0 +1,14 @@ +# commands/ + +This folder holds modules for implementing one's own commands and +command sets. All the modules' classes are essentially empty and just +imports the default implementations from Evennia; so adding anything +to them will start overloading the defaults. + +You can change the organisation of this directory as you see fit, just +remember that if you change any of the default command set classes' +locations, you need to add the appropriate paths to +`server/conf/settings.py` so that Evennia knows where to find them. +Also remember that if you create new sub directories you must put +(optionally empty) `__init__.py` files in there so that Python can +find your modules. diff --git a/taoshengshi/commands/__init__.py b/taoshengshi/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taoshengshi/commands/command.py b/taoshengshi/commands/command.py new file mode 100644 index 0000000..3c7c0e2 --- /dev/null +++ b/taoshengshi/commands/command.py @@ -0,0 +1,188 @@ +""" +Commands + +Commands describe the input the account can do to the game. + +""" + +from evennia import Command as BaseCommand + +# from evennia import default_cmds + + +class Command(BaseCommand): + """ + Inherit from this if you want to create your own command styles + from scratch. Note that Evennia's default commands inherits from + MuxCommand instead. + + Note that the class's `__doc__` string (this text) is + used by Evennia to create the automatic help entry for + the command, so make sure to document consistently here. + + Each Command implements the following methods, called + in this order (only func() is actually required): + - at_pre_cmd(): If this returns anything truthy, execution is aborted. + - parse(): Should perform any extra parsing needed on self.args + and store the result on self. + - func(): Performs the actual work. + - at_post_cmd(): Extra actions, often things done after + every command, like prompts. + + """ + + pass + + +# ------------------------------------------------------------- +# +# The default commands inherit from +# +# evennia.commands.default.muxcommand.MuxCommand. +# +# If you want to make sweeping changes to default commands you can +# uncomment this copy of the MuxCommand parent and add +# +# COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand" +# +# to your settings file. Be warned that the default commands expect +# the functionality implemented in the parse() method, so be +# careful with what you change. +# +# ------------------------------------------------------------- + +# from evennia.utils import utils +# +# +# class MuxCommand(Command): +# """ +# This sets up the basis for a MUX command. The idea +# is that most other Mux-related commands should just +# inherit from this and don't have to implement much +# parsing of their own unless they do something particularly +# advanced. +# +# Note that the class's __doc__ string (this text) is +# used by Evennia to create the automatic help entry for +# the command, so make sure to document consistently here. +# """ +# def has_perm(self, srcobj): +# """ +# This is called by the cmdhandler to determine +# if srcobj is allowed to execute this command. +# We just show it here for completeness - we +# are satisfied using the default check in Command. +# """ +# return super().has_perm(srcobj) +# +# def at_pre_cmd(self): +# """ +# This hook is called before self.parse() on all commands +# """ +# pass +# +# def at_post_cmd(self): +# """ +# This hook is called after the command has finished executing +# (after self.func()). +# """ +# pass +# +# def parse(self): +# """ +# This method is called by the cmdhandler once the command name +# has been identified. It creates a new set of member variables +# that can be later accessed from self.func() (see below) +# +# The following variables are available for our use when entering this +# method (from the command definition, and assigned on the fly by the +# cmdhandler): +# self.key - the name of this command ('look') +# self.aliases - the aliases of this cmd ('l') +# self.permissions - permission string for this command +# self.help_category - overall category of command +# +# self.caller - the object calling this command +# self.cmdstring - the actual command name used to call this +# (this allows you to know which alias was used, +# for example) +# self.args - the raw input; everything following self.cmdstring. +# self.cmdset - the cmdset from which this command was picked. Not +# often used (useful for commands like 'help' or to +# list all available commands etc) +# self.obj - the object on which this command was defined. It is often +# the same as self.caller. +# +# A MUX command has the following possible syntax: +# +# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]] +# +# The 'name[ with several words]' part is already dealt with by the +# cmdhandler at this point, and stored in self.cmdname (we don't use +# it here). The rest of the command is stored in self.args, which can +# start with the switch indicator /. +# +# This parser breaks self.args into its constituents and stores them in the +# following variables: +# self.switches = [list of /switches (without the /)] +# self.raw = This is the raw argument input, including switches +# self.args = This is re-defined to be everything *except* the switches +# self.lhs = Everything to the left of = (lhs:'left-hand side'). If +# no = is found, this is identical to self.args. +# self.rhs: Everything to the right of = (rhs:'right-hand side'). +# If no '=' is found, this is None. +# self.lhslist - [self.lhs split into a list by comma] +# self.rhslist - [list of self.rhs split into a list by comma] +# self.arglist = [list of space-separated args (stripped, including '=' if it exists)] +# +# All args and list members are stripped of excess whitespace around the +# strings, but case is preserved. +# """ +# raw = self.args +# args = raw.strip() +# +# # split out switches +# switches = [] +# if args and len(args) > 1 and args[0] == "/": +# # we have a switch, or a set of switches. These end with a space. +# switches = args[1:].split(None, 1) +# if len(switches) > 1: +# switches, args = switches +# switches = switches.split('/') +# else: +# args = "" +# switches = switches[0].split('/') +# arglist = [arg.strip() for arg in args.split()] +# +# # check for arg1, arg2, ... = argA, argB, ... constructs +# lhs, rhs = args, None +# lhslist, rhslist = [arg.strip() for arg in args.split(',')], [] +# if args and '=' in args: +# lhs, rhs = [arg.strip() for arg in args.split('=', 1)] +# lhslist = [arg.strip() for arg in lhs.split(',')] +# rhslist = [arg.strip() for arg in rhs.split(',')] +# +# # save to object properties: +# self.raw = raw +# self.switches = switches +# self.args = args.strip() +# self.arglist = arglist +# self.lhs = lhs +# self.lhslist = lhslist +# self.rhs = rhs +# self.rhslist = rhslist +# +# # if the class has the account_caller property set on itself, we make +# # sure that self.caller is always the account if possible. We also create +# # a special property "character" for the puppeted object, if any. This +# # is convenient for commands defined on the Account only. +# if hasattr(self, "account_caller") and self.account_caller: +# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"): +# # caller is an Object/Character +# self.character = self.caller +# self.caller = self.caller.account +# elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"): +# # caller was already an Account +# self.character = self.caller.get_puppet(self.session) +# else: +# self.character = None diff --git a/taoshengshi/commands/default_cmdsets.py b/taoshengshi/commands/default_cmdsets.py new file mode 100644 index 0000000..48866c4 --- /dev/null +++ b/taoshengshi/commands/default_cmdsets.py @@ -0,0 +1,97 @@ +""" +Command sets + +All commands in the game must be grouped in a cmdset. A given command +can be part of any number of cmdsets and cmdsets can be added/removed +and merged onto entities at runtime. + +To create new commands to populate the cmdset, see +`commands/command.py`. + +This module wraps the default command sets of Evennia; overloads them +to add/remove commands from the default lineup. You can create your +own cmdsets by inheriting from them or directly from `evennia.CmdSet`. + +""" + +from evennia import default_cmds +from commands.override_cmds import CmdSetOverride + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + """ + The `CharacterCmdSet` contains general in-game commands like `look`, + `get`, etc available on in-game Character objects. It is merged with + the `AccountCmdSet` when an Account puppets a Character. + """ + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + self.add(CmdSetOverride()) + # + # any commands you add below will overload the default ones. + # + + +class AccountCmdSet(default_cmds.AccountCmdSet): + """ + This is the cmdset available to the Account at all times. It is + combined with the `CharacterCmdSet` when the Account puppets a + Character. It holds game-account-specific commands, channel + commands, etc. + """ + + key = "DefaultAccount" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): + """ + Command set available to the Session before being logged in. This + holds commands like creating a new account, logging in, etc. + """ + + key = "DefaultUnloggedin" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class SessionCmdSet(default_cmds.SessionCmdSet): + """ + This cmdset is made available on Session level once logged in. It + is empty by default. + """ + + key = "DefaultSession" + + def at_cmdset_creation(self): + """ + This is the only method defined in a cmdset, called during + its creation. It should populate the set with command instances. + + As and example we just add the empty base `Command` object. + It prints some info. + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # diff --git a/taoshengshi/commands/keypad.py b/taoshengshi/commands/keypad.py new file mode 100644 index 0000000..510dd99 --- /dev/null +++ b/taoshengshi/commands/keypad.py @@ -0,0 +1,36 @@ +''' +Command and command set for using a Keypad +''' + +from evennia import Command, CmdSet + +class CmdUseKeypad(Command): + ''' + Use a keypad + + Usage: + use [the] keypad + ''' + + key = 'use keypad' + aliases = ['use the keypad', 'unlock'] + + def func(self): + ''' + Implement the command + ''' + # Have to yield here since Evennia only allows yield in command func + keycode = yield('Enter keycode: ') + self.obj.attempt_unlock(self.caller, keycode) + +class CmdSetKeypad(CmdSet): + ''' + Command set for using a keypad + ''' + key = 'cmdset_keypad' + + def at_cmdset_creation(self): + 'Attach commands to the command set' + self.add(CmdUseKeypad()) + + diff --git a/taoshengshi/commands/mech_cmds.py b/taoshengshi/commands/mech_cmds.py new file mode 100644 index 0000000..b2b7796 --- /dev/null +++ b/taoshengshi/commands/mech_cmds.py @@ -0,0 +1,150 @@ +''' +Commands and command set related to Mechs +''' + +from evennia import Command, CmdSet +from evennia.utils.utils import list_to_string + +# TODO: Pullout disconnected characters +class CmdPilot(Command): + ''' + Pilot a thing + + Usage: + pilot [target] + ''' + + key = 'pilot' + aliases = ['enter'] + locks = 'cmd:not cmdinside()' + + def func(self): + ''' + Implement the command + ''' + caller = self.caller + location = caller.location + + if not self.args: + message = "You must give a target" + caller.msg(message) + return + + target = caller.search(self.args.strip()) + if not target: + return + + drivers = list(filter(lambda x: x.is_connected, self.obj.contents)) + + if drivers: + caller.msg(f"This mech is currently piloted by {drivers[0]}, you cannot enter") + return + + caller_msg = "You climb into the %s" % target.name + location_msg = "%s climbs into the %s" % (caller.name, target.name) + caller.msg(caller_msg) + location.msg_contents(location_msg, exclude=[caller]) + self.caller.move_to(self.obj, quiet=True) + +class CmdLeave(Command): + ''' + Leave a thing + + Usage: + leave + ''' + + key = 'leave' + aliases = ['exit'] + locks = 'cmd:cmdinside()' + + def func(self): + ''' + Implement the command + ''' + caller = self.caller + + caller_msg = "You climb out of the %s" % self.obj.name + location_msg = "%s climbs out of the %s" % (caller.name, self.obj.name) + caller.msg(caller_msg) + self.obj.location.msg_contents(location_msg, exclude=[caller, self.obj]) + self.caller.move_to(self.obj.location, quiet=True) + +class CmdHUD(Command): + ''' + Look outside the mech + + Usage: + hud + ''' + + key = 'HUD' + aliases = ['hud'] + locks = 'cmd:cmdinside()' + + def func(self): + 'Implement the command' + caller = self.caller + outside = self.obj.location + + caller.msg(outside.return_appearance(self.caller, exclude=[self.obj])) + +class CmdDrive(Command): + ''' + Drive the mech + + Usage: + drive to [location] + ''' + + key = 'drive' + locks = 'cmd:cmdinside()' + + def parse(self): + if self.args: + parsed_args = [x for x in self.args.split() if x not in ('to', 'the', 'into')] + self.args = ' '.join(parsed_args) + + def func(self): + 'Implement the command' + if not self.args: + self.caller.msg('You must provide a location to drive to') + return + + search_results = self.obj.search(self.args, quiet=True) + if not search_results: + self.caller.msg(f"The mech doesn't understand how to drive to '{self.args}'") + return + + if len(search_results) > 1: + self.caller.msg('More than one result found, please be more specific') + self.caller.msg(f"Results: {list_to_string(list(search_results))}") + return + + result = search_results[0] + if result == self.caller.location: + self.caller.msg('You cannot drive into yourself') + return + + # Only exits should have a '.destination' + if not result.destination: + self.caller.msg(f"You cannot drive into {self.args}") + return + + self.caller.msg(f"Driving to {result.destination}") + self.obj.move_to(result) + self.caller.execute_cmd('HUD') + +class CmdSetMech(CmdSet): + ''' + Commands for mechs + ''' + + def at_cmdset_creation(self): + 'Attach commands to the command set' + self.add(CmdDrive()) + self.add(CmdHUD()) + self.add(CmdLeave()) + self.add(CmdPilot()) + + diff --git a/taoshengshi/commands/override_cmds.py b/taoshengshi/commands/override_cmds.py new file mode 100644 index 0000000..0f89880 --- /dev/null +++ b/taoshengshi/commands/override_cmds.py @@ -0,0 +1,41 @@ +''' +Modified default commands +''' + +from evennia import Command, CmdSet +from evennia.commands.default import ( + general +) + +# TODO: Needs tests +class CmdOverrideLook(general.CmdLook): + ''' + Look command but parses out certain words + ''' + + def parse(self): + if self.args: + parsed_args = [x for x in self.args.split() if x not in ('at', 'the')] + self.args = ' '.join(parsed_args) + + def func(self): + ''' + Call original but now with new parsed args + ''' + # I don't like this here...but I can't do it on the Mech CmdSet because it isn't on the + # character + if self.args and self.caller.location.typename == 'Mech': + results = self.caller.location.search(self.args) + if results: + self.caller.msg(f"You can't see that very well from inside a mech") + return + super().func() + +class CmdSetOverride(CmdSet): + ''' + Container for overridden commands + ''' + + def at_cmdset_creation(self): + 'Attach commands to the command set' + self.add(CmdOverrideLook()) diff --git a/taoshengshi/commands/test_keypad.py b/taoshengshi/commands/test_keypad.py new file mode 100644 index 0000000..c8913a2 --- /dev/null +++ b/taoshengshi/commands/test_keypad.py @@ -0,0 +1,25 @@ +''' +Tests for CmdPilot +''' + +from evennia.commands.default.tests import CommandTest +from evennia import create_object +from mock import Mock +from typeclasses.exits import KeypadDoor +from commands.keypad import * +from evennia.commands.default.cmdset_character import CharacterCmdSet +import re + +class TestCmdUseKeypad(CommandTest): + 'Tests for CmdPilot' + + def setUp(self): + 'Setup environment for testing mech commands' + super().setUp() + self.keypad_door = create_object(KeypadDoor, key='door', location=self.room1) + + def test_func_without_arg(self): + 'Tests func method' + + self.call(CmdUseKeypad(), '', 'Please enter keycode: ') + diff --git a/taoshengshi/commands/test_mech_cmds.py b/taoshengshi/commands/test_mech_cmds.py new file mode 100644 index 0000000..36ac610 --- /dev/null +++ b/taoshengshi/commands/test_mech_cmds.py @@ -0,0 +1,104 @@ +''' +Tests for CmdPilot +''' + +from evennia.commands.default.tests import CommandTest +from evennia import create_object +from mock import Mock +from typeclasses import mech +from commands.mech_cmds import * +from evennia.commands.default.help import CmdHelp +from evennia.commands.default.cmdset_character import CharacterCmdSet +import re + +# TODO: Make [0][2]['text'][0] not a thing... +# TODO: Test lockfunc cmdinside (self.call can't move location) + +class MechCmdHelper(CommandTest): + 'Helper class for mech command tests' + + def setUp(self): + 'Setup environment for testing mech commands' + super().setUp() + self.mech = create_object(mech.Mech, key='mech', + location=self.room1, aliases=['mech']) + + # Need to remove permissions to test locks + self.char1.account.permissions.remove('Developer') + + # Place Char 1 inside mech + self.char1.move_to(self.mech) + +class TestCmdPilot(MechCmdHelper): + 'Tests for CmdPilot' + + def setUp(self): + 'Setup tests for pilot command' + super().setUp() + + # Ensure characters start outside of the mech + self.char1.move_to(self.mech.location) + self.char2.move_to(self.mech.location) + + def test_func_without_arg(self): + 'Tests func method' + + self.call(CmdPilot(), '', 'You must give a target') + + def test_func_with_arg(self): + # Store messages received by char2 + self.char1.msg = Mock() + self.char2.msg = Mock() + + self.assertNotIn(self.char1, self.mech.contents) + self.char1.execute_cmd('pilot mech') + self.assertIn(self.char1, self.mech.contents) + + # Check messages sent + self.assertEqual(self.char1.msg.mock_calls[0][1][0], 'You climb into the mech') + self.assertEqual(self.char2.msg.mock_calls[0][2]['text'][0], 'Char climbs into the mech') + + def test_func_occupied(self): + 'Test trying to pilot an occupied mech' + # It's easier to use char2 to test because `char2.is_connected` returns true + self.char1.move_to(self.mech) + + self.char2.msg = Mock() + self.char2.execute_cmd('pilot mech') + self.assertEqual(self.char2.msg.mock_calls[0][1][0], 'This mech is currently piloted by Char, you cannot enter') + self.assertNotIn(self.char2, self.mech.contents) + +class TestCmdLeave(MechCmdHelper): + 'Tests for CmdLeave' + + def test_func(self): + 'Tests func method' + + # Store messages received by chararacters + self.char1.msg = Mock() + self.char2.msg = Mock() + + self.char1.execute_cmd('leave mech') + + self.assertEqual(self.char1.msg.mock_calls[0][1][0], 'You climb out of the mech') + self.assertEqual(self.char2.msg.mock_calls[0][2]['text'][0], 'Char climbs out of the mech') + +class TestCmdHUD(MechCmdHelper): + 'Tests for CmdHud' + + def test_func(self): + 'Tests func method' + + self.char1.msg = Mock() + self.char1.execute_cmd('hud') + + output = self.char1.msg.mock_calls[0][1][0] + self.assertEqual(output, self.room1.return_appearance(self.char1, exclude=[self.mech])) + +class TestCmdDrive(MechCmdHelper): + 'Tests for CmdDrive' + + def test_func(self): + 'Tests func method' + self.char1.execute_cmd('drive to out') + self.assertEqual(self.mech.location, self.room2) diff --git a/taoshengshi/server/README.md b/taoshengshi/server/README.md new file mode 100644 index 0000000..9f530c3 --- /dev/null +++ b/taoshengshi/server/README.md @@ -0,0 +1,38 @@ +# server/ + +This directory holds files used by and configuring the Evennia server +itself. + +Out of all the subdirectories in the game directory, Evennia does +expect this directory to exist, so you should normally not delete, +rename or change its folder structure. + +When running you will find four new files appear in this directory: + + - `server.pid` and `portal.pid`: These hold the process IDs of the + Portal and Server, so that they can be managed by the launcher. If + Evennia is shut down uncleanly (e.g. by a crash or via a kill + signal), these files might erroneously remain behind. If so Evennia + will tell you they are "stale" and they can be deleted manually. + - `server.restart` and `portal.restart`: These hold flags to tell the + server processes if it should die or start again. You never need to + modify those files. + - `evennia.db3`: This will only appear if you are using the default + SQLite3 database; it a binary file that holds the entire game + database; deleting this file will effectively reset the game for + you and you can start fresh with `evennia migrate` (useful during + development). + +## server/conf/ + +This subdirectory holds the configuration modules for the server. With +them you can change how Evennia operates and also plug in your own +functionality to replace the default. You usually need to restart the +server to apply changes done here. The most important file is the file +`settings.py` which is the main configuration file of Evennia. + +## server/logs/ + +This subdirectory holds various log files created by the running +Evennia server. It is also the default location for storing any custom +log files you might want to output using Evennia's logging mechanisms. diff --git a/taoshengshi/server/__init__.py b/taoshengshi/server/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/taoshengshi/server/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/taoshengshi/server/conf/__init__.py b/taoshengshi/server/conf/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/taoshengshi/server/conf/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/taoshengshi/server/conf/at_initial_setup.py b/taoshengshi/server/conf/at_initial_setup.py new file mode 100644 index 0000000..b394a04 --- /dev/null +++ b/taoshengshi/server/conf/at_initial_setup.py @@ -0,0 +1,19 @@ +""" +At_initial_setup module template + +Custom at_initial_setup method. This allows you to hook special +modifications to the initial server startup process. Note that this +will only be run once - when the server starts up for the very first +time! It is called last in the startup process and can thus be used to +overload things that happened before it. + +The module must contain a global function at_initial_setup(). This +will be called without arguments. Note that tracebacks in this module +will be QUIETLY ignored, so make sure to check it well to make sure it +does what you expect it to. + +""" + + +def at_initial_setup(): + pass diff --git a/taoshengshi/server/conf/at_search.py b/taoshengshi/server/conf/at_search.py new file mode 100644 index 0000000..0982894 --- /dev/null +++ b/taoshengshi/server/conf/at_search.py @@ -0,0 +1,54 @@ +""" +Search and multimatch handling + +This module allows for overloading two functions used by Evennia's +search functionality: + + at_search_result: + This is called whenever a result is returned from an object + search (a common operation in commands). It should (together + with at_multimatch_input below) define some way to present and + differentiate between multiple matches (by default these are + presented as 1-ball, 2-ball etc) + at_multimatch_input: + This is called with a search term and should be able to + identify if the user wants to separate a multimatch-result + (such as that from a previous search). By default, this + function understands input on the form 1-ball, 2-ball etc as + indicating that the 1st or 2nd match for "ball" should be + used. + +This module is not called by default, to use it, add the following +line to your settings file: + + SEARCH_AT_RESULT = "server.conf.at_search.at_search_result" + +""" + + +def at_search_result(matches, caller, query="", quiet=False, **kwargs): + """ + This is a generic hook for handling all processing of a search + result, including error reporting. + + Args: + matches (list): This is a list of 0, 1 or more typeclass instances, + the matched result of the search. If 0, a nomatch error should + be echoed, and if >1, multimatch errors should be given. Only + if a single match should the result pass through. + caller (Object): The object performing the search and/or which should + receive error messages. + query (str, optional): The search query used to produce `matches`. + quiet (bool, optional): If `True`, no messages will be echoed to caller + on errors. + + Kwargs: + nofound_string (str): Replacement string to echo on a notfound error. + multimatch_string (str): Replacement string to echo on a multimatch error. + + Returns: + processed_result (Object or None): This is always a single result + or `None`. If `None`, any error reporting/handling should + already have happened. + + """ diff --git a/taoshengshi/server/conf/at_server_startstop.py b/taoshengshi/server/conf/at_server_startstop.py new file mode 100644 index 0000000..98c29fa --- /dev/null +++ b/taoshengshi/server/conf/at_server_startstop.py @@ -0,0 +1,63 @@ +""" +Server startstop hooks + +This module contains functions called by Evennia at various +points during its startup, reload and shutdown sequence. It +allows for customizing the server operation as desired. + +This module must contain at least these global functions: + +at_server_start() +at_server_stop() +at_server_reload_start() +at_server_reload_stop() +at_server_cold_start() +at_server_cold_stop() + +""" + + +def at_server_start(): + """ + This is called every time the server starts up, regardless of + how it was shut down. + """ + pass + + +def at_server_stop(): + """ + This is called just before the server is shut down, regardless + of it is for a reload, reset or shutdown. + """ + pass + + +def at_server_reload_start(): + """ + This is called only when server starts back up after a reload. + """ + pass + + +def at_server_reload_stop(): + """ + This is called only time the server stops before a reload. + """ + pass + + +def at_server_cold_start(): + """ + This is called only when the server starts "cold", i.e. after a + shutdown or a reset. + """ + pass + + +def at_server_cold_stop(): + """ + This is called only when the server goes down due to a shutdown or + reset. + """ + pass diff --git a/taoshengshi/server/conf/cmdparser.py b/taoshengshi/server/conf/cmdparser.py new file mode 100644 index 0000000..831990a --- /dev/null +++ b/taoshengshi/server/conf/cmdparser.py @@ -0,0 +1,55 @@ +""" +Changing the default command parser + +The cmdparser is responsible for parsing the raw text inserted by the +user, identifying which command/commands match and return one or more +matching command objects. It is called by Evennia's cmdhandler and +must accept input and return results on the same form. The default +handler is very generic so you usually don't need to overload this +unless you have very exotic parsing needs; advanced parsing is best +done at the Command.parse level. + +The default cmdparser understands the following command combinations +(where [] marks optional parts.) + +[cmdname[ cmdname2 cmdname3 ...] [the rest] + +A command may consist of any number of space-separated words of any +length, and contain any character. It may also be empty. + +The parser makes use of the cmdset to find command candidates. The +parser return a list of matches. Each match is a tuple with its first +three elements being the parsed cmdname (lower case), the remaining +arguments, and the matched cmdobject from the cmdset. + + +This module is not accessed by default. To tell Evennia to use it +instead of the default command parser, add the following line to +your settings file: + + COMMAND_PARSER = "server.conf.cmdparser.cmdparser" + +""" + + +def cmdparser(raw_string, cmdset, caller, match_index=None): + """ + This function is called by the cmdhandler once it has + gathered and merged all valid cmdsets valid for this particular parsing. + + raw_string - the unparsed text entered by the caller. + cmdset - the merged, currently valid cmdset + caller - the caller triggering this parsing + match_index - an optional integer index to pick a given match in a + list of same-named command matches. + + Returns: + list of tuples: [(cmdname, args, cmdobj, cmdlen, mratio), ...] + where cmdname is the matching command name and args is + everything not included in the cmdname. Cmdobj is the actual + command instance taken from the cmdset, cmdlen is the length + of the command name and the mratio is some quality value to + (possibly) separate multiple matches. + + """ + # Your implementation here diff --git a/taoshengshi/server/conf/connection_screens.py b/taoshengshi/server/conf/connection_screens.py new file mode 100644 index 0000000..adfcd73 --- /dev/null +++ b/taoshengshi/server/conf/connection_screens.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +Connection screen + +This is the text to show the user when they first connect to the game (before +they log in). + +To change the login screen in this module, do one of the following: + +- Define a function `connection_screen()`, taking no arguments. This will be + called first and must return the full string to act as the connection screen. + This can be used to produce more dynamic screens. +- Alternatively, define a string variable in the outermost scope of this module + with the connection string that should be displayed. If more than one such + variable is given, Evennia will pick one of them at random. + +The commands available to the user when the connection screen is shown +are defined in evennia.default_cmds.UnloggedinCmdSet. The parsing and display +of the screen is done by the unlogged-in "look" command. + +""" + +from django.conf import settings +from evennia import utils + +CONNECTION_SCREEN = """ +|b==============================================================|n + Welcome to |g{}|n, version {}! + + If you have an existing account, connect to it by typing: + |wconnect |n + If you need to create an account, type (without the <>'s): + |wcreate |n + + If you have spaces in your username, enclose it in quotes. + Enter |whelp|n for more info. |wlook|n will re-show this screen. +|b==============================================================|n""".format( + settings.SERVERNAME, utils.get_evennia_version("short") +) diff --git a/taoshengshi/server/conf/inlinefuncs.py b/taoshengshi/server/conf/inlinefuncs.py new file mode 100644 index 0000000..1190597 --- /dev/null +++ b/taoshengshi/server/conf/inlinefuncs.py @@ -0,0 +1,51 @@ +""" +Inlinefunc + +Inline functions allow for direct conversion of text users mark in a +special way. Inlinefuncs are deactivated by default. To activate, add + + INLINEFUNC_ENABLED = True + +to your settings file. The default inlinefuncs are found in +evennia.utils.inlinefunc. + +In text, usage is straightforward: + +$funcname([arg1,[arg2,...]]) + +Example 1 (using the "pad" inlinefunc): + say This is $pad("a center-padded text", 50,c,-) of width 50. + -> + John says, "This is -------------- a center-padded text--------------- of width 50." + +Example 2 (using nested "pad" and "time" inlinefuncs): + say The time is $pad($time(), 30)right now. + -> + John says, "The time is Oct 25, 11:09 right now." + +To add more inline functions, add them to this module, using +the following call signature: + + def funcname(text, *args, **kwargs) + +where `text` is always the part between {funcname(args) and +{/funcname and the *args are taken from the appropriate part of the +call. If no {/funcname is given, `text` will be the empty string. + +It is important that the inline function properly clean the +incoming `args`, checking their type and replacing them with sane +defaults if needed. If impossible to resolve, the unmodified text +should be returned. The inlinefunc should never cause a traceback. + +While the inline function should accept **kwargs, the keyword is +never accepted as a valid call - this is only intended to be used +internally by Evennia, notably to send the `session` keyword to +the function; this is the session of the object viewing the string +and can be used to customize it to each session. + +""" + +# def capitalize(text, *args, **kwargs): +# "Silly capitalize example. Used as {capitalize() ... {/capitalize" +# session = kwargs.get("session") +# return text.capitalize() diff --git a/taoshengshi/server/conf/inputfuncs.py b/taoshengshi/server/conf/inputfuncs.py new file mode 100644 index 0000000..6cb226f --- /dev/null +++ b/taoshengshi/server/conf/inputfuncs.py @@ -0,0 +1,52 @@ +""" +Input functions + +Input functions are always called from the client (they handle server +input, hence the name). + +This module is loaded by being included in the +`settings.INPUT_FUNC_MODULES` tuple. + +All *global functions* included in this module are considered +input-handler functions and can be called by the client to handle +input. + +An input function must have the following call signature: + + cmdname(session, *args, **kwargs) + +Where session will be the active session and *args, **kwargs are extra +incoming arguments and keyword properties. + +A special command is the "default" command, which is will be called +when no other cmdname matches. It also receives the non-found cmdname +as argument. + + default(session, cmdname, *args, **kwargs) + +""" + +# def oob_echo(session, *args, **kwargs): +# """ +# Example echo function. Echoes args, kwargs sent to it. +# +# Args: +# session (Session): The Session to receive the echo. +# args (list of str): Echo text. +# kwargs (dict of str, optional): Keyed echo text +# +# """ +# session.msg(oob=("echo", args, kwargs)) +# +# +# def default(session, cmdname, *args, **kwargs): +# """ +# Handles commands without a matching inputhandler func. +# +# Args: +# session (Session): The active Session. +# cmdname (str): The (unmatched) command name +# args, kwargs (any): Arguments to function. +# +# """ +# pass diff --git a/taoshengshi/server/conf/lockfuncs.py b/taoshengshi/server/conf/lockfuncs.py new file mode 100644 index 0000000..9cd5046 --- /dev/null +++ b/taoshengshi/server/conf/lockfuncs.py @@ -0,0 +1,47 @@ +""" + +Lockfuncs + +Lock functions are functions available when defining lock strings, +which in turn limits access to various game systems. + +All functions defined globally in this module are assumed to be +available for use in lockstrings to determine access. See the +Evennia documentation for more info on locks. + +A lock function is always called with two arguments, accessing_obj and +accessed_obj, followed by any number of arguments. All possible +arguments should be handled with *args, **kwargs. The lock function +should handle all eventual tracebacks by logging the error and +returning False. + +Lock functions in this module extend (and will overload same-named) +lock functions from evennia.locks.lockfuncs. + +""" + +# def myfalse(accessing_obj, accessed_obj, *args, **kwargs): +# """ +# called in lockstring with myfalse(). +# A simple logger that always returns false. Prints to stdout +# for simplicity, should use utils.logger for real operation. +# """ +# print "%s tried to access %s. Access denied." % (accessing_obj, accessed_obj) +# return False + +def cmdinside(accessing_obj, accessed_obj, *args, **kwargs): + ''' + Usage: cmdinside() + + Used to lock commands and only allows access if the command + is defined on an object which accessing_obj is inside of. + ''' + return accessed_obj.obj == accessing_obj.location + +def is_a(accessing_obj, accessed_obj, expected_type=None): + ''' + Usage: is_a(typename='Example') + + Used to determine if a certain object matches expected object + ''' + return type(accessing_obj) == expected_type or accessing_obj.typename == expected_type diff --git a/taoshengshi/server/conf/mssp.py b/taoshengshi/server/conf/mssp.py new file mode 100644 index 0000000..711447b --- /dev/null +++ b/taoshengshi/server/conf/mssp.py @@ -0,0 +1,105 @@ +""" + +MSSP (Mud Server Status Protocol) meta information + +Modify this file to specify what MUD listing sites will report about your game. +All fields are static. The number of currently active players and your game's +current uptime will be added automatically by Evennia. + +You don't have to fill in everything (and most fields are not shown/used by all +crawlers anyway); leave the default if so needed. You need to reload the server +before the updated information is made available to crawlers (reloading does +not affect uptime). + +After changing the values in this file, you must register your game with the +MUD website list you want to track you. The listing crawler will then regularly +connect to your server to get the latest info. No further configuration is +needed on the Evennia side. + +""" + +MSSPTable = { + # Required fields + "NAME": "Evennia", + # Generic + "CRAWL DELAY": "-1", # limit how often crawler updates the listing. -1 for no limit + "HOSTNAME": "", # current or new hostname + "PORT": ["4000"], # most important port should be *last* in list! + "CODEBASE": "Evennia", + "CONTACT": "", # email for contacting the mud + "CREATED": "", # year MUD was created + "ICON": "", # url to icon 32x32 or larger; <32kb. + "IP": "", # current or new IP address + "LANGUAGE": "", # name of language used, e.g. English + "LOCATION": "", # full English name of server country + "MINIMUM AGE": "0", # set to 0 if not applicable + "WEBSITE": "www.evennia.com", + # Categorisation + "FAMILY": "Custom", # evennia goes under 'Custom' + "GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction + # Gameplay: Adventure, Educational, Hack and Slash, None, + # Player versus Player, Player versus Environment, + # Roleplaying, Simulation, Social or Strategy + "GAMEPLAY": "", + "STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live + "GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew + # Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein, + # Cyberpunk, Dragonlance, etc. Or None if not available. + "SUBGENRE": "None", + # World + "AREAS": "0", + "HELPFILES": "0", + "MOBILES": "0", + "OBJECTS": "0", + "ROOMS": "0", # use 0 if room-less + "CLASSES": "0", # use 0 if class-less + "LEVELS": "0", # use 0 if level-less + "RACES": "0", # use 0 if race-less + "SKILLS": "0", # use 0 if skill-less + # Protocols set to 1 or 0) + "ANSI": "1", + "GMCP": "1", + "MSDP": "1", + "MXP": "1", + "SSL": "1", + "UTF-8": "1", + "MCCP": "1", + "XTERM 256 COLORS": "1", + "XTERM TRUE COLORS": "0", + "ATCP": "0", + "MCP": "0", + "MSP": "0", + "VT100": "0", + "PUEBLO": "0", + "ZMP": "0", + # Commercial set to 1 or 0) + "PAY TO PLAY": "0", + "PAY FOR PERKS": "0", + # Hiring set to 1 or 0) + "HIRING BUILDERS": "0", + "HIRING CODERS": "0", + # Extended variables + # World + "DBSIZE": "0", + "EXITS": "0", + "EXTRA DESCRIPTIONS": "0", + "MUDPROGS": "0", + "MUDTRIGS": "0", + "RESETS": "0", + # Game (set to 1 or 0, or one of the given alternatives) + "ADULT MATERIAL": "0", + "MULTICLASSING": "0", + "NEWBIE FRIENDLY": "0", + "PLAYER CITIES": "0", + "PLAYER CLANS": "0", + "PLAYER CRAFTING": "0", + "PLAYER GUILDS": "0", + "EQUIPMENT SYSTEM": "None", # "None", "Level", "Skill", "Both" + "MULTIPLAYING": "None", # "None", "Restricted", "Full" + "PLAYERKILLING": "None", # "None", "Restricted", "Full" + "QUEST SYSTEM": "None", # "None", "Immortal Run", "Automated", "Integrated" + "ROLEPLAYING": "None", # "None", "Accepted", "Encouraged", "Enforced" + "TRAINING SYSTEM": "None", # "None", "Level", "Skill", "Both" + # World originality: "All Stock", "Mostly Stock", "Mostly Original", "All Original" + "WORLD ORIGINALITY": "All Original", +} diff --git a/taoshengshi/server/conf/portal_services_plugins.py b/taoshengshi/server/conf/portal_services_plugins.py new file mode 100644 index 0000000..b536c56 --- /dev/null +++ b/taoshengshi/server/conf/portal_services_plugins.py @@ -0,0 +1,24 @@ +""" +Start plugin services + +This plugin module can define user-created services for the Portal to +start. + +This module must handle all imports and setups required to start +twisted services (see examples in evennia.server.portal.portal). It +must also contain a function start_plugin_services(application). +Evennia will call this function with the main Portal application (so +your services can be added to it). The function should not return +anything. Plugin services are started last in the Portal startup +process. + +""" + + +def start_plugin_services(portal): + """ + This hook is called by Evennia, last in the Portal startup process. + + portal - a reference to the main portal application. + """ + pass diff --git a/taoshengshi/server/conf/server_services_plugins.py b/taoshengshi/server/conf/server_services_plugins.py new file mode 100644 index 0000000..e3d41fe --- /dev/null +++ b/taoshengshi/server/conf/server_services_plugins.py @@ -0,0 +1,24 @@ +""" + +Server plugin services + +This plugin module can define user-created services for the Server to +start. + +This module must handle all imports and setups required to start a +twisted service (see examples in evennia.server.server). It must also +contain a function start_plugin_services(application). Evennia will +call this function with the main Server application (so your services +can be added to it). The function should not return anything. Plugin +services are started last in the Server startup process. + +""" + + +def start_plugin_services(server): + """ + This hook is called by Evennia, last in the Server startup process. + + server - a reference to the main server application. + """ + pass diff --git a/taoshengshi/server/conf/serversession.py b/taoshengshi/server/conf/serversession.py new file mode 100644 index 0000000..13fbf1e --- /dev/null +++ b/taoshengshi/server/conf/serversession.py @@ -0,0 +1,37 @@ +""" +ServerSession + +The serversession is the Server-side in-memory representation of a +user connecting to the game. Evennia manages one Session per +connection to the game. So a user logged into the game with multiple +clients (if Evennia is configured to allow that) will have multiple +sessions tied to one Account object. All communication between Evennia +and the real-world user goes through the Session(s) associated with that user. + +It should be noted that modifying the Session object is not usually +necessary except for the most custom and exotic designs - and even +then it might be enough to just add custom session-level commands to +the SessionCmdSet instead. + +This module is not normally called. To tell Evennia to use the class +in this module instead of the default one, add the following to your +settings file: + + SERVER_SESSION_CLASS = "server.conf.serversession.ServerSession" + +""" + +from evennia.server.serversession import ServerSession as BaseServerSession + + +class ServerSession(BaseServerSession): + """ + This class represents a player's session and is a template for + individual protocols to communicate with Evennia. + + Each account gets one or more sessions assigned to them whenever they connect + to the game server. All communication between game and account goes + through their session(s). + """ + + pass diff --git a/taoshengshi/server/conf/settings.py b/taoshengshi/server/conf/settings.py new file mode 100644 index 0000000..551f226 --- /dev/null +++ b/taoshengshi/server/conf/settings.py @@ -0,0 +1,44 @@ +r""" +Evennia settings file. + +The available options are found in the default settings file found +here: + +/usr/src/evennia/evennia/settings_default.py + +Remember: + +Don't copy more from the default file than you actually intend to +change; this will make sure that you don't overload upstream updates +unnecessarily. + +When changing a setting requiring a file system path (like +path/to/actual/file.py), use GAME_DIR and EVENNIA_DIR to reference +your game folder and the Evennia library folders respectively. Python +paths (path.to.module) should be given relative to the game's root +folder (typeclasses.foo) whereas paths within the Evennia library +needs to be given explicitly (evennia.foo). + +If you want to share your game dir, including its settings, you can +put secret game- or server-specific settings in secret_settings.py. + +""" + +# Use the defaults from Evennia unless explicitly overridden +from evennia.settings_default import * + +###################################################################### +# Evennia base server config +###################################################################### + +# This is the name of your game. Make it catchy! +SERVERNAME = "taoshengshi" + + +###################################################################### +# Settings given in secret_settings.py override those in this file. +###################################################################### +try: + from server.conf.secret_settings import * +except ImportError: + print("secret_settings.py file not found or failed to import.") diff --git a/taoshengshi/server/conf/web_plugins.py b/taoshengshi/server/conf/web_plugins.py new file mode 100644 index 0000000..4050a82 --- /dev/null +++ b/taoshengshi/server/conf/web_plugins.py @@ -0,0 +1,28 @@ +""" +Web plugin hooks. +""" + + +def at_webserver_root_creation(web_root): + """ + This is called as the web server has finished building its default + path tree. At this point, the media/ and static/ URIs have already + been added to the web root. + + Args: + web_root (twisted.web.resource.Resource): The root + resource of the URI tree. Use .putChild() to + add new subdomains to the tree. + + Returns: + web_root (twisted.web.resource.Resource): The potentially + modified root structure. + + Example: + from twisted.web import static + my_page = static.File("web/mypage/") + my_page.indexNames = ["index.html"] + web_root.putChild("mypage", my_page) + + """ + return web_root diff --git a/taoshengshi/server/logs/README.md b/taoshengshi/server/logs/README.md new file mode 100644 index 0000000..35ad999 --- /dev/null +++ b/taoshengshi/server/logs/README.md @@ -0,0 +1,15 @@ +This directory contains Evennia's log files. The existence of this README.md file is also necessary +to correctly include the log directory in git (since log files are ignored by git and you can't +commit an empty directory). + +- `server.log` - log file from the game Server. +- `portal.log` - log file from Portal proxy (internet facing) + +Usually these logs are viewed together with `evennia -l`. They are also rotated every week so as not +to be too big. Older log names will have a name appended by `_month_date`. + +- `lockwarnings.log` - warnings from the lock system. +- `http_requests.log` - this will generally be empty unless turning on debugging inside the server. + +- `channel_.log` - these are channel logs for the in-game channels They are also used + by the `/history` flag in-game to get the latest message history. diff --git a/taoshengshi/typeclasses/README.md b/taoshengshi/typeclasses/README.md new file mode 100644 index 0000000..e114e59 --- /dev/null +++ b/taoshengshi/typeclasses/README.md @@ -0,0 +1,16 @@ +# typeclasses/ + +This directory holds the modules for overloading all the typeclasses +representing the game entities and many systems of the game. Other +server functionality not covered here is usually modified by the +modules in `server/conf/`. + +Each module holds empty classes that just imports Evennia's defaults. +Any modifications done to these classes will overload the defaults. + +You can change the structure of this directory (even rename the +directory itself) as you please, but if you do you must add the +appropriate new paths to your settings.py file so Evennia knows where +to look. Also remember that for Python to find your modules, it +requires you to add an empty `__init__.py` file in any new sub +directories you create. diff --git a/taoshengshi/typeclasses/__init__.py b/taoshengshi/typeclasses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taoshengshi/typeclasses/accounts.py b/taoshengshi/typeclasses/accounts.py new file mode 100644 index 0000000..ba293c6 --- /dev/null +++ b/taoshengshi/typeclasses/accounts.py @@ -0,0 +1,104 @@ +""" +Account + +The Account represents the game "account" and each login has only one +Account object. An Account is what chats on default channels but has no +other in-game-world existence. Rather the Account puppets Objects (such +as Characters) in order to actually participate in the game world. + + +Guest + +Guest accounts are simple low-level accounts that are created/deleted +on the fly and allows users to test the game without the commitment +of a full registration. Guest accounts are deactivated by default; to +activate them, add the following line to your settings file: + + GUEST_ENABLED = True + +You will also need to modify the connection screen to reflect the +possibility to connect with a guest account. The setting file accepts +several more options for customizing the Guest account system. + +""" + +from evennia import DefaultAccount, DefaultGuest + + +class Account(DefaultAccount): + """ + This class describes the actual OOC account (i.e. the user connecting + to the MUD). It does NOT have visual appearance in the game world (that + is handled by the character which is connected to this). Comm channels + are attended/joined using this object. + + It can be useful e.g. for storing configuration options for your game, but + should generally not hold any character-related info (that's best handled + on the character level). + + Can be set using BASE_ACCOUNT_TYPECLASS. + + + * available properties + + key (string) - name of account + name (string)- wrapper for user.username + aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + user (User, read-only) - django User authorization object + obj (Object) - game object controlled by account. 'character' can also be used. + sessions (list of Sessions) - sessions connected to this account + is_superuser (bool, read-only) - if the connected user is a superuser + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods + + msg(text=None, **kwargs) + execute_cmd(raw_string, session=None) + search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False) + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hook methods (when re-implementation, remember methods need to have self as first arg) + + basetype_setup() + at_account_creation() + + - note that the following hooks are also found on Objects and are + usually handled on the character level: + + at_init() + at_cmdset_get(**kwargs) + at_first_login() + at_post_login(session=None) + at_disconnect() + at_message_receive() + at_message_send() + at_server_reload() + at_server_shutdown() + + """ + + pass + + +class Guest(DefaultGuest): + """ + This class is used for guest logins. Unlike Accounts, Guests and their + characters are deleted after disconnection. + """ + + pass diff --git a/taoshengshi/typeclasses/channels.py b/taoshengshi/typeclasses/channels.py new file mode 100644 index 0000000..0b943d0 --- /dev/null +++ b/taoshengshi/typeclasses/channels.py @@ -0,0 +1,62 @@ +""" +Channel + +The channel class represents the out-of-character chat-room usable by +Accounts in-game. It is mostly overloaded to change its appearance, but +channels can be used to implement many different forms of message +distribution systems. + +Note that sending data to channels are handled via the CMD_CHANNEL +syscommand (see evennia.syscmds). The sending should normally not need +to be modified. + +""" + +from evennia import DefaultChannel + + +class Channel(DefaultChannel): + """ + Working methods: + at_channel_creation() - called once, when the channel is created + has_connection(account) - check if the given account listens to this channel + connect(account) - connect account to this channel + disconnect(account) - disconnect account from channel + access(access_obj, access_type='listen', default=False) - check the + access on this channel (default access_type is listen) + delete() - delete this channel + message_transform(msg, emit=False, prefix=True, + sender_strings=None, external=False) - called by + the comm system and triggers the hooks below + msg(msgobj, header=None, senders=None, sender_strings=None, + persistent=None, online=False, emit=False, external=False) - main + send method, builds and sends a new message to channel. + tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent + messages. + distribute_message(msg, online=False) - send a message to all + connected accounts on channel, optionally sending only + to accounts that are currently online (optimized for very large sends) + + Useful hooks: + channel_prefix(msg, emit=False) - how the channel should be + prefixed when returning to user. Returns a string + format_senders(senders) - should return how to display multiple + senders to a channel + pose_transform(msg, sender_string) - should detect if the + sender is posing, and if so, modify the string + format_external(msg, senders, emit=False) - format messages sent + from outside the game, like from IRC + format_message(msg, emit=False) - format the message body before + displaying it to the user. 'emit' generally means that the + message should not be displayed with the sender's name. + + pre_join_channel(joiner) - if returning False, abort join + post_join_channel(joiner) - called right after successful join + pre_leave_channel(leaver) - if returning False, abort leave + post_leave_channel(leaver) - called right after successful leave + pre_send_message(msg) - runs just before a message is sent to channel + post_send_message(msg) - called just after message was sent to channel + + """ + + pass diff --git a/taoshengshi/typeclasses/characters.py b/taoshengshi/typeclasses/characters.py new file mode 100644 index 0000000..10e098c --- /dev/null +++ b/taoshengshi/typeclasses/characters.py @@ -0,0 +1,34 @@ +""" +Characters + +Characters are (by default) Objects setup to be puppeted by Accounts. +They are what you "see" in game. The Character class in this module +is setup to be the "default" character type created by the default +creation commands. + +""" +from evennia import DefaultCharacter + + +class Character(DefaultCharacter): + """ + The Character defaults to reimplementing some of base Object's hook methods with the + following functionality: + + at_basetype_setup - always assigns the DefaultCmdSet to this object type + (important!)sets locks so character cannot be picked up + and its commands only be called by itself, not anyone else. + (to change things, use at_object_creation() instead). + at_after_move(source_location) - Launches the "look" command after every move. + at_post_unpuppet(account) - when Account disconnects from the Character, we + store the current location in the pre_logout_location Attribute and + move it to a None-location so the "unpuppeted" character + object does not need to stay on grid. Echoes "Account has disconnected" + to the room. + at_pre_puppet - Just before Account re-connects, retrieves the character's + pre_logout_location Attribute and move it back on the grid. + at_post_puppet - Echoes "AccountName has entered the game" to the room. + + """ + + # TODO: Override return_appearance diff --git a/taoshengshi/typeclasses/exits.py b/taoshengshi/typeclasses/exits.py new file mode 100644 index 0000000..0dc2bc5 --- /dev/null +++ b/taoshengshi/typeclasses/exits.py @@ -0,0 +1,64 @@ +""" +Exits +Exits are connectors between Rooms. An exit always has a destination property +set and has a single command defined on itself with the same name as its key, +for allowing Characters to traverse the exit to its destination. + +""" +from evennia import DefaultExit +from commands.keypad import CmdSetKeypad +from evennia.utils import delay + +class Exit(DefaultExit): + """ + Exits are connectors between rooms. Exits are normal Objects except + they defines the `destination` property. It also does work in the + following methods: + + basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead). + at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should + rebuild the Exit cmdset along with a command matching the name + of the Exit object. Conventionally, a kwarg `force_init` + should force a rebuild of the cmdset, this is triggered + by the `@alias` command when aliases are changed. + at_failed_traverse() - gives a default error message ("You cannot + go there") if exit traversal fails and an + attribute `err_traverse` is not defined. + + Relevant hooks to overload (compared to other types of Objects): + at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks. + If overloading this, consider using super() to use the default + movement implementation (and hook-calling). + at_after_traverse(traveller, source_loc) - called by at_traverse just after traversing. + at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will + not be called if the attribute `err_traverse` is + defined, in which case that will simply be echoed. + """ + + def return_appearance(self, looker, **kwargs): + 'Modified version of what users normally see when using look' + if not looker: + return '' + + return self.db.desc + +class KeypadDoor(Exit): + def at_object_creation(self): + self.cmdset.add(CmdSetKeypad, permanent=True) + self.db.keycode = '0000' + self.db.desc = 'You see a locked door, |xunlock|n it with the keypad' + + def at_traverse(self, traveller, target_loc): + if self.db.locked: + traveller.msg('The door is locked, |xuse the keypad|n to unlock it') + return + self.db.locked = True + traveller.msg('You hear the door lock itself again') + super().at_traverse(traveller, target_loc) + + def attempt_unlock(self, traveller, keycode): + if keycode == self.db.keycode: + traveller.msg("The keypad flashes green, the door is unlocked") + self.db.locked = False + else: + traveller.msg(f"The keypad flashes red, {keycode} is not the correct keycode") diff --git a/taoshengshi/typeclasses/mech.py b/taoshengshi/typeclasses/mech.py new file mode 100644 index 0000000..dbb0ced --- /dev/null +++ b/taoshengshi/typeclasses/mech.py @@ -0,0 +1,50 @@ +''' +Mech typeclass +''' + +from typeclasses.objects import Object +from commands.mech_cmds import CmdSetMech + +DESC_INSIDE = ''' +You see the insides of a large mech. + +Glancing at the controls you see you can do the following: + look through the |xHUD|n + |xdrive|n to a destination + |xexit|n the mech +'''.strip() + +class Mech(Object): + ''' + Mech typeclass + ''' + def at_object_creation(self): + 'Called once when object is created' + self.locks.add('get:none()') # Restrict ability to be put in inventory + self.db.get_err_msg = "You can't possibly lift this" + + self.db.desc_inside = DESC_INSIDE + self.cmdset.add_default(CmdSetMech, permanent=True) + + def msg_contents(self, text=None, exclude=[], from_obj=None, mapping=None, **kwargs): + 'Intercept in order to relay messages from pilot to the room' + super().msg_contents(text=text, exclude=exclude, from_obj=from_obj, mapping=mapping) + + # Relay messages from pilot to room + if exclude: + # Evennia will pass a tuple to this method...I convert it because I hate tuples + # TODO: Embrace the tuple and fix my implementation + exclude = list(exclude).append(self) + self.location.msg_contents(text=text, exclude=exclude, from_obj=from_obj, mapping=mapping) + + def at_msg_receive(self, text=None, exclude=None, from_obj=None, **kwargs): + 'Relay messages to pilots' + # Prevent loop when pilot uses say + self.msg_contents(text=text, exclude=exclude, from_obj=from_obj) + + def return_appearance(self, looker=None): + 'What a user sees when using look' + if looker in self.contents: + # return self.db.desc_inside + return self.db.desc_inside + return 'You see a large mech. It looks like it has missiles and stuff.' diff --git a/taoshengshi/typeclasses/objects.py b/taoshengshi/typeclasses/objects.py new file mode 100644 index 0000000..4883880 --- /dev/null +++ b/taoshengshi/typeclasses/objects.py @@ -0,0 +1,176 @@ +""" +Object + +The Object is the "naked" base class for things in the game world. + +Note that the default Character, Room and Exit does not inherit from +this Object, but from their respective default implementations in the +evennia library. If you want to use this class as a parent to change +the other types, you can do so by adding this as a multiple +inheritance. + +""" +from evennia import DefaultObject + + +class Object(DefaultObject): + """ + This is the root typeclass object, implementing an in-game Evennia + game object, such as having a location, being able to be + manipulated or looked at, etc. If you create a new typeclass, it + must always inherit from this object (or any of the other objects + in this file, since they all actually inherit from BaseObject, as + seen in src.object.objects). + + The BaseObject class implements several hooks tying into the game + engine. By re-implementing these hooks you can control the + system. You should never need to re-implement special Python + methods, such as __init__ and especially never __getattribute__ and + __setattr__ since these are used heavily by the typeclass system + of Evennia and messing with them might well break things for you. + + + * Base properties defined/available on all Objects + + key (string) - name of object + name (string)- same as key + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + + account (Account) - controlling account (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + account above). Use `sessions` handler to get the + Sessions directly. + location (Object) - current location. Is None if this is a room + home (Object) - safety start-location + has_account (bool, read-only)- will only return *connected* accounts + contents (list of Objects, read-only) - returns all objects inside this + object (including exits) + exits (list of Objects, read-only) - returns all exits from this + object, if any + destination (Object) - only set if this object is an exit. + is_superuser (bool, read-only) - True/False if this user is a superuser + + * Handlers available + + aliases - alias-handler: use aliases.add/remove/get() to use. + permissions - permission-handler: use permissions.add/remove() to + add/remove new perms. + locks - lock-handler: use locks.add() to add new lock strings + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + sessions - sessions-handler. Get Sessions connected to this + object with sessions.get() + attributes - attribute-handler. Use attributes.add/remove/get. + db - attribute-handler: Shortcut for attribute-handler. Store/retrieve + database attributes using self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create + a database entry when storing data + + * Helper methods (see src.objects.objects.py for full headers) + + search(ostring, global_search=False, attribute_name=None, + use_nicks=False, location=None, ignore_errors=False, account=False) + execute_cmd(raw_string) + msg(text=None, **kwargs) + msg_contents(message, exclude=None, from_obj=None, **kwargs) + move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) + copy(new_key=None) + delete() + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hooks (these are class methods, so args should start with self): + + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the object + has been created; Normally not modified. + + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If returning + False, deletion is aborted. Note that all objects + inside a deleted object are automatically moved + to their , they don't need to be removed here. + + at_init() - called whenever typeclass is cached from memory, + at least once every server restart/reload + at_cmdset_get(**kwargs) - this is called just before the command handler + requests a cmdset from this object. The kwargs are + not normally used unless the cmdset is created + dynamically (see e.g. Exits). + at_pre_puppet(account)- (account-controlled objects only) called just + before puppeting + at_post_puppet() - (account-controlled objects only) called just + + after completing connection account<->object + at_pre_unpuppet() - (account-controlled objects only) called just + before un-puppeting + at_post_unpuppet(account) - (account-controlled objects only) called just + after disconnecting account<->object link + at_server_reload() - called before server is reloaded + at_server_shutdown() - called just before server is fully shut down + + at_access(result, accessing_obj, access_type) - called with the result + of a lock access check on this object. Return value + does not affect check result. + + at_before_move(destination) - called just before moving object + to the destination. If returns False, move is cancelled. + announce_move_from(destination) - called in old location, just + before move, if obj.move_to() has quiet=False + announce_move_to(source_location) - called in new location, just + after move, if obj.move_to() has quiet=False + at_after_move(source_location) - always called after a move has + been successfully performed. + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_object_receive(obj, source_location) - called when this object receives + another object + + at_traverse(traversing_object, source_loc) - (exit-objects only) + handles all moving across the exit, including + calling the other exit hooks. Use super() to retain + the default functionality. + at_after_traverse(traversing_object, source_location) - (exit-objects only) + called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called if + traversal fails and property err_traverse is not defined. + + at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message + (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects + sends a message to someone via self.msg(). + + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the + appearance is requested. + at_get(getter) - called after object has been picked up. + Does not stop pickup. + at_drop(dropper) - called when this object has been dropped. + at_say(speaker, message) - by default, called if an object inside this + object speaks + + """ + + def return_appearance(self, looker, **kwargs): + 'Modified version of what users normally see when using look' + if not looker: + return '' + + return self.db.desc + +class ImmovableObject(Object): + 'Objects that are not "gettable" and do not show when using look' + + def at_object_creation(self): + self.locks.add('get:none()') + self.db.get_err_msg = "You can't possibly lift this" + self.db.immovable = True diff --git a/taoshengshi/typeclasses/rooms.py b/taoshengshi/typeclasses/rooms.py new file mode 100644 index 0000000..d846b99 --- /dev/null +++ b/taoshengshi/typeclasses/rooms.py @@ -0,0 +1,52 @@ +""" +Room + +Rooooooms +""" + +from evennia import DefaultRoom +from evennia import DefaultObject +from evennia.locks.lockhandler import check_lockstring +from evennia.utils.utils import list_to_string + +class Room(DefaultRoom): + def check_perm(self, caller, permission): + return check_lockstring(caller, f"dummy:perm({permission})") + + def return_appearance(self, looker, exclude=(), **kwargs): + """ + This is called whenever someone runs the 'look' command. + + Args: + looker (Object): Object doing the looking. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + """ + output = f"{self.db.desc}" + + users, other_objs = [], [] + contents = self.contents + if exclude: + contents = list(set(contents) - set(exclude)) + for obj in contents: + if obj.id == looker.id: + continue + obj_name = obj.get_numbered_name(1, looker)[0] + if obj.is_connected: + users.append(obj.get_display_name(looker)) + if not obj.destination and not obj.has_account and not obj.db.immovable: + other_objs.append(obj_name) + if users: + output += f"\nThese people are here: {list_to_string(users)}" + if other_objs: + output += f"\nYou also see: {list_to_string(other_objs)}" + + admin_only_objs = [] + if self.check_perm(looker, 'Admin'): + for obj in contents: + if obj.id != looker.id: + admin_only_objs.append(obj.get_display_name(looker)) + if admin_only_objs: + output += f"\n\n|xAdmin only: {list_to_string(admin_only_objs)}" + + return output diff --git a/taoshengshi/typeclasses/scripts.py b/taoshengshi/typeclasses/scripts.py new file mode 100644 index 0000000..b36db5c --- /dev/null +++ b/taoshengshi/typeclasses/scripts.py @@ -0,0 +1,92 @@ +""" +Scripts + +Scripts are powerful jacks-of-all-trades. They have no in-game +existence and can be used to represent persistent game systems in some +circumstances. Scripts can also have a time component that allows them +to "fire" regularly or a limited number of times. + +There is generally no "tree" of Scripts inheriting from each other. +Rather, each script tends to inherit from the base Script class and +just overloads its hooks to have it perform its function. + +""" + +from evennia import DefaultScript + + +class Script(DefaultScript): + """ + A script type is customized by redefining some or all of its hook + methods and variables. + + * available properties + + key (string) - name of object + name (string)- same as key + aliases (list of strings) - aliases to the object. Will be saved + to database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + desc (string) - optional description of script, shown in listings + obj (Object) - optional object that this script is connected to + and acts on (set automatically by obj.scripts.add()) + interval (int) - how often script should run, in seconds. <0 turns + off ticker + start_delay (bool) - if the script should start repeating right away or + wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. 0 means infinite repeats + persistent (bool) - if script should survive a server shutdown or not + is_active (bool) - if script is currently running + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + + * Helper methods + + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) + stop() - stop script, and delete it + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will continue + from the paused timer (but at_start() will be called). + time_until_next_repeat() - if a timed script (interval>0), returns time + until next tick + + * Hook methods (should also include self as the first argument): + + at_script_creation() - called only once, when an object of this + class is first created. + is_valid() - is called to check if the script is valid to be running + at the current time. If is_valid() returns False, the running + script is stopped and removed from the game. You can use this + to check state changes (i.e. an script tracking some combat + stats at regular intervals is only valid to run while there is + actual combat going on). + at_start() - Called every time the script is started, which for persistent + scripts is at least once every server start. Note that this is + unaffected by self.delay_start, which only delays the first + call to at_repeat(). + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, which + will delay the first call of this method by self.interval + seconds. If self.interval==0, this method will never + be called. + at_stop() - Called as the script object is stopped and is about to be + removed from the game, e.g. because is_valid() returned False. + at_server_reload() - Called when server reloads. Can be used to + save temporary variables you want should survive a reload. + at_server_shutdown() - called at a full server shutdown. + + """ + + pass diff --git a/taoshengshi/typeclasses/test_mech.py b/taoshengshi/typeclasses/test_mech.py new file mode 100644 index 0000000..59a7e22 --- /dev/null +++ b/taoshengshi/typeclasses/test_mech.py @@ -0,0 +1,34 @@ +''' +Tests for Mech +''' + +from evennia.commands.default.tests import CommandTest +from evennia.commands.default import general +from evennia import create_object + +from typeclasses import rooms, mech + +class TestMech(CommandTest): + ''' + Tests for Mech + ''' + def setUp(self): + 'Create a test mech' + super().setUp() + self.mech = create_object(mech.Mech, key='a giant mech', + location=self.room1, aliases=['mech']) + + def test_return_appearance(self): + 'Tests for look behavior' + self.call(general.CmdLook(), 'mech', "You see a large mech. It looks like it has missiles and stuff.") + self.char1.move_to(self.mech) + self.call(general.CmdLook(), 'mech', "You see the insides of a large mech.") + self.char1.move_to(self.room1) + + def test_at_object_creation(self): + 'Tests for at_object_creation method' + + # Need to remove permissions to test locks + self.char1.account.permissions.remove('Developer') + + self.call(general.CmdGet(), 'mech', "You can't possibly lift this") diff --git a/taoshengshi/typeclasses/test_rooms.py b/taoshengshi/typeclasses/test_rooms.py new file mode 100644 index 0000000..832095b --- /dev/null +++ b/taoshengshi/typeclasses/test_rooms.py @@ -0,0 +1,42 @@ +''' +Tests for Room +''' + +from evennia.utils.test_resources import EvenniaTest +from evennia import create_object + +from typeclasses import rooms, objects +import re + +class TestRoom(EvenniaTest): + 'Tests for Room' + def setUp(self): + ''' + Create Test Room, place objects in it, and move Char 1 to it + ''' + super().setUp() + self.test_room = create_object(rooms.Room, key='Test Room') + + self.giant_stone = create_object(objects.ImmovableObject, key='a giant stone', + location=self.test_room, aliases=['stone']) + self.giant_stone.locks.add('get:none()') + self.giant_stone.db.get_err_msg = "You can't possibly lift this" + + self.bobble = create_object(objects.Object, key='a bobble', + location=self.test_room, aliases=['bobble']) + self.bobble.db.desc = "A bobble that you can carry" + + self.char1.location = self.test_room + + self.char2.name = 'John' + self.char2.location = self.test_room + self.char2.account.is_connected = True + + def test_return_appearance_when_admin(self): + 'Should return output containing "Admin only:"' + self.assertIn('Admin only:', self.test_room.return_appearance(self.char1)) + + def test_return_appearance_when_not_admin(self): + 'Admin only output is not present when user is not an admin' + self.char1.account.permissions.remove('Developer') + self.assertIsNone(re.search(r'Admin only', self.test_room.return_appearance(self.char1))) diff --git a/taoshengshi/web/__init__.py b/taoshengshi/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taoshengshi/web/static_overrides/README.md b/taoshengshi/web/static_overrides/README.md new file mode 100644 index 0000000..ab9a09e --- /dev/null +++ b/taoshengshi/web/static_overrides/README.md @@ -0,0 +1,13 @@ +If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, +copy it into this directory's corresponding subdirectory, and it will be placed in the static folder when you run: + + python manage.py collectstatic + +...or when you reload the server via the command line. + +Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place. + +Also note that you may need to clear out existing static files for your new ones to be gathered in some cases. Deleting files in static/ +will force them to be recollected. + +To see what files can be overridden, find where your evennia package is installed, and look in `evennia/web/static/` diff --git a/taoshengshi/web/static_overrides/webclient/css/README.md b/taoshengshi/web/static_overrides/webclient/css/README.md new file mode 100644 index 0000000..6ab7cbb --- /dev/null +++ b/taoshengshi/web/static_overrides/webclient/css/README.md @@ -0,0 +1,3 @@ +You can replace the CSS files for Evennia's webclient here. + +You can find the original files in `evennia/web/static/webclient/css/` diff --git a/taoshengshi/web/static_overrides/webclient/js/README.md b/taoshengshi/web/static_overrides/webclient/js/README.md new file mode 100644 index 0000000..c785cb1 --- /dev/null +++ b/taoshengshi/web/static_overrides/webclient/js/README.md @@ -0,0 +1,3 @@ +You can replace the javascript files for Evennia's webclient page here. + +You can find the original files in `evennia/web/static/webclient/js/` diff --git a/taoshengshi/web/static_overrides/website/css/README.md b/taoshengshi/web/static_overrides/website/css/README.md new file mode 100644 index 0000000..004fcd8 --- /dev/null +++ b/taoshengshi/web/static_overrides/website/css/README.md @@ -0,0 +1,3 @@ +You can replace the CSS files for Evennia's homepage here. + +You can find the original files in `evennia/web/static/website/css/` diff --git a/taoshengshi/web/static_overrides/website/images/README.md b/taoshengshi/web/static_overrides/website/images/README.md new file mode 100644 index 0000000..2d2060c --- /dev/null +++ b/taoshengshi/web/static_overrides/website/images/README.md @@ -0,0 +1,3 @@ +You can replace the image files for Evennia's home page here. + +You can find the original files in `evennia/web/static/website/images/` diff --git a/taoshengshi/web/template_overrides/README.md b/taoshengshi/web/template_overrides/README.md new file mode 100644 index 0000000..87ba6f1 --- /dev/null +++ b/taoshengshi/web/template_overrides/README.md @@ -0,0 +1,4 @@ +Place your own version of templates into this file to override the default ones. +For instance, if there's a template at: `evennia/web/website/templates/website/index.html` +and you want to replace it, create the file `template_overrides/website/index.html` +and it will be loaded instead. diff --git a/taoshengshi/web/template_overrides/webclient/README.md b/taoshengshi/web/template_overrides/webclient/README.md new file mode 100644 index 0000000..b69d627 --- /dev/null +++ b/taoshengshi/web/template_overrides/webclient/README.md @@ -0,0 +1,3 @@ +Replace Evennia's webclient django templates with your own here. + +You can find the original files in `evennia/web/webclient/templates/webclient/` diff --git a/taoshengshi/web/template_overrides/website/README.md b/taoshengshi/web/template_overrides/website/README.md new file mode 100644 index 0000000..589823a --- /dev/null +++ b/taoshengshi/web/template_overrides/website/README.md @@ -0,0 +1,7 @@ +You can replace the django templates (html files) for the website +here. It uses the default "prosimii" theme. If you want to maintain +multiple themes rather than just change the default one in-place, +make new folders under `template_overrides/` and change +`settings.ACTIVE_THEME` to point to the folder name to use. + +You can find the original files under `evennia/web/website/templates/website/` diff --git a/taoshengshi/web/template_overrides/website/flatpages/README.md b/taoshengshi/web/template_overrides/website/flatpages/README.md new file mode 100644 index 0000000..9cd8142 --- /dev/null +++ b/taoshengshi/web/template_overrides/website/flatpages/README.md @@ -0,0 +1,3 @@ +Flatpages require a default.html template, which can be overwritten by placing it in this folder. + +You can find the original files in `evennia/web/website/templates/website/flatpages/` diff --git a/taoshengshi/web/template_overrides/website/registration/README.md b/taoshengshi/web/template_overrides/website/registration/README.md new file mode 100644 index 0000000..7c0dfbe --- /dev/null +++ b/taoshengshi/web/template_overrides/website/registration/README.md @@ -0,0 +1,3 @@ +The templates involving login/logout can be overwritten here. + +You can find the original files in `evennia/web/website/templates/website/registration/` diff --git a/taoshengshi/web/urls.py b/taoshengshi/web/urls.py new file mode 100644 index 0000000..741706c --- /dev/null +++ b/taoshengshi/web/urls.py @@ -0,0 +1,18 @@ +""" +Url definition file to redistribute incoming URL requests to django +views. Search the Django documentation for "URL dispatcher" for more +help. + +""" +from django.conf.urls import url, include + +# default evennia patterns +from evennia.web.urls import urlpatterns + +# eventual custom patterns +custom_patterns = [ + # url(r'/desired/url/', view, name='example'), +] + +# this is required by Django. +urlpatterns = custom_patterns + urlpatterns diff --git a/taoshengshi/world/.pylintrc b/taoshengshi/world/.pylintrc new file mode 100644 index 0000000..098a8da --- /dev/null +++ b/taoshengshi/world/.pylintrc @@ -0,0 +1,8 @@ +[MESSAGE CONTROL] + +disable=C0103, E0401 + +[VARIABLES] + +additional-builtins=caller + diff --git a/taoshengshi/world/README.md b/taoshengshi/world/README.md new file mode 100644 index 0000000..0f3862d --- /dev/null +++ b/taoshengshi/world/README.md @@ -0,0 +1,10 @@ +# world/ + +This folder is meant as a miscellanous folder for all that other stuff +related to the game. Code which are not commands or typeclasses go +here, like custom economy systems, combat code, batch-files etc. + +You can restructure and even rename this folder as best fits your +sense of organisation. Just remember that if you add new sub +directories, you must add (optionally empty) `__init__.py` files in +them for Python to be able to find the modules within. diff --git a/taoshengshi/world/__init__.py b/taoshengshi/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taoshengshi/world/prototypes.py b/taoshengshi/world/prototypes.py new file mode 100644 index 0000000..b64dd1b --- /dev/null +++ b/taoshengshi/world/prototypes.py @@ -0,0 +1,73 @@ +""" +Prototypes + +A prototype is a simple way to create individualized instances of a +given `Typeclass`. For example, you might have a Sword typeclass that +implements everything a Sword would need to do. The only difference +between different individual Swords would be their key, description +and some Attributes. The Prototype system allows to create a range of +such Swords with only minor variations. Prototypes can also inherit +and combine together to form entire hierarchies (such as giving all +Sabres and all Broadswords some common properties). Note that bigger +variations, such as custom commands or functionality belong in a +hierarchy of typeclasses instead. + +Example prototypes are read by the `@spawn` command but is also easily +available to use from code via `evennia.spawn` or `evennia.utils.spawner`. +Each prototype should be a dictionary. Use the same name as the +variable to refer to other prototypes. + +Possible keywords are: + prototype_parent - string pointing to parent prototype of this structure. + key - string, the main object identifier. + typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`. + location - this should be a valid object or #dbref. + home - valid object or #dbref. + destination - only valid for exits (object or dbref). + + permissions - string or list of permission strings. + locks - a lock-string. + aliases - string or list of strings. + + ndb_ - value of a nattribute (the "ndb_" part is ignored). + any other keywords are interpreted as Attributes and their values. + +See the `@spawn` command and `evennia.utils.spawner` for more info. + +""" + +# from random import randint +# +# GOBLIN = { +# "key": "goblin grunt", +# "health": lambda: randint(20,30), +# "resists": ["cold", "poison"], +# "attacks": ["fists"], +# "weaknesses": ["fire", "light"] +# } +# +# GOBLIN_WIZARD = { +# "prototype_parent": "GOBLIN", +# "key": "goblin wizard", +# "spells": ["fire ball", "lighting bolt"] +# } +# +# GOBLIN_ARCHER = { +# "prototype_parent": "GOBLIN", +# "key": "goblin archer", +# "attacks": ["short bow"] +# } +# +# This is an example of a prototype without a prototype +# (nor key) of its own, so it should normally only be +# used as a mix-in, as in the example of the goblin +# archwizard below. +# ARCHWIZARD_MIXIN = { +# "attacks": ["archwizard staff"], +# "spells": ["greater fire ball", "greater lighting"] +# } +# +# GOBLIN_ARCHWIZARD = { +# "key": "goblin archwizard", +# "prototype_parent" : ("GOBLIN_WIZARD", "ARCHWIZARD_MIXIN") +# } diff --git a/taoshengshi/world/taoshengshi.py b/taoshengshi/world/taoshengshi.py new file mode 100644 index 0000000..4977369 --- /dev/null +++ b/taoshengshi/world/taoshengshi.py @@ -0,0 +1,33 @@ +'taoshengshi' + +#HEADER + +from evennia import create_object, search_object +from typeclasses import exits, objects, rooms +from typeclasses.mech import Mech + +def move_or_create(typeclass, key, aliases=[], location=None, destination=None): + objects = search_object(key) + # TODO: This only errors if typeclass is different...it should error if 2 objects have the same key or aliases + matching_objects = list(filter(lambda x: type(x) == typeclass, objects)) + if len(matching_objects) > 1: + caller.msg(f"ERROR: More than one object found for '{key}'") + raise + if not matching_objects: + caller.msg(f"Creating '{key}'") + return create_object(typeclass, key=key, aliases=aliases, location=location, destination=destination) + current_object = matching_objects[0] + if location != current_object.location: + caller.msg(f"Moving '{key}' to '{destination}'") + current_object.move_to(destination) + return current_object + + caller.msg(f'Leaving {key} alone') + return current_object + +# Rooms (defined here to be available to all files) +limbo = search_object('Limbo')[0] +cave = move_or_create(rooms.Room, key='cave') +lobby = move_or_create(rooms.Room, key='lobby') + +#INSERT taoshengshi.cave diff --git a/taoshengshi/world/taoshengshi/cave.py b/taoshengshi/world/taoshengshi/cave.py new file mode 100644 index 0000000..95fde45 --- /dev/null +++ b/taoshengshi/world/taoshengshi/cave.py @@ -0,0 +1,64 @@ +#CODE +'Cave' + +entrance_to_cave = move_or_create(exits.Exit, key='taoshengshi', + aliases=['tao'], + location=limbo, + destination=cave) + +cave.db.desc = ''' +You are inside an expansive underground cave. The air is moist and smells of +earth, minerals, and age. + +Ahead is a |xfacility|n embedded into the natural rock wall. You can make out a +large metal door which appears to be a |xgarage|n of some sort, but is much too +large for any normal sized vehicle. To the right of the garage is a door that +serves as the |xentrance|n to the facility. + +To your left and right are several rows of parking spots terminating in rock +walls on either side. + +Behind you is an excavated |xtunnel|n that you cannot see the end of. +''' + + +facility = move_or_create(objects.ImmovableObject, key='facility', + aliases=['facility', 'e'], + location=cave) + +facility.db.desc = ''' +Best you can tell is this facility is used for the manufacture of some sort of +large mechanical objects. Perhaps construction or farming equipment? +''' + +garage = move_or_create(objects.ImmovableObject, key='garage', + aliases=['garage', 'g'], + location=cave) +garage.db.desc = ''' +The garage door is made of corrugated metal and is large enough to hold even +the largest machinery. You can't see how it is opened and suspect it must be +opened from the inside. It is much too large to open by hand. +''' + +tunnel = move_or_create(exits.Exit, key='excavated tunnel', + aliases=['excavated', 'e', 'tunnel', 't'], + location=cave, + destination=limbo) + +tunnel.db.desc = ''' +This tunnel looks like it stretches on forever, no light can be seen at the end. +'''.strip() + +tunnel.db.err_traverse = ''' +You walk for what seems like miles with no discernible change in scenery. + +Eventually you turn back. You'll need some sort of vehicle to leave this place. +'''.strip() +tunnel.locks.add("traverse:is_a('Mech')") + +facility_door = move_or_create(exits.KeypadDoor, key='facility door', + aliases=['entrance', 'door'], + location=cave, + destination=lobby) + +mech = move_or_create(Mech, key='giant mech', location=cave, aliases=['mech'])