EGD renamed to EGI. Backwards compatibility preserved for now.
This commit is contained in:
parent
7e0b372273
commit
64db01c7ec
5 changed files with 68 additions and 39 deletions
182
evennia/contrib/egi_client/README.md
Normal file
182
evennia/contrib/egi_client/README.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Evennia Game Index Client
|
||||
|
||||
Greg Taylor 2016
|
||||
|
||||
This contrib features a client for the [Evennia Game Index]
|
||||
(http://evennia-game-index.appspot.com/), a listing of games built on
|
||||
Evennia. By listing your game on the index, you make it easy for other
|
||||
people in the community to discover your creation.
|
||||
|
||||
*Note: Since this is still an early experiment, there is no notion of
|
||||
ownership for a game listing. As a consequence, we rely on the good behavior
|
||||
of our users in the early goings. If the index is a success, we'll work
|
||||
on remedying this.*
|
||||
|
||||
## Listing your Game
|
||||
|
||||
To list your game, you'll need to enable the Evennia Game Index client.
|
||||
Start by `cd`'ing to your game directory. From there, open up
|
||||
`server/conf/server_services_plugins.py`. It might look something like this
|
||||
if you don't have any other optional add-ons enabled:
|
||||
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
To enable the client, import `EvenniaGameIndexService` and fire it up after the
|
||||
Evennia server has finished starting:
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from evennia.contrib.egi_client import EvenniaGameIndexService
|
||||
|
||||
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.
|
||||
"""
|
||||
egi_service = EvenniaGameIndexService()
|
||||
server.services.addService(egi_service)
|
||||
|
||||
|
||||
Next, configure your game listing by opening up `server/conf/settings.py` and
|
||||
using the following as a starting point:
|
||||
|
||||
######################################################################
|
||||
# Contrib config
|
||||
######################################################################
|
||||
|
||||
GAME_INDEX_LISTING = {
|
||||
'game_status': 'pre-alpha',
|
||||
# Optional, comment out or remove if N/A
|
||||
'game_website': 'http://my-game.com',
|
||||
'short_description': 'This is my game. It is fun. You should play it.',
|
||||
# Optional but highly recommended. Markdown is supported.
|
||||
'long_description': (
|
||||
"Hello, there. You silly person.\n\n"
|
||||
"This is the start of a new paragraph. Markdown is cool. Isn't this "
|
||||
"[neat](http://evennia.com)? My game is best game. Woohoo!\n\n"
|
||||
"Time to wrap this up. One final paragraph for the road."
|
||||
),
|
||||
'listing_contact': 'me@my-game.com',
|
||||
# At minimum, specify this or the web_client_url options. Both is fine, too.
|
||||
'telnet_hostname': 'my-game.com',
|
||||
'telnet_port': 1234,
|
||||
# At minimum, specify this or the telnet_* options. Both is fine, too.
|
||||
'web_client_url': 'http://my-game.com/webclient',
|
||||
}
|
||||
|
||||
The following section in this README.md will go over all possible values.
|
||||
|
||||
At this point, you should be all set! Simply restart your game and check the
|
||||
server logs for errors. Your listing and some game state will be sent every
|
||||
half hour.
|
||||
|
||||
## Possible GAME_INDEX_LISTING settings
|
||||
|
||||
### game_status
|
||||
|
||||
Required: **Yes**
|
||||
Must be one of: 'pre-alpha', 'alpha', 'beta', 'launched'
|
||||
|
||||
Describes the current state of your game.
|
||||
|
||||
### game_website
|
||||
|
||||
Required: No
|
||||
|
||||
The URL to your game's website, if you have one.
|
||||
|
||||
### short_description
|
||||
|
||||
Required: Yes
|
||||
|
||||
A short (max of 255 characters) description of your game that will appear
|
||||
on the main game index page.
|
||||
|
||||
### long_description
|
||||
|
||||
Required: No
|
||||
|
||||
A longer, full-length description or overview of the game.
|
||||
[Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet)
|
||||
and some of the very basic HTML tags are accepted here.
|
||||
|
||||
### listing_contact
|
||||
|
||||
Required: **Yes**
|
||||
|
||||
An email address for us to get in touch with in the event of a listing issue
|
||||
or backwards-incompatible change.
|
||||
|
||||
### telnet_hostname
|
||||
|
||||
Required: **Must specify this and telnet_port OR web_client_url**
|
||||
|
||||
The hostname that players can telnet into to play your game.
|
||||
|
||||
### telnet_port
|
||||
|
||||
Required: **Must specify this and telnet_hostname OR web_client_url**
|
||||
|
||||
The port that the players can telnet into to play your game.
|
||||
|
||||
### web_client_url
|
||||
|
||||
Required: **Must specify this OR telnet_hostname + telnet_port**
|
||||
|
||||
Full URL to your game's web-based client.
|
||||
|
||||
## What information is being reported?
|
||||
|
||||
In addition the the details listed in the previous section, we send some
|
||||
simple usage stats that don't currently get displayed. These will help the
|
||||
Evennia maintainers get a feel for some technical specifics for games out in
|
||||
the wild.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My game doesn't appear on the listing!
|
||||
|
||||
If you don't see your game appear on the listing after reloading your server,
|
||||
check the server logs. You should see some error messages describing what
|
||||
went wrong.
|
||||
|
||||
### I changed my game name and now there are two entries
|
||||
|
||||
This is a side-effect of our current, naive implementation in our listing
|
||||
system. Your old entry will disappear within two hours. Alternatively,
|
||||
speak up on IRC and someone might be able to manually purge the old entry.
|
||||
1
evennia/contrib/egi_client/__init__.py
Normal file
1
evennia/contrib/egi_client/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from evennia.contrib.egi_client.service import EvenniaGameIndexService
|
||||
171
evennia/contrib/egi_client/client.py
Normal file
171
evennia/contrib/egi_client/client.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import urllib
|
||||
import platform
|
||||
import warnings
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import protocol
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from twisted.web.client import Agent, _HTTP11ClientFactory, HTTPConnectionPool
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.iweb import IBodyProducer
|
||||
from zope.interface import implements
|
||||
|
||||
from evennia.players.models import PlayerDB
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia.utils import get_evennia_version, logger
|
||||
|
||||
|
||||
class EvenniaGameIndexClient(object):
|
||||
"""
|
||||
This client class is used for gathering and sending game details to the
|
||||
Evennia Game Index. Since EGI is in the early goings, this isn't
|
||||
incredibly configurable as far as what is being sent.
|
||||
"""
|
||||
def __init__(self, on_bad_request=None):
|
||||
"""
|
||||
:param on_bad_request: Optional callable to trigger when a bad request
|
||||
was sent. This is almost always going to be due to bad config.
|
||||
"""
|
||||
self.report_host = 'http://evennia-game-index.appspot.com'
|
||||
self.report_path = '/api/v1/game/check_in'
|
||||
self.report_url = self.report_host + self.report_path
|
||||
self.logged_first_connect = False
|
||||
|
||||
self._on_bad_request = on_bad_request
|
||||
# Oh, the humanity. Silence the factory start/stop messages.
|
||||
self._conn_pool = HTTPConnectionPool(reactor)
|
||||
self._conn_pool._factory = QuietHTTP11ClientFactory
|
||||
|
||||
@inlineCallbacks
|
||||
def send_game_details(self):
|
||||
"""
|
||||
This is where the magic happens. Send details about the game to the
|
||||
Evennia Game Index.
|
||||
"""
|
||||
status_code, response_body = yield self._form_and_send_request()
|
||||
if status_code == 200:
|
||||
if not self.logged_first_connect:
|
||||
logger.log_infomsg(
|
||||
"Successfully sent game details to Evennia Game Index.")
|
||||
self.logged_first_connect = True
|
||||
return
|
||||
# At this point, either EGD is having issues or the payload we sent
|
||||
# is improperly formed (probably due to mis-configuration).
|
||||
logger.log_errmsg(
|
||||
'Failed to send game details to Evennia Game Index. HTTP '
|
||||
'status code was %s. Message was: %s' % (status_code, response_body)
|
||||
)
|
||||
if status_code == 400 and self._on_bad_request:
|
||||
# Improperly formed request. Defer to the callback as far as what
|
||||
# to do. Probably not a great idea to continue attempting to send
|
||||
# to EGD, though.
|
||||
self._on_bad_request()
|
||||
|
||||
def _get_config_dict(self):
|
||||
egi_config = getattr(settings, 'GAME_DIRECTORY_LISTING', None)
|
||||
if egi_config:
|
||||
warnings.warn(
|
||||
"settings.GAME_DIRECTORY_LISTING is deprecated. Rename this to "
|
||||
"GAME_INDEX_LISTING in your settings file.", DeprecationWarning)
|
||||
return egi_config
|
||||
return settings.GAME_INDEX_LISTING
|
||||
|
||||
def _form_and_send_request(self):
|
||||
agent = Agent(reactor, pool=self._conn_pool)
|
||||
headers = {
|
||||
'User-Agent': ['Evennia Game Index Client'],
|
||||
'Content-Type': ['application/x-www-form-urlencoded'],
|
||||
}
|
||||
egi_config = self._get_config_dict()
|
||||
# We are using `or` statements below with dict.get() to avoid sending
|
||||
# stringified 'None' values to the server.
|
||||
values = {
|
||||
# Game listing stuff
|
||||
'game_name': settings.SERVERNAME,
|
||||
'game_status': egi_config['game_status'],
|
||||
'game_website': egi_config.get('game_website') or '',
|
||||
'short_description': egi_config['short_description'],
|
||||
'long_description': egi_config.get('long_description') or '',
|
||||
'listing_contact': egi_config['listing_contact'],
|
||||
|
||||
# How to play
|
||||
'telnet_hostname': egi_config.get('telnet_hostname') or '',
|
||||
'telnet_port': egi_config.get('telnet_port') or '',
|
||||
'web_client_url': egi_config.get('web_client_url') or '',
|
||||
|
||||
# Game stats
|
||||
'connected_player_count': SESSIONS.player_count(),
|
||||
'total_player_count': PlayerDB.objects.num_total_players() or 0,
|
||||
|
||||
# System info
|
||||
'evennia_version': get_evennia_version(),
|
||||
'python_version': platform.python_version(),
|
||||
'django_version': django.get_version(),
|
||||
'server_platform': platform.platform(),
|
||||
}
|
||||
data = urllib.urlencode(values)
|
||||
|
||||
d = agent.request(
|
||||
'POST', self.report_url,
|
||||
headers=Headers(headers),
|
||||
bodyProducer=StringProducer(data))
|
||||
|
||||
d.addCallback(self.handle_egd_response)
|
||||
return d
|
||||
|
||||
def handle_egd_response(self, response):
|
||||
if 200 <= response.code < 300:
|
||||
d = defer.succeed((response.code, 'OK'))
|
||||
else:
|
||||
# Go through the horrifying process of getting the response body
|
||||
# out of Twisted's plumbing.
|
||||
d = defer.Deferred()
|
||||
response.deliverBody(SimpleResponseReceiver(response.code, d))
|
||||
return d
|
||||
|
||||
|
||||
class SimpleResponseReceiver(protocol.Protocol):
|
||||
"""
|
||||
Used for pulling the response body out of an HTTP response.
|
||||
"""
|
||||
def __init__(self, status_code, d):
|
||||
self.status_code = status_code
|
||||
self.buf = ''
|
||||
self.d = d
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
|
||||
def connectionLost(self, reason=protocol.connectionDone):
|
||||
self.d.callback((self.status_code, self.buf))
|
||||
|
||||
|
||||
class StringProducer(object):
|
||||
"""
|
||||
Used for feeding a request body to the tx HTTP client.
|
||||
"""
|
||||
implements(IBodyProducer)
|
||||
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
self.length = len(body)
|
||||
|
||||
def startProducing(self, consumer):
|
||||
consumer.write(self.body)
|
||||
return defer.succeed(None)
|
||||
|
||||
def pauseProducing(self):
|
||||
pass
|
||||
|
||||
def stopProducing(self):
|
||||
pass
|
||||
|
||||
|
||||
class QuietHTTP11ClientFactory(_HTTP11ClientFactory):
|
||||
"""
|
||||
Silences the obnoxious factory start/stop messages in the default client.
|
||||
"""
|
||||
noisy = False
|
||||
53
evennia/contrib/egi_client/service.py
Normal file
53
evennia/contrib/egi_client/service.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.application.service import Service
|
||||
|
||||
from evennia.contrib.egi_client.client import EvenniaGameIndexClient
|
||||
from evennia.utils import logger
|
||||
|
||||
# How many seconds to wait before triggering the first EGI check-in.
|
||||
_FIRST_UPDATE_DELAY = 10
|
||||
# How often to sync to the server
|
||||
_CLIENT_UPDATE_RATE = 60 * 30
|
||||
|
||||
|
||||
class EvenniaGameIndexService(Service):
|
||||
"""
|
||||
Twisted Service that contains a LoopingCall for sending details on a
|
||||
game to the Evennia Game Index.
|
||||
"""
|
||||
# We didn't stick the Evennia prefix on here because it'd get marked as
|
||||
# a core system service.
|
||||
name = 'GameIndexClient'
|
||||
|
||||
def __init__(self):
|
||||
self.client = EvenniaGameIndexClient(
|
||||
on_bad_request=self._die_on_bad_request)
|
||||
self.loop = LoopingCall(self.client.send_game_details)
|
||||
|
||||
def startService(self):
|
||||
super(EvenniaGameIndexService, self).startService()
|
||||
# TODO: Check to make sure that the client is configured.
|
||||
# Start the loop, but only after a short delay. This allows the
|
||||
# portal and the server time to sync up as far as total player counts.
|
||||
# Prevents always reporting a count of 0.
|
||||
reactor.callLater(
|
||||
_FIRST_UPDATE_DELAY, self.loop.start, _CLIENT_UPDATE_RATE)
|
||||
|
||||
def stopService(self):
|
||||
if self.running == 0:
|
||||
# @reload errors if we've stopped this service.
|
||||
return
|
||||
super(EvenniaGameIndexService, self).stopService()
|
||||
self.loop.stop()
|
||||
|
||||
def _die_on_bad_request(self):
|
||||
"""
|
||||
If it becomes apparent that our configuration is generating improperly
|
||||
formed messages to EGI, we don't want to keep sending bad messages.
|
||||
Stop the service so we're not wasting resources.
|
||||
"""
|
||||
logger.log_infomsg(
|
||||
"Shutting down Evennia Game Index client service due to "
|
||||
"invalid configuration.")
|
||||
self.stopService()
|
||||
Loading…
Add table
Add a link
Reference in a new issue