Index: ps/trunk/source/tools/lobbybots/setup.py =================================================================== --- ps/trunk/source/tools/lobbybots/setup.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/setup.py (nonexistent) @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 - -"""setup.py for 0ad XMPP lobby bots.""" - -from setuptools import find_packages, setup - -setup( - name='XpartaMuPP', - version='0.24', - description='Multiplayer lobby bots for 0ad', - packages=find_packages(), - entry_points={ - 'console_scripts': [ - 'echelon=xpartamupp.echelon:main', - 'xpartamupp=xpartamupp.xpartamupp:main', - 'echelon-db=xpartamupp.lobby_ranking:main', - ] - }, - install_requires=[ - 'dnspython', - 'sleekxmpp', - 'sqlalchemy', - ], - tests_require=[ - 'coverage', - 'hypothesis', - 'parameterized', - ], - classifiers=[ - 'Development Status :: 3 - Alpha', - 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Games/Entertainment', - 'Topic :: Internet :: XMPP', - ], - zip_safe=False, - test_suite='tests', -) Index: ps/trunk/source/tools/lobbybots/ejabberd_example.yml =================================================================== --- ps/trunk/source/tools/lobbybots/ejabberd_example.yml (revision 27093) +++ ps/trunk/source/tools/lobbybots/ejabberd_example.yml (nonexistent) @@ -1,855 +0,0 @@ -### -###' ejabberd configuration file -### -### - -### The parameters used in this configuration file are explained in more detail -### in the ejabberd Installation and Operation Guide. -### Please consult the Guide in case of doubts, it is included with -### your copy of ejabberd, and is also available online at -### http://www.process-one.net/en/ejabberd/docs/ - -### The configuration file is written in YAML. -### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. -### However, ejabberd treats different literals as different types: -### -### - unquoted or single-quoted strings. They are called "atoms". -### Example: dog, 'Jupiter', '3.14159', YELLOW -### -### - numeric literals. Example: 3, -45.0, .0 -### -### - quoted or folded strings. -### Examples of quoted string: "Lizzard", "orange". -### Example of folded string: -### > Art thou not Romeo, -### and a Montague? ---- -###. ======= -###' LOGGING - -## -## loglevel: Verbosity of log files generated by ejabberd. -## 0: No ejabberd log at all (not recommended) -## 1: Critical -## 2: Error -## 3: Warning -## 4: Info -## 5: Debug -## -loglevel: 4 - -## -## rotation: Disable ejabberd's internal log rotation, as the Debian package -## uses logrotate(8). -log_rotate_size: 0 -log_rotate_date: "" - -## -## overload protection: If you want to limit the number of messages per second -## allowed from error_logger, which is a good idea if you want to avoid a flood -## of messages when system is overloaded, you can set a limit. -## 100 is ejabberd's default. -log_rate_limit: 100 - -## -## watchdog_admins: Only useful for developers: if an ejabberd process -## consumes a lot of memory, send live notifications to these XMPP -## accounts. -## -## watchdog_admins: -## - "bob@example.com" - -###. =============== -###' NODE PARAMETERS - -## -## net_ticktime: Specifies net_kernel tick time in seconds. This options must have -## identical value on all nodes, and in most cases shouldn't be changed at all from -## default value. -## -## net_ticktime: 60 - -###. ================ -###' SERVED HOSTNAMES - -## -## hosts: Domains served by ejabberd. -## You can define one or several, for example: -## hosts: -## - "example.net" -## - "example.com" -## - "example.org" -## -hosts: - - "localhost" - -## -## route_subdomains: Delegate subdomains to other XMPP servers. -## For example, if this ejabberd serves example.org and you want -## to allow communication with an XMPP server called im.example.org. -## -## route_subdomains: s2s - -###. ============ -###' Certificates - -## List all available PEM files containing certificates for your domains, -## chains of certificates or certificate keys. Full chains will be built -## automatically by ejabberd. -## -certfiles: - - "/etc/ejabberd/ejabberd.pem" - -## If your system provides only a single CA file (CentOS/FreeBSD): -## ca_file: "/etc/ssl/certs/ca-bundle.pem" - -###. ================= -###' TLS configuration - -## Note that the following configuration is the default -## configuration of the TLS driver, so you don't need to -## uncomment it. -## -define_macro: - 'TLS_CIPHERS': "HIGH:!aNULL:!eNULL:!3DES:@STRENGTH" - 'TLS_OPTIONS': - - "no_sslv2" - - "no_sslv3" - - "no_tlsv1" - - "cipher_server_preference" - - "no_compression" - ## 'DH_FILE': "/path/to/dhparams.pem" # generated with: openssl dhparam -out dhparams.pem 2048 - -## c2s_dhfile: 'DH_FILE' -## s2s_dhfile: 'DH_FILE' -c2s_ciphers: 'TLS_CIPHERS' -s2s_ciphers: 'TLS_CIPHERS' -c2s_protocol_options: 'TLS_OPTIONS' -s2s_protocol_options: 'TLS_OPTIONS' - -###. =============== -###' LISTENING PORTS - -## -## listen: The ports ejabberd will listen on, which service each is handled -## by and what options to start it with. -## -listen: - - - port: 5222 - ip: "0.0.0.0" - module: ejabberd_c2s - starttls: true - starttls_required: false - protocol_options: 'TLS_OPTIONS' - max_stanza_size: 1048576 - shaper: c2s_shaper - access: c2s - - ## port: 5269 - ## ip: "::" - ## module: ejabberd_s2s_in - - - - port: 5280 - ip: "127.0.0.1" - module: ejabberd_http - request_handlers: - "/ws": ejabberd_http_ws - "/bosh": mod_bosh - "/api": mod_http_api - ## "/pub/archive": mod_http_fileserver - web_admin: true - ## register: true - ## captcha: true - tls: true - protocol_options: 'TLS_OPTIONS' - - ## - ## ejabberd_service: Interact with external components (transports, ...) - ## - ## - - ## port: 8888 - ## ip: "::" - ## module: ejabberd_service - ## access: all - ## shaper_rule: fast - ## ip: "127.0.0.1" - ## privilege_access: - ## roster: "both" - ## message: "outgoing" - ## presence: "roster" - ## delegations: - ## "urn:xmpp:mam:1": - ## filtering: ["node"] - ## "http://jabber.org/protocol/pubsub": - ## filtering: [] - ## hosts: - ## "icq.example.org": - ## password: "secret" - ## "sms.example.org": - ## password: "secret" - - ## - ## ejabberd_stun: Handles STUN Binding requests - ## - - - port: 3478 - transport: udp - module: ejabberd_stun - - ## - ## To handle XML-RPC requests that provide admin credentials: - ## - ## - - ## port: 4560 - ## ip: "::" - ## module: ejabberd_xmlrpc - ## maxsessions: 10 - ## timeout: 5000 - ## access_commands: - ## admin: - ## commands: all - ## options: [] - - ## - ## To enable secure http upload - ## - ## - - ## port: 5444 - ## ip: "::" - ## module: ejabberd_http - ## request_handlers: - ## "": mod_http_upload - ## tls: true - ## protocol_options: 'TLS_OPTIONS' - ## dhfile: 'DH_FILE' - ## ciphers: 'TLS_CIPHERS' - -## Disabling digest-md5 SASL authentication. digest-md5 requires plain-text -## password storage (see auth_password_format option). -disable_sasl_mechanisms: "digest-md5" - -###. ================== -###' S2S GLOBAL OPTIONS - -## -## s2s_use_starttls: Enable STARTTLS for S2S connections. -## Allowed values are: false, optional or required -## You must specify 'certfiles' option -## -s2s_use_starttls: required - -## -## S2S whitelist or blacklist -## -## Default s2s policy for undefined hosts. -## -## s2s_access: s2s - -## -## Outgoing S2S options -## -## Preferred address families (which to try first) and connect timeout -## in seconds. -## -## outgoing_s2s_families: -## - ipv4 -## - ipv6 -## outgoing_s2s_timeout: 190 - -###. ============== -###' AUTHENTICATION - -## -## auth_method: Method used to authenticate the users. -## The default method is the internal. -## If you want to use a different method, -## comment this line and enable the correct ones. -## -auth_method: internal - -## -## Store the plain passwords or hashed for SCRAM: -## auth_password_format: plain -auth_password_format: scram -## -## Define the FQDN if ejabberd doesn't detect it: -## fqdn: "server3.example.com" - -## -## Authentication using external script -## Make sure the script is executable by ejabberd. -## -## auth_method: external -## extauth_program: "/path/to/authentication/script" - -## -## Authentication using SQL -## Remember to setup a database in the next section. -## -## auth_method: sql - -## -## Authentication using PAM -## -## auth_method: pam -## pam_service: "pamservicename" - -## -## Authentication using LDAP -## -## auth_method: ldap -## -## List of LDAP servers: -## ldap_servers: -## - "lw" -## -## Encryption of connection to LDAP servers: -## ldap_encrypt: none -## ldap_encrypt: tls -## -## Port to connect to on LDAP servers: -## ldap_port: 389 -## ldap_port: 636 -## -## LDAP manager: -## ldap_rootdn: "dc=example,dc=com" -## -## Password of LDAP manager: -## ldap_password: "******" -## -## Search base of LDAP directory: -## ldap_base: "dc=example,dc=com" -## -## LDAP attribute that holds user ID: -## ldap_uids: -## - "mail": "%u@mail.example.org" -## -## LDAP filter: -## ldap_filter: "(objectClass=shadowAccount)" - -## -## Anonymous login support: -## auth_method: anonymous -## anonymous_protocol: sasl_anon | login_anon | both -## allow_multiple_connections: true | false -## -## host_config: -## "public.example.org": -## auth_method: anonymous -## allow_multiple_connections: false -## anonymous_protocol: sasl_anon -## -## To use both anonymous and internal authentication: -## -## host_config: -## "public.example.org": -## auth_method: -## - internal -## - anonymous - -###. ============== -###' DATABASE SETUP - -## ejabberd by default uses the internal Mnesia database, -## so you do not necessarily need this section. -## This section provides configuration examples in case -## you want to use other database backends. -## Please consult the ejabberd Guide for details on database creation. - -## -## MySQL server: -## -## sql_type: mysql -## sql_server: "server" -## sql_database: "database" -## sql_username: "username" -## sql_password: "password" -## -## If you want to specify the port: -## sql_port: 1234 - -## -## PostgreSQL server: -## -## sql_type: pgsql -## sql_server: "server" -## sql_database: "database" -## sql_username: "username" -## sql_password: "password" -## -## If you want to specify the port: -## sql_port: 1234 -## -## If you use PostgreSQL, have a large database, and need a -## faster but inexact replacement for "select count(*) from users" -## -## pgsql_users_number_estimate: true - -## -## SQLite: -## -## sql_type: sqlite -## sql_database: "/path/to/database.db" - -## -## ODBC compatible or MSSQL server: -## -## sql_type: odbc -## sql_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd" - -## -## Number of connections to open to the database for each virtual host -## -## sql_pool_size: 10 - -## -## Interval to make a dummy SQL request to keep the connections to the -## database alive. Specify in seconds: for example 28800 means 8 hours -## -## sql_keepalive_interval: undefined - -###. =============== -###' TRAFFIC SHAPERS - -shaper: - ## - ## The "normal" shaper limits traffic speed to 1000 B/s - ## - normal: 1000 - - ## - ## The "fast" shaper limits traffic speed to 50000 B/s - ## - fast: 50000 - -## -## This option specifies the maximum number of elements in the queue -## of the FSM. Refer to the documentation for details. -## -max_fsm_queue: 10000 - -###. ==================== -###' ACCESS CONTROL LISTS -acl: - ## - ## The 'admin' ACL grants administrative privileges to XMPP accounts. - ## You can put here as many accounts as you want. - ## - admin: - user: - - "admin@localhost" - - ## Don't use a regex, to prevent others from obtaining permissions after registering such an account. - bots: - - user: "echelon23@localhost" - - user: "wfgbot23@localhost" - - # Keep playernames short and easily typeable for everyone - validname: - user_regexp: "^[0-9A-Za-z._-]{1,20}$" - - ## - ## Blocked users - ## - ## blocked: - ## user: - ## - "baduser@example.org" - ## - "test" - - ## Local users: don't modify this. - ## - local: - user_regexp: "" - - ## - ## More examples of ACLs - ## - ## jabberorg: - ## server: - ## - "jabber.org" - ## aleksey: - ## user: - ## - "aleksey@jabber.ru" - ## test: - ## user_regexp: "^test" - ## user_glob: "test*" - - ## - ## Loopback network - ## - loopback: - ip: - - "127.0.0.0/8" - - "::1/128" - - "::FFFF:127.0.0.1/128" - - ## - ## Bad XMPP servers - ## - ## bad_servers: - ## server: - ## - "xmpp.zombie.org" - ## - "xmpp.spam.com" - -## -## Define specific ACLs in a virtual host. -## -## host_config: -## "localhost": -## acl: -## admin: -## user: -## - "bob-local@localhost" - -###. ============ -###' SHAPER RULES - -shaper_rules: - ## Maximum number of simultaneous sessions allowed for a single user: - max_user_sessions: 10 - ## Maximum number of offline messages that users can have: - max_user_offline_messages: - - 5000: admin - - 100 - ## For C2S connections, all users except admins use the "normal" shaper - c2s_shaper: - - none: admin - - none: bots - - normal - ## All S2S connections use the "fast" shaper - s2s_shaper: fast - -###. ============ -###' ACCESS RULES -access_rules: - ## This rule allows access only for local users: - local: - - allow: local - ## Only non-blocked users can use c2s connections: - c2s: - - deny: blocked - - allow - ## Only admins can send announcement messages: - announce: - - allow: admin - ## Only admins can use the configuration interface: - configure: - - allow: admin - ## Expected by the ipstamp module for XpartaMuPP - ipbots: - - allow: bots - muc_admin: - - allow: admin - ## Bots must be able to create nodes for games, ratings and boards lists - pubsub_createnode: - - allow: admin - - allow: bots - ## In-band registration allows registration of any possible username. - ## To disable in-band registration, replace 'allow' with 'deny'. - register: - - deny: blocked - - allow: validname - ## Only allow to register from localhost - trusted_network: - - allow: loopback - ## Do not establish S2S connections with bad servers - ## If you enable this you also have to uncomment "s2s_access: s2s" - ## s2s: - ## - deny: - ## - ip: "XXX.XXX.XXX.XXX/32" - ## - deny: - ## - ip: "XXX.XXX.XXX.XXX/32" - ## - allow - -## =============== -## API PERMISSIONS -## =============== -## -## This section allows you to define who and using what method -## can execute commands offered by ejabberd. -## -## By default "console commands" section allow executing all commands -## issued using ejabberdctl command, and "admin access" section allows -## users in admin acl that connect from 127.0.0.1 to execute all -## commands except start and stop with any available access method -## (ejabberdctl, http-api, xmlrpc depending what is enabled on server). -## -## If you remove "console commands" there will be one added by -## default allowing executing all commands, but if you just change -## permissions in it, version from config file will be used instead -## of default one. -## -api_permissions: - "console commands": - from: - - ejabberd_ctl - who: all - what: "*" - "admin access": - who: - - access: - - allow: - - acl: loopback - - acl: admin - - oauth: - - scope: "ejabberd:admin" - - access: - - allow: - - acl: loopback - - acl: admin - what: - - "*" - - "!stop" - - "!start" - "public commands": - who: - - ip: "127.0.0.1/8" - what: - - "status" - - "connected_users_number" - -## By default the frequency of account registrations from the same IP -## is limited to 1 account every 10 minutes. To disable, specify: infinity -registration_timeout: 3600 - -## -## Define specific Access Rules in a virtual host. -## -## host_config: -## "localhost": -## access: -## c2s: -## - allow: admin -## - deny -## register: -## - deny - -###. ================ -###' DEFAULT LANGUAGE - -## -## language: Default language used for server messages. -## -language: "en" - -## -## Set a different default language in a virtual host. -## -## host_config: -## "localhost": -## language: "ru" - -###. ======= -###' CAPTCHA - -## -## Full path to a script that generates the image. -## -## captcha_cmd: "/usr/share/ejabberd/captcha.sh" - -## -## Host for the URL and port where ejabberd listens for CAPTCHA requests. -## -## captcha_host: "example.org:5280" - -## -## Limit CAPTCHA calls per minute for JID/IP to avoid DoS. -## -## captcha_limit: 5 - -###. ==== -###' ACME -## -## In order to use the acme certificate acquiring through "Let's Encrypt" -## an http listener has to be configured to listen to port 80 so that -## the authorization challenges posed by "Let's Encrypt" can be solved. -## -## A simple way of doing this would be to add the following in the listening -## section and to configure port forwarding from 80 to 5281 either via NAT -## (for ipv4 only) or using frontends such as haproxy/nginx/sslh/etc. -## - -## port: 5281 -## ip: "::" -## module: ejabberd_http - -acme: - - ## A contact mail that the ACME Certificate Authority can contact in case of - ## an authorization issue, such as a server-initiated certificate revocation. - ## It is not mandatory to provide an email address but it is highly suggested. - contact: "mailto:example-admin@example.com" - - - ## The ACME Certificate Authority URL. - ## This could either be: - ## - https://acme-v01.api.letsencrypt.org - (Default) for the production CA - ## - https://acme-staging.api.letsencrypt.org - for the staging CA - ## - http://localhost:4000 - for a local version of the CA - ca_url: "https://acme-v01.api.letsencrypt.org" - -###. ======= -###' MODULES - -## -## Modules enabled in all ejabberd virtual hosts. -## -modules: - mod_adhoc: {} - mod_admin_extra: {} - mod_announce: # recommends mod_adhoc - access: announce - mod_blocking: {} # requires mod_privacy - mod_caps: {} - mod_carboncopy: {} - mod_client_state: {} - mod_configure: {} # requires mod_adhoc - ## mod_delegation: {} # for xep0356 - mod_disco: {} - ## mod_echo: {} - ## ipstamp module used by XpartaMuPP to insert IP addresses into the gamelist - mod_ipstamp: {} - ## mod_irc: {} - mod_bosh: {} - ## mod_http_fileserver: - ## docroot: "/var/www" - ## accesslog: "/var/log/ejabberd/access.log" - ## mod_http_upload: - ## # docroot: "@HOME@/upload" - ## put_url: "https://@HOST@:5444" - ## thumbnail: false # otherwise needs the identify command from ImageMagick installed - ## mod_http_upload_quota: - ## max_days: 30 - mod_last: {} - ## XEP-0313: Message Archive Management - ## You might want to setup a SQL backend for MAM because the mnesia database is - ## limited to 2GB which might be exceeded on large servers - ## mod_mam: {} # for xep0313, mnesia is limited to 2GB, better use an SQL backend - mod_muc: - ## host: "conference.@HOST@" - access: - - allow - access_admin: muc_admin - access_create: muc_admin - access_persistent: muc_admin - max_users: 5000 - default_room_options: - allow_change_subj: false - logging: true - max_users: 1000 - persistent: true - mod_muc_admin: {} - mod_muc_log: - outdir: "/lobby/logs" - dirtype: plain - file_format: plaintext - timezone: universal - ## mod_multicast: {} - mod_offline: - access_max_user_messages: max_user_offline_messages - mod_ping: - send_pings: true - ## mod_pres_counter: - ## count: 5 - ## interval: 60 - mod_privacy: {} - mod_private: {} - ## mod_proxy65: {} - mod_pubsub: - access_createnode: pubsub_createnode - ## reduces resource comsumption, but XEP incompliant - ignore_pep_from_offline: true - ## XEP compliant, but increases resource comsumption - ## ignore_pep_from_offline: false - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" # pep requires mod_caps - mod_push: {} - mod_push_keepalive: {} - mod_register: - ## - ## Protect In-Band account registrations with CAPTCHA. - ## - ## captcha_protected: true - ## - ## Set the minimum informational entropy for passwords. - ## - ## password_strength: 32 - ## - ## After successful registration, the user receives - ## a message with this subject and body. - ## - ## welcome_message: - ## subject: "Welcome!" - ## body: |- - ## Hi. - ## Welcome to this XMPP server. - ## - ## When a user registers, send a notification to - ## these XMPP accounts. - ## - ## registration_watchers: - ## - "admin1@example.org" - ## - ## Only clients in the server machine can register accounts - ## - ## ip_access: trusted_network - ## - ## Local c2s or remote s2s users cannot register accounts - ## - ## access_from: deny - access: register - mod_roster: - versioning: true - ## mod_shared_roster: {} - mod_stats: {} - mod_time: {} - ## mod_vcard: - ## search: false - ## mod_vcard_xupdate: {} - ## Convert all avatars posted by Android clients from WebP to JPEG - ## mod_avatar: # this module needs compile option --enable-graphics - ## convert: - ## webp: jpeg - mod_version: {} - mod_stream_mgmt: - resend_on_timeout: if_offline - ## Non-SASL Authentication (XEP-0078) is now disabled by default - ## because it's obsoleted and is used mostly by abandoned - ## client software - ## mod_legacy_auth: {} - ## The module for S2S dialback (XEP-0220). Please note that you cannot - ## rely solely on dialback if you want to federate with other servers, - ## because a lot of servers have dialback disabled and instead rely on - ## PKIX authentication. Make sure you have proper certificates installed - ## and check your accessibility at https://check.messaging.one/ - mod_s2s_dialback: {} - mod_http_api: {} - -## -## Enable modules with custom options in a specific virtual host -## -## host_config: -## "localhost": -## modules: -## mod_echo: -## host: "mirror.localhost" - -## -## Enable modules management via ejabberdctl for installation and -## uninstallation of public/private contributed modules -## (enabled by default) -## - -allow_contrib_modules: true - -###. -###' -### Local Variables: -### mode: yaml -### End: -### vim: set filetype=yaml tabstop=8 foldmarker=###',###. foldmethod=marker: - Property changes on: ps/trunk/source/tools/lobbybots/ejabberd_example.yml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/tools/lobbybots/README.md =================================================================== --- ps/trunk/source/tools/lobbybots/README.md (revision 27093) +++ ps/trunk/source/tools/lobbybots/README.md (nonexistent) @@ -1,641 +0,0 @@ -# 0 A.D. / Pyrogenesis Multiplayer Lobby Setup - -This README explains how to setup a custom Pyrogenesis Multiplayer Lobby server that can be used with the Pyrogenesis game. - -## Service description -The Pyrogenesis Multiplayer Lobby consists of three components: - -* **XMPP server: ejabberd**: - The XMPP server provides the platform where users can register accounts, chat in a public room, and can interact with lobby bots. - ejabberd is recommended. - -* **Gamelist bot: XpartaMuPP**: - This bot allows players to host and join online multiplayer matches. - -* **Rating bot: EcheLOn**: - This bot allows players to gain a rating that reflects their skill based on online multiplayer matches. - It is by no means necessary for the operation of a lobby in terms of match-making and chatting. - -## Service choices -Before installing the service, you have to make some decisions: - -#### Choice: Domain Name -Decide on a domain name where the service will be provided. -This document will use `lobby.wildfiregames.com` as an example. -If you intend to use the server only for local testing, you may choose `localhost`. - -#### Choice: Rating service -Decide whether or not you want to employ the rating service. -If you decide to not provide the rating service, you may skip the instructions for the rating bot in this document. - -#### Choice: Pyrogenesis version compatibility -Decide whether you want to support serving multiple Pyrogenesis versions. - -Serving multiple versions of Pyrogenesis allows for seamless version upgrading on the backend and -allows players that don't have the most recent version of Pyrogenesis yet to continue to play until -the new release is available for their platform (applies mostly to linux distributions). - -If you decide to do so, you should use a naming pattern that includes the targetted Pyrogenesis version. -For example to provide a Multiplayer Lobby for Pyrogenesis Alpha 23 "Ken Wood", -name the lobby room `arena23` instead of `arena` and use `xpartamupp23` and `echelon23` as lobby bot names. -Then when a version 24 of Pyrogenesis is employed, you can easily add `arena24`, `xpartamupp24` and `echelon24`. -If you only want to use the service for local testing, you can stick to a single room and a single gamelist and rating bot. - -## 1. Install dependencies - -This section explains how to install the required software on a Debian-based linux distribution. -For other operating systems, use the according package manager or consult the official documentation of the software. - -### 1.1 Install ejabberd - -The version requirement for ejabberd is 17.03 or later (due to the ipstamp module format). - -* Install `ejabberd` using the following command. Alternatively see . - - ``` - $ apt-get install ejabberd - ``` - -* Confirm that the ejabberd version you installed is the one mentioned above or later: - - ``` - $ ejabberdctl status - ``` - -* Configure ejabberd by setting the domain name of your choice and add an `admin` user.: - - ``` - $ dpkg-reconfigure ejabberd - ```` - -You should now be able to connect to this XMPP server using any XMPP client. - -### 1.2 Install python3 and SleekXmpp - -* The lobby bots are programmed in python3 and use SleekXMPP to connect to the lobby. Install these dependencies using: - - ``` - $ apt-get install python3 python3-sleekxmpp - ``` - -* Confirm that the SleekXmpp version is 1.3.1 or later: - - ``` - pip3 show sleekxmpp - ``` - -* If you would like to run the rating bot, you will need to install SQLAlchemy for python3: - - ``` - $ apt-get install python3-sqlalchemy - ``` - -## 2 (Optional) Install ejabberd ipstamp module - -### 2.1 Copy mod_ipstamp files. - -The ejabberd ipstamp module is used as a fallback for users without STUN capabilities. -It inserts the IP to GameList "register" stanzas, which XpartMuPP sends back to the host. -STUN-enabled users do not require it to host, so this is optional. - -* Adjust `/etc/ejabberd/ejabberdctl.cfg` and set `CONTRIB_MODULES_PATH` to the directory where you want to store `mod_ipstamp`: - - ``` - CONTRIB_MODULES_PATH=/opt/ejabberd-modules - ``` - -* Ensure the target directory is readable by ejabberd. -* Copy the `mod_ipstamp` directory from `XpartaMuPP/` to `CONTRIB_MODULES_PATH/sources/`. -* Check that the module is available and compatible with your ejabberd: - - ``` - $ ejabberdctl modules_available - $ ejabberdctl module_check mod_ipstamp - ``` - -* Install `mod_ipstamp`: - - ``` - $ ejabberdctl module_install mod_ipstamp - ``` - -## 2.2. Configure ejabberd mod_ipstamp - -The ejabberd configuration in the remainder of this document is performed by editing `/etc/ejabberd/ejabberd.yml`. -The directory containing this README includes a preconfigured `ejabberd_example.yml` that only needs few setting changes to work with your setup. -For a full documentation of the ejabberd configuration, see . -If something goes wrong with ejabberd, check `/var/log/ejabberd/ejabberd.log` - -* Add `mod_ipstamp` to the modules ejabberd should load: - - ``` - modules: - mod_ipstamp: {} - ``` - -* Reload the ejabberd config. - This should be done every few steps, so that configuration errors can be identified as soon as possible. - - ``` - $ ejabberdctl reload_config - ``` - -## 3. Configure ejabberd connectivity - -The settings in this section ensure that connections can be built where intended, and only where intended. - -### 3.1 Disable IPv6 -* Since the enet library which Pyrogenesis uses for multiplayer mode does not support IPv6, ejabberd must be configured to not use IPv6: - - ``` - listen: - ip: "0.0.0.0" - ``` - -### 3.2 Enable STUN -* ejabberd and Pyrogenesis support the STUN protocol. This allows players to connect to each others games even if the host did not configure the router and forward the UDP port. -0 A.D. uses STUN to let hosts find their IP. - - ``` - listen: - - - port: 3478 - transport: udp - module: ejabberd_stun - ``` - -### 3.3 Enable keep-alive - -* This helps with users becoming disconnected: - - ``` - modules: - mod_ping: - send_pings: true - ``` - -### 3.3 Disable unused services - -* Disable the currently unused server-to-server communication: - - ``` - listen: - ## - - ## port: 5269 - ## ip: "::" - ## module: ejabberd_s2s_in - ``` - -* Protect the administrative webinterface at from external access by disabling or restriction to `localhost`: - - ``` - listen: - - - port: 5280 - ip: "127.0.0.1" - ``` - -* Disable some unused modules: - - ``` - modules: - ## mod_echo: {} - ## mod_irc: {} - ## mod_shared_roster: {} - ## mod_vcard: {} - ## mod_vcard_xupdate: {} - ``` - -### 3.4 Setup TLS encryption - -Depending on whether you use the server for a player audience or only for local testing, -you may have to either obtain and install a certificate with ejabberd or disable TLS encryption. - -#### Choice A: No encryption -* If you intend to use the server solely for local testing, you may disable TLS encryption in the ejabberd config: - - ``` - listen: - starttls_required: false - ``` - -#### Choice B: Self-signed certificate - -If you want to use the server for local testing only, you may use a self-signed certificate to test encryption. -Notice the lobby bots currently reject self-signed certificates. - -* Enable TLS over the default port: - ``` - listen: - starttls: true - ``` - -* Create the key file for certificate: - - ``` - openssl genrsa -out key.pem 2048 - ``` -* Create the certificate file. “common name” should match the domainname. - - ``` - openssl req -new -key key.pem -out request.pem - ``` - -* Sign the certificate: - - ``` - openssl x509 -req -days 900 -in request.pem -signkey key.pem -out certificate.pem - ``` - -* Store it as the ejabberd certificate: - - ``` - $ cat key.pem request.pem > /etc/ejabberd/ejabberd.pem - ``` - -#### Choice C: Let's Encrypt certificate -To secure user authentication and communication with modern encryption and to comply with privacy laws, -ejabberd should be configured to use TLS with a proper, trusted certificate. - -* A free, valid, and trusted TLS certificate may be obtained from some certificate authorites, such as Let's Encrypt: - - - -* Enable TLS over the default port: - ``` - listen: - starttls: true - ``` - -* Setup the contact address if Let's Encrypt found an authentication issue: - - ``` - acme: - contact: "mailto:admin@example.com" - ``` - -* Ensure old, vulnerable SSL/TLS protocols are disabled: - - ``` - define_macro: - 'TLS_OPTIONS': - - "no_sslv2" - - "no_sslv3" - - "no_tlsv1" - ``` - -## 3. Configure ejabberd use policy - -The settings in this section grant or restrict user access rights. - -* Prevent the rooms from being destroyed if the last client leaves it: - - ``` - access_rules: - muc_admin: - - allow: admin - modules: - mod_muc: - access_persistent: muc_admin - default_room_options: - persistent: true - ``` - -* Allow users to create accounts using the game via in-band registration. - ``` - access_rules: - register: - - all: allow - ``` - -### Optional use policies - -* (Optional) It is recommended to restrict usernames to alphanumeric characters (so that playernames are easily typeable for every participant). - The username may be restricted in length (because very long usernames are uncomfortably time-consuming to read and may not fit into the playername fields). - Notice the username regex below is also used by the 0 A.D. client to indicate invalid names to the user. - ``` - acl: - validname: - user_regexp: "^[0-9A-Za-z._-]{1,20}$" - - access_rules: - register: - - allow: validname - - modules: - mod_register: - access: register - ``` - -* (Optional) Prevent users from creating new rooms: - - ``` - modules: - mod_muc: - access_create: muc_admin - ``` - -* (Optional) Increase the maximum number of users from the default 200: - - ``` - mod_muc: - max_users: 5000 - default_room_options: - max_users: 1000 - ``` - -* (Optional) Prevent users from sending too large stanzas. - Notice the bots can send large stanzas as well, so don't restrict it too much. - - ``` - max_stanza_size: 1048576 - ``` - - -* (Optional) Prevent users from changing the room topic: - - ``` - mod_muc: - default_room_options: - allow_change_subj: false - ``` - -* (Optional) Prevent malicious users from registering new accounts quickly if they were banned. - Notice this also prevents players using the same internet router from registering for that time if they want to play together. - - ``` - registration_timeout: 3600 - ``` - -* (Optional) Enable room chatlogging. - Make sure to mention this collection and the purposes in the Terms and Conditions to comply with personal data laws. - Ensure that ejabberd has write access to the given folder. - Notice that `ejabberd.service` by default prevents write access to some directories (PrivateTmp, ProtectHome, ProtectSystem). - - ``` - modules: - mod_muc_log: - outdir: "/lobby/logs" - file_format: plaintext - timezone: universal - mod_muc: - default_room_options: - logging: true - ``` - -* (Optional) Grant specific moderators administrator rights to see the IP address of a user: - See also `https://xmpp.org/extensions/xep-0133.html#get-user-stats`. - - ``` - acl: - admin: - user: - - "username@lobby.wildfiregames.com" - ``` - -* (Optional) Grant specific moderators to : - See also `https://xmpp.org/extensions/xep-0133.html#get-user-stats`. - - ``` - modules: - mod_muc: - access_admin: muc_admin - ``` - -* (Optional) Ban specific IP addresses or subnet masks for persons that create new accounts after having been banned from the room: - - ``` - acl: - blocked: - ip: - - "12.34.56.78" - - "12.34.56.0/8" - - "12.34.0.0/16" - ... - access_rules: - c2s: - - deny: blocked - - allow - register: - - deny: blocked - - allow - ``` - -## 4. Setup lobby bots - -### 4.1 Register lobby bot accounts - -* Check list of registered users: - - ``` - $ ejabberdctl registered_users lobby.wildfiregames.com - ``` - -* Register the accounts of the lobby bots. - The rating account is only needed if you decided to enable the rating service. - - ``` - $ ejabberdctl register echelon23 lobby.wildfiregames.com secure_password - $ ejabberdctl register xpartamupp23 lobby.wildfiregames.com secure_password - ``` - -### 4.2 Authorize lobby bots to see real JIDs - -* The bots need to be able to see real JIDs of users. - So either the room must be configured as non-anonymous, i.e. real JIDs are visible to all users of the room, - or the bots need to receive muc administrator rights. - -#### Choice A: Non-anonymous room -* (Recommended) This method has the advantage that bots do not gain administrative access that they don't use. - The only possible downside is that room users may not hide their username behind arbitrary nicknames anymore. - - ``` - modules: - mod_muc: - default_room_options: - anonymous: false - ``` - -#### Choice B: Non-anonymous room -* If you for any reason wish to configure the room as semi-anonymous (only muc administrators can see real JIDs), - then the bots need to be authorized as muc administrators: - - ``` - access_rules: - muc_admin: - - allow: bots - - modules: - mod_muc: - access_admin: muc_admin - ``` - -### 4.3 Authorize lobby bots with ejabberd - -* The bots need an ACL to be able to get the IPs of users hosting a match (which is what `mod_ipstamp` does). - - ``` - acl: - ## Don't use a regex, to prevent others from obtaining permissions after registering such an account. - bots: - - user: "xpartamupp23@lobby.wildfiregames.com" - - user: "echelon23@lobby.wildfiregames.com" - ``` - -* Add an access rule for `ipbots` and a rule allowing bots to create PubSub nodes: - - ``` - access_rules: - ## Expected by the ipstamp module for XpartaMuPP - ipbots: - - allow: bots - - pubsub_createnode: - - allow: bots - ``` - -* Due to the amount of traffic the bot may process, give the group containing bots either unlimited or a very high traffic shaper: - - ``` - shaper_rules: - c2s_shaper: - - none: admin, bots - - normal - ``` - -* Finally reload ejabberd's configuration: - - ``` - $ ejabberdctl reload_config - ``` - -### 4.4 Running XpartaMuPP - XMPP Multiplayer Game Manager - -* Execute the following command to run the gamelist bot: - - ``` - $ python3 XpartaMuPP.py --domain lobby.wildfiregames.com --login xpartamupp23 --password XXXXXX --nickname GamelistBot --room arena --elo echelon23 - ``` - -If you want to run XpartaMuPP without a rating bot, the `--elo` argument should be omitted. -Pass `--disable-tls` if you did not setup valid TLS encryption on the server. -Run `python3 XpartaMuPP.py --help` for the full list of options - -* If the connection and authentication succeeded, you should see the following messages in the console: - - ``` - INFO JID set to: xpartamupp23@lobby.wildfiregames.com/CC - INFO XpartaMuPP started - ``` - -### 4.5 Running EcheLOn - XMPP Multiplayer Rating Manager - -This bot can be thought of as a module of XpartaMuPP in that IQs stanzas sent to XpartaMuPP are -forwarded onto EcheLOn if its corresponding EcheLOn is online and ignored otherwise. -EcheLOn handles all aspects of operation related to ELO, the chess rating system invented by Arpad Elo. -Players gain a rating after a rated 1v1 match. -The score difference after a completed match is relative to the rating difference of the players. - -* (Optional) Some constants of the algorithm may be edited by experienced administrators at the head of `ELO.py`: - - ``` - # Difference between two ratings such that it is - # regarded as a "sure win" for the higher player. - # No points are gained or lost for such a game. - elo_sure_win_difference = 600.0 - - # Lower ratings "move faster" and change more - # dramatically than higher ones. Anything rating above - # this value moves at the same rate as this value. - elo_k_factor_constant_rating = 2200.0 - ``` - -* To initialize the `lobby_rankings.sqlite3` database, execute the following command: - - ``` - $ python3 LobbyRanking.py - ``` - -* Execute the following command to run the rating bot: - - ``` - $ python3 EcheLOn.py --domain lobby.wildfiregames.com --login echelon23 --password XXXXXX --nickname RatingBot --room arena23 - ``` - -Run `python3 EcheLOn.py --help` for the full list of options - -## 5. Configure Pyrogenesis for the new Multiplayer Lobby - -The Pyrogenesis client is now going to be configured to become able to connect to the new Multiplayer Lobby. - -The Pyrogenesis documentation of configuration files can be found at . -Available Pyrogenesis configuration settings are specified in `default.cfg`, see . - -### 5.1 Local Configuration - - * Visit to identify the local user's Pyrogenesis configuration path depending on the operating system. - - * Create or open `local.cfg` in the configuration path. - - * Add the following settings that determine the lobby server connection: - - ``` - lobby.room = "arena23" ; Default MUC room to join - lobby.server = "lobby.wildfiregames.com" ; Address of lobby server - lobby.stun.server = "lobby.wildfiregames.com" ; Address of the STUN server. - lobby.require_tls = true ; Whether to reject connecting to the lobby if TLS encryption is unavailable. - lobby.verify_certificate = true ; Whether to reject connecting to the lobby if the TLS certificate is invalid. - lobby.xpartamupp = "xpartamupp23" ; Name of the server-side XMPP-account that manage games - lobby.echelon = "echelon23" ; Name of the server-side XMPP-account that manages ratings - ``` - - If you disabled TLS encryption, set `require_tls` to `false`. - If you employed a self-signed certificate, set `verify_certificate` to `false`. - -### 5.2 Test the Multiplayer Lobby - -You should now be able to join the new multiplayer lobby with the Pyrogenesis client and play multiplayer matches. - -* To confirm that the match hosting works as intended, create two user accounts, host a game with one, join the game with the other account. - -* To confirm that the rating service works as intended, resign a rated 1v1 match with two accounts. - -### 5.3 Terms and Conditions - -Players joining public servers are subject to Terms and Conditions of the service provider and subject to privacy laws such as GDPR. -If you intend to use the server only for local testing, you may skip this step. - -* The following files should be created by the service provider: - - `Terms_of_Service.txt` to explain the service and the contract. - `Terms_of_Use.txt` to explain what the user should and should not do. - `Privacy_Policy.txt` to explain how personal data is handled. - -* To use Wildfire Games Terms as a template, obtain our Terms from a copy of the game or from or from - - -* Replace all occurrences of `Wildfire Games` in the files with the one providing the new server. - -* Update the `Terms_of_Use.txt` depending on which behavior you would like to (not) see on your service. - -* Update the `Privacy_Policy.txt` depending on the user data processing in relation to the usage policies. -Make sure to not violate privacy laws such as GDPR or COPPA while doing so. - -* The retention times of ejabberd logs are relevant to GDPR. -Visit for details. - -* The terms should be published online, so users can save and print them. - Add to your `local.cfg`: - - ``` - lobby.terms_url = "https://lobby.wildfiregames.com/terms/"; Allows the user to save the text and print the terms - ``` - -### 5.4 Distribute the configuration - -To make this a public server, distribute your `local.cfg`, `Terms_of_Service.txt`, `Terms_of_Use.txt`, `Privacy_Policy.txt`. - -It may be advisable to create a mod with a modified `default.cfg` and the new terms documents, -see . - -Congratulations, you are now running a custom Pyrogenesis Multiplayer Lobby! Property changes on: ps/trunk/source/tools/lobbybots/README.md ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/tools/lobbybots/mod_ipstamp/mod_ipstamp.spec =================================================================== --- ps/trunk/source/tools/lobbybots/mod_ipstamp/mod_ipstamp.spec (revision 27093) +++ ps/trunk/source/tools/lobbybots/mod_ipstamp/mod_ipstamp.spec (nonexistent) @@ -1,5 +0,0 @@ -author: "Wildfire Games" -category: "log" -summary: "Add senders IP address to game registration stanzas for 0ad" -home: "undefined" -url: "https://play0ad.com" Property changes on: ps/trunk/source/tools/lobbybots/mod_ipstamp/mod_ipstamp.spec ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/tools/lobbybots/mod_ipstamp/src/mod_ipstamp.erl =================================================================== --- ps/trunk/source/tools/lobbybots/mod_ipstamp/src/mod_ipstamp.erl (revision 27093) +++ ps/trunk/source/tools/lobbybots/mod_ipstamp/src/mod_ipstamp.erl (nonexistent) @@ -1,72 +0,0 @@ -%% Copyright (C) 2018 Wildfire Games. -%% This file is part of 0 A.D. -%% -%% 0 A.D. is free software: you can redistribute it and/or modify -%% it under the terms of the GNU General Public License as published by -%% the Free Software Foundation, either version 2 of the License, or -%% (at your option) any later version. -%% -%% 0 A.D. is distributed in the hope that it will be useful, -%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -%% GNU General Public License for more details. -%% -%% You should have received a copy of the GNU General Public License -%% along with 0 A.D. If not, see . - --module(mod_ipstamp). - --behaviour(gen_mod). - --include("ejabberd.hrl"). --include("logger.hrl"). --include("xmpp.hrl"). - --export([start/2, - stop/1, - depends/2, - mod_opt_type/1, - reload/3, - on_filter_packet/1]). - -start(_Host, _Opts) -> - ejabberd_hooks:add(filter_packet, global, ?MODULE, on_filter_packet, 50). - -stop(_Host) -> - ejabberd_hooks:delete(filter_packet, global, ?MODULE, on_filter_packet, 50). - -depends(_Host, _Opts) -> []. - -mod_opt_type(_) -> []. - -reload(_Host, _NewOpts, _OldOpts) -> ok. - --spec on_filter_packet(Input :: iq()) -> iq() | drop. -on_filter_packet(#iq{type = set, to = To, sub_els = [SubEl]} = Input) -> - % We only want to do something for the bots - case acl:match_rule(global, ipbots, To) of - allow -> - NS = xmpp:get_ns(SubEl), - if NS == <<"jabber:iq:gamelist">> -> - SCommand = fxml:get_path_s(SubEl, [{elem, <<"command">>}, cdata]), - if SCommand == <<"register">> -> - % Get the sender's IP. - Ip = xmpp:get_meta(Input, ip), - SIp = inet_parse:ntoa(Ip), - ?INFO_MSG(string:concat("Inserting IP into game registration " - "stanza: ", SIp), []), - Game = fxml:get_subtag(SubEl, <<"game">>), - GameWithIp = fxml:replace_tag_attr(<<"ip">>, SIp, Game), - SubEl2 = fxml:replace_subtag(GameWithIp, SubEl), - xmpp:set_els(Input, [SubEl2]); - true -> - Input - end; - true -> - Input - end; - _ -> Input - end; - -on_filter_packet(Input) -> - Input. Property changes on: ps/trunk/source/tools/lobbybots/mod_ipstamp/src/mod_ipstamp.erl ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/tools/lobbybots/mod_ipstamp/COPYING =================================================================== --- ps/trunk/source/tools/lobbybots/mod_ipstamp/COPYING (revision 27093) +++ ps/trunk/source/tools/lobbybots/mod_ipstamp/COPYING (nonexistent) @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. Property changes on: ps/trunk/source/tools/lobbybots/mod_ipstamp/COPYING ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/tools/lobbybots/mod_ipstamp/README.txt =================================================================== --- ps/trunk/source/tools/lobbybots/mod_ipstamp/README.txt (revision 27093) +++ ps/trunk/source/tools/lobbybots/mod_ipstamp/README.txt (nonexistent) @@ -1,7 +0,0 @@ -mod_ipstamp -=========== - -mod_ipstamp is an ejabberd module for 0ad which adds ip addresses of a -game host to game registration stanzas. - -For it to work the 0ad XMPP bots need to have the ACL "ipbots". Property changes on: ps/trunk/source/tools/lobbybots/mod_ipstamp/README.txt ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/tools/lobbybots/xpartamupp/echelon.py =================================================================== --- ps/trunk/source/tools/lobbybots/xpartamupp/echelon.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/xpartamupp/echelon.py (nonexistent) @@ -1,803 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""0ad XMPP-bot responsible for managing game ratings.""" - -import argparse -import difflib -import logging -import sys -from collections import deque - -import sleekxmpp -from sleekxmpp.stanza import Iq -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin -from sqlalchemy import create_engine, func -from sqlalchemy.orm import scoped_session, sessionmaker - -from xpartamupp.elo import get_rating_adjustment -from xpartamupp.lobby_ranking import Game, Player, PlayerInfo -from xpartamupp.stanzas import (BoardListXmppPlugin, GameReportXmppPlugin, ProfileXmppPlugin) -from xpartamupp.utils import LimitedSizeDict - -# Rating that new players should be inserted into the -# database with, before they've played any games. -LEADERBOARD_DEFAULT_RATING = 1200 - - -class Leaderboard(object): - """Class that provides and manages leaderboard data.""" - - def __init__(self, db_url): - """Initialize the leaderboard.""" - self.rating_messages = deque() - - engine = create_engine(db_url) - session_factory = sessionmaker(bind=engine) - self.db = scoped_session(session_factory) - - def get_or_create_player(self, jid): - """Get a player from the leaderboard database. - - Get player information from the leaderboard database and - create him first, if he doesn't exist yet. - - Arguments: - jid (sleekxmpp.jid.JID): JID of the player to get - - Returns: - Player instance representing the player specified by the - supplied JID - - """ - player = self.db.query(Player).filter(Player.jid.ilike(str(jid))).first() - if player: - return player - - player = Player(jid=str(jid), rating=-1) - self.db.add(player) - self.db.commit() - logging.debug("Created player %s", jid) - return player - - def get_profile(self, jid): - """Get the leaderboard profile for the specified player. - - Arguments: - jid (sleekxmpp.jid.JID): JID of the player to retrieve the - profile for - - Returns: - dict with statistics about the requested player or None if - the player isn't known - - """ - stats = {} - player = self.db.query(Player).filter(Player.jid.ilike(str(jid))).first() - - if not player: - logging.debug("Couldn't find profile for player %s", jid) - return {} - - if player.rating != -1: - stats['rating'] = player.rating - rank = self.db.query(Player).filter(Player.rating >= player.rating).count() - stats['rank'] = rank - - if player.highest_rating != -1: - stats['highestRating'] = player.highest_rating - - games_played = self.db.query(PlayerInfo).filter_by(player_id=player.id).count() - wins = self.db.query(Game).filter_by(winner_id=player.id).count() - stats['totalGamesPlayed'] = games_played - stats['wins'] = wins - stats['losses'] = games_played - wins - return stats - - def _add_game(self, game_report): # pylint: disable=too-many-locals - """Add a game to the database. - - Add a game to the database and update the data on a - player from game results. - - Arguments: - game_report (dict): a report about a game - - Returns: - Game object for the created game or None if the creation - failed for any reason. - - """ - # Discard any games still in progress. We shouldn't get - # reports from those games anyway. - if 'active' in dict.values(game_report['playerStates']): - logging.warning("Received a game report for an unfinished game") - return None - - players = self.db.query(Player).filter(func.lower(Player.jid).in_( - dict.keys(game_report['playerStates']))) - - winning_jid = [jid for jid, state in game_report['playerStates'].items() - if state == 'won'][0] - - # single_stats = {'timeElapsed', 'mapName', 'teamsLocked', 'matchID'} - total_score_stats = {'economyScore', 'militaryScore', 'totalScore'} - resource_stats = {'foodGathered', 'foodUsed', 'woodGathered', 'woodUsed', 'stoneGathered', - 'stoneUsed', 'metalGathered', 'metalUsed', 'vegetarianFoodGathered', - 'treasuresCollected', 'lootCollected', 'tributesSent', - 'tributesReceived'} - units_stats = {'totalUnitsTrained', 'totalUnitsLost', 'enemytotalUnitsKilled', - 'infantryUnitsTrained', 'infantryUnitsLost', 'enemyInfantryUnitsKilled', - 'workerUnitsTrained', 'workerUnitsLost', 'enemyWorkerUnitsKilled', - 'femaleCitizenUnitsTrained', 'femaleCitizenUnitsLost', - 'enemyFemaleCitizenUnitsKilled', 'cavalryUnitsTrained', 'cavalryUnitsLost', - 'enemyCavalryUnitsKilled', 'championUnitsTrained', 'championUnitsLost', - 'enemyChampionUnitsKilled', 'heroUnitsTrained', 'heroUnitsLost', - 'enemyHeroUnitsKilled', 'shipUnitsTrained', 'shipUnitsLost', - 'enemyShipUnitsKilled', 'traderUnitsTrained', 'traderUnitsLost', - 'enemyTraderUnitsKilled'} - buildings_stats = {'totalBuildingsConstructed', 'totalBuildingsLost', - 'enemytotalBuildingsDestroyed', 'civCentreBuildingsConstructed', - 'civCentreBuildingsLost', 'enemyCivCentreBuildingsDestroyed', - 'houseBuildingsConstructed', 'houseBuildingsLost', - 'enemyHouseBuildingsDestroyed', 'economicBuildingsConstructed', - 'economicBuildingsLost', 'enemyEconomicBuildingsDestroyed', - 'outpostBuildingsConstructed', 'outpostBuildingsLost', - 'enemyOutpostBuildingsDestroyed', 'militaryBuildingsConstructed', - 'militaryBuildingsLost', 'enemyMilitaryBuildingsDestroyed', - 'fortressBuildingsConstructed', 'fortressBuildingsLost', - 'enemyFortressBuildingsDestroyed', 'wonderBuildingsConstructed', - 'wonderBuildingsLost', 'enemyWonderBuildingsDestroyed'} - market_stats = {'woodBought', 'foodBought', 'stoneBought', 'metalBought', 'tradeIncome'} - misc_stats = {'civs', 'teams', 'percentMapExplored'} - - stats = total_score_stats | resource_stats | units_stats | buildings_stats | market_stats \ - | misc_stats - - player_infos = [] - for player in players: - player_jid = sleekxmpp.jid.JID(player.jid) - player_info = PlayerInfo(player=player) - for report_name in stats: - setattr(player_info, report_name, game_report[report_name][player_jid]) - player_infos.append(player_info) - - game = Game(map=game_report['mapName'], duration=int(game_report['timeElapsed']), - teamsLocked=bool(game_report['teamsLocked']), matchID=game_report['matchID']) - game.player_info.extend(player_infos) - game.winner = self.db.query(Player).filter(Player.jid.ilike(str(winning_jid))).first() - self.db.add(game) - self.db.commit() - return game - - @staticmethod - def _verify_game(game_report): - """Check whether or not the game should be rated. - - The criteria for rated games can be specified here. - - Arguments: - game_report (dict): a report about a game - - Returns: - True if the game should be rated, false otherwise. - - """ - winning_jids = [jid for jid, state in game_report['playerStates'].items() - if state == 'won'] - # We only support 1v1s right now. - if len(winning_jids) > 1 or len(dict.keys(game_report['playerStates'])) != 2: - return False - return True - - def _rate_game(self, game): - """Update player ratings based on game outcome. - - Take a game with 2 players and alters their ratings based on - the result of the game. - - Adjusts the players ratings in the database. - - Arguments: - game (Game): game to rate - """ - player1 = game.players[0] - player2 = game.players[1] - # Since it's impossible to draw in the game currently, the - # database model, and therefore this code, requires a winner. - # The Elo implementation does not, however. - result = 1 if player1 == game.winner else -1 - # Player's ratings are -1 unless they have played a rated game. - if player1.rating == -1: - player1.rating = LEADERBOARD_DEFAULT_RATING - if player2.rating == -1: - player2.rating = LEADERBOARD_DEFAULT_RATING - - try: - rating_adjustment1 = int(get_rating_adjustment(player1.rating, player2.rating, - len(player1.games), len(player2.games), - result)) - rating_adjustment2 = int(get_rating_adjustment(player2.rating, player1.rating, - len(player2.games), len(player1.games), - result * -1)) - except ValueError: - rating_adjustment1 = 0 - rating_adjustment2 = 0 - - if result == 1: - result_qualitative = 'won' - elif result == 0: - result_qualitative = 'drew' - else: - result_qualitative = 'lost' - name1 = sleekxmpp.jid.JID(player1.jid).local - name2 = sleekxmpp.jid.JID(player2.jid).local - self.rating_messages.append("A rated game has ended. %s %s against %s. Rating " - "Adjustment: %s (%s -> %s) and %s (%s -> %s)." % - (name1, result_qualitative, name2, name1, player1.rating, - player1.rating + rating_adjustment1, name2, player2.rating, - player2.rating + rating_adjustment2)) - player1.rating += rating_adjustment1 - player2.rating += rating_adjustment2 - if not player1.highest_rating: - player1.highest_rating = -1 - if not player2.highest_rating: - player2.highest_rating = -1 - player1.highest_rating = max(player1.rating, player1.highest_rating) - player2.highest_rating = max(player2.rating, player2.highest_rating) - self.db.commit() - - def get_rating_messages(self): - """Get messages announcing rated games. - - Returns: - list with the a messages about rated games - - """ - return self.rating_messages - - def add_and_rate_game(self, game_report): - """Add and rate a game. - - If the game has only two players, rate the game. - - Arguments: - game_report (dict): a report about a game - - Returns: - Game object - - """ - game = self._add_game(game_report) - if game and self._verify_game(game_report): - self._rate_game(game) - return game - - def get_board(self, limit=100): - """Return the ratings of the highest ranked players. - - Arguments: - limit (int): Number of players to return - - Returns: - dict with player JIDs, nicks and ratings - - """ - ratings = {} - players = self.db.query(Player).filter(Player.rating != -1) \ - .order_by(Player.rating.desc()).limit(limit) - for player in players: - ratings[player.jid] = {'name': sleekxmpp.jid.JID(player.jid).local, - 'rating': player.rating} - return ratings - - def get_rating_list(self, nicks): - """Return the ratings of all online players. - - The returned dictionary is by nick because the client can't - link JID to nick conveniently. - - Arguments: - nicks (dict): Players currently online - - Returns: - dict with player JIDs, nicks and ratings - - """ - ratings = {} - if nicks: - player_filter = func.lower(Player.jid).in_([str(jid).lower() for jid in list(nicks)]) - players = self.db.query(Player.jid, Player.rating).filter(player_filter) - for player in players: - rating = str(player.rating) if player.rating != -1 else '' - for jid in list(nicks): - if jid == sleekxmpp.jid.JID(player.jid): - ratings[nicks[str(jid)]] = {'name': nicks[jid], 'rating': rating} - break - return ratings - - -class ReportManager(object): - """Class which manages different game reports from clients. - - Calls leaderboard functions as appropriate. - """ - - def __init__(self, leaderboard): - """Initialize the report manager. - - Arguments: - leaderboard (Leaderboard): Leaderboard the manager is for - - """ - self.leaderboard = leaderboard - self.interim_report_tracker = LimitedSizeDict(size_limit=2**12) - - def add_report(self, jid, raw_game_report): - """Add a game to the interface between a raw report and the leaderboard database. - - Arguments: - jid (sleekxmpp.jid.JID): JID of the player who submitted - the report - raw_game_report (dict): Game report generated by 0ad - - """ - player_index = int(raw_game_report['playerID']) - 1 - del raw_game_report['playerID'] - match_id = raw_game_report['matchID'] - if match_id not in self.interim_report_tracker: - self.interim_report_tracker[match_id] = { - 'report': raw_game_report, - 'jids': {player_index: str(jid)} - } - else: - current_match = self.interim_report_tracker[match_id] - if raw_game_report != current_match['report']: - report_diff = self._get_report_diff(raw_game_report, current_match['report']) - logging.warning("Retrieved reports for match %s differ:\n %s", match_id, - report_diff) - return - - player_jids = current_match['jids'] - if player_index in player_jids: - if player_jids[player_index] == jid: - logging.warning("Received a report for match %s from player %s twice.", - match_id, jid) - else: - logging.warning("Retrieved a report for match %s for the same player twice, " - "but from two different XMPP accounts: %s vs. %s", match_id, - player_jids[player_index], jid) - return - else: - player_jids[player_index] = str(jid) - - num_players = self._get_num_players(raw_game_report) - num_retrieved_reports = len(player_jids) - if num_retrieved_reports == num_players: - try: - self.leaderboard.add_and_rate_game(self._expand_report( - current_match)) - except Exception: - logging.exception("Failed to add and rate a game.") - del current_match - elif num_retrieved_reports < num_players: - logging.warning("Haven't received all reports for the game yet. %i/%i", - num_retrieved_reports, num_players) - elif num_retrieved_reports > num_players: - logging.warning("Retrieved more reports than players. This shouldn't happen.") - - @staticmethod - def _expand_report(game_report): - """Re-formats a game report into Python data structures. - - Player specific values from the report are replaced with a - dict where the JID of the player is the key. - - Arguments: - game_report (dict): wrapped game report from 0ad - - Returns a processed gameReport of type dict. - """ - processed_game_report = {} - for key, value in game_report['report'].items(): - if ',' not in value: - processed_game_report[key] = value - else: - stat_to_jid = {} - for i, part in enumerate(game_report['report'][key].split(",")[:-1]): - stat_to_jid[game_report['jids'][i]] = part - processed_game_report[key] = stat_to_jid - return processed_game_report - - @staticmethod - def _get_num_players(raw_game_report): - """Compute the number of players from a raw game report. - - Get the number of players who played a game from the - playerStates field in a raw game report. - - Arguments: - raw_game_report (dict): Game report generated by 0ad - - Returns: - int with the number of players in the game - - Raises: - ValueError if the number of players couldn't be determined - - """ - if 'playerStates' in raw_game_report and ',' in raw_game_report['playerStates']: - return len(list(filter(None, raw_game_report['playerStates'].split(",")))) - raise ValueError() - - @staticmethod - def _get_report_diff(report1, report2): - """Get differences between two reports. - - Arguments: - report1 (dict): Game report - report2 (dict): Game report - - Returns: - str with a textual representation of the differences - between the two reports - - """ - report1_list = ['{ %s: %s }' % (key, value) for key, value in report1.items()] - report2_list = ['{ %s: %s }' % (key, value) for key, value in report2.items()] - return '\n'.join(difflib.ndiff(report1_list, report2_list)) - - -class EcheLOn(sleekxmpp.ClientXMPP): - """Main class which handles IQ data and sends new data.""" - - def __init__(self, sjid, password, room, nick, leaderboard): - """Initialize EcheLOn.""" - sleekxmpp.ClientXMPP.__init__(self, sjid, password) - self.whitespace_keepalive = False - - self.sjid = sleekxmpp.jid.JID(sjid) - self.room = room - self.nick = nick - - self.leaderboard = leaderboard - self.report_manager = ReportManager(self.leaderboard) - - register_stanza_plugin(Iq, BoardListXmppPlugin) - register_stanza_plugin(Iq, GameReportXmppPlugin) - register_stanza_plugin(Iq, ProfileXmppPlugin) - - self.register_handler(Callback('Iq Boardlist', StanzaPath('iq@type=get/boardlist'), - self._iq_board_list_handler)) - self.register_handler(Callback('Iq GameReport', StanzaPath('iq@type=set/gamereport'), - self._iq_game_report_handler)) - self.register_handler(Callback('Iq Profile', StanzaPath('iq@type=get/profile'), - self._iq_profile_handler)) - - self.add_event_handler('session_start', self._session_start) - self.add_event_handler('muc::%s::got_online' % self.room, self._muc_online) - self.add_event_handler('muc::%s::got_offline' % self.room, self._muc_offline) - self.add_event_handler('groupchat_message', self._muc_message) - - def _session_start(self, event): # pylint: disable=unused-argument - """Join MUC channel and announce presence. - - Arguments: - event (dict): empty dummy dict - - """ - self.plugin['xep_0045'].joinMUC(self.room, self.nick) - self.send_presence() - self.get_roster() - logging.info("EcheLOn started") - - def _muc_online(self, presence): - """Add joining players to the list of players. - - Arguments: - presence (sleekxmpp.stanza.presence.Presence): Received - presence stanza. - - """ - nick = str(presence['muc']['nick']) - jid = sleekxmpp.jid.JID(presence['muc']['jid']) - - if nick == self.nick: - return - - if jid.resource != '0ad': - return - - self.leaderboard.get_or_create_player(jid) - - self._broadcast_rating_list() - - logging.debug("Client '%s' connected with a nick of '%s'.", jid, nick) - - def _muc_offline(self, presence): - """Remove leaving players from the list of players. - - Arguments: - presence (sleekxmpp.stanza.presence.Presence): Received - presence stanza. - - """ - nick = str(presence['muc']['nick']) - jid = sleekxmpp.jid.JID(presence['muc']['jid']) - - if nick == self.nick: - return - - logging.debug("Client '%s' with nick '%s' disconnected", jid, nick) - - def _muc_message(self, msg): - """Process messages in the MUC room. - - Respond to messages highlighting the bots name with an - informative message. - - Arguments: - msg (sleekxmpp.stanza.message.Message): Received MUC - message - """ - if msg['mucnick'] != self.nick and self.nick.lower() in msg['body'].lower(): - self.send_message(mto=msg['from'].bare, - mbody="I am just a bot and provide the rating functionality for " - "this lobby. Please don't disturb me, calculating these " - "ratings is already difficult enough.", - mtype='groupchat') - - def _iq_board_list_handler(self, iq): - """Handle incoming leaderboard list requests. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): Received IQ stanza - - """ - if iq['from'].resource not in ['0ad']: - return - - command = iq['boardlist']['command'] - self.leaderboard.get_or_create_player(iq['from']) - if command == 'getleaderboard': - try: - self._send_leaderboard(iq) - except Exception: - logging.exception("Failed to process get leaderboard request from %s", - iq['from'].bare) - elif command == 'getratinglist': - try: - self._send_rating_list(iq) - except Exception: - logging.exception("Failed to send the rating list to %s", iq['from']) - - def _iq_game_report_handler(self, iq): - """Handle end of game reports from clients. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): Received IQ stanza - - """ - if iq['from'].resource not in ['0ad']: - return - - try: - self.report_manager.add_report(iq['from'], iq['gamereport']['game']) - except Exception: - logging.exception("Failed to update game statistics for %s", iq['from'].bare) - - rating_messages = self.leaderboard.get_rating_messages() - if rating_messages: - while rating_messages: - message = rating_messages.popleft() - self.send_message(mto=self.room, mbody=message, mtype='groupchat', mnick=self.nick) - self._broadcast_rating_list() - - def _iq_profile_handler(self, iq): - """Handle profile requests from clients. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): Received IQ stanza - - """ - if iq['from'].resource not in ['0ad']: - return - - try: - self._send_profile(iq, iq['profile']['command']) - except Exception: - logging.exception("Failed to send profile about %s to %s", iq['profile']['command'], - iq['from'].bare) - - def _send_leaderboard(self, iq): - """Send the whole leaderboard. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): IQ stanza to reply to - - """ - ratings = self.leaderboard.get_board() - - iq = iq.reply(clear=True) - stanza = BoardListXmppPlugin() - stanza.add_command('boardlist') - for player in ratings.values(): - stanza.add_item(player['name'], player['rating']) - iq.set_payload(stanza) - - try: - iq.send(block=False) - except Exception: - logging.exception("Failed to send leaderboard to %s", iq['to']) - - def _send_rating_list(self, iq): - """Send the ratings of all online players. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): IQ stanza to reply to - - """ - nicks = {} - for nick in self.plugin['xep_0045'].getRoster(self.room): - if nick == self.nick: - continue - jid_str = self.plugin['xep_0045'].getJidProperty(self.room, nick, 'jid') - jid = sleekxmpp.jid.JID(jid_str) - nicks[jid] = nick - ratings = self.leaderboard.get_rating_list(nicks) - - iq = iq.reply(clear=True) - stanza = BoardListXmppPlugin() - stanza.add_command('ratinglist') - for player in ratings.values(): - stanza.add_item(player['name'], player['rating']) - iq.set_payload(stanza) - - try: - iq.send(block=False) - except Exception: - logging.exception("Failed to send rating list to %s", iq['to']) - - def _broadcast_rating_list(self): - """Broadcast the ratings of all online players.""" - nicks = {} - for nick in self.plugin['xep_0045'].getRoster(self.room): - if nick == self.nick: - continue - jid_str = self.plugin['xep_0045'].getJidProperty(self.room, nick, 'jid') - jid = sleekxmpp.jid.JID(jid_str) - nicks[jid] = nick - ratings = self.leaderboard.get_rating_list(nicks) - - stanza = BoardListXmppPlugin() - stanza.add_command('ratinglist') - for player in ratings.values(): - stanza.add_item(player['name'], player['rating']) - - for jid in nicks: - iq = self.make_iq_result(ito=jid) - iq.set_payload(stanza) - try: - iq.send(block=False) - except Exception: - logging.exception("Failed to send rating list to %s", jid) - - def _send_profile(self, iq, player_nick): - """Send the player profile to a specified target. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): IQ stanza to reply to - player_nick (str): The nick of the player to get the - profile for - - """ - jid_str = self.plugin['xep_0045'].getJidProperty(self.room, player_nick, 'jid') - player_jid = sleekxmpp.jid.JID(jid_str) if jid_str else None - - # The player the profile got requested for is not online, so - # let's assume the JID contains the nick as local part. - if not player_jid: - player_jid = sleekxmpp.jid.JID('%s@%s/%s' % (player_nick, self.sjid.domain, '0ad')) - - try: - stats = self.leaderboard.get_profile(player_jid) - except Exception: - logging.exception("Failed to get leaderboard profile for player %s", player_jid) - stats = {} - - iq = iq.reply(clear=True) - stanza = ProfileXmppPlugin() - if stats: - stanza.add_item(player_nick, stats['rating'], stats['highestRating'], - stats['rank'], stats['totalGamesPlayed'], stats['wins'], - stats['losses']) - else: - stanza.add_item(player_nick, -2) - stanza.add_command(player_nick) - iq.set_payload(stanza) - - try: - iq.send(block=False) - except Exception: - logging.exception("Failed to send profile to %s", iq['to']) - - -def parse_args(args): - """Parse command line arguments. - - Arguments: - args (dict): Raw command line arguments given to the script - - Returns: - Parsed command line arguments - - """ - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description="EcheLOn - XMPP Rating Bot") - - log_settings = parser.add_mutually_exclusive_group() - log_settings.add_argument('-q', '--quiet', help="only log errors", action='store_const', - dest='log_level', const=logging.ERROR) - log_settings.add_argument('-d', '--debug', help="log debug messages", action='store_const', - dest='log_level', const=logging.DEBUG) - log_settings.add_argument('-v', '--verbose', help="log more informative messages", - action='store_const', dest='log_level', const=logging.INFO) - log_settings.set_defaults(log_level=logging.WARNING) - - parser.add_argument('-m', '--domain', help="XMPP server to connect to", - default='lobby.wildfiregames.com') - parser.add_argument('-l', '--login', help="username for login", default='EcheLOn') - parser.add_argument('-p', '--password', help="password for login", default='XXXXXX') - parser.add_argument('-n', '--nickname', help="nickname shown to players", default='RatingsBot') - parser.add_argument('-r', '--room', help="XMPP MUC room to join", default='arena') - parser.add_argument('--database-url', help="URL for the leaderboard database", - default='sqlite:///lobby_rankings.sqlite3') - parser.add_argument('-s', '--server', help='address of the ejabberd server', - action='store', dest='xserver', default=None) - parser.add_argument('-t', '--disable-tls', help='Pass this argument to connect without TLS encryption', - action='store_true', dest='xdisabletls', default=False) - - return parser.parse_args(args) - -def main(): - """Entry point a console script.""" - args = parse_args(sys.argv[1:]) - - logging.basicConfig(level=args.log_level, - format='%(asctime)s %(levelname)-8s %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') - - leaderboard = Leaderboard(args.database_url) - xmpp = EcheLOn(sleekxmpp.jid.JID('%s@%s/%s' % (args.login, args.domain, 'CC')), args.password, - args.room + '@conference.' + args.domain, args.nickname, leaderboard) - xmpp.register_plugin('xep_0030') # Service Discovery - xmpp.register_plugin('xep_0004') # Data Forms - xmpp.register_plugin('xep_0045') # Multi-User Chat - xmpp.register_plugin('xep_0060') # Publish-Subscribe - xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping - - if xmpp.connect((args.xserver, 5222) if args.xserver else None, True, not args.xdisabletls): - xmpp.process() - else: - logging.error("Unable to connect") - - -if __name__ == '__main__': - main() Index: ps/trunk/source/tools/lobbybots/xpartamupp/__init__.py =================================================================== Index: ps/trunk/source/tools/lobbybots/xpartamupp/elo.py =================================================================== --- ps/trunk/source/tools/lobbybots/xpartamupp/elo.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/xpartamupp/elo.py (nonexistent) @@ -1,82 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""Implementation of the ELO-rating algorithm for 0ad games.""" - -# Difference between two ratings such that it is regarded as a "sure -# win" for the higher player. No points are gained or lost for such a -# game. -ELO_SURE_WIN_DIFFERENCE = 600 - -# Lower ratings "move faster" and change more -# dramatically than higher ones. Anything rating above -# this value moves at the same rate as this value. -ELO_K_FACTOR_CONSTANT_RATING = 2200 - -# This preset number of games is the number of games where a player is -# considered "stable". Rating volatility is constant after this number. -VOLATILITY_CONSTANT = 20 - -# Fair rating adjustment loses against inflation. -# This constant will battle inflation. -# NOTE: This can be adjusted as needed by a bot/server administrator -ANTI_INFLATION = 0.015 - - -def get_rating_adjustment(rating, opponent_rating, games_played, - opponent_games_played, result): # pylint: disable=unused-argument - """Calculate the rating adjustment after rated 1v1 games. - - The rating adjustment is calculated using a simplified - ELO-algorithm. - - The given implementation doesn't work for negative ratings below - -2199. This is a known limitation which is currently considered - to be not relevant in day-to-day use. - - Arguments: - rating (int): Rating of the first player before the game. - opponent_rating (int): Rating of the second player before the - game. - games_played (int): Number of games the first player has played - before this game. - opponent_games_played (int): Number of games the second player - has played before this game. - result (int): 1 if the first player won, 0 if draw or -1 if the - second player won. - - Returns: - int: the adjustment which should be applied to the rating of - the first player - - """ - if rating < -2199 or opponent_rating < -2199: - raise ValueError('Too small rating given: rating: %i, opponent rating: %i' % - (rating, opponent_rating)) - - rating_k_factor = 50.0 * (min(rating, ELO_K_FACTOR_CONSTANT_RATING) / - ELO_K_FACTOR_CONSTANT_RATING + 1.0) / 2.0 - player_volatility = (min(max(0, games_played), VOLATILITY_CONSTANT) / - VOLATILITY_CONSTANT + 0.25) / 1.25 - volatility = rating_k_factor * player_volatility - rating_difference = opponent_rating - rating - rating_adjustment = (rating_difference + result * ELO_SURE_WIN_DIFFERENCE) / volatility - \ - ANTI_INFLATION - if result == 1: - return round(max(0.0, rating_adjustment)) - elif result == -1: - return round(min(0.0, rating_adjustment)) - return round(rating_adjustment) Index: ps/trunk/source/tools/lobbybots/xpartamupp/lobby_ranking.py =================================================================== --- ps/trunk/source/tools/lobbybots/xpartamupp/lobby_ranking.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/xpartamupp/lobby_ranking.py (nonexistent) @@ -1,175 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""Database schema used by the XMPP bots to store game information.""" - -import argparse -import sys - -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, create_engine -from sqlalchemy.orm import relationship -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - - -class Player(Base): - """Model representing players.""" - - __tablename__ = 'players' - - id = Column(Integer, primary_key=True) - jid = Column(String(255)) - rating = Column(Integer) - highest_rating = Column(Integer) - games = relationship('Game', secondary='players_info') - # These two relations really only exist to satisfy the linkage - # between PlayerInfo and Player and Game and player. - games_info = relationship('PlayerInfo', backref='player') - games_won = relationship('Game', backref='winner') - - -class PlayerInfo(Base): - """Model representing game results.""" - - __tablename__ = 'players_info' - - id = Column(Integer, primary_key=True) - player_id = Column(Integer, ForeignKey('players.id')) - game_id = Column(Integer, ForeignKey('games.id')) - civs = Column(String(20)) - teams = Column(Integer) - economyScore = Column(Integer) - militaryScore = Column(Integer) - totalScore = Column(Integer) - foodGathered = Column(Integer) - foodUsed = Column(Integer) - woodGathered = Column(Integer) - woodUsed = Column(Integer) - stoneGathered = Column(Integer) - stoneUsed = Column(Integer) - metalGathered = Column(Integer) - metalUsed = Column(Integer) - vegetarianFoodGathered = Column(Integer) - treasuresCollected = Column(Integer) - lootCollected = Column(Integer) - tributesSent = Column(Integer) - tributesReceived = Column(Integer) - totalUnitsTrained = Column(Integer) - totalUnitsLost = Column(Integer) - enemytotalUnitsKilled = Column(Integer) - infantryUnitsTrained = Column(Integer) - infantryUnitsLost = Column(Integer) - enemyInfantryUnitsKilled = Column(Integer) - workerUnitsTrained = Column(Integer) - workerUnitsLost = Column(Integer) - enemyWorkerUnitsKilled = Column(Integer) - femaleCitizenUnitsTrained = Column(Integer) - femaleCitizenUnitsLost = Column(Integer) - enemyFemaleCitizenUnitsKilled = Column(Integer) - cavalryUnitsTrained = Column(Integer) - cavalryUnitsLost = Column(Integer) - enemyCavalryUnitsKilled = Column(Integer) - championUnitsTrained = Column(Integer) - championUnitsLost = Column(Integer) - enemyChampionUnitsKilled = Column(Integer) - heroUnitsTrained = Column(Integer) - heroUnitsLost = Column(Integer) - enemyHeroUnitsKilled = Column(Integer) - shipUnitsTrained = Column(Integer) - shipUnitsLost = Column(Integer) - enemyShipUnitsKilled = Column(Integer) - traderUnitsTrained = Column(Integer) - traderUnitsLost = Column(Integer) - enemyTraderUnitsKilled = Column(Integer) - totalBuildingsConstructed = Column(Integer) - totalBuildingsLost = Column(Integer) - enemytotalBuildingsDestroyed = Column(Integer) - civCentreBuildingsConstructed = Column(Integer) - civCentreBuildingsLost = Column(Integer) - enemyCivCentreBuildingsDestroyed = Column(Integer) - houseBuildingsConstructed = Column(Integer) - houseBuildingsLost = Column(Integer) - enemyHouseBuildingsDestroyed = Column(Integer) - economicBuildingsConstructed = Column(Integer) - economicBuildingsLost = Column(Integer) - enemyEconomicBuildingsDestroyed = Column(Integer) - outpostBuildingsConstructed = Column(Integer) - outpostBuildingsLost = Column(Integer) - enemyOutpostBuildingsDestroyed = Column(Integer) - militaryBuildingsConstructed = Column(Integer) - militaryBuildingsLost = Column(Integer) - enemyMilitaryBuildingsDestroyed = Column(Integer) - fortressBuildingsConstructed = Column(Integer) - fortressBuildingsLost = Column(Integer) - enemyFortressBuildingsDestroyed = Column(Integer) - wonderBuildingsConstructed = Column(Integer) - wonderBuildingsLost = Column(Integer) - enemyWonderBuildingsDestroyed = Column(Integer) - woodBought = Column(Integer) - foodBought = Column(Integer) - stoneBought = Column(Integer) - metalBought = Column(Integer) - tradeIncome = Column(Integer) - percentMapExplored = Column(Integer) - - -class Game(Base): - """Model representing games.""" - - __tablename__ = 'games' - - id = Column(Integer, primary_key=True) - map = Column(String(80)) - duration = Column(Integer) - teamsLocked = Column(Boolean) - matchID = Column(String(20)) - winner_id = Column(Integer, ForeignKey('players.id')) - player_info = relationship('PlayerInfo', backref='game') - players = relationship('Player', secondary='players_info') - - -def parse_args(args): - """Parse command line arguments. - - Arguments: - args (dict): Raw command line arguments given to the script - - Returns: - Parsed command line arguments - - """ - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description="Helper command for database creation") - parser.add_argument('action', help='Action to apply to the database', - choices=['create']) - parser.add_argument('--database-url', help='URL for the leaderboard database', - default='sqlite:///lobby_rankings.sqlite3') - return parser.parse_args(args) - - -def main(): - """Entry point a console script.""" - args = parse_args(sys.argv[1:]) - engine = create_engine(args.database_url) - if args.action == 'create': - Base.metadata.create_all(engine) - - -if __name__ == '__main__': - main() Index: ps/trunk/source/tools/lobbybots/xpartamupp/utils.py =================================================================== --- ps/trunk/source/tools/lobbybots/xpartamupp/utils.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/xpartamupp/utils.py (nonexistent) @@ -1,48 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""Collection of utility functions used by the XMPP-bots.""" - -from collections import OrderedDict - - -class LimitedSizeDict(OrderedDict): - """Dictionary with limited size and FIFO characteristics.""" - - def __init__(self, *args, **kwargs): - """Initialize the dictionary. - - Set the limit to which size the dict should be able to grow. - """ - self.size_limit = kwargs.pop('size_limit', None) - OrderedDict.__init__(self, *args, **kwargs) - self._check_size_limit() - - def __setitem__(self, key, value): # pylint: disable=signature-differs - """Overwrite default method to add size limit check.""" - OrderedDict.__setitem__(self, key, value) - self._check_size_limit() - - def _check_size_limit(self): - """Ensure dict is not larger than the size limit. - - Compares the current size of the dict with the size limit and - removes items from the dict until the size is equal the size - limit. - """ - if self.size_limit: - while len(self) > self.size_limit: - self.popitem(last=False) Index: ps/trunk/source/tools/lobbybots/xpartamupp/xpartamupp.py =================================================================== --- ps/trunk/source/tools/lobbybots/xpartamupp/xpartamupp.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/xpartamupp/xpartamupp.py (nonexistent) @@ -1,364 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""0ad XMPP-bot responsible for managing game listings.""" - -import argparse -import logging -import time -import sys - -import sleekxmpp -from sleekxmpp.stanza import Iq -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin - -from xpartamupp.stanzas import GameListXmppPlugin -from xpartamupp.utils import LimitedSizeDict - - -class Games(object): - """Class to tracks all games in the lobby.""" - - def __init__(self): - """Initialize with empty games.""" - self.games = LimitedSizeDict(size_limit=2**7) - - def add_game(self, jid, data): - """Add a game. - - Arguments: - jid (sleekxmpp.jid.JID): JID of the player who started the - game - data (dict): information about the game - - Returns: - True if adding the game succeeded, False if not - - """ - try: - data['players-init'] = data['players'] - data['nbp-init'] = data['nbp'] - data['state'] = 'init' - except (KeyError, TypeError, ValueError): - logging.warning("Received invalid data for add game from 0ad: %s", data) - return False - else: - self.games[jid] = data - return True - - def remove_game(self, jid): - """Remove a game attached to a JID. - - Arguments: - jid (sleekxmpp.jid.JID): JID of the player whose game to - remove. - - Returns: - True if removing the game succeeded, False if not - - """ - try: - del self.games[jid] - except KeyError: - logging.warning("Game for jid %s didn't exist", jid) - return False - else: - return True - - def get_all_games(self): - """Return all games. - - Returns: - dict containing all games with the JID of the player who - started the game as key. - - """ - return self.games - - def change_game_state(self, jid, data): - """Switch game state between running and waiting. - - Arguments: - jid (sleekxmpp.jid.JID): JID of the player whose game to - change - data (dict): information about the game - - Returns: - True if changing the game state succeeded, False if not - - """ - if jid not in self.games: - logging.warning("Tried to change state for non-existent game %s", jid) - return False - - try: - if self.games[jid]['nbp-init'] > data['nbp']: - logging.debug("change game (%s) state from %s to %s", jid, - self.games[jid]['state'], 'waiting') - self.games[jid]['state'] = 'waiting' - else: - logging.debug("change game (%s) state from %s to %s", jid, - self.games[jid]['state'], 'running') - self.games[jid]['state'] = 'running' - self.games[jid]['nbp'] = data['nbp'] - self.games[jid]['players'] = data['players'] - except (KeyError, ValueError): - logging.warning("Received invalid data for change game state from 0ad: %s", data) - return False - else: - if 'startTime' not in self.games[jid]: - self.games[jid]['startTime'] = str(round(time.time())) - return True - - -class XpartaMuPP(sleekxmpp.ClientXMPP): - """Main class which handles IQ data and sends new data.""" - - def __init__(self, sjid, password, room, nick): - """Initialize XpartaMuPP. - - Arguments: - sjid (sleekxmpp.jid.JID): JID to use for authentication - password (str): password to use for authentication - room (str): XMPP MUC room to join - nick (str): Nick to use in MUC - - """ - sleekxmpp.ClientXMPP.__init__(self, sjid, password) - self.whitespace_keepalive = False - - self.room = room - self.nick = nick - - self.games = Games() - - register_stanza_plugin(Iq, GameListXmppPlugin) - - self.register_handler(Callback('Iq Gamelist', StanzaPath('iq@type=set/gamelist'), - self._iq_game_list_handler)) - - self.add_event_handler('session_start', self._session_start) - self.add_event_handler('muc::%s::got_online' % self.room, self._muc_online) - self.add_event_handler('muc::%s::got_offline' % self.room, self._muc_offline) - self.add_event_handler('groupchat_message', self._muc_message) - - def _session_start(self, event): # pylint: disable=unused-argument - """Join MUC channel and announce presence. - - Arguments: - event (dict): empty dummy dict - - """ - self.plugin['xep_0045'].joinMUC(self.room, self.nick) - self.send_presence() - self.get_roster() - logging.info("XpartaMuPP started") - - def _muc_online(self, presence): - """Add joining players to the list of players. - - Also send a list of games to them, so they see which games - are currently there. - - Arguments: - presence (sleekxmpp.stanza.presence.Presence): Received - presence stanza. - - """ - nick = str(presence['muc']['nick']) - jid = sleekxmpp.jid.JID(presence['muc']['jid']) - - if nick == self.nick: - return - - if jid.resource not in ['0ad', 'CC']: - return - - self._send_game_list(jid) - - logging.debug("Client '%s' connected with a nick '%s'.", jid, nick) - - def _muc_offline(self, presence): - """Remove leaving players from the list of players. - - Also remove the potential game this player was hosting, so we - don't end up with stale games. - - Arguments: - presence (sleekxmpp.stanza.presence.Presence): Received - presence stanza. - - """ - nick = str(presence['muc']['nick']) - jid = sleekxmpp.jid.JID(presence['muc']['jid']) - - if nick == self.nick: - return - - if self.games.remove_game(jid): - self._send_game_list() - - logging.debug("Client '%s' with nick '%s' disconnected", jid, nick) - - def _muc_message(self, msg): - """Process messages in the MUC room. - - Respond to messages highlighting the bots name with an - informative message. - - Arguments: - msg (sleekxmpp.stanza.message.Message): Received MUC - message - """ - if msg['mucnick'] != self.nick and self.nick.lower() in msg['body'].lower(): - self.send_message(mto=msg['from'].bare, - mbody="I am just a bot and I'm responsible to ensure that your're" - "able to see the list of games in here. Aside from that I'm" - "just chilling.", - mtype='groupchat') - - def _iq_game_list_handler(self, iq): - """Handle game state change requests. - - Arguments: - iq (sleekxmpp.stanza.iq.IQ): Received IQ stanza - - """ - if iq['from'].resource != '0ad': - return - - success = False - - command = iq['gamelist']['command'] - if command == 'register': - success = self.games.add_game(iq['from'], iq['gamelist']['game']) - elif command == 'unregister': - success = self.games.remove_game(iq['from']) - elif command == 'changestate': - success = self.games.change_game_state(iq['from'], iq['gamelist']['game']) - else: - logging.info('Received unknown game command: "%s"', command) - - iq.reply(clear=not success) - if not success: iq['error']['condition'] = "undefined-condition" - iq.send() - - if success: - try: - self._send_game_list() - except Exception: - logging.exception('Failed to send game list after "%s" command', command) - - def _send_game_list(self, to=None): - """Send a massive stanza with the whole game list. - - If no target is passed the gamelist is broadcasted to all - clients. - - Arguments: - to (sleekxmpp.jid.JID): Player to send the game list to. - If None, the game list will be broadcasted - """ - games = self.games.get_all_games() - - stanza = GameListXmppPlugin() - for jid in games: - stanza.add_game(games[jid]) - - if not to: - for nick in self.plugin['xep_0045'].getRoster(self.room): - if nick == self.nick: - continue - jid_str = self.plugin['xep_0045'].getJidProperty(self.room, nick, 'jid') - jid = sleekxmpp.jid.JID(jid_str) - iq = self.make_iq_result(ito=jid) - iq.set_payload(stanza) - try: - iq.send(block=False) - except Exception: - logging.exception("Failed to send game list to %s", jid) - else: - iq = self.make_iq_result(ito=to) - iq.set_payload(stanza) - try: - iq.send(block=False) - except Exception: - logging.exception("Failed to send game list to %s", to) - - -def parse_args(args): - """Parse command line arguments. - - Arguments: - args (dict): Raw command line arguments given to the script - - Returns: - Parsed command line arguments - - """ - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description="XpartaMuPP - XMPP Multiplayer Game Manager") - - log_settings = parser.add_mutually_exclusive_group() - log_settings.add_argument('-q', '--quiet', help="only log errors", action='store_const', - dest='log_level', const=logging.ERROR) - log_settings.add_argument('-d', '--debug', help="log debug messages", action='store_const', - dest='log_level', const=logging.DEBUG) - log_settings.add_argument('-v', '--verbose', help="log more informative messages", - action='store_const', dest='log_level', const=logging.INFO) - log_settings.set_defaults(log_level=logging.WARNING) - - parser.add_argument('-m', '--domain', help="XMPP server to connect to", - default='lobby.wildfiregames.com') - parser.add_argument('-l', '--login', help="username for login", default='xpartamupp') - parser.add_argument('-p', '--password', help="password for login", default='XXXXXX') - parser.add_argument('-n', '--nickname', help="nickname shown to players", default='WFGBot') - parser.add_argument('-r', '--room', help="XMPP MUC room to join", default='arena') - parser.add_argument('-s', '--server', help='address of the ejabberd server', - action='store', dest='xserver', default=None) - parser.add_argument('-t', '--disable-tls', help='Pass this argument to connect without TLS encryption', - action='store_true', dest='xdisabletls', default=False) - - return parser.parse_args(args) - - -def main(): - """Entry point a console script.""" - args = parse_args(sys.argv[1:]) - - logging.basicConfig(level=args.log_level, - format='%(asctime)s %(levelname)-8s %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') - - xmpp = XpartaMuPP(sleekxmpp.jid.JID('%s@%s/%s' % (args.login, args.domain, 'CC')), - args.password, args.room + '@conference.' + args.domain, args.nickname) - xmpp.register_plugin('xep_0030') # Service Discovery - xmpp.register_plugin('xep_0004') # Data Forms - xmpp.register_plugin('xep_0045') # Multi-User Chat - xmpp.register_plugin('xep_0060') # Publish-Subscribe - xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping - - if xmpp.connect((args.xserver, 5222) if args.xserver else None, True, not args.xdisabletls): - xmpp.process() - else: - logging.error("Unable to connect") - - -if __name__ == '__main__': - main() Index: ps/trunk/source/tools/lobbybots/xpartamupp/stanzas.py =================================================================== --- ps/trunk/source/tools/lobbybots/xpartamupp/stanzas.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/xpartamupp/stanzas.py (nonexistent) @@ -1,159 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""0ad-specific XMPP-stanzas.""" - -from sleekxmpp.xmlstream import ElementBase, ET - - -class BoardListXmppPlugin(ElementBase): - """Class for custom boardlist and ratinglist stanza extension.""" - - name = 'query' - namespace = 'jabber:iq:boardlist' - interfaces = {'board', 'command'} - sub_interfaces = interfaces - plugin_attrib = 'boardlist' - - def add_command(self, command): - """Add a command to the extension. - - Arguments: - command (str): Command to add - """ - self.xml.append(ET.fromstring('%s' % command)) - - def add_item(self, name, rating): - """Add an item to the extension. - - Arguments: - name (str): Name of the player to add - rating (int): Rating of the player to add - """ - self.xml.append(ET.Element('board', {'name': name, 'rating': str(rating)})) - - -class GameListXmppPlugin(ElementBase): - """Class for custom gamelist stanza extension.""" - - name = 'query' - namespace = 'jabber:iq:gamelist' - interfaces = {'game', 'command'} - sub_interfaces = interfaces - plugin_attrib = 'gamelist' - - def add_game(self, data): - """Add a game to the extension. - - Arguments: - data (dict): game data to add - """ - try: del data['ip'] # Don't send the IP address with the gamelist. - except: pass - - self.xml.append(ET.Element('game', data)) - - def get_game(self): - """Get game from stanza. - - Required to parse incoming stanzas with this extension. - - Returns: - dict with game data - - """ - game = self.xml.find('{%s}game' % self.namespace) - data = {} - - if game is not None: - for key, item in game.items(): - data[key] = item - return data - - -class GameReportXmppPlugin(ElementBase): - """Class for custom gamereport stanza extension.""" - - name = 'report' - namespace = 'jabber:iq:gamereport' - plugin_attrib = 'gamereport' - interfaces = 'game' - sub_interfaces = interfaces - - def add_game(self, game_report): - """Add a game to the extension. - - Arguments: - game_report (dict): a report about a game - - """ - self.xml.append(ET.fromstring(str(game_report)).find('{%s}game' % self.namespace)) - - def get_game(self): - """Get game from stanza. - - Required to parse incoming stanzas with this extension. - - Returns: - dict with game information - - """ - game = self.xml.find('{%s}game' % self.namespace) - data = {} - - if game is not None: - for key, item in game.items(): - data[key] = item - return data - - -class ProfileXmppPlugin(ElementBase): - """Class for custom profile.""" - - name = 'query' - namespace = 'jabber:iq:profile' - interfaces = {'profile', 'command'} - sub_interfaces = interfaces - plugin_attrib = 'profile' - - def add_command(self, player_nick): - """Add a command to the extension. - - Arguments: - player_nick (str): the nick of the player the profile is about - - """ - self.xml.append(ET.fromstring('%s' % player_nick)) - - def add_item(self, player, rating, highest_rating=0, # pylint: disable=too-many-arguments - rank=0, total_games_played=0, wins=0, losses=0): - """Add an item to the extension. - - Arguments: - player (str): Name of the player - rating (int): Current rating of the player - highest_rating (int): Highest rating the player had - rank (int): Rank of the player - total_games_played (int): Total number of games the player - played - wins (int): Number of won games the player had - losses (int): Number of lost games the player had - """ - item_xml = ET.Element('profile', {'player': player, 'rating': str(rating), - 'highestRating': str(highest_rating), 'rank': str(rank), - 'totalGamesPlayed': str(total_games_played), - 'wins': str(wins), 'losses': str(losses)}) - self.xml.append(item_xml) Index: ps/trunk/source/tools/lobbybots/requirements.txt =================================================================== --- ps/trunk/source/tools/lobbybots/requirements.txt (revision 27093) +++ ps/trunk/source/tools/lobbybots/requirements.txt (nonexistent) @@ -1,3 +0,0 @@ -dnspython -sleekxmpp -sqlalchemy Index: ps/trunk/source/tools/lobbybots/tests/test_echelon.py =================================================================== --- ps/trunk/source/tools/lobbybots/tests/test_echelon.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/tests/test_echelon.py (nonexistent) @@ -1,176 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -# pylint: disable=no-self-use - -"""Tests for EcheLOn.""" - -import sys - -from argparse import Namespace -from unittest import TestCase -from unittest.mock import Mock, call, patch - -from parameterized import parameterized -from sleekxmpp.jid import JID -from sqlalchemy import create_engine - -from xpartamupp.echelon import main, parse_args, Leaderboard -from xpartamupp.lobby_ranking import Base - - -class TestLeaderboard(TestCase): - """Test Leaderboard functionality.""" - - def setUp(self): - """Set up a leaderboard instance.""" - db_url = 'sqlite://' - engine = create_engine(db_url) - Base.metadata.create_all(engine) - with patch('xpartamupp.echelon.create_engine') as create_engine_mock: - create_engine_mock.return_value = engine - self.leaderboard = Leaderboard(db_url) - - def test_create_player(self): - """Test creating a new player.""" - player = self.leaderboard.get_or_create_player(JID('john@localhost')) - self.assertEqual(player.id, 1) - self.assertEqual(player.jid, 'john@localhost') - self.assertEqual(player.rating, -1) - self.assertEqual(player.highest_rating, None) - self.assertEqual(player.games, []) - self.assertEqual(player.games_info, []) - self.assertEqual(player.games_won, []) - - def test_get_profile_no_player(self): - """Test profile retrieval fro not existing player.""" - profile = self.leaderboard.get_profile(JID('john@localhost')) - self.assertEqual(profile, dict()) - - def test_get_profile_player_without_games(self): - """Test profile retrieval for existing player.""" - self.leaderboard.get_or_create_player(JID('john@localhost')) - profile = self.leaderboard.get_profile(JID('john@localhost')) - self.assertDictEqual(profile, {'highestRating': None, 'losses': 0, 'totalGamesPlayed': 0, - 'wins': 0}) - - -class TestReportManager(TestCase): - """Test ReportManager functionality.""" - - pass - - -class TestArgumentParsing(TestCase): - """Test handling of parsing command line parameters.""" - - @parameterized.expand([ - ([], Namespace(domain='lobby.wildfiregames.com', login='EcheLOn', log_level=30, xserver=None, xdisabletls=False, - nickname='RatingsBot', password='XXXXXX', room='arena', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['--debug'], - Namespace(domain='lobby.wildfiregames.com', login='EcheLOn', log_level=10, xserver=None,xdisabletls=False, - nickname='RatingsBot', password='XXXXXX', room='arena', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['--quiet'], - Namespace(domain='lobby.wildfiregames.com', login='EcheLOn', log_level=40, xserver=None,xdisabletls=False, - nickname='RatingsBot', password='XXXXXX', room='arena', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['--verbose'], - Namespace(domain='lobby.wildfiregames.com', login='EcheLOn', log_level=20, xserver=None, xdisabletls=False, - nickname='RatingsBot', password='XXXXXX', room='arena', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['-m', 'lobby.domain.tld'], - Namespace(domain='lobby.domain.tld', login='EcheLOn', log_level=30, nickname='RatingsBot', xserver=None, xdisabletls=False, - password='XXXXXX', room='arena', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['--domain=lobby.domain.tld'], - Namespace(domain='lobby.domain.tld', login='EcheLOn', log_level=30, nickname='RatingsBot', xserver=None, xdisabletls=False, - password='XXXXXX', room='arena', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['-m' 'lobby.domain.tld', '-l', 'bot', '-p', '123456', '-n', 'Bot', '-r', 'arena123', - '-v'], - Namespace(domain='lobby.domain.tld', login='bot', log_level=20, nickname='Bot', xserver=None, xdisabletls=False, - password='123456', room='arena123', - database_url='sqlite:///lobby_rankings.sqlite3')), - (['--domain=lobby.domain.tld', '--login=bot', '--password=123456', '--nickname=Bot', - '--room=arena123', '--database-url=sqlite:////tmp/db.sqlite3', '--verbose'], - Namespace(domain='lobby.domain.tld', login='bot', log_level=20, nickname='Bot', xserver=None, xdisabletls=False, - password='123456', room='arena123', - database_url='sqlite:////tmp/db.sqlite3')), - ]) - def test_valid(self, cmd_args, expected_args): - """Test valid parameter combinations.""" - self.assertEqual(parse_args(cmd_args), expected_args) - - @parameterized.expand([ - (['-f'],), - (['--foo'],), - (['--debug', '--quiet'],), - (['--quiet', '--verbose'],), - (['--debug', '--verbose'],), - (['--debug', '--quiet', '--verbose'],), - ]) - def test_invalid(self, cmd_args): - """Test invalid parameter combinations.""" - with self.assertRaises(SystemExit): - parse_args(cmd_args) - - -class TestMain(TestCase): - """Test main method.""" - - def test_success(self): - """Test successful execution.""" - with patch('xpartamupp.echelon.parse_args') as args_mock, \ - patch('xpartamupp.echelon.Leaderboard') as leaderboard_mock, \ - patch('xpartamupp.echelon.EcheLOn') as xmpp_mock: - args_mock.return_value = Mock(log_level=30, login='EcheLOn', - domain='lobby.wildfiregames.com', password='XXXXXX', - room='arena', nickname='RatingsBot', - database_url='sqlite:///lobby_rankings.sqlite3', - xserver=None, xdisabletls=False) - main() - args_mock.assert_called_once_with(sys.argv[1:]) - leaderboard_mock.assert_called_once_with('sqlite:///lobby_rankings.sqlite3') - xmpp_mock().register_plugin.assert_has_calls([call('xep_0004'), call('xep_0030'), - call('xep_0045'), call('xep_0060'), - call('xep_0199', {'keepalive': True})], - any_order=True) - xmpp_mock().connect.assert_called_once_with(None, True, True) - xmpp_mock().process.assert_called_once_with() - - def test_failing_connect(self): - """Test failing connect to XMPP server.""" - with patch('xpartamupp.echelon.parse_args') as args_mock, \ - patch('xpartamupp.echelon.Leaderboard') as leaderboard_mock, \ - patch('xpartamupp.echelon.EcheLOn') as xmpp_mock: - args_mock.return_value = Mock(log_level=30, login='EcheLOn', - domain='lobby.wildfiregames.com', password='XXXXXX', - room='arena', nickname='RatingsBot', - database_url='sqlite:///lobby_rankings.sqlite3', - xserver=None, xdisabletls=False) - - xmpp_mock().connect.return_value = False - main() - args_mock.assert_called_once_with(sys.argv[1:]) - leaderboard_mock.assert_called_once_with('sqlite:///lobby_rankings.sqlite3') - xmpp_mock().register_plugin.assert_has_calls([call('xep_0004'), call('xep_0030'), - call('xep_0045'), call('xep_0060'), - call('xep_0199', {'keepalive': True})], - any_order=True) - xmpp_mock().connect.assert_called_once_with(None, True, True) - xmpp_mock().process.assert_not_called() Index: ps/trunk/source/tools/lobbybots/tests/test_elo.py =================================================================== --- ps/trunk/source/tools/lobbybots/tests/test_elo.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/tests/test_elo.py (nonexistent) @@ -1,150 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""Tests for the ELO-implementation.""" - -from unittest import TestCase - -from hypothesis import assume, example, given -from hypothesis import strategies as st -from parameterized import parameterized - -from xpartamupp.elo import (get_rating_adjustment, ANTI_INFLATION, ELO_K_FACTOR_CONSTANT_RATING, - ELO_SURE_WIN_DIFFERENCE, VOLATILITY_CONSTANT) - - -class TestELO(TestCase): - """Test behavior of ELO calculation.""" - - @parameterized.expand([ - ([1000, 1000, 0, 0, 1], 82), - ([1000, 1000, 0, 0, -1], -83), - ([1000, 1000, 0, 0, 0], 0), - ([1200, 1200, 0, 0, 1], 78), - ([1200, 1200, 0, 0, -1], -78), - ([1200, 1200, 0, 0, 0], 0), - ([1200, 1200, 1, 0, 1], 65), - ([1200, 1200, 1, 0, 0], 0), - ([1200, 1200, 1, 0, -1], -65), - ([1200, 1200, 100, 0, 1], 16), - ([1200, 1200, 100, 0, 0], 0), - ([1200, 1200, 100, 0, -1], -16), - ([1200, 1200, 1000, 0, 1], 16), - ([1200, 1200, 1000, 0, 0], 0), - ([1200, 1200, 1000, 0, -1], -16), - ([1200, 1200, 0, 1, 1], 78), - ([1200, 1200, 0, 1, 0], 0), - ([1200, 1200, 0, 1, -1], -78), - ([1200, 1200, 0, 100, 1], 78), - ([1200, 1200, 0, 100, 0], 0), - ([1200, 1200, 0, 100, -1], -78), - ([1200, 1200, 0, 1000, 1], 78), - ([1200, 1200, 0, 1000, 0], 0), - ([1200, 1200, 0, 1000, -1], -78), - ([1400, 1000, 0, 0, 1], 24), - ([1400, 1000, 0, 0, 0], -49), - ([1400, 1000, 0, 0, -1], -122), - ([1000, 1400, 0, 0, 1], 137), - ([1000, 1400, 0, 0, 0], 55), - ([1000, 1400, 0, 0, -1], -28), - ([2200, 2300, 0, 0, 1], 70), - ([2200, 2300, 0, 0, 0], 10), - ([2200, 2300, 0, 0, -1], -50), - ]) - def test_valid_adjustments(self, args, expected_adjustment): - """Test correctness of valid rating adjustments.""" - self.assertEqual(get_rating_adjustment(*args), expected_adjustment) - - @given(st.integers(min_value=ELO_K_FACTOR_CONSTANT_RATING), - st.integers(min_value=-2099, max_value=ELO_SURE_WIN_DIFFERENCE - 1), st.integers(), - st.integers(), - st.integers(min_value=-1, max_value=1)) - @example(ELO_K_FACTOR_CONSTANT_RATING + 300, 0, 0, 0, 1) - def test_constant_rating(self, rating_player1, difference_player2, played_games_player1, - played_games_player2, result): - """Test that points gained are constant above a threshold.""" - volatility = 50.0 * (min(max(0, played_games_player1), VOLATILITY_CONSTANT) / - VOLATILITY_CONSTANT + 0.25) / 1.25 - rating_adjustment = (difference_player2 + result * ELO_SURE_WIN_DIFFERENCE) / volatility \ - - ANTI_INFLATION - if result == 1: - expected_adjustment = max(0.0, rating_adjustment) - elif result == -1: - expected_adjustment = min(0.0, rating_adjustment) - else: - expected_adjustment = rating_adjustment - - self.assertEqual(get_rating_adjustment(rating_player1, rating_player1 + difference_player2, - played_games_player1, played_games_player2, result), - round(expected_adjustment)) - - @given(st.data()) - def test_sure_win(self, data): - """Test behavior if winning player 1 has >600 points more. - - In this case the winning player shouldn't gain points, as it - was a "sure win" and the loosing player shouldn't loose - points. - """ - rating_player1 = data.draw(st.integers(min_value=-1599)) - difference_player2 = data.draw(st.integers(min_value=ELO_SURE_WIN_DIFFERENCE)) - assume(rating_player1 - difference_player2 > -2200) - played_games_player1 = data.draw(st.integers()) - played_games_player2 = data.draw(st.integers()) - - self.assertEqual(get_rating_adjustment(rating_player1, - rating_player1 - difference_player2, - played_games_player1, played_games_player2, 1), - 0) - self.assertEqual(get_rating_adjustment(rating_player1 - difference_player2, - rating_player1, played_games_player2, - played_games_player1, -1), 0) - - @given(st.integers(min_value=-2199), st.integers(min_value=ELO_SURE_WIN_DIFFERENCE), - st.integers(), - st.integers()) - @example(1000, ELO_SURE_WIN_DIFFERENCE, 0, 0) - def test_sure_loss(self, rating_player1, difference_player2, played_games_player1, - played_games_player2): - """Test behavior if winning player 2 has >600 points more. - - In this case the winning player shouldn't gain points, as it - was a "sure win" and the loosing player shouldn't loose - points. - """ - self.assertEqual(get_rating_adjustment(rating_player1, - rating_player1 - difference_player2 * -1, - played_games_player1, played_games_player2, -1), - 0) - self.assertEqual(get_rating_adjustment(rating_player1 - difference_player2 * -1, - rating_player1, played_games_player2, - played_games_player1, 1), 0) - - @given(st.integers(max_value=-2200), st.integers(), - st.integers(), - st.integers(), - st.one_of(st.just(1), st.just(-1))) - @example(-2200, 2000, 0, 0, 1) - @example(2000, -2200, 0, 0, 1) - def test_minus_2200_bug_workaround(self, rating_player1, rating_player2, - played_games_player1, played_games_player2, result): - """Test workaround for -2200 bug.""" - with self.assertRaises(ValueError): - get_rating_adjustment(rating_player1, rating_player2, played_games_player1, - played_games_player2, result) - with self.assertRaises(ValueError): - get_rating_adjustment(rating_player2, rating_player1, played_games_player1, - played_games_player2, result) Index: ps/trunk/source/tools/lobbybots/tests/__init__.py =================================================================== Index: ps/trunk/source/tools/lobbybots/tests/test_utils.py =================================================================== --- ps/trunk/source/tools/lobbybots/tests/test_utils.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/tests/test_utils.py (nonexistent) @@ -1,45 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -"""Tests for utility functions.""" - -from unittest import TestCase - -from hypothesis import given -from hypothesis import strategies as st - -from xpartamupp.utils import LimitedSizeDict - - -class TestLimitedSizeDict(TestCase): - """Test limited size dict.""" - - @given(st.integers(min_value=2, max_value=2**10)) - def test_max_items(self, size_limit): - """Test max items of dicts. - - Test that the dict doesn't grow indefinitely and that the - oldest entries are removed first. - """ - test_dict = LimitedSizeDict(size_limit=size_limit) - for i in range(size_limit): - test_dict[i] = i - self.assertEqual(size_limit, len(test_dict)) - test_dict[size_limit + 1] = size_limit + 1 - self.assertEqual(size_limit, len(test_dict)) - self.assertFalse(0 in test_dict.values()) - self.assertTrue(1 in test_dict.values()) - self.assertTrue(size_limit + 1 in test_dict.values()) Index: ps/trunk/source/tools/lobbybots/tests/test_lobby_ranking.py =================================================================== --- ps/trunk/source/tools/lobbybots/tests/test_lobby_ranking.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/tests/test_lobby_ranking.py (nonexistent) @@ -1,70 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -# pylint: disable=no-self-use - -"""Tests for the database schema.""" - -import sys - -from argparse import Namespace -from unittest import TestCase -from unittest.mock import Mock, patch - -from parameterized import parameterized - -from xpartamupp.lobby_ranking import main, parse_args - - -class TestArgumentParsing(TestCase): - """Test handling of parsing command line parameters.""" - - @parameterized.expand([ - (['create'], Namespace(action='create', database_url='sqlite:///lobby_rankings.sqlite3')), - (['--database-url', 'sqlite:////tmp/db.sqlite3', 'create'], - Namespace(action='create', database_url='sqlite:////tmp/db.sqlite3')), - ]) - def test_valid(self, cmd_args, expected_args): - """Test valid parameter combinations.""" - self.assertEqual(parse_args(cmd_args), expected_args) - - @parameterized.expand([ - ([],), - (['--database-url=sqlite:////tmp/db.sqlite3'],), - ]) - def test_missing_action(self, cmd_args): - """Test invalid parameter combinations.""" - with self.assertRaises(SystemExit): - parse_args(cmd_args) - - -class TestMain(TestCase): - """Test main method.""" - - def test_success(self): - """Test successful execution.""" - with patch('xpartamupp.lobby_ranking.parse_args') as args_mock, \ - patch('xpartamupp.lobby_ranking.create_engine') as create_engine_mock, \ - patch('xpartamupp.lobby_ranking.Base') as declarative_base_mock: - args_mock.return_value = Mock(action='create', - database_url='sqlite:///lobby_rankings.sqlite3') - engine_mock = Mock() - create_engine_mock.return_value = engine_mock - main() - args_mock.assert_called_once_with(sys.argv[1:]) - create_engine_mock.assert_called_once_with( - 'sqlite:///lobby_rankings.sqlite3') - declarative_base_mock.metadata.create_all.assert_any_call(engine_mock) Index: ps/trunk/source/tools/lobbybots/tests/test_xpartamupp.py =================================================================== --- ps/trunk/source/tools/lobbybots/tests/test_xpartamupp.py (revision 27093) +++ ps/trunk/source/tools/lobbybots/tests/test_xpartamupp.py (nonexistent) @@ -1,179 +0,0 @@ -# Copyright (C) 2021 Wildfire Games. -# This file is part of 0 A.D. -# -# 0 A.D. is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# 0 A.D. is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with 0 A.D. If not, see . - -# pylint: disable=no-self-use - -"""Tests for XPartaMuPP.""" - -import sys - -from argparse import Namespace -from unittest import TestCase -from unittest.mock import Mock, call, patch - -from parameterized import parameterized -from sleekxmpp.jid import JID - -from xpartamupp.xpartamupp import Games, main, parse_args - - -class TestGames(TestCase): - """Test Games class responsible for holding active games.""" - - def test_add(self): - """Test successfully adding a game.""" - games = Games() - jid = JID(jid='player1@domain.tld') - # TODO: Check how the real format of data looks like - game_data = {'players': ['player1', 'player2'], 'nbp': 'foo', 'state': 'init'} - self.assertTrue(games.add_game(jid, game_data)) - all_games = games.get_all_games() - game_data.update({'players-init': game_data['players'], 'nbp-init': game_data['nbp'], - 'state': game_data['state']}) - self.assertDictEqual(all_games, {jid: game_data}) - - @parameterized.expand([ - ('', {}), - ('player1@domain.tld', {}), - ('player1@domain.tld', None), - ('player1@domain.tld', ''), - ]) - def test_add_invalid(self, jid, game_data): - """Test trying to add games with invalid data.""" - games = Games() - self.assertFalse(games.add_game(jid, game_data)) - - def test_remove(self): - """Test removal of games.""" - games = Games() - jid1 = JID(jid='player1@domain.tld') - jid2 = JID(jid='player3@domain.tld') - # TODO: Check how the real format of data looks like - game_data1 = {'players': ['player1', 'player2'], 'nbp': 'foo', 'state': 'init'} - games.add_game(jid1, game_data1) - game_data2 = {'players': ['player3', 'player4'], 'nbp': 'bar', 'state': 'init'} - games.add_game(jid2, game_data2) - game_data1.update({'players-init': game_data1['players'], 'nbp-init': game_data1['nbp'], - 'state': game_data1['state']}) - game_data2.update({'players-init': game_data2['players'], 'nbp-init': game_data2['nbp'], - 'state': game_data2['state']}) - self.assertDictEqual(games.get_all_games(), {jid1: game_data1, jid2: game_data2}) - games.remove_game(jid1) - self.assertDictEqual(games.get_all_games(), {jid2: game_data2}) - games.remove_game(jid2) - self.assertDictEqual(games.get_all_games(), dict()) - - def test_remove_unknown(self): - """Test removal of a game, which doesn't exist.""" - games = Games() - jid = JID(jid='player1@domain.tld') - # TODO: Check how the real format of data looks like - game_data = {'players': ['player1', 'player2'], 'nbp': 'foo', 'state': 'init'} - games.add_game(jid, game_data) - self.assertFalse(games.remove_game(JID('foo@bar.tld'))) - - def test_change_state(self): - """Test state changes of a games.""" - pass - # slightly unknown how to do that properly, as some data structures aren't known - - -class TestArgumentParsing(TestCase): - """Test handling of parsing command line parameters.""" - - @parameterized.expand([ - ([], Namespace(domain='lobby.wildfiregames.com', login='xpartamupp', log_level=30, xserver=None, xdisabletls=False, - nickname='WFGBot', password='XXXXXX', room='arena')), - (['--debug'], - Namespace(domain='lobby.wildfiregames.com', login='xpartamupp', log_level=10, xserver=None, xdisabletls=False, - nickname='WFGBot', password='XXXXXX', room='arena')), - (['--quiet'], - Namespace(domain='lobby.wildfiregames.com', login='xpartamupp', log_level=40, xserver=None, xdisabletls=False, - nickname='WFGBot', password='XXXXXX', room='arena')), - (['--verbose'], - Namespace(domain='lobby.wildfiregames.com', login='xpartamupp', log_level=20, xserver=None, xdisabletls=False, - nickname='WFGBot', password='XXXXXX', room='arena')), - (['-m', 'lobby.domain.tld'], - Namespace(domain='lobby.domain.tld', login='xpartamupp', log_level=30, nickname='WFGBot', xserver=None, xdisabletls=False, - password='XXXXXX', room='arena')), - (['--domain=lobby.domain.tld'], - Namespace(domain='lobby.domain.tld', login='xpartamupp', log_level=30, nickname='WFGBot', xserver=None, xdisabletls=False, - password='XXXXXX', room='arena')), - (['-m' 'lobby.domain.tld', '-l', 'bot', '-p', '123456', '-n', 'Bot', '-r', 'arena123', - '-v'], - Namespace(domain='lobby.domain.tld', login='bot', log_level=20, xserver=None, xdisabletls=False, - nickname='Bot', password='123456', room='arena123')), - (['--domain=lobby.domain.tld', '--login=bot', '--password=123456', '--nickname=Bot', - '--room=arena123', '--verbose'], - Namespace(domain='lobby.domain.tld', login='bot', log_level=20, xserver=None, xdisabletls=False, - nickname='Bot', password='123456', room='arena123')), - ]) - def test_valid(self, cmd_args, expected_args): - """Test valid parameter combinations.""" - self.assertEqual(parse_args(cmd_args), expected_args) - - @parameterized.expand([ - (['-f'],), - (['--foo'],), - (['--debug', '--quiet'],), - (['--quiet', '--verbose'],), - (['--debug', '--verbose'],), - (['--debug', '--quiet', '--verbose'],), - ]) - def test_invalid(self, cmd_args): - """Test invalid parameter combinations.""" - with self.assertRaises(SystemExit): - parse_args(cmd_args) - - -class TestMain(TestCase): - """Test main method.""" - - def test_success(self): - """Test successful execution.""" - with patch('xpartamupp.xpartamupp.parse_args') as args_mock, \ - patch('xpartamupp.xpartamupp.XpartaMuPP') as xmpp_mock: - args_mock.return_value = Mock(log_level=30, login='xpartamupp', - domain='lobby.wildfiregames.com', password='XXXXXX', - room='arena', nickname='WFGBot', - xserver=None, xdisabletls=False) - main() - args_mock.assert_called_once_with(sys.argv[1:]) - xmpp_mock().register_plugin.assert_has_calls([call('xep_0004'), call('xep_0030'), - call('xep_0045'), call('xep_0060'), - call('xep_0199', {'keepalive': True})], - any_order=True) - xmpp_mock().connect.assert_called_once_with(None, True, True) - xmpp_mock().process.assert_called_once_with() - - def test_failing_connect(self): - """Test failing connect to XMPP server.""" - with patch('xpartamupp.xpartamupp.parse_args') as args_mock, \ - patch('xpartamupp.xpartamupp.XpartaMuPP') as xmpp_mock: - args_mock.return_value = Mock(log_level=30, login='xpartamupp', - domain='lobby.wildfiregames.com', password='XXXXXX', - room='arena', nickname='WFGBot', - xserver=None, xdisabletls=False) - - xmpp_mock().connect.return_value = False - main() - args_mock.assert_called_once_with(sys.argv[1:]) - xmpp_mock().register_plugin.assert_has_calls([call('xep_0004'), call('xep_0030'), - call('xep_0045'), call('xep_0060'), - call('xep_0199', {'keepalive': True})], - any_order=True) - xmpp_mock().connect.assert_called_once_with(None, True, True) - xmpp_mock().process.assert_not_called()