Skip to content

Commit

Permalink
Refactor v2.0.0
Browse files Browse the repository at this point in the history
-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
Show file tree
Hide file tree
Showing 36 changed files with 864 additions and 777 deletions.
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
__pycache__/
*.pyc

// Visual Studio
// IDEs
.vs/
*.pyproj
*.sln

// PyCharm
.idea/

// Database
Expand Down
41 changes: 23 additions & 18 deletions README.md
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.
77 changes: 77 additions & 0 deletions addons/source-python/plugins/rpg/builder.py
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,
)
29 changes: 29 additions & 0 deletions addons/source-python/plugins/rpg/config.py
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': '',
}
200 changes: 114 additions & 86 deletions addons/source-python/plugins/rpg/database.py
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
Loading

0 comments on commit 7f76ebb

Please sign in to comment.