-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
-Use SQLAlchemy for database management -Separate skills's data, strings, and code
- Loading branch information
Markus Meskanen
committed
Dec 7, 2021
1 parent
fc3e130
commit 7f76ebb
Showing
36 changed files
with
864 additions
and
777 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,10 @@ | |
__pycache__/ | ||
*.pyc | ||
|
||
// Visual Studio | ||
// IDEs | ||
.vs/ | ||
*.pyproj | ||
*.sln | ||
|
||
// PyCharm | ||
.idea/ | ||
|
||
// Database | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,43 @@ | ||
# RPG:SP | ||
*The* RPG plugin for Source.Python | ||
RPG plugin for [Source.Python](https://sourcepython.com/). | ||
|
||
### About | ||
RPG:SP is a server-sided role-playing mod built with Source.Python. | ||
It has been tested only on CS:GO and CS:S, but should work fine on most Source engine games. | ||
## About | ||
RPG:SP is a server-sided role-playing mod for CS:GO, built with Source.Python. | ||
|
||
RPG:SP extends the game's players with an experience system and a set of unique skills — additional powers for players to spice up the game with. | ||
The objective is to gain experience points (XP for short) by attacking the opposing team's players. | ||
Once a player gains enough XP to fill his XP quota, the player will level up and gain credits, which can be spent to upgrade the player's skills. | ||
Each of these skills provides an unique effect for the player, allowing him to gain an advantage over a normal player. | ||
RPG:SP extends the game's player objects with an experience system and | ||
a set of *skills* — additional powers for players to spice up the game with. | ||
|
||
### Skills | ||
Here's a list of the current skills and a short description for each: | ||
The objective is to gain experience points (*XP* for short) by attacking the opposing team's players. | ||
Reaching enough XP will level your player up, granting you *credits* that can be spent to upgrade your skills. | ||
|
||
## Skills | ||
Here's a list of the pre-implemented skills: | ||
|
||
- Health+ — Increase maximum health. | ||
- Regeneration — Regenerate lost health over time. | ||
- Long Jump — Travel further with jumps. | ||
- Vampirism — Steal health with attacks. | ||
- Blacksmith — Generate armor over time. | ||
- Impulse — Gain temporary speed boost when attacked. | ||
- Fire Grenade — Burn your enemies with grenades. | ||
- Stealth — Become partially invisible. | ||
- Ice Stab — Freeze the enemy with the stronger knife stab. | ||
|
||
### Installation | ||
Before installing RPG:SP onto your game server, you must first install the two dependencies: | ||
But you can always implement more skills yourself (or make an issue in GitHub)! | ||
You should start from the `addons/source-python/plugins/rpg/skills/README.md` file | ||
and see the pre-implemented skills for more examples. | ||
|
||
## Installation | ||
Before installing RPG:SP onto your game server, you must first install the following dependencies: | ||
|
||
- [Source.Python](http://sourcepython.com) to allow the Python programming language to be used with the Source engine. | ||
- [EasyPlayer](https://github.com/Mahi/EasyPlayer) to enable additional player effects. | ||
- [Source.Python](http://sourcepython.com) to run Python plugins on the server | ||
- [EasyPlayer](https://github.com/Mahi/EasyPlayer) to allow additional player effects for the skills | ||
- [`PyYAML`](https://pypi.org/project/PyYAML/) to parse skills from YAML files | ||
|
||
Once that's done, | ||
|
||
1. download RPG:SP's latest version from the [releases page](https://github.com/Mahi/RPG-SP/releases) | ||
2. locate the `addons` and `resource` folders inside of the downloaded `.zip` file | ||
3. extract the two folders into your game folder | ||
4. load the plugin with the `sp load rpg` command | ||
4. load the plugin with the `sp plugin load rpg` command | ||
|
||
It's highly recommended to put the `sp load rpg` command into your server's `autoexec.cfg` so that the plugin gets loaded automatically whenever the server is started. | ||
It's highly recommended to put the `sp plugin load rpg` command into your server's `autoexec.cfg` | ||
so that the plugin gets loaded automatically whenever the server is started. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# Python imports | ||
from collections import OrderedDict | ||
from inspect import getmembers | ||
from importlib import import_module | ||
from typing import Dict | ||
|
||
# Site-Package imports | ||
import yaml | ||
|
||
# Source.Python imports | ||
from path import Path | ||
from translations.strings import LangStrings | ||
|
||
# RPG:GO imports | ||
import rpg.skills | ||
from .skill import SkillType | ||
|
||
|
||
def build_skill_types(root: Path) -> Dict[str, SkillType]: | ||
"""Build skill type objects from a directory of skills. | ||
Attempt to order the skills by `order.txt` file's content. | ||
""" | ||
skill_types = OrderedDict() | ||
for path in root.dirs(): | ||
skill = build_skill(path) | ||
if skill is None: | ||
print(f"Unable to build skill for path '{path}'") | ||
else: | ||
skill_types[skill.key] = skill | ||
|
||
try: | ||
with open(root / 'order.txt') as order_file: | ||
order_data = order_file.readlines() | ||
except FileNotFoundError: | ||
return skill_types # Alphabetical order from filesystem | ||
|
||
ordered = OrderedDict() | ||
for key in order_data: | ||
key = key.strip() | ||
if key in skill_types: | ||
ordered[key] = skill_types.pop(key) | ||
else: | ||
print(f"Unable to find skill for key '{key}'") | ||
return ordered | ||
|
||
|
||
def build_skill(path: Path) -> SkillType: | ||
"""Build a skill from a directory. | ||
The directory must have `data.yml`, `events.py`, | ||
and `strings.ini` files in it. | ||
See `skills/README.md` for more information. | ||
""" | ||
key = str(path.name) # Remove "pathiness" | ||
with open(path / 'data.yml') as data_file: | ||
data = yaml.safe_load(data_file) | ||
if 'key' in data: | ||
key = data.pop('key') | ||
strings = LangStrings(path / 'strings') | ||
|
||
init_callback = None | ||
event_callbacks = {} | ||
events_module = import_module(f'rpg.skills.{path.name}.events', rpg.skills) | ||
for name, attr in getmembers(events_module): | ||
if name == 'init': | ||
init_callback = attr | ||
elif not name.startswith('_'): | ||
event_callbacks[name] = attr | ||
|
||
return SkillType( | ||
key, | ||
lang_strings=strings, | ||
init_callback=init_callback, | ||
event_callbacks=event_callbacks, | ||
**data, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from paths import PLUGIN_DATA_PATH | ||
|
||
|
||
REQUIRED_XP = { | ||
'base': 100, | ||
'per_level': 20, | ||
} | ||
|
||
|
||
XP_GAIN = { | ||
'on_kill': { | ||
'base': 80, | ||
'per_level_difference': 2, | ||
}, | ||
'on_damage': { | ||
'per_damage': 0.5, | ||
}, | ||
} | ||
|
||
|
||
DATABASE_URL = { | ||
'drivername': 'sqlite', | ||
'username': '', | ||
'password': '', | ||
'host': '', | ||
'port': None, | ||
'database': PLUGIN_DATA_PATH / 'rpg.db', | ||
'query': '', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,114 @@ | ||
import sqlite3 | ||
|
||
|
||
class Database: | ||
"""Wrapper class around sqlite3 for storing RPG players' data.""" | ||
|
||
def __init__(self, path=':memory:'): | ||
"""Connect and create ``players`` and ``skills`` tables. | ||
:param str path: | ||
Path to the database file or ``':memory:'`` for RAM | ||
""" | ||
self._connection = sqlite3.connect(path) | ||
self._connection.row_factory = sqlite3.Row | ||
self._connection.execute(''' | ||
CREATE TABLE IF NOT EXISTS players ( | ||
steamid TEXT PRIMARY KEY NOT NULL, | ||
level INTEGER NOT NULL, | ||
xp INTEGER NOT NULL, | ||
credits INTEGER NOT NULL | ||
)''') | ||
self._connection.execute(''' | ||
CREATE TABLE IF NOT EXISTS skills ( | ||
steamid TEXT NOT NULL, | ||
class_id TEXT NOT NULL, | ||
level INTEGER NOT NULL, | ||
FOREIGN KEY (steamid) REFERENCES players(steamid), | ||
PRIMARY KEY (steamid, class_id) | ||
)''') | ||
|
||
def close(self): | ||
"""Close the connection to the database.""" | ||
self._connection.close() | ||
|
||
def commit(self): | ||
"""Commit changes to the database.""" | ||
self._connection.commit() | ||
|
||
def save_player_data(self, steamid, level, xp, credits): | ||
"""Save player's data into the database.""" | ||
self._connection.execute( | ||
'INSERT OR REPLACE INTO players VALUES (?, ?, ?, ?)', | ||
(steamid, level, xp, credits)) | ||
|
||
def save_skill_data(self, steamid, class_id, level): | ||
"""Save skill's data into the database.""" | ||
self._connection.execute( | ||
'INSERT OR REPLACE INTO skills VALUES (?, ?, ?)', | ||
(steamid, class_id, level)) | ||
|
||
def load_player_data(self, steamid): | ||
"""Load player's data from the database. | ||
:returns tuple: | ||
Player's level, xp, and credits or ``(0, 0, 0)`` by default | ||
""" | ||
for row in self._connection.execute( | ||
'SELECT level, xp, credits FROM players WHERE steamid=?', | ||
(steamid,)): | ||
return row | ||
return 0, 0, 0 | ||
|
||
def load_skill_data(self, steamid, class_id): | ||
"""Load skill's data from the database. | ||
:returns tuple: | ||
Skill's level or ``(0,)`` by default | ||
""" | ||
for row in self._connection.execute( | ||
'SELECT level FROM skills WHERE steamid=? AND class_id=?', | ||
(steamid, class_id)): | ||
return row | ||
return 0, | ||
|
||
def __enter__(self): | ||
"""Enter method to allow usage with ``with`` statement. | ||
:returns Database: | ||
The database object itself | ||
""" | ||
return self | ||
|
||
def __exit__(self, exc_type, exc_value, exc_traceback): | ||
"""Commit changes and close the database safely.""" | ||
self.commit() | ||
self.close() | ||
# Site-Package imports | ||
from sqlalchemy import create_engine, Column, ForeignKey, Integer, MetaData, Table, Text | ||
from sqlalchemy.engine.url import URL | ||
from sqlalchemy.sql import select | ||
from sqlalchemy.sql.expression import bindparam | ||
|
||
# RPG:GO imports | ||
from . import config | ||
from .player import Player | ||
|
||
|
||
# Initialize table metadata | ||
metadata = MetaData() | ||
player_table = Table('player', metadata, | ||
Column('steamid', Text, primary_key=True), | ||
Column('level', Integer, nullable=False, default=0), | ||
Column('xp', Integer, nullable=False, default=0), | ||
Column('credits', Integer, nullable=False, default=0), | ||
) | ||
|
||
skill_table = Table('skill', metadata, | ||
Column('id', Integer, primary_key=True), | ||
Column('key', Text, nullable=False), | ||
Column('level', Integer, nullable=False, default=0), | ||
Column('steamid', Text, ForeignKey('player.steamid'), nullable=False), | ||
) | ||
|
||
# Create engine and missing tables | ||
engine = create_engine(URL(**config.DATABASE_URL)) | ||
metadata.create_all(bind=engine) | ||
|
||
|
||
def create_player(player: Player) -> None: | ||
"""Insert a new player and their skills into the database. | ||
Also sets the skill objects's `_db_id` to match their database ID. | ||
""" | ||
with engine.connect() as conn: | ||
|
||
conn.execute( | ||
player_table.insert().values( | ||
steamid=player.steamid, | ||
level=player.level, | ||
xp=player.xp, | ||
credits=player.credits, | ||
) | ||
) | ||
|
||
skills = list(player.skills) | ||
result = conn.execute( | ||
skill_table.insert().values([ | ||
{ | ||
'key': skill.key, | ||
'level': skill.level, | ||
'steamid': player.steamid, | ||
} | ||
for skill in skills | ||
]) | ||
) | ||
|
||
for id, skill in zip(result.inserted_primary_key, skills): | ||
skill._db_id = id | ||
|
||
|
||
def save_player(player: Player) -> None: | ||
"""Update a player's and their skills's data into the database.""" | ||
with engine.connect() as conn: | ||
|
||
conn.execute( | ||
player_table.update().where(player_table.c.steamid==player.steamid).values( | ||
level=player.level, | ||
xp=player.xp, | ||
credits=player.credits | ||
) | ||
) | ||
|
||
conn.execute( | ||
skill_table.update().where(skill_table.c.id==bindparam('db_id')).values( | ||
{ | ||
'level': skill.level, | ||
'db_id': skill._db_id | ||
} | ||
for skill in list(player.skills) | ||
) | ||
) | ||
|
||
|
||
def load_player(player: Player) -> bool: | ||
"""Fetch a player's and their skills's data from the database. | ||
Returns `False` if there was no match for the player's steamid. | ||
""" | ||
with engine.connect() as conn: | ||
|
||
result = conn.execute( | ||
select([player_table]).where(player_table.c.steamid==player.steamid) | ||
) | ||
player_data = result.first() | ||
if player_data is None: | ||
return False | ||
player._level = player_data.level | ||
player._xp = player_data.xp | ||
player.credits = player_data.credits | ||
|
||
result = conn.execute( | ||
select([skill_table]).where(skill_table.c.steamid==player.steamid) | ||
) | ||
for skill_data in result: | ||
skill = player.get_skill(skill_data.key) | ||
if skill is not None: | ||
skill.level = skill_data.level | ||
skill._db_id = skill_data.id | ||
|
||
return True |
Oops, something went wrong.