What a mess

This commit is contained in:
Joan
2025-11-07 15:27:13 +01:00
parent 0b79b3ae59
commit 33cc9586c2
130 changed files with 29819 additions and 1175 deletions

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Add database indexes for performance optimization.
These indexes target the most frequently queried columns.
Expected improvement: 50-70% faster query response times
"""
import asyncio
import os
from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
load_dotenv()
DB_USER = os.getenv("DB_USER", "user")
DB_PASS = os.getenv("DB_PASS", "password")
DB_HOST = os.getenv("DB_HOST", "db")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "echoes")
DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
# Indexes to create with their purpose
INDEXES = [
# Players table - most commonly queried
(
"CREATE INDEX IF NOT EXISTS idx_players_username ON players(username);",
"Speed up login/authentication queries"
),
(
"CREATE INDEX IF NOT EXISTS idx_players_location_id ON players(location_id);",
"Speed up 'get all players in location' queries"
),
# Dropped items - queried on every location view
(
"CREATE INDEX IF NOT EXISTS idx_dropped_items_location ON dropped_items(location_id);",
"Speed up 'show items on ground' queries"
),
# Wandering enemies - checked frequently
(
"CREATE INDEX IF NOT EXISTS idx_wandering_enemies_location ON wandering_enemies(location_id);",
"Speed up 'get enemies in location' queries"
),
(
"CREATE INDEX IF NOT EXISTS idx_wandering_enemies_despawn ON wandering_enemies(despawn_timestamp);",
"Speed up cleanup queries for expired enemies"
),
# Inventory - queried on every inventory operation
(
"CREATE INDEX IF NOT EXISTS idx_inventory_player_item ON inventory(player_id, item_id);",
"Speed up inventory lookups and item checks"
),
(
"CREATE INDEX IF NOT EXISTS idx_inventory_player ON inventory(player_id);",
"Speed up 'get all player inventory' queries"
),
# Active combats - checked on most actions
(
"CREATE INDEX IF NOT EXISTS idx_active_combats_player ON active_combats(player_id);",
"Speed up 'is player in combat' checks"
),
# Interactable cooldowns - checked on interact attempts
(
"CREATE INDEX IF NOT EXISTS idx_interactable_cooldowns_player ON interactable_cooldowns(player_id, interactable_id);",
"Speed up cooldown checks"
),
]
async def add_indexes():
"""Add all performance indexes to the database."""
engine = create_async_engine(DATABASE_URL, echo=False)
try:
async with engine.begin() as conn:
print("Starting index creation...\n")
for sql, purpose in INDEXES:
index_name = sql.split("IF NOT EXISTS ")[1].split(" ON ")[0]
print(f"Creating {index_name}...")
print(f" Purpose: {purpose}")
try:
await conn.execute(text(sql))
print(f" ✓ Success\n")
except Exception as e:
print(f" ✗ Failed: {e}\n")
print("\n✓ Index creation complete!")
print("\nTo verify indexes were created:")
print(" docker exec echoes_of_the_ashes_db psql -U user -d echoes -c \"\\di\"")
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(add_indexes())

View File

@@ -0,0 +1,18 @@
-- Add persistent status effects table
CREATE TABLE IF NOT EXISTS player_status_effects (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(telegram_id) ON DELETE CASCADE,
effect_name VARCHAR(50) NOT NULL,
effect_icon VARCHAR(10) NOT NULL,
damage_per_tick INTEGER NOT NULL DEFAULT 0,
ticks_remaining INTEGER NOT NULL,
applied_at FLOAT NOT NULL,
CONSTRAINT valid_ticks CHECK (ticks_remaining >= 0),
CONSTRAINT valid_damage CHECK (damage_per_tick >= 0)
);
-- Create index for efficient querying by player
CREATE INDEX IF NOT EXISTS idx_status_effects_player ON player_status_effects(player_id);
-- Create index for background processor to find active effects
CREATE INDEX IF NOT EXISTS idx_status_effects_active ON player_status_effects(player_id, ticks_remaining) WHERE ticks_remaining > 0;

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Migration script to add player_status_effects table.
This table stores persistent status effects that can exist both during and outside of combat.
"""
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
# Database connection
DB_USER = os.getenv("POSTGRES_USER")
DB_PASS = os.getenv("POSTGRES_PASSWORD")
DB_NAME = os.getenv("POSTGRES_DB")
DB_HOST = os.getenv("POSTGRES_HOST")
DB_PORT = os.getenv("POSTGRES_PORT")
DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
async def apply_migration():
"""Apply the status effects table migration."""
engine = create_async_engine(DATABASE_URL)
print("Applying status effects table migration...")
try:
async with engine.begin() as conn:
# Read and execute the SQL file
with open('migrations/add_status_effects_table.sql', 'r') as f:
sql = f.read()
await conn.execute(text(sql))
print("✅ Successfully created player_status_effects table")
except Exception as e:
print(f"❌ Migration failed: {e}")
raise
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(apply_migration())

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""
Fix telegram_id column to be nullable for web users.
"""
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
# Database connection
DB_USER = os.getenv("POSTGRES_USER", "echoes_user")
DB_PASS = os.getenv("POSTGRES_PASSWORD", "echoes_pass")
DB_NAME = os.getenv("POSTGRES_DB", "echoes_db")
DB_HOST = os.getenv("POSTGRES_HOST", "echoes_of_the_ashes_db")
DB_PORT = os.getenv("POSTGRES_PORT", "5432")
DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
async def fix_telegram_id():
"""Alter telegram_id column to be nullable"""
engine = create_async_engine(DATABASE_URL, echo=True)
try:
async with engine.begin() as conn:
print("Making telegram_id nullable...")
await conn.execute(text(
"ALTER TABLE players ALTER COLUMN telegram_id DROP NOT NULL;"
))
print("✅ telegram_id is now nullable!")
except Exception as e:
print(f"❌ Error: {e}")
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(fix_telegram_id())

View File

@@ -0,0 +1,40 @@
"""
Migration: Add last_movement_time column to players table
"""
import asyncio
import sys
sys.path.insert(0, '/app')
from api import database as db
async def migrate():
await db.init_db()
try:
async with db.DatabaseSession() as session:
# Check if column exists
result = await session.execute(db.text("""
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_name = 'players'
AND column_name = 'last_movement_time'
"""))
count = result.scalar()
if count == 0:
print("Adding last_movement_time column to players table...")
await session.execute(db.text("""
ALTER TABLE players
ADD COLUMN last_movement_time FLOAT DEFAULT 0
"""))
await session.commit()
print("✅ Column added successfully!")
else:
print("⚠️ Column last_movement_time already exists, skipping.")
except Exception as e:
print(f"❌ Error: {e}")
raise
if __name__ == "__main__":
asyncio.run(migrate())

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
Migration script to add acknowledged flags to pvp_combats table
"""
import asyncio
from api.database import DatabaseSession, pvp_combats
from sqlalchemy import text
async def migrate():
"""Add attacker_acknowledged and defender_acknowledged columns"""
async with DatabaseSession() as session:
# Add attacker_acknowledged column
await session.execute(text(
"ALTER TABLE pvp_combats ADD COLUMN IF NOT EXISTS attacker_acknowledged BOOLEAN DEFAULT FALSE"
))
# Add defender_acknowledged column
await session.execute(text(
"ALTER TABLE pvp_combats ADD COLUMN IF NOT EXISTS defender_acknowledged BOOLEAN DEFAULT FALSE"
))
await session.commit()
print("✅ Added attacker_acknowledged and defender_acknowledged columns to pvp_combats table")
if __name__ == "__main__":
asyncio.run(migrate())

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""
Migration script to add pvp_combats table
"""
import asyncio
import sys
import os
# Add parent directory to path
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
from api.database import engine, metadata, pvp_combats
async def migrate():
"""Create pvp_combats table"""
async with engine.begin() as conn:
print("Creating pvp_combats table...")
await conn.run_sync(pvp_combats.create, checkfirst=True)
print("✅ pvp_combats table created successfully!")
if __name__ == "__main__":
print("=== PvP Combat Table Migration ===")
asyncio.run(migrate())
print("Migration complete!")

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""
Migration: Add last_action field to pvp_combats table
This allows the opponent to see what happened in the last turn
"""
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
async def migrate():
"""Add last_action column to pvp_combats table"""
# Database connection details
db_host = os.getenv('DB_HOST', 'localhost')
db_port = os.getenv('DB_PORT', '5432')
db_name = os.getenv('DB_NAME', 'echoes_db')
db_user = os.getenv('DB_USER', 'echoes_user')
db_password = os.getenv('DB_PASSWORD', 'change_this_password')
# Create async engine
database_url = f"postgresql+asyncpg://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
engine = create_async_engine(database_url, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
try:
# Add last_action column to pvp_combats
await session.execute(text("""
ALTER TABLE pvp_combats
ADD COLUMN IF NOT EXISTS last_action TEXT DEFAULT NULL;
"""))
await session.commit()
print("✅ Added last_action column to pvp_combats table")
except Exception as e:
await session.rollback()
print(f"❌ Error: {e}")
raise
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(migrate())
print("✅ Migration completed successfully!")

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Migration script to add PvP statistics columns
"""
import asyncio
import sys
import os
# Add parent directory to path
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
from api.database import engine
async def migrate():
"""Add PvP statistics columns to player_statistics table"""
async with engine.begin() as conn:
print("Adding PvP statistics columns...")
# Add PvP columns
await conn.execute(text("""
ALTER TABLE player_statistics
ADD COLUMN IF NOT EXISTS pvp_combats_initiated INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_combats_won INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_combats_lost INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_damage_dealt INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_damage_taken INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS players_killed INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_deaths INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_successful_flees INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_failed_flees INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_attacks_landed INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS pvp_attacks_received INTEGER DEFAULT 0
"""))
print("✅ PvP statistics columns added successfully!")
if __name__ == "__main__":
from sqlalchemy import text
print("=== PvP Statistics Migration ===")
asyncio.run(migrate())
print("Migration complete!")

View File

@@ -0,0 +1,92 @@
"""
Migration script for equipment system
Adds equipment slots, encumbrance stat, and item durability/tier system
"""
import asyncio
import sys
sys.path.insert(0, '/app')
from api import database as db
async def migrate():
"""Add equipment system to database"""
await db.init_db()
try:
async with db.DatabaseSession() as session:
print("🔄 Starting equipment system migration...")
# 1. Add encumbrance to players table
print("📊 Adding encumbrance stat to players...")
await session.execute(db.text("""
ALTER TABLE players
ADD COLUMN IF NOT EXISTS encumbrance INTEGER DEFAULT 0;
"""))
# 2. Create equipment_slots table
print("🎽 Creating equipment_slots table...")
await session.execute(db.text("""
CREATE TABLE IF NOT EXISTS equipment_slots (
player_id INTEGER REFERENCES players(id) ON DELETE CASCADE,
slot_type VARCHAR(20) NOT NULL,
item_id INTEGER REFERENCES inventory(id) ON DELETE SET NULL,
PRIMARY KEY (player_id, slot_type),
CONSTRAINT valid_slot_type CHECK (slot_type IN (
'head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack'
))
);
"""))
# 3. Add durability and tier columns to inventory
print("🔧 Adding durability and tier to inventory...")
await session.execute(db.text("""
ALTER TABLE inventory
ADD COLUMN IF NOT EXISTS durability INTEGER,
ADD COLUMN IF NOT EXISTS max_durability INTEGER,
ADD COLUMN IF NOT EXISTS tier INTEGER DEFAULT 1,
ADD COLUMN IF NOT EXISTS unique_stats JSONB;
"""))
# 4. Add is_equipped flag if not exists (should exist, but just in case)
print("📌 Ensuring is_equipped column exists...")
await session.execute(db.text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name='inventory' AND column_name='is_equipped'
) THEN
ALTER TABLE inventory ADD COLUMN is_equipped BOOLEAN DEFAULT FALSE;
END IF;
END $$;
"""))
await session.commit()
# 5. Initialize equipment slots for all existing players
print("👤 Initializing equipment slots for existing players...")
result = await session.execute(db.text("SELECT id FROM players"))
players = result.fetchall()
slots = ['head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack']
for player in players:
player_id = player[0]
for slot in slots:
await session.execute(db.text("""
INSERT INTO equipment_slots (player_id, slot_type, item_id)
VALUES (:player_id, :slot_type, NULL)
ON CONFLICT (player_id, slot_type) DO NOTHING
"""), {"player_id": player_id, "slot_type": slot})
await session.commit()
print(f"✅ Initialized equipment slots for {len(players)} players")
print("✅ Equipment system migration completed successfully!")
except Exception as e:
print(f"❌ Error during migration: {e}")
raise
if __name__ == "__main__":
asyncio.run(migrate())

View File

@@ -0,0 +1,58 @@
"""
Migration: Add unique item properties to dropped_items table
This migration adds durability, max_durability, tier, and unique_stats columns
to the dropped_items table so that dropped equipment items preserve their state.
"""
import asyncio
from api.database import DatabaseSession, engine, metadata
from sqlalchemy import text
async def migrate():
"""Add unique item columns to dropped_items"""
async with DatabaseSession() as session:
print("Starting migration: Add unique item properties to dropped_items...")
# Add durability column
try:
await session.execute(text(
"ALTER TABLE dropped_items ADD COLUMN durability INTEGER"
))
print("✓ Added durability column")
except Exception as e:
print(f"✗ durability column may already exist: {e}")
# Add max_durability column
try:
await session.execute(text(
"ALTER TABLE dropped_items ADD COLUMN max_durability INTEGER"
))
print("✓ Added max_durability column")
except Exception as e:
print(f"✗ max_durability column may already exist: {e}")
# Add tier column
try:
await session.execute(text(
"ALTER TABLE dropped_items ADD COLUMN tier INTEGER DEFAULT 1"
))
print("✓ Added tier column")
except Exception as e:
print(f"✗ tier column may already exist: {e}")
# Add unique_stats JSONB column
try:
await session.execute(text(
"ALTER TABLE dropped_items ADD COLUMN unique_stats JSONB"
))
print("✓ Added unique_stats column")
except Exception as e:
print(f"✗ unique_stats column may already exist: {e}")
await session.commit()
print("\n✓ Migration completed successfully!")
if __name__ == "__main__":
asyncio.run(migrate())

View File

@@ -0,0 +1,147 @@
"""
Migration: Create unique_items table and refactor item tracking
This creates a proper architecture where:
1. unique_items table stores individual item instances with their properties
2. inventory/dropped_items reference unique_item_id instead of duplicating data
3. When item is picked up, only the reference changes (dropped_items -> inventory)
4. When item decays/breaks, delete from unique_items (cascades to references)
"""
import asyncio
from api.database import DatabaseSession, engine, metadata
from sqlalchemy import text
async def migrate():
"""Create unique_items table and refactor references"""
async with DatabaseSession() as session:
print("Starting migration: Create unique_items table...")
# Step 1: Create unique_items table
try:
await session.execute(text("""
CREATE TABLE IF NOT EXISTS unique_items (
id SERIAL PRIMARY KEY,
item_id VARCHAR NOT NULL,
durability INTEGER,
max_durability INTEGER,
tier INTEGER DEFAULT 1,
unique_stats JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
print("✓ Created unique_items table")
except Exception as e:
print(f"✗ Error creating unique_items table: {e}")
return
# Step 2: Add unique_item_id to inventory (nullable for now)
try:
await session.execute(text(
"ALTER TABLE inventory ADD COLUMN IF NOT EXISTS unique_item_id INTEGER REFERENCES unique_items(id) ON DELETE CASCADE"
))
print("✓ Added unique_item_id to inventory")
except Exception as e:
print(f"✗ unique_item_id may already exist in inventory: {e}")
# Step 3: Add unique_item_id to dropped_items (nullable for now)
try:
await session.execute(text(
"ALTER TABLE dropped_items ADD COLUMN IF NOT EXISTS unique_item_id INTEGER REFERENCES unique_items(id) ON DELETE CASCADE"
))
print("✓ Added unique_item_id to dropped_items")
except Exception as e:
print(f"✗ unique_item_id may already exist in dropped_items: {e}")
# Step 4: Migrate existing inventory items with durability to unique_items
print("\nMigrating existing inventory items to unique_items...")
result = await session.execute(text("""
SELECT id, item_id, durability, max_durability, tier, unique_stats
FROM inventory
WHERE durability IS NOT NULL OR tier IS NOT NULL OR unique_stats IS NOT NULL
"""))
inventory_items = result.fetchall()
migrated_count = 0
for inv_item in inventory_items:
# Create unique_item entry
result = await session.execute(text("""
INSERT INTO unique_items (item_id, durability, max_durability, tier, unique_stats)
VALUES (:item_id, :durability, :max_durability, :tier, :unique_stats)
RETURNING id
"""), {
'item_id': inv_item.item_id,
'durability': inv_item.durability,
'max_durability': inv_item.max_durability,
'tier': inv_item.tier,
'unique_stats': inv_item.unique_stats
})
unique_item_id = result.fetchone()[0]
# Update inventory to reference it
await session.execute(text("""
UPDATE inventory
SET unique_item_id = :unique_item_id
WHERE id = :inv_id
"""), {
'unique_item_id': unique_item_id,
'inv_id': inv_item.id
})
migrated_count += 1
print(f"✓ Migrated {migrated_count} inventory items to unique_items")
# Step 5: Migrate existing dropped_items with durability to unique_items
print("\nMigrating existing dropped items to unique_items...")
result = await session.execute(text("""
SELECT id, item_id, durability, max_durability, tier, unique_stats
FROM dropped_items
WHERE durability IS NOT NULL OR tier IS NOT NULL OR unique_stats IS NOT NULL
"""))
dropped_items_list = result.fetchall()
migrated_dropped = 0
for dropped_item in dropped_items_list:
# Create unique_item entry
result = await session.execute(text("""
INSERT INTO unique_items (item_id, durability, max_durability, tier, unique_stats)
VALUES (:item_id, :durability, :max_durability, :tier, :unique_stats)
RETURNING id
"""), {
'item_id': dropped_item.item_id,
'durability': dropped_item.durability,
'max_durability': dropped_item.max_durability,
'tier': dropped_item.tier,
'unique_stats': dropped_item.unique_stats
})
unique_item_id = result.fetchone()[0]
# Update dropped_items to reference it
await session.execute(text("""
UPDATE dropped_items
SET unique_item_id = :unique_item_id
WHERE id = :dropped_id
"""), {
'unique_item_id': unique_item_id,
'dropped_id': dropped_item.id
})
migrated_dropped += 1
print(f"✓ Migrated {migrated_dropped} dropped items to unique_items")
# Step 6: Drop old columns from inventory (keep for backward compatibility for now)
# We'll drop these in a future migration after verifying everything works
print("\n⚠️ Old durability/tier columns still exist for backward compatibility")
print(" They can be safely removed in a future migration")
await session.commit()
print("\n✅ Migration completed successfully!")
print(f"\n📊 Summary:")
print(f" - Created unique_items table")
print(f" - Migrated {migrated_count} inventory items")
print(f" - Migrated {migrated_dropped} dropped items")
print(f" - Total unique items: {migrated_count + migrated_dropped}")
if __name__ == "__main__":
asyncio.run(migrate())