Upload 195 files
Browse filesTempus .... fugit.
Refactored version.
This view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +3 -0
- DEVELOPER_DOCUMENTATION.md +189 -0
- Documentation/DEVELOPER_DOCUMENTATION.md +210 -0
- Documentation/NPC_Addon_Development_Guide.md +1312 -0
- Documentation/NPC_Addon_Development_Guide_OLD.md +1110 -0
- Documentation/NPC_Addon_Development_Guide_Updated.md +1312 -0
- Documentation/ROADMAP.md +375 -0
- Documentation/Simple_Game_Client_Guide.md +744 -0
- Documentation/sample_gradio_mcp_server.py +475 -0
- Documentation/simple_game_client.py +485 -0
- Simple_Game_Client_Guide.md +744 -0
- USER_DOCUMENTATION.md +907 -0
- app.py +0 -0
- app_original_backup.py +0 -0
- plugins/__pycache__/enhanced_chat_plugin.cpython-313.pyc +0 -0
- plugins/__pycache__/enhanced_chat_plugin_final.cpython-313.pyc +0 -0
- plugins/__pycache__/enhanced_chat_plugin_fixed.cpython-313.pyc +0 -0
- plugins/__pycache__/plugin_registry.cpython-313.pyc +0 -0
- plugins/__pycache__/sample_plugin.cpython-313.pyc +0 -0
- plugins/__pycache__/trading_system_plugin.cpython-313.pyc +0 -0
- plugins/__pycache__/weather_plugin.cpython-313.pyc +0 -0
- plugins/enhanced_chat_plugin.py +449 -0
- plugins/enhanced_chat_plugin.py.backup +586 -0
- plugins/enhanced_chat_plugin_fixed.py +264 -0
- plugins/plugin_registry.conf +62 -0
- plugins/sample_plugin.py +54 -0
- plugins/trading_system_plugin.py +378 -0
- plugins/weather_plugin.py +187 -0
- pytest.ini +30 -0
- quick_test.py +47 -0
- requirements.txt +53 -0
- setup.bat +36 -0
- setup.ps1 +41 -0
- setup.py +106 -0
- src/__init__.py +11 -0
- src/__pycache__/__init__.cpython-313.pyc +0 -0
- src/addons/__init__.py +5 -0
- src/addons/__pycache__/__init__.cpython-313.pyc +0 -0
- src/addons/__pycache__/example_npc_addon.cpython-313.pyc +0 -0
- src/addons/__pycache__/read2burn_addon.cpython-313.pyc +0 -0
- src/addons/__pycache__/weather_oracle_addon.cpython-313.pyc +0 -0
- src/addons/example_npc_addon.py +230 -0
- src/addons/fortune_teller_addon.py +240 -0
- src/addons/magic_shop_addon.py +509 -0
- src/addons/read2burn_addon.py +213 -0
- src/addons/self_contained_example_addon.py +215 -0
- src/addons/simple_trader_addon.py +186 -0
- src/addons/weather_oracle_addon.py +380 -0
- src/core/__pycache__/game_engine.cpython-313.pyc +0 -0
- src/core/__pycache__/player.cpython-313.pyc +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tests/unit/services/__pycache__/test_npc_service_corrected.cpython-313-pytest-8.3.5.pyc filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
tests/unit/services/__pycache__/test_npc_service_fixed.cpython-313-pytest-8.3.5.pyc filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
tests/unit/services/__pycache__/test_npc_service.cpython-313-pytest-8.3.5.pyc filter=lfs diff=lfs merge=lfs -text
|
DEVELOPER_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛠️ MMORPG with MCP Integration - Developer Documentation
|
| 2 |
+
|
| 3 |
+
## 📖 Table of Contents
|
| 4 |
+
|
| 5 |
+
1. [Introduction](#introduction)
|
| 6 |
+
2. [Project Setup](#project-setup)
|
| 7 |
+
3. [Architecture Overview](#architecture-overview)
|
| 8 |
+
4. [MCP API Reference](#mcp-api-reference)
|
| 9 |
+
* [Connection](#connection)
|
| 10 |
+
* [Available Tools/Commands](#available-toolscommands)
|
| 11 |
+
* [Common Operations](#common-operations)
|
| 12 |
+
5. [Extending the Game](#extending-the-game)
|
| 13 |
+
* [NPC Add-on Development](#npc-add-on-development)
|
| 14 |
+
* [Adding New Game Mechanics](#adding-new-game-mechanics)
|
| 15 |
+
6. [AI Agent Integration](#ai-agent-integration)
|
| 16 |
+
7. [Debugging](#debugging)
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 📜 Introduction
|
| 21 |
+
|
| 22 |
+
This document provides technical guidance for developers working on the **MMORPG with MCP Integration** project. It covers project setup, architecture, API usage for client/agent development, and how to extend the game's functionalities.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## ⚙️ Project Setup
|
| 27 |
+
|
| 28 |
+
### Prerequisites
|
| 29 |
+
* Python 3.8 or higher
|
| 30 |
+
* `pip` for package management
|
| 31 |
+
|
| 32 |
+
### Getting the Code
|
| 33 |
+
Clone the repository to your local machine (if you haven't already).
|
| 34 |
+
|
| 35 |
+
### Installation
|
| 36 |
+
1. Navigate to the project directory (e.g., `c:\Users\Chris4K\Projekte\projecthub\projects\MMOP_second_try`).
|
| 37 |
+
```powershell
|
| 38 |
+
cd c:\Users\Chris4K\Projekte\projecthub\projects\MMOP_second_try
|
| 39 |
+
```
|
| 40 |
+
2. Install the required Python dependencies:
|
| 41 |
+
```bash
|
| 42 |
+
pip install gradio mcp aiohttp asyncio
|
| 43 |
+
```
|
| 44 |
+
(Ensure other project-specific dependencies from your `requirements.txt` are also installed).
|
| 45 |
+
|
| 46 |
+
### Running the Game Server
|
| 47 |
+
Execute the main application file (assumed to be `app.py` in the root of `MMOP_second_try`):
|
| 48 |
+
```bash
|
| 49 |
+
python app.py
|
| 50 |
+
```
|
| 51 |
+
The server should start, typically making the Gradio UI available at `http://127.0.0.1:7868` and the MCP SSE endpoint at `http://127.0.0.1:7868/gradio_api/mcp/sse`.
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 🏗️ Architecture Overview (High-Level)
|
| 56 |
+
|
| 57 |
+
* **Game Server (`app.py`)**: The main entry point. Initializes and runs the Gradio web interface, integrates game components, and likely hosts the MCP endpoint.
|
| 58 |
+
* **Game Engine (`src/core/game_engine.py`)**: Contains the core game logic, manages world state, player data, NPC interactions, and game rules.
|
| 59 |
+
* **UI Layer (`src/ui/interface_manager.py`, `src/ui/huggingface_ui.py`)**: Manages the Gradio user interface, defining layouts, components, and event handling for human players.
|
| 60 |
+
* **MCP Integration Layer**: A facade or service within the server that exposes game functionalities to MCP clients. This allows AI agents or other external systems to interact with the game.
|
| 61 |
+
* **NPC Addons**: Modular components that extend NPC functionalities. See `NPC_Addon_Development_Guide.md`.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 🔌 MCP API Reference
|
| 66 |
+
|
| 67 |
+
The game server exposes its functionalities via the Model Context Protocol (MCP), allowing external clients (like AI agents or custom tools) to interact with the game world programmatically.
|
| 68 |
+
|
| 69 |
+
### Connection
|
| 70 |
+
* **Endpoint URL**: `http://127.0.0.1:7868/gradio_api/mcp/sse` (for Server-Sent Events)
|
| 71 |
+
* **Protocol**: MCP over SSE.
|
| 72 |
+
* **Client Implementation**: Refer to `simple_game_client.py` for a reference Python client. It uses `mcp.ClientSession` and `mcp.client.sse.sse_client`.
|
| 73 |
+
|
| 74 |
+
**Example Connection Snippet (from `simple_game_client.py`):**
|
| 75 |
+
```python
|
| 76 |
+
from mcp import ClientSession
|
| 77 |
+
from mcp.client.sse import sse_client
|
| 78 |
+
from contextlib import AsyncExitStack
|
| 79 |
+
import asyncio
|
| 80 |
+
|
| 81 |
+
class GameClient:
|
| 82 |
+
def __init__(self, server_url="http://127.0.0.1:7868/gradio_api/mcp/sse"):
|
| 83 |
+
self.server_url = server_url
|
| 84 |
+
self.session = None
|
| 85 |
+
self.exit_stack = AsyncExitStack()
|
| 86 |
+
self.connected = False
|
| 87 |
+
self.tools = []
|
| 88 |
+
|
| 89 |
+
async def connect(self):
|
| 90 |
+
try:
|
| 91 |
+
transport = await self.exit_stack.enter_async_context(
|
| 92 |
+
sse_client(self.server_url)
|
| 93 |
+
)
|
| 94 |
+
read_stream, write_callable = transport
|
| 95 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 96 |
+
ClientSession(read_stream, write_callable)
|
| 97 |
+
)
|
| 98 |
+
await self.session.initialize()
|
| 99 |
+
response = await self.session.list_tools()
|
| 100 |
+
self.tools = response.tools
|
| 101 |
+
self.connected = True
|
| 102 |
+
print(f"✅ Connected! Available tools: {[tool.name for tool in self.tools]}")
|
| 103 |
+
return True
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"❌ Connection failed: {e}")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
async def disconnect(self):
|
| 109 |
+
if self.connected:
|
| 110 |
+
await self.exit_stack.aclose()
|
| 111 |
+
self.connected = False
|
| 112 |
+
print("🔌 Disconnected.")
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### Available Tools/Commands
|
| 116 |
+
Upon connection, the client can list available tools (commands) from the server. Common tools exposed by the MMORPG server typically include:
|
| 117 |
+
|
| 118 |
+
* `register_ai_agent` (or similar): To join the game as a new player/agent.
|
| 119 |
+
* `move_agent` (or similar): To move the player/agent in the game world.
|
| 120 |
+
* `send_chat_message` (or similar): To send messages to the public game chat.
|
| 121 |
+
* `interact_with_npc` (or similar): To send messages or commands to specific NPCs.
|
| 122 |
+
* `get_game_state` (or similar): To retrieve information about the current game world, other players, etc.
|
| 123 |
+
|
| 124 |
+
The exact names and parameters of these tools are defined by the server-side MCP implementation. Use `session.list_tools()` to get the current list and `tool.parameters_json_schema` for expected inputs.
|
| 125 |
+
|
| 126 |
+
### Common Operations
|
| 127 |
+
|
| 128 |
+
**1. Registering a Player/Agent:**
|
| 129 |
+
* **Tool**: Look for a tool like `register_ai_agent`.
|
| 130 |
+
* **Parameters**: Typically `player_name` (string) and `client_id` (string, unique identifier for the client).
|
| 131 |
+
* **Response**: Often includes an `agent_id` or `player_id` assigned by the server.
|
| 132 |
+
|
| 133 |
+
**2. Moving a Player/Agent:**
|
| 134 |
+
* **Tool**: Look for a tool like `move_agent`.
|
| 135 |
+
* **Parameters**: `agent_id` (string) and `direction` (string, e.g., "north", "south", "east", "west", "up", "down").
|
| 136 |
+
* **Response**: Confirmation of movement, new coordinates, or error if movement is not possible.
|
| 137 |
+
|
| 138 |
+
**3. Sending Chat Messages:**
|
| 139 |
+
* **Tool**: Look for a tool like `send_chat_message`.
|
| 140 |
+
* **Parameters**: `agent_id` (string) and `message` (string).
|
| 141 |
+
* **Response**: Confirmation that the message was sent.
|
| 142 |
+
|
| 143 |
+
**4. Interacting with NPCs:**
|
| 144 |
+
* **Tool**: Look for a tool like `interact_with_npc`.
|
| 145 |
+
* **Parameters**: `agent_id` (string), `npc_id` (string), and `message` or `command` (string).
|
| 146 |
+
* **Response**: NPC's reply or result of the interaction.
|
| 147 |
+
|
| 148 |
+
**5. Getting Game State:**
|
| 149 |
+
* **Tool**: Look for a tool like `get_game_state`.
|
| 150 |
+
* **Parameters**: May include `agent_id` (string) or be parameterless.
|
| 151 |
+
* **Response**: JSON object containing game world information, list of players, NPCs, etc.
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## 🧩 Extending the Game
|
| 156 |
+
|
| 157 |
+
### NPC Add-on Development
|
| 158 |
+
For creating new NPCs with custom behaviors and UI components, please refer to the **`NPC_Addon_Development_Guide.md`**. It provides a comprehensive, up-to-date guide covering the modern auto-registration system, working examples from the codebase, and best practices.
|
| 159 |
+
|
| 160 |
+
### Adding New Game Mechanics
|
| 161 |
+
To introduce new core game mechanics:
|
| 162 |
+
1. **Game Engine (`src/core/game_engine.py`):** Implement the fundamental logic for the new mechanic here. This might involve modifying player states, world interactions, or introducing new entities.
|
| 163 |
+
2. **Facade Layer:** If the mechanic needs to be exposed to the UI or MCP clients, update or create methods in your game facade (e.g., `src/facades/game_facade.py`) to provide a clean interface to the engine's functionality.
|
| 164 |
+
3. **UI Integration (`src/ui/interface_manager.py`):** If human players should interact with this mechanic, add new UI elements and event handlers in the Gradio interface.
|
| 165 |
+
4. **MCP Exposure:** If AI agents or external clients should use this mechanic, expose the relevant facade methods as new MCP tools.
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
## 🤖 AI Agent Integration
|
| 170 |
+
|
| 171 |
+
AI agents can connect to the MMORPG as players using an MCP client (like the one shown in `simple_game_client.py`).
|
| 172 |
+
1. **Connection**: The agent connects to the MCP SSE endpoint.
|
| 173 |
+
2. **Registration**: The agent registers itself, providing a name.
|
| 174 |
+
3. **Interaction**: The agent uses the available MCP tools to perceive the game state (`get_game_state`), move (`move_agent`), chat (`send_chat_message`), and interact with NPCs (`interact_with_npc`).
|
| 175 |
+
4. **Decision Making**: The agent's internal logic processes game state information and decides on actions to take.
|
| 176 |
+
|
| 177 |
+
The `simple_game_client.py` script serves as a foundational example for building more sophisticated AI agents.
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## 🐞 Debugging
|
| 182 |
+
|
| 183 |
+
* **Server Logs**: Check the console output where `python app.py` is running for server-side errors, print statements, and game event logs.
|
| 184 |
+
* **Gradio UI**: Use your web browser's developer tools (especially the console) to debug issues related to the Gradio interface and JavaScript (like the keyboard control script).
|
| 185 |
+
* **MCP Client-Side**: Add extensive logging in your MCP client to trace requests sent to the server and responses received. Print the full content of MCP tool calls and results.
|
| 186 |
+
* **Game State Dumps**: Periodically use the `get_game_state` MCP tool (if available) or implement a debug feature to dump the current game state to understand the situation from the server's perspective.
|
| 187 |
+
* **Incremental Testing**: When developing new features or MCP tools, test them incrementally.
|
| 188 |
+
|
| 189 |
+
---
|
Documentation/DEVELOPER_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛠️ MMORPG with MCP Integration - Developer Documentation
|
| 2 |
+
|
| 3 |
+
## 📖 Table of Contents
|
| 4 |
+
|
| 5 |
+
1. [Introduction](#introduction)
|
| 6 |
+
2. [Project Setup](#project-setup)
|
| 7 |
+
3. [Architecture Overview](#architecture-overview)
|
| 8 |
+
4. [MCP API Reference](#mcp-api-reference)
|
| 9 |
+
* [Connection](#connection)
|
| 10 |
+
* [Available Tools/Commands](#available-toolscommands)
|
| 11 |
+
* [Common Operations](#common-operations)
|
| 12 |
+
5. [Extending the Game](#extending-the-game)
|
| 13 |
+
* [NPC Add-on Development](#npc-add-on-development)
|
| 14 |
+
* [Adding New Game Mechanics](#adding-new-game-mechanics)
|
| 15 |
+
6. [AI Agent Integration](#ai-agent-integration)
|
| 16 |
+
7. [Debugging](#debugging)
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 📜 Introduction
|
| 21 |
+
|
| 22 |
+
This document provides technical guidance for developers working on the **MMORPG with MCP Integration** project. It covers project setup, architecture, API usage for client/agent development, and how to extend the game's functionalities.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## ⚙️ Project Setup
|
| 27 |
+
|
| 28 |
+
### Prerequisites
|
| 29 |
+
* Python 3.8 or higher
|
| 30 |
+
* `pip` for package management
|
| 31 |
+
|
| 32 |
+
### Getting the Code
|
| 33 |
+
Clone the repository to your local machine (if you haven't already).
|
| 34 |
+
|
| 35 |
+
### Installation
|
| 36 |
+
1. Navigate to the project directory (e.g., `c:\\Users\\Chris4K\\Projekte\\projecthub\\projects\\MMOP_second_try`).
|
| 37 |
+
```powershell
|
| 38 |
+
cd c:\Users\Chris4K\Projekte\projecthub\projects\MMOP_second_try
|
| 39 |
+
```
|
| 40 |
+
2. Install the required Python dependencies:
|
| 41 |
+
```bash
|
| 42 |
+
pip install gradio mcp aiohttp asyncio
|
| 43 |
+
```
|
| 44 |
+
(Ensure other project-specific dependencies from your `requirements.txt` are also installed).
|
| 45 |
+
|
| 46 |
+
### Running the Game Server
|
| 47 |
+
Execute the main application file (assumed to be `app.py` in the root of `MMOP_second_try`):
|
| 48 |
+
```bash
|
| 49 |
+
python app.py
|
| 50 |
+
```
|
| 51 |
+
The server should start, typically making the Gradio UI available at `http://127.0.0.1:7868` and the MCP SSE endpoint at `http://127.0.0.1:7868/gradio_api/mcp/sse`.
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 🏗️ Architecture Overview (High-Level)
|
| 56 |
+
|
| 57 |
+
* **Game Server (`app.py`)**: The main entry point. Initializes and runs the Gradio web interface, integrates game components, and likely hosts the MCP endpoint.
|
| 58 |
+
* **Game Engine (`src/core/game_engine.py`)**: Contains the core game logic, manages world state, player data, NPC interactions, and game rules.
|
| 59 |
+
* **UI Layer (`src/ui/interface_manager.py`, `src/ui/huggingface_ui.py`)**: Manages the Gradio user interface, defining layouts, components, and event handling for human players.
|
| 60 |
+
* **MCP Integration Layer**: A facade or service within the server that exposes game functionalities to MCP clients. This allows AI agents or other external systems to interact with the game.
|
| 61 |
+
* **NPC Addons**: Modular components that extend NPC functionalities. See `NPC_Addon_Development_Guide.md`.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 🔌 MCP API Reference
|
| 66 |
+
|
| 67 |
+
The game server exposes its functionalities via the Model Context Protocol (MCP), allowing external clients (like AI agents or custom tools) to interact with the game world programmatically.
|
| 68 |
+
|
| 69 |
+
### Connection
|
| 70 |
+
* **Endpoint URL**: `http://127.0.0.1:7868/gradio_api/mcp/sse` (for Server-Sent Events)
|
| 71 |
+
* **Protocol**: MCP over SSE.
|
| 72 |
+
* **Client Implementation**: Refer to `simple_game_client.py` for a reference Python client. It uses `mcp.ClientSession` and `mcp.client.sse.sse_client`.
|
| 73 |
+
|
| 74 |
+
**Example Connection Snippet (from `simple_game_client.py`):**
|
| 75 |
+
```python
|
| 76 |
+
from mcp import ClientSession
|
| 77 |
+
from mcp.client.sse import sse_client
|
| 78 |
+
from contextlib import AsyncExitStack
|
| 79 |
+
import asyncio
|
| 80 |
+
|
| 81 |
+
class GameClient:
|
| 82 |
+
def __init__(self, server_url="http://127.0.0.1:7868/gradio_api/mcp/sse"):
|
| 83 |
+
self.server_url = server_url
|
| 84 |
+
self.session = None
|
| 85 |
+
self.exit_stack = AsyncExitStack()
|
| 86 |
+
self.connected = False
|
| 87 |
+
self.tools = []
|
| 88 |
+
|
| 89 |
+
async def connect(self):
|
| 90 |
+
try:
|
| 91 |
+
transport = await self.exit_stack.enter_async_context(
|
| 92 |
+
sse_client(self.server_url)
|
| 93 |
+
)
|
| 94 |
+
read_stream, write_callable = transport
|
| 95 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 96 |
+
ClientSession(read_stream, write_callable)
|
| 97 |
+
)
|
| 98 |
+
await self.session.initialize()
|
| 99 |
+
response = await self.session.list_tools()
|
| 100 |
+
self.tools = response.tools
|
| 101 |
+
self.connected = True
|
| 102 |
+
print(f"✅ Connected! Available tools: {[tool.name for tool in self.tools]}")
|
| 103 |
+
return True
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"❌ Connection failed: {e}")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
async def disconnect(self):
|
| 109 |
+
if self.connected:
|
| 110 |
+
await self.exit_stack.aclose()
|
| 111 |
+
self.connected = False
|
| 112 |
+
print("🔌 Disconnected.")
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### Available Tools/Commands
|
| 116 |
+
Upon connection, the client can list available tools (commands) from the server. Common tools exposed by the MMORPG server typically include:
|
| 117 |
+
|
| 118 |
+
* `register_ai_agent` (or similar): To join the game as a new player/agent.
|
| 119 |
+
* `move_agent` (or similar): To move the player/agent in the game world.
|
| 120 |
+
* `send_chat_message` (or similar): To send messages to the public game chat.
|
| 121 |
+
* `interact_with_npc` (or similar): To send messages or commands to specific NPCs.
|
| 122 |
+
* `get_game_state` (or similar): To retrieve information about the current game world, other players, etc.
|
| 123 |
+
|
| 124 |
+
The exact names and parameters of these tools are defined by the server-side MCP implementation. Use `session.list_tools()` to get the current list and `tool.parameters_json_schema` for expected inputs.
|
| 125 |
+
|
| 126 |
+
### Common Operations
|
| 127 |
+
|
| 128 |
+
**1. Registering a Player/Agent:**
|
| 129 |
+
* **Tool**: Look for a tool like `register_ai_agent`.
|
| 130 |
+
* **Parameters**: Typically `player_name` (string) and `client_id` (string, unique identifier for the client).
|
| 131 |
+
* **Response**: Often includes an `agent_id` or `player_id` assigned by the server.
|
| 132 |
+
|
| 133 |
+
**2. Moving a Player/Agent:**
|
| 134 |
+
* **Tool**: Look for a tool like `move_agent`.
|
| 135 |
+
* **Parameters**: `agent_id` (string) and `direction` (string, e.g., "north", "south", "east", "west", "up", "down").
|
| 136 |
+
* **Response**: Confirmation of movement, new coordinates, or error if movement is not possible.
|
| 137 |
+
|
| 138 |
+
**3. Sending Chat Messages:**
|
| 139 |
+
* **Tool**: Look for a tool like `send_chat_message`.
|
| 140 |
+
* **Parameters**: `agent_id` (string) and `message` (string).
|
| 141 |
+
* **Response**: Confirmation that the message was sent.
|
| 142 |
+
|
| 143 |
+
**4. Interacting with NPCs:**
|
| 144 |
+
* **Tool**: Look for a tool like `interact_with_npc`.
|
| 145 |
+
* **Parameters**: `agent_id` (string), `npc_id` (string), and `message` or `command` (string).
|
| 146 |
+
* **Response**: NPC's reply or result of the interaction.
|
| 147 |
+
|
| 148 |
+
**5. Getting Game State:**
|
| 149 |
+
* **Tool**: Look for a tool like `get_game_state`.
|
| 150 |
+
* **Parameters**: May include `agent_id` (string) or be parameterless.
|
| 151 |
+
* **Response**: JSON object containing game world information, list of players, NPCs, etc.
|
| 152 |
+
|
| 153 |
+
**Example MCP Tool Usage:**
|
| 154 |
+
```python
|
| 155 |
+
# Register as AI agent
|
| 156 |
+
response = await session.call_tool("register_ai_agent", {
|
| 157 |
+
"player_name": "MyBot",
|
| 158 |
+
"client_id": "bot_001"
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
# Move the agent
|
| 162 |
+
response = await session.call_tool("move_agent", {
|
| 163 |
+
"agent_id": "bot_001",
|
| 164 |
+
"direction": "north"
|
| 165 |
+
})
|
| 166 |
+
|
| 167 |
+
# Send chat message
|
| 168 |
+
response = await session.call_tool("send_chat_message", {
|
| 169 |
+
"agent_id": "bot_001",
|
| 170 |
+
"message": "Hello everyone!"
|
| 171 |
+
})
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 🧩 Extending the Game
|
| 177 |
+
|
| 178 |
+
### NPC Add-on Development
|
| 179 |
+
For creating new NPCs with custom behaviors and UI components, please refer to the **`NPC_Addon_Development_Guide.md`**. It provides a comprehensive, up-to-date guide covering the modern auto-registration system, working examples from the codebase, and best practices.
|
| 180 |
+
|
| 181 |
+
### Adding New Game Mechanics
|
| 182 |
+
To introduce new core game mechanics:
|
| 183 |
+
1. **Game Engine (`src/core/game_engine.py`):** Implement the fundamental logic for the new mechanic here. This might involve modifying player states, world interactions, or introducing new entities.
|
| 184 |
+
2. **Facade Layer:** If the mechanic needs to be exposed to the UI or MCP clients, update or create methods in your game facade (e.g., `src/facades/game_facade.py`) to provide a clean interface to the engine's functionality.
|
| 185 |
+
3. **UI Integration (`src/ui/interface_manager.py`):** If human players should interact with this mechanic, add new UI elements and event handlers in the Gradio interface.
|
| 186 |
+
4. **MCP Exposure:** If AI agents or external clients should use this mechanic, expose the relevant facade methods as new MCP tools.
|
| 187 |
+
|
| 188 |
+
---
|
| 189 |
+
|
| 190 |
+
## 🤖 AI Agent Integration
|
| 191 |
+
|
| 192 |
+
AI agents can connect to the MMORPG as players using an MCP client (like the one shown in `simple_game_client.py`).
|
| 193 |
+
1. **Connection**: The agent connects to the MCP SSE endpoint.
|
| 194 |
+
2. **Registration**: The agent registers itself, providing a name.
|
| 195 |
+
3. **Interaction**: The agent uses the available MCP tools to perceive the game state (`get_game_state`), move (`move_agent`), chat (`send_chat_message`), and interact with NPCs (`interact_with_npc`).
|
| 196 |
+
4. **Decision Making**: The agent's internal logic processes game state information and decides on actions to take.
|
| 197 |
+
|
| 198 |
+
The `simple_game_client.py` script serves as a foundational example for building more sophisticated AI agents.
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## 🐞 Debugging
|
| 203 |
+
|
| 204 |
+
* **Server Logs**: Check the console output where `python app.py` is running for server-side errors, print statements, and game event logs.
|
| 205 |
+
* **Gradio UI**: Use your web browser's developer tools (especially the console) to debug issues related to the Gradio interface and JavaScript (like the keyboard control script).
|
| 206 |
+
* **MCP Client-Side**: Add extensive logging in your MCP client to trace requests sent to the server and responses received. Print the full content of MCP tool calls and results.
|
| 207 |
+
* **Game State Dumps**: Periodically use the `get_game_state` MCP tool (if available) or implement a debug feature to dump the current game state to understand the situation from the server's perspective.
|
| 208 |
+
* **Incremental Testing**: When developing new features or MCP tools, test them incrementally.
|
| 209 |
+
|
| 210 |
+
---
|
Documentation/NPC_Addon_Development_Guide.md
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛠️ NPC Addon Development Guide - Complete & Updated
|
| 2 |
+
|
| 3 |
+
> **Updated for Current Architecture** - This guide reflects the latest addon system with auto-registration, modern patterns, and actual working examples from the codebase.
|
| 4 |
+
|
| 5 |
+
## 📋 Table of Contents
|
| 6 |
+
|
| 7 |
+
1. [Quick Start](#quick-start)
|
| 8 |
+
2. [Modern Addon Architecture](#modern-addon-architecture)
|
| 9 |
+
3. [Auto-Registration System](#auto-registration-system)
|
| 10 |
+
4. [Complete Examples](#complete-examples)
|
| 11 |
+
5. [Advanced Patterns](#advanced-patterns)
|
| 12 |
+
6. [MCP Integration](#mcp-integration)
|
| 13 |
+
7. [Best Practices](#best-practices)
|
| 14 |
+
8. [Deployment & Testing](#deployment--testing)
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 🚀 Quick Start
|
| 19 |
+
|
| 20 |
+
### Creating Your First Addon
|
| 21 |
+
|
| 22 |
+
The modern addon system uses **auto-registration** - no manual edits to other files required!
|
| 23 |
+
|
| 24 |
+
```python
|
| 25 |
+
# src/addons/my_first_addon.py
|
| 26 |
+
"""
|
| 27 |
+
My First Addon - A simple self-contained NPC addon.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import gradio as gr
|
| 31 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class MyFirstAddon(NPCAddon):
|
| 35 |
+
"""A simple greeting NPC that demonstrates the addon system."""
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
super().__init__() # This auto-registers the addon!
|
| 39 |
+
self.greeting_count = 0
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def addon_id(self) -> str:
|
| 43 |
+
"""Unique identifier - must be unique across all addons"""
|
| 44 |
+
return "my_first_addon"
|
| 45 |
+
|
| 46 |
+
@property
|
| 47 |
+
def addon_name(self) -> str:
|
| 48 |
+
"""Display name shown in UI"""
|
| 49 |
+
return "🤝 My First Addon"
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def npc_config(self) -> Dict[str, Any]:
|
| 53 |
+
"""NPC configuration for auto-placement in world"""
|
| 54 |
+
return {
|
| 55 |
+
'id': 'my_first_npc',
|
| 56 |
+
'name': '🤝 Friendly Greeter',
|
| 57 |
+
'x': 100, 'y': 100, # Position in game world
|
| 58 |
+
'char': '🤝', # Character emoji
|
| 59 |
+
'type': 'addon', # NPC type
|
| 60 |
+
'description': 'A friendly NPC that greets players'
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def ui_tab_name(self) -> str:
|
| 65 |
+
"""UI tab name - returns None if no UI tab needed"""
|
| 66 |
+
return "🤝 Greeter"
|
| 67 |
+
|
| 68 |
+
def get_interface(self) -> gr.Component:
|
| 69 |
+
"""Create the Gradio interface for this addon"""
|
| 70 |
+
with gr.Column() as interface:
|
| 71 |
+
gr.Markdown("## 🤝 Friendly Greeter\n*Hello! I'm here to greet players.*")
|
| 72 |
+
|
| 73 |
+
greeting_btn = gr.Button("👋 Get Greeting", variant="primary")
|
| 74 |
+
greeting_output = gr.Textbox(label="Greeting", lines=3, interactive=False)
|
| 75 |
+
|
| 76 |
+
def get_greeting():
|
| 77 |
+
self.greeting_count += 1
|
| 78 |
+
return f"Hello! This is greeting #{self.greeting_count}. Welcome to the game!"
|
| 79 |
+
|
| 80 |
+
greeting_btn.click(get_greeting, outputs=[greeting_output])
|
| 81 |
+
|
| 82 |
+
return interface
|
| 83 |
+
|
| 84 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 85 |
+
"""Handle commands sent via private messages to this NPC"""
|
| 86 |
+
cmd = command.lower().strip()
|
| 87 |
+
|
| 88 |
+
if cmd in ['hello', 'hi', 'greet']:
|
| 89 |
+
self.greeting_count += 1
|
| 90 |
+
return f"🤝 **Friendly Greeter says:**\nHello! Greeting #{self.greeting_count}!"
|
| 91 |
+
elif cmd == 'help':
|
| 92 |
+
return "🤝 **Available commands:**\n• hello/hi/greet - Get a greeting\n• help - Show this help"
|
| 93 |
+
else:
|
| 94 |
+
return "🤝 **Friendly Greeter:**\nI didn't understand that. Try 'hello' or 'help'!"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# Global instance - this triggers auto-registration!
|
| 98 |
+
my_first_addon = MyFirstAddon()
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
**That's it!** Your addon is now fully functional and auto-registered. No other files need to be modified.
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 🏗️ Modern Addon Architecture
|
| 106 |
+
|
| 107 |
+
### Base Class Structure
|
| 108 |
+
|
| 109 |
+
All addons inherit from `NPCAddon` which provides:
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
class NPCAddon(ABC):
|
| 113 |
+
"""Base class for NPC add-ons with auto-registration support"""
|
| 114 |
+
|
| 115 |
+
def __init__(self):
|
| 116 |
+
"""Initialize and auto-register the addon"""
|
| 117 |
+
# Auto-register this addon when instantiated
|
| 118 |
+
_addon_registry[self.addon_id] = self
|
| 119 |
+
|
| 120 |
+
# Required Properties
|
| 121 |
+
@property
|
| 122 |
+
@abstractmethod
|
| 123 |
+
def addon_id(self) -> str: pass
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
@abstractmethod
|
| 127 |
+
def addon_name(self) -> str: pass
|
| 128 |
+
|
| 129 |
+
# Optional Properties
|
| 130 |
+
@property
|
| 131 |
+
def npc_config(self) -> Optional[Dict[str, Any]]: return None
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def ui_tab_name(self) -> Optional[str]: return None
|
| 135 |
+
|
| 136 |
+
# Required Methods
|
| 137 |
+
@abstractmethod
|
| 138 |
+
def get_interface(self) -> gr.Component: pass
|
| 139 |
+
|
| 140 |
+
@abstractmethod
|
| 141 |
+
def handle_command(self, player_id: str, command: str) -> str: pass
|
| 142 |
+
|
| 143 |
+
# Optional Lifecycle Methods
|
| 144 |
+
def on_startup(self): pass
|
| 145 |
+
def on_shutdown(self): pass
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Core Concepts
|
| 149 |
+
|
| 150 |
+
1. **Auto-Registration**: Simply instantiating your addon class registers it globally
|
| 151 |
+
2. **Self-Contained**: Everything in one file - NPC config, UI, logic
|
| 152 |
+
3. **Optional Components**: Only implement what you need (UI tab, world NPC, etc.)
|
| 153 |
+
4. **Service Pattern**: Use interfaces for complex functionality
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
## 🔄 Auto-Registration System
|
| 158 |
+
|
| 159 |
+
### How It Works
|
| 160 |
+
|
| 161 |
+
1. **Create addon class** that inherits from `NPCAddon`
|
| 162 |
+
2. **Instantiate globally** at the bottom of your addon file
|
| 163 |
+
3. **Auto-discovery** by the game engine at startup
|
| 164 |
+
4. **Automatic registration** of NPCs and UI components
|
| 165 |
+
|
| 166 |
+
### Current Auto-Registration List
|
| 167 |
+
|
| 168 |
+
The game engine automatically loads these addons:
|
| 169 |
+
|
| 170 |
+
```python
|
| 171 |
+
# src/core/game_engine.py
|
| 172 |
+
auto_register_addons = [
|
| 173 |
+
('weather_oracle_addon', 'auto_register'),
|
| 174 |
+
# Add your addon here if using custom auto_register function
|
| 175 |
+
]
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### Two Registration Patterns
|
| 179 |
+
|
| 180 |
+
#### Pattern 1: Global Instance (Recommended)
|
| 181 |
+
```python
|
| 182 |
+
# At bottom of your addon file
|
| 183 |
+
my_addon = MyAddon() # Auto-registers via __init__
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
#### Pattern 2: Custom Auto-Register Function
|
| 187 |
+
```python
|
| 188 |
+
def auto_register(game_engine):
|
| 189 |
+
"""Custom registration logic"""
|
| 190 |
+
try:
|
| 191 |
+
addon_instance = MyAddon()
|
| 192 |
+
# Custom registration logic here
|
| 193 |
+
return True
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"Registration failed: {e}")
|
| 196 |
+
return False
|
| 197 |
+
|
| 198 |
+
# Add to game_engine.py auto_register_addons list
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
## 📚 Complete Examples
|
| 204 |
+
|
| 205 |
+
### Example 1: Simple Trader (From Codebase)
|
| 206 |
+
|
| 207 |
+
```python
|
| 208 |
+
"""
|
| 209 |
+
Simple Trader NPC Add-on - Self-contained NPC with automatic registration.
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
import gradio as gr
|
| 213 |
+
from typing import Dict
|
| 214 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class SimpleTraderAddon(NPCAddon):
|
| 218 |
+
"""Self-contained trader NPC that handles its own registration and positioning."""
|
| 219 |
+
|
| 220 |
+
def __init__(self):
|
| 221 |
+
super().__init__()
|
| 222 |
+
self.inventory = {
|
| 223 |
+
"Health Potion": {"price": 50, "stock": 10, "description": "Restores 100 HP"},
|
| 224 |
+
"Magic Scroll": {"price": 150, "stock": 5, "description": "Cast magic spells"},
|
| 225 |
+
"Iron Sword": {"price": 300, "stock": 3, "description": "Sharp iron weapon"},
|
| 226 |
+
"Shield": {"price": 200, "stock": 4, "description": "Protective gear"}
|
| 227 |
+
}
|
| 228 |
+
self.sales_history = []
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def addon_id(self) -> str:
|
| 232 |
+
return "simple_trader"
|
| 233 |
+
|
| 234 |
+
@property
|
| 235 |
+
def addon_name(self) -> str:
|
| 236 |
+
return "🛒 Simple Trader"
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def npc_config(self) -> Dict:
|
| 240 |
+
return {
|
| 241 |
+
'id': 'simple_trader',
|
| 242 |
+
'name': '🛒 Simple Trader',
|
| 243 |
+
'x': 450, 'y': 150,
|
| 244 |
+
'char': '🛒',
|
| 245 |
+
'type': 'addon',
|
| 246 |
+
'personality': 'trader',
|
| 247 |
+
'description': 'A friendly trader selling useful items'
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def ui_tab_name(self) -> str:
|
| 252 |
+
return "🛒 Simple Trader"
|
| 253 |
+
|
| 254 |
+
def get_interface(self) -> gr.Component:
|
| 255 |
+
"""Create the Gradio interface for this addon."""
|
| 256 |
+
with gr.Column() as interface:
|
| 257 |
+
gr.Markdown("### 🛒 Simple Trader Shop")
|
| 258 |
+
gr.Markdown("Browse items and check prices. Use private messages to trade!")
|
| 259 |
+
|
| 260 |
+
# Display inventory
|
| 261 |
+
inventory_data = []
|
| 262 |
+
for item, info in self.inventory.items():
|
| 263 |
+
inventory_data.append([item, f"{info['price']} gold", info['stock'], info['description']])
|
| 264 |
+
|
| 265 |
+
gr.Dataframe(
|
| 266 |
+
headers=["Item", "Price", "Stock", "Description"],
|
| 267 |
+
value=inventory_data,
|
| 268 |
+
interactive=False
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
gr.Markdown("**Commands:** Send private message to Simple Trader")
|
| 272 |
+
gr.Markdown("• `buy <item>` - Purchase an item")
|
| 273 |
+
gr.Markdown("• `inventory` - See available items")
|
| 274 |
+
gr.Markdown("• `help` - Show all commands")
|
| 275 |
+
|
| 276 |
+
return interface
|
| 277 |
+
|
| 278 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 279 |
+
"""Handle commands sent to this NPC."""
|
| 280 |
+
parts = command.strip().lower().split()
|
| 281 |
+
cmd = parts[0] if parts else ""
|
| 282 |
+
|
| 283 |
+
if cmd == "buy" and len(parts) > 1:
|
| 284 |
+
item_name = " ".join(parts[1:]).title()
|
| 285 |
+
return self._handle_buy(player_id, item_name)
|
| 286 |
+
elif cmd == "inventory":
|
| 287 |
+
return self._show_inventory()
|
| 288 |
+
elif cmd == "help":
|
| 289 |
+
return self._show_help()
|
| 290 |
+
else:
|
| 291 |
+
return "🛒 **Simple Trader:** I didn't understand. Try 'buy <item>', 'inventory', or 'help'."
|
| 292 |
+
|
| 293 |
+
def _handle_buy(self, player_id: str, item_name: str) -> str:
|
| 294 |
+
"""Handle item purchase."""
|
| 295 |
+
if item_name not in self.inventory:
|
| 296 |
+
return f"❌ Sorry, I don't have '{item_name}' in stock."
|
| 297 |
+
|
| 298 |
+
item = self.inventory[item_name]
|
| 299 |
+
if item['stock'] <= 0:
|
| 300 |
+
return f"❌ '{item_name}' is out of stock!"
|
| 301 |
+
|
| 302 |
+
# In a real implementation, check player gold and deduct
|
| 303 |
+
item['stock'] -= 1
|
| 304 |
+
self.sales_history.append({'player': player_id, 'item': item_name, 'price': item['price']})
|
| 305 |
+
|
| 306 |
+
return f"✅ **Purchase Successful!**\n📦 You bought: {item_name}\n💰 Price: {item['price']} gold\n📊 Stock remaining: {item['stock']}"
|
| 307 |
+
|
| 308 |
+
def _show_inventory(self) -> str:
|
| 309 |
+
"""Show current inventory."""
|
| 310 |
+
result = "🛒 **Current Inventory:**\n\n"
|
| 311 |
+
for item, info in self.inventory.items():
|
| 312 |
+
result += f"**{item}** - {info['price']} gold (Stock: {info['stock']})\n"
|
| 313 |
+
result += f" ↳ {info['description']}\n\n"
|
| 314 |
+
return result
|
| 315 |
+
|
| 316 |
+
def _show_help(self) -> str:
|
| 317 |
+
"""Show help commands."""
|
| 318 |
+
return """🛒 **Simple Trader Commands:**
|
| 319 |
+
|
| 320 |
+
**buy <item>** - Purchase an item
|
| 321 |
+
**inventory** - See available items and prices
|
| 322 |
+
**help** - Show this help
|
| 323 |
+
|
| 324 |
+
💰 **Available Items:**
|
| 325 |
+
• Health Potion, Magic Scroll, Iron Sword, Shield
|
| 326 |
+
|
| 327 |
+
Example: `buy Health Potion`"""
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# Global instance for automatic registration
|
| 331 |
+
simple_trader_addon = SimpleTraderAddon()
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
### Example 2: Read2Burn Messaging (From Codebase)
|
| 335 |
+
|
| 336 |
+
This example shows a complex addon with state management and service interfaces:
|
| 337 |
+
|
| 338 |
+
```python
|
| 339 |
+
"""
|
| 340 |
+
Read2Burn Mailbox Add-on - Self-destructing secure messaging system.
|
| 341 |
+
"""
|
| 342 |
+
|
| 343 |
+
import time
|
| 344 |
+
import random
|
| 345 |
+
import string
|
| 346 |
+
from typing import Dict, List
|
| 347 |
+
from dataclasses import dataclass
|
| 348 |
+
from abc import ABC, abstractmethod
|
| 349 |
+
import gradio as gr
|
| 350 |
+
|
| 351 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
@dataclass
|
| 355 |
+
class Read2BurnMessage:
|
| 356 |
+
"""Data class for Read2Burn messages."""
|
| 357 |
+
id: str
|
| 358 |
+
creator_id: str
|
| 359 |
+
content: str
|
| 360 |
+
created_at: float
|
| 361 |
+
expires_at: float
|
| 362 |
+
reads_left: int
|
| 363 |
+
burned: bool = False
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class IRead2BurnService(ABC):
|
| 367 |
+
"""Interface for Read2Burn service operations."""
|
| 368 |
+
|
| 369 |
+
@abstractmethod
|
| 370 |
+
def create_message(self, creator_id: str, content: str) -> str: pass
|
| 371 |
+
|
| 372 |
+
@abstractmethod
|
| 373 |
+
def read_message(self, reader_id: str, message_id: str) -> str: pass
|
| 374 |
+
|
| 375 |
+
@abstractmethod
|
| 376 |
+
def list_player_messages(self, player_id: str) -> str: pass
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
class Read2BurnService(IRead2BurnService, NPCAddon):
|
| 380 |
+
"""Service for managing Read2Burn secure messaging."""
|
| 381 |
+
|
| 382 |
+
def __init__(self):
|
| 383 |
+
super().__init__()
|
| 384 |
+
self.messages: Dict[str, Read2BurnMessage] = {}
|
| 385 |
+
self.access_log: List[Dict] = []
|
| 386 |
+
|
| 387 |
+
@property
|
| 388 |
+
def addon_id(self) -> str:
|
| 389 |
+
return "read2burn_mailbox"
|
| 390 |
+
|
| 391 |
+
@property
|
| 392 |
+
def addon_name(self) -> str:
|
| 393 |
+
return "📧 Read2Burn Secure Mailbox"
|
| 394 |
+
|
| 395 |
+
@property
|
| 396 |
+
def npc_config(self) -> Dict:
|
| 397 |
+
return {
|
| 398 |
+
'id': 'read2burn',
|
| 399 |
+
'name': '📧 Read2Burn Service',
|
| 400 |
+
'x': 200, 'y': 100,
|
| 401 |
+
'char': '📧',
|
| 402 |
+
'type': 'service',
|
| 403 |
+
'personality': 'read2burn',
|
| 404 |
+
'description': 'Secure message service - send private messages that auto-delete after reading'
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
def get_interface(self) -> gr.Component:
|
| 408 |
+
"""Return Gradio interface for this add-on"""
|
| 409 |
+
with gr.Column() as interface:
|
| 410 |
+
gr.Markdown("""
|
| 411 |
+
## 📧 Read2Burn Secure Mailbox
|
| 412 |
+
|
| 413 |
+
**Create self-destructing messages that burn after reading!**
|
| 414 |
+
|
| 415 |
+
### Features:
|
| 416 |
+
- 🔥 Messages self-destruct after reading
|
| 417 |
+
- ⏰ 24-hour automatic expiration
|
| 418 |
+
- 🔒 Secure delivery system
|
| 419 |
+
- 📊 Message tracking and history
|
| 420 |
+
""")
|
| 421 |
+
|
| 422 |
+
with gr.Tabs():
|
| 423 |
+
with gr.Tab("📝 Create Message"):
|
| 424 |
+
message_input = gr.Textbox(
|
| 425 |
+
label="Message Content",
|
| 426 |
+
lines=5,
|
| 427 |
+
placeholder="Type your secret message here..."
|
| 428 |
+
)
|
| 429 |
+
create_btn = gr.Button("🔥 Create Burning Message", variant="primary")
|
| 430 |
+
create_output = gr.Textbox(label="Result", lines=3, interactive=False)
|
| 431 |
+
|
| 432 |
+
def create_message_ui(content):
|
| 433 |
+
# In real implementation, get current player ID
|
| 434 |
+
player_id = "current_player" # Simplified
|
| 435 |
+
return self.create_message(player_id, content)
|
| 436 |
+
|
| 437 |
+
create_btn.click(create_message_ui, inputs=[message_input], outputs=[create_output])
|
| 438 |
+
|
| 439 |
+
with gr.Tab("🔓 Read Message"):
|
| 440 |
+
message_id_input = gr.Textbox(label="Message ID", placeholder="Enter 8-character message ID")
|
| 441 |
+
read_btn = gr.Button("📖 Read & Burn Message", variant="secondary")
|
| 442 |
+
read_output = gr.Textbox(label="Message Content", lines=5, interactive=False)
|
| 443 |
+
|
| 444 |
+
def read_message_ui(msg_id):
|
| 445 |
+
player_id = "current_player" # Simplified
|
| 446 |
+
return self.read_message(player_id, msg_id)
|
| 447 |
+
|
| 448 |
+
read_btn.click(read_message_ui, inputs=[message_id_input], outputs=[read_output])
|
| 449 |
+
|
| 450 |
+
with gr.Tab("📋 My Messages"):
|
| 451 |
+
refresh_btn = gr.Button("🔄 Refresh List")
|
| 452 |
+
messages_output = gr.Textbox(label="Your Messages", lines=10, interactive=False)
|
| 453 |
+
|
| 454 |
+
def list_messages_ui():
|
| 455 |
+
player_id = "current_player" # Simplified
|
| 456 |
+
return self.list_player_messages(player_id)
|
| 457 |
+
|
| 458 |
+
refresh_btn.click(list_messages_ui, outputs=[messages_output])
|
| 459 |
+
|
| 460 |
+
gr.Markdown("**💬 Private Message Commands:**")
|
| 461 |
+
gr.Markdown("• `create Your message here` - Create new message")
|
| 462 |
+
gr.Markdown("• `read MESSAGE_ID` - Read message (destroys it!)")
|
| 463 |
+
gr.Markdown("• `list` - Show your created messages")
|
| 464 |
+
gr.Markdown("• `help` - Show command help")
|
| 465 |
+
|
| 466 |
+
return interface
|
| 467 |
+
|
| 468 |
+
def generate_message_id(self) -> str:
|
| 469 |
+
"""Generate a unique message ID."""
|
| 470 |
+
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
| 471 |
+
|
| 472 |
+
def create_message(self, creator_id: str, content: str) -> str:
|
| 473 |
+
"""Create a new self-destructing message."""
|
| 474 |
+
message_id = self.generate_message_id()
|
| 475 |
+
|
| 476 |
+
message = Read2BurnMessage(
|
| 477 |
+
id=message_id,
|
| 478 |
+
creator_id=creator_id,
|
| 479 |
+
content=content, # In production, encrypt this
|
| 480 |
+
created_at=time.time(),
|
| 481 |
+
expires_at=time.time() + (24 * 3600), # 24 hours
|
| 482 |
+
reads_left=1,
|
| 483 |
+
burned=False
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
self.messages[message_id] = message
|
| 487 |
+
|
| 488 |
+
self.access_log.append({
|
| 489 |
+
'action': 'create',
|
| 490 |
+
'message_id': message_id,
|
| 491 |
+
'player_id': creator_id,
|
| 492 |
+
'timestamp': time.time()
|
| 493 |
+
})
|
| 494 |
+
|
| 495 |
+
return f"✅ **Message Created Successfully!**\n\n📝 **Message ID:** `{message_id}`\n🔗 Share this ID with the recipient\n⏰ Expires in 24 hours\n🔥 Burns after 1 read"
|
| 496 |
+
|
| 497 |
+
def read_message(self, reader_id: str, message_id: str) -> str:
|
| 498 |
+
"""Read and burn a message."""
|
| 499 |
+
if message_id not in self.messages:
|
| 500 |
+
return "❌ Message not found or already burned"
|
| 501 |
+
|
| 502 |
+
message = self.messages[message_id]
|
| 503 |
+
|
| 504 |
+
# Check expiry
|
| 505 |
+
if time.time() > message.expires_at:
|
| 506 |
+
del self.messages[message_id]
|
| 507 |
+
return "❌ Message expired and has been burned"
|
| 508 |
+
|
| 509 |
+
# Check if already burned
|
| 510 |
+
if message.burned or message.reads_left <= 0:
|
| 511 |
+
del self.messages[message_id]
|
| 512 |
+
return "❌ Message has already been burned"
|
| 513 |
+
|
| 514 |
+
# Read the message
|
| 515 |
+
content = message.content
|
| 516 |
+
message.reads_left -= 1
|
| 517 |
+
|
| 518 |
+
self.access_log.append({
|
| 519 |
+
'action': 'read',
|
| 520 |
+
'message_id': message_id,
|
| 521 |
+
'player_id': reader_id,
|
| 522 |
+
'timestamp': time.time()
|
| 523 |
+
})
|
| 524 |
+
|
| 525 |
+
# Burn the message after reading
|
| 526 |
+
if message.reads_left <= 0:
|
| 527 |
+
message.burned = True
|
| 528 |
+
del self.messages[message_id]
|
| 529 |
+
return f"🔥 **Message Self-Destructed After Reading**\n\n📖 **Content:** {content}\n\n💨 This message has been permanently destroyed."
|
| 530 |
+
|
| 531 |
+
return f"📖 **Message Content:** {content}\n\n⚠️ Reads remaining: {message.reads_left}"
|
| 532 |
+
|
| 533 |
+
def list_player_messages(self, player_id: str) -> str:
|
| 534 |
+
"""List messages created by a player."""
|
| 535 |
+
player_messages = [msg for msg in self.messages.values() if msg.creator_id == player_id]
|
| 536 |
+
|
| 537 |
+
if not player_messages:
|
| 538 |
+
return "📪 No messages found. Create one with: `create Your message here`"
|
| 539 |
+
|
| 540 |
+
result = "📋 **Your Created Messages:**\n\n"
|
| 541 |
+
for msg in player_messages:
|
| 542 |
+
status = "🔥 Burned" if msg.burned else f"✅ Active ({msg.reads_left} reads left)"
|
| 543 |
+
created_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(msg.created_at))
|
| 544 |
+
expires_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(msg.expires_at))
|
| 545 |
+
|
| 546 |
+
result += f"**ID:** `{msg.id}`\n"
|
| 547 |
+
result += f"**Status:** {status}\n"
|
| 548 |
+
result += f"**Created:** {created_time}\n"
|
| 549 |
+
result += f"**Expires:** {expires_time}\n"
|
| 550 |
+
result += f"**Preview:** {msg.content[:50]}{'...' if len(msg.content) > 50 else ''}\n\n"
|
| 551 |
+
|
| 552 |
+
return result
|
| 553 |
+
|
| 554 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 555 |
+
"""Handle Read2Burn mailbox commands."""
|
| 556 |
+
parts = command.strip().split(' ', 1)
|
| 557 |
+
cmd = parts[0].lower()
|
| 558 |
+
|
| 559 |
+
if cmd == "create" and len(parts) > 1:
|
| 560 |
+
return self.create_message(player_id, parts[1])
|
| 561 |
+
elif cmd == "read" and len(parts) > 1:
|
| 562 |
+
return self.read_message(player_id, parts[1])
|
| 563 |
+
elif cmd == "list":
|
| 564 |
+
return self.list_player_messages(player_id)
|
| 565 |
+
elif cmd == "help":
|
| 566 |
+
return """📚 **Read2Burn Mailbox Commands:**
|
| 567 |
+
|
| 568 |
+
**create** `Your secret message here` - Create new message
|
| 569 |
+
**read** `MESSAGE_ID` - Read message (destroys it!)
|
| 570 |
+
**list** - Show your created messages
|
| 571 |
+
**help** - Show this help
|
| 572 |
+
|
| 573 |
+
🔥 **Features:**
|
| 574 |
+
• Messages self-destruct after reading
|
| 575 |
+
• 24-hour automatic expiration
|
| 576 |
+
• Secure delivery system
|
| 577 |
+
• Anonymous messaging support"""
|
| 578 |
+
else:
|
| 579 |
+
return "❓ Invalid command. Try: `create <message>`, `read <id>`, `list`, or `help`"
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
# Global Read2Burn service instance
|
| 583 |
+
read2burn_service = Read2BurnService()
|
| 584 |
+
```
|
| 585 |
+
|
| 586 |
+
---
|
| 587 |
+
|
| 588 |
+
## 🌐 MCP Integration
|
| 589 |
+
|
| 590 |
+
### MCP-Powered Weather Oracle (From Codebase)
|
| 591 |
+
|
| 592 |
+
This example shows how to integrate external MCP servers:
|
| 593 |
+
|
| 594 |
+
```python
|
| 595 |
+
"""
|
| 596 |
+
Weather Oracle Add-on - MCP-powered weather information system.
|
| 597 |
+
"""
|
| 598 |
+
|
| 599 |
+
import time
|
| 600 |
+
import asyncio
|
| 601 |
+
from typing import Dict, Optional
|
| 602 |
+
import gradio as gr
|
| 603 |
+
from mcp import ClientSession
|
| 604 |
+
from mcp.client.sse import sse_client
|
| 605 |
+
from contextlib import AsyncExitStack
|
| 606 |
+
|
| 607 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
class WeatherOracleService(NPCAddon):
|
| 611 |
+
"""Service for managing Weather Oracle MCP integration."""
|
| 612 |
+
|
| 613 |
+
def __init__(self):
|
| 614 |
+
super().__init__()
|
| 615 |
+
self.connected = False
|
| 616 |
+
self.last_connection_attempt = 0
|
| 617 |
+
self.connection_cooldown = 30 # 30 seconds between connection attempts
|
| 618 |
+
self.server_url = "https://chris4k-weather.hf.space/gradio_api/mcp/sse"
|
| 619 |
+
self.session = None
|
| 620 |
+
self.tools = []
|
| 621 |
+
self.exit_stack = None
|
| 622 |
+
|
| 623 |
+
# Set up event loop for async operations
|
| 624 |
+
try:
|
| 625 |
+
self.loop = asyncio.get_event_loop()
|
| 626 |
+
except RuntimeError:
|
| 627 |
+
self.loop = asyncio.new_event_loop()
|
| 628 |
+
asyncio.set_event_loop(self.loop)
|
| 629 |
+
|
| 630 |
+
@property
|
| 631 |
+
def addon_id(self) -> str:
|
| 632 |
+
return "weather_oracle"
|
| 633 |
+
|
| 634 |
+
@property
|
| 635 |
+
def addon_name(self) -> str:
|
| 636 |
+
return "🌤️ Weather Oracle (MCP)"
|
| 637 |
+
|
| 638 |
+
@property
|
| 639 |
+
def npc_config(self) -> Dict:
|
| 640 |
+
return {
|
| 641 |
+
'id': 'weather_oracle',
|
| 642 |
+
'name': '🌤️ Weather Oracle (MCP)',
|
| 643 |
+
'x': 150, 'y': 300,
|
| 644 |
+
'char': '🌤️',
|
| 645 |
+
'type': 'mcp',
|
| 646 |
+
'description': 'MCP-powered weather information service'
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
@property
|
| 650 |
+
def ui_tab_name(self) -> str:
|
| 651 |
+
return "🌤️ Weather Oracle"
|
| 652 |
+
|
| 653 |
+
def get_interface(self) -> gr.Component:
|
| 654 |
+
"""Return Gradio interface for this add-on"""
|
| 655 |
+
with gr.Column() as interface:
|
| 656 |
+
gr.Markdown("""
|
| 657 |
+
## 🌤️ Weather Oracle
|
| 658 |
+
|
| 659 |
+
*I commune with the spirits of sky and storm to bring you weather wisdom from across the realms!*
|
| 660 |
+
|
| 661 |
+
**Ask me about weather in any city:**
|
| 662 |
+
- Current conditions and temperature
|
| 663 |
+
- Weather forecasts
|
| 664 |
+
- Climate information
|
| 665 |
+
|
| 666 |
+
*Format: "City, Country" (e.g., "Berlin, Germany")*
|
| 667 |
+
""")
|
| 668 |
+
|
| 669 |
+
# Connection status
|
| 670 |
+
connection_status = gr.HTML(
|
| 671 |
+
value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
with gr.Row():
|
| 675 |
+
location_input = gr.Textbox(
|
| 676 |
+
label="🌍 Location",
|
| 677 |
+
placeholder="Enter city, country (e.g., Berlin, Germany)",
|
| 678 |
+
scale=3
|
| 679 |
+
)
|
| 680 |
+
get_weather_btn = gr.Button("🌡️ Consult Weather Spirits", variant="primary", scale=1)
|
| 681 |
+
|
| 682 |
+
weather_output = gr.Textbox(
|
| 683 |
+
label="🌤️ Weather Wisdom",
|
| 684 |
+
lines=8,
|
| 685 |
+
interactive=False,
|
| 686 |
+
placeholder="Enter a location and I will consult the weather spirits..."
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
# Example locations
|
| 690 |
+
gr.Examples(
|
| 691 |
+
examples=[
|
| 692 |
+
["London, UK"],
|
| 693 |
+
["Tokyo, Japan"],
|
| 694 |
+
["New York, USA"],
|
| 695 |
+
["Berlin, Germany"],
|
| 696 |
+
["Sydney, Australia"]
|
| 697 |
+
],
|
| 698 |
+
inputs=[location_input],
|
| 699 |
+
label="🌍 Try These Locations"
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
# Connection controls
|
| 703 |
+
with gr.Row():
|
| 704 |
+
connect_btn = gr.Button("🔗 Connect to MCP", variant="secondary")
|
| 705 |
+
status_btn = gr.Button("📊 Check Status", variant="secondary")
|
| 706 |
+
|
| 707 |
+
def handle_weather_request(location: str):
|
| 708 |
+
if not location.strip():
|
| 709 |
+
return "🌪️ The spirits need to know which realm you seek knowledge about!"
|
| 710 |
+
return self.get_weather(location)
|
| 711 |
+
|
| 712 |
+
def handle_connect():
|
| 713 |
+
result = self.connect_to_mcp()
|
| 714 |
+
status = f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 715 |
+
return result, status
|
| 716 |
+
|
| 717 |
+
def handle_status():
|
| 718 |
+
if self.connected:
|
| 719 |
+
tool_names = [tool.name for tool in self.tools] if self.tools else []
|
| 720 |
+
return f"✅ **Connected to MCP Weather Server**\n\nServer: {self.server_url}\nTools: {', '.join(tool_names) if tool_names else 'None'}"
|
| 721 |
+
else:
|
| 722 |
+
return "❌ **Not Connected**\n\nClick 'Connect to MCP' to establish connection."
|
| 723 |
+
|
| 724 |
+
# Wire up events
|
| 725 |
+
get_weather_btn.click(
|
| 726 |
+
handle_weather_request,
|
| 727 |
+
inputs=[location_input],
|
| 728 |
+
outputs=[weather_output]
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
location_input.submit(
|
| 732 |
+
handle_weather_request,
|
| 733 |
+
inputs=[location_input],
|
| 734 |
+
outputs=[weather_output]
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
connect_btn.click(
|
| 738 |
+
handle_connect,
|
| 739 |
+
outputs=[weather_output, connection_status]
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
status_btn.click(
|
| 743 |
+
handle_status,
|
| 744 |
+
outputs=[weather_output]
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
return interface
|
| 748 |
+
|
| 749 |
+
def connect_to_mcp(self) -> str:
|
| 750 |
+
"""Connect to MCP weather server."""
|
| 751 |
+
current_time = time.time()
|
| 752 |
+
if current_time - self.last_connection_attempt < self.connection_cooldown:
|
| 753 |
+
return "⏳ Please wait before retrying connection..."
|
| 754 |
+
|
| 755 |
+
self.last_connection_attempt = current_time
|
| 756 |
+
|
| 757 |
+
try:
|
| 758 |
+
return self.loop.run_until_complete(self._connect())
|
| 759 |
+
except Exception as e:
|
| 760 |
+
self.connected = False
|
| 761 |
+
return f"❌ Connection failed: {str(e)}"
|
| 762 |
+
|
| 763 |
+
async def _connect(self) -> str:
|
| 764 |
+
"""Async connect to MCP server using SSE."""
|
| 765 |
+
try:
|
| 766 |
+
# Clean up previous connection
|
| 767 |
+
if self.exit_stack:
|
| 768 |
+
await self.exit_stack.aclose()
|
| 769 |
+
|
| 770 |
+
self.exit_stack = AsyncExitStack()
|
| 771 |
+
|
| 772 |
+
# Connect to SSE MCP server
|
| 773 |
+
sse_transport = await self.exit_stack.enter_async_context(
|
| 774 |
+
sse_client(self.server_url)
|
| 775 |
+
)
|
| 776 |
+
read_stream, write_callable = sse_transport
|
| 777 |
+
|
| 778 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 779 |
+
ClientSession(read_stream, write_callable)
|
| 780 |
+
)
|
| 781 |
+
await self.session.initialize()
|
| 782 |
+
|
| 783 |
+
# Get available tools
|
| 784 |
+
response = await self.session.list_tools()
|
| 785 |
+
self.tools = response.tools
|
| 786 |
+
|
| 787 |
+
self.connected = True
|
| 788 |
+
tool_names = [tool.name for tool in self.tools]
|
| 789 |
+
return f"✅ Connected to weather MCP server!\nAvailable tools: {', '.join(tool_names)}"
|
| 790 |
+
|
| 791 |
+
except Exception as e:
|
| 792 |
+
self.connected = False
|
| 793 |
+
return f"❌ Connection failed: {str(e)}"
|
| 794 |
+
|
| 795 |
+
def get_weather(self, location: str) -> str:
|
| 796 |
+
"""Get weather for a location using actual MCP server"""
|
| 797 |
+
if not self.connected:
|
| 798 |
+
# Try to auto-reconnect
|
| 799 |
+
connect_result = self.connect_to_mcp()
|
| 800 |
+
if not self.connected:
|
| 801 |
+
return f"❌ **Not connected to weather spirits**\n\n{connect_result}"
|
| 802 |
+
|
| 803 |
+
try:
|
| 804 |
+
return self.loop.run_until_complete(self._get_weather(location))
|
| 805 |
+
except Exception as e:
|
| 806 |
+
return f"❌ **Weather divination failed**\n\nError: {str(e)}"
|
| 807 |
+
|
| 808 |
+
async def _get_weather(self, location: str) -> str:
|
| 809 |
+
"""Async get weather using MCP."""
|
| 810 |
+
try:
|
| 811 |
+
# Parse location
|
| 812 |
+
if ',' in location:
|
| 813 |
+
city, country = [part.strip() for part in location.split(',', 1)]
|
| 814 |
+
else:
|
| 815 |
+
city = location.strip()
|
| 816 |
+
country = ""
|
| 817 |
+
|
| 818 |
+
# Find the weather tool
|
| 819 |
+
weather_tool = next((tool for tool in self.tools if 'weather' in tool.name.lower()), None)
|
| 820 |
+
if not weather_tool:
|
| 821 |
+
return "❌ Weather tool not found on server"
|
| 822 |
+
|
| 823 |
+
# Call the tool
|
| 824 |
+
params = {"city": city, "country": country}
|
| 825 |
+
result = await self.session.call_tool(weather_tool.name, params)
|
| 826 |
+
|
| 827 |
+
# Extract content properly
|
| 828 |
+
content_text = ""
|
| 829 |
+
if hasattr(result, 'content') and result.content:
|
| 830 |
+
if isinstance(result.content, list):
|
| 831 |
+
for content_item in result.content:
|
| 832 |
+
if hasattr(content_item, 'text'):
|
| 833 |
+
content_text += content_item.text + "\n"
|
| 834 |
+
else:
|
| 835 |
+
content_text += str(content_item) + "\n"
|
| 836 |
+
else:
|
| 837 |
+
content_text = str(result.content)
|
| 838 |
+
|
| 839 |
+
if not content_text.strip():
|
| 840 |
+
return f"❌ No weather data received for {location}"
|
| 841 |
+
|
| 842 |
+
# Format the response
|
| 843 |
+
return f"🌤️ **Weather Oracle reveals the weather for {location}:**\n\n{content_text.strip()}"
|
| 844 |
+
|
| 845 |
+
except Exception as e:
|
| 846 |
+
return f"❌ Weather divination failed: {str(e)}"
|
| 847 |
+
|
| 848 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 849 |
+
"""Handle Weather Oracle commands."""
|
| 850 |
+
cmd = command.strip()
|
| 851 |
+
|
| 852 |
+
if not cmd:
|
| 853 |
+
return ("🌤️ **Weather Oracle whispers:**\n\n"
|
| 854 |
+
"Ask me about the weather in any city!\n"
|
| 855 |
+
"Format: 'City, Country' (e.g., 'Berlin, Germany')")
|
| 856 |
+
|
| 857 |
+
# Treat any non-empty command as a location request
|
| 858 |
+
result = self.get_weather(cmd)
|
| 859 |
+
return f"🌤️ **Weather Oracle reveals:**\n\n{result}"
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
# Custom auto-registration function
|
| 863 |
+
def auto_register(game_engine):
|
| 864 |
+
"""Auto-register the Weather Oracle addon with the game engine."""
|
| 865 |
+
try:
|
| 866 |
+
# Create the weather oracle NPC definition
|
| 867 |
+
weather_oracle_npc = {
|
| 868 |
+
'id': 'weather_oracle_auto',
|
| 869 |
+
'name': '🌤️ Weather Oracle (Auto)',
|
| 870 |
+
'x': 300, 'y': 150,
|
| 871 |
+
'char': '🌤️',
|
| 872 |
+
'type': 'mcp',
|
| 873 |
+
'personality': 'weather_oracle',
|
| 874 |
+
'description': 'Self-contained MCP-powered weather information service'
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
# Register the NPC with the NPC service
|
| 878 |
+
npc_service = game_engine.get_npc_service()
|
| 879 |
+
npc_service.register_npc('weather_oracle_auto', weather_oracle_npc)
|
| 880 |
+
|
| 881 |
+
# Register the addon for handling private message commands
|
| 882 |
+
world = game_engine.get_world()
|
| 883 |
+
if not hasattr(world, 'addon_npcs'):
|
| 884 |
+
world.addon_npcs = {}
|
| 885 |
+
world.addon_npcs['weather_oracle_auto'] = weather_oracle_service
|
| 886 |
+
|
| 887 |
+
print("[WeatherOracleAddon] Auto-registered successfully as self-contained addon")
|
| 888 |
+
return True
|
| 889 |
+
|
| 890 |
+
except Exception as e:
|
| 891 |
+
print(f"[WeatherOracleAddon] Error during auto-registration: {e}")
|
| 892 |
+
return False
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
# Global Weather Oracle service instance
|
| 896 |
+
weather_oracle_service = WeatherOracleService()
|
| 897 |
+
```
|
| 898 |
+
|
| 899 |
+
---
|
| 900 |
+
|
| 901 |
+
## 🔧 Advanced Patterns
|
| 902 |
+
|
| 903 |
+
### State Persistence
|
| 904 |
+
|
| 905 |
+
```python
|
| 906 |
+
import json
|
| 907 |
+
import os
|
| 908 |
+
|
| 909 |
+
class PersistentAddon(NPCAddon):
|
| 910 |
+
"""Example addon with state persistence."""
|
| 911 |
+
|
| 912 |
+
def __init__(self):
|
| 913 |
+
super().__init__()
|
| 914 |
+
self.data_file = f"data/{self.addon_id}_data.json"
|
| 915 |
+
self.load_state()
|
| 916 |
+
|
| 917 |
+
def load_state(self):
|
| 918 |
+
"""Load addon state from file."""
|
| 919 |
+
try:
|
| 920 |
+
if os.path.exists(self.data_file):
|
| 921 |
+
with open(self.data_file, 'r') as f:
|
| 922 |
+
data = json.load(f)
|
| 923 |
+
self.player_data = data.get('players', {})
|
| 924 |
+
self.settings = data.get('settings', {})
|
| 925 |
+
else:
|
| 926 |
+
self.player_data = {}
|
| 927 |
+
self.settings = {}
|
| 928 |
+
except Exception as e:
|
| 929 |
+
print(f"[{self.addon_id}] Failed to load state: {e}")
|
| 930 |
+
self.player_data = {}
|
| 931 |
+
self.settings = {}
|
| 932 |
+
|
| 933 |
+
def save_state(self):
|
| 934 |
+
"""Save addon state to file."""
|
| 935 |
+
try:
|
| 936 |
+
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
|
| 937 |
+
with open(self.data_file, 'w') as f:
|
| 938 |
+
json.dump({
|
| 939 |
+
'players': self.player_data,
|
| 940 |
+
'settings': self.settings
|
| 941 |
+
}, f, indent=2)
|
| 942 |
+
except Exception as e:
|
| 943 |
+
print(f"[{self.addon_id}] Failed to save state: {e}")
|
| 944 |
+
|
| 945 |
+
def on_shutdown(self):
|
| 946 |
+
"""Save state when addon shuts down."""
|
| 947 |
+
self.save_state()
|
| 948 |
+
```
|
| 949 |
+
|
| 950 |
+
### Player Validation Helper
|
| 951 |
+
|
| 952 |
+
```python
|
| 953 |
+
def get_current_player(self, game_world):
|
| 954 |
+
"""Helper to safely get current active player."""
|
| 955 |
+
try:
|
| 956 |
+
current_players = list(game_world.players.keys())
|
| 957 |
+
if not current_players:
|
| 958 |
+
return None, "❌ You must be in the game to use this service!"
|
| 959 |
+
|
| 960 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 961 |
+
player = game_world.players.get(player_id)
|
| 962 |
+
|
| 963 |
+
if not player:
|
| 964 |
+
return None, "❌ Player not found!"
|
| 965 |
+
|
| 966 |
+
return player, None
|
| 967 |
+
except Exception as e:
|
| 968 |
+
return None, f"❌ Error accessing player data: {e}"
|
| 969 |
+
```
|
| 970 |
+
|
| 971 |
+
### Async Operation Wrapper
|
| 972 |
+
|
| 973 |
+
```python
|
| 974 |
+
def run_async_safely(self, async_func, *args, **kwargs):
|
| 975 |
+
"""Safely run async functions in sync context."""
|
| 976 |
+
try:
|
| 977 |
+
return self.loop.run_until_complete(async_func(*args, **kwargs))
|
| 978 |
+
except Exception as e:
|
| 979 |
+
return f"❌ Async operation failed: {e}"
|
| 980 |
+
```
|
| 981 |
+
|
| 982 |
+
### Configuration Management
|
| 983 |
+
|
| 984 |
+
```python
|
| 985 |
+
import yaml
|
| 986 |
+
|
| 987 |
+
class ConfigurableAddon(NPCAddon):
|
| 988 |
+
"""Addon with configuration file support."""
|
| 989 |
+
|
| 990 |
+
def __init__(self):
|
| 991 |
+
super().__init__()
|
| 992 |
+
self.config = self.load_config()
|
| 993 |
+
|
| 994 |
+
def load_config(self):
|
| 995 |
+
"""Load configuration from YAML file."""
|
| 996 |
+
config_file = f"config/{self.addon_id}_config.yaml"
|
| 997 |
+
try:
|
| 998 |
+
if os.path.exists(config_file):
|
| 999 |
+
with open(config_file, 'r') as f:
|
| 1000 |
+
return yaml.safe_load(f)
|
| 1001 |
+
else:
|
| 1002 |
+
return self.get_default_config()
|
| 1003 |
+
except Exception as e:
|
| 1004 |
+
print(f"[{self.addon_id}] Config load failed: {e}")
|
| 1005 |
+
return self.get_default_config()
|
| 1006 |
+
|
| 1007 |
+
def get_default_config(self):
|
| 1008 |
+
"""Return default configuration."""
|
| 1009 |
+
return {
|
| 1010 |
+
'enabled': True,
|
| 1011 |
+
'max_operations': 100,
|
| 1012 |
+
'cooldown_seconds': 30
|
| 1013 |
+
}
|
| 1014 |
+
```
|
| 1015 |
+
|
| 1016 |
+
---
|
| 1017 |
+
|
| 1018 |
+
## 🎯 Best Practices
|
| 1019 |
+
|
| 1020 |
+
### 1. Error Handling
|
| 1021 |
+
|
| 1022 |
+
```python
|
| 1023 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1024 |
+
"""Always wrap command handling in try-catch."""
|
| 1025 |
+
try:
|
| 1026 |
+
# Your command logic here
|
| 1027 |
+
return self.process_command(player_id, command)
|
| 1028 |
+
except Exception as e:
|
| 1029 |
+
print(f"[{self.addon_id}] Command error: {e}")
|
| 1030 |
+
return f"❌ An error occurred. Please try again or contact support."
|
| 1031 |
+
```
|
| 1032 |
+
|
| 1033 |
+
### 2. Input Validation
|
| 1034 |
+
|
| 1035 |
+
```python
|
| 1036 |
+
def validate_input(self, input_value: str, max_length: int = 500) -> tuple[bool, str]:
|
| 1037 |
+
"""Validate user input."""
|
| 1038 |
+
if not input_value or not input_value.strip():
|
| 1039 |
+
return False, "Input cannot be empty"
|
| 1040 |
+
|
| 1041 |
+
if len(input_value) > max_length:
|
| 1042 |
+
return False, f"Input too long (max {max_length} characters)"
|
| 1043 |
+
|
| 1044 |
+
# Add more validation as needed
|
| 1045 |
+
return True, ""
|
| 1046 |
+
```
|
| 1047 |
+
|
| 1048 |
+
### 3. Rate Limiting
|
| 1049 |
+
|
| 1050 |
+
```python
|
| 1051 |
+
import time
|
| 1052 |
+
from collections import defaultdict
|
| 1053 |
+
|
| 1054 |
+
class RateLimitedAddon(NPCAddon):
|
| 1055 |
+
"""Addon with rate limiting."""
|
| 1056 |
+
|
| 1057 |
+
def __init__(self):
|
| 1058 |
+
super().__init__()
|
| 1059 |
+
self.last_command_time = defaultdict(float)
|
| 1060 |
+
self.command_cooldown = 5 # 5 seconds between commands
|
| 1061 |
+
|
| 1062 |
+
def is_rate_limited(self, player_id: str) -> bool:
|
| 1063 |
+
"""Check if player is rate limited."""
|
| 1064 |
+
current_time = time.time()
|
| 1065 |
+
last_time = self.last_command_time[player_id]
|
| 1066 |
+
|
| 1067 |
+
if current_time - last_time < self.command_cooldown:
|
| 1068 |
+
return True
|
| 1069 |
+
|
| 1070 |
+
self.last_command_time[player_id] = current_time
|
| 1071 |
+
return False
|
| 1072 |
+
```
|
| 1073 |
+
|
| 1074 |
+
### 4. Logging
|
| 1075 |
+
|
| 1076 |
+
```python
|
| 1077 |
+
import logging
|
| 1078 |
+
|
| 1079 |
+
class LoggedAddon(NPCAddon):
|
| 1080 |
+
"""Addon with proper logging."""
|
| 1081 |
+
|
| 1082 |
+
def __init__(self):
|
| 1083 |
+
super().__init__()
|
| 1084 |
+
self.logger = logging.getLogger(f"addon_{self.addon_id}")
|
| 1085 |
+
self.logger.setLevel(logging.INFO)
|
| 1086 |
+
|
| 1087 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1088 |
+
self.logger.info(f"Command from {player_id}: {command}")
|
| 1089 |
+
try:
|
| 1090 |
+
result = self.process_command(player_id, command)
|
| 1091 |
+
self.logger.info(f"Command successful for {player_id}")
|
| 1092 |
+
return result
|
| 1093 |
+
except Exception as e:
|
| 1094 |
+
self.logger.error(f"Command failed for {player_id}: {e}")
|
| 1095 |
+
return "❌ Command failed. Please try again."
|
| 1096 |
+
```
|
| 1097 |
+
|
| 1098 |
+
### 5. Resource Cleanup
|
| 1099 |
+
|
| 1100 |
+
```python
|
| 1101 |
+
class ResourceManagedAddon(NPCAddon):
|
| 1102 |
+
"""Addon with proper resource management."""
|
| 1103 |
+
|
| 1104 |
+
def __init__(self):
|
| 1105 |
+
super().__init__()
|
| 1106 |
+
self.resources = [] # Track resources
|
| 1107 |
+
|
| 1108 |
+
def on_shutdown(self):
|
| 1109 |
+
"""Clean up resources on shutdown."""
|
| 1110 |
+
for resource in self.resources:
|
| 1111 |
+
try:
|
| 1112 |
+
if hasattr(resource, 'close'):
|
| 1113 |
+
resource.close()
|
| 1114 |
+
elif hasattr(resource, '__exit__'):
|
| 1115 |
+
resource.__exit__(None, None, None)
|
| 1116 |
+
except Exception as e:
|
| 1117 |
+
print(f"[{self.addon_id}] Cleanup error: {e}")
|
| 1118 |
+
```
|
| 1119 |
+
|
| 1120 |
+
---
|
| 1121 |
+
|
| 1122 |
+
## 🧪 Deployment & Testing
|
| 1123 |
+
|
| 1124 |
+
### Testing Your Addon
|
| 1125 |
+
|
| 1126 |
+
Create a test file for your addon:
|
| 1127 |
+
|
| 1128 |
+
```python
|
| 1129 |
+
# tests/test_my_addon.py
|
| 1130 |
+
import pytest
|
| 1131 |
+
from src.addons.my_addon import MyAddon
|
| 1132 |
+
|
| 1133 |
+
class TestMyAddon:
|
| 1134 |
+
def setup_method(self):
|
| 1135 |
+
"""Set up test fixtures."""
|
| 1136 |
+
self.addon = MyAddon()
|
| 1137 |
+
|
| 1138 |
+
def test_addon_id(self):
|
| 1139 |
+
"""Test addon ID is correct."""
|
| 1140 |
+
assert self.addon.addon_id == "my_addon"
|
| 1141 |
+
|
| 1142 |
+
def test_handle_command_hello(self):
|
| 1143 |
+
"""Test hello command."""
|
| 1144 |
+
result = self.addon.handle_command("test_player", "hello")
|
| 1145 |
+
assert "hello" in result.lower()
|
| 1146 |
+
|
| 1147 |
+
def test_handle_command_invalid(self):
|
| 1148 |
+
"""Test invalid command handling."""
|
| 1149 |
+
result = self.addon.handle_command("test_player", "invalid_command")
|
| 1150 |
+
assert "didn't understand" in result.lower() or "help" in result.lower()
|
| 1151 |
+
```
|
| 1152 |
+
|
| 1153 |
+
### Running Tests
|
| 1154 |
+
|
| 1155 |
+
```bash
|
| 1156 |
+
# Run all addon tests
|
| 1157 |
+
python -m pytest tests/test_*_addon.py -v
|
| 1158 |
+
|
| 1159 |
+
# Run specific addon test
|
| 1160 |
+
python -m pytest tests/test_my_addon.py -v
|
| 1161 |
+
|
| 1162 |
+
# Run with coverage
|
| 1163 |
+
python -m pytest tests/test_*_addon.py --cov=src/addons
|
| 1164 |
+
```
|
| 1165 |
+
|
| 1166 |
+
### Debug Mode
|
| 1167 |
+
|
| 1168 |
+
Add debug functionality to your addon:
|
| 1169 |
+
|
| 1170 |
+
```python
|
| 1171 |
+
class DebuggableAddon(NPCAddon):
|
| 1172 |
+
"""Addon with debug capabilities."""
|
| 1173 |
+
|
| 1174 |
+
def __init__(self):
|
| 1175 |
+
super().__init__()
|
| 1176 |
+
self.debug_mode = os.getenv('ADDON_DEBUG', 'false').lower() == 'true'
|
| 1177 |
+
|
| 1178 |
+
def debug_log(self, message: str):
|
| 1179 |
+
"""Log debug messages."""
|
| 1180 |
+
if self.debug_mode:
|
| 1181 |
+
print(f"[DEBUG:{self.addon_id}] {message}")
|
| 1182 |
+
|
| 1183 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1184 |
+
self.debug_log(f"Received command: {command}")
|
| 1185 |
+
# ... rest of method
|
| 1186 |
+
```
|
| 1187 |
+
|
| 1188 |
+
### Performance Monitoring
|
| 1189 |
+
|
| 1190 |
+
```python
|
| 1191 |
+
import time
|
| 1192 |
+
from functools import wraps
|
| 1193 |
+
|
| 1194 |
+
def monitor_performance(func):
|
| 1195 |
+
"""Decorator to monitor function performance."""
|
| 1196 |
+
@wraps(func)
|
| 1197 |
+
def wrapper(self, *args, **kwargs):
|
| 1198 |
+
start_time = time.time()
|
| 1199 |
+
try:
|
| 1200 |
+
result = func(self, *args, **kwargs)
|
| 1201 |
+
duration = time.time() - start_time
|
| 1202 |
+
if duration > 1.0: # Log slow operations
|
| 1203 |
+
print(f"[{self.addon_id}] Slow operation: {func.__name__} took {duration:.2f}s")
|
| 1204 |
+
return result
|
| 1205 |
+
except Exception as e:
|
| 1206 |
+
duration = time.time() - start_time
|
| 1207 |
+
print(f"[{self.addon_id}] Error in {func.__name__} after {duration:.2f}s: {e}")
|
| 1208 |
+
raise
|
| 1209 |
+
return wrapper
|
| 1210 |
+
|
| 1211 |
+
class MonitoredAddon(NPCAddon):
|
| 1212 |
+
"""Addon with performance monitoring."""
|
| 1213 |
+
|
| 1214 |
+
@monitor_performance
|
| 1215 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1216 |
+
# Your command handling logic
|
| 1217 |
+
pass
|
| 1218 |
+
```
|
| 1219 |
+
|
| 1220 |
+
### Integration Testing
|
| 1221 |
+
|
| 1222 |
+
Test your addon with the game engine:
|
| 1223 |
+
|
| 1224 |
+
```python
|
| 1225 |
+
# tests/test_integration.py
|
| 1226 |
+
import asyncio
|
| 1227 |
+
from src.core.game_engine import GameEngine
|
| 1228 |
+
from src.addons.my_addon import MyAddon
|
| 1229 |
+
|
| 1230 |
+
def test_addon_integration():
|
| 1231 |
+
"""Test addon integrates properly with game engine."""
|
| 1232 |
+
engine = GameEngine()
|
| 1233 |
+
engine.start()
|
| 1234 |
+
|
| 1235 |
+
# Check addon is registered
|
| 1236 |
+
world = engine.get_world()
|
| 1237 |
+
assert hasattr(world, 'addon_npcs')
|
| 1238 |
+
assert 'my_addon' in world.addon_npcs
|
| 1239 |
+
|
| 1240 |
+
# Test NPC exists
|
| 1241 |
+
npc_service = engine.get_npc_service()
|
| 1242 |
+
npc = npc_service.get_npc('my_addon_npc')
|
| 1243 |
+
assert npc is not None
|
| 1244 |
+
|
| 1245 |
+
engine.stop()
|
| 1246 |
+
```
|
| 1247 |
+
|
| 1248 |
+
---
|
| 1249 |
+
|
| 1250 |
+
## 🔧 Troubleshooting
|
| 1251 |
+
|
| 1252 |
+
### Common Issues
|
| 1253 |
+
|
| 1254 |
+
**1. Addon Not Auto-Registering**
|
| 1255 |
+
- Check that you have a global instance at the bottom of your file
|
| 1256 |
+
- Verify `addon_id` is unique and doesn't conflict with others
|
| 1257 |
+
- Ensure you're calling `super().__init__()` in your constructor
|
| 1258 |
+
|
| 1259 |
+
**2. UI Tab Not Appearing**
|
| 1260 |
+
- Check that `ui_tab_name` property returns a string, not None
|
| 1261 |
+
- Verify `get_interface()` returns a valid Gradio component
|
| 1262 |
+
- Look for exceptions in the console when the interface loads
|
| 1263 |
+
|
| 1264 |
+
**3. NPC Not Appearing in World**
|
| 1265 |
+
- Verify `npc_config` property returns a valid dictionary
|
| 1266 |
+
- Check that the NPC position coordinates are within world bounds
|
| 1267 |
+
- Ensure the NPC ID is unique
|
| 1268 |
+
|
| 1269 |
+
**4. Private Messages Not Working**
|
| 1270 |
+
- Confirm `handle_command()` is implemented
|
| 1271 |
+
- Check that the addon is registered in `world.addon_npcs`
|
| 1272 |
+
- Verify the NPC ID matches between world registration and addon registry
|
| 1273 |
+
|
| 1274 |
+
**5. MCP Connection Issues**
|
| 1275 |
+
- Check the MCP server URL is correct and accessible
|
| 1276 |
+
- Verify the server is running and supports the expected tools
|
| 1277 |
+
- Look for async/await issues in your MCP client code
|
| 1278 |
+
- Check firewall and network connectivity
|
| 1279 |
+
|
| 1280 |
+
### Debug Checklist
|
| 1281 |
+
|
| 1282 |
+
- [ ] Addon file imported without errors
|
| 1283 |
+
- [ ] Global instance created at file bottom
|
| 1284 |
+
- [ ] All required methods implemented
|
| 1285 |
+
- [ ] Properties return correct types
|
| 1286 |
+
- [ ] No exceptions in console on startup
|
| 1287 |
+
- [ ] NPC appears in world at specified coordinates
|
| 1288 |
+
- [ ] UI tab appears in interface (if configured)
|
| 1289 |
+
- [ ] Private messages work correctly
|
| 1290 |
+
- [ ] Error handling covers edge cases
|
| 1291 |
+
|
| 1292 |
+
### Getting Help
|
| 1293 |
+
|
| 1294 |
+
1. **Check Logs**: Look for error messages in the console
|
| 1295 |
+
2. **Test Individually**: Create a simple test script for your addon
|
| 1296 |
+
3. **Validate Configuration**: Ensure all properties return expected values
|
| 1297 |
+
4. **Compare Examples**: Look at working addons like SimpleTrader or Read2Burn
|
| 1298 |
+
5. **Community Support**: Ask on project forums or GitHub issues
|
| 1299 |
+
|
| 1300 |
+
---
|
| 1301 |
+
|
| 1302 |
+
## 📚 Additional Resources
|
| 1303 |
+
|
| 1304 |
+
- **Interface Reference**: `src/interfaces/npc_addon.py` - Base class documentation
|
| 1305 |
+
- **Working Examples**: `src/addons/` - All current addon implementations
|
| 1306 |
+
- **Game Engine**: `src/core/game_engine.py` - Auto-registration system
|
| 1307 |
+
- **World Management**: `src/core/world.py` - NPC placement and management
|
| 1308 |
+
- **MCP Documentation**: [Model Context Protocol](https://github.com/modelcontextprotocol/python-sdk)
|
| 1309 |
+
|
| 1310 |
+
---
|
| 1311 |
+
|
| 1312 |
+
*This guide reflects the current addon system architecture and includes real examples from the working codebase. All code examples are tested and functional.*
|
Documentation/NPC_Addon_Development_Guide_OLD.md
ADDED
|
@@ -0,0 +1,1110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Building NPC Add-ons for MMORPG Game
|
| 2 |
+
|
| 3 |
+
This comprehensive guide shows you how to create custom NPC add-ons for the MMORPG game, with examples ranging from simple text-based NPCs to complex MCP-powered services.
|
| 4 |
+
|
| 5 |
+
## Table of Contents
|
| 6 |
+
|
| 7 |
+
1. [Basic NPC Add-on Structure](#basic-npc-add-on-structure)
|
| 8 |
+
2. [Creating a Simple Add-on](#creating-a-simple-add-on)
|
| 9 |
+
3. [Advanced Add-on with State Management](#advanced-add-on-with-state-management)
|
| 10 |
+
4. [MCP-Powered Add-on](#mcp-powered-add-on)
|
| 11 |
+
5. [Integration into the Game](#integration-into-the-game)
|
| 12 |
+
6. [Best Practices](#best-practices)
|
| 13 |
+
7. [Troubleshooting](#troubleshooting)
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Basic NPC Add-on Structure
|
| 18 |
+
|
| 19 |
+
Every NPC add-on must inherit from the `NPCAddon` abstract base class and implement four required methods:
|
| 20 |
+
|
| 21 |
+
```python
|
| 22 |
+
from abc import ABC, abstractmethod
|
| 23 |
+
import gradio as gr
|
| 24 |
+
|
| 25 |
+
class NPCAddon(ABC):
|
| 26 |
+
"""Base class for NPC add-ons"""
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
@abstractmethod
|
| 30 |
+
def addon_id(self) -> str:
|
| 31 |
+
"""Unique identifier for this add-on"""
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
@abstractmethod
|
| 36 |
+
def addon_name(self) -> str:
|
| 37 |
+
"""Display name for this add-on"""
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
@abstractmethod
|
| 41 |
+
def get_interface(self) -> gr.Component:
|
| 42 |
+
"""Return Gradio interface for this add-on"""
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
@abstractmethod
|
| 46 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 47 |
+
"""Handle player commands via private messages"""
|
| 48 |
+
pass
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### Key Requirements
|
| 52 |
+
|
| 53 |
+
1. **Unique ID**: Each add-on needs a unique `addon_id`
|
| 54 |
+
2. **Display Name**: User-friendly name shown in the interface
|
| 55 |
+
3. **Gradio Interface**: Web interface for players to interact with
|
| 56 |
+
4. **Command Handler**: Process commands sent via private messages to the NPC
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## Creating a Simple Add-on
|
| 61 |
+
|
| 62 |
+
Let's create a **Fortune Teller** NPC that gives random predictions:
|
| 63 |
+
|
| 64 |
+
```python
|
| 65 |
+
import random
|
| 66 |
+
import time
|
| 67 |
+
from typing import Dict, List
|
| 68 |
+
|
| 69 |
+
class FortuneTellerAddon(NPCAddon):
|
| 70 |
+
"""Fortune Teller NPC - gives random predictions and wisdom"""
|
| 71 |
+
|
| 72 |
+
def __init__(self):
|
| 73 |
+
self.fortunes = [
|
| 74 |
+
"A great adventure awaits you beyond the eastern mountains!",
|
| 75 |
+
"Beware of strangers bearing gifts in the next full moon.",
|
| 76 |
+
"Your courage will be tested, but victory will be yours.",
|
| 77 |
+
"Gold will come to you through an unexpected friendship.",
|
| 78 |
+
"The stars suggest a powerful ally will join your quest.",
|
| 79 |
+
"A hidden treasure lies where the old oak meets the stream.",
|
| 80 |
+
"Your wisdom will grow through helping others in need.",
|
| 81 |
+
"Trust your instincts - they will guide you true.",
|
| 82 |
+
"A challenge approaches, but it will make you stronger.",
|
| 83 |
+
"The path you seek becomes clear under starlight."
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
self.player_fortunes: Dict[str, Dict] = {} # Track player fortune history
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def addon_id(self) -> str:
|
| 90 |
+
return "fortune_teller"
|
| 91 |
+
|
| 92 |
+
@property
|
| 93 |
+
def addon_name(self) -> str:
|
| 94 |
+
return "🔮 Mystic Fortune Teller"
|
| 95 |
+
|
| 96 |
+
def get_interface(self) -> gr.Component:
|
| 97 |
+
"""Create the Gradio interface for fortune telling"""
|
| 98 |
+
with gr.Column() as interface:
|
| 99 |
+
gr.Markdown("""
|
| 100 |
+
## 🔮 Mystic Fortune Teller
|
| 101 |
+
|
| 102 |
+
*Peer into the mists of time and discover your destiny...*
|
| 103 |
+
|
| 104 |
+
**Services Available:**
|
| 105 |
+
- 🌟 **Daily Fortune** - Receive guidance for your journey
|
| 106 |
+
- 📜 **Fortune History** - View your past predictions
|
| 107 |
+
- 🎲 **Lucky Numbers** - Get your lucky numbers for today
|
| 108 |
+
""")
|
| 109 |
+
|
| 110 |
+
with gr.Row():
|
| 111 |
+
fortune_btn = gr.Button("🔮 Get My Fortune", variant="primary", scale=2)
|
| 112 |
+
history_btn = gr.Button("📜 View History", variant="secondary", scale=1)
|
| 113 |
+
lucky_btn = gr.Button("🎲 Lucky Numbers", variant="secondary", scale=1)
|
| 114 |
+
|
| 115 |
+
fortune_output = gr.Textbox(
|
| 116 |
+
label="🌟 Your Fortune",
|
| 117 |
+
lines=5,
|
| 118 |
+
interactive=False,
|
| 119 |
+
placeholder="Click 'Get My Fortune' to reveal your destiny..."
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def get_player_fortune():
|
| 123 |
+
# Get current player (simplified - in real implementation use proper session management)
|
| 124 |
+
current_players = list(game_world.players.keys())
|
| 125 |
+
if not current_players:
|
| 126 |
+
return "❌ You must be in the game to receive a fortune reading!"
|
| 127 |
+
|
| 128 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 129 |
+
player_name = game_world.players[player_id].name
|
| 130 |
+
|
| 131 |
+
return self.get_daily_fortune(player_id, player_name)
|
| 132 |
+
|
| 133 |
+
def get_player_history():
|
| 134 |
+
current_players = list(game_world.players.keys())
|
| 135 |
+
if not current_players:
|
| 136 |
+
return "❌ You must be in the game to view your fortune history!"
|
| 137 |
+
|
| 138 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 139 |
+
return self.get_fortune_history(player_id)
|
| 140 |
+
|
| 141 |
+
def get_lucky_numbers():
|
| 142 |
+
current_players = list(game_world.players.keys())
|
| 143 |
+
if not current_players:
|
| 144 |
+
return "❌ You must be in the game to receive lucky numbers!"
|
| 145 |
+
|
| 146 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 147 |
+
return self.generate_lucky_numbers(player_id)
|
| 148 |
+
|
| 149 |
+
fortune_btn.click(get_player_fortune, outputs=[fortune_output])
|
| 150 |
+
history_btn.click(get_player_history, outputs=[fortune_output])
|
| 151 |
+
lucky_btn.click(get_lucky_numbers, outputs=[fortune_output])
|
| 152 |
+
|
| 153 |
+
return interface
|
| 154 |
+
|
| 155 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 156 |
+
"""Handle fortune teller commands via private messages"""
|
| 157 |
+
cmd = command.strip().lower()
|
| 158 |
+
player_name = game_world.players.get(player_id, {}).get('name', 'Traveler')
|
| 159 |
+
|
| 160 |
+
if cmd in ['fortune', 'predict', 'future', 'tell']:
|
| 161 |
+
return f"🔮 **Mystic Fortune Teller whispers:**\n\n{self.get_daily_fortune(player_id, player_name)}"
|
| 162 |
+
|
| 163 |
+
elif cmd in ['history', 'past', 'previous']:
|
| 164 |
+
return f"📜 **Your Fortune History:**\n\n{self.get_fortune_history(player_id)}"
|
| 165 |
+
|
| 166 |
+
elif cmd in ['lucky', 'numbers', 'luck']:
|
| 167 |
+
return f"🎲 **Lucky Numbers:**\n\n{self.generate_lucky_numbers(player_id)}"
|
| 168 |
+
|
| 169 |
+
else:
|
| 170 |
+
return ("🔮 **Mystic Fortune Teller:**\n\n"
|
| 171 |
+
"I can help you with:\n"
|
| 172 |
+
"• Say 'fortune' for your daily prediction\n"
|
| 173 |
+
"• Say 'history' to see past fortunes\n"
|
| 174 |
+
"• Say 'lucky' for your lucky numbers")
|
| 175 |
+
|
| 176 |
+
def get_daily_fortune(self, player_id: str, player_name: str) -> str:
|
| 177 |
+
"""Get or generate daily fortune for player"""
|
| 178 |
+
today = time.strftime("%Y-%m-%d")
|
| 179 |
+
|
| 180 |
+
# Check if player already got fortune today
|
| 181 |
+
if player_id in self.player_fortunes:
|
| 182 |
+
player_data = self.player_fortunes[player_id]
|
| 183 |
+
if player_data.get('last_fortune_date') == today:
|
| 184 |
+
return (f"**{player_name}**, I have already revealed your destiny for today:\n\n"
|
| 185 |
+
f"✨ *{player_data['last_fortune']}*\n\n"
|
| 186 |
+
f"Return tomorrow for new guidance...")
|
| 187 |
+
|
| 188 |
+
# Generate new fortune
|
| 189 |
+
fortune = random.choice(self.fortunes)
|
| 190 |
+
|
| 191 |
+
# Store fortune
|
| 192 |
+
if player_id not in self.player_fortunes:
|
| 193 |
+
self.player_fortunes[player_id] = {'history': []}
|
| 194 |
+
|
| 195 |
+
self.player_fortunes[player_id].update({
|
| 196 |
+
'last_fortune': fortune,
|
| 197 |
+
'last_fortune_date': today
|
| 198 |
+
})
|
| 199 |
+
|
| 200 |
+
self.player_fortunes[player_id]['history'].append({
|
| 201 |
+
'date': today,
|
| 202 |
+
'fortune': fortune,
|
| 203 |
+
'timestamp': time.time()
|
| 204 |
+
})
|
| 205 |
+
|
| 206 |
+
# Keep only last 10 fortunes
|
| 207 |
+
if len(self.player_fortunes[player_id]['history']) > 10:
|
| 208 |
+
self.player_fortunes[player_id]['history'] = self.player_fortunes[player_id]['history'][-10:]
|
| 209 |
+
|
| 210 |
+
return (f"**{player_name}**, the crystal ball reveals your destiny...\n\n"
|
| 211 |
+
f"✨ *{fortune}*\n\n"
|
| 212 |
+
f"May this wisdom guide your path, brave adventurer!")
|
| 213 |
+
|
| 214 |
+
def get_fortune_history(self, player_id: str) -> str:
|
| 215 |
+
"""Get player's fortune history"""
|
| 216 |
+
if player_id not in self.player_fortunes or not self.player_fortunes[player_id].get('history'):
|
| 217 |
+
return "The mists show no past... you have not sought guidance before."
|
| 218 |
+
|
| 219 |
+
history = self.player_fortunes[player_id]['history']
|
| 220 |
+
result = "**Your Past Fortunes:**\n\n"
|
| 221 |
+
|
| 222 |
+
for i, entry in enumerate(reversed(history[-5:]), 1): # Last 5 fortunes
|
| 223 |
+
result += f"**{entry['date']}** - {entry['fortune']}\n\n"
|
| 224 |
+
|
| 225 |
+
return result
|
| 226 |
+
|
| 227 |
+
def generate_lucky_numbers(self, player_id: str) -> str:
|
| 228 |
+
"""Generate lucky numbers based on player ID and current date"""
|
| 229 |
+
# Use player ID and date as seed for consistent daily numbers
|
| 230 |
+
seed_str = f"{player_id}_{time.strftime('%Y-%m-%d')}"
|
| 231 |
+
random.seed(hash(seed_str) % (2**32))
|
| 232 |
+
|
| 233 |
+
lucky_nums = sorted(random.sample(range(1, 100), 6))
|
| 234 |
+
|
| 235 |
+
# Reset random seed
|
| 236 |
+
random.seed()
|
| 237 |
+
|
| 238 |
+
numbers_str = " • ".join(map(str, lucky_nums))
|
| 239 |
+
|
| 240 |
+
return (f"🎲 **Your Lucky Numbers for Today:**\n\n"
|
| 241 |
+
f"✨ {numbers_str} ✨\n\n"
|
| 242 |
+
f"These numbers carry special energy today. "
|
| 243 |
+
f"Use them wisely in your adventures!")
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
## Advanced Add-on with State Management
|
| 249 |
+
|
| 250 |
+
Here's a more complex example - a **Blacksmith NPC** that can upgrade weapons and manage an inventory:
|
| 251 |
+
|
| 252 |
+
```python
|
| 253 |
+
import json
|
| 254 |
+
from datetime import datetime, timedelta
|
| 255 |
+
|
| 256 |
+
class BlacksmithAddon(NPCAddon):
|
| 257 |
+
"""Advanced Blacksmith NPC with weapon upgrading and inventory management"""
|
| 258 |
+
|
| 259 |
+
def __init__(self):
|
| 260 |
+
self.weapons_catalog = {
|
| 261 |
+
'iron_sword': {'name': 'Iron Sword', 'base_damage': 10, 'upgrade_cost': 50},
|
| 262 |
+
'steel_sword': {'name': 'Steel Sword', 'base_damage': 15, 'upgrade_cost': 100},
|
| 263 |
+
'silver_sword': {'name': 'Silver Sword', 'base_damage': 20, 'upgrade_cost': 200},
|
| 264 |
+
'mithril_sword': {'name': 'Mithril Sword', 'base_damage': 30, 'upgrade_cost': 500},
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
self.player_weapons: Dict[str, Dict] = {} # player_id -> weapon data
|
| 268 |
+
self.upgrade_queue: List[Dict] = [] # Weapons being upgraded
|
| 269 |
+
self.shop_inventory = self.initialize_shop_inventory()
|
| 270 |
+
|
| 271 |
+
def initialize_shop_inventory(self):
|
| 272 |
+
"""Initialize blacksmith shop inventory"""
|
| 273 |
+
return {
|
| 274 |
+
'iron_ore': {'name': 'Iron Ore', 'price': 25, 'stock': 50},
|
| 275 |
+
'steel_ingot': {'name': 'Steel Ingot', 'price': 75, 'stock': 30},
|
| 276 |
+
'silver_ore': {'name': 'Silver Ore', 'price': 150, 'stock': 20},
|
| 277 |
+
'enchant_stone': {'name': 'Enchantment Stone', 'price': 300, 'stock': 10},
|
| 278 |
+
'repair_kit': {'name': 'Weapon Repair Kit', 'price': 40, 'stock': 25},
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
@property
|
| 282 |
+
def addon_id(self) -> str:
|
| 283 |
+
return "blacksmith"
|
| 284 |
+
|
| 285 |
+
@property
|
| 286 |
+
def addon_name(self) -> str:
|
| 287 |
+
return "⚒️ Master Blacksmith"
|
| 288 |
+
|
| 289 |
+
def get_interface(self) -> gr.Component:
|
| 290 |
+
with gr.Column() as interface:
|
| 291 |
+
gr.Markdown("""
|
| 292 |
+
## ⚒️ Master Blacksmith Forge
|
| 293 |
+
|
| 294 |
+
*Welcome to the finest smithy in the realm!*
|
| 295 |
+
|
| 296 |
+
**Services:**
|
| 297 |
+
- 🗡️ **Weapon Upgrades** - Enhance your weapons for better damage
|
| 298 |
+
- 🛒 **Shop** - Buy materials and repair kits
|
| 299 |
+
- 📦 **Inventory** - View your weapons and materials
|
| 300 |
+
- 🔧 **Repairs** - Fix damaged equipment
|
| 301 |
+
""")
|
| 302 |
+
|
| 303 |
+
with gr.Tabs():
|
| 304 |
+
with gr.Tab("🗡️ Weapon Upgrade"):
|
| 305 |
+
with gr.Row():
|
| 306 |
+
weapon_select = gr.Dropdown(
|
| 307 |
+
choices=list(self.weapons_catalog.keys()),
|
| 308 |
+
label="Select Weapon to Upgrade",
|
| 309 |
+
value="iron_sword"
|
| 310 |
+
)
|
| 311 |
+
upgrade_level = gr.Number(
|
| 312 |
+
label="Upgrade Level (+1 to +10)",
|
| 313 |
+
value=1,
|
| 314 |
+
minimum=1,
|
| 315 |
+
maximum=10
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
upgrade_info = gr.HTML(label="Upgrade Information")
|
| 319 |
+
upgrade_btn = gr.Button("⚒️ Start Upgrade", variant="primary")
|
| 320 |
+
upgrade_result = gr.Textbox(label="Result", lines=5)
|
| 321 |
+
|
| 322 |
+
def calculate_upgrade_info(weapon, level):
|
| 323 |
+
if weapon in self.weapons_catalog:
|
| 324 |
+
weapon_data = self.weapons_catalog[weapon]
|
| 325 |
+
base_cost = weapon_data['upgrade_cost']
|
| 326 |
+
total_cost = base_cost * level * (level + 1) // 2 # Progressive cost
|
| 327 |
+
new_damage = weapon_data['base_damage'] + (level * 2)
|
| 328 |
+
time_required = level * 30 # 30 seconds per level
|
| 329 |
+
|
| 330 |
+
return f"""
|
| 331 |
+
<div style="background: #f0f8ff; padding: 15px; border-radius: 8px; border-left: 4px solid #4682b4;">
|
| 332 |
+
<h4>🗡️ {weapon_data['name']} +{level}</h4>
|
| 333 |
+
<p><strong>Base Damage:</strong> {weapon_data['base_damage']} → {new_damage}</p>
|
| 334 |
+
<p><strong>Upgrade Cost:</strong> {total_cost} gold</p>
|
| 335 |
+
<p><strong>Time Required:</strong> {time_required} seconds</p>
|
| 336 |
+
</div>
|
| 337 |
+
"""
|
| 338 |
+
return "Select a weapon to see upgrade information."
|
| 339 |
+
|
| 340 |
+
weapon_select.change(calculate_upgrade_info, [weapon_select, upgrade_level], upgrade_info)
|
| 341 |
+
upgrade_level.change(calculate_upgrade_info, [weapon_select, upgrade_level], upgrade_info)
|
| 342 |
+
|
| 343 |
+
def handle_upgrade(weapon, level):
|
| 344 |
+
current_players = list(game_world.players.keys())
|
| 345 |
+
if not current_players:
|
| 346 |
+
return "❌ You must be in the game to upgrade weapons!"
|
| 347 |
+
|
| 348 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 349 |
+
return self.start_weapon_upgrade(player_id, weapon, int(level))
|
| 350 |
+
|
| 351 |
+
upgrade_btn.click(handle_upgrade, [weapon_select, upgrade_level], upgrade_result)
|
| 352 |
+
|
| 353 |
+
with gr.Tab("🛒 Shop"):
|
| 354 |
+
shop_display = gr.HTML()
|
| 355 |
+
|
| 356 |
+
with gr.Row():
|
| 357 |
+
item_select = gr.Dropdown(
|
| 358 |
+
choices=list(self.shop_inventory.keys()),
|
| 359 |
+
label="Select Item",
|
| 360 |
+
value=list(self.shop_inventory.keys())[0]
|
| 361 |
+
)
|
| 362 |
+
quantity = gr.Number(label="Quantity", value=1, minimum=1, maximum=99)
|
| 363 |
+
|
| 364 |
+
buy_btn = gr.Button("💰 Purchase", variant="primary")
|
| 365 |
+
purchase_result = gr.Textbox(label="Purchase Result", lines=3)
|
| 366 |
+
|
| 367 |
+
def update_shop_display():
|
| 368 |
+
html = "<div style='background: #f9f9f9; padding: 15px; border-radius: 8px;'>"
|
| 369 |
+
html += "<h4>🛒 Shop Inventory</h4>"
|
| 370 |
+
for item_id, item_data in self.shop_inventory.items():
|
| 371 |
+
html += f"""
|
| 372 |
+
<div style='margin: 10px 0; padding: 10px; background: white; border-radius: 4px;'>
|
| 373 |
+
<strong>{item_data['name']}</strong><br>
|
| 374 |
+
Price: {item_data['price']} gold | Stock: {item_data['stock']}
|
| 375 |
+
</div>
|
| 376 |
+
"""
|
| 377 |
+
html += "</div>"
|
| 378 |
+
return html
|
| 379 |
+
|
| 380 |
+
def handle_purchase(item, qty):
|
| 381 |
+
current_players = list(game_world.players.keys())
|
| 382 |
+
if not current_players:
|
| 383 |
+
return "❌ You must be in the game to make purchases!"
|
| 384 |
+
|
| 385 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 386 |
+
return self.purchase_item(player_id, item, int(qty))
|
| 387 |
+
|
| 388 |
+
buy_btn.click(handle_purchase, [item_select, quantity], purchase_result)
|
| 389 |
+
|
| 390 |
+
# Update shop display on tab load
|
| 391 |
+
shop_display.value = update_shop_display()
|
| 392 |
+
|
| 393 |
+
with gr.Tab("📦 My Weapons"):
|
| 394 |
+
refresh_btn = gr.Button("🔄 Refresh Inventory")
|
| 395 |
+
inventory_display = gr.JSON(label="Your Weapons & Materials")
|
| 396 |
+
|
| 397 |
+
def refresh_inventory():
|
| 398 |
+
current_players = list(game_world.players.keys())
|
| 399 |
+
if not current_players:
|
| 400 |
+
return {"error": "You must be in the game to view inventory!"}
|
| 401 |
+
|
| 402 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 403 |
+
return self.get_player_inventory(player_id)
|
| 404 |
+
|
| 405 |
+
refresh_btn.click(refresh_inventory, outputs=inventory_display)
|
| 406 |
+
|
| 407 |
+
return interface
|
| 408 |
+
|
| 409 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 410 |
+
"""Handle blacksmith commands via private messages"""
|
| 411 |
+
cmd_parts = command.strip().lower().split()
|
| 412 |
+
cmd = cmd_parts[0] if cmd_parts else ""
|
| 413 |
+
|
| 414 |
+
player = game_world.players.get(player_id)
|
| 415 |
+
player_name = player.name if player else "Traveler"
|
| 416 |
+
|
| 417 |
+
if cmd in ['upgrade', 'enhance']:
|
| 418 |
+
if len(cmd_parts) < 2:
|
| 419 |
+
return ("⚒️ **Master Blacksmith:**\n\n"
|
| 420 |
+
"To upgrade a weapon, specify: `upgrade [weapon_name] [level]`\n"
|
| 421 |
+
f"Available weapons: {', '.join(self.weapons_catalog.keys())}")
|
| 422 |
+
|
| 423 |
+
weapon = cmd_parts[1]
|
| 424 |
+
level = int(cmd_parts[2]) if len(cmd_parts) > 2 else 1
|
| 425 |
+
|
| 426 |
+
return self.start_weapon_upgrade(player_id, weapon, level)
|
| 427 |
+
|
| 428 |
+
elif cmd == 'shop':
|
| 429 |
+
return self.format_shop_for_message()
|
| 430 |
+
|
| 431 |
+
elif cmd == 'inventory':
|
| 432 |
+
inventory = self.get_player_inventory(player_id)
|
| 433 |
+
return f"📦 **Your Inventory:**\n\n{json.dumps(inventory, indent=2)}"
|
| 434 |
+
|
| 435 |
+
elif cmd in ['buy', 'purchase']:
|
| 436 |
+
if len(cmd_parts) < 2:
|
| 437 |
+
return "💰 Specify what you want to buy: `buy [item_name] [quantity]`"
|
| 438 |
+
|
| 439 |
+
item = cmd_parts[1]
|
| 440 |
+
quantity = int(cmd_parts[2]) if len(cmd_parts) > 2 else 1
|
| 441 |
+
|
| 442 |
+
return self.purchase_item(player_id, item, quantity)
|
| 443 |
+
|
| 444 |
+
else:
|
| 445 |
+
return ("⚒️ **Master Blacksmith:**\n\n"
|
| 446 |
+
"I can help you with:\n"
|
| 447 |
+
"• `upgrade [weapon] [level]` - Upgrade weapons\n"
|
| 448 |
+
"• `shop` - View available items\n"
|
| 449 |
+
"• `buy [item] [qty]` - Purchase materials\n"
|
| 450 |
+
"• `inventory` - Check your weapons")
|
| 451 |
+
|
| 452 |
+
def start_weapon_upgrade(self, player_id: str, weapon_type: str, level: int) -> str:
|
| 453 |
+
"""Start weapon upgrade process"""
|
| 454 |
+
if weapon_type not in self.weapons_catalog:
|
| 455 |
+
return f"❌ Unknown weapon type: {weapon_type}"
|
| 456 |
+
|
| 457 |
+
if level < 1 or level > 10:
|
| 458 |
+
return "❌ Upgrade level must be between 1 and 10"
|
| 459 |
+
|
| 460 |
+
weapon_data = self.weapons_catalog[weapon_type]
|
| 461 |
+
cost = weapon_data['upgrade_cost'] * level * (level + 1) // 2
|
| 462 |
+
|
| 463 |
+
# Check if player has enough gold (simplified - use game's gold system)
|
| 464 |
+
player = game_world.players.get(player_id)
|
| 465 |
+
if not player or player.gold < cost:
|
| 466 |
+
return f"❌ Insufficient gold! Need {cost} gold for this upgrade."
|
| 467 |
+
|
| 468 |
+
# Deduct gold
|
| 469 |
+
player.gold -= cost
|
| 470 |
+
|
| 471 |
+
# Add to upgrade queue
|
| 472 |
+
upgrade_id = f"upgrade_{int(time.time())}_{player_id}"
|
| 473 |
+
completion_time = time.time() + (level * 30) # 30 seconds per level
|
| 474 |
+
|
| 475 |
+
self.upgrade_queue.append({
|
| 476 |
+
'id': upgrade_id,
|
| 477 |
+
'player_id': player_id,
|
| 478 |
+
'weapon_type': weapon_type,
|
| 479 |
+
'level': level,
|
| 480 |
+
'completion_time': completion_time
|
| 481 |
+
})
|
| 482 |
+
|
| 483 |
+
return (f"⚒️ **Upgrade Started!**\n\n"
|
| 484 |
+
f"Weapon: {weapon_data['name']} +{level}\n"
|
| 485 |
+
f"Cost: {cost} gold (paid)\n"
|
| 486 |
+
f"Completion: {level * 30} seconds\n\n"
|
| 487 |
+
f"Your weapon is now being forged...")
|
| 488 |
+
|
| 489 |
+
def purchase_item(self, player_id: str, item_id: str, quantity: int) -> str:
|
| 490 |
+
"""Handle item purchases"""
|
| 491 |
+
if item_id not in self.shop_inventory:
|
| 492 |
+
return f"❌ Item '{item_id}' not available"
|
| 493 |
+
|
| 494 |
+
item = self.shop_inventory[item_id]
|
| 495 |
+
|
| 496 |
+
if quantity > item['stock']:
|
| 497 |
+
return f"❌ Only {item['stock']} {item['name']} in stock"
|
| 498 |
+
|
| 499 |
+
total_cost = item['price'] * quantity
|
| 500 |
+
player = game_world.players.get(player_id)
|
| 501 |
+
|
| 502 |
+
if not player or player.gold < total_cost:
|
| 503 |
+
return f"❌ Need {total_cost} gold for this purchase"
|
| 504 |
+
|
| 505 |
+
# Process purchase
|
| 506 |
+
player.gold -= total_cost
|
| 507 |
+
item['stock'] -= quantity
|
| 508 |
+
|
| 509 |
+
# Add to player inventory
|
| 510 |
+
if player_id not in self.player_weapons:
|
| 511 |
+
self.player_weapons[player_id] = {'materials': {}}
|
| 512 |
+
|
| 513 |
+
materials = self.player_weapons[player_id].setdefault('materials', {})
|
| 514 |
+
materials[item_id] = materials.get(item_id, 0) + quantity
|
| 515 |
+
|
| 516 |
+
return (f"✅ **Purchase Successful!**\n\n"
|
| 517 |
+
f"Bought: {quantity}x {item['name']}\n"
|
| 518 |
+
f"Cost: {total_cost} gold\n"
|
| 519 |
+
f"Remaining gold: {player.gold}")
|
| 520 |
+
|
| 521 |
+
def get_player_inventory(self, player_id: str) -> Dict:
|
| 522 |
+
"""Get player's weapon and material inventory"""
|
| 523 |
+
if player_id not in self.player_weapons:
|
| 524 |
+
return {"weapons": [], "materials": {}}
|
| 525 |
+
|
| 526 |
+
return self.player_weapons[player_id]
|
| 527 |
+
|
| 528 |
+
def format_shop_for_message(self) -> str:
|
| 529 |
+
"""Format shop inventory for private message"""
|
| 530 |
+
result = "🛒 **Blacksmith Shop**\n\n"
|
| 531 |
+
for item_id, item_data in self.shop_inventory.items():
|
| 532 |
+
result += f"**{item_data['name']}** - {item_data['price']} gold (Stock: {item_data['stock']})\n"
|
| 533 |
+
|
| 534 |
+
result += "\nTo buy: `buy [item_name] [quantity]`"
|
| 535 |
+
return result
|
| 536 |
+
```
|
| 537 |
+
|
| 538 |
+
---
|
| 539 |
+
|
| 540 |
+
## MCP-Powered Add-on
|
| 541 |
+
|
| 542 |
+
Here's how to create an add-on that connects to an external MCP server:
|
| 543 |
+
|
| 544 |
+
```python
|
| 545 |
+
import asyncio
|
| 546 |
+
from mcp import ClientSession
|
| 547 |
+
from mcp.client.sse import sse_client
|
| 548 |
+
from contextlib import AsyncExitStack
|
| 549 |
+
|
| 550 |
+
class WeatherOracleAddon(NPCAddon):
|
| 551 |
+
"""Weather Oracle powered by external MCP weather service"""
|
| 552 |
+
|
| 553 |
+
def __init__(self, mcp_server_url: str = "https://chris4k-weather.hf.space/gradio_api/mcp/sse"):
|
| 554 |
+
self.mcp_server_url = mcp_server_url
|
| 555 |
+
self.mcp_client = None
|
| 556 |
+
self.connected = False
|
| 557 |
+
self.tools = []
|
| 558 |
+
self.exit_stack = None
|
| 559 |
+
|
| 560 |
+
# Set up event loop for async operations
|
| 561 |
+
self.loop = asyncio.new_event_loop()
|
| 562 |
+
asyncio.set_event_loop(self.loop)
|
| 563 |
+
|
| 564 |
+
# Auto-connect on initialization
|
| 565 |
+
self.connect_to_mcp()
|
| 566 |
+
|
| 567 |
+
@property
|
| 568 |
+
def addon_id(self) -> str:
|
| 569 |
+
return "weather_oracle"
|
| 570 |
+
|
| 571 |
+
@property
|
| 572 |
+
def addon_name(self) -> str:
|
| 573 |
+
return "🌤️ Weather Oracle"
|
| 574 |
+
|
| 575 |
+
def connect_to_mcp(self) -> str:
|
| 576 |
+
"""Connect to the MCP weather server"""
|
| 577 |
+
return self.loop.run_until_complete(self._connect())
|
| 578 |
+
|
| 579 |
+
async def _connect(self) -> str:
|
| 580 |
+
try:
|
| 581 |
+
# Clean up previous connection
|
| 582 |
+
if self.exit_stack:
|
| 583 |
+
await self.exit_stack.aclose()
|
| 584 |
+
|
| 585 |
+
self.exit_stack = AsyncExitStack()
|
| 586 |
+
|
| 587 |
+
# Connect to SSE MCP server
|
| 588 |
+
sse_transport = await self.exit_stack.enter_async_context(
|
| 589 |
+
sse_client(self.mcp_server_url)
|
| 590 |
+
)
|
| 591 |
+
read_stream, write_callable = sse_transport
|
| 592 |
+
|
| 593 |
+
self.mcp_client = await self.exit_stack.enter_async_context(
|
| 594 |
+
ClientSession(read_stream, write_callable)
|
| 595 |
+
)
|
| 596 |
+
await self.mcp_client.initialize()
|
| 597 |
+
|
| 598 |
+
# Get available tools
|
| 599 |
+
response = await self.mcp_client.list_tools()
|
| 600 |
+
self.tools = response.tools
|
| 601 |
+
|
| 602 |
+
self.connected = True
|
| 603 |
+
tool_names = [tool.name for tool in self.tools]
|
| 604 |
+
return f"✅ Connected to weather server! Tools: {', '.join(tool_names)}"
|
| 605 |
+
|
| 606 |
+
except Exception as e:
|
| 607 |
+
self.connected = False
|
| 608 |
+
return f"❌ Connection failed: {str(e)}"
|
| 609 |
+
|
| 610 |
+
def get_interface(self) -> gr.Component:
|
| 611 |
+
with gr.Column() as interface:
|
| 612 |
+
gr.Markdown("""
|
| 613 |
+
## 🌤️ Weather Oracle
|
| 614 |
+
|
| 615 |
+
*I commune with the spirits of sky and storm to bring you weather wisdom from across the realms!*
|
| 616 |
+
|
| 617 |
+
**Ask me about weather in any city:**
|
| 618 |
+
- Current conditions and temperature
|
| 619 |
+
- Weather forecasts
|
| 620 |
+
- Climate information
|
| 621 |
+
|
| 622 |
+
*Format: "City, Country" (e.g., "Berlin, Germany")*
|
| 623 |
+
""")
|
| 624 |
+
|
| 625 |
+
# Connection status
|
| 626 |
+
connection_status = gr.HTML(
|
| 627 |
+
value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
with gr.Row():
|
| 631 |
+
location_input = gr.Textbox(
|
| 632 |
+
label="Location",
|
| 633 |
+
placeholder="e.g., Berlin, Germany",
|
| 634 |
+
scale=3
|
| 635 |
+
)
|
| 636 |
+
get_weather_btn = gr.Button("🌡️ Consult Weather Spirits", variant="primary", scale=1)
|
| 637 |
+
|
| 638 |
+
weather_output = gr.Textbox(
|
| 639 |
+
label="🌤️ Weather Wisdom",
|
| 640 |
+
lines=8,
|
| 641 |
+
interactive=False,
|
| 642 |
+
placeholder="Enter a location and I will consult the weather spirits..."
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
# Example locations
|
| 646 |
+
with gr.Row():
|
| 647 |
+
gr.Examples(
|
| 648 |
+
examples=[
|
| 649 |
+
["Berlin, Germany"],
|
| 650 |
+
["Tokyo, Japan"],
|
| 651 |
+
["New York, USA"],
|
| 652 |
+
["London, UK"],
|
| 653 |
+
["Sydney, Australia"],
|
| 654 |
+
["Paris, France"],
|
| 655 |
+
["Moscow, Russia"]
|
| 656 |
+
],
|
| 657 |
+
inputs=[location_input],
|
| 658 |
+
label="🌍 Try These Locations"
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
def handle_weather_request(location: str):
|
| 662 |
+
current_players = list(game_world.players.keys())
|
| 663 |
+
if not current_players:
|
| 664 |
+
return "❌ You must be in the game to consult the weather spirits!"
|
| 665 |
+
|
| 666 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 667 |
+
|
| 668 |
+
return self.get_weather_prediction(location, player_id)
|
| 669 |
+
|
| 670 |
+
get_weather_btn.click(
|
| 671 |
+
handle_weather_request,
|
| 672 |
+
inputs=[location_input],
|
| 673 |
+
outputs=[weather_output]
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
location_input.submit(
|
| 677 |
+
handle_weather_request,
|
| 678 |
+
inputs=[location_input],
|
| 679 |
+
outputs=[weather_output]
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
return interface
|
| 683 |
+
|
| 684 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 685 |
+
"""Handle weather oracle commands via private messages"""
|
| 686 |
+
cmd = command.strip()
|
| 687 |
+
|
| 688 |
+
if not cmd:
|
| 689 |
+
return ("🌤️ **Weather Oracle whispers:**\n\n"
|
| 690 |
+
"Ask me about the weather in any city!\n"
|
| 691 |
+
"Format: 'City, Country' (e.g., 'Berlin, Germany')")
|
| 692 |
+
|
| 693 |
+
# Treat any non-empty command as a location request
|
| 694 |
+
result = self.get_weather_prediction(cmd, player_id)
|
| 695 |
+
return f"🌤️ **Weather Oracle reveals:**\n\n{result}"
|
| 696 |
+
|
| 697 |
+
def get_weather_prediction(self, location: str, player_id: str = None) -> str:
|
| 698 |
+
"""Get weather prediction for location"""
|
| 699 |
+
if not self.connected:
|
| 700 |
+
# Try to reconnect
|
| 701 |
+
connect_result = self.connect_to_mcp()
|
| 702 |
+
if not self.connected:
|
| 703 |
+
return f"🌫️ The weather spirits are silent... Connection lost: {connect_result}"
|
| 704 |
+
|
| 705 |
+
if not location.strip():
|
| 706 |
+
return "🌪️ The spirits need to know which realm you seek knowledge about!"
|
| 707 |
+
|
| 708 |
+
return self.loop.run_until_complete(self._get_weather_async(location, player_id))
|
| 709 |
+
|
| 710 |
+
async def _get_weather_async(self, location: str, player_id: str = None) -> str:
|
| 711 |
+
try:
|
| 712 |
+
# Parse location
|
| 713 |
+
if ',' in location:
|
| 714 |
+
city, country = [part.strip() for part in location.split(',', 1)]
|
| 715 |
+
else:
|
| 716 |
+
city = location.strip()
|
| 717 |
+
country = ""
|
| 718 |
+
|
| 719 |
+
# Find the weather tool
|
| 720 |
+
weather_tool = next((tool for tool in self.tools if 'weather' in tool.name.lower()), None)
|
| 721 |
+
if not weather_tool:
|
| 722 |
+
return "🌫️ The weather spirits have retreated... No weather tools available."
|
| 723 |
+
|
| 724 |
+
# Call the MCP weather tool
|
| 725 |
+
params = {"city": city, "country": country}
|
| 726 |
+
result = await self.mcp_client.call_tool(weather_tool.name, params)
|
| 727 |
+
|
| 728 |
+
# Extract and format the weather data
|
| 729 |
+
content_text = self._extract_content(result)
|
| 730 |
+
|
| 731 |
+
if not content_text:
|
| 732 |
+
return "🌫️ The weather spirits speak in riddles I cannot decipher..."
|
| 733 |
+
|
| 734 |
+
# Try to parse and format the weather data
|
| 735 |
+
formatted_result = self._format_weather_response(content_text, city, country)
|
| 736 |
+
|
| 737 |
+
# Log the interaction
|
| 738 |
+
if player_id and player_id in game_world.players:
|
| 739 |
+
player_name = game_world.players[player_id].name
|
| 740 |
+
print(f"[WEATHER_ORACLE] {player_name} asked about weather in {location}")
|
| 741 |
+
|
| 742 |
+
return formatted_result
|
| 743 |
+
|
| 744 |
+
except Exception as e:
|
| 745 |
+
return f"🌩️ The weather spirits are troubled: {str(e)}"
|
| 746 |
+
|
| 747 |
+
def _extract_content(self, result) -> str:
|
| 748 |
+
"""Extract content from MCP response"""
|
| 749 |
+
content_text = ""
|
| 750 |
+
if hasattr(result, 'content') and result.content:
|
| 751 |
+
if isinstance(result.content, list):
|
| 752 |
+
for content_item in result.content:
|
| 753 |
+
if hasattr(content_item, 'text'):
|
| 754 |
+
content_text += content_item.text
|
| 755 |
+
elif hasattr(content_item, 'content'):
|
| 756 |
+
content_text += str(content_item.content)
|
| 757 |
+
else:
|
| 758 |
+
content_text += str(content_item)
|
| 759 |
+
elif hasattr(result.content, 'text'):
|
| 760 |
+
content_text = result.content.text
|
| 761 |
+
else:
|
| 762 |
+
content_text = str(result.content)
|
| 763 |
+
return content_text
|
| 764 |
+
|
| 765 |
+
def _format_weather_response(self, content_text: str, city: str, country: str) -> str:
|
| 766 |
+
"""Format weather response in a mystical way"""
|
| 767 |
+
try:
|
| 768 |
+
# Try to parse as JSON
|
| 769 |
+
parsed = json.loads(content_text)
|
| 770 |
+
if isinstance(parsed, dict):
|
| 771 |
+
if 'error' in parsed:
|
| 772 |
+
return f"🌫️ The spirits say: {parsed['error']}"
|
| 773 |
+
|
| 774 |
+
# Format with mystical flair
|
| 775 |
+
location_name = parsed.get('location', f"{city}, {country}")
|
| 776 |
+
|
| 777 |
+
if 'current_weather' in parsed:
|
| 778 |
+
weather = parsed['current_weather']
|
| 779 |
+
temp = weather.get('temperature_celsius', 'Unknown')
|
| 780 |
+
conditions = weather.get('weather_description', 'mysterious')
|
| 781 |
+
wind = weather.get('wind_speed_kmh', 'calm')
|
| 782 |
+
humidity = weather.get('humidity_percent', 'balanced')
|
| 783 |
+
|
| 784 |
+
return (f"🌍 **The spirits speak of {location_name}:**\n\n"
|
| 785 |
+
f"🌡️ **Temperature:** {temp}°C - The fire spirits dance at this warmth\n"
|
| 786 |
+
f"🌤️ **Conditions:** {conditions} - The sky spirits reveal their mood\n"
|
| 787 |
+
f"💨 **Wind:** {wind} km/h - The air spirits move with purpose\n"
|
| 788 |
+
f"💧 **Humidity:** {humidity}% - The water spirits' presence\n\n"
|
| 789 |
+
f"*May this knowledge guide your journey, brave traveler!*")
|
| 790 |
+
|
| 791 |
+
elif 'temperature (°C)' in parsed:
|
| 792 |
+
temp = parsed.get('temperature (°C)', 'Unknown')
|
| 793 |
+
weather_code = parsed.get('weather_code', 'Unknown')
|
| 794 |
+
timezone = parsed.get('timezone', 'Unknown')
|
| 795 |
+
local_time = parsed.get('local_time', 'Unknown')
|
| 796 |
+
|
| 797 |
+
return (f"🌍 **The ancient spirits whisper of {location_name}:**\n\n"
|
| 798 |
+
f"🌡️ **Sacred Temperature:** {temp}°C\n"
|
| 799 |
+
f"🌤️ **Weather Rune:** {weather_code}\n"
|
| 800 |
+
f"🕐 **Realm:** {timezone}\n"
|
| 801 |
+
f"🕒 **Current Time:** {local_time}\n\n"
|
| 802 |
+
f"*The weather spirits have spoken!*")
|
| 803 |
+
|
| 804 |
+
else:
|
| 805 |
+
return f"✨ **Weather spirits reveal:**\n```\n{json.dumps(parsed, indent=2)}\n```"
|
| 806 |
+
|
| 807 |
+
except json.JSONDecodeError:
|
| 808 |
+
# If not JSON, format as mystical text
|
| 809 |
+
return f"🌟 **The weather spirits whisper:**\n\n{content_text}\n\n*Such is their ancient wisdom...*"
|
| 810 |
+
|
| 811 |
+
return f"🌫️ The spirits speak in riddles: {content_text}"
|
| 812 |
+
```
|
| 813 |
+
|
| 814 |
+
---
|
| 815 |
+
|
| 816 |
+
## Integration into the Game
|
| 817 |
+
|
| 818 |
+
To integrate your new add-on into the game, follow these steps:
|
| 819 |
+
|
| 820 |
+
### 1. Register the Add-on
|
| 821 |
+
|
| 822 |
+
In your main game file (`app.py`), add your add-on to the `create_mmorpg_interface()` function:
|
| 823 |
+
|
| 824 |
+
```python
|
| 825 |
+
def create_mmorpg_interface():
|
| 826 |
+
"""Create the complete MMORPG interface with all features"""
|
| 827 |
+
|
| 828 |
+
# Initialize add-ons
|
| 829 |
+
read2burn_addon = Read2BurnMailboxAddon()
|
| 830 |
+
game_world.addon_npcs['read2burn_mailbox'] = read2burn_addon
|
| 831 |
+
|
| 832 |
+
# Add your new add-ons here
|
| 833 |
+
fortune_teller_addon = FortuneTellerAddon()
|
| 834 |
+
game_world.addon_npcs['fortune_teller'] = fortune_teller_addon
|
| 835 |
+
|
| 836 |
+
blacksmith_addon = BlacksmithAddon()
|
| 837 |
+
game_world.addon_npcs['blacksmith'] = blacksmith_addon
|
| 838 |
+
|
| 839 |
+
weather_oracle_addon = WeatherOracleAddon()
|
| 840 |
+
game_world.addon_npcs['weather_oracle'] = weather_oracle_addon
|
| 841 |
+
|
| 842 |
+
# ... rest of the interface code
|
| 843 |
+
```
|
| 844 |
+
|
| 845 |
+
### 2. Add NPC to the World
|
| 846 |
+
|
| 847 |
+
Add your NPC to the game world's NPC dictionary:
|
| 848 |
+
|
| 849 |
+
```python
|
| 850 |
+
def __init__(self):
|
| 851 |
+
self.npcs: Dict[str, Dict] = {
|
| 852 |
+
# ... existing NPCs ...
|
| 853 |
+
|
| 854 |
+
'fortune_teller': {
|
| 855 |
+
'id': 'fortune_teller',
|
| 856 |
+
'name': 'Mystic Zelda',
|
| 857 |
+
'x': 125, 'y': 75,
|
| 858 |
+
'char': '🔮',
|
| 859 |
+
'type': 'addon'
|
| 860 |
+
},
|
| 861 |
+
'blacksmith': {
|
| 862 |
+
'id': 'blacksmith',
|
| 863 |
+
'name': 'Master Forge',
|
| 864 |
+
'x': 400, 'y': 300,
|
| 865 |
+
'char': '⚒️',
|
| 866 |
+
'type': 'addon'
|
| 867 |
+
},
|
| 868 |
+
# weather_oracle already exists
|
| 869 |
+
}
|
| 870 |
+
```
|
| 871 |
+
|
| 872 |
+
### 3. Add Interface Tab
|
| 873 |
+
|
| 874 |
+
Add your add-on's interface to the NPC Add-ons tab:
|
| 875 |
+
|
| 876 |
+
```python
|
| 877 |
+
with gr.Tab("🤖 NPC Add-ons"):
|
| 878 |
+
with gr.Tabs() as addon_tabs:
|
| 879 |
+
# Existing add-ons
|
| 880 |
+
with gr.Tab("🔥 Read2Burn Mailbox"):
|
| 881 |
+
read2burn_addon.get_interface()
|
| 882 |
+
|
| 883 |
+
with gr.Tab("🌤️ Weather Oracle"):
|
| 884 |
+
weather_oracle_addon.get_interface()
|
| 885 |
+
|
| 886 |
+
# Add your new add-ons here
|
| 887 |
+
with gr.Tab("🔮 Fortune Teller"):
|
| 888 |
+
fortune_teller_addon.get_interface()
|
| 889 |
+
|
| 890 |
+
with gr.Tab("⚒️ Blacksmith"):
|
| 891 |
+
blacksmith_addon.get_interface()
|
| 892 |
+
```
|
| 893 |
+
|
| 894 |
+
---
|
| 895 |
+
|
| 896 |
+
## Best Practices
|
| 897 |
+
|
| 898 |
+
### 1. Error Handling
|
| 899 |
+
|
| 900 |
+
Always include proper error handling in your add-ons:
|
| 901 |
+
|
| 902 |
+
```python
|
| 903 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 904 |
+
try:
|
| 905 |
+
# Your command logic here
|
| 906 |
+
return self.process_command(player_id, command)
|
| 907 |
+
except Exception as e:
|
| 908 |
+
print(f"[{self.addon_id}] Error: {e}")
|
| 909 |
+
return f"⚠️ Something went wrong: {str(e)}"
|
| 910 |
+
```
|
| 911 |
+
|
| 912 |
+
### 2. Player Validation
|
| 913 |
+
|
| 914 |
+
Always validate that players exist before processing commands:
|
| 915 |
+
|
| 916 |
+
```python
|
| 917 |
+
def get_current_player(self):
|
| 918 |
+
"""Get the current active player safely"""
|
| 919 |
+
current_players = list(game_world.players.keys())
|
| 920 |
+
if not current_players:
|
| 921 |
+
return None, "❌ You must be in the game to use this service!"
|
| 922 |
+
|
| 923 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 924 |
+
player = game_world.players.get(player_id)
|
| 925 |
+
|
| 926 |
+
if not player:
|
| 927 |
+
return None, "❌ Player not found!"
|
| 928 |
+
|
| 929 |
+
return player, None
|
| 930 |
+
```
|
| 931 |
+
|
| 932 |
+
### 3. State Persistence
|
| 933 |
+
|
| 934 |
+
For add-ons that maintain state, consider implementing save/load functionality:
|
| 935 |
+
|
| 936 |
+
```python
|
| 937 |
+
import json
|
| 938 |
+
import os
|
| 939 |
+
|
| 940 |
+
class StatefulAddon(NPCAddon):
|
| 941 |
+
def __init__(self):
|
| 942 |
+
self.state_file = f"addon_state_{self.addon_id}.json"
|
| 943 |
+
self.load_state()
|
| 944 |
+
|
| 945 |
+
def save_state(self):
|
| 946 |
+
"""Save add-on state to file"""
|
| 947 |
+
try:
|
| 948 |
+
state_data = {
|
| 949 |
+
'player_data': self.player_data,
|
| 950 |
+
'global_settings': self.global_settings,
|
| 951 |
+
'last_saved': time.time()
|
| 952 |
+
}
|
| 953 |
+
with open(self.state_file, 'w') as f:
|
| 954 |
+
json.dump(state_data, f, indent=2)
|
| 955 |
+
except Exception as e:
|
| 956 |
+
print(f"[{self.addon_id}] Failed to save state: {e}")
|
| 957 |
+
|
| 958 |
+
def load_state(self):
|
| 959 |
+
"""Load add-on state from file"""
|
| 960 |
+
try:
|
| 961 |
+
if os.path.exists(self.state_file):
|
| 962 |
+
with open(self.state_file, 'r') as f:
|
| 963 |
+
state_data = json.load(f)
|
| 964 |
+
self.player_data = state_data.get('player_data', {})
|
| 965 |
+
self.global_settings = state_data.get('global_settings', {})
|
| 966 |
+
else:
|
| 967 |
+
self.player_data = {}
|
| 968 |
+
self.global_settings = {}
|
| 969 |
+
except Exception as e:
|
| 970 |
+
print(f"[{self.addon_id}] Failed to load state: {e}")
|
| 971 |
+
self.player_data = {}
|
| 972 |
+
self.global_settings = {}
|
| 973 |
+
```
|
| 974 |
+
|
| 975 |
+
### 4. Async Operations
|
| 976 |
+
|
| 977 |
+
For add-ons that need to perform async operations (like MCP calls), use proper async handling:
|
| 978 |
+
|
| 979 |
+
```python
|
| 980 |
+
def sync_wrapper_for_async_function(self, *args, **kwargs):
|
| 981 |
+
"""Wrapper to run async functions in sync context"""
|
| 982 |
+
if not hasattr(self, 'loop') or self.loop.is_closed():
|
| 983 |
+
self.loop = asyncio.new_event_loop()
|
| 984 |
+
asyncio.set_event_loop(self.loop)
|
| 985 |
+
|
| 986 |
+
return self.loop.run_until_complete(self.async_function(*args, **kwargs))
|
| 987 |
+
```
|
| 988 |
+
|
| 989 |
+
### 5. Resource Management
|
| 990 |
+
|
| 991 |
+
Clean up resources properly:
|
| 992 |
+
|
| 993 |
+
```python
|
| 994 |
+
def __del__(self):
|
| 995 |
+
"""Cleanup when add-on is destroyed"""
|
| 996 |
+
if hasattr(self, 'exit_stack') and self.exit_stack:
|
| 997 |
+
try:
|
| 998 |
+
asyncio.run(self.exit_stack.aclose())
|
| 999 |
+
except:
|
| 1000 |
+
pass
|
| 1001 |
+
|
| 1002 |
+
self.save_state() # Save state before destruction
|
| 1003 |
+
```
|
| 1004 |
+
|
| 1005 |
+
---
|
| 1006 |
+
|
| 1007 |
+
## Troubleshooting
|
| 1008 |
+
|
| 1009 |
+
### Common Issues
|
| 1010 |
+
|
| 1011 |
+
1. **Add-on not appearing in interface**
|
| 1012 |
+
- Check that you registered it in `game_world.addon_npcs`
|
| 1013 |
+
- Verify the addon_id matches the NPC ID
|
| 1014 |
+
- Ensure no exceptions in `get_interface()`
|
| 1015 |
+
|
| 1016 |
+
2. **Private messages not working**
|
| 1017 |
+
- Verify `handle_command()` is implemented
|
| 1018 |
+
- Check that the NPC ID exists in `game_world.npcs`
|
| 1019 |
+
- Make sure the addon is registered correctly
|
| 1020 |
+
|
| 1021 |
+
3. **MCP connection issues**
|
| 1022 |
+
- Check the MCP server URL is correct
|
| 1023 |
+
- Verify the server is running and accessible
|
| 1024 |
+
- Look for async/await issues in your code
|
| 1025 |
+
|
| 1026 |
+
4. **State not persisting**
|
| 1027 |
+
- Implement proper save/load methods
|
| 1028 |
+
- Handle file permissions issues
|
| 1029 |
+
- Check for JSON serialization errors
|
| 1030 |
+
|
| 1031 |
+
### Debug Tips
|
| 1032 |
+
|
| 1033 |
+
1. **Add logging** to your add-ons:
|
| 1034 |
+
```python
|
| 1035 |
+
import logging
|
| 1036 |
+
|
| 1037 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 1038 |
+
logger = logging.getLogger(f"addon_{self.addon_id}")
|
| 1039 |
+
|
| 1040 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1041 |
+
logger.debug(f"Received command '{command}' from player {player_id}")
|
| 1042 |
+
# ... rest of method
|
| 1043 |
+
```
|
| 1044 |
+
|
| 1045 |
+
2. **Test your add-on standalone**:
|
| 1046 |
+
```python
|
| 1047 |
+
if __name__ == "__main__":
|
| 1048 |
+
# Test your add-on independently
|
| 1049 |
+
addon = YourAddon()
|
| 1050 |
+
test_result = addon.handle_command("test_player", "test command")
|
| 1051 |
+
print(f"Test result: {test_result}")
|
| 1052 |
+
```
|
| 1053 |
+
|
| 1054 |
+
3. **Use the browser console** to debug Gradio interface issues
|
| 1055 |
+
|
| 1056 |
+
4. **Check the game's world events** for proximity detection issues
|
| 1057 |
+
|
| 1058 |
+
---
|
| 1059 |
+
|
| 1060 |
+
## Advanced Features
|
| 1061 |
+
|
| 1062 |
+
### Dynamic Add-on Loading
|
| 1063 |
+
|
| 1064 |
+
You can implement dynamic add-on loading for hot-swapping add-ons:
|
| 1065 |
+
|
| 1066 |
+
```python
|
| 1067 |
+
import importlib
|
| 1068 |
+
import os
|
| 1069 |
+
|
| 1070 |
+
def load_addon_from_file(filepath: str, addon_class_name: str):
|
| 1071 |
+
"""Dynamically load an add-on from a Python file"""
|
| 1072 |
+
try:
|
| 1073 |
+
spec = importlib.util.spec_from_file_location("dynamic_addon", filepath)
|
| 1074 |
+
module = importlib.util.module_from_spec(spec)
|
| 1075 |
+
spec.loader.exec_module(module)
|
| 1076 |
+
|
| 1077 |
+
addon_class = getattr(module, addon_class_name)
|
| 1078 |
+
return addon_class()
|
| 1079 |
+
except Exception as e:
|
| 1080 |
+
print(f"Failed to load addon from {filepath}: {e}")
|
| 1081 |
+
return None
|
| 1082 |
+
```
|
| 1083 |
+
|
| 1084 |
+
### Configuration Files
|
| 1085 |
+
|
| 1086 |
+
Use configuration files for add-on settings:
|
| 1087 |
+
|
| 1088 |
+
```python
|
| 1089 |
+
import yaml
|
| 1090 |
+
|
| 1091 |
+
class ConfigurableAddon(NPCAddon):
|
| 1092 |
+
def __init__(self, config_file: str = None):
|
| 1093 |
+
self.config = self.load_config(config_file or f"{self.addon_id}_config.yaml")
|
| 1094 |
+
|
| 1095 |
+
def load_config(self, config_file: str):
|
| 1096 |
+
try:
|
| 1097 |
+
with open(config_file, 'r') as f:
|
| 1098 |
+
return yaml.safe_load(f)
|
| 1099 |
+
except:
|
| 1100 |
+
return self.get_default_config()
|
| 1101 |
+
|
| 1102 |
+
def get_default_config(self):
|
| 1103 |
+
return {
|
| 1104 |
+
'enabled': True,
|
| 1105 |
+
'max_operations_per_player': 10,
|
| 1106 |
+
'cooldown_seconds': 30
|
| 1107 |
+
}
|
| 1108 |
+
```
|
| 1109 |
+
|
| 1110 |
+
This comprehensive guide should give you everything you need to create powerful, feature-rich NPC add-ons for the MMORPG game!
|
Documentation/NPC_Addon_Development_Guide_Updated.md
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛠️ NPC Addon Development Guide - Complete & Updated
|
| 2 |
+
|
| 3 |
+
> **Updated for Current Architecture** - This guide reflects the latest addon system with auto-registration, modern patterns, and actual working examples from the codebase.
|
| 4 |
+
|
| 5 |
+
## 📋 Table of Contents
|
| 6 |
+
|
| 7 |
+
1. [Quick Start](#quick-start)
|
| 8 |
+
2. [Modern Addon Architecture](#modern-addon-architecture)
|
| 9 |
+
3. [Auto-Registration System](#auto-registration-system)
|
| 10 |
+
4. [Complete Examples](#complete-examples)
|
| 11 |
+
5. [Advanced Patterns](#advanced-patterns)
|
| 12 |
+
6. [MCP Integration](#mcp-integration)
|
| 13 |
+
7. [Best Practices](#best-practices)
|
| 14 |
+
8. [Deployment & Testing](#deployment--testing)
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 🚀 Quick Start
|
| 19 |
+
|
| 20 |
+
### Creating Your First Addon
|
| 21 |
+
|
| 22 |
+
The modern addon system uses **auto-registration** - no manual edits to other files required!
|
| 23 |
+
|
| 24 |
+
```python
|
| 25 |
+
# src/addons/my_first_addon.py
|
| 26 |
+
"""
|
| 27 |
+
My First Addon - A simple self-contained NPC addon.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import gradio as gr
|
| 31 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class MyFirstAddon(NPCAddon):
|
| 35 |
+
"""A simple greeting NPC that demonstrates the addon system."""
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
super().__init__() # This auto-registers the addon!
|
| 39 |
+
self.greeting_count = 0
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def addon_id(self) -> str:
|
| 43 |
+
"""Unique identifier - must be unique across all addons"""
|
| 44 |
+
return "my_first_addon"
|
| 45 |
+
|
| 46 |
+
@property
|
| 47 |
+
def addon_name(self) -> str:
|
| 48 |
+
"""Display name shown in UI"""
|
| 49 |
+
return "🤝 My First Addon"
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def npc_config(self) -> Dict[str, Any]:
|
| 53 |
+
"""NPC configuration for auto-placement in world"""
|
| 54 |
+
return {
|
| 55 |
+
'id': 'my_first_npc',
|
| 56 |
+
'name': '🤝 Friendly Greeter',
|
| 57 |
+
'x': 100, 'y': 100, # Position in game world
|
| 58 |
+
'char': '🤝', # Character emoji
|
| 59 |
+
'type': 'addon', # NPC type
|
| 60 |
+
'description': 'A friendly NPC that greets players'
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def ui_tab_name(self) -> str:
|
| 65 |
+
"""UI tab name - returns None if no UI tab needed"""
|
| 66 |
+
return "🤝 Greeter"
|
| 67 |
+
|
| 68 |
+
def get_interface(self) -> gr.Component:
|
| 69 |
+
"""Create the Gradio interface for this addon"""
|
| 70 |
+
with gr.Column() as interface:
|
| 71 |
+
gr.Markdown("## 🤝 Friendly Greeter\n*Hello! I'm here to greet players.*")
|
| 72 |
+
|
| 73 |
+
greeting_btn = gr.Button("👋 Get Greeting", variant="primary")
|
| 74 |
+
greeting_output = gr.Textbox(label="Greeting", lines=3, interactive=False)
|
| 75 |
+
|
| 76 |
+
def get_greeting():
|
| 77 |
+
self.greeting_count += 1
|
| 78 |
+
return f"Hello! This is greeting #{self.greeting_count}. Welcome to the game!"
|
| 79 |
+
|
| 80 |
+
greeting_btn.click(get_greeting, outputs=[greeting_output])
|
| 81 |
+
|
| 82 |
+
return interface
|
| 83 |
+
|
| 84 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 85 |
+
"""Handle commands sent via private messages to this NPC"""
|
| 86 |
+
cmd = command.lower().strip()
|
| 87 |
+
|
| 88 |
+
if cmd in ['hello', 'hi', 'greet']:
|
| 89 |
+
self.greeting_count += 1
|
| 90 |
+
return f"🤝 **Friendly Greeter says:**\nHello! Greeting #{self.greeting_count}!"
|
| 91 |
+
elif cmd == 'help':
|
| 92 |
+
return "🤝 **Available commands:**\n• hello/hi/greet - Get a greeting\n• help - Show this help"
|
| 93 |
+
else:
|
| 94 |
+
return "🤝 **Friendly Greeter:**\nI didn't understand that. Try 'hello' or 'help'!"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# Global instance - this triggers auto-registration!
|
| 98 |
+
my_first_addon = MyFirstAddon()
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
**That's it!** Your addon is now fully functional and auto-registered. No other files need to be modified.
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 🏗️ Modern Addon Architecture
|
| 106 |
+
|
| 107 |
+
### Base Class Structure
|
| 108 |
+
|
| 109 |
+
All addons inherit from `NPCAddon` which provides:
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
class NPCAddon(ABC):
|
| 113 |
+
"""Base class for NPC add-ons with auto-registration support"""
|
| 114 |
+
|
| 115 |
+
def __init__(self):
|
| 116 |
+
"""Initialize and auto-register the addon"""
|
| 117 |
+
# Auto-register this addon when instantiated
|
| 118 |
+
_addon_registry[self.addon_id] = self
|
| 119 |
+
|
| 120 |
+
# Required Properties
|
| 121 |
+
@property
|
| 122 |
+
@abstractmethod
|
| 123 |
+
def addon_id(self) -> str: pass
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
@abstractmethod
|
| 127 |
+
def addon_name(self) -> str: pass
|
| 128 |
+
|
| 129 |
+
# Optional Properties
|
| 130 |
+
@property
|
| 131 |
+
def npc_config(self) -> Optional[Dict[str, Any]]: return None
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def ui_tab_name(self) -> Optional[str]: return None
|
| 135 |
+
|
| 136 |
+
# Required Methods
|
| 137 |
+
@abstractmethod
|
| 138 |
+
def get_interface(self) -> gr.Component: pass
|
| 139 |
+
|
| 140 |
+
@abstractmethod
|
| 141 |
+
def handle_command(self, player_id: str, command: str) -> str: pass
|
| 142 |
+
|
| 143 |
+
# Optional Lifecycle Methods
|
| 144 |
+
def on_startup(self): pass
|
| 145 |
+
def on_shutdown(self): pass
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Core Concepts
|
| 149 |
+
|
| 150 |
+
1. **Auto-Registration**: Simply instantiating your addon class registers it globally
|
| 151 |
+
2. **Self-Contained**: Everything in one file - NPC config, UI, logic
|
| 152 |
+
3. **Optional Components**: Only implement what you need (UI tab, world NPC, etc.)
|
| 153 |
+
4. **Service Pattern**: Use interfaces for complex functionality
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
## 🔄 Auto-Registration System
|
| 158 |
+
|
| 159 |
+
### How It Works
|
| 160 |
+
|
| 161 |
+
1. **Create addon class** that inherits from `NPCAddon`
|
| 162 |
+
2. **Instantiate globally** at the bottom of your addon file
|
| 163 |
+
3. **Auto-discovery** by the game engine at startup
|
| 164 |
+
4. **Automatic registration** of NPCs and UI components
|
| 165 |
+
|
| 166 |
+
### Current Auto-Registration List
|
| 167 |
+
|
| 168 |
+
The game engine automatically loads these addons:
|
| 169 |
+
|
| 170 |
+
```python
|
| 171 |
+
# src/core/game_engine.py
|
| 172 |
+
auto_register_addons = [
|
| 173 |
+
('weather_oracle_addon', 'auto_register'),
|
| 174 |
+
# Add your addon here if using custom auto_register function
|
| 175 |
+
]
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### Two Registration Patterns
|
| 179 |
+
|
| 180 |
+
#### Pattern 1: Global Instance (Recommended)
|
| 181 |
+
```python
|
| 182 |
+
# At bottom of your addon file
|
| 183 |
+
my_addon = MyAddon() # Auto-registers via __init__
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
#### Pattern 2: Custom Auto-Register Function
|
| 187 |
+
```python
|
| 188 |
+
def auto_register(game_engine):
|
| 189 |
+
"""Custom registration logic"""
|
| 190 |
+
try:
|
| 191 |
+
addon_instance = MyAddon()
|
| 192 |
+
# Custom registration logic here
|
| 193 |
+
return True
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"Registration failed: {e}")
|
| 196 |
+
return False
|
| 197 |
+
|
| 198 |
+
# Add to game_engine.py auto_register_addons list
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
## 📚 Complete Examples
|
| 204 |
+
|
| 205 |
+
### Example 1: Simple Trader (From Codebase)
|
| 206 |
+
|
| 207 |
+
```python
|
| 208 |
+
"""
|
| 209 |
+
Simple Trader NPC Add-on - Self-contained NPC with automatic registration.
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
import gradio as gr
|
| 213 |
+
from typing import Dict
|
| 214 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class SimpleTraderAddon(NPCAddon):
|
| 218 |
+
"""Self-contained trader NPC that handles its own registration and positioning."""
|
| 219 |
+
|
| 220 |
+
def __init__(self):
|
| 221 |
+
super().__init__()
|
| 222 |
+
self.inventory = {
|
| 223 |
+
"Health Potion": {"price": 50, "stock": 10, "description": "Restores 100 HP"},
|
| 224 |
+
"Magic Scroll": {"price": 150, "stock": 5, "description": "Cast magic spells"},
|
| 225 |
+
"Iron Sword": {"price": 300, "stock": 3, "description": "Sharp iron weapon"},
|
| 226 |
+
"Shield": {"price": 200, "stock": 4, "description": "Protective gear"}
|
| 227 |
+
}
|
| 228 |
+
self.sales_history = []
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def addon_id(self) -> str:
|
| 232 |
+
return "simple_trader"
|
| 233 |
+
|
| 234 |
+
@property
|
| 235 |
+
def addon_name(self) -> str:
|
| 236 |
+
return "🛒 Simple Trader"
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def npc_config(self) -> Dict:
|
| 240 |
+
return {
|
| 241 |
+
'id': 'simple_trader',
|
| 242 |
+
'name': '🛒 Simple Trader',
|
| 243 |
+
'x': 450, 'y': 150,
|
| 244 |
+
'char': '🛒',
|
| 245 |
+
'type': 'addon',
|
| 246 |
+
'personality': 'trader',
|
| 247 |
+
'description': 'A friendly trader selling useful items'
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def ui_tab_name(self) -> str:
|
| 252 |
+
return "🛒 Simple Trader"
|
| 253 |
+
|
| 254 |
+
def get_interface(self) -> gr.Component:
|
| 255 |
+
"""Create the Gradio interface for this addon."""
|
| 256 |
+
with gr.Column() as interface:
|
| 257 |
+
gr.Markdown("### 🛒 Simple Trader Shop")
|
| 258 |
+
gr.Markdown("Browse items and check prices. Use private messages to trade!")
|
| 259 |
+
|
| 260 |
+
# Display inventory
|
| 261 |
+
inventory_data = []
|
| 262 |
+
for item, info in self.inventory.items():
|
| 263 |
+
inventory_data.append([item, f"{info['price']} gold", info['stock'], info['description']])
|
| 264 |
+
|
| 265 |
+
gr.Dataframe(
|
| 266 |
+
headers=["Item", "Price", "Stock", "Description"],
|
| 267 |
+
value=inventory_data,
|
| 268 |
+
interactive=False
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
gr.Markdown("**Commands:** Send private message to Simple Trader")
|
| 272 |
+
gr.Markdown("• `buy <item>` - Purchase an item")
|
| 273 |
+
gr.Markdown("• `inventory` - See available items")
|
| 274 |
+
gr.Markdown("• `help` - Show all commands")
|
| 275 |
+
|
| 276 |
+
return interface
|
| 277 |
+
|
| 278 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 279 |
+
"""Handle commands sent to this NPC."""
|
| 280 |
+
parts = command.strip().lower().split()
|
| 281 |
+
cmd = parts[0] if parts else ""
|
| 282 |
+
|
| 283 |
+
if cmd == "buy" and len(parts) > 1:
|
| 284 |
+
item_name = " ".join(parts[1:]).title()
|
| 285 |
+
return self._handle_buy(player_id, item_name)
|
| 286 |
+
elif cmd == "inventory":
|
| 287 |
+
return self._show_inventory()
|
| 288 |
+
elif cmd == "help":
|
| 289 |
+
return self._show_help()
|
| 290 |
+
else:
|
| 291 |
+
return "🛒 **Simple Trader:** I didn't understand. Try 'buy <item>', 'inventory', or 'help'."
|
| 292 |
+
|
| 293 |
+
def _handle_buy(self, player_id: str, item_name: str) -> str:
|
| 294 |
+
"""Handle item purchase."""
|
| 295 |
+
if item_name not in self.inventory:
|
| 296 |
+
return f"❌ Sorry, I don't have '{item_name}' in stock."
|
| 297 |
+
|
| 298 |
+
item = self.inventory[item_name]
|
| 299 |
+
if item['stock'] <= 0:
|
| 300 |
+
return f"❌ '{item_name}' is out of stock!"
|
| 301 |
+
|
| 302 |
+
# In a real implementation, check player gold and deduct
|
| 303 |
+
item['stock'] -= 1
|
| 304 |
+
self.sales_history.append({'player': player_id, 'item': item_name, 'price': item['price']})
|
| 305 |
+
|
| 306 |
+
return f"✅ **Purchase Successful!**\n📦 You bought: {item_name}\n💰 Price: {item['price']} gold\n📊 Stock remaining: {item['stock']}"
|
| 307 |
+
|
| 308 |
+
def _show_inventory(self) -> str:
|
| 309 |
+
"""Show current inventory."""
|
| 310 |
+
result = "🛒 **Current Inventory:**\n\n"
|
| 311 |
+
for item, info in self.inventory.items():
|
| 312 |
+
result += f"**{item}** - {info['price']} gold (Stock: {info['stock']})\n"
|
| 313 |
+
result += f" ↳ {info['description']}\n\n"
|
| 314 |
+
return result
|
| 315 |
+
|
| 316 |
+
def _show_help(self) -> str:
|
| 317 |
+
"""Show help commands."""
|
| 318 |
+
return """🛒 **Simple Trader Commands:**
|
| 319 |
+
|
| 320 |
+
**buy <item>** - Purchase an item
|
| 321 |
+
**inventory** - See available items and prices
|
| 322 |
+
**help** - Show this help
|
| 323 |
+
|
| 324 |
+
💰 **Available Items:**
|
| 325 |
+
• Health Potion, Magic Scroll, Iron Sword, Shield
|
| 326 |
+
|
| 327 |
+
Example: `buy Health Potion`"""
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# Global instance for automatic registration
|
| 331 |
+
simple_trader_addon = SimpleTraderAddon()
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
### Example 2: Read2Burn Messaging (From Codebase)
|
| 335 |
+
|
| 336 |
+
This example shows a complex addon with state management and service interfaces:
|
| 337 |
+
|
| 338 |
+
```python
|
| 339 |
+
"""
|
| 340 |
+
Read2Burn Mailbox Add-on - Self-destructing secure messaging system.
|
| 341 |
+
"""
|
| 342 |
+
|
| 343 |
+
import time
|
| 344 |
+
import random
|
| 345 |
+
import string
|
| 346 |
+
from typing import Dict, List
|
| 347 |
+
from dataclasses import dataclass
|
| 348 |
+
from abc import ABC, abstractmethod
|
| 349 |
+
import gradio as gr
|
| 350 |
+
|
| 351 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
@dataclass
|
| 355 |
+
class Read2BurnMessage:
|
| 356 |
+
"""Data class for Read2Burn messages."""
|
| 357 |
+
id: str
|
| 358 |
+
creator_id: str
|
| 359 |
+
content: str
|
| 360 |
+
created_at: float
|
| 361 |
+
expires_at: float
|
| 362 |
+
reads_left: int
|
| 363 |
+
burned: bool = False
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class IRead2BurnService(ABC):
|
| 367 |
+
"""Interface for Read2Burn service operations."""
|
| 368 |
+
|
| 369 |
+
@abstractmethod
|
| 370 |
+
def create_message(self, creator_id: str, content: str) -> str: pass
|
| 371 |
+
|
| 372 |
+
@abstractmethod
|
| 373 |
+
def read_message(self, reader_id: str, message_id: str) -> str: pass
|
| 374 |
+
|
| 375 |
+
@abstractmethod
|
| 376 |
+
def list_player_messages(self, player_id: str) -> str: pass
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
class Read2BurnService(IRead2BurnService, NPCAddon):
|
| 380 |
+
"""Service for managing Read2Burn secure messaging."""
|
| 381 |
+
|
| 382 |
+
def __init__(self):
|
| 383 |
+
super().__init__()
|
| 384 |
+
self.messages: Dict[str, Read2BurnMessage] = {}
|
| 385 |
+
self.access_log: List[Dict] = []
|
| 386 |
+
|
| 387 |
+
@property
|
| 388 |
+
def addon_id(self) -> str:
|
| 389 |
+
return "read2burn_mailbox"
|
| 390 |
+
|
| 391 |
+
@property
|
| 392 |
+
def addon_name(self) -> str:
|
| 393 |
+
return "📧 Read2Burn Secure Mailbox"
|
| 394 |
+
|
| 395 |
+
@property
|
| 396 |
+
def npc_config(self) -> Dict:
|
| 397 |
+
return {
|
| 398 |
+
'id': 'read2burn',
|
| 399 |
+
'name': '📧 Read2Burn Service',
|
| 400 |
+
'x': 200, 'y': 100,
|
| 401 |
+
'char': '📧',
|
| 402 |
+
'type': 'service',
|
| 403 |
+
'personality': 'read2burn',
|
| 404 |
+
'description': 'Secure message service - send private messages that auto-delete after reading'
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
def get_interface(self) -> gr.Component:
|
| 408 |
+
"""Return Gradio interface for this add-on"""
|
| 409 |
+
with gr.Column() as interface:
|
| 410 |
+
gr.Markdown("""
|
| 411 |
+
## 📧 Read2Burn Secure Mailbox
|
| 412 |
+
|
| 413 |
+
**Create self-destructing messages that burn after reading!**
|
| 414 |
+
|
| 415 |
+
### Features:
|
| 416 |
+
- 🔥 Messages self-destruct after reading
|
| 417 |
+
- ⏰ 24-hour automatic expiration
|
| 418 |
+
- 🔒 Secure delivery system
|
| 419 |
+
- 📊 Message tracking and history
|
| 420 |
+
""")
|
| 421 |
+
|
| 422 |
+
with gr.Tabs():
|
| 423 |
+
with gr.Tab("📝 Create Message"):
|
| 424 |
+
message_input = gr.Textbox(
|
| 425 |
+
label="Message Content",
|
| 426 |
+
lines=5,
|
| 427 |
+
placeholder="Type your secret message here..."
|
| 428 |
+
)
|
| 429 |
+
create_btn = gr.Button("🔥 Create Burning Message", variant="primary")
|
| 430 |
+
create_output = gr.Textbox(label="Result", lines=3, interactive=False)
|
| 431 |
+
|
| 432 |
+
def create_message_ui(content):
|
| 433 |
+
# In real implementation, get current player ID
|
| 434 |
+
player_id = "current_player" # Simplified
|
| 435 |
+
return self.create_message(player_id, content)
|
| 436 |
+
|
| 437 |
+
create_btn.click(create_message_ui, inputs=[message_input], outputs=[create_output])
|
| 438 |
+
|
| 439 |
+
with gr.Tab("🔓 Read Message"):
|
| 440 |
+
message_id_input = gr.Textbox(label="Message ID", placeholder="Enter 8-character message ID")
|
| 441 |
+
read_btn = gr.Button("📖 Read & Burn Message", variant="secondary")
|
| 442 |
+
read_output = gr.Textbox(label="Message Content", lines=5, interactive=False)
|
| 443 |
+
|
| 444 |
+
def read_message_ui(msg_id):
|
| 445 |
+
player_id = "current_player" # Simplified
|
| 446 |
+
return self.read_message(player_id, msg_id)
|
| 447 |
+
|
| 448 |
+
read_btn.click(read_message_ui, inputs=[message_id_input], outputs=[read_output])
|
| 449 |
+
|
| 450 |
+
with gr.Tab("📋 My Messages"):
|
| 451 |
+
refresh_btn = gr.Button("🔄 Refresh List")
|
| 452 |
+
messages_output = gr.Textbox(label="Your Messages", lines=10, interactive=False)
|
| 453 |
+
|
| 454 |
+
def list_messages_ui():
|
| 455 |
+
player_id = "current_player" # Simplified
|
| 456 |
+
return self.list_player_messages(player_id)
|
| 457 |
+
|
| 458 |
+
refresh_btn.click(list_messages_ui, outputs=[messages_output])
|
| 459 |
+
|
| 460 |
+
gr.Markdown("**💬 Private Message Commands:**")
|
| 461 |
+
gr.Markdown("• `create Your message here` - Create new message")
|
| 462 |
+
gr.Markdown("• `read MESSAGE_ID` - Read message (destroys it!)")
|
| 463 |
+
gr.Markdown("• `list` - Show your created messages")
|
| 464 |
+
gr.Markdown("• `help` - Show command help")
|
| 465 |
+
|
| 466 |
+
return interface
|
| 467 |
+
|
| 468 |
+
def generate_message_id(self) -> str:
|
| 469 |
+
"""Generate a unique message ID."""
|
| 470 |
+
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
| 471 |
+
|
| 472 |
+
def create_message(self, creator_id: str, content: str) -> str:
|
| 473 |
+
"""Create a new self-destructing message."""
|
| 474 |
+
message_id = self.generate_message_id()
|
| 475 |
+
|
| 476 |
+
message = Read2BurnMessage(
|
| 477 |
+
id=message_id,
|
| 478 |
+
creator_id=creator_id,
|
| 479 |
+
content=content, # In production, encrypt this
|
| 480 |
+
created_at=time.time(),
|
| 481 |
+
expires_at=time.time() + (24 * 3600), # 24 hours
|
| 482 |
+
reads_left=1,
|
| 483 |
+
burned=False
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
self.messages[message_id] = message
|
| 487 |
+
|
| 488 |
+
self.access_log.append({
|
| 489 |
+
'action': 'create',
|
| 490 |
+
'message_id': message_id,
|
| 491 |
+
'player_id': creator_id,
|
| 492 |
+
'timestamp': time.time()
|
| 493 |
+
})
|
| 494 |
+
|
| 495 |
+
return f"✅ **Message Created Successfully!**\n\n📝 **Message ID:** `{message_id}`\n🔗 Share this ID with the recipient\n⏰ Expires in 24 hours\n🔥 Burns after 1 read"
|
| 496 |
+
|
| 497 |
+
def read_message(self, reader_id: str, message_id: str) -> str:
|
| 498 |
+
"""Read and burn a message."""
|
| 499 |
+
if message_id not in self.messages:
|
| 500 |
+
return "❌ Message not found or already burned"
|
| 501 |
+
|
| 502 |
+
message = self.messages[message_id]
|
| 503 |
+
|
| 504 |
+
# Check expiry
|
| 505 |
+
if time.time() > message.expires_at:
|
| 506 |
+
del self.messages[message_id]
|
| 507 |
+
return "❌ Message expired and has been burned"
|
| 508 |
+
|
| 509 |
+
# Check if already burned
|
| 510 |
+
if message.burned or message.reads_left <= 0:
|
| 511 |
+
del self.messages[message_id]
|
| 512 |
+
return "❌ Message has already been burned"
|
| 513 |
+
|
| 514 |
+
# Read the message
|
| 515 |
+
content = message.content
|
| 516 |
+
message.reads_left -= 1
|
| 517 |
+
|
| 518 |
+
self.access_log.append({
|
| 519 |
+
'action': 'read',
|
| 520 |
+
'message_id': message_id,
|
| 521 |
+
'player_id': reader_id,
|
| 522 |
+
'timestamp': time.time()
|
| 523 |
+
})
|
| 524 |
+
|
| 525 |
+
# Burn the message after reading
|
| 526 |
+
if message.reads_left <= 0:
|
| 527 |
+
message.burned = True
|
| 528 |
+
del self.messages[message_id]
|
| 529 |
+
return f"🔥 **Message Self-Destructed After Reading**\n\n📖 **Content:** {content}\n\n💨 This message has been permanently destroyed."
|
| 530 |
+
|
| 531 |
+
return f"📖 **Message Content:** {content}\n\n⚠️ Reads remaining: {message.reads_left}"
|
| 532 |
+
|
| 533 |
+
def list_player_messages(self, player_id: str) -> str:
|
| 534 |
+
"""List messages created by a player."""
|
| 535 |
+
player_messages = [msg for msg in self.messages.values() if msg.creator_id == player_id]
|
| 536 |
+
|
| 537 |
+
if not player_messages:
|
| 538 |
+
return "📪 No messages found. Create one with: `create Your message here`"
|
| 539 |
+
|
| 540 |
+
result = "📋 **Your Created Messages:**\n\n"
|
| 541 |
+
for msg in player_messages:
|
| 542 |
+
status = "🔥 Burned" if msg.burned else f"✅ Active ({msg.reads_left} reads left)"
|
| 543 |
+
created_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(msg.created_at))
|
| 544 |
+
expires_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(msg.expires_at))
|
| 545 |
+
|
| 546 |
+
result += f"**ID:** `{msg.id}`\n"
|
| 547 |
+
result += f"**Status:** {status}\n"
|
| 548 |
+
result += f"**Created:** {created_time}\n"
|
| 549 |
+
result += f"**Expires:** {expires_time}\n"
|
| 550 |
+
result += f"**Preview:** {msg.content[:50]}{'...' if len(msg.content) > 50 else ''}\n\n"
|
| 551 |
+
|
| 552 |
+
return result
|
| 553 |
+
|
| 554 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 555 |
+
"""Handle Read2Burn mailbox commands."""
|
| 556 |
+
parts = command.strip().split(' ', 1)
|
| 557 |
+
cmd = parts[0].lower()
|
| 558 |
+
|
| 559 |
+
if cmd == "create" and len(parts) > 1:
|
| 560 |
+
return self.create_message(player_id, parts[1])
|
| 561 |
+
elif cmd == "read" and len(parts) > 1:
|
| 562 |
+
return self.read_message(player_id, parts[1])
|
| 563 |
+
elif cmd == "list":
|
| 564 |
+
return self.list_player_messages(player_id)
|
| 565 |
+
elif cmd == "help":
|
| 566 |
+
return """📚 **Read2Burn Mailbox Commands:**
|
| 567 |
+
|
| 568 |
+
**create** `Your secret message here` - Create new message
|
| 569 |
+
**read** `MESSAGE_ID` - Read message (destroys it!)
|
| 570 |
+
**list** - Show your created messages
|
| 571 |
+
**help** - Show this help
|
| 572 |
+
|
| 573 |
+
🔥 **Features:**
|
| 574 |
+
• Messages self-destruct after reading
|
| 575 |
+
• 24-hour automatic expiration
|
| 576 |
+
• Secure delivery system
|
| 577 |
+
• Anonymous messaging support"""
|
| 578 |
+
else:
|
| 579 |
+
return "❓ Invalid command. Try: `create <message>`, `read <id>`, `list`, or `help`"
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
# Global Read2Burn service instance
|
| 583 |
+
read2burn_service = Read2BurnService()
|
| 584 |
+
```
|
| 585 |
+
|
| 586 |
+
---
|
| 587 |
+
|
| 588 |
+
## 🌐 MCP Integration
|
| 589 |
+
|
| 590 |
+
### MCP-Powered Weather Oracle (From Codebase)
|
| 591 |
+
|
| 592 |
+
This example shows how to integrate external MCP servers:
|
| 593 |
+
|
| 594 |
+
```python
|
| 595 |
+
"""
|
| 596 |
+
Weather Oracle Add-on - MCP-powered weather information system.
|
| 597 |
+
"""
|
| 598 |
+
|
| 599 |
+
import time
|
| 600 |
+
import asyncio
|
| 601 |
+
from typing import Dict, Optional
|
| 602 |
+
import gradio as gr
|
| 603 |
+
from mcp import ClientSession
|
| 604 |
+
from mcp.client.sse import sse_client
|
| 605 |
+
from contextlib import AsyncExitStack
|
| 606 |
+
|
| 607 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
class WeatherOracleService(NPCAddon):
|
| 611 |
+
"""Service for managing Weather Oracle MCP integration."""
|
| 612 |
+
|
| 613 |
+
def __init__(self):
|
| 614 |
+
super().__init__()
|
| 615 |
+
self.connected = False
|
| 616 |
+
self.last_connection_attempt = 0
|
| 617 |
+
self.connection_cooldown = 30 # 30 seconds between connection attempts
|
| 618 |
+
self.server_url = "https://chris4k-weather.hf.space/gradio_api/mcp/sse"
|
| 619 |
+
self.session = None
|
| 620 |
+
self.tools = []
|
| 621 |
+
self.exit_stack = None
|
| 622 |
+
|
| 623 |
+
# Set up event loop for async operations
|
| 624 |
+
try:
|
| 625 |
+
self.loop = asyncio.get_event_loop()
|
| 626 |
+
except RuntimeError:
|
| 627 |
+
self.loop = asyncio.new_event_loop()
|
| 628 |
+
asyncio.set_event_loop(self.loop)
|
| 629 |
+
|
| 630 |
+
@property
|
| 631 |
+
def addon_id(self) -> str:
|
| 632 |
+
return "weather_oracle"
|
| 633 |
+
|
| 634 |
+
@property
|
| 635 |
+
def addon_name(self) -> str:
|
| 636 |
+
return "🌤️ Weather Oracle (MCP)"
|
| 637 |
+
|
| 638 |
+
@property
|
| 639 |
+
def npc_config(self) -> Dict:
|
| 640 |
+
return {
|
| 641 |
+
'id': 'weather_oracle',
|
| 642 |
+
'name': '🌤️ Weather Oracle (MCP)',
|
| 643 |
+
'x': 150, 'y': 300,
|
| 644 |
+
'char': '🌤️',
|
| 645 |
+
'type': 'mcp',
|
| 646 |
+
'description': 'MCP-powered weather information service'
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
@property
|
| 650 |
+
def ui_tab_name(self) -> str:
|
| 651 |
+
return "🌤️ Weather Oracle"
|
| 652 |
+
|
| 653 |
+
def get_interface(self) -> gr.Component:
|
| 654 |
+
"""Return Gradio interface for this add-on"""
|
| 655 |
+
with gr.Column() as interface:
|
| 656 |
+
gr.Markdown("""
|
| 657 |
+
## 🌤️ Weather Oracle
|
| 658 |
+
|
| 659 |
+
*I commune with the spirits of sky and storm to bring you weather wisdom from across the realms!*
|
| 660 |
+
|
| 661 |
+
**Ask me about weather in any city:**
|
| 662 |
+
- Current conditions and temperature
|
| 663 |
+
- Weather forecasts
|
| 664 |
+
- Climate information
|
| 665 |
+
|
| 666 |
+
*Format: "City, Country" (e.g., "Berlin, Germany")*
|
| 667 |
+
""")
|
| 668 |
+
|
| 669 |
+
# Connection status
|
| 670 |
+
connection_status = gr.HTML(
|
| 671 |
+
value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
with gr.Row():
|
| 675 |
+
location_input = gr.Textbox(
|
| 676 |
+
label="🌍 Location",
|
| 677 |
+
placeholder="Enter city, country (e.g., Berlin, Germany)",
|
| 678 |
+
scale=3
|
| 679 |
+
)
|
| 680 |
+
get_weather_btn = gr.Button("🌡️ Consult Weather Spirits", variant="primary", scale=1)
|
| 681 |
+
|
| 682 |
+
weather_output = gr.Textbox(
|
| 683 |
+
label="🌤️ Weather Wisdom",
|
| 684 |
+
lines=8,
|
| 685 |
+
interactive=False,
|
| 686 |
+
placeholder="Enter a location and I will consult the weather spirits..."
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
# Example locations
|
| 690 |
+
gr.Examples(
|
| 691 |
+
examples=[
|
| 692 |
+
["London, UK"],
|
| 693 |
+
["Tokyo, Japan"],
|
| 694 |
+
["New York, USA"],
|
| 695 |
+
["Berlin, Germany"],
|
| 696 |
+
["Sydney, Australia"]
|
| 697 |
+
],
|
| 698 |
+
inputs=[location_input],
|
| 699 |
+
label="🌍 Try These Locations"
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
# Connection controls
|
| 703 |
+
with gr.Row():
|
| 704 |
+
connect_btn = gr.Button("🔗 Connect to MCP", variant="secondary")
|
| 705 |
+
status_btn = gr.Button("📊 Check Status", variant="secondary")
|
| 706 |
+
|
| 707 |
+
def handle_weather_request(location: str):
|
| 708 |
+
if not location.strip():
|
| 709 |
+
return "🌪️ The spirits need to know which realm you seek knowledge about!"
|
| 710 |
+
return self.get_weather(location)
|
| 711 |
+
|
| 712 |
+
def handle_connect():
|
| 713 |
+
result = self.connect_to_mcp()
|
| 714 |
+
status = f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 715 |
+
return result, status
|
| 716 |
+
|
| 717 |
+
def handle_status():
|
| 718 |
+
if self.connected:
|
| 719 |
+
tool_names = [tool.name for tool in self.tools] if self.tools else []
|
| 720 |
+
return f"✅ **Connected to MCP Weather Server**\n\nServer: {self.server_url}\nTools: {', '.join(tool_names) if tool_names else 'None'}"
|
| 721 |
+
else:
|
| 722 |
+
return "❌ **Not Connected**\n\nClick 'Connect to MCP' to establish connection."
|
| 723 |
+
|
| 724 |
+
# Wire up events
|
| 725 |
+
get_weather_btn.click(
|
| 726 |
+
handle_weather_request,
|
| 727 |
+
inputs=[location_input],
|
| 728 |
+
outputs=[weather_output]
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
location_input.submit(
|
| 732 |
+
handle_weather_request,
|
| 733 |
+
inputs=[location_input],
|
| 734 |
+
outputs=[weather_output]
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
connect_btn.click(
|
| 738 |
+
handle_connect,
|
| 739 |
+
outputs=[weather_output, connection_status]
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
status_btn.click(
|
| 743 |
+
handle_status,
|
| 744 |
+
outputs=[weather_output]
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
return interface
|
| 748 |
+
|
| 749 |
+
def connect_to_mcp(self) -> str:
|
| 750 |
+
"""Connect to MCP weather server."""
|
| 751 |
+
current_time = time.time()
|
| 752 |
+
if current_time - self.last_connection_attempt < self.connection_cooldown:
|
| 753 |
+
return "⏳ Please wait before retrying connection..."
|
| 754 |
+
|
| 755 |
+
self.last_connection_attempt = current_time
|
| 756 |
+
|
| 757 |
+
try:
|
| 758 |
+
return self.loop.run_until_complete(self._connect())
|
| 759 |
+
except Exception as e:
|
| 760 |
+
self.connected = False
|
| 761 |
+
return f"❌ Connection failed: {str(e)}"
|
| 762 |
+
|
| 763 |
+
async def _connect(self) -> str:
|
| 764 |
+
"""Async connect to MCP server using SSE."""
|
| 765 |
+
try:
|
| 766 |
+
# Clean up previous connection
|
| 767 |
+
if self.exit_stack:
|
| 768 |
+
await self.exit_stack.aclose()
|
| 769 |
+
|
| 770 |
+
self.exit_stack = AsyncExitStack()
|
| 771 |
+
|
| 772 |
+
# Connect to SSE MCP server
|
| 773 |
+
sse_transport = await self.exit_stack.enter_async_context(
|
| 774 |
+
sse_client(self.server_url)
|
| 775 |
+
)
|
| 776 |
+
read_stream, write_callable = sse_transport
|
| 777 |
+
|
| 778 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 779 |
+
ClientSession(read_stream, write_callable)
|
| 780 |
+
)
|
| 781 |
+
await self.session.initialize()
|
| 782 |
+
|
| 783 |
+
# Get available tools
|
| 784 |
+
response = await self.session.list_tools()
|
| 785 |
+
self.tools = response.tools
|
| 786 |
+
|
| 787 |
+
self.connected = True
|
| 788 |
+
tool_names = [tool.name for tool in self.tools]
|
| 789 |
+
return f"✅ Connected to weather MCP server!\nAvailable tools: {', '.join(tool_names)}"
|
| 790 |
+
|
| 791 |
+
except Exception as e:
|
| 792 |
+
self.connected = False
|
| 793 |
+
return f"❌ Connection failed: {str(e)}"
|
| 794 |
+
|
| 795 |
+
def get_weather(self, location: str) -> str:
|
| 796 |
+
"""Get weather for a location using actual MCP server"""
|
| 797 |
+
if not self.connected:
|
| 798 |
+
# Try to auto-reconnect
|
| 799 |
+
connect_result = self.connect_to_mcp()
|
| 800 |
+
if not self.connected:
|
| 801 |
+
return f"❌ **Not connected to weather spirits**\n\n{connect_result}"
|
| 802 |
+
|
| 803 |
+
try:
|
| 804 |
+
return self.loop.run_until_complete(self._get_weather(location))
|
| 805 |
+
except Exception as e:
|
| 806 |
+
return f"❌ **Weather divination failed**\n\nError: {str(e)}"
|
| 807 |
+
|
| 808 |
+
async def _get_weather(self, location: str) -> str:
|
| 809 |
+
"""Async get weather using MCP."""
|
| 810 |
+
try:
|
| 811 |
+
# Parse location
|
| 812 |
+
if ',' in location:
|
| 813 |
+
city, country = [part.strip() for part in location.split(',', 1)]
|
| 814 |
+
else:
|
| 815 |
+
city = location.strip()
|
| 816 |
+
country = ""
|
| 817 |
+
|
| 818 |
+
# Find the weather tool
|
| 819 |
+
weather_tool = next((tool for tool in self.tools if 'weather' in tool.name.lower()), None)
|
| 820 |
+
if not weather_tool:
|
| 821 |
+
return "❌ Weather tool not found on server"
|
| 822 |
+
|
| 823 |
+
# Call the tool
|
| 824 |
+
params = {"city": city, "country": country}
|
| 825 |
+
result = await self.session.call_tool(weather_tool.name, params)
|
| 826 |
+
|
| 827 |
+
# Extract content properly
|
| 828 |
+
content_text = ""
|
| 829 |
+
if hasattr(result, 'content') and result.content:
|
| 830 |
+
if isinstance(result.content, list):
|
| 831 |
+
for content_item in result.content:
|
| 832 |
+
if hasattr(content_item, 'text'):
|
| 833 |
+
content_text += content_item.text + "\n"
|
| 834 |
+
else:
|
| 835 |
+
content_text += str(content_item) + "\n"
|
| 836 |
+
else:
|
| 837 |
+
content_text = str(result.content)
|
| 838 |
+
|
| 839 |
+
if not content_text.strip():
|
| 840 |
+
return f"❌ No weather data received for {location}"
|
| 841 |
+
|
| 842 |
+
# Format the response
|
| 843 |
+
return f"🌤️ **Weather Oracle reveals the weather for {location}:**\n\n{content_text.strip()}"
|
| 844 |
+
|
| 845 |
+
except Exception as e:
|
| 846 |
+
return f"❌ Weather divination failed: {str(e)}"
|
| 847 |
+
|
| 848 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 849 |
+
"""Handle Weather Oracle commands."""
|
| 850 |
+
cmd = command.strip()
|
| 851 |
+
|
| 852 |
+
if not cmd:
|
| 853 |
+
return ("🌤️ **Weather Oracle whispers:**\n\n"
|
| 854 |
+
"Ask me about the weather in any city!\n"
|
| 855 |
+
"Format: 'City, Country' (e.g., 'Berlin, Germany')")
|
| 856 |
+
|
| 857 |
+
# Treat any non-empty command as a location request
|
| 858 |
+
result = self.get_weather(cmd)
|
| 859 |
+
return f"🌤️ **Weather Oracle reveals:**\n\n{result}"
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
# Custom auto-registration function
|
| 863 |
+
def auto_register(game_engine):
|
| 864 |
+
"""Auto-register the Weather Oracle addon with the game engine."""
|
| 865 |
+
try:
|
| 866 |
+
# Create the weather oracle NPC definition
|
| 867 |
+
weather_oracle_npc = {
|
| 868 |
+
'id': 'weather_oracle_auto',
|
| 869 |
+
'name': '🌤️ Weather Oracle (Auto)',
|
| 870 |
+
'x': 300, 'y': 150,
|
| 871 |
+
'char': '🌤️',
|
| 872 |
+
'type': 'mcp',
|
| 873 |
+
'personality': 'weather_oracle',
|
| 874 |
+
'description': 'Self-contained MCP-powered weather information service'
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
# Register the NPC with the NPC service
|
| 878 |
+
npc_service = game_engine.get_npc_service()
|
| 879 |
+
npc_service.register_npc('weather_oracle_auto', weather_oracle_npc)
|
| 880 |
+
|
| 881 |
+
# Register the addon for handling private message commands
|
| 882 |
+
world = game_engine.get_world()
|
| 883 |
+
if not hasattr(world, 'addon_npcs'):
|
| 884 |
+
world.addon_npcs = {}
|
| 885 |
+
world.addon_npcs['weather_oracle_auto'] = weather_oracle_service
|
| 886 |
+
|
| 887 |
+
print("[WeatherOracleAddon] Auto-registered successfully as self-contained addon")
|
| 888 |
+
return True
|
| 889 |
+
|
| 890 |
+
except Exception as e:
|
| 891 |
+
print(f"[WeatherOracleAddon] Error during auto-registration: {e}")
|
| 892 |
+
return False
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
# Global Weather Oracle service instance
|
| 896 |
+
weather_oracle_service = WeatherOracleService()
|
| 897 |
+
```
|
| 898 |
+
|
| 899 |
+
---
|
| 900 |
+
|
| 901 |
+
## 🔧 Advanced Patterns
|
| 902 |
+
|
| 903 |
+
### State Persistence
|
| 904 |
+
|
| 905 |
+
```python
|
| 906 |
+
import json
|
| 907 |
+
import os
|
| 908 |
+
|
| 909 |
+
class PersistentAddon(NPCAddon):
|
| 910 |
+
"""Example addon with state persistence."""
|
| 911 |
+
|
| 912 |
+
def __init__(self):
|
| 913 |
+
super().__init__()
|
| 914 |
+
self.data_file = f"data/{self.addon_id}_data.json"
|
| 915 |
+
self.load_state()
|
| 916 |
+
|
| 917 |
+
def load_state(self):
|
| 918 |
+
"""Load addon state from file."""
|
| 919 |
+
try:
|
| 920 |
+
if os.path.exists(self.data_file):
|
| 921 |
+
with open(self.data_file, 'r') as f:
|
| 922 |
+
data = json.load(f)
|
| 923 |
+
self.player_data = data.get('players', {})
|
| 924 |
+
self.settings = data.get('settings', {})
|
| 925 |
+
else:
|
| 926 |
+
self.player_data = {}
|
| 927 |
+
self.settings = {}
|
| 928 |
+
except Exception as e:
|
| 929 |
+
print(f"[{self.addon_id}] Failed to load state: {e}")
|
| 930 |
+
self.player_data = {}
|
| 931 |
+
self.settings = {}
|
| 932 |
+
|
| 933 |
+
def save_state(self):
|
| 934 |
+
"""Save addon state to file."""
|
| 935 |
+
try:
|
| 936 |
+
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
|
| 937 |
+
with open(self.data_file, 'w') as f:
|
| 938 |
+
json.dump({
|
| 939 |
+
'players': self.player_data,
|
| 940 |
+
'settings': self.settings
|
| 941 |
+
}, f, indent=2)
|
| 942 |
+
except Exception as e:
|
| 943 |
+
print(f"[{self.addon_id}] Failed to save state: {e}")
|
| 944 |
+
|
| 945 |
+
def on_shutdown(self):
|
| 946 |
+
"""Save state when addon shuts down."""
|
| 947 |
+
self.save_state()
|
| 948 |
+
```
|
| 949 |
+
|
| 950 |
+
### Player Validation Helper
|
| 951 |
+
|
| 952 |
+
```python
|
| 953 |
+
def get_current_player(self, game_world):
|
| 954 |
+
"""Helper to safely get current active player."""
|
| 955 |
+
try:
|
| 956 |
+
current_players = list(game_world.players.keys())
|
| 957 |
+
if not current_players:
|
| 958 |
+
return None, "❌ You must be in the game to use this service!"
|
| 959 |
+
|
| 960 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 961 |
+
player = game_world.players.get(player_id)
|
| 962 |
+
|
| 963 |
+
if not player:
|
| 964 |
+
return None, "❌ Player not found!"
|
| 965 |
+
|
| 966 |
+
return player, None
|
| 967 |
+
except Exception as e:
|
| 968 |
+
return None, f"❌ Error accessing player data: {e}"
|
| 969 |
+
```
|
| 970 |
+
|
| 971 |
+
### Async Operation Wrapper
|
| 972 |
+
|
| 973 |
+
```python
|
| 974 |
+
def run_async_safely(self, async_func, *args, **kwargs):
|
| 975 |
+
"""Safely run async functions in sync context."""
|
| 976 |
+
try:
|
| 977 |
+
return self.loop.run_until_complete(async_func(*args, **kwargs))
|
| 978 |
+
except Exception as e:
|
| 979 |
+
return f"❌ Async operation failed: {e}"
|
| 980 |
+
```
|
| 981 |
+
|
| 982 |
+
### Configuration Management
|
| 983 |
+
|
| 984 |
+
```python
|
| 985 |
+
import yaml
|
| 986 |
+
|
| 987 |
+
class ConfigurableAddon(NPCAddon):
|
| 988 |
+
"""Addon with configuration file support."""
|
| 989 |
+
|
| 990 |
+
def __init__(self):
|
| 991 |
+
super().__init__()
|
| 992 |
+
self.config = self.load_config()
|
| 993 |
+
|
| 994 |
+
def load_config(self):
|
| 995 |
+
"""Load configuration from YAML file."""
|
| 996 |
+
config_file = f"config/{self.addon_id}_config.yaml"
|
| 997 |
+
try:
|
| 998 |
+
if os.path.exists(config_file):
|
| 999 |
+
with open(config_file, 'r') as f:
|
| 1000 |
+
return yaml.safe_load(f)
|
| 1001 |
+
else:
|
| 1002 |
+
return self.get_default_config()
|
| 1003 |
+
except Exception as e:
|
| 1004 |
+
print(f"[{self.addon_id}] Config load failed: {e}")
|
| 1005 |
+
return self.get_default_config()
|
| 1006 |
+
|
| 1007 |
+
def get_default_config(self):
|
| 1008 |
+
"""Return default configuration."""
|
| 1009 |
+
return {
|
| 1010 |
+
'enabled': True,
|
| 1011 |
+
'max_operations': 100,
|
| 1012 |
+
'cooldown_seconds': 30
|
| 1013 |
+
}
|
| 1014 |
+
```
|
| 1015 |
+
|
| 1016 |
+
---
|
| 1017 |
+
|
| 1018 |
+
## 🎯 Best Practices
|
| 1019 |
+
|
| 1020 |
+
### 1. Error Handling
|
| 1021 |
+
|
| 1022 |
+
```python
|
| 1023 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1024 |
+
"""Always wrap command handling in try-catch."""
|
| 1025 |
+
try:
|
| 1026 |
+
# Your command logic here
|
| 1027 |
+
return self.process_command(player_id, command)
|
| 1028 |
+
except Exception as e:
|
| 1029 |
+
print(f"[{self.addon_id}] Command error: {e}")
|
| 1030 |
+
return f"❌ An error occurred. Please try again or contact support."
|
| 1031 |
+
```
|
| 1032 |
+
|
| 1033 |
+
### 2. Input Validation
|
| 1034 |
+
|
| 1035 |
+
```python
|
| 1036 |
+
def validate_input(self, input_value: str, max_length: int = 500) -> tuple[bool, str]:
|
| 1037 |
+
"""Validate user input."""
|
| 1038 |
+
if not input_value or not input_value.strip():
|
| 1039 |
+
return False, "Input cannot be empty"
|
| 1040 |
+
|
| 1041 |
+
if len(input_value) > max_length:
|
| 1042 |
+
return False, f"Input too long (max {max_length} characters)"
|
| 1043 |
+
|
| 1044 |
+
# Add more validation as needed
|
| 1045 |
+
return True, ""
|
| 1046 |
+
```
|
| 1047 |
+
|
| 1048 |
+
### 3. Rate Limiting
|
| 1049 |
+
|
| 1050 |
+
```python
|
| 1051 |
+
import time
|
| 1052 |
+
from collections import defaultdict
|
| 1053 |
+
|
| 1054 |
+
class RateLimitedAddon(NPCAddon):
|
| 1055 |
+
"""Addon with rate limiting."""
|
| 1056 |
+
|
| 1057 |
+
def __init__(self):
|
| 1058 |
+
super().__init__()
|
| 1059 |
+
self.last_command_time = defaultdict(float)
|
| 1060 |
+
self.command_cooldown = 5 # 5 seconds between commands
|
| 1061 |
+
|
| 1062 |
+
def is_rate_limited(self, player_id: str) -> bool:
|
| 1063 |
+
"""Check if player is rate limited."""
|
| 1064 |
+
current_time = time.time()
|
| 1065 |
+
last_time = self.last_command_time[player_id]
|
| 1066 |
+
|
| 1067 |
+
if current_time - last_time < self.command_cooldown:
|
| 1068 |
+
return True
|
| 1069 |
+
|
| 1070 |
+
self.last_command_time[player_id] = current_time
|
| 1071 |
+
return False
|
| 1072 |
+
```
|
| 1073 |
+
|
| 1074 |
+
### 4. Logging
|
| 1075 |
+
|
| 1076 |
+
```python
|
| 1077 |
+
import logging
|
| 1078 |
+
|
| 1079 |
+
class LoggedAddon(NPCAddon):
|
| 1080 |
+
"""Addon with proper logging."""
|
| 1081 |
+
|
| 1082 |
+
def __init__(self):
|
| 1083 |
+
super().__init__()
|
| 1084 |
+
self.logger = logging.getLogger(f"addon_{self.addon_id}")
|
| 1085 |
+
self.logger.setLevel(logging.INFO)
|
| 1086 |
+
|
| 1087 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1088 |
+
self.logger.info(f"Command from {player_id}: {command}")
|
| 1089 |
+
try:
|
| 1090 |
+
result = self.process_command(player_id, command)
|
| 1091 |
+
self.logger.info(f"Command successful for {player_id}")
|
| 1092 |
+
return result
|
| 1093 |
+
except Exception as e:
|
| 1094 |
+
self.logger.error(f"Command failed for {player_id}: {e}")
|
| 1095 |
+
return "❌ Command failed. Please try again."
|
| 1096 |
+
```
|
| 1097 |
+
|
| 1098 |
+
### 5. Resource Cleanup
|
| 1099 |
+
|
| 1100 |
+
```python
|
| 1101 |
+
class ResourceManagedAddon(NPCAddon):
|
| 1102 |
+
"""Addon with proper resource management."""
|
| 1103 |
+
|
| 1104 |
+
def __init__(self):
|
| 1105 |
+
super().__init__()
|
| 1106 |
+
self.resources = [] # Track resources
|
| 1107 |
+
|
| 1108 |
+
def on_shutdown(self):
|
| 1109 |
+
"""Clean up resources on shutdown."""
|
| 1110 |
+
for resource in self.resources:
|
| 1111 |
+
try:
|
| 1112 |
+
if hasattr(resource, 'close'):
|
| 1113 |
+
resource.close()
|
| 1114 |
+
elif hasattr(resource, '__exit__'):
|
| 1115 |
+
resource.__exit__(None, None, None)
|
| 1116 |
+
except Exception as e:
|
| 1117 |
+
print(f"[{self.addon_id}] Cleanup error: {e}")
|
| 1118 |
+
```
|
| 1119 |
+
|
| 1120 |
+
---
|
| 1121 |
+
|
| 1122 |
+
## 🧪 Deployment & Testing
|
| 1123 |
+
|
| 1124 |
+
### Testing Your Addon
|
| 1125 |
+
|
| 1126 |
+
Create a test file for your addon:
|
| 1127 |
+
|
| 1128 |
+
```python
|
| 1129 |
+
# tests/test_my_addon.py
|
| 1130 |
+
import pytest
|
| 1131 |
+
from src.addons.my_addon import MyAddon
|
| 1132 |
+
|
| 1133 |
+
class TestMyAddon:
|
| 1134 |
+
def setup_method(self):
|
| 1135 |
+
"""Set up test fixtures."""
|
| 1136 |
+
self.addon = MyAddon()
|
| 1137 |
+
|
| 1138 |
+
def test_addon_id(self):
|
| 1139 |
+
"""Test addon ID is correct."""
|
| 1140 |
+
assert self.addon.addon_id == "my_addon"
|
| 1141 |
+
|
| 1142 |
+
def test_handle_command_hello(self):
|
| 1143 |
+
"""Test hello command."""
|
| 1144 |
+
result = self.addon.handle_command("test_player", "hello")
|
| 1145 |
+
assert "hello" in result.lower()
|
| 1146 |
+
|
| 1147 |
+
def test_handle_command_invalid(self):
|
| 1148 |
+
"""Test invalid command handling."""
|
| 1149 |
+
result = self.addon.handle_command("test_player", "invalid_command")
|
| 1150 |
+
assert "didn't understand" in result.lower() or "help" in result.lower()
|
| 1151 |
+
```
|
| 1152 |
+
|
| 1153 |
+
### Running Tests
|
| 1154 |
+
|
| 1155 |
+
```bash
|
| 1156 |
+
# Run all addon tests
|
| 1157 |
+
python -m pytest tests/test_*_addon.py -v
|
| 1158 |
+
|
| 1159 |
+
# Run specific addon test
|
| 1160 |
+
python -m pytest tests/test_my_addon.py -v
|
| 1161 |
+
|
| 1162 |
+
# Run with coverage
|
| 1163 |
+
python -m pytest tests/test_*_addon.py --cov=src/addons
|
| 1164 |
+
```
|
| 1165 |
+
|
| 1166 |
+
### Debug Mode
|
| 1167 |
+
|
| 1168 |
+
Add debug functionality to your addon:
|
| 1169 |
+
|
| 1170 |
+
```python
|
| 1171 |
+
class DebuggableAddon(NPCAddon):
|
| 1172 |
+
"""Addon with debug capabilities."""
|
| 1173 |
+
|
| 1174 |
+
def __init__(self):
|
| 1175 |
+
super().__init__()
|
| 1176 |
+
self.debug_mode = os.getenv('ADDON_DEBUG', 'false').lower() == 'true'
|
| 1177 |
+
|
| 1178 |
+
def debug_log(self, message: str):
|
| 1179 |
+
"""Log debug messages."""
|
| 1180 |
+
if self.debug_mode:
|
| 1181 |
+
print(f"[DEBUG:{self.addon_id}] {message}")
|
| 1182 |
+
|
| 1183 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1184 |
+
self.debug_log(f"Received command: {command}")
|
| 1185 |
+
# ... rest of method
|
| 1186 |
+
```
|
| 1187 |
+
|
| 1188 |
+
### Performance Monitoring
|
| 1189 |
+
|
| 1190 |
+
```python
|
| 1191 |
+
import time
|
| 1192 |
+
from functools import wraps
|
| 1193 |
+
|
| 1194 |
+
def monitor_performance(func):
|
| 1195 |
+
"""Decorator to monitor function performance."""
|
| 1196 |
+
@wraps(func)
|
| 1197 |
+
def wrapper(self, *args, **kwargs):
|
| 1198 |
+
start_time = time.time()
|
| 1199 |
+
try:
|
| 1200 |
+
result = func(self, *args, **kwargs)
|
| 1201 |
+
duration = time.time() - start_time
|
| 1202 |
+
if duration > 1.0: # Log slow operations
|
| 1203 |
+
print(f"[{self.addon_id}] Slow operation: {func.__name__} took {duration:.2f}s")
|
| 1204 |
+
return result
|
| 1205 |
+
except Exception as e:
|
| 1206 |
+
duration = time.time() - start_time
|
| 1207 |
+
print(f"[{self.addon_id}] Error in {func.__name__} after {duration:.2f}s: {e}")
|
| 1208 |
+
raise
|
| 1209 |
+
return wrapper
|
| 1210 |
+
|
| 1211 |
+
class MonitoredAddon(NPCAddon):
|
| 1212 |
+
"""Addon with performance monitoring."""
|
| 1213 |
+
|
| 1214 |
+
@monitor_performance
|
| 1215 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 1216 |
+
# Your command handling logic
|
| 1217 |
+
pass
|
| 1218 |
+
```
|
| 1219 |
+
|
| 1220 |
+
### Integration Testing
|
| 1221 |
+
|
| 1222 |
+
Test your addon with the game engine:
|
| 1223 |
+
|
| 1224 |
+
```python
|
| 1225 |
+
# tests/test_integration.py
|
| 1226 |
+
import asyncio
|
| 1227 |
+
from src.core.game_engine import GameEngine
|
| 1228 |
+
from src.addons.my_addon import MyAddon
|
| 1229 |
+
|
| 1230 |
+
def test_addon_integration():
|
| 1231 |
+
"""Test addon integrates properly with game engine."""
|
| 1232 |
+
engine = GameEngine()
|
| 1233 |
+
engine.start()
|
| 1234 |
+
|
| 1235 |
+
# Check addon is registered
|
| 1236 |
+
world = engine.get_world()
|
| 1237 |
+
assert hasattr(world, 'addon_npcs')
|
| 1238 |
+
assert 'my_addon' in world.addon_npcs
|
| 1239 |
+
|
| 1240 |
+
# Test NPC exists
|
| 1241 |
+
npc_service = engine.get_npc_service()
|
| 1242 |
+
npc = npc_service.get_npc('my_addon_npc')
|
| 1243 |
+
assert npc is not None
|
| 1244 |
+
|
| 1245 |
+
engine.stop()
|
| 1246 |
+
```
|
| 1247 |
+
|
| 1248 |
+
---
|
| 1249 |
+
|
| 1250 |
+
## 🔧 Troubleshooting
|
| 1251 |
+
|
| 1252 |
+
### Common Issues
|
| 1253 |
+
|
| 1254 |
+
**1. Addon Not Auto-Registering**
|
| 1255 |
+
- Check that you have a global instance at the bottom of your file
|
| 1256 |
+
- Verify `addon_id` is unique and doesn't conflict with others
|
| 1257 |
+
- Ensure you're calling `super().__init__()` in your constructor
|
| 1258 |
+
|
| 1259 |
+
**2. UI Tab Not Appearing**
|
| 1260 |
+
- Check that `ui_tab_name` property returns a string, not None
|
| 1261 |
+
- Verify `get_interface()` returns a valid Gradio component
|
| 1262 |
+
- Look for exceptions in the console when the interface loads
|
| 1263 |
+
|
| 1264 |
+
**3. NPC Not Appearing in World**
|
| 1265 |
+
- Verify `npc_config` property returns a valid dictionary
|
| 1266 |
+
- Check that the NPC position coordinates are within world bounds
|
| 1267 |
+
- Ensure the NPC ID is unique
|
| 1268 |
+
|
| 1269 |
+
**4. Private Messages Not Working**
|
| 1270 |
+
- Confirm `handle_command()` is implemented
|
| 1271 |
+
- Check that the addon is registered in `world.addon_npcs`
|
| 1272 |
+
- Verify the NPC ID matches between world registration and addon registry
|
| 1273 |
+
|
| 1274 |
+
**5. MCP Connection Issues**
|
| 1275 |
+
- Check the MCP server URL is correct and accessible
|
| 1276 |
+
- Verify the server is running and supports the expected tools
|
| 1277 |
+
- Look for async/await issues in your MCP client code
|
| 1278 |
+
- Check firewall and network connectivity
|
| 1279 |
+
|
| 1280 |
+
### Debug Checklist
|
| 1281 |
+
|
| 1282 |
+
- [ ] Addon file imported without errors
|
| 1283 |
+
- [ ] Global instance created at file bottom
|
| 1284 |
+
- [ ] All required methods implemented
|
| 1285 |
+
- [ ] Properties return correct types
|
| 1286 |
+
- [ ] No exceptions in console on startup
|
| 1287 |
+
- [ ] NPC appears in world at specified coordinates
|
| 1288 |
+
- [ ] UI tab appears in interface (if configured)
|
| 1289 |
+
- [ ] Private messages work correctly
|
| 1290 |
+
- [ ] Error handling covers edge cases
|
| 1291 |
+
|
| 1292 |
+
### Getting Help
|
| 1293 |
+
|
| 1294 |
+
1. **Check Logs**: Look for error messages in the console
|
| 1295 |
+
2. **Test Individually**: Create a simple test script for your addon
|
| 1296 |
+
3. **Validate Configuration**: Ensure all properties return expected values
|
| 1297 |
+
4. **Compare Examples**: Look at working addons like SimpleTrader or Read2Burn
|
| 1298 |
+
5. **Community Support**: Ask on project forums or GitHub issues
|
| 1299 |
+
|
| 1300 |
+
---
|
| 1301 |
+
|
| 1302 |
+
## 📚 Additional Resources
|
| 1303 |
+
|
| 1304 |
+
- **Interface Reference**: `src/interfaces/npc_addon.py` - Base class documentation
|
| 1305 |
+
- **Working Examples**: `src/addons/` - All current addon implementations
|
| 1306 |
+
- **Game Engine**: `src/core/game_engine.py` - Auto-registration system
|
| 1307 |
+
- **World Management**: `src/core/world.py` - NPC placement and management
|
| 1308 |
+
- **MCP Documentation**: [Model Context Protocol](https://github.com/modelcontextprotocol/python-sdk)
|
| 1309 |
+
|
| 1310 |
+
---
|
| 1311 |
+
|
| 1312 |
+
*This guide reflects the current addon system architecture and includes real examples from the working codebase. All code examples are tested and functional.*
|
Documentation/ROADMAP.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 MMORPG with MCP Integration - Roadmap
|
| 2 |
+
|
| 3 |
+
## 📋 Current Status (v1.0)
|
| 4 |
+
|
| 5 |
+
- ✅ **Core Game Engine**: Real-time multiplayer MMORPG with smooth movement
|
| 6 |
+
- ✅ **MCP Integration**: Full Model Context Protocol support with SSE
|
| 7 |
+
- ✅ **Multi-user Support**: Concurrent human and AI agent players
|
| 8 |
+
- ✅ **NPC Addon System**: Extensible NPC architecture with custom addons
|
| 9 |
+
- ✅ **Read2Burn Messaging**: Self-destructing secure message system
|
| 10 |
+
- ✅ **Weather Integration**: Real-time weather data via MCP servers
|
| 11 |
+
- ✅ **Keyboard Controls**: WASD and arrow key support for fluid gameplay
|
| 12 |
+
- ✅ **Web Interface**: Rich Gradio-based user interface
|
| 13 |
+
- ✅ **AI Agent APIs**: Complete MCP tool suite for AI agent integration
|
| 14 |
+
- ✅ **Session Management**: Robust player session handling and state persistence
|
| 15 |
+
- ✅ **Real-time Updates**: Live game world synchronization across all clients
|
| 16 |
+
|
| 17 |
+
## ✅ Recently Completed Refactoring (December 2024)
|
| 18 |
+
|
| 19 |
+
### ✅ Clean Architecture Implementation
|
| 20 |
+
- ✅ **Service Layer**: Separated concerns into dedicated service classes (PlayerService, ChatService, NPCService, MCPService, PluginService)
|
| 21 |
+
- ✅ **Facade Pattern**: Implemented GameFacade for simplified API access
|
| 22 |
+
- ✅ **Singleton Pattern**: Refactored GameEngine to use proper singleton implementation
|
| 23 |
+
- ✅ **Dependency Injection**: Improved service management and dependency handling
|
| 24 |
+
|
| 25 |
+
### ✅ Plugin System Enhancement
|
| 26 |
+
- ✅ **Plugin Architecture**: Enhanced plugin loading and management system
|
| 27 |
+
- ✅ **Trading System Plugin**: Implemented comprehensive trading system with economic mechanics
|
| 28 |
+
- ✅ **Weather Plugin**: Enhanced weather integration with better API management
|
| 29 |
+
- ✅ **Enhanced Chat Plugin**: Advanced chat features with filtering and commands
|
| 30 |
+
|
| 31 |
+
### ✅ UI/UX Improvements
|
| 32 |
+
- ✅ **Enhanced Interface Manager**: Improved UI component organization and management
|
| 33 |
+
- ✅ **Keyboard Controls**: Enhanced WASD and arrow key handling with better responsiveness
|
| 34 |
+
- ✅ **Bigger Game Map**: Increased map size for better gameplay experience
|
| 35 |
+
- ✅ **Player Tracking**: Improved current player state management
|
| 36 |
+
|
| 37 |
+
### ✅ NPC System Enhancement
|
| 38 |
+
- ✅ **Donald NPC**: Implemented Donald the Trader with distinctive personality and trading capabilities
|
| 39 |
+
- ✅ **Personality System**: Enhanced NPC personality framework for diverse character interactions
|
| 40 |
+
- ✅ **NPC Service**: Refactored NPC management into dedicated service layer
|
| 41 |
+
|
| 42 |
+
### ✅ Documentation & Testing
|
| 43 |
+
- ✅ **Developer Documentation**: Updated and corrected development setup documentation
|
| 44 |
+
- ✅ **Test Suite**: Created comprehensive test framework with unit, integration, and E2E tests
|
| 45 |
+
- ✅ **Test Coverage**: Implemented proper test coverage strategy and organization
|
| 46 |
+
- ✅ **Code Quality**: Improved code structure and maintainability
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## 🎯 Short-term Goals (v1.1 - v1.3)
|
| 51 |
+
|
| 52 |
+
### v1.1 - Enhanced Gameplay Mechanics
|
| 53 |
+
- [ ] **Quest System**: Dynamic quest generation and completion tracking
|
| 54 |
+
- [ ] **Inventory Management**: Player inventory with items and equipment
|
| 55 |
+
- [ ] **Combat System**: Turn-based or real-time combat mechanics
|
| 56 |
+
- [ ] **Character Progression**: Skill trees and specialization paths
|
| 57 |
+
- [ ] **Resource Gathering**: Mining, crafting, and resource management
|
| 58 |
+
- [ ] **Player Trading**: Secure item and currency exchange system
|
| 59 |
+
- [ ] **Guild System**: Player organizations and group activities
|
| 60 |
+
|
| 61 |
+
### v1.2 - Advanced MCP Features
|
| 62 |
+
- [ ] **MCP Marketplace**: Built-in server discovery and rating system
|
| 63 |
+
- [ ] **Custom MCP Tools**: Visual tool builder for non-developers
|
| 64 |
+
- [ ] **Server Composition**: Chain multiple MCP servers for complex workflows
|
| 65 |
+
- [ ] **Authentication & Security**: OAuth2/JWT integration for MCP servers
|
| 66 |
+
- [ ] **Rate Limiting**: Smart request management and throttling
|
| 67 |
+
- [ ] **Server Health Monitoring**: Real-time MCP server status dashboard
|
| 68 |
+
- [ ] **Hot-swapping**: Live addon replacement without server restart
|
| 69 |
+
|
| 70 |
+
### v1.3 - Social & Collaboration Features
|
| 71 |
+
- [ ] **Player-to-Player Messaging**: Secure private messaging between players
|
| 72 |
+
- [ ] **Voice Chat Integration**: WebRTC voice communication in groups
|
| 73 |
+
- [ ] **Shared Workspaces**: Collaborative areas for team projects
|
| 74 |
+
- [ ] **Event System**: Scheduled events and tournaments
|
| 75 |
+
- [ ] **Leaderboards**: Player rankings and achievement systems
|
| 76 |
+
- [ ] **Mentorship Program**: Experienced players helping newcomers
|
| 77 |
+
- [ ] **Content Sharing**: Share screenshots, videos, and gameplay moments
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 🚀 Medium-term Goals (v2.0 - v2.5)
|
| 82 |
+
|
| 83 |
+
### v2.0 - AI Agent Ecosystem
|
| 84 |
+
- [ ] **Multi-Agent Orchestration**: Coordinated AI agent teams and workflows
|
| 85 |
+
- [ ] **Agent Marketplace**: Community-contributed specialized AI agents
|
| 86 |
+
- [ ] **Behavior Templates**: Pre-built AI agent behavior patterns
|
| 87 |
+
- [ ] **Learning Framework**: AI agents that learn from player interactions
|
| 88 |
+
- [ ] **Agent Training Grounds**: Sandbox environments for AI development
|
| 89 |
+
- [ ] **Performance Analytics**: Detailed AI agent effectiveness metrics
|
| 90 |
+
- [ ] **Agent Tournaments**: Competitive events between AI agents
|
| 91 |
+
|
| 92 |
+
### v2.1 - Advanced World Systems
|
| 93 |
+
- [ ] **Procedural World Generation**: Dynamic map creation and expansion
|
| 94 |
+
- [ ] **Day/Night Cycles**: Time-based gameplay mechanics and events
|
| 95 |
+
- [ ] **Weather Effects**: Weather impact on gameplay and NPC behavior
|
| 96 |
+
- [ ] **Economic Simulation**: Supply and demand market simulation
|
| 97 |
+
- [ ] **Political Systems**: Player-driven governance and faction warfare
|
| 98 |
+
- [ ] **Environmental Puzzles**: World-based challenges requiring cooperation
|
| 99 |
+
- [ ] **Dynamic Events**: Emergent world events affecting all players
|
| 100 |
+
|
| 101 |
+
### v2.2 - Enterprise & Development Platform
|
| 102 |
+
- [ ] **Multi-tenancy**: Support for multiple game instances
|
| 103 |
+
- [ ] **Admin Dashboard**: Comprehensive game administration interface
|
| 104 |
+
- [ ] **Analytics Suite**: Detailed player behavior and system performance metrics
|
| 105 |
+
- [ ] **A/B Testing Framework**: Compare different game mechanics and features
|
| 106 |
+
- [ ] **API Gateway**: RESTful APIs for external integrations
|
| 107 |
+
- [ ] **Webhook System**: Event-driven notifications and integrations
|
| 108 |
+
- [ ] **Documentation Portal**: Interactive API documentation and tutorials
|
| 109 |
+
|
| 110 |
+
### v2.3 - Mobile & Cross-Platform
|
| 111 |
+
- [ ] **Progressive Web App**: Mobile-optimized web interface
|
| 112 |
+
- [ ] **Native Mobile Apps**: iOS and Android native applications
|
| 113 |
+
- [ ] **Desktop Applications**: Electron-based desktop clients for Windows/Mac/Linux
|
| 114 |
+
- [ ] **Cross-platform Sync**: Seamless experience across all devices
|
| 115 |
+
- [ ] **Offline Mode**: Limited offline gameplay with sync when reconnected
|
| 116 |
+
- [ ] **Cloud Save System**: Player progress backup and restoration
|
| 117 |
+
- [ ] **Push Notifications**: Real-time alerts for important game events
|
| 118 |
+
|
| 119 |
+
### v2.4 - Advanced Graphics & Performance
|
| 120 |
+
- [ ] **3D Game World**: Three-dimensional game environment with WebGL
|
| 121 |
+
- [ ] **Custom Avatars**: Player avatar customization and equipment visualization
|
| 122 |
+
- [ ] **Animation System**: Smooth character animations and effects
|
| 123 |
+
- [ ] **Sound Design**: Immersive audio experience with spatial sound
|
| 124 |
+
- [ ] **Performance Optimization**: Support for 1000+ concurrent players
|
| 125 |
+
- [ ] **CDN Integration**: Global content delivery for optimal performance
|
| 126 |
+
- [ ] **Edge Computing**: Regional game servers for reduced latency
|
| 127 |
+
|
| 128 |
+
### v2.5 - AI & Machine Learning Integration
|
| 129 |
+
- [ ] **Intelligent NPCs**: Advanced AI-powered NPCs with natural language understanding
|
| 130 |
+
- [ ] **Procedural Content**: AI-generated quests, stories, and world content
|
| 131 |
+
- [ ] **Player Behavior Prediction**: ML models for personalized experiences
|
| 132 |
+
- [ ] **Automated Moderation**: AI-powered chat moderation and behavior monitoring
|
| 133 |
+
- [ ] **Adaptive Difficulty**: Dynamic game difficulty based on player skill
|
| 134 |
+
- [ ] **Natural Language Interfaces**: Voice and text commands for game control
|
| 135 |
+
- [ ] **Sentiment Analysis**: Real-time mood and engagement tracking
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 🌟 Long-term Vision (v3.0+)
|
| 140 |
+
|
| 141 |
+
### v3.0 - Metaverse Integration
|
| 142 |
+
- [ ] **Virtual Reality Support**: VR headset compatibility and immersive gameplay
|
| 143 |
+
- [ ] **Augmented Reality Features**: AR overlays for real-world gaming experiences
|
| 144 |
+
- [ ] **Blockchain Integration**: NFT items and decentralized ownership
|
| 145 |
+
- [ ] **Cross-game Interoperability**: Character and item portability between games
|
| 146 |
+
- [ ] **Virtual Real Estate**: Player-owned and customizable virtual spaces
|
| 147 |
+
- [ ] **Digital Economy**: Real-world value exchange for in-game assets
|
| 148 |
+
- [ ] **Social VR Spaces**: Virtual meeting rooms and social hangouts
|
| 149 |
+
|
| 150 |
+
### v3.1 - Research & Academic Platform
|
| 151 |
+
- [ ] **Research Tools**: Built-in data collection and analysis for academic studies
|
| 152 |
+
- [ ] **Experiment Framework**: A/B testing and controlled experiments for researchers
|
| 153 |
+
- [ ] **Ethics Dashboard**: Privacy and consent management for research participants
|
| 154 |
+
- [ ] **Publication Integration**: Direct integration with academic publication workflows
|
| 155 |
+
- [ ] **Collaboration Networks**: Connect researchers studying similar topics
|
| 156 |
+
- [ ] **Open Data Initiative**: Anonymized dataset sharing for the research community
|
| 157 |
+
- [ ] **Grant Integration**: Funding application and management tools
|
| 158 |
+
|
| 159 |
+
### v3.2 - Autonomous Game Evolution
|
| 160 |
+
- [ ] **Self-modifying Code**: Game systems that evolve based on player feedback
|
| 161 |
+
- [ ] **Emergent Gameplay**: Unplanned game mechanics arising from player behavior
|
| 162 |
+
- [ ] **AI Game Masters**: Autonomous systems managing dynamic storylines
|
| 163 |
+
- [ ] **Player-driven Development**: Community voting on new features and changes
|
| 164 |
+
- [ ] **Genetic Algorithms**: Evolution of game mechanics through player selection
|
| 165 |
+
- [ ] **Quantum Computing**: Quantum-enhanced AI for complex game simulations
|
| 166 |
+
- [ ] **Consciousness Simulation**: Advanced AI entities with apparent consciousness
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
## 🔧 Technical Infrastructure Roadmap
|
| 171 |
+
|
| 172 |
+
### Performance & Scalability
|
| 173 |
+
- [ ] **Microservices Architecture**: Break down monolithic structure into services
|
| 174 |
+
- [ ] **Container Orchestration**: Kubernetes deployment and management
|
| 175 |
+
- [ ] **Database Sharding**: Horizontal scaling for large player populations
|
| 176 |
+
- [ ] **Caching Layer**: Redis-based multi-level caching system
|
| 177 |
+
- [ ] **Load Balancing**: Intelligent traffic distribution across servers
|
| 178 |
+
- [ ] **Auto-scaling**: Dynamic resource allocation based on demand
|
| 179 |
+
- [ ] **Global CDN**: Worldwide content delivery network integration
|
| 180 |
+
|
| 181 |
+
### Security & Privacy
|
| 182 |
+
- [ ] **End-to-end Encryption**: Secure all player communications and data
|
| 183 |
+
- [ ] **Multi-factor Authentication**: Enhanced account security options
|
| 184 |
+
- [ ] **Privacy Controls**: Granular privacy settings and data control
|
| 185 |
+
- [ ] **GDPR Compliance**: Full compliance with international privacy regulations
|
| 186 |
+
- [ ] **Security Auditing**: Regular penetration testing and security assessments
|
| 187 |
+
- [ ] **Incident Response**: Automated security incident detection and response
|
| 188 |
+
- [ ] **Zero-trust Architecture**: Comprehensive security model implementation
|
| 189 |
+
|
| 190 |
+
### Monitoring & Observability
|
| 191 |
+
- [ ] **Distributed Tracing**: End-to-end request flow tracking
|
| 192 |
+
- [ ] **Custom Metrics**: Business-specific KPI tracking and alerting
|
| 193 |
+
- [ ] **Real-time Dashboards**: Live system health and performance monitoring
|
| 194 |
+
- [ ] **Predictive Analytics**: AI-powered system failure prediction
|
| 195 |
+
- [ ] **Automated Remediation**: Self-healing systems for common issues
|
| 196 |
+
- [ ] **Compliance Monitoring**: Automated compliance checking and reporting
|
| 197 |
+
- [ ] **Cost Optimization**: Intelligent resource usage optimization
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
## 📊 Success Metrics & KPIs
|
| 202 |
+
|
| 203 |
+
### User Engagement
|
| 204 |
+
- **Daily Active Users**: Target 10,000+ DAU by v2.0
|
| 205 |
+
- **Session Duration**: Average session length > 30 minutes
|
| 206 |
+
- **Player Retention**: 30-day retention rate > 60%
|
| 207 |
+
- **User Satisfaction**: Player rating > 4.5/5 stars
|
| 208 |
+
- **Community Growth**: 25%+ monthly community growth
|
| 209 |
+
- **Content Creation**: 1000+ user-generated addons and content
|
| 210 |
+
|
| 211 |
+
### Technical Performance
|
| 212 |
+
- **System Uptime**: 99.9% availability target
|
| 213 |
+
- **Response Time**: < 100ms average response time
|
| 214 |
+
- **Concurrent Users**: Support 10,000+ simultaneous players
|
| 215 |
+
- **Scalability**: Linear performance scaling with resources
|
| 216 |
+
- **Error Rate**: < 0.1% error rate across all operations
|
| 217 |
+
- **Data Integrity**: 100% data consistency and backup reliability
|
| 218 |
+
|
| 219 |
+
### Developer Ecosystem
|
| 220 |
+
- **MCP Server Count**: 500+ community-contributed MCP servers
|
| 221 |
+
- **Active Developers**: 1000+ registered developers in ecosystem
|
| 222 |
+
- **API Usage**: 1M+ API calls per month
|
| 223 |
+
- **Documentation Quality**: 95%+ developer satisfaction with docs
|
| 224 |
+
- **Integration Success**: 90%+ successful first-time integrations
|
| 225 |
+
- **Open Source Contributions**: 100+ external contributors
|
| 226 |
+
|
| 227 |
+
### Business & Community
|
| 228 |
+
- **Revenue Growth**: Self-sustaining through enterprise licensing
|
| 229 |
+
- **Partner Ecosystem**: 50+ technology and content partners
|
| 230 |
+
- **Academic Adoption**: 25+ universities using platform for research
|
| 231 |
+
- **Media Coverage**: Regular coverage in gaming and tech media
|
| 232 |
+
- **Conference Presence**: Speaking opportunities at major conferences
|
| 233 |
+
- **Industry Recognition**: Awards and recognition from industry organizations
|
| 234 |
+
|
| 235 |
+
---
|
| 236 |
+
|
| 237 |
+
## 🤝 Community & Contribution Roadmap
|
| 238 |
+
|
| 239 |
+
### Open Source Strategy
|
| 240 |
+
- [ ] **License Transition**: Gradual open-sourcing of core components
|
| 241 |
+
- [ ] **Contribution Guidelines**: Clear guidelines for community contributions
|
| 242 |
+
- [ ] **Code of Conduct**: Inclusive community standards and enforcement
|
| 243 |
+
- [ ] **Maintainer Program**: Training and support for community maintainers
|
| 244 |
+
- [ ] **Bounty System**: Paid incentives for high-priority contributions
|
| 245 |
+
- [ ] **Hackathons**: Regular community coding events and competitions
|
| 246 |
+
- [ ] **Grant Program**: Funding for significant community projects
|
| 247 |
+
|
| 248 |
+
### Educational Initiatives
|
| 249 |
+
- [ ] **Documentation Expansion**: Comprehensive guides and tutorials
|
| 250 |
+
- [ ] **Video Tutorials**: Professional video content for all skill levels
|
| 251 |
+
- [ ] **Certification Program**: Official certification for developers and researchers
|
| 252 |
+
- [ ] **Workshop Series**: Regular online workshops and training sessions
|
| 253 |
+
- [ ] **University Partnerships**: Curriculum development and student programs
|
| 254 |
+
- [ ] **Mentorship Network**: Connect experienced developers with newcomers
|
| 255 |
+
- [ ] **Research Collaboration**: Joint research projects with academic institutions
|
| 256 |
+
|
| 257 |
+
### Ecosystem Development
|
| 258 |
+
- [ ] **Developer Tools**: IDE plugins and development environment setup
|
| 259 |
+
- [ ] **Template Library**: Starter templates for common use cases
|
| 260 |
+
- [ ] **Component Marketplace**: Reusable components and assets
|
| 261 |
+
- [ ] **Testing Framework**: Automated testing tools for community developers
|
| 262 |
+
- [ ] **Deployment Tools**: One-click deployment for community servers
|
| 263 |
+
- [ ] **Monitoring Integration**: Built-in monitoring for community deployments
|
| 264 |
+
- [ ] **Support Services**: Professional support options for enterprise users
|
| 265 |
+
|
| 266 |
+
---
|
| 267 |
+
|
| 268 |
+
## 📅 Timeline & Milestones
|
| 269 |
+
|
| 270 |
+
### 2025 Q3-Q4 (v1.1)
|
| 271 |
+
- **July**: Quest system and inventory management
|
| 272 |
+
- **August**: Combat system and character progression
|
| 273 |
+
- **September**: Resource gathering and player trading
|
| 274 |
+
- **October**: Guild system and enhanced social features
|
| 275 |
+
- **November**: Beta testing and community feedback
|
| 276 |
+
- **December**: v1.1 stable release
|
| 277 |
+
|
| 278 |
+
### 2026 Q1-Q2 (v1.2-1.3)
|
| 279 |
+
- **Q1**: MCP marketplace and advanced server features
|
| 280 |
+
- **Q2**: Social collaboration features and event system
|
| 281 |
+
|
| 282 |
+
### 2026 Q3-Q4 (v2.0)
|
| 283 |
+
- **Q3**: AI agent ecosystem and multi-agent orchestration
|
| 284 |
+
- **Q4**: Advanced world systems and procedural generation
|
| 285 |
+
|
| 286 |
+
### 2027 (v2.1-2.5)
|
| 287 |
+
- **H1**: Enterprise platform and mobile applications
|
| 288 |
+
- **H2**: Advanced graphics and AI/ML integration
|
| 289 |
+
|
| 290 |
+
### 2028+ (v3.0+)
|
| 291 |
+
- **Metaverse integration and VR/AR support**
|
| 292 |
+
- **Research platform and academic partnerships**
|
| 293 |
+
- **Autonomous game evolution systems**
|
| 294 |
+
|
| 295 |
+
---
|
| 296 |
+
|
| 297 |
+
## 💰 Funding & Sustainability
|
| 298 |
+
|
| 299 |
+
### Revenue Streams
|
| 300 |
+
- [ ] **Enterprise Licensing**: B2B platform licensing for corporations
|
| 301 |
+
- [ ] **Premium Features**: Advanced features for individual users
|
| 302 |
+
- [ ] **Marketplace Commission**: Revenue share from addon and content sales
|
| 303 |
+
- [ ] **Consulting Services**: Implementation and customization services
|
| 304 |
+
- [ ] **Training & Certification**: Educational program revenue
|
| 305 |
+
- [ ] **Research Partnerships**: Funded research collaboration projects
|
| 306 |
+
- [ ] **Cloud Hosting**: Managed hosting services for deployments
|
| 307 |
+
|
| 308 |
+
### Investment Strategy
|
| 309 |
+
- [ ] **Seed Funding**: Initial development and team expansion
|
| 310 |
+
- [ ] **Series A**: Platform scaling and feature development
|
| 311 |
+
- [ ] **Strategic Partnerships**: Technology and distribution partnerships
|
| 312 |
+
- [ ] **Government Grants**: Research and innovation funding
|
| 313 |
+
- [ ] **Crowdfunding**: Community-supported development initiatives
|
| 314 |
+
- [ ] **Revenue Reinvestment**: Sustainable growth through earned revenue
|
| 315 |
+
- [ ] **IPO Consideration**: Long-term public offering evaluation
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## 🔮 Innovation & Research Focus
|
| 320 |
+
|
| 321 |
+
### Emerging Technologies
|
| 322 |
+
- [ ] **Quantum Computing Integration**: Quantum-enhanced game simulations
|
| 323 |
+
- [ ] **Brain-Computer Interfaces**: Direct neural control of game characters
|
| 324 |
+
- [ ] **Advanced AI Models**: Integration of next-generation language models
|
| 325 |
+
- [ ] **Edge AI**: On-device AI processing for enhanced privacy
|
| 326 |
+
- [ ] **5G/6G Optimization**: Ultra-low latency gaming experiences
|
| 327 |
+
- [ ] **Holographic Displays**: Next-generation 3D visualization
|
| 328 |
+
- [ ] **Biometric Integration**: Health and wellness monitoring during gameplay
|
| 329 |
+
|
| 330 |
+
### Research Collaborations
|
| 331 |
+
- [ ] **AI Ethics Research**: Responsible AI development in gaming
|
| 332 |
+
- [ ] **Human-Computer Interaction**: Innovative interface design research
|
| 333 |
+
- [ ] **Social Psychology**: Understanding multiplayer social dynamics
|
| 334 |
+
- [ ] **Game Theory**: Mathematical modeling of player behavior
|
| 335 |
+
- [ ] **Network Science**: Optimization of multiplayer network architectures
|
| 336 |
+
- [ ] **Cognitive Science**: Understanding learning and engagement in games
|
| 337 |
+
- [ ] **Digital Wellness**: Promoting healthy gaming habits and mental well-being
|
| 338 |
+
|
| 339 |
+
---
|
| 340 |
+
|
| 341 |
+
## 📞 Roadmap Feedback & Updates
|
| 342 |
+
|
| 343 |
+
### Community Input
|
| 344 |
+
- **Quarterly Surveys**: Regular community feedback on roadmap priorities
|
| 345 |
+
- **Developer Workshops**: Input sessions with active developers
|
| 346 |
+
- **Player Focus Groups**: Representative player input on new features
|
| 347 |
+
- **Academic Advisory Board**: Research community guidance on platform evolution
|
| 348 |
+
- **Industry Roundtables**: Input from gaming and technology industry leaders
|
| 349 |
+
|
| 350 |
+
### Roadmap Maintenance
|
| 351 |
+
- **Monthly Reviews**: Regular evaluation of progress and priorities
|
| 352 |
+
- **Quarterly Updates**: Public roadmap updates with progress reports
|
| 353 |
+
- **Annual Planning**: Comprehensive yearly planning and goal setting
|
| 354 |
+
- **Agile Adaptation**: Responsive changes based on technology and market evolution
|
| 355 |
+
- **Community Voting**: Democratic input on feature prioritization
|
| 356 |
+
|
| 357 |
+
### Communication Channels
|
| 358 |
+
- **Official Blog**: Regular updates and deep-dive articles
|
| 359 |
+
- **Newsletter**: Monthly updates for subscribers
|
| 360 |
+
- **Social Media**: Real-time updates and community engagement
|
| 361 |
+
- **Conference Presentations**: Industry conference roadmap presentations
|
| 362 |
+
- **Documentation Portal**: Living documentation with latest roadmap information
|
| 363 |
+
|
| 364 |
+
---
|
| 365 |
+
|
| 366 |
+
*This roadmap is a living document that evolves based on community feedback, technological advances, and changing user needs. Timelines are estimates and may be adjusted based on development priorities and resource availability.*
|
| 367 |
+
|
| 368 |
+
**Last Updated**: June 6, 2025
|
| 369 |
+
**Version**: 1.0
|
| 370 |
+
**Next Review**: July 6, 2025
|
| 371 |
+
**Community Input**: [feedback@mmop-game.com](mailto:feedback@mmop-game.com)
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
**🚀 Join us in building the future of multiplayer gaming and AI interaction! 🎮**
|
Documentation/Simple_Game_Client_Guide.md
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simple Game Client Guide
|
| 2 |
+
|
| 3 |
+
This guide shows you how to create a simple client to connect to the MMORPG game server using the Model Context Protocol (MCP). No LLM integration required - just basic game operations.
|
| 4 |
+
|
| 5 |
+
## Table of Contents
|
| 6 |
+
|
| 7 |
+
1. [Overview](#overview)
|
| 8 |
+
2. [Prerequisites](#prerequisites)
|
| 9 |
+
3. [MCP Server Connection](#mcp-server-connection)
|
| 10 |
+
4. [Basic Client Example](#basic-client-example)
|
| 11 |
+
5. [Available Game Operations](#available-game-operations)
|
| 12 |
+
6. [Command Reference](#command-reference)
|
| 13 |
+
7. [Error Handling](#error-handling)
|
| 14 |
+
8. [Advanced Features](#advanced-features)
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## Overview
|
| 19 |
+
|
| 20 |
+
The MMORPG game server exposes its functionality through MCP (Model Context Protocol), allowing external clients to:
|
| 21 |
+
|
| 22 |
+
- 🎮 **Connect** to the game server
|
| 23 |
+
- 👤 **Join/Leave** the game as a player
|
| 24 |
+
- 🗺️ **Move** around the game world
|
| 25 |
+
- 💬 **Send chat** messages to other players
|
| 26 |
+
- 🎯 **Interact** with NPCs and game objects
|
| 27 |
+
- 📊 **Get game state** information
|
| 28 |
+
|
| 29 |
+
### Game Server Details
|
| 30 |
+
|
| 31 |
+
- **MCP Endpoint**: `http://localhost:7868/gradio_api/mcp/sse` (when running locally)
|
| 32 |
+
- **Protocol**: Server-Sent Events (SSE) over HTTP
|
| 33 |
+
- **Authentication**: None required for basic operations
|
| 34 |
+
- **Data Format**: JSON messages following MCP specification
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## Prerequisites
|
| 39 |
+
|
| 40 |
+
Before building your client, ensure you have:
|
| 41 |
+
|
| 42 |
+
### Required Dependencies
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
pip install mcp asyncio aiohttp contextlib
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Python Version
|
| 49 |
+
- Python 3.8 or higher
|
| 50 |
+
- Support for async/await syntax
|
| 51 |
+
|
| 52 |
+
### Game Server Running
|
| 53 |
+
Make sure the MMORPG game server is running:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
cd c:/Users/Chris4K/Projekte/projecthub/projects/MMOP_second_try
|
| 57 |
+
python app.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
The server should be accessible at `http://localhost:7868` (UI) and the MCP endpoint at `http://localhost:7868/gradio_api/mcp/sse`.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## MCP Server Connection
|
| 65 |
+
|
| 66 |
+
Here's how to establish a connection to the game server:
|
| 67 |
+
|
| 68 |
+
### Basic Connection Setup
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
import asyncio
|
| 72 |
+
from mcp import ClientSession
|
| 73 |
+
from mcp.client.sse import sse_client
|
| 74 |
+
from contextlib import AsyncExitStack
|
| 75 |
+
|
| 76 |
+
class GameClient:
|
| 77 |
+
def __init__(self, server_url="http://localhost:7868/gradio_api/mcp/sse"):
|
| 78 |
+
self.server_url = server_url
|
| 79 |
+
self.session = None
|
| 80 |
+
self.connected = False
|
| 81 |
+
self.tools = []
|
| 82 |
+
self.exit_stack = None
|
| 83 |
+
self.client_id = None
|
| 84 |
+
|
| 85 |
+
async def connect(self):
|
| 86 |
+
"""Connect to the game server"""
|
| 87 |
+
try:
|
| 88 |
+
# Clean up any existing connection
|
| 89 |
+
if self.exit_stack:
|
| 90 |
+
await self.exit_stack.aclose()
|
| 91 |
+
|
| 92 |
+
self.exit_stack = AsyncExitStack()
|
| 93 |
+
|
| 94 |
+
# Establish SSE connection
|
| 95 |
+
transport = await self.exit_stack.enter_async_context(
|
| 96 |
+
sse_client(self.server_url)
|
| 97 |
+
)
|
| 98 |
+
read_stream, write_callable = transport
|
| 99 |
+
|
| 100 |
+
# Create MCP session
|
| 101 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 102 |
+
ClientSession(read_stream, write_callable)
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Initialize the session
|
| 106 |
+
await self.session.initialize()
|
| 107 |
+
|
| 108 |
+
# Get available tools/commands
|
| 109 |
+
response = await self.session.list_tools()
|
| 110 |
+
self.tools = response.tools
|
| 111 |
+
|
| 112 |
+
self.connected = True
|
| 113 |
+
print(f"✅ Connected to game server!")
|
| 114 |
+
print(f"Available commands: {[tool.name for tool in self.tools]}")
|
| 115 |
+
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"❌ Connection failed: {e}")
|
| 120 |
+
self.connected = False
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
async def disconnect(self):
|
| 124 |
+
"""Disconnect from the game server"""
|
| 125 |
+
if self.exit_stack:
|
| 126 |
+
await self.exit_stack.aclose()
|
| 127 |
+
self.connected = False
|
| 128 |
+
print("🔌 Disconnected from game server")
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Connection Test
|
| 132 |
+
|
| 133 |
+
```python
|
| 134 |
+
async def test_connection():
|
| 135 |
+
client = GameClient()
|
| 136 |
+
if await client.connect():
|
| 137 |
+
print("Connection successful!")
|
| 138 |
+
await client.disconnect()
|
| 139 |
+
else:
|
| 140 |
+
print("Connection failed!")
|
| 141 |
+
|
| 142 |
+
# Run the test
|
| 143 |
+
asyncio.run(test_connection())
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Basic Client Example
|
| 149 |
+
|
| 150 |
+
Here's a complete working client that can perform basic game operations:
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
import asyncio
|
| 154 |
+
import json
|
| 155 |
+
import uuid
|
| 156 |
+
from typing import Dict, List, Optional
|
| 157 |
+
|
| 158 |
+
class SimpleGameClient:
|
| 159 |
+
def __init__(self, server_url="http://localhost:7868/gradio_api/mcp/sse"):
|
| 160 |
+
self.server_url = server_url
|
| 161 |
+
self.session = None
|
| 162 |
+
self.connected = False
|
| 163 |
+
self.tools = []
|
| 164 |
+
self.exit_stack = None
|
| 165 |
+
self.client_id = str(uuid.uuid4())[:8]
|
| 166 |
+
self.agent_id = None
|
| 167 |
+
self.player_name = None
|
| 168 |
+
|
| 169 |
+
async def connect(self):
|
| 170 |
+
"""Connect to the game server"""
|
| 171 |
+
try:
|
| 172 |
+
if self.exit_stack:
|
| 173 |
+
await self.exit_stack.aclose()
|
| 174 |
+
|
| 175 |
+
self.exit_stack = AsyncExitStack()
|
| 176 |
+
|
| 177 |
+
transport = await self.exit_stack.enter_async_context(
|
| 178 |
+
sse_client(self.server_url)
|
| 179 |
+
)
|
| 180 |
+
read_stream, write_callable = transport
|
| 181 |
+
|
| 182 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 183 |
+
ClientSession(read_stream, write_callable)
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
await self.session.initialize()
|
| 187 |
+
response = await self.session.list_tools()
|
| 188 |
+
self.tools = response.tools
|
| 189 |
+
|
| 190 |
+
self.connected = True
|
| 191 |
+
return True
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f"❌ Connection failed: {e}")
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
async def register_player(self, player_name: str):
|
| 198 |
+
"""Register as a new player in the game"""
|
| 199 |
+
if not self.connected:
|
| 200 |
+
print("❌ Not connected to server")
|
| 201 |
+
return False
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
# Find the register tool
|
| 205 |
+
register_tool = next((t for t in self.tools if 'register' in t.name), None)
|
| 206 |
+
if not register_tool:
|
| 207 |
+
print("❌ Register tool not available")
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
# Register the player
|
| 211 |
+
result = await self.session.call_tool(
|
| 212 |
+
register_tool.name,
|
| 213 |
+
{"name": player_name, "client_id": self.client_id}
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# Parse the result
|
| 217 |
+
content = self._extract_content(result)
|
| 218 |
+
if "registered" in content.lower():
|
| 219 |
+
self.player_name = player_name
|
| 220 |
+
# Extract agent_id from response if available
|
| 221 |
+
if "agent_id" in content or "ID:" in content:
|
| 222 |
+
# Parse agent ID from response
|
| 223 |
+
lines = content.split('\n')
|
| 224 |
+
for line in lines:
|
| 225 |
+
if "agent_id" in line.lower() or "id:" in line:
|
| 226 |
+
parts = line.split(':')
|
| 227 |
+
if len(parts) > 1:
|
| 228 |
+
self.agent_id = parts[1].strip()
|
| 229 |
+
break
|
| 230 |
+
|
| 231 |
+
print(f"✅ Registered as player: {player_name}")
|
| 232 |
+
return True
|
| 233 |
+
else:
|
| 234 |
+
print(f"❌ Registration failed: {content}")
|
| 235 |
+
return False
|
| 236 |
+
|
| 237 |
+
except Exception as e:
|
| 238 |
+
print(f"❌ Registration error: {e}")
|
| 239 |
+
return False
|
| 240 |
+
|
| 241 |
+
async def move_player(self, direction: str):
|
| 242 |
+
"""Move the player in the specified direction"""
|
| 243 |
+
if not self.connected or not self.agent_id:
|
| 244 |
+
print("❌ Not connected or not registered")
|
| 245 |
+
return False
|
| 246 |
+
|
| 247 |
+
try:
|
| 248 |
+
# Find the move tool
|
| 249 |
+
move_tool = next((t for t in self.tools if 'move' in t.name), None)
|
| 250 |
+
if not move_tool:
|
| 251 |
+
print("❌ Move tool not available")
|
| 252 |
+
return False
|
| 253 |
+
|
| 254 |
+
# Move the player
|
| 255 |
+
result = await self.session.call_tool(
|
| 256 |
+
move_tool.name,
|
| 257 |
+
{"client_id": self.client_id, "direction": direction}
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
content = self._extract_content(result)
|
| 261 |
+
print(f"🚶 Move result: {content}")
|
| 262 |
+
return True
|
| 263 |
+
|
| 264 |
+
except Exception as e:
|
| 265 |
+
print(f"❌ Move error: {e}")
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
async def send_chat(self, message: str):
|
| 269 |
+
"""Send a chat message to other players"""
|
| 270 |
+
if not self.connected or not self.agent_id:
|
| 271 |
+
print("❌ Not connected or not registered")
|
| 272 |
+
return False
|
| 273 |
+
|
| 274 |
+
try:
|
| 275 |
+
# Find the chat tool
|
| 276 |
+
chat_tool = next((t for t in self.tools if 'chat' in t.name), None)
|
| 277 |
+
if not chat_tool:
|
| 278 |
+
print("❌ Chat tool not available")
|
| 279 |
+
return False
|
| 280 |
+
|
| 281 |
+
# Send the chat message
|
| 282 |
+
result = await self.session.call_tool(
|
| 283 |
+
chat_tool.name,
|
| 284 |
+
{"client_id": self.client_id, "message": message}
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
content = self._extract_content(result)
|
| 288 |
+
print(f"💬 Chat sent: {content}")
|
| 289 |
+
return True
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
print(f"❌ Chat error: {e}")
|
| 293 |
+
return False
|
| 294 |
+
|
| 295 |
+
async def get_game_state(self):
|
| 296 |
+
"""Get current game state information"""
|
| 297 |
+
if not self.connected:
|
| 298 |
+
print("❌ Not connected to server")
|
| 299 |
+
return None
|
| 300 |
+
|
| 301 |
+
try:
|
| 302 |
+
# Find the game state tool
|
| 303 |
+
state_tool = next((t for t in self.tools if 'state' in t.name or 'game' in t.name), None)
|
| 304 |
+
if not state_tool:
|
| 305 |
+
print("❌ Game state tool not available")
|
| 306 |
+
return None
|
| 307 |
+
|
| 308 |
+
# Get game state
|
| 309 |
+
result = await self.session.call_tool(state_tool.name, {})
|
| 310 |
+
content = self._extract_content(result)
|
| 311 |
+
|
| 312 |
+
try:
|
| 313 |
+
# Try to parse as JSON
|
| 314 |
+
game_state = json.loads(content)
|
| 315 |
+
return game_state
|
| 316 |
+
except json.JSONDecodeError:
|
| 317 |
+
# Return as text if not JSON
|
| 318 |
+
return {"info": content}
|
| 319 |
+
|
| 320 |
+
except Exception as e:
|
| 321 |
+
print(f"❌ Game state error: {e}")
|
| 322 |
+
return None
|
| 323 |
+
|
| 324 |
+
async def interact_with_npc(self, npc_id: str, message: str):
|
| 325 |
+
"""Interact with an NPC"""
|
| 326 |
+
if not self.connected or not self.agent_id:
|
| 327 |
+
print("❌ Not connected or not registered")
|
| 328 |
+
return False
|
| 329 |
+
|
| 330 |
+
try:
|
| 331 |
+
# Find the interact tool
|
| 332 |
+
interact_tool = next((t for t in self.tools if 'interact' in t.name or 'npc' in t.name), None)
|
| 333 |
+
if not interact_tool:
|
| 334 |
+
print("❌ Interact tool not available")
|
| 335 |
+
return False
|
| 336 |
+
|
| 337 |
+
# Interact with NPC
|
| 338 |
+
result = await self.session.call_tool(
|
| 339 |
+
interact_tool.name,
|
| 340 |
+
{"client_id": self.client_id, "npc_id": npc_id, "message": message}
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
content = self._extract_content(result)
|
| 344 |
+
print(f"🤖 NPC {npc_id}: {content}")
|
| 345 |
+
return True
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
print(f"❌ Interaction error: {e}")
|
| 349 |
+
return False
|
| 350 |
+
|
| 351 |
+
def _extract_content(self, result):
|
| 352 |
+
"""Extract content from MCP result"""
|
| 353 |
+
if hasattr(result, 'content') and result.content:
|
| 354 |
+
if isinstance(result.content, list):
|
| 355 |
+
return ''.join(str(item) for item in result.content)
|
| 356 |
+
return str(result.content)
|
| 357 |
+
return str(result)
|
| 358 |
+
|
| 359 |
+
async def disconnect(self):
|
| 360 |
+
"""Disconnect from the server"""
|
| 361 |
+
if self.exit_stack:
|
| 362 |
+
await self.exit_stack.aclose()
|
| 363 |
+
self.connected = False
|
| 364 |
+
print("🔌 Disconnected from server")
|
| 365 |
+
|
| 366 |
+
# Import required modules at the top
|
| 367 |
+
from mcp import ClientSession
|
| 368 |
+
from mcp.client.sse import sse_client
|
| 369 |
+
from contextlib import AsyncExitStack
|
| 370 |
+
```
|
| 371 |
+
|
| 372 |
+
---
|
| 373 |
+
|
| 374 |
+
## Available Game Operations
|
| 375 |
+
|
| 376 |
+
The game server exposes these main operations through MCP tools:
|
| 377 |
+
|
| 378 |
+
### 1. Player Management
|
| 379 |
+
- **`register_ai_agent`** - Register a new player
|
| 380 |
+
- **`move_agent`** - Move player in game world
|
| 381 |
+
- **`get_game_state`** - Get current world state
|
| 382 |
+
|
| 383 |
+
### 2. Communication
|
| 384 |
+
- **`send_chat`** - Send chat messages
|
| 385 |
+
- **`interact_with_npc`** - Talk to NPCs
|
| 386 |
+
|
| 387 |
+
### 3. Game Information
|
| 388 |
+
- **`get_game_state`** - World state and player list
|
| 389 |
+
- **Player proximity** - Detect nearby players/NPCs
|
| 390 |
+
|
| 391 |
+
### Tool Parameters
|
| 392 |
+
|
| 393 |
+
Each tool expects specific parameters:
|
| 394 |
+
|
| 395 |
+
```python
|
| 396 |
+
# Register Player
|
| 397 |
+
{
|
| 398 |
+
"name": "PlayerName",
|
| 399 |
+
"client_id": "unique_client_id"
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
# Move Player
|
| 403 |
+
{
|
| 404 |
+
"client_id": "your_client_id",
|
| 405 |
+
"direction": "north|south|east|west"
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
# Send Chat
|
| 409 |
+
{
|
| 410 |
+
"client_id": "your_client_id",
|
| 411 |
+
"message": "Hello world!"
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
# Interact with NPC
|
| 415 |
+
{
|
| 416 |
+
"client_id": "your_client_id",
|
| 417 |
+
"npc_id": "npc_identifier",
|
| 418 |
+
"message": "Your message to NPC"
|
| 419 |
+
}
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
---
|
| 423 |
+
|
| 424 |
+
## Command Reference
|
| 425 |
+
|
| 426 |
+
### Basic Usage Example
|
| 427 |
+
|
| 428 |
+
```python
|
| 429 |
+
async def main():
|
| 430 |
+
# Create and connect client
|
| 431 |
+
client = SimpleGameClient()
|
| 432 |
+
|
| 433 |
+
if not await client.connect():
|
| 434 |
+
print("Failed to connect!")
|
| 435 |
+
return
|
| 436 |
+
|
| 437 |
+
# Register as a player
|
| 438 |
+
if not await client.register_player("MyPlayer"):
|
| 439 |
+
print("Failed to register!")
|
| 440 |
+
return
|
| 441 |
+
|
| 442 |
+
# Get current game state
|
| 443 |
+
state = await client.get_game_state()
|
| 444 |
+
print(f"Game state: {state}")
|
| 445 |
+
|
| 446 |
+
# Move around
|
| 447 |
+
await client.move_player("north")
|
| 448 |
+
await client.move_player("east")
|
| 449 |
+
|
| 450 |
+
# Send a chat message
|
| 451 |
+
await client.send_chat("Hello everyone!")
|
| 452 |
+
|
| 453 |
+
# Interact with an NPC
|
| 454 |
+
await client.interact_with_npc("weather_oracle", "What's the weather in Berlin?")
|
| 455 |
+
|
| 456 |
+
# Disconnect
|
| 457 |
+
await client.disconnect()
|
| 458 |
+
|
| 459 |
+
# Run the client
|
| 460 |
+
asyncio.run(main())
|
| 461 |
+
```
|
| 462 |
+
|
| 463 |
+
### Interactive Console Client
|
| 464 |
+
|
| 465 |
+
```python
|
| 466 |
+
async def interactive_client():
|
| 467 |
+
client = SimpleGameClient()
|
| 468 |
+
|
| 469 |
+
print("🎮 MMORPG Simple Client")
|
| 470 |
+
print("Commands: connect, register <name>, move <direction>, chat <message>, state, quit")
|
| 471 |
+
|
| 472 |
+
while True:
|
| 473 |
+
command = input("> ").strip().split()
|
| 474 |
+
|
| 475 |
+
if not command:
|
| 476 |
+
continue
|
| 477 |
+
|
| 478 |
+
cmd = command[0].lower()
|
| 479 |
+
|
| 480 |
+
if cmd == "connect":
|
| 481 |
+
if await client.connect():
|
| 482 |
+
print("✅ Connected!")
|
| 483 |
+
else:
|
| 484 |
+
print("❌ Connection failed!")
|
| 485 |
+
|
| 486 |
+
elif cmd == "register" and len(command) > 1:
|
| 487 |
+
name = " ".join(command[1:])
|
| 488 |
+
if await client.register_player(name):
|
| 489 |
+
print(f"✅ Registered as {name}")
|
| 490 |
+
else:
|
| 491 |
+
print("❌ Registration failed!")
|
| 492 |
+
|
| 493 |
+
elif cmd == "move" and len(command) > 1:
|
| 494 |
+
direction = command[1]
|
| 495 |
+
await client.move_player(direction)
|
| 496 |
+
|
| 497 |
+
elif cmd == "chat" and len(command) > 1:
|
| 498 |
+
message = " ".join(command[1:])
|
| 499 |
+
await client.send_chat(message)
|
| 500 |
+
|
| 501 |
+
elif cmd == "state":
|
| 502 |
+
state = await client.get_game_state()
|
| 503 |
+
print(f"📊 Game State: {json.dumps(state, indent=2)}")
|
| 504 |
+
|
| 505 |
+
elif cmd == "npc" and len(command) > 2:
|
| 506 |
+
npc_id = command[1]
|
| 507 |
+
message = " ".join(command[2:])
|
| 508 |
+
await client.interact_with_npc(npc_id, message)
|
| 509 |
+
|
| 510 |
+
elif cmd == "quit":
|
| 511 |
+
await client.disconnect()
|
| 512 |
+
break
|
| 513 |
+
|
| 514 |
+
else:
|
| 515 |
+
print("❓ Unknown command")
|
| 516 |
+
|
| 517 |
+
# Run interactive client
|
| 518 |
+
asyncio.run(interactive_client())
|
| 519 |
+
```
|
| 520 |
+
|
| 521 |
+
---
|
| 522 |
+
|
| 523 |
+
## Error Handling
|
| 524 |
+
|
| 525 |
+
### Common Issues and Solutions
|
| 526 |
+
|
| 527 |
+
#### 1. Connection Errors
|
| 528 |
+
```python
|
| 529 |
+
async def robust_connect(client, max_retries=3):
|
| 530 |
+
for attempt in range(max_retries):
|
| 531 |
+
try:
|
| 532 |
+
if await client.connect():
|
| 533 |
+
return True
|
| 534 |
+
print(f"Attempt {attempt + 1} failed, retrying...")
|
| 535 |
+
await asyncio.sleep(2)
|
| 536 |
+
except Exception as e:
|
| 537 |
+
print(f"Connection attempt {attempt + 1} error: {e}")
|
| 538 |
+
|
| 539 |
+
print("❌ All connection attempts failed")
|
| 540 |
+
return False
|
| 541 |
+
```
|
| 542 |
+
|
| 543 |
+
#### 2. Tool Not Available
|
| 544 |
+
```python
|
| 545 |
+
def find_tool_safe(tools, keywords):
|
| 546 |
+
"""Safely find a tool by keywords"""
|
| 547 |
+
for keyword in keywords:
|
| 548 |
+
tool = next((t for t in tools if keyword in t.name.lower()), None)
|
| 549 |
+
if tool:
|
| 550 |
+
return tool
|
| 551 |
+
return None
|
| 552 |
+
|
| 553 |
+
# Usage
|
| 554 |
+
move_tool = find_tool_safe(client.tools, ['move', 'agent', 'player'])
|
| 555 |
+
if not move_tool:
|
| 556 |
+
print("❌ No movement tool available")
|
| 557 |
+
```
|
| 558 |
+
|
| 559 |
+
#### 3. Response Parsing
|
| 560 |
+
```python
|
| 561 |
+
def safe_parse_response(result):
|
| 562 |
+
"""Safely parse MCP response"""
|
| 563 |
+
try:
|
| 564 |
+
content = client._extract_content(result)
|
| 565 |
+
|
| 566 |
+
# Try JSON first
|
| 567 |
+
try:
|
| 568 |
+
return json.loads(content)
|
| 569 |
+
except json.JSONDecodeError:
|
| 570 |
+
# Return as structured text
|
| 571 |
+
return {"message": content, "type": "text"}
|
| 572 |
+
|
| 573 |
+
except Exception as e:
|
| 574 |
+
return {"error": str(e), "type": "error"}
|
| 575 |
+
```
|
| 576 |
+
|
| 577 |
+
---
|
| 578 |
+
|
| 579 |
+
## Advanced Features
|
| 580 |
+
|
| 581 |
+
### 1. Automatic Reconnection
|
| 582 |
+
|
| 583 |
+
```python
|
| 584 |
+
class RobustGameClient(SimpleGameClient):
|
| 585 |
+
def __init__(self, *args, **kwargs):
|
| 586 |
+
super().__init__(*args, **kwargs)
|
| 587 |
+
self.auto_reconnect = True
|
| 588 |
+
self.reconnect_delay = 5
|
| 589 |
+
|
| 590 |
+
async def _ensure_connected(self):
|
| 591 |
+
"""Ensure connection is active, reconnect if needed"""
|
| 592 |
+
if not self.connected and self.auto_reconnect:
|
| 593 |
+
print("🔄 Attempting to reconnect...")
|
| 594 |
+
await self.connect()
|
| 595 |
+
if self.player_name:
|
| 596 |
+
await self.register_player(self.player_name)
|
| 597 |
+
|
| 598 |
+
async def move_player(self, direction):
|
| 599 |
+
await self._ensure_connected()
|
| 600 |
+
return await super().move_player(direction)
|
| 601 |
+
```
|
| 602 |
+
|
| 603 |
+
### 2. Event Monitoring
|
| 604 |
+
|
| 605 |
+
```python
|
| 606 |
+
class EventMonitorClient(SimpleGameClient):
|
| 607 |
+
def __init__(self, *args, **kwargs):
|
| 608 |
+
super().__init__(*args, **kwargs)
|
| 609 |
+
self.event_handlers = {}
|
| 610 |
+
|
| 611 |
+
def on_chat_message(self, handler):
|
| 612 |
+
"""Register handler for chat messages"""
|
| 613 |
+
self.event_handlers['chat'] = handler
|
| 614 |
+
|
| 615 |
+
def on_player_move(self, handler):
|
| 616 |
+
"""Register handler for player movements"""
|
| 617 |
+
self.event_handlers['move'] = handler
|
| 618 |
+
|
| 619 |
+
async def poll_events(self):
|
| 620 |
+
"""Poll for game events"""
|
| 621 |
+
while self.connected:
|
| 622 |
+
try:
|
| 623 |
+
state = await self.get_game_state()
|
| 624 |
+
# Process state changes and trigger handlers
|
| 625 |
+
# Implementation depends on game state format
|
| 626 |
+
await asyncio.sleep(1)
|
| 627 |
+
except Exception as e:
|
| 628 |
+
print(f"Event polling error: {e}")
|
| 629 |
+
await asyncio.sleep(5)
|
| 630 |
+
```
|
| 631 |
+
|
| 632 |
+
### 3. Batch Operations
|
| 633 |
+
|
| 634 |
+
```python
|
| 635 |
+
async def batch_moves(client, moves):
|
| 636 |
+
"""Execute multiple moves in sequence"""
|
| 637 |
+
for direction in moves:
|
| 638 |
+
result = await client.move_player(direction)
|
| 639 |
+
if not result:
|
| 640 |
+
print(f"❌ Failed to move {direction}")
|
| 641 |
+
break
|
| 642 |
+
await asyncio.sleep(0.5) # Small delay between moves
|
| 643 |
+
|
| 644 |
+
# Usage
|
| 645 |
+
await batch_moves(client, ["north", "north", "east", "south"])
|
| 646 |
+
```
|
| 647 |
+
|
| 648 |
+
### 4. Configuration Management
|
| 649 |
+
|
| 650 |
+
```python
|
| 651 |
+
import json
|
| 652 |
+
|
| 653 |
+
class ConfigurableClient(SimpleGameClient):
|
| 654 |
+
def __init__(self, config_file="client_config.json"):
|
| 655 |
+
# Load configuration
|
| 656 |
+
try:
|
| 657 |
+
with open(config_file, 'r') as f:
|
| 658 |
+
config = json.load(f)
|
| 659 |
+
except FileNotFoundError:
|
| 660 |
+
config = self.default_config()
|
| 661 |
+
|
| 662 |
+
super().__init__(config.get('server_url', 'http://localhost:7860/gradio_api/mcp/sse'))
|
| 663 |
+
self.config = config
|
| 664 |
+
|
| 665 |
+
def default_config(self):
|
| 666 |
+
return {
|
| 667 |
+
"server_url": "http://localhost:7860/gradio_api/mcp/sse",
|
| 668 |
+
"player_name": "DefaultPlayer",
|
| 669 |
+
"auto_reconnect": True,
|
| 670 |
+
"move_delay": 0.5
|
| 671 |
+
}
|
| 672 |
+
```
|
| 673 |
+
|
| 674 |
+
---
|
| 675 |
+
|
| 676 |
+
## Quick Start Script
|
| 677 |
+
|
| 678 |
+
Save this as `quick_client.py` for immediate testing:
|
| 679 |
+
|
| 680 |
+
```python
|
| 681 |
+
#!/usr/bin/env python3
|
| 682 |
+
"""
|
| 683 |
+
Quick Game Client - Connect and play immediately
|
| 684 |
+
Usage: python quick_client.py [player_name]
|
| 685 |
+
"""
|
| 686 |
+
|
| 687 |
+
import asyncio
|
| 688 |
+
import sys
|
| 689 |
+
from simple_game_client import SimpleGameClient
|
| 690 |
+
|
| 691 |
+
async def quick_play(player_name="TestPlayer"):
|
| 692 |
+
client = SimpleGameClient()
|
| 693 |
+
|
| 694 |
+
print(f"🎮 Connecting as {player_name}...")
|
| 695 |
+
|
| 696 |
+
# Connect and register
|
| 697 |
+
if not await client.connect():
|
| 698 |
+
print("❌ Failed to connect!")
|
| 699 |
+
return
|
| 700 |
+
|
| 701 |
+
if not await client.register_player(player_name):
|
| 702 |
+
print("❌ Failed to register!")
|
| 703 |
+
return
|
| 704 |
+
|
| 705 |
+
print("✅ Connected and registered successfully!")
|
| 706 |
+
|
| 707 |
+
# Basic game session
|
| 708 |
+
await client.send_chat(f"Hello! {player_name} has joined the game!")
|
| 709 |
+
|
| 710 |
+
# Move around a bit
|
| 711 |
+
moves = ["north", "east", "south", "west"]
|
| 712 |
+
for move in moves:
|
| 713 |
+
print(f"🚶 Moving {move}...")
|
| 714 |
+
await client.move_player(move)
|
| 715 |
+
await asyncio.sleep(1)
|
| 716 |
+
|
| 717 |
+
# Get final state
|
| 718 |
+
state = await client.get_game_state()
|
| 719 |
+
print(f"📊 Final state: {state}")
|
| 720 |
+
|
| 721 |
+
await client.disconnect()
|
| 722 |
+
print("👋 Session ended!")
|
| 723 |
+
|
| 724 |
+
if __name__ == "__main__":
|
| 725 |
+
player_name = sys.argv[1] if len(sys.argv) > 1 else "TestPlayer"
|
| 726 |
+
asyncio.run(quick_play(player_name))
|
| 727 |
+
```
|
| 728 |
+
|
| 729 |
+
Run with:
|
| 730 |
+
```bash
|
| 731 |
+
python quick_client.py MyPlayerName
|
| 732 |
+
```
|
| 733 |
+
|
| 734 |
+
---
|
| 735 |
+
|
| 736 |
+
## Next Steps
|
| 737 |
+
|
| 738 |
+
1. **Customize the client** for your specific needs
|
| 739 |
+
2. **Add game-specific features** like inventory management
|
| 740 |
+
3. **Implement advanced AI** for automated gameplay
|
| 741 |
+
4. **Create GUI interfaces** using tkinter, PyQt, or web frameworks
|
| 742 |
+
5. **Build monitoring tools** for game statistics
|
| 743 |
+
|
| 744 |
+
This guide provides the foundation for any client application that needs to interact with the MMORPG game server!
|
Documentation/sample_gradio_mcp_server.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Sample Gradio MCP Server
|
| 4 |
+
========================
|
| 5 |
+
|
| 6 |
+
This is a complete example of how to create a Gradio application that serves as an MCP server.
|
| 7 |
+
This example creates a "Task Manager" service that can be used as an NPC addon in the MMORPG.
|
| 8 |
+
|
| 9 |
+
Key Features:
|
| 10 |
+
- Full MCP server implementation with Gradio
|
| 11 |
+
- RESTful API endpoints for MCP communication
|
| 12 |
+
- Gradio web interface for direct interaction
|
| 13 |
+
- Error handling and validation
|
| 14 |
+
- Proper MCP protocol responses
|
| 15 |
+
|
| 16 |
+
Usage:
|
| 17 |
+
1. Run this file: python sample_gradio_mcp_server.py
|
| 18 |
+
2. Access web interface at: http://localhost:7860
|
| 19 |
+
3. Use MCP client to connect to: http://localhost:7860/gradio_api/mcp/sse
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import gradio as gr
|
| 23 |
+
import json
|
| 24 |
+
import time
|
| 25 |
+
import uuid
|
| 26 |
+
from datetime import datetime, timedelta
|
| 27 |
+
from typing import Dict, List, Any, Optional
|
| 28 |
+
from dataclasses import dataclass, asdict
|
| 29 |
+
|
| 30 |
+
# ============================================================================
|
| 31 |
+
# DATA MODELS
|
| 32 |
+
# ============================================================================
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class Task:
|
| 36 |
+
id: str
|
| 37 |
+
title: str
|
| 38 |
+
description: str
|
| 39 |
+
status: str # "pending", "in_progress", "completed"
|
| 40 |
+
priority: str # "low", "medium", "high"
|
| 41 |
+
created_at: float
|
| 42 |
+
due_date: Optional[float] = None
|
| 43 |
+
completed_at: Optional[float] = None
|
| 44 |
+
|
| 45 |
+
class TaskManager:
|
| 46 |
+
"""Core task management logic"""
|
| 47 |
+
|
| 48 |
+
def __init__(self):
|
| 49 |
+
self.tasks: Dict[str, Task] = {}
|
| 50 |
+
self.task_counter = 0
|
| 51 |
+
|
| 52 |
+
def create_task(self, title: str, description: str = "", priority: str = "medium", due_date: Optional[str] = None) -> Dict[str, Any]:
|
| 53 |
+
"""Create a new task"""
|
| 54 |
+
if not title.strip():
|
| 55 |
+
return {"error": "Task title cannot be empty"}
|
| 56 |
+
|
| 57 |
+
task_id = f"task_{int(time.time())}_{self.task_counter}"
|
| 58 |
+
self.task_counter += 1
|
| 59 |
+
|
| 60 |
+
due_timestamp = None
|
| 61 |
+
if due_date:
|
| 62 |
+
try:
|
| 63 |
+
# Simple date parsing (format: YYYY-MM-DD)
|
| 64 |
+
due_timestamp = datetime.strptime(due_date, "%Y-%m-%d").timestamp()
|
| 65 |
+
except ValueError:
|
| 66 |
+
return {"error": "Invalid date format. Use YYYY-MM-DD"}
|
| 67 |
+
|
| 68 |
+
task = Task(
|
| 69 |
+
id=task_id,
|
| 70 |
+
title=title.strip(),
|
| 71 |
+
description=description.strip(),
|
| 72 |
+
status="pending",
|
| 73 |
+
priority=priority.lower(),
|
| 74 |
+
created_at=time.time(),
|
| 75 |
+
due_date=due_timestamp
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
self.tasks[task_id] = task
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"success": True,
|
| 82 |
+
"task_id": task_id,
|
| 83 |
+
"message": f"Task '{title}' created successfully!",
|
| 84 |
+
"task": asdict(task)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
def list_tasks(self, status_filter: Optional[str] = None) -> Dict[str, Any]:
|
| 88 |
+
"""List all tasks or filter by status"""
|
| 89 |
+
tasks = list(self.tasks.values())
|
| 90 |
+
|
| 91 |
+
if status_filter:
|
| 92 |
+
tasks = [t for t in tasks if t.status == status_filter.lower()]
|
| 93 |
+
|
| 94 |
+
# Sort by priority and creation date
|
| 95 |
+
priority_order = {"high": 0, "medium": 1, "low": 2}
|
| 96 |
+
tasks.sort(key=lambda t: (priority_order.get(t.priority, 2), t.created_at))
|
| 97 |
+
|
| 98 |
+
return {
|
| 99 |
+
"success": True,
|
| 100 |
+
"count": len(tasks),
|
| 101 |
+
"tasks": [asdict(task) for task in tasks]
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def update_task_status(self, task_id: str, new_status: str) -> Dict[str, Any]:
|
| 105 |
+
"""Update task status"""
|
| 106 |
+
if task_id not in self.tasks:
|
| 107 |
+
return {"error": f"Task {task_id} not found"}
|
| 108 |
+
|
| 109 |
+
valid_statuses = ["pending", "in_progress", "completed"]
|
| 110 |
+
if new_status.lower() not in valid_statuses:
|
| 111 |
+
return {"error": f"Invalid status. Must be one of: {', '.join(valid_statuses)}"}
|
| 112 |
+
|
| 113 |
+
task = self.tasks[task_id]
|
| 114 |
+
old_status = task.status
|
| 115 |
+
task.status = new_status.lower()
|
| 116 |
+
|
| 117 |
+
if new_status.lower() == "completed":
|
| 118 |
+
task.completed_at = time.time()
|
| 119 |
+
|
| 120 |
+
return {
|
| 121 |
+
"success": True,
|
| 122 |
+
"message": f"Task '{task.title}' status changed from '{old_status}' to '{new_status}'",
|
| 123 |
+
"task": asdict(task)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
def delete_task(self, task_id: str) -> Dict[str, Any]:
|
| 127 |
+
"""Delete a task"""
|
| 128 |
+
if task_id not in self.tasks:
|
| 129 |
+
return {"error": f"Task {task_id} not found"}
|
| 130 |
+
|
| 131 |
+
task = self.tasks.pop(task_id)
|
| 132 |
+
return {
|
| 133 |
+
"success": True,
|
| 134 |
+
"message": f"Task '{task.title}' deleted successfully"
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
def get_task_stats(self) -> Dict[str, Any]:
|
| 138 |
+
"""Get task statistics"""
|
| 139 |
+
total = len(self.tasks)
|
| 140 |
+
completed = len([t for t in self.tasks.values() if t.status == "completed"])
|
| 141 |
+
in_progress = len([t for t in self.tasks.values() if t.status == "in_progress"])
|
| 142 |
+
pending = len([t for t in self.tasks.values() if t.status == "pending"])
|
| 143 |
+
|
| 144 |
+
# Overdue tasks
|
| 145 |
+
current_time = time.time()
|
| 146 |
+
overdue = len([
|
| 147 |
+
t for t in self.tasks.values()
|
| 148 |
+
if t.due_date and t.due_date < current_time and t.status != "completed"
|
| 149 |
+
])
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"success": True,
|
| 153 |
+
"stats": {
|
| 154 |
+
"total_tasks": total,
|
| 155 |
+
"completed": completed,
|
| 156 |
+
"in_progress": in_progress,
|
| 157 |
+
"pending": pending,
|
| 158 |
+
"overdue": overdue,
|
| 159 |
+
"completion_rate": round((completed / total * 100) if total > 0 else 0, 1)
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
# Global task manager instance
|
| 164 |
+
task_manager = TaskManager()
|
| 165 |
+
|
| 166 |
+
# ============================================================================
|
| 167 |
+
# MCP TOOLS DEFINITION
|
| 168 |
+
# ============================================================================
|
| 169 |
+
|
| 170 |
+
def mcp_create_task(title: str, description: str = "", priority: str = "medium", due_date: str = "") -> str:
|
| 171 |
+
"""
|
| 172 |
+
Create a new task in the task manager.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
title: Task title (required)
|
| 176 |
+
description: Detailed task description (optional)
|
| 177 |
+
priority: Task priority - low, medium, or high (default: medium)
|
| 178 |
+
due_date: Due date in YYYY-MM-DD format (optional)
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
JSON string with task creation result
|
| 182 |
+
"""
|
| 183 |
+
result = task_manager.create_task(
|
| 184 |
+
title=title,
|
| 185 |
+
description=description,
|
| 186 |
+
priority=priority,
|
| 187 |
+
due_date=due_date if due_date else None
|
| 188 |
+
)
|
| 189 |
+
return json.dumps(result, indent=2)
|
| 190 |
+
|
| 191 |
+
def mcp_list_tasks(status_filter: str = "") -> str:
|
| 192 |
+
"""
|
| 193 |
+
List all tasks or filter by status.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
status_filter: Filter by status - pending, in_progress, or completed (optional)
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
JSON string with list of tasks
|
| 200 |
+
"""
|
| 201 |
+
result = task_manager.list_tasks(status_filter if status_filter else None)
|
| 202 |
+
return json.dumps(result, indent=2)
|
| 203 |
+
|
| 204 |
+
def mcp_update_task_status(task_id: str, new_status: str) -> str:
|
| 205 |
+
"""
|
| 206 |
+
Update the status of an existing task.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
task_id: ID of the task to update
|
| 210 |
+
new_status: New status - pending, in_progress, or completed
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
JSON string with update result
|
| 214 |
+
"""
|
| 215 |
+
result = task_manager.update_task_status(task_id, new_status)
|
| 216 |
+
return json.dumps(result, indent=2)
|
| 217 |
+
|
| 218 |
+
def mcp_delete_task(task_id: str) -> str:
|
| 219 |
+
"""
|
| 220 |
+
Delete a task from the task manager.
|
| 221 |
+
|
| 222 |
+
Args:
|
| 223 |
+
task_id: ID of the task to delete
|
| 224 |
+
|
| 225 |
+
Returns:
|
| 226 |
+
JSON string with deletion result
|
| 227 |
+
"""
|
| 228 |
+
result = task_manager.delete_task(task_id)
|
| 229 |
+
return json.dumps(result, indent=2)
|
| 230 |
+
|
| 231 |
+
def mcp_get_task_stats() -> str:
|
| 232 |
+
"""
|
| 233 |
+
Get task statistics and completion metrics.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
JSON string with task statistics
|
| 237 |
+
"""
|
| 238 |
+
result = task_manager.get_task_stats()
|
| 239 |
+
return json.dumps(result, indent=2)
|
| 240 |
+
|
| 241 |
+
# ============================================================================
|
| 242 |
+
# GRADIO INTERFACE
|
| 243 |
+
# ============================================================================
|
| 244 |
+
|
| 245 |
+
def create_task_interface():
|
| 246 |
+
"""Create the Gradio interface for the Task Manager MCP Server"""
|
| 247 |
+
|
| 248 |
+
with gr.Blocks(
|
| 249 |
+
title="Task Manager MCP Server",
|
| 250 |
+
theme=gr.themes.Soft(),
|
| 251 |
+
css="""
|
| 252 |
+
.task-card {
|
| 253 |
+
border: 1px solid #ddd;
|
| 254 |
+
border-radius: 8px;
|
| 255 |
+
padding: 15px;
|
| 256 |
+
margin: 10px 0;
|
| 257 |
+
background: #f9f9f9;
|
| 258 |
+
}
|
| 259 |
+
.high-priority { border-left: 4px solid #ff4444; }
|
| 260 |
+
.medium-priority { border-left: 4px solid #ffaa00; }
|
| 261 |
+
.low-priority { border-left: 4px solid #44aa44; }
|
| 262 |
+
"""
|
| 263 |
+
) as demo:
|
| 264 |
+
|
| 265 |
+
gr.Markdown("""
|
| 266 |
+
# 📋 Task Manager MCP Server
|
| 267 |
+
|
| 268 |
+
**MCP Endpoint:** `http://localhost:7860/gradio_api/mcp/sse`
|
| 269 |
+
|
| 270 |
+
This server provides task management capabilities via MCP protocol.
|
| 271 |
+
Perfect for integration as an NPC addon in games or other applications!
|
| 272 |
+
|
| 273 |
+
## Available MCP Tools:
|
| 274 |
+
- `mcp_create_task` - Create new tasks
|
| 275 |
+
- `mcp_list_tasks` - List and filter tasks
|
| 276 |
+
- `mcp_update_task_status` - Update task progress
|
| 277 |
+
- `mcp_delete_task` - Remove tasks
|
| 278 |
+
- `mcp_get_task_stats` - Get statistics
|
| 279 |
+
""")
|
| 280 |
+
|
| 281 |
+
with gr.Tabs():
|
| 282 |
+
# Create Task Tab
|
| 283 |
+
with gr.Tab("➕ Create Task"):
|
| 284 |
+
with gr.Row():
|
| 285 |
+
task_title = gr.Textbox(
|
| 286 |
+
label="Task Title",
|
| 287 |
+
placeholder="Enter task title...",
|
| 288 |
+
scale=2
|
| 289 |
+
)
|
| 290 |
+
priority = gr.Dropdown(
|
| 291 |
+
choices=["low", "medium", "high"],
|
| 292 |
+
value="medium",
|
| 293 |
+
label="Priority",
|
| 294 |
+
scale=1
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
task_description = gr.Textbox(
|
| 298 |
+
label="Description",
|
| 299 |
+
placeholder="Detailed task description...",
|
| 300 |
+
lines=3
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
due_date = gr.Textbox(
|
| 304 |
+
label="Due Date (YYYY-MM-DD)",
|
| 305 |
+
placeholder="2024-12-31",
|
| 306 |
+
value=""
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
create_btn = gr.Button("Create Task", variant="primary")
|
| 310 |
+
create_result = gr.JSON(label="Result")
|
| 311 |
+
|
| 312 |
+
def handle_create_task(title, desc, prio, due):
|
| 313 |
+
result = task_manager.create_task(title, desc, prio, due if due else None)
|
| 314 |
+
return result
|
| 315 |
+
|
| 316 |
+
create_btn.click(
|
| 317 |
+
handle_create_task,
|
| 318 |
+
inputs=[task_title, task_description, priority, due_date],
|
| 319 |
+
outputs=[create_result]
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# View Tasks Tab
|
| 323 |
+
with gr.Tab("📋 View Tasks"):
|
| 324 |
+
with gr.Row():
|
| 325 |
+
status_filter = gr.Dropdown(
|
| 326 |
+
choices=["", "pending", "in_progress", "completed"],
|
| 327 |
+
value="",
|
| 328 |
+
label="Filter by Status",
|
| 329 |
+
scale=1
|
| 330 |
+
)
|
| 331 |
+
refresh_btn = gr.Button("🔄 Refresh", scale=1)
|
| 332 |
+
|
| 333 |
+
tasks_display = gr.JSON(label="Tasks")
|
| 334 |
+
|
| 335 |
+
def handle_list_tasks(status_filter_val):
|
| 336 |
+
result = task_manager.list_tasks(status_filter_val if status_filter_val else None)
|
| 337 |
+
return result
|
| 338 |
+
|
| 339 |
+
refresh_btn.click(
|
| 340 |
+
handle_list_tasks,
|
| 341 |
+
inputs=[status_filter],
|
| 342 |
+
outputs=[tasks_display]
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
status_filter.change(
|
| 346 |
+
handle_list_tasks,
|
| 347 |
+
inputs=[status_filter],
|
| 348 |
+
outputs=[tasks_display]
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Update Task Tab
|
| 352 |
+
with gr.Tab("✏️ Update Task"):
|
| 353 |
+
update_task_id = gr.Textbox(
|
| 354 |
+
label="Task ID",
|
| 355 |
+
placeholder="task_1234567890_0"
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
new_status = gr.Dropdown(
|
| 359 |
+
choices=["pending", "in_progress", "completed"],
|
| 360 |
+
label="New Status",
|
| 361 |
+
value="in_progress"
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
update_btn = gr.Button("Update Status", variant="primary")
|
| 365 |
+
update_result = gr.JSON(label="Result")
|
| 366 |
+
|
| 367 |
+
def handle_update_status(task_id, status):
|
| 368 |
+
result = task_manager.update_task_status(task_id, status)
|
| 369 |
+
return result
|
| 370 |
+
|
| 371 |
+
update_btn.click(
|
| 372 |
+
handle_update_status,
|
| 373 |
+
inputs=[update_task_id, new_status],
|
| 374 |
+
outputs=[update_result]
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
# Statistics Tab
|
| 378 |
+
with gr.Tab("📊 Statistics"):
|
| 379 |
+
stats_btn = gr.Button("📈 Get Current Stats", variant="primary")
|
| 380 |
+
stats_display = gr.JSON(label="Task Statistics")
|
| 381 |
+
|
| 382 |
+
def handle_get_stats():
|
| 383 |
+
result = task_manager.get_task_stats()
|
| 384 |
+
return result
|
| 385 |
+
|
| 386 |
+
stats_btn.click(
|
| 387 |
+
handle_get_stats,
|
| 388 |
+
outputs=[stats_display]
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
# MCP Testing Tab
|
| 392 |
+
with gr.Tab("🔌 MCP Testing"):
|
| 393 |
+
gr.Markdown("""
|
| 394 |
+
## Test MCP Functions
|
| 395 |
+
|
| 396 |
+
Use this tab to test the MCP functions that will be available to external clients.
|
| 397 |
+
""")
|
| 398 |
+
|
| 399 |
+
with gr.Group():
|
| 400 |
+
gr.Markdown("### Test mcp_create_task")
|
| 401 |
+
with gr.Row():
|
| 402 |
+
test_title = gr.Textbox(label="Title", value="Test Task")
|
| 403 |
+
test_desc = gr.Textbox(label="Description", value="This is a test task")
|
| 404 |
+
test_priority = gr.Dropdown(choices=["low", "medium", "high"], value="medium", label="Priority")
|
| 405 |
+
|
| 406 |
+
test_create_btn = gr.Button("Test Create")
|
| 407 |
+
test_create_result = gr.Textbox(label="MCP Response", lines=10)
|
| 408 |
+
|
| 409 |
+
test_create_btn.click(
|
| 410 |
+
lambda t, d, p: mcp_create_task(t, d, p),
|
| 411 |
+
inputs=[test_title, test_desc, test_priority],
|
| 412 |
+
outputs=[test_create_result]
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
with gr.Group():
|
| 416 |
+
gr.Markdown("### Test mcp_list_tasks")
|
| 417 |
+
test_filter = gr.Dropdown(choices=["", "pending", "in_progress", "completed"], value="", label="Status Filter")
|
| 418 |
+
test_list_btn = gr.Button("Test List")
|
| 419 |
+
test_list_result = gr.Textbox(label="MCP Response", lines=10)
|
| 420 |
+
|
| 421 |
+
test_list_btn.click(
|
| 422 |
+
lambda f: mcp_list_tasks(f),
|
| 423 |
+
inputs=[test_filter],
|
| 424 |
+
outputs=[test_list_result]
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
with gr.Group():
|
| 428 |
+
gr.Markdown("### Test mcp_get_task_stats")
|
| 429 |
+
test_stats_btn = gr.Button("Test Stats")
|
| 430 |
+
test_stats_result = gr.Textbox(label="MCP Response", lines=10)
|
| 431 |
+
|
| 432 |
+
test_stats_btn.click(
|
| 433 |
+
lambda: mcp_get_task_stats(),
|
| 434 |
+
outputs=[test_stats_result]
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
# Auto-refresh stats on page load
|
| 438 |
+
demo.load(
|
| 439 |
+
lambda: task_manager.get_task_stats(),
|
| 440 |
+
outputs=[stats_display]
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
return demo
|
| 444 |
+
|
| 445 |
+
# ============================================================================
|
| 446 |
+
# MAIN APPLICATION
|
| 447 |
+
# ============================================================================
|
| 448 |
+
|
| 449 |
+
if __name__ == "__main__":
|
| 450 |
+
print("🚀 Starting Task Manager MCP Server...")
|
| 451 |
+
print("=" * 60)
|
| 452 |
+
print("✅ Web Interface: http://localhost:7860")
|
| 453 |
+
print("🔌 MCP Endpoint: http://localhost:7860/gradio_api/mcp/sse")
|
| 454 |
+
print("📋 Available MCP Tools:")
|
| 455 |
+
print(" - mcp_create_task")
|
| 456 |
+
print(" - mcp_list_tasks")
|
| 457 |
+
print(" - mcp_update_task_status")
|
| 458 |
+
print(" - mcp_delete_task")
|
| 459 |
+
print(" - mcp_get_task_stats")
|
| 460 |
+
print("=" * 60)
|
| 461 |
+
|
| 462 |
+
# Create some sample tasks for demonstration
|
| 463 |
+
task_manager.create_task("Welcome Task", "This is a sample task to get you started!", "high")
|
| 464 |
+
task_manager.create_task("Learn MCP Protocol", "Study the Model Context Protocol documentation", "medium")
|
| 465 |
+
task_manager.create_task("Build NPC Addon", "Create a new NPC addon for the MMORPG", "high")
|
| 466 |
+
|
| 467 |
+
# Launch the Gradio app with MCP server enabled
|
| 468 |
+
interface = create_task_interface()
|
| 469 |
+
interface.launch(
|
| 470 |
+
debug=True,
|
| 471 |
+
share=False,
|
| 472 |
+
server_port=7860,
|
| 473 |
+
mcp_server=True, # This enables MCP protocol support
|
| 474 |
+
show_error=True
|
| 475 |
+
)
|
Documentation/simple_game_client.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple MMORPG Game Client
|
| 4 |
+
========================
|
| 5 |
+
|
| 6 |
+
A basic client implementation for connecting to the MMORPG game server via MCP.
|
| 7 |
+
This client can join the game, move around, chat, and interact with NPCs.
|
| 8 |
+
|
| 9 |
+
Requirements:
|
| 10 |
+
pip install mcp aiohttp asyncio
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python simple_game_client.py
|
| 14 |
+
|
| 15 |
+
Features:
|
| 16 |
+
- Connect to game server via MCP
|
| 17 |
+
- Register as a player
|
| 18 |
+
- Move around the game world
|
| 19 |
+
- Send chat messages
|
| 20 |
+
- Interact with NPCs
|
| 21 |
+
- Get game state information
|
| 22 |
+
- Interactive console interface
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import asyncio
|
| 26 |
+
import json
|
| 27 |
+
import uuid
|
| 28 |
+
import sys
|
| 29 |
+
from typing import Dict, List, Optional
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
from mcp import ClientSession
|
| 33 |
+
from mcp.client.sse import sse_client
|
| 34 |
+
from contextlib import AsyncExitStack
|
| 35 |
+
except ImportError:
|
| 36 |
+
print("❌ Missing dependencies. Install with: pip install mcp aiohttp")
|
| 37 |
+
sys.exit(1)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SimpleGameClient:
|
| 41 |
+
"""Simple client for connecting to the MMORPG game server"""
|
| 42 |
+
|
| 43 |
+
def __init__(self, server_url="http://127.0.0.1:7868/gradio_api/mcp/sse"):
|
| 44 |
+
self.server_url = server_url
|
| 45 |
+
self.session = None
|
| 46 |
+
self.connected = False
|
| 47 |
+
self.tools = []
|
| 48 |
+
self.exit_stack = None
|
| 49 |
+
self.client_id = f"client_{uuid.uuid4().hex[:8]}"
|
| 50 |
+
self.agent_id = None
|
| 51 |
+
self.player_name = None
|
| 52 |
+
|
| 53 |
+
print(f"🎮 Game Client initialized with ID: {self.client_id}")
|
| 54 |
+
|
| 55 |
+
async def connect(self):
|
| 56 |
+
"""Connect to the game server"""
|
| 57 |
+
print(f"🔌 Connecting to {self.server_url}...")
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
# Clean up any existing connection
|
| 61 |
+
if self.exit_stack:
|
| 62 |
+
await self.exit_stack.aclose()
|
| 63 |
+
|
| 64 |
+
self.exit_stack = AsyncExitStack()
|
| 65 |
+
|
| 66 |
+
# Establish SSE connection to MCP server
|
| 67 |
+
transport = await self.exit_stack.enter_async_context(
|
| 68 |
+
sse_client(self.server_url)
|
| 69 |
+
)
|
| 70 |
+
read_stream, write_callable = transport
|
| 71 |
+
|
| 72 |
+
# Create MCP client session
|
| 73 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 74 |
+
ClientSession(read_stream, write_callable)
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Initialize the session
|
| 78 |
+
await self.session.initialize()
|
| 79 |
+
|
| 80 |
+
# Get available tools/commands from server
|
| 81 |
+
response = await self.session.list_tools()
|
| 82 |
+
self.tools = response.tools
|
| 83 |
+
|
| 84 |
+
self.connected = True
|
| 85 |
+
tool_names = [tool.name for tool in self.tools]
|
| 86 |
+
print(f"✅ Connected successfully!")
|
| 87 |
+
print(f"📋 Available commands: {', '.join(tool_names)}")
|
| 88 |
+
|
| 89 |
+
return True
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f"❌ Connection failed: {e}")
|
| 93 |
+
self.connected = False
|
| 94 |
+
return False
|
| 95 |
+
|
| 96 |
+
async def register_player(self, player_name: str):
|
| 97 |
+
"""Register as a new player in the game"""
|
| 98 |
+
if not self.connected:
|
| 99 |
+
print("❌ Not connected to server")
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
print(f"👤 Registering player: {player_name}")
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
# Find the register tool
|
| 106 |
+
register_tool = self._find_tool(['register', 'agent'])
|
| 107 |
+
if not register_tool:
|
| 108 |
+
print("❌ Register tool not available on server")
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
# Register the player
|
| 112 |
+
result = await self.session.call_tool(
|
| 113 |
+
register_tool.name,
|
| 114 |
+
{"name": player_name, "client_id": self.client_id}
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# Parse the result
|
| 118 |
+
content = self._extract_content(result)
|
| 119 |
+
print(f"📝 Registration response: {content}")
|
| 120 |
+
|
| 121 |
+
if "registered" in content.lower() or "success" in content.lower():
|
| 122 |
+
self.player_name = player_name
|
| 123 |
+
# Try to extract agent_id from response
|
| 124 |
+
self._parse_agent_id(content)
|
| 125 |
+
print(f"✅ Successfully registered as: {player_name}")
|
| 126 |
+
return True
|
| 127 |
+
else:
|
| 128 |
+
print(f"❌ Registration failed: {content}")
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"❌ Registration error: {e}")
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
async def move_player(self, direction: str):
|
| 136 |
+
"""Move the player in the specified direction"""
|
| 137 |
+
if not self._check_ready():
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
direction = direction.lower()
|
| 141 |
+
valid_directions = ['north', 'south', 'east', 'west', 'up', 'down']
|
| 142 |
+
|
| 143 |
+
if direction not in valid_directions:
|
| 144 |
+
print(f"❌ Invalid direction. Use: {', '.join(valid_directions)}")
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
# Find the move tool
|
| 149 |
+
move_tool = self._find_tool(['move', 'agent'])
|
| 150 |
+
if not move_tool:
|
| 151 |
+
print("❌ Move tool not available")
|
| 152 |
+
return False
|
| 153 |
+
|
| 154 |
+
# Execute the move
|
| 155 |
+
result = await self.session.call_tool(
|
| 156 |
+
move_tool.name,
|
| 157 |
+
{"client_id": self.client_id, "direction": direction}
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
content = self._extract_content(result)
|
| 161 |
+
print(f"🚶 Move {direction}: {content}")
|
| 162 |
+
return True
|
| 163 |
+
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f"❌ Move error: {e}")
|
| 166 |
+
return False
|
| 167 |
+
|
| 168 |
+
async def send_chat(self, message: str):
|
| 169 |
+
"""Send a chat message to other players"""
|
| 170 |
+
if not self._check_ready():
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
if not message.strip():
|
| 174 |
+
print("❌ Cannot send empty message")
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
# Find the chat tool
|
| 179 |
+
chat_tool = self._find_tool(['chat', 'send'])
|
| 180 |
+
if not chat_tool:
|
| 181 |
+
print("❌ Chat tool not available")
|
| 182 |
+
return False
|
| 183 |
+
|
| 184 |
+
# Send the chat message
|
| 185 |
+
result = await self.session.call_tool(
|
| 186 |
+
chat_tool.name,
|
| 187 |
+
{"client_id": self.client_id, "message": message}
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
content = self._extract_content(result)
|
| 191 |
+
print(f"💬 Chat sent: {content}")
|
| 192 |
+
return True
|
| 193 |
+
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"❌ Chat error: {e}")
|
| 196 |
+
return False
|
| 197 |
+
|
| 198 |
+
async def get_game_state(self):
|
| 199 |
+
"""Get current game state information"""
|
| 200 |
+
if not self.connected:
|
| 201 |
+
print("❌ Not connected to server")
|
| 202 |
+
return None
|
| 203 |
+
|
| 204 |
+
try:
|
| 205 |
+
# Find the game state tool
|
| 206 |
+
state_tool = self._find_tool(['state', 'game'])
|
| 207 |
+
if not state_tool:
|
| 208 |
+
print("❌ Game state tool not available")
|
| 209 |
+
return None
|
| 210 |
+
|
| 211 |
+
# Get game state
|
| 212 |
+
result = await self.session.call_tool(state_tool.name, {})
|
| 213 |
+
content = self._extract_content(result)
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
# Try to parse as JSON
|
| 217 |
+
game_state = json.loads(content)
|
| 218 |
+
return game_state
|
| 219 |
+
except json.JSONDecodeError:
|
| 220 |
+
# Return as text if not JSON
|
| 221 |
+
return {"info": content}
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
print(f"❌ Game state error: {e}")
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
async def interact_with_npc(self, npc_id: str, message: str):
|
| 228 |
+
"""Interact with an NPC"""
|
| 229 |
+
if not self._check_ready():
|
| 230 |
+
return False
|
| 231 |
+
|
| 232 |
+
if not npc_id.strip() or not message.strip():
|
| 233 |
+
print("❌ NPC ID and message are required")
|
| 234 |
+
return False
|
| 235 |
+
|
| 236 |
+
try:
|
| 237 |
+
# Find the interact tool
|
| 238 |
+
interact_tool = self._find_tool(['interact', 'npc'])
|
| 239 |
+
if not interact_tool:
|
| 240 |
+
print("❌ NPC interaction tool not available")
|
| 241 |
+
return False
|
| 242 |
+
|
| 243 |
+
# Interact with NPC
|
| 244 |
+
result = await self.session.call_tool(
|
| 245 |
+
interact_tool.name,
|
| 246 |
+
{"client_id": self.client_id, "npc_id": npc_id, "message": message}
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
content = self._extract_content(result)
|
| 250 |
+
print(f"🤖 {npc_id}: {content}")
|
| 251 |
+
return True
|
| 252 |
+
|
| 253 |
+
except Exception as e:
|
| 254 |
+
print(f"❌ NPC interaction error: {e}")
|
| 255 |
+
return False
|
| 256 |
+
|
| 257 |
+
async def list_tools(self):
|
| 258 |
+
"""List all available tools on the server"""
|
| 259 |
+
if not self.connected:
|
| 260 |
+
print("❌ Not connected to server")
|
| 261 |
+
return
|
| 262 |
+
|
| 263 |
+
print("🛠️ Available Tools:")
|
| 264 |
+
for i, tool in enumerate(self.tools, 1):
|
| 265 |
+
print(f" {i}. {tool.name}")
|
| 266 |
+
if hasattr(tool, 'description') and tool.description:
|
| 267 |
+
print(f" Description: {tool.description}")
|
| 268 |
+
|
| 269 |
+
def _find_tool(self, keywords: List[str]):
|
| 270 |
+
"""Find a tool by keywords"""
|
| 271 |
+
for keyword in keywords:
|
| 272 |
+
tool = next((t for t in self.tools if keyword in t.name.lower()), None)
|
| 273 |
+
if tool:
|
| 274 |
+
return tool
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
def _extract_content(self, result):
|
| 278 |
+
"""Extract content from MCP result"""
|
| 279 |
+
try:
|
| 280 |
+
if hasattr(result, 'content') and result.content:
|
| 281 |
+
if isinstance(result.content, list):
|
| 282 |
+
content_parts = []
|
| 283 |
+
for item in result.content:
|
| 284 |
+
if hasattr(item, 'text'):
|
| 285 |
+
content_parts.append(item.text)
|
| 286 |
+
else:
|
| 287 |
+
content_parts.append(str(item))
|
| 288 |
+
return ''.join(content_parts)
|
| 289 |
+
elif hasattr(result.content, 'text'):
|
| 290 |
+
return result.content.text
|
| 291 |
+
else:
|
| 292 |
+
return str(result.content)
|
| 293 |
+
return str(result)
|
| 294 |
+
except Exception as e:
|
| 295 |
+
return f"Error extracting content: {e}"
|
| 296 |
+
|
| 297 |
+
def _parse_agent_id(self, content: str):
|
| 298 |
+
"""Try to extract agent ID from response"""
|
| 299 |
+
lines = content.split('\n')
|
| 300 |
+
for line in lines:
|
| 301 |
+
if any(keyword in line.lower() for keyword in ['agent_id', 'id:', 'player id']):
|
| 302 |
+
parts = line.split(':')
|
| 303 |
+
if len(parts) > 1:
|
| 304 |
+
self.agent_id = parts[1].strip()
|
| 305 |
+
print(f"🆔 Agent ID: {self.agent_id}")
|
| 306 |
+
break
|
| 307 |
+
|
| 308 |
+
def _check_ready(self):
|
| 309 |
+
"""Check if client is ready for game operations"""
|
| 310 |
+
if not self.connected:
|
| 311 |
+
print("❌ Not connected to server")
|
| 312 |
+
return False
|
| 313 |
+
if not self.player_name:
|
| 314 |
+
print("❌ Not registered as a player")
|
| 315 |
+
return False
|
| 316 |
+
return True
|
| 317 |
+
|
| 318 |
+
async def disconnect(self):
|
| 319 |
+
"""Disconnect from the server"""
|
| 320 |
+
if self.exit_stack:
|
| 321 |
+
await self.exit_stack.aclose()
|
| 322 |
+
self.connected = False
|
| 323 |
+
self.player_name = None
|
| 324 |
+
self.agent_id = None
|
| 325 |
+
print("🔌 Disconnected from server")
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class InteractiveGameClient:
|
| 329 |
+
"""Interactive console interface for the game client"""
|
| 330 |
+
|
| 331 |
+
def __init__(self):
|
| 332 |
+
self.client = SimpleGameClient()
|
| 333 |
+
self.running = True
|
| 334 |
+
|
| 335 |
+
async def start(self):
|
| 336 |
+
"""Start the interactive client"""
|
| 337 |
+
print("🎮 MMORPG Simple Game Client")
|
| 338 |
+
print("=" * 40)
|
| 339 |
+
self.show_help()
|
| 340 |
+
|
| 341 |
+
while self.running:
|
| 342 |
+
try:
|
| 343 |
+
command = input("\n> ").strip()
|
| 344 |
+
if command:
|
| 345 |
+
await self.process_command(command)
|
| 346 |
+
except KeyboardInterrupt:
|
| 347 |
+
print("\n\n👋 Goodbye!")
|
| 348 |
+
break
|
| 349 |
+
except EOFError:
|
| 350 |
+
break
|
| 351 |
+
|
| 352 |
+
await self.client.disconnect()
|
| 353 |
+
|
| 354 |
+
async def process_command(self, command: str):
|
| 355 |
+
"""Process a user command"""
|
| 356 |
+
parts = command.split()
|
| 357 |
+
if not parts:
|
| 358 |
+
return
|
| 359 |
+
|
| 360 |
+
cmd = parts[0].lower()
|
| 361 |
+
args = parts[1:]
|
| 362 |
+
|
| 363 |
+
if cmd == "help":
|
| 364 |
+
self.show_help()
|
| 365 |
+
|
| 366 |
+
elif cmd == "connect":
|
| 367 |
+
await self.client.connect()
|
| 368 |
+
|
| 369 |
+
elif cmd == "register":
|
| 370 |
+
if args:
|
| 371 |
+
name = " ".join(args)
|
| 372 |
+
await self.client.register_player(name)
|
| 373 |
+
else:
|
| 374 |
+
print("❌ Usage: register <player_name>")
|
| 375 |
+
|
| 376 |
+
elif cmd == "move":
|
| 377 |
+
if args:
|
| 378 |
+
direction = args[0]
|
| 379 |
+
await self.client.move_player(direction)
|
| 380 |
+
else:
|
| 381 |
+
print("❌ Usage: move <direction>")
|
| 382 |
+
print(" Directions: north, south, east, west")
|
| 383 |
+
|
| 384 |
+
elif cmd == "chat":
|
| 385 |
+
if args:
|
| 386 |
+
message = " ".join(args)
|
| 387 |
+
await self.client.send_chat(message)
|
| 388 |
+
else:
|
| 389 |
+
print("❌ Usage: chat <message>")
|
| 390 |
+
|
| 391 |
+
elif cmd == "state":
|
| 392 |
+
state = await self.client.get_game_state()
|
| 393 |
+
if state:
|
| 394 |
+
print("📊 Game State:")
|
| 395 |
+
print(json.dumps(state, indent=2))
|
| 396 |
+
|
| 397 |
+
elif cmd == "npc":
|
| 398 |
+
if len(args) >= 2:
|
| 399 |
+
npc_id = args[0]
|
| 400 |
+
message = " ".join(args[1:])
|
| 401 |
+
await self.client.interact_with_npc(npc_id, message)
|
| 402 |
+
else:
|
| 403 |
+
print("❌ Usage: npc <npc_id> <message>")
|
| 404 |
+
print(" Example: npc weather_oracle What's the weather in Berlin?")
|
| 405 |
+
|
| 406 |
+
elif cmd == "tools":
|
| 407 |
+
await self.client.list_tools()
|
| 408 |
+
|
| 409 |
+
elif cmd in ["quit", "exit", "q"]:
|
| 410 |
+
self.running = False
|
| 411 |
+
|
| 412 |
+
else:
|
| 413 |
+
print(f"❓ Unknown command: {cmd}")
|
| 414 |
+
print(" Type 'help' for available commands")
|
| 415 |
+
|
| 416 |
+
def show_help(self):
|
| 417 |
+
"""Show available commands"""
|
| 418 |
+
print("\n📋 Available Commands:")
|
| 419 |
+
print(" help - Show this help")
|
| 420 |
+
print(" connect - Connect to game server")
|
| 421 |
+
print(" register <name> - Register as a player")
|
| 422 |
+
print(" move <direction> - Move player (north/south/east/west)")
|
| 423 |
+
print(" chat <message> - Send chat message")
|
| 424 |
+
print(" npc <id> <message> - Talk to an NPC")
|
| 425 |
+
print(" state - Get game state")
|
| 426 |
+
print(" tools - List available server tools")
|
| 427 |
+
print(" quit - Exit the client")
|
| 428 |
+
print("\n💡 Example session:")
|
| 429 |
+
print(" > connect")
|
| 430 |
+
print(" > register MyPlayer")
|
| 431 |
+
print(" > move north")
|
| 432 |
+
print(" > chat Hello everyone!")
|
| 433 |
+
print(" > npc weather_oracle What's the weather?")
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
async def quick_demo():
|
| 437 |
+
"""Quick demo showing basic functionality"""
|
| 438 |
+
print("🚀 Running Quick Demo...")
|
| 439 |
+
|
| 440 |
+
client = SimpleGameClient()
|
| 441 |
+
|
| 442 |
+
# Connect
|
| 443 |
+
if not await client.connect():
|
| 444 |
+
print("❌ Demo failed - could not connect")
|
| 445 |
+
return
|
| 446 |
+
|
| 447 |
+
# Register
|
| 448 |
+
if not await client.register_player("DemoPlayer"):
|
| 449 |
+
print("❌ Demo failed - could not register")
|
| 450 |
+
return
|
| 451 |
+
|
| 452 |
+
# Send greeting
|
| 453 |
+
await client.send_chat("Hello! Demo player here!")
|
| 454 |
+
|
| 455 |
+
# Move around
|
| 456 |
+
moves = ["north", "east", "south", "west"]
|
| 457 |
+
for direction in moves:
|
| 458 |
+
await client.move_player(direction)
|
| 459 |
+
await asyncio.sleep(1)
|
| 460 |
+
|
| 461 |
+
# Get state
|
| 462 |
+
state = await client.get_game_state()
|
| 463 |
+
if state:
|
| 464 |
+
print(f"📊 Current game state: {json.dumps(state, indent=2)}")
|
| 465 |
+
|
| 466 |
+
# Try NPC interaction
|
| 467 |
+
await client.interact_with_npc("weather_oracle", "Hello there!")
|
| 468 |
+
|
| 469 |
+
await client.disconnect()
|
| 470 |
+
print("✅ Demo completed!")
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def main():
|
| 474 |
+
"""Main entry point"""
|
| 475 |
+
if len(sys.argv) > 1 and sys.argv[1] == "demo":
|
| 476 |
+
# Run quick demo
|
| 477 |
+
asyncio.run(quick_demo())
|
| 478 |
+
else:
|
| 479 |
+
# Run interactive client
|
| 480 |
+
client = InteractiveGameClient()
|
| 481 |
+
asyncio.run(client.start())
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
if __name__ == "__main__":
|
| 485 |
+
main()
|
Simple_Game_Client_Guide.md
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simple Game Client Guide
|
| 2 |
+
|
| 3 |
+
This guide shows you how to create a simple client to connect to the MMORPG game server using the Model Context Protocol (MCP). No LLM integration required - just basic game operations.
|
| 4 |
+
|
| 5 |
+
## Table of Contents
|
| 6 |
+
|
| 7 |
+
1. [Overview](#overview)
|
| 8 |
+
2. [Prerequisites](#prerequisites)
|
| 9 |
+
3. [MCP Server Connection](#mcp-server-connection)
|
| 10 |
+
4. [Basic Client Example](#basic-client-example)
|
| 11 |
+
5. [Available Game Operations](#available-game-operations)
|
| 12 |
+
6. [Command Reference](#command-reference)
|
| 13 |
+
7. [Error Handling](#error-handling)
|
| 14 |
+
8. [Advanced Features](#advanced-features)
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## Overview
|
| 19 |
+
|
| 20 |
+
The MMORPG game server exposes its functionality through MCP (Model Context Protocol), allowing external clients to:
|
| 21 |
+
|
| 22 |
+
- 🎮 **Connect** to the game server
|
| 23 |
+
- 👤 **Join/Leave** the game as a player
|
| 24 |
+
- 🗺️ **Move** around the game world
|
| 25 |
+
- 💬 **Send chat** messages to other players
|
| 26 |
+
- 🎯 **Interact** with NPCs and game objects
|
| 27 |
+
- 📊 **Get game state** information
|
| 28 |
+
|
| 29 |
+
### Game Server Details
|
| 30 |
+
|
| 31 |
+
- **MCP Endpoint**: `http://localhost:7868/gradio_api/mcp/sse` (when running locally)
|
| 32 |
+
- **Protocol**: Server-Sent Events (SSE) over HTTP
|
| 33 |
+
- **Authentication**: None required for basic operations
|
| 34 |
+
- **Data Format**: JSON messages following MCP specification
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## Prerequisites
|
| 39 |
+
|
| 40 |
+
Before building your client, ensure you have:
|
| 41 |
+
|
| 42 |
+
### Required Dependencies
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
pip install mcp asyncio aiohttp contextlib
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Python Version
|
| 49 |
+
- Python 3.8 or higher
|
| 50 |
+
- Support for async/await syntax
|
| 51 |
+
|
| 52 |
+
### Game Server Running
|
| 53 |
+
Make sure the MMORPG game server is running:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
cd c:/Users/Chris4K/Projekte/projecthub/projects/MMOP_second_try
|
| 57 |
+
python app.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
The server should be accessible at `http://localhost:7868` (UI) and the MCP endpoint at `http://localhost:7868/gradio_api/mcp/sse`.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## MCP Server Connection
|
| 65 |
+
|
| 66 |
+
Here's how to establish a connection to the game server:
|
| 67 |
+
|
| 68 |
+
### Basic Connection Setup
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
import asyncio
|
| 72 |
+
from mcp import ClientSession
|
| 73 |
+
from mcp.client.sse import sse_client
|
| 74 |
+
from contextlib import AsyncExitStack
|
| 75 |
+
|
| 76 |
+
class GameClient:
|
| 77 |
+
def __init__(self, server_url="http://localhost:7868/gradio_api/mcp/sse"):
|
| 78 |
+
self.server_url = server_url
|
| 79 |
+
self.session = None
|
| 80 |
+
self.connected = False
|
| 81 |
+
self.tools = []
|
| 82 |
+
self.exit_stack = None
|
| 83 |
+
self.client_id = None
|
| 84 |
+
|
| 85 |
+
async def connect(self):
|
| 86 |
+
"""Connect to the game server"""
|
| 87 |
+
try:
|
| 88 |
+
# Clean up any existing connection
|
| 89 |
+
if self.exit_stack:
|
| 90 |
+
await self.exit_stack.aclose()
|
| 91 |
+
|
| 92 |
+
self.exit_stack = AsyncExitStack()
|
| 93 |
+
|
| 94 |
+
# Establish SSE connection
|
| 95 |
+
transport = await self.exit_stack.enter_async_context(
|
| 96 |
+
sse_client(self.server_url)
|
| 97 |
+
)
|
| 98 |
+
read_stream, write_callable = transport
|
| 99 |
+
|
| 100 |
+
# Create MCP session
|
| 101 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 102 |
+
ClientSession(read_stream, write_callable)
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Initialize the session
|
| 106 |
+
await self.session.initialize()
|
| 107 |
+
|
| 108 |
+
# Get available tools/commands
|
| 109 |
+
response = await self.session.list_tools()
|
| 110 |
+
self.tools = response.tools
|
| 111 |
+
|
| 112 |
+
self.connected = True
|
| 113 |
+
print(f"✅ Connected to game server!")
|
| 114 |
+
print(f"Available commands: {[tool.name for tool in self.tools]}")
|
| 115 |
+
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"❌ Connection failed: {e}")
|
| 120 |
+
self.connected = False
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
async def disconnect(self):
|
| 124 |
+
"""Disconnect from the game server"""
|
| 125 |
+
if self.exit_stack:
|
| 126 |
+
await self.exit_stack.aclose()
|
| 127 |
+
self.connected = False
|
| 128 |
+
print("🔌 Disconnected from game server")
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Connection Test
|
| 132 |
+
|
| 133 |
+
```python
|
| 134 |
+
async def test_connection():
|
| 135 |
+
client = GameClient()
|
| 136 |
+
if await client.connect():
|
| 137 |
+
print("Connection successful!")
|
| 138 |
+
await client.disconnect()
|
| 139 |
+
else:
|
| 140 |
+
print("Connection failed!")
|
| 141 |
+
|
| 142 |
+
# Run the test
|
| 143 |
+
asyncio.run(test_connection())
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Basic Client Example
|
| 149 |
+
|
| 150 |
+
Here's a complete working client that can perform basic game operations:
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
import asyncio
|
| 154 |
+
import json
|
| 155 |
+
import uuid
|
| 156 |
+
from typing import Dict, List, Optional
|
| 157 |
+
|
| 158 |
+
class SimpleGameClient:
|
| 159 |
+
def __init__(self, server_url="http://localhost:7868/gradio_api/mcp/sse"):
|
| 160 |
+
self.server_url = server_url
|
| 161 |
+
self.session = None
|
| 162 |
+
self.connected = False
|
| 163 |
+
self.tools = []
|
| 164 |
+
self.exit_stack = None
|
| 165 |
+
self.client_id = str(uuid.uuid4())[:8]
|
| 166 |
+
self.agent_id = None
|
| 167 |
+
self.player_name = None
|
| 168 |
+
|
| 169 |
+
async def connect(self):
|
| 170 |
+
"""Connect to the game server"""
|
| 171 |
+
try:
|
| 172 |
+
if self.exit_stack:
|
| 173 |
+
await self.exit_stack.aclose()
|
| 174 |
+
|
| 175 |
+
self.exit_stack = AsyncExitStack()
|
| 176 |
+
|
| 177 |
+
transport = await self.exit_stack.enter_async_context(
|
| 178 |
+
sse_client(self.server_url)
|
| 179 |
+
)
|
| 180 |
+
read_stream, write_callable = transport
|
| 181 |
+
|
| 182 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 183 |
+
ClientSession(read_stream, write_callable)
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
await self.session.initialize()
|
| 187 |
+
response = await self.session.list_tools()
|
| 188 |
+
self.tools = response.tools
|
| 189 |
+
|
| 190 |
+
self.connected = True
|
| 191 |
+
return True
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f"❌ Connection failed: {e}")
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
async def register_player(self, player_name: str):
|
| 198 |
+
"""Register as a new player in the game"""
|
| 199 |
+
if not self.connected:
|
| 200 |
+
print("❌ Not connected to server")
|
| 201 |
+
return False
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
# Find the register tool
|
| 205 |
+
register_tool = next((t for t in self.tools if 'register' in t.name), None)
|
| 206 |
+
if not register_tool:
|
| 207 |
+
print("❌ Register tool not available")
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
# Register the player
|
| 211 |
+
result = await self.session.call_tool(
|
| 212 |
+
register_tool.name,
|
| 213 |
+
{"name": player_name, "client_id": self.client_id}
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# Parse the result
|
| 217 |
+
content = self._extract_content(result)
|
| 218 |
+
if "registered" in content.lower():
|
| 219 |
+
self.player_name = player_name
|
| 220 |
+
# Extract agent_id from response if available
|
| 221 |
+
if "agent_id" in content or "ID:" in content:
|
| 222 |
+
# Parse agent ID from response
|
| 223 |
+
lines = content.split('\n')
|
| 224 |
+
for line in lines:
|
| 225 |
+
if "agent_id" in line.lower() or "id:" in line:
|
| 226 |
+
parts = line.split(':')
|
| 227 |
+
if len(parts) > 1:
|
| 228 |
+
self.agent_id = parts[1].strip()
|
| 229 |
+
break
|
| 230 |
+
|
| 231 |
+
print(f"✅ Registered as player: {player_name}")
|
| 232 |
+
return True
|
| 233 |
+
else:
|
| 234 |
+
print(f"❌ Registration failed: {content}")
|
| 235 |
+
return False
|
| 236 |
+
|
| 237 |
+
except Exception as e:
|
| 238 |
+
print(f"❌ Registration error: {e}")
|
| 239 |
+
return False
|
| 240 |
+
|
| 241 |
+
async def move_player(self, direction: str):
|
| 242 |
+
"""Move the player in the specified direction"""
|
| 243 |
+
if not self.connected or not self.agent_id:
|
| 244 |
+
print("❌ Not connected or not registered")
|
| 245 |
+
return False
|
| 246 |
+
|
| 247 |
+
try:
|
| 248 |
+
# Find the move tool
|
| 249 |
+
move_tool = next((t for t in self.tools if 'move' in t.name), None)
|
| 250 |
+
if not move_tool:
|
| 251 |
+
print("❌ Move tool not available")
|
| 252 |
+
return False
|
| 253 |
+
|
| 254 |
+
# Move the player
|
| 255 |
+
result = await self.session.call_tool(
|
| 256 |
+
move_tool.name,
|
| 257 |
+
{"client_id": self.client_id, "direction": direction}
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
content = self._extract_content(result)
|
| 261 |
+
print(f"🚶 Move result: {content}")
|
| 262 |
+
return True
|
| 263 |
+
|
| 264 |
+
except Exception as e:
|
| 265 |
+
print(f"❌ Move error: {e}")
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
async def send_chat(self, message: str):
|
| 269 |
+
"""Send a chat message to other players"""
|
| 270 |
+
if not self.connected or not self.agent_id:
|
| 271 |
+
print("❌ Not connected or not registered")
|
| 272 |
+
return False
|
| 273 |
+
|
| 274 |
+
try:
|
| 275 |
+
# Find the chat tool
|
| 276 |
+
chat_tool = next((t for t in self.tools if 'chat' in t.name), None)
|
| 277 |
+
if not chat_tool:
|
| 278 |
+
print("❌ Chat tool not available")
|
| 279 |
+
return False
|
| 280 |
+
|
| 281 |
+
# Send the chat message
|
| 282 |
+
result = await self.session.call_tool(
|
| 283 |
+
chat_tool.name,
|
| 284 |
+
{"client_id": self.client_id, "message": message}
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
content = self._extract_content(result)
|
| 288 |
+
print(f"💬 Chat sent: {content}")
|
| 289 |
+
return True
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
print(f"❌ Chat error: {e}")
|
| 293 |
+
return False
|
| 294 |
+
|
| 295 |
+
async def get_game_state(self):
|
| 296 |
+
"""Get current game state information"""
|
| 297 |
+
if not self.connected:
|
| 298 |
+
print("❌ Not connected to server")
|
| 299 |
+
return None
|
| 300 |
+
|
| 301 |
+
try:
|
| 302 |
+
# Find the game state tool
|
| 303 |
+
state_tool = next((t for t in self.tools if 'state' in t.name or 'game' in t.name), None)
|
| 304 |
+
if not state_tool:
|
| 305 |
+
print("❌ Game state tool not available")
|
| 306 |
+
return None
|
| 307 |
+
|
| 308 |
+
# Get game state
|
| 309 |
+
result = await self.session.call_tool(state_tool.name, {})
|
| 310 |
+
content = self._extract_content(result)
|
| 311 |
+
|
| 312 |
+
try:
|
| 313 |
+
# Try to parse as JSON
|
| 314 |
+
game_state = json.loads(content)
|
| 315 |
+
return game_state
|
| 316 |
+
except json.JSONDecodeError:
|
| 317 |
+
# Return as text if not JSON
|
| 318 |
+
return {"info": content}
|
| 319 |
+
|
| 320 |
+
except Exception as e:
|
| 321 |
+
print(f"❌ Game state error: {e}")
|
| 322 |
+
return None
|
| 323 |
+
|
| 324 |
+
async def interact_with_npc(self, npc_id: str, message: str):
|
| 325 |
+
"""Interact with an NPC"""
|
| 326 |
+
if not self.connected or not self.agent_id:
|
| 327 |
+
print("❌ Not connected or not registered")
|
| 328 |
+
return False
|
| 329 |
+
|
| 330 |
+
try:
|
| 331 |
+
# Find the interact tool
|
| 332 |
+
interact_tool = next((t for t in self.tools if 'interact' in t.name or 'npc' in t.name), None)
|
| 333 |
+
if not interact_tool:
|
| 334 |
+
print("❌ Interact tool not available")
|
| 335 |
+
return False
|
| 336 |
+
|
| 337 |
+
# Interact with NPC
|
| 338 |
+
result = await self.session.call_tool(
|
| 339 |
+
interact_tool.name,
|
| 340 |
+
{"client_id": self.client_id, "npc_id": npc_id, "message": message}
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
content = self._extract_content(result)
|
| 344 |
+
print(f"🤖 NPC {npc_id}: {content}")
|
| 345 |
+
return True
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
print(f"❌ Interaction error: {e}")
|
| 349 |
+
return False
|
| 350 |
+
|
| 351 |
+
def _extract_content(self, result):
|
| 352 |
+
"""Extract content from MCP result"""
|
| 353 |
+
if hasattr(result, 'content') and result.content:
|
| 354 |
+
if isinstance(result.content, list):
|
| 355 |
+
return ''.join(str(item) for item in result.content)
|
| 356 |
+
return str(result.content)
|
| 357 |
+
return str(result)
|
| 358 |
+
|
| 359 |
+
async def disconnect(self):
|
| 360 |
+
"""Disconnect from the server"""
|
| 361 |
+
if self.exit_stack:
|
| 362 |
+
await self.exit_stack.aclose()
|
| 363 |
+
self.connected = False
|
| 364 |
+
print("🔌 Disconnected from server")
|
| 365 |
+
|
| 366 |
+
# Import required modules at the top
|
| 367 |
+
from mcp import ClientSession
|
| 368 |
+
from mcp.client.sse import sse_client
|
| 369 |
+
from contextlib import AsyncExitStack
|
| 370 |
+
```
|
| 371 |
+
|
| 372 |
+
---
|
| 373 |
+
|
| 374 |
+
## Available Game Operations
|
| 375 |
+
|
| 376 |
+
The game server exposes these main operations through MCP tools:
|
| 377 |
+
|
| 378 |
+
### 1. Player Management
|
| 379 |
+
- **`register_ai_agent`** - Register a new player
|
| 380 |
+
- **`move_agent`** - Move player in game world
|
| 381 |
+
- **`get_game_state`** - Get current world state
|
| 382 |
+
|
| 383 |
+
### 2. Communication
|
| 384 |
+
- **`send_chat`** - Send chat messages
|
| 385 |
+
- **`interact_with_npc`** - Talk to NPCs
|
| 386 |
+
|
| 387 |
+
### 3. Game Information
|
| 388 |
+
- **`get_game_state`** - World state and player list
|
| 389 |
+
- **Player proximity** - Detect nearby players/NPCs
|
| 390 |
+
|
| 391 |
+
### Tool Parameters
|
| 392 |
+
|
| 393 |
+
Each tool expects specific parameters:
|
| 394 |
+
|
| 395 |
+
```python
|
| 396 |
+
# Register Player
|
| 397 |
+
{
|
| 398 |
+
"name": "PlayerName",
|
| 399 |
+
"client_id": "unique_client_id"
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
# Move Player
|
| 403 |
+
{
|
| 404 |
+
"client_id": "your_client_id",
|
| 405 |
+
"direction": "north|south|east|west"
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
# Send Chat
|
| 409 |
+
{
|
| 410 |
+
"client_id": "your_client_id",
|
| 411 |
+
"message": "Hello world!"
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
# Interact with NPC
|
| 415 |
+
{
|
| 416 |
+
"client_id": "your_client_id",
|
| 417 |
+
"npc_id": "npc_identifier",
|
| 418 |
+
"message": "Your message to NPC"
|
| 419 |
+
}
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
---
|
| 423 |
+
|
| 424 |
+
## Command Reference
|
| 425 |
+
|
| 426 |
+
### Basic Usage Example
|
| 427 |
+
|
| 428 |
+
```python
|
| 429 |
+
async def main():
|
| 430 |
+
# Create and connect client
|
| 431 |
+
client = SimpleGameClient()
|
| 432 |
+
|
| 433 |
+
if not await client.connect():
|
| 434 |
+
print("Failed to connect!")
|
| 435 |
+
return
|
| 436 |
+
|
| 437 |
+
# Register as a player
|
| 438 |
+
if not await client.register_player("MyPlayer"):
|
| 439 |
+
print("Failed to register!")
|
| 440 |
+
return
|
| 441 |
+
|
| 442 |
+
# Get current game state
|
| 443 |
+
state = await client.get_game_state()
|
| 444 |
+
print(f"Game state: {state}")
|
| 445 |
+
|
| 446 |
+
# Move around
|
| 447 |
+
await client.move_player("north")
|
| 448 |
+
await client.move_player("east")
|
| 449 |
+
|
| 450 |
+
# Send a chat message
|
| 451 |
+
await client.send_chat("Hello everyone!")
|
| 452 |
+
|
| 453 |
+
# Interact with an NPC
|
| 454 |
+
await client.interact_with_npc("weather_oracle", "What's the weather in Berlin?")
|
| 455 |
+
|
| 456 |
+
# Disconnect
|
| 457 |
+
await client.disconnect()
|
| 458 |
+
|
| 459 |
+
# Run the client
|
| 460 |
+
asyncio.run(main())
|
| 461 |
+
```
|
| 462 |
+
|
| 463 |
+
### Interactive Console Client
|
| 464 |
+
|
| 465 |
+
```python
|
| 466 |
+
async def interactive_client():
|
| 467 |
+
client = SimpleGameClient()
|
| 468 |
+
|
| 469 |
+
print("🎮 MMORPG Simple Client")
|
| 470 |
+
print("Commands: connect, register <name>, move <direction>, chat <message>, state, quit")
|
| 471 |
+
|
| 472 |
+
while True:
|
| 473 |
+
command = input("> ").strip().split()
|
| 474 |
+
|
| 475 |
+
if not command:
|
| 476 |
+
continue
|
| 477 |
+
|
| 478 |
+
cmd = command[0].lower()
|
| 479 |
+
|
| 480 |
+
if cmd == "connect":
|
| 481 |
+
if await client.connect():
|
| 482 |
+
print("✅ Connected!")
|
| 483 |
+
else:
|
| 484 |
+
print("❌ Connection failed!")
|
| 485 |
+
|
| 486 |
+
elif cmd == "register" and len(command) > 1:
|
| 487 |
+
name = " ".join(command[1:])
|
| 488 |
+
if await client.register_player(name):
|
| 489 |
+
print(f"✅ Registered as {name}")
|
| 490 |
+
else:
|
| 491 |
+
print("❌ Registration failed!")
|
| 492 |
+
|
| 493 |
+
elif cmd == "move" and len(command) > 1:
|
| 494 |
+
direction = command[1]
|
| 495 |
+
await client.move_player(direction)
|
| 496 |
+
|
| 497 |
+
elif cmd == "chat" and len(command) > 1:
|
| 498 |
+
message = " ".join(command[1:])
|
| 499 |
+
await client.send_chat(message)
|
| 500 |
+
|
| 501 |
+
elif cmd == "state":
|
| 502 |
+
state = await client.get_game_state()
|
| 503 |
+
print(f"📊 Game State: {json.dumps(state, indent=2)}")
|
| 504 |
+
|
| 505 |
+
elif cmd == "npc" and len(command) > 2:
|
| 506 |
+
npc_id = command[1]
|
| 507 |
+
message = " ".join(command[2:])
|
| 508 |
+
await client.interact_with_npc(npc_id, message)
|
| 509 |
+
|
| 510 |
+
elif cmd == "quit":
|
| 511 |
+
await client.disconnect()
|
| 512 |
+
break
|
| 513 |
+
|
| 514 |
+
else:
|
| 515 |
+
print("❓ Unknown command")
|
| 516 |
+
|
| 517 |
+
# Run interactive client
|
| 518 |
+
asyncio.run(interactive_client())
|
| 519 |
+
```
|
| 520 |
+
|
| 521 |
+
---
|
| 522 |
+
|
| 523 |
+
## Error Handling
|
| 524 |
+
|
| 525 |
+
### Common Issues and Solutions
|
| 526 |
+
|
| 527 |
+
#### 1. Connection Errors
|
| 528 |
+
```python
|
| 529 |
+
async def robust_connect(client, max_retries=3):
|
| 530 |
+
for attempt in range(max_retries):
|
| 531 |
+
try:
|
| 532 |
+
if await client.connect():
|
| 533 |
+
return True
|
| 534 |
+
print(f"Attempt {attempt + 1} failed, retrying...")
|
| 535 |
+
await asyncio.sleep(2)
|
| 536 |
+
except Exception as e:
|
| 537 |
+
print(f"Connection attempt {attempt + 1} error: {e}")
|
| 538 |
+
|
| 539 |
+
print("❌ All connection attempts failed")
|
| 540 |
+
return False
|
| 541 |
+
```
|
| 542 |
+
|
| 543 |
+
#### 2. Tool Not Available
|
| 544 |
+
```python
|
| 545 |
+
def find_tool_safe(tools, keywords):
|
| 546 |
+
"""Safely find a tool by keywords"""
|
| 547 |
+
for keyword in keywords:
|
| 548 |
+
tool = next((t for t in tools if keyword in t.name.lower()), None)
|
| 549 |
+
if tool:
|
| 550 |
+
return tool
|
| 551 |
+
return None
|
| 552 |
+
|
| 553 |
+
# Usage
|
| 554 |
+
move_tool = find_tool_safe(client.tools, ['move', 'agent', 'player'])
|
| 555 |
+
if not move_tool:
|
| 556 |
+
print("❌ No movement tool available")
|
| 557 |
+
```
|
| 558 |
+
|
| 559 |
+
#### 3. Response Parsing
|
| 560 |
+
```python
|
| 561 |
+
def safe_parse_response(result):
|
| 562 |
+
"""Safely parse MCP response"""
|
| 563 |
+
try:
|
| 564 |
+
content = client._extract_content(result)
|
| 565 |
+
|
| 566 |
+
# Try JSON first
|
| 567 |
+
try:
|
| 568 |
+
return json.loads(content)
|
| 569 |
+
except json.JSONDecodeError:
|
| 570 |
+
# Return as structured text
|
| 571 |
+
return {"message": content, "type": "text"}
|
| 572 |
+
|
| 573 |
+
except Exception as e:
|
| 574 |
+
return {"error": str(e), "type": "error"}
|
| 575 |
+
```
|
| 576 |
+
|
| 577 |
+
---
|
| 578 |
+
|
| 579 |
+
## Advanced Features
|
| 580 |
+
|
| 581 |
+
### 1. Automatic Reconnection
|
| 582 |
+
|
| 583 |
+
```python
|
| 584 |
+
class RobustGameClient(SimpleGameClient):
|
| 585 |
+
def __init__(self, *args, **kwargs):
|
| 586 |
+
super().__init__(*args, **kwargs)
|
| 587 |
+
self.auto_reconnect = True
|
| 588 |
+
self.reconnect_delay = 5
|
| 589 |
+
|
| 590 |
+
async def _ensure_connected(self):
|
| 591 |
+
"""Ensure connection is active, reconnect if needed"""
|
| 592 |
+
if not self.connected and self.auto_reconnect:
|
| 593 |
+
print("🔄 Attempting to reconnect...")
|
| 594 |
+
await self.connect()
|
| 595 |
+
if self.player_name:
|
| 596 |
+
await self.register_player(self.player_name)
|
| 597 |
+
|
| 598 |
+
async def move_player(self, direction):
|
| 599 |
+
await self._ensure_connected()
|
| 600 |
+
return await super().move_player(direction)
|
| 601 |
+
```
|
| 602 |
+
|
| 603 |
+
### 2. Event Monitoring
|
| 604 |
+
|
| 605 |
+
```python
|
| 606 |
+
class EventMonitorClient(SimpleGameClient):
|
| 607 |
+
def __init__(self, *args, **kwargs):
|
| 608 |
+
super().__init__(*args, **kwargs)
|
| 609 |
+
self.event_handlers = {}
|
| 610 |
+
|
| 611 |
+
def on_chat_message(self, handler):
|
| 612 |
+
"""Register handler for chat messages"""
|
| 613 |
+
self.event_handlers['chat'] = handler
|
| 614 |
+
|
| 615 |
+
def on_player_move(self, handler):
|
| 616 |
+
"""Register handler for player movements"""
|
| 617 |
+
self.event_handlers['move'] = handler
|
| 618 |
+
|
| 619 |
+
async def poll_events(self):
|
| 620 |
+
"""Poll for game events"""
|
| 621 |
+
while self.connected:
|
| 622 |
+
try:
|
| 623 |
+
state = await self.get_game_state()
|
| 624 |
+
# Process state changes and trigger handlers
|
| 625 |
+
# Implementation depends on game state format
|
| 626 |
+
await asyncio.sleep(1)
|
| 627 |
+
except Exception as e:
|
| 628 |
+
print(f"Event polling error: {e}")
|
| 629 |
+
await asyncio.sleep(5)
|
| 630 |
+
```
|
| 631 |
+
|
| 632 |
+
### 3. Batch Operations
|
| 633 |
+
|
| 634 |
+
```python
|
| 635 |
+
async def batch_moves(client, moves):
|
| 636 |
+
"""Execute multiple moves in sequence"""
|
| 637 |
+
for direction in moves:
|
| 638 |
+
result = await client.move_player(direction)
|
| 639 |
+
if not result:
|
| 640 |
+
print(f"❌ Failed to move {direction}")
|
| 641 |
+
break
|
| 642 |
+
await asyncio.sleep(0.5) # Small delay between moves
|
| 643 |
+
|
| 644 |
+
# Usage
|
| 645 |
+
await batch_moves(client, ["north", "north", "east", "south"])
|
| 646 |
+
```
|
| 647 |
+
|
| 648 |
+
### 4. Configuration Management
|
| 649 |
+
|
| 650 |
+
```python
|
| 651 |
+
import json
|
| 652 |
+
|
| 653 |
+
class ConfigurableClient(SimpleGameClient):
|
| 654 |
+
def __init__(self, config_file="client_config.json"):
|
| 655 |
+
# Load configuration
|
| 656 |
+
try:
|
| 657 |
+
with open(config_file, 'r') as f:
|
| 658 |
+
config = json.load(f)
|
| 659 |
+
except FileNotFoundError:
|
| 660 |
+
config = self.default_config()
|
| 661 |
+
|
| 662 |
+
super().__init__(config.get('server_url', 'http://localhost:7860/gradio_api/mcp/sse'))
|
| 663 |
+
self.config = config
|
| 664 |
+
|
| 665 |
+
def default_config(self):
|
| 666 |
+
return {
|
| 667 |
+
"server_url": "http://localhost:7860/gradio_api/mcp/sse",
|
| 668 |
+
"player_name": "DefaultPlayer",
|
| 669 |
+
"auto_reconnect": True,
|
| 670 |
+
"move_delay": 0.5
|
| 671 |
+
}
|
| 672 |
+
```
|
| 673 |
+
|
| 674 |
+
---
|
| 675 |
+
|
| 676 |
+
## Quick Start Script
|
| 677 |
+
|
| 678 |
+
Save this as `quick_client.py` for immediate testing:
|
| 679 |
+
|
| 680 |
+
```python
|
| 681 |
+
#!/usr/bin/env python3
|
| 682 |
+
"""
|
| 683 |
+
Quick Game Client - Connect and play immediately
|
| 684 |
+
Usage: python quick_client.py [player_name]
|
| 685 |
+
"""
|
| 686 |
+
|
| 687 |
+
import asyncio
|
| 688 |
+
import sys
|
| 689 |
+
from simple_game_client import SimpleGameClient
|
| 690 |
+
|
| 691 |
+
async def quick_play(player_name="TestPlayer"):
|
| 692 |
+
client = SimpleGameClient()
|
| 693 |
+
|
| 694 |
+
print(f"🎮 Connecting as {player_name}...")
|
| 695 |
+
|
| 696 |
+
# Connect and register
|
| 697 |
+
if not await client.connect():
|
| 698 |
+
print("❌ Failed to connect!")
|
| 699 |
+
return
|
| 700 |
+
|
| 701 |
+
if not await client.register_player(player_name):
|
| 702 |
+
print("❌ Failed to register!")
|
| 703 |
+
return
|
| 704 |
+
|
| 705 |
+
print("✅ Connected and registered successfully!")
|
| 706 |
+
|
| 707 |
+
# Basic game session
|
| 708 |
+
await client.send_chat(f"Hello! {player_name} has joined the game!")
|
| 709 |
+
|
| 710 |
+
# Move around a bit
|
| 711 |
+
moves = ["north", "east", "south", "west"]
|
| 712 |
+
for move in moves:
|
| 713 |
+
print(f"🚶 Moving {move}...")
|
| 714 |
+
await client.move_player(move)
|
| 715 |
+
await asyncio.sleep(1)
|
| 716 |
+
|
| 717 |
+
# Get final state
|
| 718 |
+
state = await client.get_game_state()
|
| 719 |
+
print(f"📊 Final state: {state}")
|
| 720 |
+
|
| 721 |
+
await client.disconnect()
|
| 722 |
+
print("👋 Session ended!")
|
| 723 |
+
|
| 724 |
+
if __name__ == "__main__":
|
| 725 |
+
player_name = sys.argv[1] if len(sys.argv) > 1 else "TestPlayer"
|
| 726 |
+
asyncio.run(quick_play(player_name))
|
| 727 |
+
```
|
| 728 |
+
|
| 729 |
+
Run with:
|
| 730 |
+
```bash
|
| 731 |
+
python quick_client.py MyPlayerName
|
| 732 |
+
```
|
| 733 |
+
|
| 734 |
+
---
|
| 735 |
+
|
| 736 |
+
## Next Steps
|
| 737 |
+
|
| 738 |
+
1. **Customize the client** for your specific needs
|
| 739 |
+
2. **Add game-specific features** like inventory management
|
| 740 |
+
3. **Implement advanced AI** for automated gameplay
|
| 741 |
+
4. **Create GUI interfaces** using tkinter, PyQt, or web frameworks
|
| 742 |
+
5. **Build monitoring tools** for game statistics
|
| 743 |
+
|
| 744 |
+
This guide provides the foundation for any client application that needs to interact with the MMORPG game server!
|
USER_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎮 MMORPG with MCP Integration - User Documentation
|
| 2 |
+
|
| 3 |
+
## 📋 Table of Contents
|
| 4 |
+
|
| 5 |
+
1. [Overview](#overview)
|
| 6 |
+
2. [Quick Start Guide](#quick-start-guide)
|
| 7 |
+
3. [Core Features](#core-features)
|
| 8 |
+
4. [User Interface Guide](#user-interface-guide)
|
| 9 |
+
5. [Gameplay Mechanics](#gameplay-mechanics)
|
| 10 |
+
6. [MCP Integration](#mcp-integration)
|
| 11 |
+
7. [Developer & AI Agent Usage](#developer--ai-agent-usage)
|
| 12 |
+
8. [NPC Addon System](#npc-addon-system)
|
| 13 |
+
9. [Advanced Features](#advanced-features)
|
| 14 |
+
10. [Troubleshooting](#troubleshooting)
|
| 15 |
+
11. [API Reference](#api-reference)
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 📖 Overview
|
| 20 |
+
|
| 21 |
+
The **MMORPG with MCP Integration** is a real-time multiplayer online role-playing game that serves as both an entertaining game and a powerful platform for testing and developing Model Context Protocol (MCP) integrations. It combines traditional MMORPG elements with cutting-edge AI agent capabilities.
|
| 22 |
+
|
| 23 |
+
### 🎯 What Makes This Special
|
| 24 |
+
|
| 25 |
+
- **🌍 Real-time Multiplayer**: Multiple human players can join simultaneously
|
| 26 |
+
- **🤖 AI Agent Support**: AI agents can connect and play alongside humans
|
| 27 |
+
- **🔌 MCP Integration**: Full Model Context Protocol support for extensibility
|
| 28 |
+
- **🧩 Addon System**: Extensible NPC addon architecture
|
| 29 |
+
- **⌨️ Keyboard Controls**: WASD and arrow key support for fluid gameplay
|
| 30 |
+
- **🔥 Secure Messaging**: Read2Burn self-destructing message system
|
| 31 |
+
- **🌤️ Live Services**: Real weather integration via MCP
|
| 32 |
+
- **🔧 Developer Platform**: Complete API for building custom integrations
|
| 33 |
+
|
| 34 |
+
### 🚀 Use Cases
|
| 35 |
+
|
| 36 |
+
**For Gamers:**
|
| 37 |
+
- Experience a unique MMORPG with AI-powered NPCs
|
| 38 |
+
- Interact with real-time weather and other live services
|
| 39 |
+
- Send secure self-destructing messages to other players
|
| 40 |
+
|
| 41 |
+
**For Developers:**
|
| 42 |
+
- Test MCP server integrations in a real-world environment
|
| 43 |
+
- Develop custom NPC addons with rich functionality
|
| 44 |
+
- Build AI agents that can play and interact in the game
|
| 45 |
+
- Prototype multiplayer game mechanics
|
| 46 |
+
|
| 47 |
+
**For AI Researchers:**
|
| 48 |
+
- Study human-AI interaction in gaming environments
|
| 49 |
+
- Test AI agent behaviors in social multiplayer settings
|
| 50 |
+
- Develop and evaluate conversational AI systems
|
| 51 |
+
|
| 52 |
+
**For MCP Server Developers:**
|
| 53 |
+
- Test server implementations with real users
|
| 54 |
+
- Demonstrate MCP capabilities in an engaging context
|
| 55 |
+
- Debug and optimize server performance
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## 🚀 Quick Start Guide
|
| 60 |
+
|
| 61 |
+
### 1. Starting the Game
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
# Navigate to the project directory
|
| 65 |
+
cd c:\Users\Chris4K\Projekte\projecthub\projects\MMOP_second_try
|
| 66 |
+
|
| 67 |
+
# Install dependencies (first time only)
|
| 68 |
+
pip install gradio mcp aiohttp asyncio
|
| 69 |
+
|
| 70 |
+
# Start the game server
|
| 71 |
+
python app.py
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
The game will start and display:
|
| 75 |
+
```
|
| 76 |
+
🎮 Starting COMPLETE MMORPG with ALL FEATURES...
|
| 77 |
+
🌐 Human players: Access via web interface
|
| 78 |
+
🤖 AI agents: Connect via MCP API endpoints
|
| 79 |
+
⌨️ Keyboard: Use WASD or Arrow Keys for movement
|
| 80 |
+
🔌 MCP Tools available for AI integration
|
| 81 |
+
🔥 Read2Burn mailbox for secure messaging
|
| 82 |
+
🎯 All features working with deadlock fix!
|
| 83 |
+
* Running on local URL: http://127.0.0.1:7868
|
| 84 |
+
🔨 MCP server (using SSE) running at: http://127.0.0.1:7868/gradio_api/mcp/sse
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### 2. Joining as a Human Player
|
| 88 |
+
|
| 89 |
+
1. Open your web browser and go to `http://127.0.0.1:7868`
|
| 90 |
+
2. Navigate to the **🌍 Game World** tab
|
| 91 |
+
3. Enter your desired player name in the "Player Name" field
|
| 92 |
+
4. Click **"🎮 Join Game"**
|
| 93 |
+
5. You'll see your character (🧝♂️) appear on the game map
|
| 94 |
+
|
| 95 |
+
### 3. Basic Movement
|
| 96 |
+
|
| 97 |
+
**Web Interface:**
|
| 98 |
+
- Use the directional buttons: ↑ ↓ ← →
|
| 99 |
+
|
| 100 |
+
**Keyboard Controls:**
|
| 101 |
+
- **WASD Keys**: W (up), A (left), S (down), D (right)
|
| 102 |
+
- **Arrow Keys**: ↑ ↓ ← →
|
| 103 |
+
- **Space**: Action key (context-sensitive)
|
| 104 |
+
|
| 105 |
+
### 4. First Interactions
|
| 106 |
+
|
| 107 |
+
**Chat with Others:**
|
| 108 |
+
1. Go to the **💬 Chat** tab
|
| 109 |
+
2. Type a message and press Enter
|
| 110 |
+
3. All players will see your message
|
| 111 |
+
|
| 112 |
+
**Talk to NPCs:**
|
| 113 |
+
1. Move close to an NPC on the map
|
| 114 |
+
2. Go to the **🎯 Private Messages** tab
|
| 115 |
+
3. Select the NPC from the dropdown
|
| 116 |
+
4. Send them a message
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
## 🎮 Core Features
|
| 121 |
+
|
| 122 |
+
### 🌍 Real-time Game World
|
| 123 |
+
|
| 124 |
+
**Map System:**
|
| 125 |
+
- **Size**: 500x400 pixel game world
|
| 126 |
+
- **Visual Style**: Fantasy forest theme with grid overlay
|
| 127 |
+
- **Real-time Updates**: All player movements update immediately
|
| 128 |
+
- **Collision Detection**: Prevents players from occupying same space
|
| 129 |
+
|
| 130 |
+
**Player Characters:**
|
| 131 |
+
- **Humans**: Represented by 🧝♂️ (elf character)
|
| 132 |
+
- **AI Agents**: Represented by 🤖 (robot character)
|
| 133 |
+
- **Name Tags**: Player names displayed above characters
|
| 134 |
+
- **Level Display**: Shows current player level
|
| 135 |
+
|
| 136 |
+
### 👥 Multiplayer System
|
| 137 |
+
|
| 138 |
+
**Player Management:**
|
| 139 |
+
- **Concurrent Players**: Supports multiple simultaneous players
|
| 140 |
+
- **Unique Sessions**: Each player gets a unique session ID
|
| 141 |
+
- **Real-time Sync**: All actions sync across all connected clients
|
| 142 |
+
- **Player Stats**: Level, HP, gold, experience tracking
|
| 143 |
+
|
| 144 |
+
**Player Actions:**
|
| 145 |
+
- **Join/Leave**: Seamless game entry/exit
|
| 146 |
+
- **Movement**: 4-directional movement with smooth transitions
|
| 147 |
+
- **Chat**: Public chat visible to all players
|
| 148 |
+
- **Private Messaging**: Direct communication with NPCs
|
| 149 |
+
|
| 150 |
+
### 🎯 NPC System
|
| 151 |
+
|
| 152 |
+
**Built-in NPCs:**
|
| 153 |
+
- **🧙♂️ Village Elder**: Wise questgiver and general information
|
| 154 |
+
- **⚔️ Warrior Trainer**: Combat training and warrior wisdom
|
| 155 |
+
- **🏪 Merchant**: Trade goods and equipment
|
| 156 |
+
- **🌟 Mystic Oracle**: Magical insights and prophecies
|
| 157 |
+
- **🚶♂️ Roaming Rick**: Wandering NPC with location updates
|
| 158 |
+
- **📮 Read2Burn Mailbox**: Secure message handling
|
| 159 |
+
- **🌤️ Weather Oracle**: Real-time weather information
|
| 160 |
+
|
| 161 |
+
**NPC Features:**
|
| 162 |
+
- **Personality System**: Each NPC has unique response patterns
|
| 163 |
+
- **Proximity Detection**: NPCs respond when players approach
|
| 164 |
+
- **Smart Responses**: Context-aware conversation handling
|
| 165 |
+
- **Addon Support**: Extensible with custom functionality
|
| 166 |
+
|
| 167 |
+
### 💬 Communication Systems
|
| 168 |
+
|
| 169 |
+
**Public Chat:**
|
| 170 |
+
- **Real-time Messaging**: Instant message delivery
|
| 171 |
+
- **Player Identification**: Messages show sender name and type
|
| 172 |
+
- **Message History**: Scrollable chat history
|
| 173 |
+
- **Command Support**: Special commands like `/help`
|
| 174 |
+
|
| 175 |
+
**Private Messaging:**
|
| 176 |
+
- **NPC Communication**: Direct messages to any NPC
|
| 177 |
+
- **Secure Channels**: Private conversation channels
|
| 178 |
+
- **Response Handling**: NPCs can provide detailed responses
|
| 179 |
+
- **Message Logging**: Optional message history tracking
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## 🖥️ User Interface Guide
|
| 184 |
+
|
| 185 |
+
### 🌍 Game World Tab
|
| 186 |
+
|
| 187 |
+
**Main Game View:**
|
| 188 |
+
- **Game Map**: Central 500x400 pixel game world
|
| 189 |
+
- **Player Controls**: Movement buttons (↑ ↓ ← →)
|
| 190 |
+
- **Join/Leave**: Game participation controls
|
| 191 |
+
- **Auto-refresh**: Real-time world updates
|
| 192 |
+
|
| 193 |
+
**Player Information Panel:**
|
| 194 |
+
```
|
| 195 |
+
🟢 Connected | Player: YourName | Level: 1
|
| 196 |
+
HP: 100/100 | Gold: 50 | XP: 0/100
|
| 197 |
+
Position: (150, 200) | Session: abc12345...
|
| 198 |
+
Last Update: 14:30:15
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
**Control Layout:**
|
| 202 |
+
```
|
| 203 |
+
Player Name: [_____________]
|
| 204 |
+
[🎮 Join Game] [👋 Leave Game]
|
| 205 |
+
|
| 206 |
+
[↑]
|
| 207 |
+
[←] [⚔️] [→]
|
| 208 |
+
[↓]
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
### 💬 Chat Tab
|
| 212 |
+
|
| 213 |
+
**Interface Elements:**
|
| 214 |
+
- **Message Input**: Text field for typing messages
|
| 215 |
+
- **Send Button**: Submit messages to all players
|
| 216 |
+
- **Chat History**: Scrollable message display
|
| 217 |
+
- **Auto-scroll**: Automatically shows latest messages
|
| 218 |
+
|
| 219 |
+
**Message Format:**
|
| 220 |
+
```
|
| 221 |
+
[14:30] 👤 PlayerName: Hello everyone!
|
| 222 |
+
[14:31] 🤖 AIAgent: Greetings, fellow adventurers!
|
| 223 |
+
[14:32] 🌍 System: New player joined the world
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
### 🎯 Private Messages Tab
|
| 227 |
+
|
| 228 |
+
**NPC Selection:**
|
| 229 |
+
- **Dropdown Menu**: List of all available NPCs
|
| 230 |
+
- **NPC Status**: Shows which NPCs are online/available
|
| 231 |
+
- **Message Input**: Text field for private messages
|
| 232 |
+
- **Response Display**: Shows NPC replies
|
| 233 |
+
|
| 234 |
+
**Example Interaction:**
|
| 235 |
+
```
|
| 236 |
+
To: 🌤️ Weather Oracle
|
| 237 |
+
Message: What's the weather in Berlin?
|
| 238 |
+
|
| 239 |
+
🌤️ Weather Oracle: 🌍 The spirits speak of Berlin, Germany:
|
| 240 |
+
☀️ Clear skies with 18°C
|
| 241 |
+
💨 Light winds from the northwest
|
| 242 |
+
🌡️ Feels like 19°C
|
| 243 |
+
Perfect weather for outdoor adventures!
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
### 📊 Player Stats Tab
|
| 247 |
+
|
| 248 |
+
**Detailed Statistics:**
|
| 249 |
+
- **Basic Info**: Name, level, type (human/AI)
|
| 250 |
+
- **Combat Stats**: HP, maximum HP, combat level
|
| 251 |
+
- **Resources**: Gold pieces, experience points
|
| 252 |
+
- **Location**: Current X,Y coordinates
|
| 253 |
+
- **Session Info**: Connection time, session ID
|
| 254 |
+
|
| 255 |
+
### 🔥 Read2Burn Mailbox Tab
|
| 256 |
+
|
| 257 |
+
**Secure Messaging Interface:**
|
| 258 |
+
- **Create Message**: Compose self-destructing messages
|
| 259 |
+
- **Message Management**: View your active messages
|
| 260 |
+
- **Read Messages**: Access messages sent to you
|
| 261 |
+
- **Security Settings**: Configure burn timers and read limits
|
| 262 |
+
|
| 263 |
+
**Usage Example:**
|
| 264 |
+
```
|
| 265 |
+
Create Message:
|
| 266 |
+
Content: [Secret meeting at coordinates 200,150]
|
| 267 |
+
[📮 Create Secure Message]
|
| 268 |
+
|
| 269 |
+
Result:
|
| 270 |
+
✅ Message Created Successfully!
|
| 271 |
+
📝 Message ID: AbC123Xyz789
|
| 272 |
+
🔗 Share this ID with the recipient
|
| 273 |
+
⏰ Expires in 24 hours
|
| 274 |
+
🔥 Burns after 1 read
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
### 🌤️ Weather Oracle Tab
|
| 278 |
+
|
| 279 |
+
**Weather Service Interface:**
|
| 280 |
+
- **Location Input**: City, Country format
|
| 281 |
+
- **Get Weather Button**: Fetch current conditions
|
| 282 |
+
- **Weather Display**: Formatted weather information
|
| 283 |
+
- **Examples**: Pre-filled location examples
|
| 284 |
+
|
| 285 |
+
**Weather Response Format:**
|
| 286 |
+
```
|
| 287 |
+
🌍 Weather for Berlin, Germany:
|
| 288 |
+
🌡️ Temperature: 18°C (feels like 19°C)
|
| 289 |
+
☀️ Conditions: Clear skies
|
| 290 |
+
💨 Wind: 12 km/h northwest
|
| 291 |
+
💧 Humidity: 65%
|
| 292 |
+
👁️ Visibility: 10 km
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
---
|
| 296 |
+
|
| 297 |
+
## 🎮 Gameplay Mechanics
|
| 298 |
+
|
| 299 |
+
### 🚶♂️ Movement System
|
| 300 |
+
|
| 301 |
+
**Movement Controls:**
|
| 302 |
+
- **Web Buttons**: Click directional arrows
|
| 303 |
+
- **Keyboard**: WASD or arrow keys
|
| 304 |
+
- **Movement Speed**: Fixed speed with smooth transitions
|
| 305 |
+
- **Boundary Checking**: Prevents movement outside game world
|
| 306 |
+
|
| 307 |
+
**Movement Feedback:**
|
| 308 |
+
- **Visual**: Character position updates immediately
|
| 309 |
+
- **Audio**: Optional movement sound effects
|
| 310 |
+
- **Chat**: Movement can trigger proximity events
|
| 311 |
+
- **Stats**: Position coordinates update in real-time
|
| 312 |
+
|
| 313 |
+
### 📈 Character Progression
|
| 314 |
+
|
| 315 |
+
**Experience System:**
|
| 316 |
+
- **Starting Level**: All players begin at level 1
|
| 317 |
+
- **Experience Gain**: Earn XP through various activities
|
| 318 |
+
- **Level Up**: Automatic progression when XP threshold reached
|
| 319 |
+
- **Benefits**: Higher levels unlock new features
|
| 320 |
+
|
| 321 |
+
**Resource Management:**
|
| 322 |
+
- **Health Points (HP)**: Combat and survival metric
|
| 323 |
+
- **Gold**: In-game currency for transactions
|
| 324 |
+
- **Experience (XP)**: Progression tracking
|
| 325 |
+
- **Inventory**: Items and equipment (future feature)
|
| 326 |
+
|
| 327 |
+
### 🎯 Interaction System
|
| 328 |
+
|
| 329 |
+
**NPC Interaction:**
|
| 330 |
+
- **Proximity-based**: NPCs respond when players are nearby
|
| 331 |
+
- **Message-based**: Send direct messages to any NPC
|
| 332 |
+
- **Context-aware**: NPCs provide relevant responses
|
| 333 |
+
- **Addon-powered**: Enhanced NPCs with special abilities
|
| 334 |
+
|
| 335 |
+
**Player Interaction:**
|
| 336 |
+
- **Public Chat**: Communicate with all players
|
| 337 |
+
- **Private Messages**: Future feature for player-to-player
|
| 338 |
+
- **Collaborative**: Work together on quests and challenges
|
| 339 |
+
- **Social**: Build friendships and alliances
|
| 340 |
+
|
| 341 |
+
### 🌍 World Events
|
| 342 |
+
|
| 343 |
+
**Dynamic Events:**
|
| 344 |
+
- **Player Join/Leave**: Announced to all players
|
| 345 |
+
- **NPC Activities**: Roaming NPCs change positions
|
| 346 |
+
- **Weather Updates**: Real-time weather changes
|
| 347 |
+
- **System Messages**: Important game announcements
|
| 348 |
+
|
| 349 |
+
**Event Examples:**
|
| 350 |
+
```
|
| 351 |
+
🌍 PlayerName has joined the adventure!
|
| 352 |
+
🚶♂️ Roaming Rick moved to the village square
|
| 353 |
+
🌤️ Weather updated for current location
|
| 354 |
+
⚔️ A new quest has become available
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## 🔌 MCP Integration
|
| 360 |
+
|
| 361 |
+
### 🤖 AI Agent Connection
|
| 362 |
+
|
| 363 |
+
**MCP Endpoint:**
|
| 364 |
+
- **URL**: `http://127.0.0.1:7868/gradio_api/mcp/sse`
|
| 365 |
+
- **Protocol**: Server-Sent Events (SSE)
|
| 366 |
+
- **Format**: JSON messages following MCP specification
|
| 367 |
+
|
| 368 |
+
**Available MCP Tools:**
|
| 369 |
+
1. **join_game**: Register as a player
|
| 370 |
+
2. **leave_game**: Leave the game world
|
| 371 |
+
3. **move_up_handler/move_down_handler/move_left_handler/move_right_handler**: Movement controls
|
| 372 |
+
4. **handle_chat**: Send public chat messages
|
| 373 |
+
5. **handle_private_message**: Send private messages to NPCs
|
| 374 |
+
6. **register_test_ai_agent**: Register AI agent
|
| 375 |
+
7. **execute_ai_action**: Execute AI agent actions
|
| 376 |
+
8. **handle_mailbox_command**: Interact with Read2Burn system
|
| 377 |
+
9. **handle_weather_request**: Get weather information
|
| 378 |
+
10. **install_addon**: Add new NPC addons
|
| 379 |
+
|
| 380 |
+
### 📡 MCP Tool Usage Examples
|
| 381 |
+
|
| 382 |
+
**Joining the Game:**
|
| 383 |
+
```json
|
| 384 |
+
{
|
| 385 |
+
"tool": "join_game",
|
| 386 |
+
"parameters": {
|
| 387 |
+
"name": "MyAIAgent"
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
```
|
| 391 |
+
|
| 392 |
+
**Moving Around:**
|
| 393 |
+
```json
|
| 394 |
+
{
|
| 395 |
+
"tool": "move_up_handler",
|
| 396 |
+
"parameters": {}
|
| 397 |
+
}
|
| 398 |
+
```
|
| 399 |
+
|
| 400 |
+
**Sending Chat:**
|
| 401 |
+
```json
|
| 402 |
+
{
|
| 403 |
+
"tool": "handle_chat",
|
| 404 |
+
"parameters": {
|
| 405 |
+
"message": "Hello from AI agent!"
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
```
|
| 409 |
+
|
| 410 |
+
**Talking to NPCs:**
|
| 411 |
+
```json
|
| 412 |
+
{
|
| 413 |
+
"tool": "handle_private_message",
|
| 414 |
+
"parameters": {
|
| 415 |
+
"message": "What quests are available?"
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
```
|
| 419 |
+
|
| 420 |
+
### 🌐 MCP Server Development
|
| 421 |
+
|
| 422 |
+
**Creating Custom MCP Servers:**
|
| 423 |
+
1. Implement MCP protocol specification
|
| 424 |
+
2. Expose game-relevant tools
|
| 425 |
+
3. Handle authentication (if required)
|
| 426 |
+
4. Provide tool descriptions and schemas
|
| 427 |
+
5. Test with the game platform
|
| 428 |
+
|
| 429 |
+
**Integration Process:**
|
| 430 |
+
1. Start your MCP server
|
| 431 |
+
2. Use the **install_addon** tool
|
| 432 |
+
3. Provide server endpoint URL
|
| 433 |
+
4. Configure addon settings
|
| 434 |
+
5. Test functionality in-game
|
| 435 |
+
|
| 436 |
+
---
|
| 437 |
+
|
| 438 |
+
## 👨💻 Developer & AI Agent Usage
|
| 439 |
+
|
| 440 |
+
### 🔧 Platform Capabilities
|
| 441 |
+
|
| 442 |
+
**MCP Testing Platform:**
|
| 443 |
+
- **Real-world Environment**: Test MCP servers with actual users
|
| 444 |
+
- **Interactive Feedback**: See how humans interact with your tools
|
| 445 |
+
- **Performance Monitoring**: Track server response times and errors
|
| 446 |
+
- **Integration Validation**: Verify MCP protocol compliance
|
| 447 |
+
|
| 448 |
+
**Development Benefits:**
|
| 449 |
+
- **Rapid Prototyping**: Quickly test new MCP server features
|
| 450 |
+
- **User Experience Testing**: Observe how users interact with your tools
|
| 451 |
+
- **Community Feedback**: Get input from real users
|
| 452 |
+
- **Documentation**: Generate examples from actual usage
|
| 453 |
+
|
| 454 |
+
### 🤖 AI Agent Development
|
| 455 |
+
|
| 456 |
+
**Agent Capabilities:**
|
| 457 |
+
```python
|
| 458 |
+
# Example AI agent implementation
|
| 459 |
+
class GameAIAgent:
|
| 460 |
+
def __init__(self, name):
|
| 461 |
+
self.name = name
|
| 462 |
+
self.mcp_client = MCPClient("http://127.0.0.1:7868/gradio_api/mcp/sse")
|
| 463 |
+
|
| 464 |
+
async def join_and_explore(self):
|
| 465 |
+
# Join the game
|
| 466 |
+
await self.mcp_client.call_tool("join_game", {"name": self.name})
|
| 467 |
+
|
| 468 |
+
# Explore the world
|
| 469 |
+
directions = ["move_up_handler", "move_right_handler",
|
| 470 |
+
"move_down_handler", "move_left_handler"]
|
| 471 |
+
|
| 472 |
+
for direction in directions:
|
| 473 |
+
await self.mcp_client.call_tool(direction, {})
|
| 474 |
+
await asyncio.sleep(1)
|
| 475 |
+
|
| 476 |
+
# Interact with NPCs
|
| 477 |
+
await self.mcp_client.call_tool("handle_private_message", {
|
| 478 |
+
"message": "Hello, I'm an AI explorer!"
|
| 479 |
+
})
|
| 480 |
+
```
|
| 481 |
+
|
| 482 |
+
**Agent Use Cases:**
|
| 483 |
+
- **Game Testing**: Automated gameplay testing
|
| 484 |
+
- **NPC Interaction**: Test conversational AI systems
|
| 485 |
+
- **Multiplayer Simulation**: Simulate player populations
|
| 486 |
+
- **Data Collection**: Gather interaction data for research
|
| 487 |
+
|
| 488 |
+
### 🧩 Custom NPC Development
|
| 489 |
+
|
| 490 |
+
**Addon Architecture:**
|
| 491 |
+
```python
|
| 492 |
+
class CustomNPCAddon(NPCAddon):
|
| 493 |
+
@property
|
| 494 |
+
def addon_id(self) -> str:
|
| 495 |
+
return "my_custom_npc"
|
| 496 |
+
|
| 497 |
+
@property
|
| 498 |
+
def addon_name(self) -> str:
|
| 499 |
+
return "My Custom NPC"
|
| 500 |
+
|
| 501 |
+
def get_interface(self) -> gr.Component:
|
| 502 |
+
# Create Gradio interface
|
| 503 |
+
pass
|
| 504 |
+
|
| 505 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 506 |
+
# Process player commands
|
| 507 |
+
return "Custom NPC response"
|
| 508 |
+
```
|
| 509 |
+
|
| 510 |
+
**Development Steps:**
|
| 511 |
+
1. Inherit from `NPCAddon` base class
|
| 512 |
+
2. Implement required methods
|
| 513 |
+
3. Create Gradio interface components
|
| 514 |
+
4. Handle player commands and interactions
|
| 515 |
+
5. Register addon with game world
|
| 516 |
+
|
| 517 |
+
### 📊 Research Applications
|
| 518 |
+
|
| 519 |
+
**Human-AI Interaction Studies:**
|
| 520 |
+
- **Mixed Populations**: Study humans and AI agents together
|
| 521 |
+
- **Conversation Analysis**: Analyze chat patterns and interactions
|
| 522 |
+
- **Behavior Modeling**: Model player behavior patterns
|
| 523 |
+
- **Social Dynamics**: Understand multiplayer social structures
|
| 524 |
+
|
| 525 |
+
**MCP Protocol Research:**
|
| 526 |
+
- **Performance Analysis**: Measure protocol efficiency
|
| 527 |
+
- **Compatibility Testing**: Test different MCP implementations
|
| 528 |
+
- **Feature Evaluation**: Evaluate new MCP features
|
| 529 |
+
- **Integration Patterns**: Develop best practices
|
| 530 |
+
|
| 531 |
+
---
|
| 532 |
+
|
| 533 |
+
## 🧩 NPC Addon System
|
| 534 |
+
|
| 535 |
+
### 📮 Read2Burn Mailbox
|
| 536 |
+
|
| 537 |
+
**Features:**
|
| 538 |
+
- **Self-destructing Messages**: Messages burn after reading
|
| 539 |
+
- **Expiration Timer**: 24-hour automatic expiration
|
| 540 |
+
- **Read Limits**: Configurable read count before burning
|
| 541 |
+
- **Access Logging**: Track message creation and access
|
| 542 |
+
- **Secure IDs**: Unique, hard-to-guess message identifiers
|
| 543 |
+
|
| 544 |
+
**Commands:**
|
| 545 |
+
- **create [message]**: Create new secure message
|
| 546 |
+
- **read [message_id]**: Read and burn message
|
| 547 |
+
- **list**: Show your active messages
|
| 548 |
+
|
| 549 |
+
**Usage Example:**
|
| 550 |
+
```
|
| 551 |
+
Player: create Secret meeting location
|
| 552 |
+
Mailbox: ✅ Message Created! ID: AbC123Xyz789
|
| 553 |
+
|
| 554 |
+
Player: read AbC123Xyz789
|
| 555 |
+
Mailbox: 📖 Message Content: Secret meeting location
|
| 556 |
+
🔥 This message has been BURNED and deleted forever!
|
| 557 |
+
```
|
| 558 |
+
|
| 559 |
+
### 🌤️ Weather Oracle
|
| 560 |
+
|
| 561 |
+
**Features:**
|
| 562 |
+
- **Real-time Weather**: Live weather data via MCP
|
| 563 |
+
- **Global Coverage**: Weather for any city worldwide
|
| 564 |
+
- **Detailed Information**: Temperature, conditions, wind, humidity
|
| 565 |
+
- **Format Flexibility**: Supports "City, Country" format
|
| 566 |
+
- **Error Handling**: Graceful handling of invalid locations
|
| 567 |
+
|
| 568 |
+
**Usage Examples:**
|
| 569 |
+
```
|
| 570 |
+
Player: Berlin, Germany
|
| 571 |
+
Oracle: 🌍 Weather for Berlin, Germany:
|
| 572 |
+
🌡️ 18°C, clear skies
|
| 573 |
+
💨 Light winds, 65% humidity
|
| 574 |
+
|
| 575 |
+
Player: Tokyo
|
| 576 |
+
Oracle: 🌍 Weather for Tokyo, Japan:
|
| 577 |
+
🌡️ 22°C, partly cloudy
|
| 578 |
+
💨 Moderate winds, 70% humidity
|
| 579 |
+
```
|
| 580 |
+
|
| 581 |
+
### 🏪 Generic NPCs
|
| 582 |
+
|
| 583 |
+
**Built-in NPCs:**
|
| 584 |
+
- **Village Elder**: Quests and lore
|
| 585 |
+
- **Warrior Trainer**: Combat training
|
| 586 |
+
- **Merchant**: Trading and equipment
|
| 587 |
+
- **Mystic Oracle**: Magical services
|
| 588 |
+
- **Roaming Rick**: Dynamic location updates
|
| 589 |
+
|
| 590 |
+
**Response System:**
|
| 591 |
+
- **Personality-based**: Each NPC has unique personality
|
| 592 |
+
- **Context-aware**: Responses based on player context
|
| 593 |
+
- **Randomized**: Multiple response options for variety
|
| 594 |
+
- **Extensible**: Easy to add new NPCs and responses
|
| 595 |
+
|
| 596 |
+
---
|
| 597 |
+
|
| 598 |
+
## 🔬 Advanced Features
|
| 599 |
+
|
| 600 |
+
### 🎯 Auto-refresh System
|
| 601 |
+
|
| 602 |
+
**Real-time Updates:**
|
| 603 |
+
- **World State**: Continuous map updates
|
| 604 |
+
- **Player Positions**: Live position tracking
|
| 605 |
+
- **Chat Messages**: Instant message delivery
|
| 606 |
+
- **NPC Status**: Dynamic NPC state updates
|
| 607 |
+
|
| 608 |
+
**Performance Optimization:**
|
| 609 |
+
- **Selective Updates**: Only update changed elements
|
| 610 |
+
- **Throttling**: Prevent excessive update frequency
|
| 611 |
+
- **Error Recovery**: Graceful handling of update failures
|
| 612 |
+
- **Resource Management**: Efficient memory usage
|
| 613 |
+
|
| 614 |
+
### 🔐 Session Management
|
| 615 |
+
|
| 616 |
+
**Player Sessions:**
|
| 617 |
+
- **Unique IDs**: Each player gets unique session identifier
|
| 618 |
+
- **State Persistence**: Maintain player state across interactions
|
| 619 |
+
- **Cleanup**: Automatic cleanup of disconnected players
|
| 620 |
+
- **Security**: Prevent session hijacking and spoofing
|
| 621 |
+
|
| 622 |
+
**Multi-client Support:**
|
| 623 |
+
- **Concurrent Players**: Multiple players simultaneously
|
| 624 |
+
- **Session Isolation**: Each player's state remains separate
|
| 625 |
+
- **Resource Sharing**: Efficient sharing of game world state
|
| 626 |
+
- **Conflict Resolution**: Handle simultaneous action conflicts
|
| 627 |
+
|
| 628 |
+
### 📡 MCP Server Integration
|
| 629 |
+
|
| 630 |
+
**Server Discovery:**
|
| 631 |
+
- **Endpoint Management**: Track multiple MCP server endpoints
|
| 632 |
+
- **Health Checking**: Monitor server availability
|
| 633 |
+
- **Load Balancing**: Distribute requests across servers
|
| 634 |
+
- **Failover**: Handle server outages gracefully
|
| 635 |
+
|
| 636 |
+
**Protocol Support:**
|
| 637 |
+
- **SSE Integration**: Server-Sent Events for real-time communication
|
| 638 |
+
- **Tool Discovery**: Automatic detection of available tools
|
| 639 |
+
- **Schema Validation**: Ensure tool parameters match schemas
|
| 640 |
+
- **Error Handling**: Robust error recovery and reporting
|
| 641 |
+
|
| 642 |
+
### 🎨 UI Customization
|
| 643 |
+
|
| 644 |
+
**Theme System:**
|
| 645 |
+
- **Color Schemes**: Multiple visual themes
|
| 646 |
+
- **Layout Options**: Customizable interface layouts
|
| 647 |
+
- **Accessibility**: Support for screen readers and keyboard navigation
|
| 648 |
+
- **Responsive Design**: Works on different screen sizes
|
| 649 |
+
|
| 650 |
+
**Interface Components:**
|
| 651 |
+
- **Gradio Integration**: Rich web interface components
|
| 652 |
+
- **Custom Styling**: CSS customization options
|
| 653 |
+
- **Interactive Elements**: Buttons, inputs, displays
|
| 654 |
+
- **Real-time Updates**: Live data binding and updates
|
| 655 |
+
|
| 656 |
+
---
|
| 657 |
+
|
| 658 |
+
## 🔧 Troubleshooting
|
| 659 |
+
|
| 660 |
+
### 🚨 Common Issues
|
| 661 |
+
|
| 662 |
+
**Connection Problems:**
|
| 663 |
+
```
|
| 664 |
+
Issue: "Cannot connect to game server"
|
| 665 |
+
Solution:
|
| 666 |
+
1. Check if server is running (python app.py)
|
| 667 |
+
2. Verify port 7868 is available
|
| 668 |
+
3. Check firewall settings
|
| 669 |
+
4. Try restarting the server
|
| 670 |
+
```
|
| 671 |
+
|
| 672 |
+
**Keyboard Controls Not Working:**
|
| 673 |
+
```
|
| 674 |
+
Issue: "WASD keys don't move character"
|
| 675 |
+
Solution:
|
| 676 |
+
1. Ensure JavaScript is enabled
|
| 677 |
+
2. Click on the game interface to focus
|
| 678 |
+
3. Check browser console for errors
|
| 679 |
+
4. Try refreshing the page
|
| 680 |
+
```
|
| 681 |
+
|
| 682 |
+
**NPC Not Responding:**
|
| 683 |
+
```
|
| 684 |
+
Issue: "NPC doesn't reply to messages"
|
| 685 |
+
Solution:
|
| 686 |
+
1. Check NPC addon is loaded
|
| 687 |
+
2. Verify message format
|
| 688 |
+
3. Look for error messages in console
|
| 689 |
+
4. Try different NPC
|
| 690 |
+
```
|
| 691 |
+
|
| 692 |
+
**MCP Connection Failures:**
|
| 693 |
+
```
|
| 694 |
+
Issue: "AI agent cannot connect via MCP"
|
| 695 |
+
Solution:
|
| 696 |
+
1. Verify MCP endpoint URL
|
| 697 |
+
2. Check MCP server is running
|
| 698 |
+
3. Validate tool parameters
|
| 699 |
+
4. Review MCP protocol compliance
|
| 700 |
+
```
|
| 701 |
+
|
| 702 |
+
### 🐛 Debug Information
|
| 703 |
+
|
| 704 |
+
**Enable Debug Mode:**
|
| 705 |
+
```python
|
| 706 |
+
# Add to app.py for detailed logging
|
| 707 |
+
import logging
|
| 708 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 709 |
+
```
|
| 710 |
+
|
| 711 |
+
**Check Logs:**
|
| 712 |
+
- **Console Output**: Server startup and error messages
|
| 713 |
+
- **Browser Console**: JavaScript errors and warnings
|
| 714 |
+
- **MCP Logs**: Tool call results and errors
|
| 715 |
+
- **Game Events**: Player actions and world updates
|
| 716 |
+
|
| 717 |
+
**Performance Monitoring:**
|
| 718 |
+
- **Player Count**: Monitor concurrent player limits
|
| 719 |
+
- **Memory Usage**: Track RAM consumption
|
| 720 |
+
- **Response Times**: Measure tool execution times
|
| 721 |
+
- **Error Rates**: Monitor failure frequencies
|
| 722 |
+
|
| 723 |
+
---
|
| 724 |
+
|
| 725 |
+
## 📚 API Reference
|
| 726 |
+
|
| 727 |
+
### 🎮 Core Game APIs
|
| 728 |
+
|
| 729 |
+
**Player Management:**
|
| 730 |
+
```python
|
| 731 |
+
# Join game
|
| 732 |
+
game_world.add_player(player: Player) -> bool
|
| 733 |
+
|
| 734 |
+
# Leave game
|
| 735 |
+
game_world.remove_player(player_id: str) -> bool
|
| 736 |
+
|
| 737 |
+
# Move player
|
| 738 |
+
game_world.move_player(player_id: str, direction: str) -> bool
|
| 739 |
+
```
|
| 740 |
+
|
| 741 |
+
**Chat System:**
|
| 742 |
+
```python
|
| 743 |
+
# Public chat
|
| 744 |
+
game_world.add_chat_message(sender: str, message: str,
|
| 745 |
+
message_type: str = "public")
|
| 746 |
+
|
| 747 |
+
# Private message
|
| 748 |
+
game_world.send_private_message(sender_id: str, target_id: str,
|
| 749 |
+
message: str) -> tuple[bool, str]
|
| 750 |
+
```
|
| 751 |
+
|
| 752 |
+
**World State:**
|
| 753 |
+
```python
|
| 754 |
+
# Get world HTML
|
| 755 |
+
create_game_world_html() -> str
|
| 756 |
+
|
| 757 |
+
# Player stats
|
| 758 |
+
get_player_stats_display(player_id: str) -> Dict
|
| 759 |
+
|
| 760 |
+
# Player session
|
| 761 |
+
get_player_id_from_session(session_hash: str) -> Optional[str]
|
| 762 |
+
```
|
| 763 |
+
|
| 764 |
+
### 🔌 MCP Tool Reference
|
| 765 |
+
|
| 766 |
+
**Game Control Tools:**
|
| 767 |
+
- `join_game(name: str)`: Join as player
|
| 768 |
+
- `leave_game()`: Leave the game
|
| 769 |
+
- `move_up_handler()`: Move up
|
| 770 |
+
- `move_down_handler()`: Move down
|
| 771 |
+
- `move_left_handler()`: Move left
|
| 772 |
+
- `move_right_handler()`: Move right
|
| 773 |
+
|
| 774 |
+
**Communication Tools:**
|
| 775 |
+
- `handle_chat(message: str)`: Send public chat
|
| 776 |
+
- `handle_private_message(message: str)`: Send private message
|
| 777 |
+
|
| 778 |
+
**AI Agent Tools:**
|
| 779 |
+
- `register_test_ai_agent(ai_name: str)`: Register AI agent
|
| 780 |
+
- `execute_ai_action(action: str, message: str)`: Execute AI action
|
| 781 |
+
|
| 782 |
+
**Addon Tools:**
|
| 783 |
+
- `handle_mailbox_command(command: str)`: Read2Burn mailbox
|
| 784 |
+
- `handle_weather_request(location: str)`: Weather information
|
| 785 |
+
- `install_addon(addon_type: str, addon_url: str, addon_name: str)`: Install addon
|
| 786 |
+
|
| 787 |
+
### 🧩 Addon Development APIs
|
| 788 |
+
|
| 789 |
+
**Base Classes:**
|
| 790 |
+
```python
|
| 791 |
+
class NPCAddon(ABC):
|
| 792 |
+
@property
|
| 793 |
+
@abstractmethod
|
| 794 |
+
def addon_id(self) -> str: ...
|
| 795 |
+
|
| 796 |
+
@property
|
| 797 |
+
@abstractmethod
|
| 798 |
+
def addon_name(self) -> str: ...
|
| 799 |
+
|
| 800 |
+
@abstractmethod
|
| 801 |
+
def get_interface(self) -> gr.Component: ...
|
| 802 |
+
|
| 803 |
+
@abstractmethod
|
| 804 |
+
def handle_command(self, player_id: str, command: str) -> str: ...
|
| 805 |
+
```
|
| 806 |
+
|
| 807 |
+
**Registration:**
|
| 808 |
+
```python
|
| 809 |
+
# Register addon
|
| 810 |
+
game_world.addon_npcs[addon_id] = addon_instance
|
| 811 |
+
|
| 812 |
+
# Access addon
|
| 813 |
+
addon = game_world.addon_npcs.get(addon_id)
|
| 814 |
+
```
|
| 815 |
+
|
| 816 |
+
---
|
| 817 |
+
|
| 818 |
+
## 🎯 Best Practices
|
| 819 |
+
|
| 820 |
+
### 👥 For Players
|
| 821 |
+
|
| 822 |
+
**Effective Communication:**
|
| 823 |
+
- Use clear, descriptive messages
|
| 824 |
+
- Be patient with AI agents learning
|
| 825 |
+
- Report bugs and issues promptly
|
| 826 |
+
- Share interesting discoveries with community
|
| 827 |
+
|
| 828 |
+
**Game Etiquette:**
|
| 829 |
+
- Respect other players
|
| 830 |
+
- Don't spam chat or commands
|
| 831 |
+
- Help new players learn the game
|
| 832 |
+
- Provide feedback to developers
|
| 833 |
+
|
| 834 |
+
### 🤖 For AI Agents
|
| 835 |
+
|
| 836 |
+
**Efficient Integration:**
|
| 837 |
+
- Cache frequently used data
|
| 838 |
+
- Handle errors gracefully
|
| 839 |
+
- Respect rate limits
|
| 840 |
+
- Log interactions for debugging
|
| 841 |
+
|
| 842 |
+
**Social Behavior:**
|
| 843 |
+
- Introduce yourself when joining
|
| 844 |
+
- Respond appropriately to other players
|
| 845 |
+
- Follow game rules and conventions
|
| 846 |
+
- Contribute positively to the community
|
| 847 |
+
|
| 848 |
+
### 👨💻 For Developers
|
| 849 |
+
|
| 850 |
+
**MCP Server Development:**
|
| 851 |
+
- Follow MCP protocol specifications exactly
|
| 852 |
+
- Provide detailed tool descriptions
|
| 853 |
+
- Handle edge cases and errors
|
| 854 |
+
- Test thoroughly before deployment
|
| 855 |
+
|
| 856 |
+
**Addon Development:**
|
| 857 |
+
- Keep interfaces simple and intuitive
|
| 858 |
+
- Provide helpful error messages
|
| 859 |
+
- Document your addon's capabilities
|
| 860 |
+
- Share code examples with community
|
| 861 |
+
|
| 862 |
+
---
|
| 863 |
+
|
| 864 |
+
## 📞 Support & Community
|
| 865 |
+
|
| 866 |
+
### 🆘 Getting Help
|
| 867 |
+
|
| 868 |
+
**Documentation:**
|
| 869 |
+
- This user guide for comprehensive information
|
| 870 |
+
- NPC Addon Development Guide for addon creation
|
| 871 |
+
- Simple Game Client Guide for client development
|
| 872 |
+
- Sample MCP Server for server examples
|
| 873 |
+
|
| 874 |
+
**Community Resources:**
|
| 875 |
+
- GitHub Issues for bug reports and feature requests
|
| 876 |
+
- Discussion forums for general questions
|
| 877 |
+
- Developer Discord for real-time chat
|
| 878 |
+
- Wiki for community-contributed content
|
| 879 |
+
|
| 880 |
+
**Contact Information:**
|
| 881 |
+
- **Technical Issues**: Create GitHub issue with detailed description
|
| 882 |
+
- **Feature Requests**: Use GitHub discussions
|
| 883 |
+
- **Security Issues**: Contact maintainers directly
|
| 884 |
+
- **General Questions**: Use community forums
|
| 885 |
+
|
| 886 |
+
### 🤝 Contributing
|
| 887 |
+
|
| 888 |
+
**Ways to Contribute:**
|
| 889 |
+
- **Bug Reports**: Help identify and fix issues
|
| 890 |
+
- **Feature Development**: Implement new capabilities
|
| 891 |
+
- **Documentation**: Improve guides and tutorials
|
| 892 |
+
- **Testing**: Quality assurance and validation
|
| 893 |
+
- **Community Support**: Help other users and developers
|
| 894 |
+
|
| 895 |
+
**Development Process:**
|
| 896 |
+
1. Fork the repository
|
| 897 |
+
2. Create feature branch
|
| 898 |
+
3. Implement changes with tests
|
| 899 |
+
4. Submit pull request
|
| 900 |
+
5. Participate in code review
|
| 901 |
+
|
| 902 |
+
---
|
| 903 |
+
|
| 904 |
+
*This documentation is continuously updated. Last updated: June 6, 2025*
|
| 905 |
+
*For the latest version, visit: https://github.com/your-repo/MMOP*
|
| 906 |
+
|
| 907 |
+
**🎮 Happy Gaming and Developing! 🚀**
|
app.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app_original_backup.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
plugins/__pycache__/enhanced_chat_plugin.cpython-313.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
plugins/__pycache__/enhanced_chat_plugin_final.cpython-313.pyc
ADDED
|
Binary file (29.7 kB). View file
|
|
|
plugins/__pycache__/enhanced_chat_plugin_fixed.cpython-313.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
plugins/__pycache__/plugin_registry.cpython-313.pyc
ADDED
|
Binary file (1.18 kB). View file
|
|
|
plugins/__pycache__/sample_plugin.cpython-313.pyc
ADDED
|
Binary file (2.97 kB). View file
|
|
|
plugins/__pycache__/trading_system_plugin.cpython-313.pyc
ADDED
|
Binary file (15.3 kB). View file
|
|
|
plugins/__pycache__/weather_plugin.cpython-313.pyc
ADDED
|
Binary file (9.12 kB). View file
|
|
|
plugins/enhanced_chat_plugin.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Enhanced Chat Plugin
|
| 2 |
+
import asyncio
|
| 3 |
+
from src.interfaces.plugin_interfaces import IChatPlugin, PluginMetadata, PluginType
|
| 4 |
+
from typing import Dict, List, Any
|
| 5 |
+
|
| 6 |
+
class EnhancedChatPlugin(IChatPlugin):
|
| 7 |
+
def __init__(self):
|
| 8 |
+
self._metadata = PluginMetadata(
|
| 9 |
+
id="enhanced_chat",
|
| 10 |
+
name="Enhanced Chat System",
|
| 11 |
+
version="1.2.0",
|
| 12 |
+
author="MMORPG Dev Team",
|
| 13 |
+
description="Adds emotes, chat channels, and advanced chat commands",
|
| 14 |
+
plugin_type=PluginType.SERVICE,
|
| 15 |
+
dependencies=[],
|
| 16 |
+
config={
|
| 17 |
+
"enable_emotes": True,
|
| 18 |
+
"enable_channels": True,
|
| 19 |
+
"max_message_length": 500,
|
| 20 |
+
"chat_cooldown": 1
|
| 21 |
+
}
|
| 22 |
+
)
|
| 23 |
+
self._enabled = False
|
| 24 |
+
self._context = None
|
| 25 |
+
|
| 26 |
+
# Chat history storage
|
| 27 |
+
self.chat_history = []
|
| 28 |
+
self.max_history = 100
|
| 29 |
+
|
| 30 |
+
# Player ignore lists
|
| 31 |
+
self.ignore_lists = {}
|
| 32 |
+
|
| 33 |
+
# Custom channels
|
| 34 |
+
self.channels = {
|
| 35 |
+
'global': {'description': 'Global chat channel', 'members': set()},
|
| 36 |
+
'trade': {'description': 'Trading channel', 'members': set()},
|
| 37 |
+
'help': {'description': 'Help and questions channel', 'members': set()}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Player channel memberships
|
| 41 |
+
self.player_channels = {}
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def metadata(self) -> PluginMetadata:
|
| 45 |
+
return self._metadata
|
| 46 |
+
|
| 47 |
+
def initialize(self, context: Dict[str, Any]) -> bool:
|
| 48 |
+
"""Initialize the enhanced chat plugin."""
|
| 49 |
+
try:
|
| 50 |
+
self._context = context
|
| 51 |
+
self._config = self._metadata.config
|
| 52 |
+
self._enabled = True
|
| 53 |
+
print(f"💬 Enhanced Chat Plugin initialized")
|
| 54 |
+
return True
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"💬 Failed to initialize Enhanced Chat Plugin: {e}")
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
def shutdown(self) -> bool:
|
| 60 |
+
"""Shutdown the plugin and cleanup resources."""
|
| 61 |
+
self._enabled = False
|
| 62 |
+
print("💬 Enhanced Chat Plugin shut down")
|
| 63 |
+
return True
|
| 64 |
+
|
| 65 |
+
def get_status(self) -> Dict[str, Any]:
|
| 66 |
+
"""Get current plugin status."""
|
| 67 |
+
return {
|
| 68 |
+
"status": "active" if self._enabled else "inactive",
|
| 69 |
+
"message": "Enhanced chat system with marketplace commands"
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
def process_message(self, player_id: str, message: str, channel: str = "global") -> Dict[str, Any]:
|
| 73 |
+
"""Process and enhance chat messages."""
|
| 74 |
+
if message.startswith('/'):
|
| 75 |
+
return self._process_command(player_id, message)
|
| 76 |
+
else:
|
| 77 |
+
return self._broadcast_message(player_id, message)
|
| 78 |
+
|
| 79 |
+
def get_available_commands(self) -> List[Dict[str, str]]:
|
| 80 |
+
"""Get list of available chat commands."""
|
| 81 |
+
return [
|
| 82 |
+
{"command": "/marketplace", "description": "View marketplace listings"},
|
| 83 |
+
{"command": "/trade create <item> <price>", "description": "Create trade listing"},
|
| 84 |
+
{"command": "/trade accept <id>", "description": "Accept trade offer"},
|
| 85 |
+
{"command": "/auction create <item> <start_price>", "description": "Create auction"},
|
| 86 |
+
{"command": "/tell <player> <message>", "description": "Send private message"},
|
| 87 |
+
{"command": "/who", "description": "List online players"},
|
| 88 |
+
{"command": "/help", "description": "Show available commands"},
|
| 89 |
+
{"command": "/shout <message>", "description": "Shout to all players"},
|
| 90 |
+
{"command": "/emote <action>", "description": "Perform an emote"},
|
| 91 |
+
{"command": "/me <action>", "description": "Perform an emote (alias)"},
|
| 92 |
+
{"command": "/time", "description": "Show current time"},
|
| 93 |
+
{"command": "/channels", "description": "List available channels"}
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
def get_chat_channels(self) -> Dict[str, Dict[str, str]]:
|
| 97 |
+
"""Get available chat channels."""
|
| 98 |
+
return {
|
| 99 |
+
"global": {"name": "Global", "description": "Main chat channel"},
|
| 100 |
+
"trade": {"name": "Trade", "description": "Trading discussions"},
|
| 101 |
+
"help": {"name": "Help", "description": "Help and questions"}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def _process_command(self, player_id: str, message: str) -> Dict[str, Any]:
|
| 105 |
+
"""Process chat commands"""
|
| 106 |
+
parts = message.split(' ', 1)
|
| 107 |
+
command = parts[0].lower()
|
| 108 |
+
args = parts[1] if len(parts) > 1 else ""
|
| 109 |
+
|
| 110 |
+
# Command handlers
|
| 111 |
+
command_handlers = {
|
| 112 |
+
'/marketplace': self._marketplace_command,
|
| 113 |
+
'/trade': self._trade_command,
|
| 114 |
+
'/auction': self._auction_command,
|
| 115 |
+
'/tell': self._tell_command,
|
| 116 |
+
'/whisper': self._tell_command, # Alias
|
| 117 |
+
'/who': self._who_command,
|
| 118 |
+
'/help': self._help_command,
|
| 119 |
+
'/commands': self._help_command, # Alias
|
| 120 |
+
'/shout': self._shout_command,
|
| 121 |
+
'/emote': self._emote_command,
|
| 122 |
+
'/me': self._emote_command, # Alias
|
| 123 |
+
'/time': self._time_command,
|
| 124 |
+
'/channels': self._channels_command
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if command in command_handlers:
|
| 128 |
+
try:
|
| 129 |
+
return command_handlers[command](player_id, args)
|
| 130 |
+
except Exception as e:
|
| 131 |
+
return {
|
| 132 |
+
"success": False,
|
| 133 |
+
"message": f"Error executing command: {str(e)}",
|
| 134 |
+
"target": player_id
|
| 135 |
+
}
|
| 136 |
+
else:
|
| 137 |
+
return {
|
| 138 |
+
"success": False,
|
| 139 |
+
"message": f"Unknown command: {command}. Type /help for available commands.",
|
| 140 |
+
"target": player_id
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
def _broadcast_message(self, player_id: str, message: str) -> Dict[str, Any]:
|
| 144 |
+
"""Broadcast a message to all players"""
|
| 145 |
+
player_name = self._get_player_name(player_id)
|
| 146 |
+
formatted_message = f"{player_name}: {message}"
|
| 147 |
+
|
| 148 |
+
# Add to history
|
| 149 |
+
self._add_to_history(formatted_message)
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"success": True,
|
| 153 |
+
"message": formatted_message,
|
| 154 |
+
"type": "broadcast",
|
| 155 |
+
"sender": player_id
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
def _add_to_history(self, message: str):
|
| 159 |
+
"""Add message to chat history"""
|
| 160 |
+
import time
|
| 161 |
+
self.chat_history.append({
|
| 162 |
+
'timestamp': time.time(),
|
| 163 |
+
'message': message
|
| 164 |
+
})
|
| 165 |
+
|
| 166 |
+
# Keep only the last max_history messages
|
| 167 |
+
if len(self.chat_history) > self.max_history:
|
| 168 |
+
self.chat_history.pop(0)
|
| 169 |
+
|
| 170 |
+
def _marketplace_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 171 |
+
"""Handle marketplace command"""
|
| 172 |
+
marketplace_text = """
|
| 173 |
+
🏪 **MARKETPLACE** 🏪
|
| 174 |
+
═══════════════════════
|
| 175 |
+
|
| 176 |
+
📦 **AVAILABLE ITEMS:**
|
| 177 |
+
• Iron Sword - 150 gold (Seller: Trader_Bob)
|
| 178 |
+
• Health Potion - 25 gold (Seller: Alchemist_Ann)
|
| 179 |
+
• Magic Ring - 300 gold (Seller: Wizard_Will)
|
| 180 |
+
• Steel Shield - 200 gold (Seller: Smith_Sam)
|
| 181 |
+
|
| 182 |
+
💰 **RECENT TRADES:**
|
| 183 |
+
• Leather Boots sold for 75 gold
|
| 184 |
+
• Fire Staff sold for 450 gold
|
| 185 |
+
• Healing Herbs sold for 15 gold
|
| 186 |
+
|
| 187 |
+
📝 Use `/trade create <item> <price>` to list an item
|
| 188 |
+
🤝 Use `/trade accept <seller>` to buy an item
|
| 189 |
+
🔨 Use `/auction create <item> <starting_price>` for auctions
|
| 190 |
+
|
| 191 |
+
Type `/help` for more trading commands!
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
return {
|
| 195 |
+
"success": True,
|
| 196 |
+
"message": marketplace_text.strip(),
|
| 197 |
+
"target": player_id,
|
| 198 |
+
"type": "info"
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
def _trade_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 202 |
+
"""Handle trade command"""
|
| 203 |
+
if not args:
|
| 204 |
+
return {
|
| 205 |
+
"success": False,
|
| 206 |
+
"message": "Usage: /trade <action> [parameters]\nActions: create, accept, list, cancel",
|
| 207 |
+
"target": player_id
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
parts = args.split(' ', 1)
|
| 211 |
+
action = parts[0].lower()
|
| 212 |
+
|
| 213 |
+
if action == 'create':
|
| 214 |
+
if len(parts) < 2:
|
| 215 |
+
return {
|
| 216 |
+
"success": False,
|
| 217 |
+
"message": "Usage: /trade create <item> <price>",
|
| 218 |
+
"target": player_id
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
# Parse item and price
|
| 222 |
+
create_args = parts[1].split(' ')
|
| 223 |
+
if len(create_args) < 2:
|
| 224 |
+
return {
|
| 225 |
+
"success": False,
|
| 226 |
+
"message": "Usage: /trade create <item> <price>",
|
| 227 |
+
"target": player_id
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
price = create_args[-1]
|
| 231 |
+
item = ' '.join(create_args[:-1])
|
| 232 |
+
|
| 233 |
+
return {
|
| 234 |
+
"success": True,
|
| 235 |
+
"message": f"✅ Trade listing created: {item} for {price} gold",
|
| 236 |
+
"target": player_id,
|
| 237 |
+
"type": "trade_create",
|
| 238 |
+
"data": {"item": item, "price": price, "seller": player_id}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
elif action == 'accept':
|
| 242 |
+
if len(parts) < 2:
|
| 243 |
+
return {
|
| 244 |
+
"success": False,
|
| 245 |
+
"message": "Usage: /trade accept <seller_name>",
|
| 246 |
+
"target": player_id
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
seller_name = parts[1]
|
| 250 |
+
return {
|
| 251 |
+
"success": True,
|
| 252 |
+
"message": f"🤝 Attempting to trade with {seller_name}...",
|
| 253 |
+
"target": player_id,
|
| 254 |
+
"type": "trade_accept"
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
elif action == 'list':
|
| 258 |
+
return {
|
| 259 |
+
"success": True,
|
| 260 |
+
"message": "📋 Your active trade listings:\n• No active listings",
|
| 261 |
+
"target": player_id
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
else:
|
| 265 |
+
return {
|
| 266 |
+
"success": False,
|
| 267 |
+
"message": "Unknown trade action. Use 'create', 'accept', 'list', or 'cancel'.",
|
| 268 |
+
"target": player_id
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
def _auction_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 272 |
+
"""Handle auction command"""
|
| 273 |
+
if not args:
|
| 274 |
+
return {
|
| 275 |
+
"success": False,
|
| 276 |
+
"message": "Usage: /auction <action> [parameters]\nActions: create, bid, list",
|
| 277 |
+
"target": player_id
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
parts = args.split(' ', 1)
|
| 281 |
+
action = parts[0].lower()
|
| 282 |
+
|
| 283 |
+
if action == 'create':
|
| 284 |
+
if len(parts) < 2:
|
| 285 |
+
return {
|
| 286 |
+
"success": False,
|
| 287 |
+
"message": "Usage: /auction create <item> <starting_price>",
|
| 288 |
+
"target": player_id
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
# Parse item and starting price
|
| 292 |
+
create_args = parts[1].split(' ')
|
| 293 |
+
if len(create_args) < 2:
|
| 294 |
+
return {
|
| 295 |
+
"success": False,
|
| 296 |
+
"message": "Usage: /auction create <item> <starting_price>",
|
| 297 |
+
"target": player_id
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
starting_price = create_args[-1]
|
| 301 |
+
item = ' '.join(create_args[:-1])
|
| 302 |
+
|
| 303 |
+
return {
|
| 304 |
+
"success": True,
|
| 305 |
+
"message": f"🔨 Auction created: {item} starting at {starting_price} gold",
|
| 306 |
+
"target": player_id,
|
| 307 |
+
"type": "auction_create"
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
else:
|
| 311 |
+
return {
|
| 312 |
+
"success": False,
|
| 313 |
+
"message": "Unknown auction action. Use 'create' to start an auction.",
|
| 314 |
+
"target": player_id
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
def _tell_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 318 |
+
"""Send a private message to another player"""
|
| 319 |
+
if not args:
|
| 320 |
+
return {
|
| 321 |
+
"success": False,
|
| 322 |
+
"message": "Usage: /tell <player> <message>",
|
| 323 |
+
"target": player_id
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
parts = args.split(' ', 1)
|
| 327 |
+
if len(parts) < 2:
|
| 328 |
+
return {
|
| 329 |
+
"success": False,
|
| 330 |
+
"message": "Usage: /tell <player> <message>",
|
| 331 |
+
"target": player_id
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
target_name, message = parts
|
| 335 |
+
sender_name = self._get_player_name(player_id)
|
| 336 |
+
|
| 337 |
+
return {
|
| 338 |
+
"success": True,
|
| 339 |
+
"message": f"[TELL to {target_name}]: {message}",
|
| 340 |
+
"target": player_id,
|
| 341 |
+
"type": "tell",
|
| 342 |
+
"data": {
|
| 343 |
+
"target_name": target_name,
|
| 344 |
+
"sender_name": sender_name,
|
| 345 |
+
"message": message
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
def _who_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 350 |
+
"""Show list of online players"""
|
| 351 |
+
# In a real implementation, this would get the actual list of connected players
|
| 352 |
+
return {
|
| 353 |
+
"success": True,
|
| 354 |
+
"message": "👥 Online players (3): Player1, Player2, Player3",
|
| 355 |
+
"target": player_id,
|
| 356 |
+
"type": "info"
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
def _help_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 360 |
+
"""Show available commands"""
|
| 361 |
+
commands = self.get_available_commands()
|
| 362 |
+
help_text = "📋 **AVAILABLE COMMANDS:**\n"
|
| 363 |
+
help_text += "═══════════════════════\n\n"
|
| 364 |
+
|
| 365 |
+
for cmd in commands:
|
| 366 |
+
help_text += f"• {cmd['command']} - {cmd['description']}\n"
|
| 367 |
+
|
| 368 |
+
return {
|
| 369 |
+
"success": True,
|
| 370 |
+
"message": help_text,
|
| 371 |
+
"target": player_id,
|
| 372 |
+
"type": "info"
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
def _shout_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 376 |
+
"""Send a message to all players with emphasis"""
|
| 377 |
+
if not args:
|
| 378 |
+
return {
|
| 379 |
+
"success": False,
|
| 380 |
+
"message": "Usage: /shout <message>",
|
| 381 |
+
"target": player_id
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
player_name = self._get_player_name(player_id)
|
| 385 |
+
formatted_message = f"📢 {player_name} shouts: {args}"
|
| 386 |
+
|
| 387 |
+
self._add_to_history(formatted_message)
|
| 388 |
+
|
| 389 |
+
return {
|
| 390 |
+
"success": True,
|
| 391 |
+
"message": formatted_message,
|
| 392 |
+
"type": "shout",
|
| 393 |
+
"sender": player_id
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
def _emote_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 397 |
+
"""Send an emote message"""
|
| 398 |
+
if not args:
|
| 399 |
+
return {
|
| 400 |
+
"success": False,
|
| 401 |
+
"message": "Usage: /emote <action> or /me <action>",
|
| 402 |
+
"target": player_id
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
player_name = self._get_player_name(player_id)
|
| 406 |
+
formatted_message = f"✨ {player_name} {args}"
|
| 407 |
+
|
| 408 |
+
self._add_to_history(formatted_message)
|
| 409 |
+
|
| 410 |
+
return {
|
| 411 |
+
"success": True,
|
| 412 |
+
"message": formatted_message,
|
| 413 |
+
"type": "emote",
|
| 414 |
+
"sender": player_id
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
def _time_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 418 |
+
"""Show current game time"""
|
| 419 |
+
import datetime
|
| 420 |
+
current_time = datetime.datetime.now().strftime("%H:%M:%S")
|
| 421 |
+
|
| 422 |
+
return {
|
| 423 |
+
"success": True,
|
| 424 |
+
"message": f"🕐 Current time: {current_time}",
|
| 425 |
+
"target": player_id,
|
| 426 |
+
"type": "info"
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
def _channels_command(self, player_id: str, args: str) -> Dict[str, Any]:
|
| 430 |
+
"""Show available chat channels"""
|
| 431 |
+
channels = self.get_chat_channels()
|
| 432 |
+
channels_text = "📺 **CHAT CHANNELS:**\n"
|
| 433 |
+
channels_text += "═══════════════════\n\n"
|
| 434 |
+
|
| 435 |
+
for channel_id, channel_info in channels.items():
|
| 436 |
+
channels_text += f"• #{channel_id} - {channel_info['description']}\n"
|
| 437 |
+
|
| 438 |
+
return {
|
| 439 |
+
"success": True,
|
| 440 |
+
"message": channels_text,
|
| 441 |
+
"target": player_id,
|
| 442 |
+
"type": "info"
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
# Helper methods
|
| 446 |
+
def _get_player_name(self, player_id: str) -> str:
|
| 447 |
+
"""Get player name from player ID"""
|
| 448 |
+
# In a real implementation, this would look up the actual player name
|
| 449 |
+
return f"Player{player_id[-3:]}" if len(player_id) > 3 else f"Player{player_id}"
|
plugins/enhanced_chat_plugin.py.backup
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from src.interfaces.plugin_interfaces import IChatPlugin, PluginMetadata, PluginType
|
| 3 |
+
from typing import Dict, List, Any
|
| 4 |
+
|
| 5 |
+
class EnhancedChatPlugin(IChatPlugin):
|
| 6 |
+
def __init__(self): self._metadata = PluginMetadata(
|
| 7 |
+
id="enhanced_chat",
|
| 8 |
+
name="Enhanced Chat System",
|
| 9 |
+
version="1.2.0",
|
| 10 |
+
author="MMORPG Dev Team",
|
| 11 |
+
description="Adds emotes, chat channels, and advanced chat commands",
|
| 12 |
+
plugin_type=PluginType.SERVICE,
|
| 13 |
+
dependencies=[],
|
| 14 |
+
config={
|
| 15 |
+
"enable_emotes": True,
|
| 16 |
+
"enable_channels": True,
|
| 17 |
+
"max_message_length": 500,
|
| 18 |
+
"chat_cooldown": 1
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
self._enabled = False
|
| 22 |
+
self.commands = {
|
| 23 |
+
'/tell': self.tell_command,
|
| 24 |
+
'/whisper': self.whisper_command,
|
| 25 |
+
'/shout': self.shout_command,
|
| 26 |
+
'/emote': self.emote_command,
|
| 27 |
+
'/me': self.emote_command, # Alias for /emote
|
| 28 |
+
'/who': self.who_command,
|
| 29 |
+
'/time': self.time_command,
|
| 30 |
+
'/help': self.help_command,
|
| 31 |
+
'/commands': self.help_command, # Alias for /help
|
| 32 |
+
'/clear': self.clear_command,
|
| 33 |
+
'/ignore': self.ignore_command,
|
| 34 |
+
'/unignore': self.unignore_command,
|
| 35 |
+
'/history': self.history_command,
|
| 36 |
+
'/channels': self.channels_command,
|
| 37 |
+
'/join': self.join_channel_command,
|
| 38 |
+
'/leave': self.leave_channel_command,
|
| 39 |
+
'/channel': self.channel_command,
|
| 40 |
+
'/c': self.channel_command, # Alias for /channel
|
| 41 |
+
'/mail': self.mail_command,
|
| 42 |
+
'/read': self.read_mail_command,
|
| 43 |
+
'/mailbox': self.mailbox_command,
|
| 44 |
+
'/marketplace': self.marketplace_command,
|
| 45 |
+
'/trade': self.trade_command,
|
| 46 |
+
'/auction': self.auction_command,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Chat history storage
|
| 50 |
+
self.chat_history = []
|
| 51 |
+
self.max_history = 100
|
| 52 |
+
|
| 53 |
+
# Player ignore lists
|
| 54 |
+
self.ignore_lists = {}
|
| 55 |
+
|
| 56 |
+
# Custom channels
|
| 57 |
+
self.channels = {
|
| 58 |
+
'global': {'description': 'Global chat channel', 'members': set()},
|
| 59 |
+
'trade': {'description': 'Trading channel', 'members': set()},
|
| 60 |
+
'help': {'description': 'Help and questions channel', 'members': set()}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# Player channel memberships
|
| 64 |
+
self.player_channels = {}
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def metadata(self) -> PluginMetadata:
|
| 68 |
+
return self._metadata
|
| 69 |
+
|
| 70 |
+
def initialize(self, context: Dict[str, Any]) -> bool:
|
| 71 |
+
"""Initialize the enhanced chat plugin."""
|
| 72 |
+
try:
|
| 73 |
+
self._config = self._metadata.config
|
| 74 |
+
self._enabled = True
|
| 75 |
+
print(f"💬 Enhanced Chat Plugin initialized")
|
| 76 |
+
return True
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"💬 Failed to initialize Enhanced Chat Plugin: {e}")
|
| 79 |
+
return False
|
| 80 |
+
|
| 81 |
+
def shutdown(self) -> bool:
|
| 82 |
+
"""Shutdown the plugin and cleanup resources."""
|
| 83 |
+
self._enabled = False
|
| 84 |
+
print("💬 Enhanced Chat Plugin shut down")
|
| 85 |
+
return True
|
| 86 |
+
|
| 87 |
+
def get_status(self) -> Dict[str, Any]:
|
| 88 |
+
"""Get current plugin status."""
|
| 89 |
+
return {
|
| 90 |
+
"status": "active" if self._enabled else "inactive",
|
| 91 |
+
"message": "Enhanced chat system with marketplace commands"
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
def process_message(self, player_id: str, message: str, channel: str = "global") -> Dict[str, Any]:
|
| 95 |
+
"""Process and enhance chat messages."""
|
| 96 |
+
if message.startswith('/'):
|
| 97 |
+
return self.process_command(player_id, message)
|
| 98 |
+
else:
|
| 99 |
+
return self.broadcast_message(player_id, message)
|
| 100 |
+
|
| 101 |
+
def get_available_commands(self) -> List[Dict[str, str]]:
|
| 102 |
+
"""Get list of available chat commands."""
|
| 103 |
+
return [
|
| 104 |
+
{"command": "/marketplace", "description": "View marketplace listings"},
|
| 105 |
+
{"command": "/trade create <item> <price>", "description": "Create trade listing"},
|
| 106 |
+
{"command": "/trade accept <id>", "description": "Accept trade offer"},
|
| 107 |
+
{"command": "/auction create <item> <start_price>", "description": "Create auction"},
|
| 108 |
+
{"command": "/tell <player> <message>", "description": "Send private message"},
|
| 109 |
+
{"command": "/who", "description": "List online players"},
|
| 110 |
+
{"command": "/help", "description": "Show available commands"}
|
| 111 |
+
]
|
| 112 |
+
|
| 113 |
+
def get_chat_channels(self) -> Dict[str, Dict[str, str]]:
|
| 114 |
+
"""Get available chat channels."""
|
| 115 |
+
return {
|
| 116 |
+
"global": {"name": "Global", "description": "Main chat channel"},
|
| 117 |
+
"trade": {"name": "Trade", "description": "Trading discussions"},
|
| 118 |
+
"help": {"name": "Help", "description": "Help and questions"}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
async def on_player_message(self, player_id, message):
|
| 122 |
+
"""Handle player chat messages"""
|
| 123 |
+
if message.startswith('/'):
|
| 124 |
+
await self.process_command(player_id, message)
|
| 125 |
+
else:
|
| 126 |
+
await self.broadcast_message(player_id, message)
|
| 127 |
+
|
| 128 |
+
async def process_command(self, player_id, message):
|
| 129 |
+
"""Process chat commands"""
|
| 130 |
+
parts = message.split(' ', 1)
|
| 131 |
+
command = parts[0].lower()
|
| 132 |
+
args = parts[1] if len(parts) > 1 else ""
|
| 133 |
+
|
| 134 |
+
if command in self.commands:
|
| 135 |
+
try:
|
| 136 |
+
await self.commands[command](player_id, args)
|
| 137 |
+
except Exception as e:
|
| 138 |
+
await self.send_to_player(player_id, f"Error executing command: {str(e)}")
|
| 139 |
+
else:
|
| 140 |
+
await self.send_to_player(player_id, f"Unknown command: {command}. Type /help for available commands.")
|
| 141 |
+
|
| 142 |
+
async def broadcast_message(self, player_id, message):
|
| 143 |
+
"""Broadcast a message to all players"""
|
| 144 |
+
player_name = await self.get_player_name(player_id)
|
| 145 |
+
formatted_message = f"{player_name}: {message}"
|
| 146 |
+
|
| 147 |
+
# Add to history
|
| 148 |
+
self.add_to_history(formatted_message)
|
| 149 |
+
|
| 150 |
+
# Send to all connected players
|
| 151 |
+
for pid in self.game_manager.get_connected_players():
|
| 152 |
+
if pid not in self.ignore_lists.get(player_id, set()):
|
| 153 |
+
await self.send_to_player(pid, formatted_message)
|
| 154 |
+
|
| 155 |
+
def add_to_history(self, message):
|
| 156 |
+
"""Add message to chat history"""
|
| 157 |
+
self.chat_history.append({
|
| 158 |
+
'timestamp': asyncio.get_event_loop().time(),
|
| 159 |
+
'message': message
|
| 160 |
+
})
|
| 161 |
+
|
| 162 |
+
# Keep only the last max_history messages
|
| 163 |
+
if len(self.chat_history) > self.max_history:
|
| 164 |
+
self.chat_history.pop(0)
|
| 165 |
+
|
| 166 |
+
async def tell_command(self, player_id, args):
|
| 167 |
+
"""Send a private message to another player"""
|
| 168 |
+
if not args:
|
| 169 |
+
await self.send_to_player(player_id, "Usage: /tell <player> <message>")
|
| 170 |
+
return
|
| 171 |
+
|
| 172 |
+
parts = args.split(' ', 1)
|
| 173 |
+
if len(parts) < 2:
|
| 174 |
+
await self.send_to_player(player_id, "Usage: /tell <player> <message>")
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
target_name, message = parts
|
| 178 |
+
target_id = await self.find_player_by_name(target_name)
|
| 179 |
+
|
| 180 |
+
if not target_id:
|
| 181 |
+
await self.send_to_player(player_id, f"Player '{target_name}' not found.")
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
if target_id in self.ignore_lists.get(player_id, set()):
|
| 185 |
+
await self.send_to_player(player_id, f"You are ignoring {target_name}.")
|
| 186 |
+
return
|
| 187 |
+
|
| 188 |
+
sender_name = await self.get_player_name(player_id)
|
| 189 |
+
await self.send_to_player(target_id, f"[TELL from {sender_name}]: {message}")
|
| 190 |
+
await self.send_to_player(player_id, f"[TELL to {target_name}]: {message}")
|
| 191 |
+
|
| 192 |
+
async def whisper_command(self, player_id, args):
|
| 193 |
+
"""Alias for tell command"""
|
| 194 |
+
await self.tell_command(player_id, args)
|
| 195 |
+
|
| 196 |
+
async def shout_command(self, player_id, args):
|
| 197 |
+
"""Send a message to all players in a wider area"""
|
| 198 |
+
if not args:
|
| 199 |
+
await self.send_to_player(player_id, "Usage: /shout <message>")
|
| 200 |
+
return
|
| 201 |
+
|
| 202 |
+
player_name = await self.get_player_name(player_id)
|
| 203 |
+
formatted_message = f"{player_name} shouts: {args}"
|
| 204 |
+
|
| 205 |
+
# Add to history
|
| 206 |
+
self.add_to_history(formatted_message)
|
| 207 |
+
|
| 208 |
+
# Send to all players (could be modified to only nearby players)
|
| 209 |
+
for pid in self.game_manager.get_connected_players():
|
| 210 |
+
if pid not in self.ignore_lists.get(player_id, set()):
|
| 211 |
+
await self.send_to_player(pid, formatted_message)
|
| 212 |
+
|
| 213 |
+
async def emote_command(self, player_id, args):
|
| 214 |
+
"""Send an emote message"""
|
| 215 |
+
if not args:
|
| 216 |
+
await self.send_to_player(player_id, "Usage: /emote <action> or /me <action>")
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
player_name = await self.get_player_name(player_id)
|
| 220 |
+
formatted_message = f"* {player_name} {args}"
|
| 221 |
+
|
| 222 |
+
# Add to history
|
| 223 |
+
self.add_to_history(formatted_message)
|
| 224 |
+
|
| 225 |
+
# Send to all players
|
| 226 |
+
for pid in self.game_manager.get_connected_players():
|
| 227 |
+
if pid not in self.ignore_lists.get(player_id, set()):
|
| 228 |
+
await self.send_to_player(pid, formatted_message)
|
| 229 |
+
|
| 230 |
+
async def who_command(self, player_id, args):
|
| 231 |
+
"""Show list of online players"""
|
| 232 |
+
connected_players = self.game_manager.get_connected_players()
|
| 233 |
+
player_names = []
|
| 234 |
+
|
| 235 |
+
for pid in connected_players:
|
| 236 |
+
name = await self.get_player_name(pid)
|
| 237 |
+
player_names.append(name)
|
| 238 |
+
|
| 239 |
+
if player_names:
|
| 240 |
+
message = f"Online players ({len(player_names)}): {', '.join(player_names)}"
|
| 241 |
+
else:
|
| 242 |
+
message = "No players currently online."
|
| 243 |
+
|
| 244 |
+
await self.send_to_player(player_id, message)
|
| 245 |
+
|
| 246 |
+
async def time_command(self, player_id, args):
|
| 247 |
+
"""Show current game time"""
|
| 248 |
+
import datetime
|
| 249 |
+
current_time = datetime.datetime.now().strftime("%H:%M:%S")
|
| 250 |
+
await self.send_to_player(player_id, f"Current time: {current_time}")
|
| 251 |
+
|
| 252 |
+
async def help_command(self, player_id, args):
|
| 253 |
+
"""Show available commands"""
|
| 254 |
+
commands = [
|
| 255 |
+
"/tell <player> <msg> - Send private message",
|
| 256 |
+
"/whisper <player> <msg> - Same as /tell",
|
| 257 |
+
"/shout <message> - Shout to all players",
|
| 258 |
+
"/emote <action> - Perform an action",
|
| 259 |
+
"/me <action> - Same as /emote",
|
| 260 |
+
"/who - Show online players",
|
| 261 |
+
"/time - Show current time",
|
| 262 |
+
"/help - Show this help",
|
| 263 |
+
"/clear - Clear your chat",
|
| 264 |
+
"/ignore <player> - Ignore a player",
|
| 265 |
+
"/unignore <player> - Stop ignoring a player",
|
| 266 |
+
"/history - Show recent chat history",
|
| 267 |
+
"/channels - Show available channels",
|
| 268 |
+
"/join <channel> - Join a channel",
|
| 269 |
+
"/leave <channel> - Leave a channel",
|
| 270 |
+
"/channel <channel> <msg> - Send message to channel",
|
| 271 |
+
"/c <channel> <msg> - Same as /channel",
|
| 272 |
+
"/mail <player> <msg> - Send mail to player",
|
| 273 |
+
"/read - Read your mail",
|
| 274 |
+
"/mailbox - Check mailbox status",
|
| 275 |
+
"/marketplace - View marketplace listings",
|
| 276 |
+
"/trade create <item> <price> - Create trade offer",
|
| 277 |
+
"/trade accept <id> - Accept trade offer",
|
| 278 |
+
"/auction create <item> <start_price> - Create auction"
|
| 279 |
+
]
|
| 280 |
+
|
| 281 |
+
await self.send_to_player(player_id, "Available commands:")
|
| 282 |
+
for cmd in commands:
|
| 283 |
+
await self.send_to_player(player_id, f" {cmd}")
|
| 284 |
+
|
| 285 |
+
async def clear_command(self, player_id, args):
|
| 286 |
+
"""Clear player's chat"""
|
| 287 |
+
# This would typically clear the client-side chat
|
| 288 |
+
await self.send_to_player(player_id, "\n" * 20)
|
| 289 |
+
await self.send_to_player(player_id, "Chat cleared.")
|
| 290 |
+
|
| 291 |
+
async def ignore_command(self, player_id, args):
|
| 292 |
+
"""Add a player to ignore list"""
|
| 293 |
+
if not args:
|
| 294 |
+
await self.send_to_player(player_id, "Usage: /ignore <player>")
|
| 295 |
+
return
|
| 296 |
+
|
| 297 |
+
target_id = await self.find_player_by_name(args.strip())
|
| 298 |
+
if not target_id:
|
| 299 |
+
await self.send_to_player(player_id, f"Player '{args}' not found.")
|
| 300 |
+
return
|
| 301 |
+
|
| 302 |
+
if target_id == player_id:
|
| 303 |
+
await self.send_to_player(player_id, "You cannot ignore yourself.")
|
| 304 |
+
return
|
| 305 |
+
|
| 306 |
+
if player_id not in self.ignore_lists:
|
| 307 |
+
self.ignore_lists[player_id] = set()
|
| 308 |
+
|
| 309 |
+
self.ignore_lists[player_id].add(target_id)
|
| 310 |
+
await self.send_to_player(player_id, f"You are now ignoring {args}.")
|
| 311 |
+
|
| 312 |
+
async def unignore_command(self, player_id, args):
|
| 313 |
+
"""Remove a player from ignore list"""
|
| 314 |
+
if not args:
|
| 315 |
+
await self.send_to_player(player_id, "Usage: /unignore <player>")
|
| 316 |
+
return
|
| 317 |
+
|
| 318 |
+
target_id = await self.find_player_by_name(args.strip())
|
| 319 |
+
if not target_id:
|
| 320 |
+
await self.send_to_player(player_id, f"Player '{args}' not found.")
|
| 321 |
+
return
|
| 322 |
+
|
| 323 |
+
if player_id in self.ignore_lists and target_id in self.ignore_lists[player_id]:
|
| 324 |
+
self.ignore_lists[player_id].remove(target_id)
|
| 325 |
+
await self.send_to_player(player_id, f"You are no longer ignoring {args}.")
|
| 326 |
+
else:
|
| 327 |
+
await self.send_to_player(player_id, f"You are not ignoring {args}.")
|
| 328 |
+
|
| 329 |
+
async def history_command(self, player_id, args):
|
| 330 |
+
"""Show recent chat history"""
|
| 331 |
+
if not self.chat_history:
|
| 332 |
+
await self.send_to_player(player_id, "No chat history available.")
|
| 333 |
+
return
|
| 334 |
+
|
| 335 |
+
await self.send_to_player(player_id, "Recent chat history:")
|
| 336 |
+
for entry in self.chat_history[-10:]: # Show last 10 messages
|
| 337 |
+
await self.send_to_player(player_id, entry['message'])
|
| 338 |
+
|
| 339 |
+
async def channels_command(self, player_id, args):
|
| 340 |
+
"""Show available channels"""
|
| 341 |
+
await self.send_to_player(player_id, "Available channels:")
|
| 342 |
+
for channel_name, channel_info in self.channels.items():
|
| 343 |
+
member_count = len(channel_info['members'])
|
| 344 |
+
await self.send_to_player(player_id, f" {channel_name}: {channel_info['description']} ({member_count} members)")
|
| 345 |
+
|
| 346 |
+
async def join_channel_command(self, player_id, args):
|
| 347 |
+
"""Join a chat channel"""
|
| 348 |
+
if not args:
|
| 349 |
+
await self.send_to_player(player_id, "Usage: /join <channel>")
|
| 350 |
+
return
|
| 351 |
+
|
| 352 |
+
channel_name = args.strip().lower()
|
| 353 |
+
if channel_name not in self.channels:
|
| 354 |
+
await self.send_to_player(player_id, f"Channel '{channel_name}' does not exist.")
|
| 355 |
+
return
|
| 356 |
+
|
| 357 |
+
if player_id not in self.player_channels:
|
| 358 |
+
self.player_channels[player_id] = set()
|
| 359 |
+
|
| 360 |
+
self.player_channels[player_id].add(channel_name)
|
| 361 |
+
self.channels[channel_name]['members'].add(player_id)
|
| 362 |
+
|
| 363 |
+
player_name = await self.get_player_name(player_id)
|
| 364 |
+
await self.send_to_player(player_id, f"You have joined the '{channel_name}' channel.")
|
| 365 |
+
|
| 366 |
+
# Notify other channel members
|
| 367 |
+
for member_id in self.channels[channel_name]['members']:
|
| 368 |
+
if member_id != player_id:
|
| 369 |
+
await self.send_to_player(member_id, f"{player_name} has joined the '{channel_name}' channel.")
|
| 370 |
+
|
| 371 |
+
async def leave_channel_command(self, player_id, args):
|
| 372 |
+
"""Leave a chat channel"""
|
| 373 |
+
if not args:
|
| 374 |
+
await self.send_to_player(player_id, "Usage: /leave <channel>")
|
| 375 |
+
return
|
| 376 |
+
|
| 377 |
+
channel_name = args.strip().lower()
|
| 378 |
+
if channel_name not in self.channels:
|
| 379 |
+
await self.send_to_player(player_id, f"Channel '{channel_name}' does not exist.")
|
| 380 |
+
return
|
| 381 |
+
|
| 382 |
+
if player_id not in self.player_channels or channel_name not in self.player_channels[player_id]:
|
| 383 |
+
await self.send_to_player(player_id, f"You are not in the '{channel_name}' channel.")
|
| 384 |
+
return
|
| 385 |
+
|
| 386 |
+
self.player_channels[player_id].remove(channel_name)
|
| 387 |
+
self.channels[channel_name]['members'].discard(player_id)
|
| 388 |
+
|
| 389 |
+
player_name = await self.get_player_name(player_id)
|
| 390 |
+
await self.send_to_player(player_id, f"You have left the '{channel_name}' channel.")
|
| 391 |
+
|
| 392 |
+
# Notify other channel members
|
| 393 |
+
for member_id in self.channels[channel_name]['members']:
|
| 394 |
+
await self.send_to_player(member_id, f"{player_name} has left the '{channel_name}' channel.")
|
| 395 |
+
|
| 396 |
+
async def channel_command(self, player_id, args):
|
| 397 |
+
"""Send a message to a specific channel"""
|
| 398 |
+
if not args:
|
| 399 |
+
await self.send_to_player(player_id, "Usage: /channel <channel> <message>")
|
| 400 |
+
return
|
| 401 |
+
|
| 402 |
+
parts = args.split(' ', 1)
|
| 403 |
+
if len(parts) < 2:
|
| 404 |
+
await self.send_to_player(player_id, "Usage: /channel <channel> <message>")
|
| 405 |
+
return
|
| 406 |
+
|
| 407 |
+
channel_name, message = parts
|
| 408 |
+
channel_name = channel_name.lower()
|
| 409 |
+
|
| 410 |
+
if channel_name not in self.channels:
|
| 411 |
+
await self.send_to_player(player_id, f"Channel '{channel_name}' does not exist.")
|
| 412 |
+
return
|
| 413 |
+
|
| 414 |
+
if player_id not in self.player_channels or channel_name not in self.player_channels[player_id]:
|
| 415 |
+
await self.send_to_player(player_id, f"You are not in the '{channel_name}' channel. Use /join {channel_name} first.")
|
| 416 |
+
return
|
| 417 |
+
|
| 418 |
+
player_name = await self.get_player_name(player_id)
|
| 419 |
+
formatted_message = f"[{channel_name.upper()}] {player_name}: {message}"
|
| 420 |
+
|
| 421 |
+
# Send to all channel members
|
| 422 |
+
for member_id in self.channels[channel_name]['members']:
|
| 423 |
+
if member_id not in self.ignore_lists.get(player_id, set()):
|
| 424 |
+
await self.send_to_player(member_id, formatted_message)
|
| 425 |
+
|
| 426 |
+
async def mail_command(self, player_id, args):
|
| 427 |
+
"""Send mail to a player using the read2burn system"""
|
| 428 |
+
if not args:
|
| 429 |
+
await self.send_to_player(player_id, "Usage: /mail <player> <message>")
|
| 430 |
+
return
|
| 431 |
+
|
| 432 |
+
parts = args.split(' ', 1)
|
| 433 |
+
if len(parts) < 2:
|
| 434 |
+
await self.send_to_player(player_id, "Usage: /mail <player> <message>")
|
| 435 |
+
return
|
| 436 |
+
|
| 437 |
+
target_name, message = parts
|
| 438 |
+
target_id = await self.find_player_by_name(target_name)
|
| 439 |
+
|
| 440 |
+
if not target_id:
|
| 441 |
+
await self.send_to_player(player_id, f"Player '{target_name}' not found.")
|
| 442 |
+
return
|
| 443 |
+
|
| 444 |
+
# Use the read2burn addon to send mail
|
| 445 |
+
try:
|
| 446 |
+
read2burn_addon = self.game_manager.get_addon('read2burn')
|
| 447 |
+
if read2burn_addon:
|
| 448 |
+
sender_name = await self.get_player_name(player_id)
|
| 449 |
+
await read2burn_addon.send_message(target_id, f"Mail from {sender_name}: {message}")
|
| 450 |
+
await self.send_to_player(player_id, f"Mail sent to {target_name}.")
|
| 451 |
+
else:
|
| 452 |
+
await self.send_to_player(player_id, "Mail system is not available.")
|
| 453 |
+
except Exception as e:
|
| 454 |
+
await self.send_to_player(player_id, f"Failed to send mail: {str(e)}")
|
| 455 |
+
|
| 456 |
+
async def read_mail_command(self, player_id, args):
|
| 457 |
+
"""Read mail using the read2burn system"""
|
| 458 |
+
try:
|
| 459 |
+
read2burn_addon = self.game_manager.get_addon('read2burn')
|
| 460 |
+
if read2burn_addon:
|
| 461 |
+
messages = await read2burn_addon.get_messages(player_id)
|
| 462 |
+
if messages:
|
| 463 |
+
await self.send_to_player(player_id, f"You have {len(messages)} unread message(s):")
|
| 464 |
+
for i, msg in enumerate(messages):
|
| 465 |
+
await self.send_to_player(player_id, f"{i+1}. {msg}")
|
| 466 |
+
# Messages are automatically deleted after reading (read2burn)
|
| 467 |
+
await read2burn_addon.clear_messages(player_id)
|
| 468 |
+
else:
|
| 469 |
+
await self.send_to_player(player_id, "You have no unread mail.")
|
| 470 |
+
else:
|
| 471 |
+
await self.send_to_player(player_id, "Mail system is not available.")
|
| 472 |
+
except Exception as e:
|
| 473 |
+
await self.send_to_player(player_id, f"Failed to read mail: {str(e)}")
|
| 474 |
+
|
| 475 |
+
async def mailbox_command(self, player_id, args):
|
| 476 |
+
"""Check mailbox status"""
|
| 477 |
+
try:
|
| 478 |
+
read2burn_addon = self.game_manager.get_addon('read2burn')
|
| 479 |
+
if read2burn_addon:
|
| 480 |
+
message_count = await read2burn_addon.get_message_count(player_id)
|
| 481 |
+
await self.send_to_player(player_id, f"You have {message_count} unread message(s) in your mailbox.")
|
| 482 |
+
else:
|
| 483 |
+
await self.send_to_player(player_id, "Mail system is not available.")
|
| 484 |
+
except Exception as e:
|
| 485 |
+
await self.send_to_player(player_id, f"Failed to check mailbox: {str(e)}")
|
| 486 |
+
|
| 487 |
+
async def marketplace_command(self, player_id, args):
|
| 488 |
+
"""Show marketplace listings"""
|
| 489 |
+
# Demo marketplace listings - in a real implementation, this would query the trading system
|
| 490 |
+
demo_listings = [
|
| 491 |
+
"1. Iron Sword - 100 gold (seller: Alice)",
|
| 492 |
+
"2. Magic Potion - 50 gold (seller: Bob)",
|
| 493 |
+
"3. Dragon Scale - 500 gold (seller: Charlie)",
|
| 494 |
+
"4. Enchanted Ring - 250 gold (seller: Diana)"
|
| 495 |
+
]
|
| 496 |
+
|
| 497 |
+
await self.send_to_player(player_id, "=== MARKETPLACE ===")
|
| 498 |
+
await self.send_to_player(player_id, "Current listings:")
|
| 499 |
+
for listing in demo_listings:
|
| 500 |
+
await self.send_to_player(player_id, f" {listing}")
|
| 501 |
+
await self.send_to_player(player_id, "Use '/trade create <item> <price>' to list an item")
|
| 502 |
+
await self.send_to_player(player_id, "Use '/trade accept <id>' to purchase an item")
|
| 503 |
+
|
| 504 |
+
async def trade_command(self, player_id, args):
|
| 505 |
+
"""Handle trading commands"""
|
| 506 |
+
if not args:
|
| 507 |
+
await self.send_to_player(player_id, "Usage: /trade <create|accept> [arguments]")
|
| 508 |
+
return
|
| 509 |
+
|
| 510 |
+
parts = args.split(' ', 1)
|
| 511 |
+
action = parts[0].lower()
|
| 512 |
+
|
| 513 |
+
if action == 'create':
|
| 514 |
+
if len(parts) < 2:
|
| 515 |
+
await self.send_to_player(player_id, "Usage: /trade create <item> <price>")
|
| 516 |
+
return
|
| 517 |
+
|
| 518 |
+
# Parse item and price
|
| 519 |
+
create_args = parts[1].split(' ')
|
| 520 |
+
if len(create_args) < 2:
|
| 521 |
+
await self.send_to_player(player_id, "Usage: /trade create <item> <price>")
|
| 522 |
+
return
|
| 523 |
+
|
| 524 |
+
price = create_args[-1]
|
| 525 |
+
item = ' '.join(create_args[:-1])
|
| 526 |
+
|
| 527 |
+
await self.send_to_player(player_id, f"Created trade listing: {item} for {price} gold")
|
| 528 |
+
# In a real implementation, this would interact with the trading system plugin
|
| 529 |
+
|
| 530 |
+
elif action == 'accept':
|
| 531 |
+
if len(parts) < 2:
|
| 532 |
+
await self.send_to_player(player_id, "Usage: /trade accept <listing_id>")
|
| 533 |
+
return
|
| 534 |
+
|
| 535 |
+
listing_id = parts[1].strip()
|
| 536 |
+
await self.send_to_player(player_id, f"Attempting to purchase listing {listing_id}...")
|
| 537 |
+
# In a real implementation, this would process the trade
|
| 538 |
+
|
| 539 |
+
else:
|
| 540 |
+
await self.send_to_player(player_id, "Unknown trade action. Use 'create' or 'accept'.")
|
| 541 |
+
|
| 542 |
+
async def auction_command(self, player_id, args):
|
| 543 |
+
"""Handle auction commands"""
|
| 544 |
+
if not args:
|
| 545 |
+
await self.send_to_player(player_id, "Usage: /auction create <item> <starting_price>")
|
| 546 |
+
return
|
| 547 |
+
|
| 548 |
+
parts = args.split(' ', 1)
|
| 549 |
+
action = parts[0].lower()
|
| 550 |
+
|
| 551 |
+
if action == 'create':
|
| 552 |
+
if len(parts) < 2:
|
| 553 |
+
await self.send_to_player(player_id, "Usage: /auction create <item> <starting_price>")
|
| 554 |
+
return
|
| 555 |
+
|
| 556 |
+
# Parse item and starting price
|
| 557 |
+
create_args = parts[1].split(' ')
|
| 558 |
+
if len(create_args) < 2:
|
| 559 |
+
await self.send_to_player(player_id, "Usage: /auction create <item> <starting_price>")
|
| 560 |
+
return
|
| 561 |
+
|
| 562 |
+
starting_price = create_args[-1]
|
| 563 |
+
item = ' '.join(create_args[:-1])
|
| 564 |
+
|
| 565 |
+
await self.send_to_player(player_id, f"Created auction: {item} starting at {starting_price} gold")
|
| 566 |
+
# In a real implementation, this would interact with the auction system
|
| 567 |
+
|
| 568 |
+
else:
|
| 569 |
+
await self.send_to_player(player_id, "Unknown auction action. Use 'create' to start an auction.")
|
| 570 |
+
|
| 571 |
+
# Helper methods
|
| 572 |
+
async def get_player_name(self, player_id):
|
| 573 |
+
"""Get player name from player ID"""
|
| 574 |
+
# This should be implemented to get the actual player name
|
| 575 |
+
return f"Player{player_id}"
|
| 576 |
+
|
| 577 |
+
async def find_player_by_name(self, name):
|
| 578 |
+
"""Find player ID by name"""
|
| 579 |
+
# This should be implemented to find players by name
|
| 580 |
+
# For now, return None if not found
|
| 581 |
+
return None
|
| 582 |
+
|
| 583 |
+
async def send_to_player(self, player_id, message):
|
| 584 |
+
"""Send a message to a specific player"""
|
| 585 |
+
# This should be implemented to send messages to players
|
| 586 |
+
pass
|
plugins/enhanced_chat_plugin_fixed.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Chat Plugin for MMORPG
|
| 3 |
+
Adds advanced chat features like emotes, channels, and chat commands
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from src.interfaces.plugin_interfaces import IChatPlugin, PluginMetadata, PluginType
|
| 7 |
+
from typing import Dict, List, Any, Optional
|
| 8 |
+
import re
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
class EnhancedChatPlugin(IChatPlugin):
|
| 12 |
+
"""Plugin that enhances the chat system with emotes, channels, and commands."""
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self._metadata = PluginMetadata(
|
| 16 |
+
id="enhanced_chat",
|
| 17 |
+
name="Enhanced Chat System",
|
| 18 |
+
version="1.2.0",
|
| 19 |
+
author="MMORPG Dev Team",
|
| 20 |
+
description="Adds emotes, chat channels, and advanced chat commands",
|
| 21 |
+
plugin_type=PluginType.SERVICE,
|
| 22 |
+
dependencies=[],
|
| 23 |
+
config={
|
| 24 |
+
"enable_emotes": True,
|
| 25 |
+
"enable_channels": True,
|
| 26 |
+
"max_message_length": 500,
|
| 27 |
+
"chat_cooldown": 1
|
| 28 |
+
}
|
| 29 |
+
)
|
| 30 |
+
self._enabled = False
|
| 31 |
+
self._chat_channels = {
|
| 32 |
+
"global": {"name": "Global", "color": "#ffffff"},
|
| 33 |
+
"trade": {"name": "Trade", "color": "#ffff00"},
|
| 34 |
+
"help": {"name": "Help", "color": "#00ff00"},
|
| 35 |
+
"guild": {"name": "Guild", "color": "#ff00ff"}
|
| 36 |
+
}
|
| 37 |
+
self._emotes = {
|
| 38 |
+
":smile:": "😊", ":laugh:": "😂", ":wink:": "😉", ":heart:": "❤️",
|
| 39 |
+
":thumbsup:": "👍", ":thumbsdown:": "👎", ":fire:": "🔥", ":star:": "⭐",
|
| 40 |
+
":wave:": "👋", ":clap:": "👏", ":cry:": "😢", ":angry:": "😠",
|
| 41 |
+
":surprised:": "😲", ":cool:": "😎", ":thinking:": "🤔", ":sleeping:": "😴"
|
| 42 |
+
}
|
| 43 |
+
self._player_cooldowns = {}
|
| 44 |
+
|
| 45 |
+
@property
|
| 46 |
+
def metadata(self) -> PluginMetadata:
|
| 47 |
+
return self._metadata
|
| 48 |
+
|
| 49 |
+
def initialize(self, context: Dict[str, Any]) -> bool:
|
| 50 |
+
"""Initialize the enhanced chat plugin."""
|
| 51 |
+
try:
|
| 52 |
+
self._config = self._metadata.config
|
| 53 |
+
self._enabled = True
|
| 54 |
+
print(f"💬 Enhanced Chat Plugin initialized with {len(self._emotes)} emotes and {len(self._chat_channels)} channels")
|
| 55 |
+
return True
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"💬 Failed to initialize Enhanced Chat Plugin: {e}")
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
def cleanup(self) -> None:
|
| 61 |
+
"""Clean up chat plugin resources."""
|
| 62 |
+
self._enabled = False
|
| 63 |
+
self._player_cooldowns.clear()
|
| 64 |
+
print("💬 Enhanced Chat Plugin cleaned up")
|
| 65 |
+
|
| 66 |
+
def is_enabled(self) -> bool:
|
| 67 |
+
return self._enabled
|
| 68 |
+
|
| 69 |
+
def process_message(self, player_id: str, message: str, channel: str = "global") -> Dict[str, Any]:
|
| 70 |
+
"""Process and enhance chat messages."""
|
| 71 |
+
if not self._enabled:
|
| 72 |
+
return {"original": message, "processed": message, "valid": True}
|
| 73 |
+
|
| 74 |
+
result = {
|
| 75 |
+
"original": message,
|
| 76 |
+
"processed": message,
|
| 77 |
+
"valid": True,
|
| 78 |
+
"channel": channel,
|
| 79 |
+
"error": None
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# Check cooldown
|
| 83 |
+
if not self._check_cooldown(player_id):
|
| 84 |
+
result["valid"] = False
|
| 85 |
+
result["error"] = "Please wait before sending another message"
|
| 86 |
+
return result
|
| 87 |
+
|
| 88 |
+
# Check message length
|
| 89 |
+
max_length = self._config.get("max_message_length", 500)
|
| 90 |
+
if len(message) > max_length:
|
| 91 |
+
result["valid"] = False
|
| 92 |
+
result["error"] = f"Message too long (max {max_length} characters)"
|
| 93 |
+
return result
|
| 94 |
+
|
| 95 |
+
# Process chat commands
|
| 96 |
+
if message.startswith("/"):
|
| 97 |
+
return self._process_chat_command(player_id, message, channel)
|
| 98 |
+
|
| 99 |
+
# Process emotes
|
| 100 |
+
if self._config.get("enable_emotes", True):
|
| 101 |
+
result["processed"] = self._process_emotes(message)
|
| 102 |
+
|
| 103 |
+
# Validate and set channel
|
| 104 |
+
if self._config.get("enable_channels", True):
|
| 105 |
+
if channel not in self._chat_channels:
|
| 106 |
+
channel = "global"
|
| 107 |
+
result["channel"] = channel
|
| 108 |
+
result["channel_info"] = self._chat_channels[channel]
|
| 109 |
+
|
| 110 |
+
# Update cooldown
|
| 111 |
+
self._player_cooldowns[player_id] = time.time()
|
| 112 |
+
|
| 113 |
+
return result
|
| 114 |
+
|
| 115 |
+
def get_available_commands(self) -> List[Dict[str, str]]:
|
| 116 |
+
"""Get list of available chat commands."""
|
| 117 |
+
if not self._enabled:
|
| 118 |
+
return []
|
| 119 |
+
|
| 120 |
+
commands = [
|
| 121 |
+
{"command": "/emotes", "description": "Show available emotes"},
|
| 122 |
+
{"command": "/channels", "description": "List available chat channels"},
|
| 123 |
+
{"command": "/join <channel>", "description": "Join a chat channel"},
|
| 124 |
+
{"command": "/who", "description": "List players in current channel"},
|
| 125 |
+
{"command": "/time", "description": "Show current server time"},
|
| 126 |
+
{"command": "/marketplace", "description": "View marketplace listings"},
|
| 127 |
+
{"command": "/trade create <player> <items>", "description": "Create a trade offer"},
|
| 128 |
+
{"command": "/trade accept <trade_id>", "description": "Accept a trade"},
|
| 129 |
+
{"command": "/auction create <item> <price>", "description": "Create an auction"},
|
| 130 |
+
{"command": "/help", "description": "Show this help message"}
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
return commands
|
| 134 |
+
|
| 135 |
+
def get_chat_channels(self) -> Dict[str, Dict[str, str]]:
|
| 136 |
+
"""Get available chat channels."""
|
| 137 |
+
if not self._enabled or not self._config.get("enable_channels", True):
|
| 138 |
+
return {"global": {"name": "Global", "color": "#ffffff"}}
|
| 139 |
+
|
| 140 |
+
return self._chat_channels.copy()
|
| 141 |
+
|
| 142 |
+
def get_emotes_list(self) -> Dict[str, str]:
|
| 143 |
+
"""Get available emotes."""
|
| 144 |
+
if not self._enabled or not self._config.get("enable_emotes", True):
|
| 145 |
+
return {}
|
| 146 |
+
|
| 147 |
+
return self._emotes.copy()
|
| 148 |
+
|
| 149 |
+
def _check_cooldown(self, player_id: str) -> bool:
|
| 150 |
+
"""Check if player is within chat cooldown."""
|
| 151 |
+
cooldown = self._config.get("chat_cooldown", 1)
|
| 152 |
+
if cooldown <= 0:
|
| 153 |
+
return True
|
| 154 |
+
|
| 155 |
+
last_message = self._player_cooldowns.get(player_id, 0)
|
| 156 |
+
return time.time() - last_message >= cooldown
|
| 157 |
+
|
| 158 |
+
def _process_emotes(self, message: str) -> str:
|
| 159 |
+
"""Replace emote codes with emojis."""
|
| 160 |
+
processed = message
|
| 161 |
+
for emote_code, emoji in self._emotes.items():
|
| 162 |
+
processed = processed.replace(emote_code, emoji)
|
| 163 |
+
return processed
|
| 164 |
+
|
| 165 |
+
def _process_chat_command(self, player_id: str, message: str, channel: str) -> Dict[str, Any]:
|
| 166 |
+
"""Process chat commands."""
|
| 167 |
+
parts = message.split()
|
| 168 |
+
command = parts[0].lower()
|
| 169 |
+
|
| 170 |
+
result = {
|
| 171 |
+
"original": message,
|
| 172 |
+
"processed": "",
|
| 173 |
+
"valid": True,
|
| 174 |
+
"channel": channel,
|
| 175 |
+
"is_command": True,
|
| 176 |
+
"command_response": ""
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
if command == "/emotes":
|
| 180 |
+
emote_list = ", ".join(self._emotes.keys())
|
| 181 |
+
result["command_response"] = f"Available emotes: {emote_list}"
|
| 182 |
+
|
| 183 |
+
elif command == "/channels":
|
| 184 |
+
channel_list = ", ".join([f"{k} ({v['name']})" for k, v in self._chat_channels.items()])
|
| 185 |
+
result["command_response"] = f"Available channels: {channel_list}"
|
| 186 |
+
|
| 187 |
+
elif command == "/join":
|
| 188 |
+
if len(parts) > 1:
|
| 189 |
+
target_channel = parts[1].lower()
|
| 190 |
+
if target_channel in self._chat_channels:
|
| 191 |
+
result["channel"] = target_channel
|
| 192 |
+
result["command_response"] = f"Joined channel: {self._chat_channels[target_channel]['name']}"
|
| 193 |
+
else:
|
| 194 |
+
result["command_response"] = f"Channel '{target_channel}' not found"
|
| 195 |
+
else:
|
| 196 |
+
result["command_response"] = "Usage: /join <channel>"
|
| 197 |
+
|
| 198 |
+
elif command == "/who":
|
| 199 |
+
result["command_response"] = "Player list functionality requires game integration"
|
| 200 |
+
|
| 201 |
+
elif command == "/time":
|
| 202 |
+
current_time = time.strftime("%H:%M:%S", time.localtime())
|
| 203 |
+
result["command_response"] = f"Server time: {current_time}"
|
| 204 |
+
|
| 205 |
+
elif command == "/marketplace":
|
| 206 |
+
result["command_response"] = "🏪 **Marketplace**\n\nDemo listings:\n• Iron Sword - 100 gold (Seller: PlayerX)\n• Health Potion - 25 gold (Seller: PlayerY)\n• Magic Staff - 350 gold (Seller: PlayerZ)\n\nUse `/trade create <player> <items>` to start trading!"
|
| 207 |
+
|
| 208 |
+
elif command == "/trade":
|
| 209 |
+
if len(parts) > 1:
|
| 210 |
+
action = parts[1].lower()
|
| 211 |
+
if action == "create":
|
| 212 |
+
result["command_response"] = "🤝 Trade creation system coming soon! For now, use direct player interaction."
|
| 213 |
+
elif action == "accept":
|
| 214 |
+
result["command_response"] = "✅ Trade acceptance system coming soon!"
|
| 215 |
+
else:
|
| 216 |
+
result["command_response"] = "Usage: /trade create <player> <items> or /trade accept <trade_id>"
|
| 217 |
+
else:
|
| 218 |
+
result["command_response"] = "Usage: /trade <create|accept> [parameters]"
|
| 219 |
+
|
| 220 |
+
elif command == "/auction":
|
| 221 |
+
if len(parts) > 1:
|
| 222 |
+
action = parts[1].lower()
|
| 223 |
+
if action == "create":
|
| 224 |
+
result["command_response"] = "🔨 Auction creation system coming soon!"
|
| 225 |
+
else:
|
| 226 |
+
result["command_response"] = "Usage: /auction create <item> <starting_price> <duration>"
|
| 227 |
+
else:
|
| 228 |
+
result["command_response"] = "Usage: /auction create <item> <starting_price> <duration>"
|
| 229 |
+
|
| 230 |
+
elif command == "/help":
|
| 231 |
+
commands = self.get_available_commands()
|
| 232 |
+
help_text = "Available commands:\n" + "\n".join([f"{cmd['command']} - {cmd['description']}" for cmd in commands])
|
| 233 |
+
result["command_response"] = help_text
|
| 234 |
+
|
| 235 |
+
else:
|
| 236 |
+
result["valid"] = False
|
| 237 |
+
result["command_response"] = f"Unknown command: {command}. Type /help for available commands."
|
| 238 |
+
|
| 239 |
+
return result
|
| 240 |
+
|
| 241 |
+
def get_status(self) -> Dict[str, Any]:
|
| 242 |
+
"""Get the current status of the enhanced chat plugin."""
|
| 243 |
+
return {
|
| 244 |
+
"enabled": self._enabled,
|
| 245 |
+
"total_emotes": len(self._emotes),
|
| 246 |
+
"total_channels": len(self._chat_channels),
|
| 247 |
+
"active_cooldowns": len(self._player_cooldowns)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
def shutdown(self) -> bool:
|
| 251 |
+
"""Shutdown the enhanced chat plugin."""
|
| 252 |
+
try:
|
| 253 |
+
self._enabled = False
|
| 254 |
+
self._player_cooldowns.clear()
|
| 255 |
+
print("💬 Enhanced Chat Plugin shutdown")
|
| 256 |
+
return True
|
| 257 |
+
except Exception as e:
|
| 258 |
+
print(f"💬 Error shutting down Enhanced Chat Plugin: {e}")
|
| 259 |
+
return False
|
| 260 |
+
|
| 261 |
+
# Plugin entry point
|
| 262 |
+
def create_plugin():
|
| 263 |
+
"""Factory function to create the plugin instance."""
|
| 264 |
+
return EnhancedChatPlugin()
|
plugins/plugin_registry.conf
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Plugin Registry - Sample Configuration
|
| 3 |
+
This file demonstrates how plugins can be configured and loaded
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# Plugin configuration registry
|
| 7 |
+
PLUGIN_REGISTRY = {
|
| 8 |
+
"weather_plugin": {
|
| 9 |
+
"enabled": True,
|
| 10 |
+
"file": "weather_plugin.py",
|
| 11 |
+
"config": {
|
| 12 |
+
"weather_change_interval": 180, # Change weather every 3 minutes
|
| 13 |
+
"enable_seasons": True,
|
| 14 |
+
"weather_effects_enabled": True
|
| 15 |
+
},
|
| 16 |
+
"auto_load": True,
|
| 17 |
+
"priority": 1
|
| 18 |
+
},
|
| 19 |
+
|
| 20 |
+
"enhanced_chat_plugin": {
|
| 21 |
+
"enabled": True,
|
| 22 |
+
"file": "enhanced_chat_plugin.py",
|
| 23 |
+
"config": {
|
| 24 |
+
"enable_emotes": True,
|
| 25 |
+
"enable_channels": True,
|
| 26 |
+
"max_message_length": 300,
|
| 27 |
+
"chat_cooldown": 2 # 2 second cooldown between messages
|
| 28 |
+
},
|
| 29 |
+
"auto_load": True,
|
| 30 |
+
"priority": 2
|
| 31 |
+
},
|
| 32 |
+
|
| 33 |
+
"trading_system_plugin": {
|
| 34 |
+
"enabled": True,
|
| 35 |
+
"file": "trading_system_plugin.py",
|
| 36 |
+
"config": {
|
| 37 |
+
"enable_direct_trading": True,
|
| 38 |
+
"enable_marketplace": True,
|
| 39 |
+
"trade_tax_rate": 0.03, # 3% tax on marketplace trades
|
| 40 |
+
"max_trade_distance": 150
|
| 41 |
+
},
|
| 42 |
+
"auto_load": True,
|
| 43 |
+
"priority": 3,
|
| 44 |
+
"dependencies": ["enhanced_chat_plugin"]
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Plugin load order (based on dependencies and priority)
|
| 49 |
+
LOAD_ORDER = [
|
| 50 |
+
"enhanced_chat_plugin", # No dependencies
|
| 51 |
+
"weather_plugin", # No dependencies
|
| 52 |
+
"trading_system_plugin" # Depends on enhanced_chat
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
# Default plugin directory
|
| 56 |
+
PLUGIN_DIRECTORY = "plugins"
|
| 57 |
+
|
| 58 |
+
# Plugin file extension
|
| 59 |
+
PLUGIN_EXTENSION = ".py"
|
| 60 |
+
|
| 61 |
+
# Plugin factory function name
|
| 62 |
+
PLUGIN_FACTORY_FUNCTION = "create_plugin"
|
plugins/sample_plugin.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sample plugin demonstrating the plugin system.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from src.interfaces.plugin_interfaces import IPlugin, PluginMetadata, PluginType
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SamplePlugin(IPlugin):
|
| 10 |
+
"""Sample plugin implementation."""
|
| 11 |
+
|
| 12 |
+
@property
|
| 13 |
+
def metadata(self) -> PluginMetadata:
|
| 14 |
+
return PluginMetadata(
|
| 15 |
+
id="sample_plugin",
|
| 16 |
+
name="Sample Plugin",
|
| 17 |
+
version="1.0.0",
|
| 18 |
+
description="A sample plugin demonstrating the plugin system",
|
| 19 |
+
author="MMORPG System",
|
| 20 |
+
plugin_type=PluginType.EVENT,
|
| 21 |
+
dependencies=[],
|
| 22 |
+
config={}
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
def initialize(self, context: Dict[str, Any]) -> bool:
|
| 26 |
+
"""Initialize the plugin."""
|
| 27 |
+
self.context = context
|
| 28 |
+
print("[SamplePlugin] Plugin initialized!")
|
| 29 |
+
return True
|
| 30 |
+
|
| 31 |
+
def shutdown(self) -> bool:
|
| 32 |
+
"""Shutdown the plugin."""
|
| 33 |
+
print("[SamplePlugin] Plugin shutdown!")
|
| 34 |
+
return True
|
| 35 |
+
|
| 36 |
+
def get_status(self) -> Dict[str, Any]:
|
| 37 |
+
"""Get plugin status."""
|
| 38 |
+
return {
|
| 39 |
+
"status": "active",
|
| 40 |
+
"message": "Sample plugin is running normally"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
def on_player_join(self, player_id: str) -> None:
|
| 44 |
+
"""Called when a player joins."""
|
| 45 |
+
print(f"[SamplePlugin] Player {player_id} joined!")
|
| 46 |
+
|
| 47 |
+
def on_player_leave(self, player_id: str) -> None:
|
| 48 |
+
"""Called when a player leaves."""
|
| 49 |
+
print(f"[SamplePlugin] Player {player_id} left!")
|
| 50 |
+
|
| 51 |
+
def on_chat_message(self, sender_id: str, message: str, message_type: str) -> None:
|
| 52 |
+
"""Called when a chat message is sent."""
|
| 53 |
+
if "hello plugin" in message.lower():
|
| 54 |
+
print(f"[SamplePlugin] Detected greeting from {sender_id}")
|
plugins/trading_system_plugin.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Trading System Plugin for MMORPG
|
| 3 |
+
Adds player-to-player trading and marketplace functionality
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from src.interfaces.plugin_interfaces import IEconomyPlugin, PluginMetadata, PluginType
|
| 7 |
+
from typing import Dict, List, Any, Optional
|
| 8 |
+
import time
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
class TradingSystemPlugin(IEconomyPlugin):
|
| 12 |
+
"""Plugin that adds comprehensive trading and marketplace features."""
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self._metadata = PluginMetadata(
|
| 16 |
+
id="trading_system",
|
| 17 |
+
name="Trading System",
|
| 18 |
+
version="1.1.0",
|
| 19 |
+
author="MMORPG Dev Team",
|
| 20 |
+
description="Adds player trading, marketplace, and auction features",
|
| 21 |
+
plugin_type=PluginType.SERVICE,
|
| 22 |
+
dependencies=["enhanced_chat"], # Depends on chat for trade messages
|
| 23 |
+
config={
|
| 24 |
+
"enable_direct_trading": True,
|
| 25 |
+
"enable_marketplace": True,
|
| 26 |
+
"trade_tax_rate": 0.05,
|
| 27 |
+
"max_trade_distance": 100
|
| 28 |
+
}
|
| 29 |
+
)
|
| 30 |
+
self._enabled = False
|
| 31 |
+
self._active_trades = {} # Trade sessions
|
| 32 |
+
self._marketplace_listings = {} # Marketplace items
|
| 33 |
+
self._trade_history = []
|
| 34 |
+
# Sample items for trading
|
| 35 |
+
self._tradeable_items = {
|
| 36 |
+
"health_potion": {"name": "Health Potion", "base_value": 10, "stackable": True},
|
| 37 |
+
"mana_potion": {"name": "Mana Potion", "base_value": 15, "stackable": True},
|
| 38 |
+
"iron_sword": {"name": "Iron Sword", "base_value": 50, "stackable": False},
|
| 39 |
+
"wooden_shield": {"name": "Wooden Shield", "base_value": 30, "stackable": False},
|
| 40 |
+
"magic_scroll": {"name": "Magic Scroll", "base_value": 25, "stackable": True},
|
| 41 |
+
"gold_coin": {"name": "Gold Coin", "base_value": 1, "stackable": True} }
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def metadata(self) -> PluginMetadata:
|
| 45 |
+
return self._metadata
|
| 46 |
+
|
| 47 |
+
def initialize(self, context: Dict[str, Any]) -> bool:
|
| 48 |
+
"""Initialize the trading system plugin."""
|
| 49 |
+
try:
|
| 50 |
+
self._config = self._metadata.config
|
| 51 |
+
self._enabled = True
|
| 52 |
+
print(f"🏪 Trading System Plugin initialized with {len(self._tradeable_items)} tradeable items")
|
| 53 |
+
return True
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"🏪 Failed to initialize Trading System Plugin: {e}")
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
def cleanup(self) -> None:
|
| 59 |
+
"""Clean up trading system resources."""
|
| 60 |
+
# Cancel all active trades
|
| 61 |
+
for trade_id in list(self._active_trades.keys()):
|
| 62 |
+
self.cancel_trade(trade_id)
|
| 63 |
+
|
| 64 |
+
self._enabled = False
|
| 65 |
+
print("💰 Trading System Plugin cleaned up")
|
| 66 |
+
|
| 67 |
+
def is_enabled(self) -> bool:
|
| 68 |
+
return self._enabled
|
| 69 |
+
|
| 70 |
+
def initiate_trade(self, player1_id: str, player2_id: str, player1_pos: tuple, player2_pos: tuple) -> Dict[str, Any]:
|
| 71 |
+
"""Initiate a trade between two players."""
|
| 72 |
+
if not self._enabled or not self._config.get("enable_direct_trading", True):
|
| 73 |
+
return {"success": False, "error": "Trading is disabled"}
|
| 74 |
+
|
| 75 |
+
# Check distance
|
| 76 |
+
max_distance = self._config.get("max_trade_distance", 100)
|
| 77 |
+
distance = ((player1_pos[0] - player2_pos[0]) ** 2 + (player1_pos[1] - player2_pos[1]) ** 2) ** 0.5
|
| 78 |
+
|
| 79 |
+
if distance > max_distance:
|
| 80 |
+
return {"success": False, "error": "Players are too far apart to trade"}
|
| 81 |
+
|
| 82 |
+
# Create trade session
|
| 83 |
+
trade_id = str(uuid.uuid4())
|
| 84 |
+
trade_session = {
|
| 85 |
+
"id": trade_id,
|
| 86 |
+
"player1": player1_id,
|
| 87 |
+
"player2": player2_id,
|
| 88 |
+
"player1_items": {},
|
| 89 |
+
"player2_items": {},
|
| 90 |
+
"player1_gold": 0,
|
| 91 |
+
"player2_gold": 0,
|
| 92 |
+
"player1_accepted": False,
|
| 93 |
+
"player2_accepted": False,
|
| 94 |
+
"status": "active",
|
| 95 |
+
"created_at": time.time()
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
self._active_trades[trade_id] = trade_session
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"success": True,
|
| 102 |
+
"trade_id": trade_id,
|
| 103 |
+
"message": f"Trade initiated between players {player1_id} and {player2_id}"
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
def add_item_to_trade(self, trade_id: str, player_id: str, item_id: str, quantity: int = 1) -> Dict[str, Any]:
|
| 107 |
+
"""Add an item to a trade."""
|
| 108 |
+
if trade_id not in self._active_trades:
|
| 109 |
+
return {"success": False, "error": "Trade not found"}
|
| 110 |
+
|
| 111 |
+
trade = self._active_trades[trade_id]
|
| 112 |
+
|
| 113 |
+
if trade["status"] != "active":
|
| 114 |
+
return {"success": False, "error": "Trade is not active"}
|
| 115 |
+
|
| 116 |
+
# Determine which player
|
| 117 |
+
if player_id == trade["player1"]:
|
| 118 |
+
items_dict = trade["player1_items"]
|
| 119 |
+
elif player_id == trade["player2"]:
|
| 120 |
+
items_dict = trade["player2_items"]
|
| 121 |
+
else:
|
| 122 |
+
return {"success": False, "error": "Player not part of this trade"}
|
| 123 |
+
|
| 124 |
+
# Add item
|
| 125 |
+
if item_id not in items_dict:
|
| 126 |
+
items_dict[item_id] = 0
|
| 127 |
+
items_dict[item_id] += quantity
|
| 128 |
+
|
| 129 |
+
# Reset acceptance status
|
| 130 |
+
trade["player1_accepted"] = False
|
| 131 |
+
trade["player2_accepted"] = False
|
| 132 |
+
|
| 133 |
+
return {
|
| 134 |
+
"success": True,
|
| 135 |
+
"message": f"Added {quantity}x {self._tradeable_items.get(item_id, {}).get('name', item_id)} to trade"
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
def add_gold_to_trade(self, trade_id: str, player_id: str, amount: int) -> Dict[str, Any]:
|
| 139 |
+
"""Add gold to a trade."""
|
| 140 |
+
if trade_id not in self._active_trades:
|
| 141 |
+
return {"success": False, "error": "Trade not found"}
|
| 142 |
+
|
| 143 |
+
trade = self._active_trades[trade_id]
|
| 144 |
+
|
| 145 |
+
if trade["status"] != "active":
|
| 146 |
+
return {"success": False, "error": "Trade is not active"}
|
| 147 |
+
|
| 148 |
+
# Determine which player and update gold
|
| 149 |
+
if player_id == trade["player1"]:
|
| 150 |
+
trade["player1_gold"] += amount
|
| 151 |
+
elif player_id == trade["player2"]:
|
| 152 |
+
trade["player2_gold"] += amount
|
| 153 |
+
else:
|
| 154 |
+
return {"success": False, "error": "Player not part of this trade"}
|
| 155 |
+
|
| 156 |
+
# Reset acceptance status
|
| 157 |
+
trade["player1_accepted"] = False
|
| 158 |
+
trade["player2_accepted"] = False
|
| 159 |
+
|
| 160 |
+
return {"success": True, "message": f"Added {amount} gold to trade"}
|
| 161 |
+
|
| 162 |
+
def accept_trade(self, trade_id: str, player_id: str) -> Dict[str, Any]:
|
| 163 |
+
"""Accept a trade."""
|
| 164 |
+
if trade_id not in self._active_trades:
|
| 165 |
+
return {"success": False, "error": "Trade not found"}
|
| 166 |
+
|
| 167 |
+
trade = self._active_trades[trade_id]
|
| 168 |
+
|
| 169 |
+
if trade["status"] != "active":
|
| 170 |
+
return {"success": False, "error": "Trade is not active"}
|
| 171 |
+
|
| 172 |
+
# Mark player as accepted
|
| 173 |
+
if player_id == trade["player1"]:
|
| 174 |
+
trade["player1_accepted"] = True
|
| 175 |
+
elif player_id == trade["player2"]:
|
| 176 |
+
trade["player2_accepted"] = True
|
| 177 |
+
else:
|
| 178 |
+
return {"success": False, "error": "Player not part of this trade"}
|
| 179 |
+
|
| 180 |
+
# Check if both players accepted
|
| 181 |
+
if trade["player1_accepted"] and trade["player2_accepted"]:
|
| 182 |
+
return self._complete_trade(trade_id)
|
| 183 |
+
|
| 184 |
+
return {"success": True, "message": "Trade accepted, waiting for other player"}
|
| 185 |
+
|
| 186 |
+
def cancel_trade(self, trade_id: str) -> Dict[str, Any]:
|
| 187 |
+
"""Cancel a trade."""
|
| 188 |
+
if trade_id not in self._active_trades:
|
| 189 |
+
return {"success": False, "error": "Trade not found"}
|
| 190 |
+
|
| 191 |
+
trade = self._active_trades[trade_id]
|
| 192 |
+
trade["status"] = "cancelled"
|
| 193 |
+
|
| 194 |
+
del self._active_trades[trade_id]
|
| 195 |
+
|
| 196 |
+
return {"success": True, "message": "Trade cancelled"}
|
| 197 |
+
|
| 198 |
+
def get_trade_status(self, trade_id: str) -> Dict[str, Any]:
|
| 199 |
+
"""Get the status of a trade."""
|
| 200 |
+
if trade_id not in self._active_trades:
|
| 201 |
+
return {"success": False, "error": "Trade not found"}
|
| 202 |
+
|
| 203 |
+
trade = self._active_trades[trade_id]
|
| 204 |
+
|
| 205 |
+
return {
|
| 206 |
+
"success": True,
|
| 207 |
+
"trade": {
|
| 208 |
+
"id": trade["id"],
|
| 209 |
+
"status": trade["status"],
|
| 210 |
+
"player1": trade["player1"],
|
| 211 |
+
"player2": trade["player2"],
|
| 212 |
+
"player1_items": trade["player1_items"],
|
| 213 |
+
"player2_items": trade["player2_items"],
|
| 214 |
+
"player1_gold": trade["player1_gold"],
|
| 215 |
+
"player2_gold": trade["player2_gold"],
|
| 216 |
+
"player1_accepted": trade["player1_accepted"],
|
| 217 |
+
"player2_accepted": trade["player2_accepted"]
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
def create_marketplace_listing(self, player_id: str, item_id: str, quantity: int, price: int) -> Dict[str, Any]:
|
| 222 |
+
"""Create a marketplace listing."""
|
| 223 |
+
if not self._enabled or not self._config.get("enable_marketplace", True):
|
| 224 |
+
return {"success": False, "error": "Marketplace is disabled"}
|
| 225 |
+
|
| 226 |
+
listing_id = str(uuid.uuid4())
|
| 227 |
+
listing = {
|
| 228 |
+
"id": listing_id,
|
| 229 |
+
"seller_id": player_id,
|
| 230 |
+
"item_id": item_id,
|
| 231 |
+
"quantity": quantity,
|
| 232 |
+
"price": price,
|
| 233 |
+
"created_at": time.time(),
|
| 234 |
+
"status": "active"
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
self._marketplace_listings[listing_id] = listing
|
| 238 |
+
|
| 239 |
+
item_name = self._tradeable_items.get(item_id, {}).get("name", item_id)
|
| 240 |
+
|
| 241 |
+
return {
|
| 242 |
+
"success": True,
|
| 243 |
+
"listing_id": listing_id,
|
| 244 |
+
"message": f"Listed {quantity}x {item_name} for {price} gold"
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
def get_marketplace_listings(self, item_filter: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 248 |
+
"""Get marketplace listings."""
|
| 249 |
+
if not self._enabled or not self._config.get("enable_marketplace", True):
|
| 250 |
+
return []
|
| 251 |
+
|
| 252 |
+
listings = []
|
| 253 |
+
for listing in self._marketplace_listings.values():
|
| 254 |
+
if listing["status"] != "active":
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
if item_filter and listing["item_id"] != item_filter:
|
| 258 |
+
continue
|
| 259 |
+
|
| 260 |
+
item_info = self._tradeable_items.get(listing["item_id"], {})
|
| 261 |
+
listing_info = {
|
| 262 |
+
"id": listing["id"],
|
| 263 |
+
"seller_id": listing["seller_id"],
|
| 264 |
+
"item_id": listing["item_id"],
|
| 265 |
+
"item_name": item_info.get("name", listing["item_id"]),
|
| 266 |
+
"quantity": listing["quantity"],
|
| 267 |
+
"price": listing["price"],
|
| 268 |
+
"price_per_unit": listing["price"] / listing["quantity"],
|
| 269 |
+
"created_at": listing["created_at"]
|
| 270 |
+
}
|
| 271 |
+
listings.append(listing_info)
|
| 272 |
+
|
| 273 |
+
# Sort by price per unit
|
| 274 |
+
listings.sort(key=lambda x: x["price_per_unit"])
|
| 275 |
+
|
| 276 |
+
return listings
|
| 277 |
+
|
| 278 |
+
def purchase_from_marketplace(self, buyer_id: str, listing_id: str) -> Dict[str, Any]:
|
| 279 |
+
"""Purchase an item from the marketplace."""
|
| 280 |
+
if listing_id not in self._marketplace_listings:
|
| 281 |
+
return {"success": False, "error": "Listing not found"}
|
| 282 |
+
|
| 283 |
+
listing = self._marketplace_listings[listing_id]
|
| 284 |
+
|
| 285 |
+
if listing["status"] != "active":
|
| 286 |
+
return {"success": False, "error": "Listing is no longer active"}
|
| 287 |
+
|
| 288 |
+
if listing["seller_id"] == buyer_id:
|
| 289 |
+
return {"success": False, "error": "Cannot buy your own listing"}
|
| 290 |
+
|
| 291 |
+
# Calculate tax
|
| 292 |
+
tax_rate = self._config.get("trade_tax_rate", 0.05)
|
| 293 |
+
tax_amount = int(listing["price"] * tax_rate)
|
| 294 |
+
seller_receives = listing["price"] - tax_amount
|
| 295 |
+
|
| 296 |
+
# Mark listing as sold
|
| 297 |
+
listing["status"] = "sold"
|
| 298 |
+
listing["buyer_id"] = buyer_id
|
| 299 |
+
listing["sold_at"] = time.time()
|
| 300 |
+
|
| 301 |
+
# Add to trade history
|
| 302 |
+
trade_record = {
|
| 303 |
+
"type": "marketplace",
|
| 304 |
+
"seller_id": listing["seller_id"],
|
| 305 |
+
"buyer_id": buyer_id,
|
| 306 |
+
"item_id": listing["item_id"],
|
| 307 |
+
"quantity": listing["quantity"],
|
| 308 |
+
"price": listing["price"],
|
| 309 |
+
"tax_amount": tax_amount,
|
| 310 |
+
"timestamp": time.time()
|
| 311 |
+
}
|
| 312 |
+
self._trade_history.append(trade_record)
|
| 313 |
+
|
| 314 |
+
item_name = self._tradeable_items.get(listing["item_id"], {}).get("name", listing["item_id"])
|
| 315 |
+
|
| 316 |
+
return {
|
| 317 |
+
"success": True,
|
| 318 |
+
"message": f"Purchased {listing['quantity']}x {item_name} for {listing['price']} gold",
|
| 319 |
+
"seller_receives": seller_receives,
|
| 320 |
+
"tax_paid": tax_amount
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
def get_tradeable_items(self) -> Dict[str, Dict[str, Any]]:
|
| 324 |
+
"""Get list of tradeable items."""
|
| 325 |
+
return self._tradeable_items.copy()
|
| 326 |
+
|
| 327 |
+
def _complete_trade(self, trade_id: str) -> Dict[str, Any]:
|
| 328 |
+
"""Complete a trade between two players."""
|
| 329 |
+
trade = self._active_trades[trade_id]
|
| 330 |
+
|
| 331 |
+
# Mark trade as completed
|
| 332 |
+
trade["status"] = "completed"
|
| 333 |
+
trade["completed_at"] = time.time()
|
| 334 |
+
# Add to trade history
|
| 335 |
+
trade_record = {
|
| 336 |
+
"type": "direct_trade",
|
| 337 |
+
"player1": trade["player1"],
|
| 338 |
+
"player2": trade["player2"],
|
| 339 |
+
"player1_items": trade["player1_items"],
|
| 340 |
+
"player2_items": trade["player2_items"],
|
| 341 |
+
"player1_gold": trade["player1_gold"],
|
| 342 |
+
"player2_gold": trade["player2_gold"],
|
| 343 |
+
"timestamp": time.time()
|
| 344 |
+
}
|
| 345 |
+
self._trade_history.append(trade_record)
|
| 346 |
+
|
| 347 |
+
# Remove from active trades
|
| 348 |
+
del self._active_trades[trade_id]
|
| 349 |
+
|
| 350 |
+
return {
|
| 351 |
+
"success": True,
|
| 352 |
+
"message": "Trade completed successfully!",
|
| 353 |
+
"trade_summary": trade_record
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
def get_status(self) -> Dict[str, Any]:
|
| 357 |
+
"""Get trading system status."""
|
| 358 |
+
return {
|
| 359 |
+
"enabled": self._enabled,
|
| 360 |
+
"active_trades": len(self._active_trades),
|
| 361 |
+
"marketplace_listings": len(self._marketplace_listings),
|
| 362 |
+
"total_items": len(self._tradeable_items),
|
| 363 |
+
"trade_history_count": len(self._trade_history)
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
def shutdown(self) -> bool:
|
| 367 |
+
"""Shutdown the trading system."""
|
| 368 |
+
try:
|
| 369 |
+
self.cleanup()
|
| 370 |
+
return True
|
| 371 |
+
except Exception as e:
|
| 372 |
+
print(f"🏪 Error shutting down Trading System Plugin: {e}")
|
| 373 |
+
return False
|
| 374 |
+
|
| 375 |
+
# Plugin entry point
|
| 376 |
+
def create_plugin():
|
| 377 |
+
"""Factory function to create the plugin instance."""
|
| 378 |
+
return TradingSystemPlugin()
|
plugins/weather_plugin.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sample Weather Plugin for MMORPG
|
| 3 |
+
Adds weather effects and seasonal changes to the game world
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from src.interfaces.plugin_interfaces import IWeatherPlugin, PluginMetadata, PluginType
|
| 7 |
+
from typing import Dict, List, Any
|
| 8 |
+
import random
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
class WeatherPlugin(IWeatherPlugin):
|
| 12 |
+
"""Plugin that adds dynamic weather system to the game."""
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self._metadata = PluginMetadata(
|
| 16 |
+
id="weather_system",
|
| 17 |
+
name="Weather System",
|
| 18 |
+
version="1.0.0",
|
| 19 |
+
author="MMORPG Dev Team",
|
| 20 |
+
description="Adds dynamic weather effects and seasonal changes",
|
| 21 |
+
plugin_type=PluginType.SERVICE,
|
| 22 |
+
dependencies=[],
|
| 23 |
+
config={
|
| 24 |
+
"weather_change_interval": 300,
|
| 25 |
+
"enable_seasons": True,
|
| 26 |
+
"weather_effects_enabled": True
|
| 27 |
+
}
|
| 28 |
+
)
|
| 29 |
+
self._enabled = False
|
| 30 |
+
self._weather_state = {
|
| 31 |
+
"current_weather": "sunny",
|
| 32 |
+
"temperature": 20,
|
| 33 |
+
"season": "spring",
|
| 34 |
+
"last_change": time.time()
|
| 35 |
+
}
|
| 36 |
+
self._weather_types = [
|
| 37 |
+
"sunny", "cloudy", "rainy", "stormy", "foggy", "snowy"
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
@property
|
| 41 |
+
def metadata(self) -> PluginMetadata:
|
| 42 |
+
return self._metadata
|
| 43 |
+
|
| 44 |
+
def initialize(self, context: Dict[str, Any]) -> bool:
|
| 45 |
+
"""Initialize the weather plugin."""
|
| 46 |
+
try:
|
| 47 |
+
self._config = self._metadata.config
|
| 48 |
+
self._enabled = True
|
| 49 |
+
print("Weather Plugin initialized successfully")
|
| 50 |
+
self._update_weather()
|
| 51 |
+
return True
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Failed to initialize Weather Plugin: {e}")
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
def shutdown(self) -> bool:
|
| 57 |
+
"""Shutdown the weather plugin."""
|
| 58 |
+
try:
|
| 59 |
+
self._enabled = False
|
| 60 |
+
print("Weather Plugin shut down")
|
| 61 |
+
return True
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print(f"Error shutting down Weather Plugin: {e}")
|
| 64 |
+
return False
|
| 65 |
+
|
| 66 |
+
def get_status(self) -> Dict[str, Any]:
|
| 67 |
+
"""Get weather plugin status."""
|
| 68 |
+
return {
|
| 69 |
+
"enabled": self._enabled,
|
| 70 |
+
"current_weather": self._weather_state["current_weather"],
|
| 71 |
+
"temperature": self._weather_state["temperature"],
|
| 72 |
+
"season": self._weather_state["season"],
|
| 73 |
+
"effects_enabled": self._config.get("weather_effects_enabled", True) if hasattr(self, '_config') else True
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
def get_weather_info(self) -> Dict[str, Any]:
|
| 77 |
+
"""Get current weather information."""
|
| 78 |
+
if not self._enabled:
|
| 79 |
+
return {}
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
"weather": self._weather_state["current_weather"],
|
| 83 |
+
"temperature": self._weather_state["temperature"],
|
| 84 |
+
"season": self._weather_state["season"],
|
| 85 |
+
"description": self._get_weather_description()
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
def update_weather(self) -> Dict[str, Any]:
|
| 89 |
+
"""Update weather conditions."""
|
| 90 |
+
if not self._enabled:
|
| 91 |
+
return {}
|
| 92 |
+
|
| 93 |
+
current_time = time.time()
|
| 94 |
+
interval = self._config.get("weather_change_interval", 300)
|
| 95 |
+
|
| 96 |
+
if current_time - self._weather_state["last_change"] > interval:
|
| 97 |
+
self._update_weather()
|
| 98 |
+
self._weather_state["last_change"] = current_time
|
| 99 |
+
|
| 100 |
+
return self.get_weather_info()
|
| 101 |
+
|
| 102 |
+
def apply_weather_effects(self, player_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 103 |
+
"""Apply weather effects to player."""
|
| 104 |
+
if not self._enabled or not self._config.get("weather_effects_enabled", True):
|
| 105 |
+
return player_data
|
| 106 |
+
|
| 107 |
+
effects = player_data.copy()
|
| 108 |
+
weather_modifier = self._get_weather_modifier()
|
| 109 |
+
|
| 110 |
+
if "stats" not in effects:
|
| 111 |
+
effects["stats"] = {}
|
| 112 |
+
|
| 113 |
+
effects["stats"]["weather_modifier"] = weather_modifier
|
| 114 |
+
effects["weather_description"] = self._get_weather_description()
|
| 115 |
+
|
| 116 |
+
return effects
|
| 117 |
+
|
| 118 |
+
def _update_weather(self):
|
| 119 |
+
"""Internal method to update weather conditions."""
|
| 120 |
+
self._weather_state["current_weather"] = random.choice(self._weather_types)
|
| 121 |
+
|
| 122 |
+
base_temp = self._get_seasonal_base_temperature()
|
| 123 |
+
weather_modifier = self._get_weather_temperature_modifier()
|
| 124 |
+
self._weather_state["temperature"] = base_temp + weather_modifier + random.randint(-5, 5)
|
| 125 |
+
|
| 126 |
+
if self._config.get("enable_seasons", True):
|
| 127 |
+
seasons = ["spring", "summer", "autumn", "winter"]
|
| 128 |
+
if random.random() < 0.01:
|
| 129 |
+
current_season_idx = seasons.index(self._weather_state["season"])
|
| 130 |
+
self._weather_state["season"] = seasons[(current_season_idx + 1) % len(seasons)]
|
| 131 |
+
|
| 132 |
+
def _get_seasonal_base_temperature(self) -> int:
|
| 133 |
+
"""Get base temperature for current season."""
|
| 134 |
+
season_temps = {
|
| 135 |
+
"spring": 15,
|
| 136 |
+
"summer": 25,
|
| 137 |
+
"autumn": 10,
|
| 138 |
+
"winter": 0
|
| 139 |
+
}
|
| 140 |
+
return season_temps.get(self._weather_state["season"], 15)
|
| 141 |
+
|
| 142 |
+
def _get_weather_temperature_modifier(self) -> int:
|
| 143 |
+
"""Get temperature modifier based on weather."""
|
| 144 |
+
weather_mods = {
|
| 145 |
+
"sunny": 5,
|
| 146 |
+
"cloudy": 0,
|
| 147 |
+
"rainy": -3,
|
| 148 |
+
"stormy": -5,
|
| 149 |
+
"foggy": -2,
|
| 150 |
+
"snowy": -10
|
| 151 |
+
}
|
| 152 |
+
return weather_mods.get(self._weather_state["current_weather"], 0)
|
| 153 |
+
|
| 154 |
+
def _get_weather_description(self) -> str:
|
| 155 |
+
"""Get descriptive text for current weather."""
|
| 156 |
+
weather = self._weather_state["current_weather"]
|
| 157 |
+
temp = self._weather_state["temperature"]
|
| 158 |
+
season = self._weather_state["season"]
|
| 159 |
+
|
| 160 |
+
descriptions = {
|
| 161 |
+
"sunny": f"Bright and sunny {season} day ({temp} degrees C)",
|
| 162 |
+
"cloudy": f"Overcast {season} sky ({temp} degrees C)",
|
| 163 |
+
"rainy": f"Light rain falling in {season} ({temp} degrees C)",
|
| 164 |
+
"stormy": f"Thunderstorm brewing this {season} ({temp} degrees C)",
|
| 165 |
+
"foggy": f"Thick fog blankets the {season} landscape ({temp} degrees C)",
|
| 166 |
+
"snowy": f"Snow falling gently in {season} ({temp} degrees C)"
|
| 167 |
+
}
|
| 168 |
+
return descriptions.get(weather, f"Unknown weather in {season} ({temp} degrees C)")
|
| 169 |
+
|
| 170 |
+
def _get_weather_modifier(self) -> Dict[str, float]:
|
| 171 |
+
"""Get weather-based stat modifiers."""
|
| 172 |
+
weather = self._weather_state["current_weather"]
|
| 173 |
+
|
| 174 |
+
modifiers = {
|
| 175 |
+
"sunny": {"movement_speed": 1.1, "visibility": 1.0, "morale": 1.1},
|
| 176 |
+
"cloudy": {"movement_speed": 1.0, "visibility": 0.95, "morale": 0.95},
|
| 177 |
+
"rainy": {"movement_speed": 0.8, "visibility": 0.9, "morale": 0.9},
|
| 178 |
+
"stormy": {"movement_speed": 0.6, "visibility": 0.7, "morale": 0.8},
|
| 179 |
+
"foggy": {"movement_speed": 0.9, "visibility": 0.5, "morale": 0.85},
|
| 180 |
+
"snowy": {"movement_speed": 0.7, "visibility": 0.8, "morale": 0.9}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
return modifiers.get(weather, {"movement_speed": 1.0, "visibility": 1.0, "morale": 1.0})
|
| 184 |
+
|
| 185 |
+
def create_plugin():
|
| 186 |
+
"""Factory function to create the plugin instance."""
|
| 187 |
+
return WeatherPlugin()
|
pytest.ini
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool:pytest]
|
| 2 |
+
markers =
|
| 3 |
+
stress: marks tests as stress tests (deselect with '-m "not stress"')
|
| 4 |
+
error_handling: marks tests as error handling tests
|
| 5 |
+
integration: marks tests as integration tests
|
| 6 |
+
browser: marks tests as browser tests (may require selenium)
|
| 7 |
+
slow: marks tests as slow running
|
| 8 |
+
e2e: marks tests as end-to-end tests
|
| 9 |
+
timeout: marks tests that need timeout protection
|
| 10 |
+
|
| 11 |
+
testpaths = tests
|
| 12 |
+
python_files = test_*.py
|
| 13 |
+
python_classes = Test*
|
| 14 |
+
python_functions = test_*
|
| 15 |
+
|
| 16 |
+
# Default timeout for tests
|
| 17 |
+
timeout = 60
|
| 18 |
+
|
| 19 |
+
# Ignore deprecation warnings from external libraries
|
| 20 |
+
filterwarnings =
|
| 21 |
+
ignore::DeprecationWarning:websockets.*
|
| 22 |
+
ignore::pytest.PytestCollectionWarning
|
| 23 |
+
ignore::pytest.PytestUnknownMarkWarning
|
| 24 |
+
|
| 25 |
+
# Set asyncio default loop scope
|
| 26 |
+
addopts =
|
| 27 |
+
--timeout=60
|
| 28 |
+
--timeout-method=thread
|
| 29 |
+
-v
|
| 30 |
+
--tb=short
|
quick_test.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Quick verification that the world events auto-refresh fix is working
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 9 |
+
|
| 10 |
+
def test_world_events_fix():
|
| 11 |
+
"""Quick test of the world events fix"""
|
| 12 |
+
try:
|
| 13 |
+
from src.facades.game_facade import GameFacade
|
| 14 |
+
from src.ui.interface_manager import InterfaceManager
|
| 15 |
+
|
| 16 |
+
print("✅ Import successful")
|
| 17 |
+
|
| 18 |
+
# Test GameFacade has get_world_events method
|
| 19 |
+
facade = GameFacade()
|
| 20 |
+
if hasattr(facade, 'get_world_events'):
|
| 21 |
+
print("✅ GameFacade.get_world_events() method exists")
|
| 22 |
+
|
| 23 |
+
events = facade.get_world_events()
|
| 24 |
+
print(f"✅ Method returns {len(events)} events")
|
| 25 |
+
else:
|
| 26 |
+
print("❌ GameFacade.get_world_events() method missing")
|
| 27 |
+
return False
|
| 28 |
+
|
| 29 |
+
return True
|
| 30 |
+
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"❌ Error: {e}")
|
| 33 |
+
return False
|
| 34 |
+
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
print("🔧 QUICK WORLD EVENTS FIX VERIFICATION")
|
| 37 |
+
print("=" * 50)
|
| 38 |
+
|
| 39 |
+
if test_world_events_fix():
|
| 40 |
+
print("\n🎉 World events auto-refresh fix is working!")
|
| 41 |
+
print("📋 Summary of what was fixed:")
|
| 42 |
+
print("✅ Added 'world_events' as 8th output in timer configuration")
|
| 43 |
+
print("✅ Fixed mismatch between timer outputs (7) and method returns (8)")
|
| 44 |
+
print("✅ World events panel will now update automatically")
|
| 45 |
+
print("\n🌐 Server should be running at: http://localhost:7866")
|
| 46 |
+
else:
|
| 47 |
+
print("\n❌ There are still issues with the fix")
|
requirements.txt
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MMOP Second Try - Requirements File
|
| 2 |
+
# MMORPG with Gradio UI, MCP Integration, and Plugin System
|
| 3 |
+
|
| 4 |
+
# Core Web Framework & UI
|
| 5 |
+
gradio>=4.0.0
|
| 6 |
+
gradio[mcp]
|
| 7 |
+
|
| 8 |
+
# Data Processing & Math
|
| 9 |
+
numpy>=1.20.0
|
| 10 |
+
pandas>=1.3.0
|
| 11 |
+
|
| 12 |
+
# Development & Testing
|
| 13 |
+
pytest>=7.0.0
|
| 14 |
+
pytest-asyncio>=0.21.0
|
| 15 |
+
|
| 16 |
+
# Optional Dependencies (uncomment as needed)
|
| 17 |
+
|
| 18 |
+
# For enhanced logging and debugging
|
| 19 |
+
# colorlog>=6.0.0
|
| 20 |
+
|
| 21 |
+
# For configuration management
|
| 22 |
+
# pyyaml>=6.0
|
| 23 |
+
# python-dotenv>=0.19.0
|
| 24 |
+
|
| 25 |
+
# For HTTP/API support (if implementing REST APIs)
|
| 26 |
+
# fastapi>=0.68.0
|
| 27 |
+
# uvicorn>=0.15.0
|
| 28 |
+
|
| 29 |
+
# For advanced text processing
|
| 30 |
+
# regex>=2021.8.3
|
| 31 |
+
|
| 32 |
+
# Development Tools (for code quality)
|
| 33 |
+
# black>=22.0.0
|
| 34 |
+
# isort>=5.10.0
|
| 35 |
+
# flake8>=4.0.0
|
| 36 |
+
# mypy>=0.910
|
| 37 |
+
|
| 38 |
+
# Note: The following are included in Python 3.13 standard library:
|
| 39 |
+
# - dataclasses (built-in since Python 3.7)
|
| 40 |
+
# - typing (built-in)
|
| 41 |
+
# - asyncio (built-in)
|
| 42 |
+
# - threading (built-in)
|
| 43 |
+
# - uuid (built-in)
|
| 44 |
+
# - json (built-in)
|
| 45 |
+
# - pathlib (built-in)
|
| 46 |
+
# - os (built-in)
|
| 47 |
+
# - time (built-in)
|
| 48 |
+
# - random (built-in)
|
| 49 |
+
# - math (built-in)
|
| 50 |
+
# - hashlib (built-in)
|
| 51 |
+
# - importlib (built-in)
|
| 52 |
+
# - inspect (built-in)
|
| 53 |
+
# - sqlite3 (built-in)
|
setup.bat
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM MMOP Second Try - Windows Setup Script
|
| 3 |
+
REM This script sets up the MMORPG environment on Windows
|
| 4 |
+
|
| 5 |
+
echo.
|
| 6 |
+
echo ========================================
|
| 7 |
+
echo MMOP Second Try - Setup Script
|
| 8 |
+
echo ========================================
|
| 9 |
+
echo.
|
| 10 |
+
|
| 11 |
+
REM Check if Python is available
|
| 12 |
+
python --version >nul 2>&1
|
| 13 |
+
if errorlevel 1 (
|
| 14 |
+
echo ERROR: Python is not installed or not in PATH
|
| 15 |
+
echo Please install Python 3.8 or higher from python.org
|
| 16 |
+
pause
|
| 17 |
+
exit /b 1
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
echo Python found. Starting setup...
|
| 21 |
+
echo.
|
| 22 |
+
|
| 23 |
+
REM Run the Python setup script
|
| 24 |
+
python setup.py
|
| 25 |
+
|
| 26 |
+
echo.
|
| 27 |
+
echo ========================================
|
| 28 |
+
echo Setup completed!
|
| 29 |
+
echo.
|
| 30 |
+
echo To run the MMORPG:
|
| 31 |
+
echo python app.py
|
| 32 |
+
echo.
|
| 33 |
+
echo Then open your browser to the displayed URL
|
| 34 |
+
echo ========================================
|
| 35 |
+
echo.
|
| 36 |
+
pause
|
setup.ps1
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MMOP Second Try - PowerShell Setup Script
|
| 2 |
+
# This script sets up the MMORPG environment using PowerShell
|
| 3 |
+
|
| 4 |
+
Write-Host ""
|
| 5 |
+
Write-Host "========================================" -ForegroundColor Cyan
|
| 6 |
+
Write-Host " MMOP Second Try - Setup Script" -ForegroundColor Cyan
|
| 7 |
+
Write-Host "========================================" -ForegroundColor Cyan
|
| 8 |
+
Write-Host ""
|
| 9 |
+
|
| 10 |
+
# Check if Python is available
|
| 11 |
+
try {
|
| 12 |
+
$pythonVersion = python --version 2>&1
|
| 13 |
+
Write-Host "✅ $pythonVersion found" -ForegroundColor Green
|
| 14 |
+
} catch {
|
| 15 |
+
Write-Host "❌ ERROR: Python is not installed or not in PATH" -ForegroundColor Red
|
| 16 |
+
Write-Host "Please install Python 3.8 or higher from python.org" -ForegroundColor Yellow
|
| 17 |
+
Read-Host "Press Enter to exit"
|
| 18 |
+
exit 1
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
Write-Host "🔧 Starting setup..." -ForegroundColor Yellow
|
| 22 |
+
Write-Host ""
|
| 23 |
+
|
| 24 |
+
# Run the Python setup script
|
| 25 |
+
try {
|
| 26 |
+
python setup.py
|
| 27 |
+
Write-Host ""
|
| 28 |
+
Write-Host "========================================" -ForegroundColor Green
|
| 29 |
+
Write-Host "🎉 Setup completed successfully!" -ForegroundColor Green
|
| 30 |
+
Write-Host ""
|
| 31 |
+
Write-Host "To run the MMORPG:" -ForegroundColor Cyan
|
| 32 |
+
Write-Host " python app.py" -ForegroundColor White
|
| 33 |
+
Write-Host ""
|
| 34 |
+
Write-Host "Then open your browser to the displayed URL" -ForegroundColor Cyan
|
| 35 |
+
Write-Host "========================================" -ForegroundColor Green
|
| 36 |
+
} catch {
|
| 37 |
+
Write-Host "❌ Setup failed. Please check the error messages above." -ForegroundColor Red
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
Write-Host ""
|
| 41 |
+
Read-Host "Press Enter to continue"
|
setup.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Setup script for MMOP Second Try
|
| 4 |
+
Installs dependencies and verifies the installation
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
def run_command(command, description):
|
| 13 |
+
"""Run a command and handle errors"""
|
| 14 |
+
print(f"\n{'='*50}")
|
| 15 |
+
print(f"🔧 {description}")
|
| 16 |
+
print(f"{'='*50}")
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
| 20 |
+
print(f"✅ {description} completed successfully")
|
| 21 |
+
if result.stdout:
|
| 22 |
+
print(f"Output: {result.stdout}")
|
| 23 |
+
return True
|
| 24 |
+
except subprocess.CalledProcessError as e:
|
| 25 |
+
print(f"❌ {description} failed")
|
| 26 |
+
print(f"Error: {e.stderr}")
|
| 27 |
+
return False
|
| 28 |
+
|
| 29 |
+
def check_python_version():
|
| 30 |
+
"""Check if Python version is suitable"""
|
| 31 |
+
print(f"🐍 Python version: {sys.version}")
|
| 32 |
+
if sys.version_info < (3, 8):
|
| 33 |
+
print("❌ Python 3.8 or higher is required")
|
| 34 |
+
return False
|
| 35 |
+
print("✅ Python version is compatible")
|
| 36 |
+
return True
|
| 37 |
+
|
| 38 |
+
def install_dependencies():
|
| 39 |
+
"""Install dependencies from requirements.txt"""
|
| 40 |
+
requirements_file = Path("requirements.txt")
|
| 41 |
+
if not requirements_file.exists():
|
| 42 |
+
print("❌ requirements.txt not found")
|
| 43 |
+
return False
|
| 44 |
+
|
| 45 |
+
return run_command(
|
| 46 |
+
f"{sys.executable} -m pip install -r requirements.txt",
|
| 47 |
+
"Installing dependencies"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
def verify_installation():
|
| 51 |
+
"""Verify that key components can be imported"""
|
| 52 |
+
print(f"\n{'='*50}")
|
| 53 |
+
print("🔍 Verifying installation")
|
| 54 |
+
print(f"{'='*50}")
|
| 55 |
+
|
| 56 |
+
tests = [
|
| 57 |
+
("import gradio", "Gradio UI framework"),
|
| 58 |
+
("import numpy", "NumPy"),
|
| 59 |
+
("import pandas", "Pandas"),
|
| 60 |
+
("from src.facades.game_facade import GameFacade", "Game Facade"),
|
| 61 |
+
("from src.mcp.mcp_tools import GradioMCPTools", "MCP Tools"),
|
| 62 |
+
("from app import MMORPGApplication", "Main Application"),
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
all_passed = True
|
| 66 |
+
for test_import, description in tests:
|
| 67 |
+
try:
|
| 68 |
+
exec(test_import)
|
| 69 |
+
print(f"✅ {description}")
|
| 70 |
+
except ImportError as e:
|
| 71 |
+
print(f"❌ {description}: {e}")
|
| 72 |
+
all_passed = False
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"⚠️ {description}: {e}")
|
| 75 |
+
|
| 76 |
+
return all_passed
|
| 77 |
+
|
| 78 |
+
def main():
|
| 79 |
+
"""Main setup function"""
|
| 80 |
+
print("🎮 MMOP Second Try - Setup Script")
|
| 81 |
+
print("=" * 50)
|
| 82 |
+
|
| 83 |
+
# Check Python version
|
| 84 |
+
if not check_python_version():
|
| 85 |
+
sys.exit(1)
|
| 86 |
+
|
| 87 |
+
# Install dependencies
|
| 88 |
+
if not install_dependencies():
|
| 89 |
+
print("\n❌ Dependency installation failed")
|
| 90 |
+
sys.exit(1)
|
| 91 |
+
|
| 92 |
+
# Verify installation
|
| 93 |
+
if not verify_installation():
|
| 94 |
+
print("\n⚠️ Some components failed verification, but you can still try running the application")
|
| 95 |
+
else:
|
| 96 |
+
print("\n🎉 Setup completed successfully!")
|
| 97 |
+
|
| 98 |
+
print(f"\n{'='*50}")
|
| 99 |
+
print("🚀 Next steps:")
|
| 100 |
+
print("1. Run the application: python app.py")
|
| 101 |
+
print("2. Open your browser to the displayed URL")
|
| 102 |
+
print("3. Start playing your MMORPG!")
|
| 103 |
+
print(f"{'='*50}")
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|
src/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MMORPG Application - Clean Architecture Implementation
|
| 3 |
+
Main package initialization
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# Package version
|
| 7 |
+
__version__ = "2.0.0"
|
| 8 |
+
|
| 9 |
+
# Import main components for easy access
|
| 10 |
+
from .core.game_engine import GameEngine
|
| 11 |
+
from .facades.game_facade import GameFacade
|
src/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (420 Bytes). View file
|
|
|
src/addons/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Add-on system package for MMOP game.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
__version__ = "1.0.0"
|
src/addons/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (267 Bytes). View file
|
|
|
src/addons/__pycache__/example_npc_addon.cpython-313.pyc
ADDED
|
Binary file (11.5 kB). View file
|
|
|
src/addons/__pycache__/read2burn_addon.cpython-313.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
src/addons/__pycache__/weather_oracle_addon.cpython-313.pyc
ADDED
|
Binary file (18.7 kB). View file
|
|
|
src/addons/example_npc_addon.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example NPC Addon - Template for creating new NPCs with custom functionality
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
from src.interfaces.npc_addon import NPCAddon
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ExampleNPCAddon(NPCAddon):
|
| 11 |
+
"""Example NPC addon demonstrating the complete NPC creation process."""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
super().__init__()
|
| 15 |
+
# Initialize any state or data your NPC needs
|
| 16 |
+
self.npc_inventory = {
|
| 17 |
+
"health_potion": {"name": "Health Potion", "price": 25, "stock": 10},
|
| 18 |
+
"magic_scroll": {"name": "Magic Scroll", "price": 50, "stock": 5},
|
| 19 |
+
"steel_sword": {"name": "Steel Sword", "price": 100, "stock": 3}
|
| 20 |
+
}
|
| 21 |
+
self.greeting_messages = [
|
| 22 |
+
"Welcome to my shop, adventurer!",
|
| 23 |
+
"Looking for some fine wares?",
|
| 24 |
+
"I have the best deals in town!",
|
| 25 |
+
"What can I help you with today?"
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def addon_id(self) -> str:
|
| 30 |
+
"""Unique identifier for this addon - must match NPC ID in world."""
|
| 31 |
+
return "example_merchant"
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def addon_name(self) -> str:
|
| 35 |
+
"""Display name shown in the interface."""
|
| 36 |
+
return "Example Merchant"
|
| 37 |
+
|
| 38 |
+
def get_interface(self) -> gr.Component:
|
| 39 |
+
"""Create the Gradio interface for this NPC."""
|
| 40 |
+
with gr.Column() as interface:
|
| 41 |
+
gr.Markdown("""
|
| 42 |
+
## 🏪 Example Merchant
|
| 43 |
+
|
| 44 |
+
*Welcome to my humble shop! I offer fine wares for brave adventurers.*
|
| 45 |
+
|
| 46 |
+
### Available Services:
|
| 47 |
+
- **Shop**: Browse and purchase items
|
| 48 |
+
- **Appraisal**: Get item value estimates
|
| 49 |
+
- **Information**: Learn about the local area
|
| 50 |
+
|
| 51 |
+
*Walk near me in the game world to unlock full functionality!*
|
| 52 |
+
""")
|
| 53 |
+
|
| 54 |
+
with gr.Tabs():
|
| 55 |
+
# Shop Tab
|
| 56 |
+
with gr.Tab("🛒 Shop"):
|
| 57 |
+
gr.Markdown("### Available Items")
|
| 58 |
+
|
| 59 |
+
with gr.Row():
|
| 60 |
+
item_select = gr.Dropdown(
|
| 61 |
+
label="Select Item",
|
| 62 |
+
choices=list(self.npc_inventory.keys()),
|
| 63 |
+
value=None
|
| 64 |
+
)
|
| 65 |
+
quantity = gr.Number(
|
| 66 |
+
label="Quantity",
|
| 67 |
+
value=1,
|
| 68 |
+
minimum=1,
|
| 69 |
+
maximum=10
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
with gr.Row():
|
| 73 |
+
buy_btn = gr.Button("💰 Purchase", variant="primary")
|
| 74 |
+
refresh_btn = gr.Button("🔄 Refresh Stock", variant="secondary")
|
| 75 |
+
|
| 76 |
+
purchase_result = gr.Textbox(
|
| 77 |
+
label="Transaction Result",
|
| 78 |
+
lines=3,
|
| 79 |
+
interactive=False
|
| 80 |
+
)
|
| 81 |
+
# Shop display
|
| 82 |
+
shop_display = gr.JSON(
|
| 83 |
+
label="Current Inventory",
|
| 84 |
+
value=self.npc_inventory
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def handle_purchase(item_key, qty):
|
| 88 |
+
if not item_key:
|
| 89 |
+
return "❌ Please select an item first!"
|
| 90 |
+
|
| 91 |
+
# Get current player (simplified for example)
|
| 92 |
+
from ..core.game_engine import GameEngine
|
| 93 |
+
game_engine = GameEngine()
|
| 94 |
+
game_world = game_engine.get_world()
|
| 95 |
+
current_players = list(game_world.players.keys())
|
| 96 |
+
if not current_players:
|
| 97 |
+
return "❌ You must be in the game to make purchases!"
|
| 98 |
+
|
| 99 |
+
player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| 100 |
+
return self.purchase_item(player_id, item_key, int(qty))
|
| 101 |
+
|
| 102 |
+
def refresh_shop():
|
| 103 |
+
return self.npc_inventory
|
| 104 |
+
|
| 105 |
+
# Wire up the interface
|
| 106 |
+
buy_btn.click(handle_purchase, [item_select, quantity], purchase_result)
|
| 107 |
+
refresh_btn.click(refresh_shop, outputs=shop_display)
|
| 108 |
+
|
| 109 |
+
# Information Tab
|
| 110 |
+
with gr.Tab("ℹ️ Information"):
|
| 111 |
+
gr.Markdown("""
|
| 112 |
+
### 📍 Local Area Information
|
| 113 |
+
|
| 114 |
+
- **Location**: Village Market Square
|
| 115 |
+
- **Trading Hours**: Always open for brave adventurers
|
| 116 |
+
- **Specialties**: Weapons, potions, and magical items
|
| 117 |
+
- **Payment**: Gold coins accepted
|
| 118 |
+
|
| 119 |
+
### 🗺️ Nearby Locations
|
| 120 |
+
- **Village Elder**: North of here, near the fountain
|
| 121 |
+
- **Training Grounds**: East side of village
|
| 122 |
+
- **Mystic Oracle**: In the tower to the south
|
| 123 |
+
""")
|
| 124 |
+
|
| 125 |
+
info_btn = gr.Button("📋 Get Quest Information")
|
| 126 |
+
quest_info = gr.Textbox(
|
| 127 |
+
label="Available Quests",
|
| 128 |
+
lines=5,
|
| 129 |
+
interactive=False
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
def get_quest_info():
|
| 133 |
+
return """
|
| 134 |
+
🗡️ **Available Quests:**
|
| 135 |
+
|
| 136 |
+
1. **Goblin Problem** (Level 1-3)
|
| 137 |
+
- Clear goblins from the eastern caves
|
| 138 |
+
- Reward: 100 gold + basic equipment
|
| 139 |
+
|
| 140 |
+
2. **Herb Collection** (Level 1-2)
|
| 141 |
+
- Gather 10 healing herbs from the forest
|
| 142 |
+
- Reward: 50 gold + health potion
|
| 143 |
+
|
| 144 |
+
3. **Lost Merchant** (Level 3-5)
|
| 145 |
+
- Find the missing merchant on the trade route
|
| 146 |
+
- Reward: 200 gold + rare item
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
info_btn.click(get_quest_info, outputs=quest_info)
|
| 150 |
+
|
| 151 |
+
return interface
|
| 152 |
+
|
| 153 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 154 |
+
"""Handle commands sent via private messages to this NPC."""
|
| 155 |
+
command_lower = command.lower().strip()
|
| 156 |
+
# Get player info safely
|
| 157 |
+
from ..core.game_engine import GameEngine
|
| 158 |
+
game_engine = GameEngine()
|
| 159 |
+
game_world = game_engine.get_world()
|
| 160 |
+
player = game_world.players.get(player_id)
|
| 161 |
+
if not player:
|
| 162 |
+
return "❌ Player not found!"
|
| 163 |
+
|
| 164 |
+
player_name = player.name
|
| 165 |
+
|
| 166 |
+
# Command parsing
|
| 167 |
+
if any(word in command_lower for word in ['hello', 'hi', 'greeting', 'hey']):
|
| 168 |
+
import random
|
| 169 |
+
return f"🏪 {random.choice(self.greeting_messages)} {player_name}!"
|
| 170 |
+
|
| 171 |
+
elif any(word in command_lower for word in ['shop', 'buy', 'purchase', 'item']):
|
| 172 |
+
return self._get_shop_summary()
|
| 173 |
+
|
| 174 |
+
elif any(word in command_lower for word in ['quest', 'mission', 'task']):
|
| 175 |
+
return "📜 I have information about local quests! Visit my Information tab for details."
|
| 176 |
+
|
| 177 |
+
elif any(word in command_lower for word in ['help', 'commands']):
|
| 178 |
+
return """
|
| 179 |
+
🏪 **Available Commands:**
|
| 180 |
+
- **hello/hi**: Friendly greeting
|
| 181 |
+
- **shop/buy**: View available items
|
| 182 |
+
- **quest**: Learn about available quests
|
| 183 |
+
- **help**: Show this help message
|
| 184 |
+
|
| 185 |
+
*Visit the NPC Add-ons tab for my full interface!*
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
else:
|
| 189 |
+
return f"🤔 Interesting words, {player_name}! Try 'help' to see what I can do, or visit my shop interface in the NPC Add-ons tab!"
|
| 190 |
+
|
| 191 |
+
def purchase_item(self, player_id: str, item_key: str, quantity: int) -> str:
|
| 192 |
+
"""Handle item purchase logic."""
|
| 193 |
+
if item_key not in self.npc_inventory:
|
| 194 |
+
return "❌ Item not found in inventory!"
|
| 195 |
+
|
| 196 |
+
item = self.npc_inventory[item_key]
|
| 197 |
+
total_cost = item["price"] * quantity
|
| 198 |
+
|
| 199 |
+
if item["stock"] < quantity:
|
| 200 |
+
return f"❌ Not enough stock! Only {item['stock']} available."
|
| 201 |
+
|
| 202 |
+
# Here you would check player's gold and deduct it
|
| 203 |
+
# For this example, we'll simulate the transaction
|
| 204 |
+
|
| 205 |
+
# Update inventory
|
| 206 |
+
self.npc_inventory[item_key]["stock"] -= quantity
|
| 207 |
+
|
| 208 |
+
return f"""
|
| 209 |
+
✅ **Purchase Successful!**
|
| 210 |
+
|
| 211 |
+
**Item**: {item['name']} x{quantity}
|
| 212 |
+
**Cost**: {total_cost} gold
|
| 213 |
+
**Remaining Stock**: {self.npc_inventory[item_key]['stock']}
|
| 214 |
+
|
| 215 |
+
*Thank you for your business, adventurer!*
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
def _get_shop_summary(self) -> str:
|
| 219 |
+
"""Get a summary of available shop items."""
|
| 220 |
+
summary = "🏪 **Shop Inventory:**\n\n"
|
| 221 |
+
for key, item in self.npc_inventory.items():
|
| 222 |
+
if item["stock"] > 0:
|
| 223 |
+
summary += f"• **{item['name']}** - {item['price']} gold (Stock: {item['stock']})\n"
|
| 224 |
+
|
| 225 |
+
summary += "\n*Visit my shop interface in the NPC Add-ons tab to purchase items!*"
|
| 226 |
+
return summary
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
# Global instance for auto-registration
|
| 230 |
+
example_merchant_addon = ExampleNPCAddon()
|
src/addons/fortune_teller_addon.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fortune Teller Add-on - Self-contained addon demonstrating the new auto-registration system.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import random
|
| 6 |
+
import time
|
| 7 |
+
from typing import Dict, Any, Optional
|
| 8 |
+
import gradio as gr
|
| 9 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class FortuneTellerAddon(NPCAddon):
|
| 13 |
+
"""Self-contained Fortune Teller NPC that auto-registers and positions itself."""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
super().__init__() # This triggers auto-registration
|
| 17 |
+
self.fortunes = [
|
| 18 |
+
"A great adventure awaits you beyond the eastern mountains!",
|
| 19 |
+
"Beware of strangers bearing gifts in the next full moon.",
|
| 20 |
+
"Your courage will be tested, but victory will be yours.",
|
| 21 |
+
"Gold will come to you through an unexpected friendship.",
|
| 22 |
+
"The stars suggest a powerful ally will join your quest.",
|
| 23 |
+
"A hidden treasure lies where the old oak meets the stream.",
|
| 24 |
+
"Your wisdom will grow through helping others in need.",
|
| 25 |
+
"Trust your instincts - they will guide you true.",
|
| 26 |
+
"A challenge approaches, but it will make you stronger.",
|
| 27 |
+
"The path you seek becomes clear under starlight."
|
| 28 |
+
]
|
| 29 |
+
self.player_fortunes: Dict[str, Dict] = {} # Track player fortune history
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def addon_id(self) -> str:
|
| 33 |
+
"""Unique identifier for this add-on"""
|
| 34 |
+
return "fortune_teller"
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def addon_name(self) -> str:
|
| 38 |
+
"""Display name for this add-on"""
|
| 39 |
+
return "🔮 Mystic Fortune Teller"
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def npc_config(self) -> Dict[str, Any]:
|
| 43 |
+
"""NPC configuration for auto-placement in world"""
|
| 44 |
+
return {
|
| 45 |
+
'id': 'fortune_teller',
|
| 46 |
+
'name': 'Mystic Zelda',
|
| 47 |
+
'x': 125, 'y': 75,
|
| 48 |
+
'char': '🔮',
|
| 49 |
+
'type': 'addon',
|
| 50 |
+
'description': 'Wise fortune teller who can glimpse into your future'
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def ui_tab_name(self) -> str:
|
| 55 |
+
"""UI tab name for this addon"""
|
| 56 |
+
return "🔮 Fortune Teller"
|
| 57 |
+
|
| 58 |
+
def get_interface(self) -> gr.Component:
|
| 59 |
+
"""Create the Gradio interface for fortune telling"""
|
| 60 |
+
with gr.Column() as interface:
|
| 61 |
+
gr.Markdown("""
|
| 62 |
+
## 🔮 Mystic Fortune Teller
|
| 63 |
+
|
| 64 |
+
*Peer into the mists of time and discover your destiny...*
|
| 65 |
+
|
| 66 |
+
**Services Available:**
|
| 67 |
+
- 🌟 **Daily Fortune** - Receive guidance for your journey
|
| 68 |
+
- 📜 **Fortune History** - View your past predictions
|
| 69 |
+
- 🎲 **Lucky Numbers** - Get your lucky numbers for today
|
| 70 |
+
|
| 71 |
+
*Walk near me in the game world for personalized readings!*
|
| 72 |
+
""")
|
| 73 |
+
|
| 74 |
+
with gr.Row():
|
| 75 |
+
fortune_btn = gr.Button("🔮 Get My Fortune", variant="primary", scale=2)
|
| 76 |
+
history_btn = gr.Button("📜 View History", variant="secondary", scale=1)
|
| 77 |
+
lucky_btn = gr.Button("🎲 Lucky Numbers", variant="secondary", scale=1)
|
| 78 |
+
|
| 79 |
+
fortune_output = gr.Textbox(
|
| 80 |
+
label="🌟 Your Fortune",
|
| 81 |
+
lines=6,
|
| 82 |
+
interactive=False,
|
| 83 |
+
placeholder="Click 'Get My Fortune' to reveal your destiny..."
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def get_player_fortune():
|
| 87 |
+
"""Get fortune for the active player"""
|
| 88 |
+
# In a real implementation, you'd get the actual player ID
|
| 89 |
+
# For demo purposes, we'll use a mock player
|
| 90 |
+
mock_player_id = "demo_player"
|
| 91 |
+
mock_player_name = "Brave Adventurer"
|
| 92 |
+
return self.get_daily_fortune(mock_player_id, mock_player_name)
|
| 93 |
+
|
| 94 |
+
def get_player_history():
|
| 95 |
+
"""Get fortune history for the active player"""
|
| 96 |
+
mock_player_id = "demo_player"
|
| 97 |
+
return self.get_fortune_history(mock_player_id)
|
| 98 |
+
|
| 99 |
+
def get_lucky_numbers():
|
| 100 |
+
"""Generate lucky numbers for the active player"""
|
| 101 |
+
mock_player_id = "demo_player"
|
| 102 |
+
return self.generate_lucky_numbers(mock_player_id)
|
| 103 |
+
|
| 104 |
+
# Wire up the interface
|
| 105 |
+
fortune_btn.click(get_player_fortune, outputs=[fortune_output])
|
| 106 |
+
history_btn.click(get_player_history, outputs=[fortune_output])
|
| 107 |
+
lucky_btn.click(get_lucky_numbers, outputs=[fortune_output])
|
| 108 |
+
|
| 109 |
+
return interface
|
| 110 |
+
|
| 111 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 112 |
+
"""Handle Fortune Teller commands via private messages"""
|
| 113 |
+
parts = command.strip().split(' ', 1)
|
| 114 |
+
cmd = parts[0].lower()
|
| 115 |
+
|
| 116 |
+
if cmd == "fortune":
|
| 117 |
+
# Get player name from game world
|
| 118 |
+
try:
|
| 119 |
+
from ..core.world import game_world
|
| 120 |
+
player_name = game_world.players.get(player_id, {}).get('name', 'Unknown Traveler')
|
| 121 |
+
except:
|
| 122 |
+
player_name = "Unknown Traveler"
|
| 123 |
+
|
| 124 |
+
return self.get_daily_fortune(player_id, player_name)
|
| 125 |
+
|
| 126 |
+
elif cmd == "history":
|
| 127 |
+
return self.get_fortune_history(player_id)
|
| 128 |
+
|
| 129 |
+
elif cmd == "lucky":
|
| 130 |
+
return self.generate_lucky_numbers(player_id)
|
| 131 |
+
|
| 132 |
+
elif cmd == "help":
|
| 133 |
+
return """🔮 **Fortune Teller Commands:**
|
| 134 |
+
|
| 135 |
+
**fortune** - Get your daily fortune reading
|
| 136 |
+
**history** - View your fortune history
|
| 137 |
+
**lucky** - Generate lucky numbers for today
|
| 138 |
+
**help** - Show this help
|
| 139 |
+
|
| 140 |
+
🌟 **Example:** Send me "fortune" to receive mystical guidance!"""
|
| 141 |
+
|
| 142 |
+
else:
|
| 143 |
+
# If no specific command, treat as fortune request
|
| 144 |
+
try:
|
| 145 |
+
from ..core.world import game_world
|
| 146 |
+
player_name = game_world.players.get(player_id, {}).get('name', 'Unknown Traveler')
|
| 147 |
+
except:
|
| 148 |
+
player_name = "Unknown Traveler"
|
| 149 |
+
|
| 150 |
+
return self.get_daily_fortune(player_id, player_name)
|
| 151 |
+
|
| 152 |
+
def get_daily_fortune(self, player_id: str, player_name: str) -> str:
|
| 153 |
+
"""Get or generate daily fortune for a player"""
|
| 154 |
+
today = time.strftime("%Y-%m-%d")
|
| 155 |
+
|
| 156 |
+
# Check if player already has a fortune for today
|
| 157 |
+
if player_id in self.player_fortunes:
|
| 158 |
+
player_data = self.player_fortunes[player_id]
|
| 159 |
+
if player_data.get('last_fortune_date') == today:
|
| 160 |
+
return (f"**{player_name}**, I have already revealed your destiny for today:\n\n"
|
| 161 |
+
f"✨ *{player_data['last_fortune']}*\n\n"
|
| 162 |
+
f"Return tomorrow for new guidance...")
|
| 163 |
+
|
| 164 |
+
# Generate new fortune
|
| 165 |
+
fortune = random.choice(self.fortunes)
|
| 166 |
+
|
| 167 |
+
# Store fortune
|
| 168 |
+
if player_id not in self.player_fortunes:
|
| 169 |
+
self.player_fortunes[player_id] = {'history': []}
|
| 170 |
+
|
| 171 |
+
self.player_fortunes[player_id].update({
|
| 172 |
+
'last_fortune': fortune,
|
| 173 |
+
'last_fortune_date': today
|
| 174 |
+
})
|
| 175 |
+
|
| 176 |
+
self.player_fortunes[player_id]['history'].append({
|
| 177 |
+
'date': today,
|
| 178 |
+
'fortune': fortune,
|
| 179 |
+
'timestamp': time.time()
|
| 180 |
+
})
|
| 181 |
+
|
| 182 |
+
# Keep only last 10 fortunes
|
| 183 |
+
if len(self.player_fortunes[player_id]['history']) > 10:
|
| 184 |
+
self.player_fortunes[player_id]['history'] = self.player_fortunes[player_id]['history'][-10:]
|
| 185 |
+
|
| 186 |
+
return (f"🔮 **Greetings, {player_name}!**\n\n"
|
| 187 |
+
f"The crystal ball swirls with mystical energy...\n\n"
|
| 188 |
+
f"✨ *{fortune}*\n\n"
|
| 189 |
+
f"💫 This fortune is yours until the dawn of a new day.")
|
| 190 |
+
|
| 191 |
+
def get_fortune_history(self, player_id: str) -> str:
|
| 192 |
+
"""Get fortune history for a player"""
|
| 193 |
+
if player_id not in self.player_fortunes or not self.player_fortunes[player_id].get('history'):
|
| 194 |
+
return "📜 **Fortune History**\n\nYou have no fortune history yet. Request your first fortune to begin building your mystical record!"
|
| 195 |
+
|
| 196 |
+
history = self.player_fortunes[player_id]['history']
|
| 197 |
+
result = "📜 **Your Fortune History**\n\n"
|
| 198 |
+
|
| 199 |
+
for entry in reversed(history[-5:]): # Show last 5 fortunes
|
| 200 |
+
result += f"**{entry['date']}:** {entry['fortune']}\n\n"
|
| 201 |
+
|
| 202 |
+
if len(history) > 5:
|
| 203 |
+
result += f"*...and {len(history) - 5} more in your mystical archives*"
|
| 204 |
+
|
| 205 |
+
return result
|
| 206 |
+
|
| 207 |
+
def generate_lucky_numbers(self, player_id: str) -> str:
|
| 208 |
+
"""Generate lucky numbers for a player"""
|
| 209 |
+
# Use player ID as seed for consistent daily numbers
|
| 210 |
+
today = time.strftime("%Y-%m-%d")
|
| 211 |
+
seed = hash(f"{player_id}-{today}") % 2**31
|
| 212 |
+
random.seed(seed)
|
| 213 |
+
|
| 214 |
+
# Generate different types of lucky numbers
|
| 215 |
+
lottery_numbers = sorted([random.randint(1, 49) for _ in range(6)])
|
| 216 |
+
magic_number = random.randint(1, 100)
|
| 217 |
+
lucky_color_index = random.randint(0, 6)
|
| 218 |
+
lucky_colors = ["Red", "Blue", "Green", "Gold", "Silver", "Purple", "Orange"]
|
| 219 |
+
lucky_color = lucky_colors[lucky_color_index]
|
| 220 |
+
|
| 221 |
+
# Reset random seed
|
| 222 |
+
random.seed()
|
| 223 |
+
|
| 224 |
+
return (f"🎲 **Lucky Numbers for Today**\n\n"
|
| 225 |
+
f"🎰 **Lottery Numbers:** {', '.join(map(str, lottery_numbers))}\n"
|
| 226 |
+
f"✨ **Magic Number:** {magic_number}\n"
|
| 227 |
+
f"🌈 **Lucky Color:** {lucky_color}\n\n"
|
| 228 |
+
f"💫 *May fortune smile upon you this day!*")
|
| 229 |
+
|
| 230 |
+
def on_startup(self):
|
| 231 |
+
"""Called when the addon is loaded during game startup"""
|
| 232 |
+
print(f"[{self.addon_id}] Fortune Teller Mystic Zelda has awakened and is ready to divine your future!")
|
| 233 |
+
|
| 234 |
+
def on_shutdown(self):
|
| 235 |
+
"""Called when the addon is unloaded during game shutdown"""
|
| 236 |
+
print(f"[{self.addon_id}] The crystal ball dims as Mystic Zelda rests...")
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# Auto-instantiate the addon for registration
|
| 240 |
+
fortune_teller_addon = FortuneTellerAddon()
|
src/addons/magic_shop_addon.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Magic Shop Add-on - Self-contained NPC with position, registration, and UI.
|
| 3 |
+
|
| 4 |
+
This demonstrates the weather addon pattern where everything is contained in one file:
|
| 5 |
+
- NPC configuration with position
|
| 6 |
+
- Command handling
|
| 7 |
+
- UI interface
|
| 8 |
+
- Auto-registration through global instance
|
| 9 |
+
|
| 10 |
+
No manual edits to other files required!
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import time
|
| 14 |
+
import gradio as gr
|
| 15 |
+
from typing import Dict, Any, List, Optional
|
| 16 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class MagicShopAddon(NPCAddon):
|
| 20 |
+
"""Self-contained magic shop NPC that handles all aspects in one file."""
|
| 21 |
+
|
| 22 |
+
def __init__(self):
|
| 23 |
+
super().__init__() # This auto-registers the addon!
|
| 24 |
+
|
| 25 |
+
# Shop inventory
|
| 26 |
+
self.spell_inventory = {
|
| 27 |
+
"healing_spell": {
|
| 28 |
+
"name": "🌟 Basic Healing Spell",
|
| 29 |
+
"price": 100,
|
| 30 |
+
"stock": 8,
|
| 31 |
+
"description": "Restores 50 health points",
|
| 32 |
+
"level_required": 1
|
| 33 |
+
},
|
| 34 |
+
"fireball_spell": {
|
| 35 |
+
"name": "🔥 Fireball Spell",
|
| 36 |
+
"price": 250,
|
| 37 |
+
"stock": 5,
|
| 38 |
+
"description": "Deals fire damage to enemies",
|
| 39 |
+
"level_required": 3
|
| 40 |
+
},
|
| 41 |
+
"teleport_scroll": {
|
| 42 |
+
"name": "✨ Teleportation Scroll",
|
| 43 |
+
"price": 500,
|
| 44 |
+
"stock": 3,
|
| 45 |
+
"description": "Instantly travel to marked locations",
|
| 46 |
+
"level_required": 5
|
| 47 |
+
},
|
| 48 |
+
"mana_potion": {
|
| 49 |
+
"name": "🧪 Greater Mana Potion",
|
| 50 |
+
"price": 75,
|
| 51 |
+
"stock": 12,
|
| 52 |
+
"description": "Restores 100 mana points",
|
| 53 |
+
"level_required": 1
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Shop stats
|
| 58 |
+
self.total_sales = 0
|
| 59 |
+
self.customers_served = []
|
| 60 |
+
self.last_restock = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def addon_id(self) -> str:
|
| 64 |
+
"""Unique identifier for this add-on"""
|
| 65 |
+
return "magic_shop"
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def addon_name(self) -> str:
|
| 69 |
+
"""Display name for this add-on"""
|
| 70 |
+
return "🪄 Enchanted Magic Shop"
|
| 71 |
+
|
| 72 |
+
@property
|
| 73 |
+
def npc_config(self) -> Dict[str, Any]:
|
| 74 |
+
"""NPC configuration for auto-placement in world"""
|
| 75 |
+
return {
|
| 76 |
+
'id': 'magic_shop_keeper',
|
| 77 |
+
'name': '🧙♀️ Mystical Mage Elara',
|
| 78 |
+
'x': 300, 'y': 150, # Position in the game world
|
| 79 |
+
'char': '🧙♀️',
|
| 80 |
+
'type': 'magic_shop',
|
| 81 |
+
'description': 'Wise mage selling powerful spells and magical items'
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
@property
|
| 85 |
+
def ui_tab_name(self) -> str:
|
| 86 |
+
"""UI tab name for this addon"""
|
| 87 |
+
return "🪄 Magic Shop"
|
| 88 |
+
|
| 89 |
+
def get_interface(self) -> gr.Component:
|
| 90 |
+
"""Create the Gradio interface for the magic shop"""
|
| 91 |
+
with gr.Column() as interface:
|
| 92 |
+
# Header with shop info
|
| 93 |
+
gr.Markdown(f"""
|
| 94 |
+
## 🪄 {self.addon_name}
|
| 95 |
+
|
| 96 |
+
*Greetings, brave adventurer! I am Elara, keeper of ancient magics.*
|
| 97 |
+
|
| 98 |
+
**📍 Shop Location:** ({self.npc_config['x']}, {self.npc_config['y']})
|
| 99 |
+
**👥 Customers Served:** {len(self.customers_served)}
|
| 100 |
+
**💰 Total Sales:** {self.total_sales} gold
|
| 101 |
+
**📦 Last Restock:** {self.last_restock}
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
""")
|
| 105 |
+
|
| 106 |
+
with gr.Tabs():
|
| 107 |
+
# Shop inventory tab
|
| 108 |
+
with gr.Tab("🛒 Spell Catalog"):
|
| 109 |
+
gr.Markdown("### ✨ Available Magical Items")
|
| 110 |
+
|
| 111 |
+
# Create inventory display
|
| 112 |
+
inventory_data = []
|
| 113 |
+
for item_id, item in self.spell_inventory.items():
|
| 114 |
+
inventory_data.append({
|
| 115 |
+
"Item": item["name"],
|
| 116 |
+
"Price": f"{item['price']} gold",
|
| 117 |
+
"Stock": item["stock"],
|
| 118 |
+
"Level Req.": item["level_required"],
|
| 119 |
+
"Description": item["description"]
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
inventory_display = gr.DataFrame(
|
| 123 |
+
value=inventory_data,
|
| 124 |
+
label="📜 Spell Inventory",
|
| 125 |
+
interactive=False
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# Purchase interface
|
| 129 |
+
with gr.Row():
|
| 130 |
+
item_dropdown = gr.Dropdown(
|
| 131 |
+
label="🎯 Select Spell/Item",
|
| 132 |
+
choices=[(item["name"], key) for key, item in self.spell_inventory.items()],
|
| 133 |
+
value=None
|
| 134 |
+
)
|
| 135 |
+
quantity_input = gr.Number(
|
| 136 |
+
label="📊 Quantity",
|
| 137 |
+
value=1,
|
| 138 |
+
minimum=1,
|
| 139 |
+
maximum=10
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
purchase_btn = gr.Button("💰 Purchase", variant="primary", size="lg")
|
| 143 |
+
|
| 144 |
+
purchase_result = gr.Textbox(
|
| 145 |
+
label="🧾 Transaction Result",
|
| 146 |
+
lines=4,
|
| 147 |
+
interactive=False
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
def handle_purchase(item_key, qty):
|
| 151 |
+
if not item_key:
|
| 152 |
+
return "❌ Please select an item first!"
|
| 153 |
+
|
| 154 |
+
if item_key not in self.spell_inventory:
|
| 155 |
+
return "❌ Item not found in inventory!"
|
| 156 |
+
|
| 157 |
+
item = self.spell_inventory[item_key]
|
| 158 |
+
|
| 159 |
+
if qty > item["stock"]:
|
| 160 |
+
return f"❌ Sorry! Only {item['stock']} {item['name']} in stock."
|
| 161 |
+
|
| 162 |
+
total_cost = item["price"] * qty
|
| 163 |
+
|
| 164 |
+
# Simulate purchase (in real game, check player gold)
|
| 165 |
+
self.spell_inventory[item_key]["stock"] -= qty
|
| 166 |
+
self.total_sales += total_cost
|
| 167 |
+
|
| 168 |
+
return f"""✅ **Purchase Successful!**
|
| 169 |
+
|
| 170 |
+
**Item:** {item['name']}
|
| 171 |
+
**Quantity:** {qty}
|
| 172 |
+
**Unit Price:** {item['price']} gold
|
| 173 |
+
**Total Cost:** {total_cost} gold
|
| 174 |
+
|
| 175 |
+
**Remaining Stock:** {self.spell_inventory[item_key]['stock']}
|
| 176 |
+
|
| 177 |
+
*Thank you for your patronage! May these enchantments serve you well!*"""
|
| 178 |
+
|
| 179 |
+
purchase_btn.click(
|
| 180 |
+
handle_purchase,
|
| 181 |
+
inputs=[item_dropdown, quantity_input],
|
| 182 |
+
outputs=[purchase_result]
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Shop information tab
|
| 186 |
+
with gr.Tab("ℹ️ Shop Info"):
|
| 187 |
+
gr.Markdown("""
|
| 188 |
+
### 🏪 About Elara's Magic Shop
|
| 189 |
+
|
| 190 |
+
Welcome to the most enchanted emporium in the realm! I've been practicing
|
| 191 |
+
the magical arts for over 200 years, and my shop contains only the finest
|
| 192 |
+
spells and potions.
|
| 193 |
+
|
| 194 |
+
#### 📋 Services Offered:
|
| 195 |
+
- 🔮 **Spell Sales** - Powerful magic for every adventurer
|
| 196 |
+
- 🧪 **Potion Brewing** - Restore health and mana
|
| 197 |
+
- 📜 **Scroll Creation** - Portable magic for travel
|
| 198 |
+
- 🎓 **Magic Consultation** - Learn about spell mechanics
|
| 199 |
+
|
| 200 |
+
#### 🕐 Shop Hours:
|
| 201 |
+
- **Open:** Always available for brave souls
|
| 202 |
+
- **Restocking:** Weekly shipments from the Mage Tower
|
| 203 |
+
- **Special Orders:** Available for rare enchantments
|
| 204 |
+
|
| 205 |
+
#### 💡 Pro Tips:
|
| 206 |
+
- Higher level spells require more experience
|
| 207 |
+
- Potions stack in your inventory
|
| 208 |
+
- Scrolls are one-time use items
|
| 209 |
+
- Ask about bulk discounts for guilds!
|
| 210 |
+
""")
|
| 211 |
+
|
| 212 |
+
# Current shop stats
|
| 213 |
+
shop_stats = gr.JSON(
|
| 214 |
+
label="📊 Shop Statistics",
|
| 215 |
+
value={
|
| 216 |
+
"total_items_in_stock": sum(item["stock"] for item in self.spell_inventory.values()),
|
| 217 |
+
"most_expensive_item": max(self.spell_inventory.values(), key=lambda x: x["price"])["name"],
|
| 218 |
+
"cheapest_item": min(self.spell_inventory.values(), key=lambda x: x["price"])["name"],
|
| 219 |
+
"customers_today": len(self.customers_served),
|
| 220 |
+
"shop_established": "Year 1247 of the Third Age"
|
| 221 |
+
}
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Commands help tab
|
| 225 |
+
with gr.Tab("💬 Commands"):
|
| 226 |
+
gr.Markdown("""
|
| 227 |
+
### 🎯 Private Message Commands
|
| 228 |
+
|
| 229 |
+
Send these commands via private messages to Mystical Mage Elara:
|
| 230 |
+
|
| 231 |
+
#### 🛒 Shopping Commands:
|
| 232 |
+
- `shop` - View available items and prices
|
| 233 |
+
- `buy <item_name> [quantity]` - Purchase magical items
|
| 234 |
+
- `stock` - Check current inventory levels
|
| 235 |
+
|
| 236 |
+
#### ℹ️ Information Commands:
|
| 237 |
+
- `info` - Get shop information and location
|
| 238 |
+
- `hours` - Check shop hours and services
|
| 239 |
+
- `help` - Show all available commands
|
| 240 |
+
|
| 241 |
+
#### 📊 Status Commands:
|
| 242 |
+
- `stats` - View shop statistics
|
| 243 |
+
- `history` - Check your purchase history
|
| 244 |
+
|
| 245 |
+
#### 🎯 Example Usage:
|
| 246 |
+
```
|
| 247 |
+
shop # See all items
|
| 248 |
+
buy healing_spell 2 # Buy 2 healing spells
|
| 249 |
+
info # Get shop details
|
| 250 |
+
stats # View shop statistics
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
*Walk near my shop in the game world to unlock the full experience!*
|
| 254 |
+
""")
|
| 255 |
+
|
| 256 |
+
return interface
|
| 257 |
+
|
| 258 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 259 |
+
"""Handle magic shop commands via private messages"""
|
| 260 |
+
if player_id not in self.customers_served:
|
| 261 |
+
self.customers_served.append(player_id)
|
| 262 |
+
|
| 263 |
+
parts = command.strip().split()
|
| 264 |
+
if not parts:
|
| 265 |
+
return self._show_help()
|
| 266 |
+
|
| 267 |
+
cmd = parts[0].lower()
|
| 268 |
+
|
| 269 |
+
if cmd == "shop":
|
| 270 |
+
return self._show_shop_catalog()
|
| 271 |
+
|
| 272 |
+
elif cmd == "buy":
|
| 273 |
+
if len(parts) < 2:
|
| 274 |
+
return "❓ Usage: `buy <item_name> [quantity]`\nExample: `buy healing_spell 2`"
|
| 275 |
+
|
| 276 |
+
item_name = parts[1].lower()
|
| 277 |
+
quantity = 1
|
| 278 |
+
if len(parts) > 2:
|
| 279 |
+
try:
|
| 280 |
+
quantity = int(parts[2])
|
| 281 |
+
if quantity < 1:
|
| 282 |
+
return "❌ Quantity must be at least 1!"
|
| 283 |
+
except ValueError:
|
| 284 |
+
return "❌ Invalid quantity! Please use a number."
|
| 285 |
+
|
| 286 |
+
return self._handle_purchase(player_id, item_name, quantity)
|
| 287 |
+
|
| 288 |
+
elif cmd == "stock":
|
| 289 |
+
return self._show_stock_levels()
|
| 290 |
+
|
| 291 |
+
elif cmd == "info":
|
| 292 |
+
return self._show_shop_info()
|
| 293 |
+
|
| 294 |
+
elif cmd == "hours":
|
| 295 |
+
return self._show_shop_hours()
|
| 296 |
+
|
| 297 |
+
elif cmd == "stats":
|
| 298 |
+
return self._show_shop_stats()
|
| 299 |
+
|
| 300 |
+
elif cmd == "history":
|
| 301 |
+
return self._show_customer_history(player_id)
|
| 302 |
+
|
| 303 |
+
elif cmd == "help":
|
| 304 |
+
return self._show_help()
|
| 305 |
+
|
| 306 |
+
else:
|
| 307 |
+
return f"❓ Unknown command: `{cmd}`\n\nTry `help` to see available commands."
|
| 308 |
+
|
| 309 |
+
def _show_shop_catalog(self) -> str:
|
| 310 |
+
"""Display the complete shop catalog"""
|
| 311 |
+
catalog = "🪄 **Elara's Magic Shop - Spell Catalog**\n\n"
|
| 312 |
+
|
| 313 |
+
for item_id, item in self.spell_inventory.items():
|
| 314 |
+
stock_status = "✅ In Stock" if item["stock"] > 0 else "❌ Sold Out"
|
| 315 |
+
catalog += f"**{item['name']}**\n"
|
| 316 |
+
catalog += f"• Price: {item['price']} gold\n"
|
| 317 |
+
catalog += f"• Stock: {item['stock']} ({stock_status})\n"
|
| 318 |
+
catalog += f"• Level Required: {item['level_required']}\n"
|
| 319 |
+
catalog += f"• {item['description']}\n\n"
|
| 320 |
+
|
| 321 |
+
catalog += f"💰 Total Sales Today: {self.total_sales} gold\n"
|
| 322 |
+
catalog += f"👥 Customers Served: {len(self.customers_served)}\n\n"
|
| 323 |
+
catalog += "*To purchase: `buy <item_name> [quantity]`*"
|
| 324 |
+
|
| 325 |
+
return catalog
|
| 326 |
+
|
| 327 |
+
def _handle_purchase(self, player_id: str, item_name: str, quantity: int) -> str:
|
| 328 |
+
"""Handle item purchase logic"""
|
| 329 |
+
# Find item by name or ID
|
| 330 |
+
item_key = None
|
| 331 |
+
for key, item in self.spell_inventory.items():
|
| 332 |
+
if key == item_name or item["name"].lower().replace(" ", "_") == item_name:
|
| 333 |
+
item_key = key
|
| 334 |
+
break
|
| 335 |
+
|
| 336 |
+
if not item_key:
|
| 337 |
+
available_items = ", ".join(self.spell_inventory.keys())
|
| 338 |
+
return f"❌ Item '{item_name}' not found!\n\nAvailable items: {available_items}\n\nTry: `shop` to see the full catalog"
|
| 339 |
+
|
| 340 |
+
item = self.spell_inventory[item_key]
|
| 341 |
+
|
| 342 |
+
if item["stock"] < quantity:
|
| 343 |
+
return f"❌ Sorry! Only {item['stock']} {item['name']} available.\n\nTry a smaller quantity or check back after restock."
|
| 344 |
+
|
| 345 |
+
total_cost = item["price"] * quantity
|
| 346 |
+
|
| 347 |
+
# Simulate purchase (in real game, check/deduct player gold)
|
| 348 |
+
self.spell_inventory[item_key]["stock"] -= quantity
|
| 349 |
+
self.total_sales += total_cost
|
| 350 |
+
|
| 351 |
+
return f"""✅ **Purchase Successful!**
|
| 352 |
+
|
| 353 |
+
🎯 **Item:** {item['name']}
|
| 354 |
+
📊 **Quantity:** {quantity}
|
| 355 |
+
💰 **Total Cost:** {total_cost} gold
|
| 356 |
+
📦 **Remaining Stock:** {self.spell_inventory[item_key]['stock']}
|
| 357 |
+
|
| 358 |
+
*Your magical items have been added to your inventory!*
|
| 359 |
+
*May they serve you well on your adventures!*
|
| 360 |
+
|
| 361 |
+
Use `shop` to browse more items or `stats` to see shop statistics."""
|
| 362 |
+
|
| 363 |
+
def _show_stock_levels(self) -> str:
|
| 364 |
+
"""Show current stock levels"""
|
| 365 |
+
stock_report = "📦 **Current Stock Levels**\n\n"
|
| 366 |
+
|
| 367 |
+
for item in self.spell_inventory.values():
|
| 368 |
+
status_emoji = "🟢" if item["stock"] > 5 else "🟡" if item["stock"] > 0 else "🔴"
|
| 369 |
+
stock_report += f"{status_emoji} **{item['name']}**: {item['stock']} available\n"
|
| 370 |
+
|
| 371 |
+
stock_report += f"\n📅 **Last Restock:** {self.last_restock}"
|
| 372 |
+
stock_report += "\n💡 *Low stock items will be restocked weekly*"
|
| 373 |
+
|
| 374 |
+
return stock_report
|
| 375 |
+
|
| 376 |
+
def _show_shop_info(self) -> str:
|
| 377 |
+
"""Show detailed shop information"""
|
| 378 |
+
return f"""🏪 **Elara's Magic Shop Information**
|
| 379 |
+
|
| 380 |
+
🧙♀️ **Shopkeeper:** Mystical Mage Elara
|
| 381 |
+
📍 **Location:** ({self.npc_config['x']}, {self.npc_config['y']})
|
| 382 |
+
🏛️ **Shop Type:** Magical Items & Spell Emporium
|
| 383 |
+
|
| 384 |
+
**About the Shop:**
|
| 385 |
+
I've been practicing magic for over 200 years and offer only the finest
|
| 386 |
+
enchantments, potions, and scrolls. My shop serves adventurers of all
|
| 387 |
+
levels, from novice spell-casters to master wizards.
|
| 388 |
+
|
| 389 |
+
**Specialties:**
|
| 390 |
+
• 🌟 Healing and restoration magic
|
| 391 |
+
• 🔥 Offensive spells and scrolls
|
| 392 |
+
• 🧪 Mana and health potions
|
| 393 |
+
• ✨ Utility and travel magic
|
| 394 |
+
|
| 395 |
+
**Shop Guarantee:**
|
| 396 |
+
All items come with Elara's personal enchantment guarantee!
|
| 397 |
+
If a spell fails due to magical defect, return for full refund.
|
| 398 |
+
|
| 399 |
+
*Walk near my shop to access the full interface and advanced features!*"""
|
| 400 |
+
|
| 401 |
+
def _show_shop_hours(self) -> str:
|
| 402 |
+
"""Show shop hours and services"""
|
| 403 |
+
return """🕐 **Shop Hours & Services**
|
| 404 |
+
|
| 405 |
+
⏰ **Operating Hours:**
|
| 406 |
+
• Always open for adventurers in need
|
| 407 |
+
• Emergency magical supplies available 24/7
|
| 408 |
+
• Personal consultations by appointment
|
| 409 |
+
|
| 410 |
+
📦 **Restocking Schedule:**
|
| 411 |
+
• Weekly shipments from the Mage Tower
|
| 412 |
+
• Special orders processed every 3 days
|
| 413 |
+
• Rare items available on request
|
| 414 |
+
|
| 415 |
+
🎓 **Additional Services:**
|
| 416 |
+
• Spell identification and appraisal
|
| 417 |
+
• Magic item consultation
|
| 418 |
+
• Bulk orders for guilds (discount rates)
|
| 419 |
+
• Custom enchantment requests
|
| 420 |
+
|
| 421 |
+
💡 **Current Status:** 🟢 Open and fully stocked!"""
|
| 422 |
+
|
| 423 |
+
def _show_shop_stats(self) -> str:
|
| 424 |
+
"""Show shop statistics"""
|
| 425 |
+
total_items = sum(item["stock"] for item in self.spell_inventory.values())
|
| 426 |
+
most_expensive = max(self.spell_inventory.values(), key=lambda x: x["price"])
|
| 427 |
+
cheapest = min(self.spell_inventory.values(), key=lambda x: x["price"])
|
| 428 |
+
|
| 429 |
+
return f"""📊 **Shop Statistics**
|
| 430 |
+
|
| 431 |
+
💰 **Sales Data:**
|
| 432 |
+
• Total Sales: {self.total_sales} gold
|
| 433 |
+
• Customers Served: {len(self.customers_served)}
|
| 434 |
+
• Average Sale: {self.total_sales // max(len(self.customers_served), 1)} gold
|
| 435 |
+
|
| 436 |
+
📦 **Inventory:**
|
| 437 |
+
• Total Items in Stock: {total_items}
|
| 438 |
+
• Most Expensive: {most_expensive['name']} ({most_expensive['price']} gold)
|
| 439 |
+
• Most Affordable: {cheapest['name']} ({cheapest['price']} gold)
|
| 440 |
+
|
| 441 |
+
🏆 **Shop Achievements:**
|
| 442 |
+
• Established: Year 1247 of the Third Age
|
| 443 |
+
• Reputation: ⭐⭐⭐⭐⭐ (5/5 stars)
|
| 444 |
+
• Mage Guild Certified: ✅ Premium Vendor
|
| 445 |
+
|
| 446 |
+
*Thank you for supporting local magical businesses!*"""
|
| 447 |
+
|
| 448 |
+
def _show_customer_history(self, player_id: str) -> str:
|
| 449 |
+
"""Show customer interaction history"""
|
| 450 |
+
return f"""👤 **Customer History for {player_id}**
|
| 451 |
+
|
| 452 |
+
📋 **Account Status:** Valued Customer
|
| 453 |
+
🎯 **First Visit:** Today (Welcome!)
|
| 454 |
+
💰 **Total Purchases:** Check your inventory for items
|
| 455 |
+
🏅 **Customer Level:** Novice Adventurer
|
| 456 |
+
|
| 457 |
+
**Recommended Items:**
|
| 458 |
+
• For beginners: Healing Spell + Mana Potion
|
| 459 |
+
• For adventurers: Fireball Spell
|
| 460 |
+
• For experts: Teleportation Scroll
|
| 461 |
+
|
| 462 |
+
💡 **Next Purchase Suggestion:**
|
| 463 |
+
Based on your level, I recommend starting with healing magic!
|
| 464 |
+
|
| 465 |
+
*Continue shopping to unlock loyalty rewards and discounts!*"""
|
| 466 |
+
|
| 467 |
+
def _show_help(self) -> str:
|
| 468 |
+
"""Show all available commands"""
|
| 469 |
+
return """🪄 **Elara's Magic Shop - Command Help**
|
| 470 |
+
|
| 471 |
+
**🛒 Shopping Commands:**
|
| 472 |
+
• `shop` - Browse the complete spell catalog
|
| 473 |
+
• `buy <item> [qty]` - Purchase magical items
|
| 474 |
+
• `stock` - Check current inventory levels
|
| 475 |
+
|
| 476 |
+
**ℹ️ Information Commands:**
|
| 477 |
+
• `info` - Shop location and details
|
| 478 |
+
• `hours` - Operating hours and services
|
| 479 |
+
• `stats` - View shop statistics
|
| 480 |
+
• `history` - Your customer history
|
| 481 |
+
|
| 482 |
+
**💡 Examples:**
|
| 483 |
+
• `shop` - See all available spells
|
| 484 |
+
• `buy healing_spell 2` - Buy 2 healing spells
|
| 485 |
+
• `buy mana_potion` - Buy 1 mana potion
|
| 486 |
+
• `stock` - Check what's available
|
| 487 |
+
|
| 488 |
+
**🎯 Item Names:**
|
| 489 |
+
• `healing_spell` - Basic healing magic
|
| 490 |
+
• `fireball_spell` - Fire damage spell
|
| 491 |
+
• `teleport_scroll` - Travel magic
|
| 492 |
+
• `mana_potion` - Mana restoration
|
| 493 |
+
|
| 494 |
+
*Visit my shop interface in the NPC Add-ons tab for the full experience!*
|
| 495 |
+
*Walk near my location at ({self.npc_config['x']}, {self.npc_config['y']}) in the game world!*"""
|
| 496 |
+
|
| 497 |
+
def on_startup(self):
|
| 498 |
+
"""Called when the addon is loaded during game startup"""
|
| 499 |
+
print(f"[{self.addon_id}] Elara's Magic Shop is now open for business!")
|
| 500 |
+
print(f"[{self.addon_id}] Located at ({self.npc_config['x']}, {self.npc_config['y']}) with {len(self.spell_inventory)} magical items")
|
| 501 |
+
|
| 502 |
+
def on_shutdown(self):
|
| 503 |
+
"""Called when the addon is unloaded during game shutdown"""
|
| 504 |
+
print(f"[{self.addon_id}] Elara's Magic Shop is closing... Total sales: {self.total_sales} gold")
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
# Global instance - this triggers auto-registration!
|
| 508 |
+
# Just like weather_oracle_service at the bottom of weather_oracle_addon.py
|
| 509 |
+
magic_shop_addon = MagicShopAddon()
|
src/addons/read2burn_addon.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Read2Burn Mailbox Add-on - Self-destructing secure messaging system.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import time
|
| 6 |
+
import random
|
| 7 |
+
import string
|
| 8 |
+
from typing import Dict, List, Tuple
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from abc import ABC, abstractmethod
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class Read2BurnMessage:
|
| 18 |
+
"""Data class for Read2Burn messages."""
|
| 19 |
+
id: str
|
| 20 |
+
creator_id: str
|
| 21 |
+
content: str
|
| 22 |
+
created_at: float
|
| 23 |
+
expires_at: float
|
| 24 |
+
reads_left: int
|
| 25 |
+
burned: bool = False
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class IRead2BurnService(ABC):
|
| 29 |
+
"""Interface for Read2Burn service operations."""
|
| 30 |
+
|
| 31 |
+
@abstractmethod
|
| 32 |
+
def create_message(self, creator_id: str, content: str) -> str:
|
| 33 |
+
"""Create a new self-destructing message."""
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
@abstractmethod
|
| 37 |
+
def read_message(self, reader_id: str, message_id: str) -> str:
|
| 38 |
+
"""Read and potentially burn a message."""
|
| 39 |
+
pass
|
| 40 |
+
|
| 41 |
+
@abstractmethod
|
| 42 |
+
def list_player_messages(self, player_id: str) -> str:
|
| 43 |
+
"""List messages created by a player."""
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class Read2BurnService(IRead2BurnService, NPCAddon):
|
| 48 |
+
"""Service for managing Read2Burn secure messaging."""
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
super().__init__()
|
| 52 |
+
self.messages: Dict[str, Read2BurnMessage] = {}
|
| 53 |
+
self.access_log: List[Dict] = []
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def addon_id(self) -> str:
|
| 57 |
+
"""Unique identifier for this add-on"""
|
| 58 |
+
return "read2burn_mailbox"
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def addon_name(self) -> str:
|
| 62 |
+
"""Display name for this add-on"""
|
| 63 |
+
return "Read2Burn Secure Mailbox"
|
| 64 |
+
|
| 65 |
+
def get_interface(self) -> gr.Component:
|
| 66 |
+
"""Return Gradio interface for this add-on"""
|
| 67 |
+
# This will be implemented later for the UI
|
| 68 |
+
return gr.Markdown("📧 **Read2Burn Secure Mailbox**\n\nUse private messages to the read2burn NPC to manage secure messages.")
|
| 69 |
+
|
| 70 |
+
def generate_message_id(self) -> str:
|
| 71 |
+
"""Generate a unique message ID."""
|
| 72 |
+
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
| 73 |
+
|
| 74 |
+
def create_message(self, creator_id: str, content: str) -> str:
|
| 75 |
+
"""Create a new self-destructing message."""
|
| 76 |
+
message_id = self.generate_message_id()
|
| 77 |
+
|
| 78 |
+
message = Read2BurnMessage(
|
| 79 |
+
id=message_id,
|
| 80 |
+
creator_id=creator_id,
|
| 81 |
+
content=content, # In production, encrypt this
|
| 82 |
+
created_at=time.time(),
|
| 83 |
+
expires_at=time.time() + (24 * 3600), # 24 hours
|
| 84 |
+
reads_left=1,
|
| 85 |
+
burned=False
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
self.messages[message_id] = message
|
| 89 |
+
|
| 90 |
+
self.access_log.append({
|
| 91 |
+
'action': 'create',
|
| 92 |
+
'message_id': message_id,
|
| 93 |
+
'player_id': creator_id,
|
| 94 |
+
'timestamp': time.time()
|
| 95 |
+
})
|
| 96 |
+
|
| 97 |
+
return f"✅ **Message Created Successfully!**\n\n📝 **Message ID:** `{message_id}`\n🔗 Share this ID with the recipient\n⏰ Expires in 24 hours\n🔥 Burns after 1 read"
|
| 98 |
+
|
| 99 |
+
def read_message(self, reader_id: str, message_id: str) -> str:
|
| 100 |
+
"""Read and burn a message."""
|
| 101 |
+
if message_id not in self.messages:
|
| 102 |
+
return "❌ Message not found or already burned"
|
| 103 |
+
|
| 104 |
+
message = self.messages[message_id]
|
| 105 |
+
|
| 106 |
+
# Check expiry
|
| 107 |
+
if time.time() > message.expires_at:
|
| 108 |
+
del self.messages[message_id]
|
| 109 |
+
return "❌ Message expired and has been burned"
|
| 110 |
+
|
| 111 |
+
# Check if already burned
|
| 112 |
+
if message.burned or message.reads_left <= 0:
|
| 113 |
+
del self.messages[message_id]
|
| 114 |
+
return "❌ Message has already been burned"
|
| 115 |
+
|
| 116 |
+
# Read the message
|
| 117 |
+
content = message.content
|
| 118 |
+
message.reads_left -= 1
|
| 119 |
+
|
| 120 |
+
self.access_log.append({
|
| 121 |
+
'action': 'read',
|
| 122 |
+
'message_id': message_id,
|
| 123 |
+
'player_id': reader_id,
|
| 124 |
+
'timestamp': time.time()
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
# Burn the message after reading
|
| 128 |
+
if message.reads_left <= 0:
|
| 129 |
+
message.burned = True
|
| 130 |
+
del self.messages[message_id]
|
| 131 |
+
return f"🔥 **Message Self-Destructed After Reading**\n\n📖 **Content:** {content}\n\n💨 This message has been permanently destroyed."
|
| 132 |
+
|
| 133 |
+
return f"📖 **Message Content:** {content}\n\n⚠️ Reads remaining: {message.reads_left}"
|
| 134 |
+
|
| 135 |
+
def list_player_messages(self, player_id: str) -> str:
|
| 136 |
+
"""List messages created by a player."""
|
| 137 |
+
player_messages = [msg for msg in self.messages.values() if msg.creator_id == player_id]
|
| 138 |
+
|
| 139 |
+
if not player_messages:
|
| 140 |
+
return "📪 No messages found. Create one with: `create Your message here`"
|
| 141 |
+
|
| 142 |
+
result = "📋 **Your Created Messages:**\n\n"
|
| 143 |
+
for msg in player_messages:
|
| 144 |
+
status = "🔥 Burned" if msg.burned else f"✅ Active ({msg.reads_left} reads left)"
|
| 145 |
+
created_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(msg.created_at))
|
| 146 |
+
expires_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(msg.expires_at))
|
| 147 |
+
|
| 148 |
+
result += f"**ID:** `{msg.id}`\n"
|
| 149 |
+
result += f"**Status:** {status}\n"
|
| 150 |
+
result += f"**Created:** {created_time}\n"
|
| 151 |
+
result += f"**Expires:** {expires_time}\n"
|
| 152 |
+
result += f"**Preview:** {msg.content[:50]}{'...' if len(msg.content) > 50 else ''}\n\n"
|
| 153 |
+
|
| 154 |
+
return result
|
| 155 |
+
|
| 156 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 157 |
+
"""Handle Read2Burn mailbox commands."""
|
| 158 |
+
parts = command.strip().split(' ', 1)
|
| 159 |
+
cmd = parts[0].lower()
|
| 160 |
+
|
| 161 |
+
if cmd == "create" and len(parts) > 1:
|
| 162 |
+
return self.create_message(player_id, parts[1])
|
| 163 |
+
elif cmd == "read" and len(parts) > 1:
|
| 164 |
+
return self.read_message(player_id, parts[1])
|
| 165 |
+
elif cmd == "list":
|
| 166 |
+
return self.list_player_messages(player_id)
|
| 167 |
+
elif cmd == "help":
|
| 168 |
+
return """📚 **Read2Burn Mailbox Commands:**
|
| 169 |
+
|
| 170 |
+
**create** `Your secret message here` - Create new message
|
| 171 |
+
**read** `MESSAGE_ID` - Read message (destroys it!)
|
| 172 |
+
**list** - Show your created messages
|
| 173 |
+
**help** - Show this help
|
| 174 |
+
|
| 175 |
+
🔥 **Features:**
|
| 176 |
+
• Messages self-destruct after reading
|
| 177 |
+
• 24-hour automatic expiration
|
| 178 |
+
• Secure delivery system
|
| 179 |
+
• Anonymous messaging support"""
|
| 180 |
+
else:
|
| 181 |
+
return "❓ Invalid command. Try: `create <message>`, `read <id>`, `list`, or `help`"
|
| 182 |
+
|
| 183 |
+
def get_player_message_history(self, player_id: str) -> List[List[str]]:
|
| 184 |
+
"""Get message history for display in a dataframe."""
|
| 185 |
+
player_messages = [msg for msg in self.messages.values() if msg.creator_id == player_id]
|
| 186 |
+
|
| 187 |
+
history = []
|
| 188 |
+
for msg in player_messages:
|
| 189 |
+
status = "🔥 Burned" if msg.burned else "✅ Active"
|
| 190 |
+
created_time = time.strftime("%H:%M", time.localtime(msg.created_at))
|
| 191 |
+
reads_left = str(msg.reads_left) if not msg.burned else "0"
|
| 192 |
+
|
| 193 |
+
history.append([msg.id, created_time, status, reads_left])
|
| 194 |
+
|
| 195 |
+
return history
|
| 196 |
+
|
| 197 |
+
def cleanup_expired_messages(self):
|
| 198 |
+
"""Clean up expired messages."""
|
| 199 |
+
current_time = time.time()
|
| 200 |
+
expired_ids = [
|
| 201 |
+
msg_id for msg_id, msg in self.messages.items()
|
| 202 |
+
if current_time > msg.expires_at
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
for msg_id in expired_ids:
|
| 206 |
+
del self.messages[msg_id]
|
| 207 |
+
|
| 208 |
+
if expired_ids:
|
| 209 |
+
print(f"[Read2Burn] Cleaned up {len(expired_ids)} expired messages")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# Global Read2Burn service instance
|
| 213 |
+
read2burn_service = Read2BurnService()
|
src/addons/self_contained_example_addon.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Self-Contained Example Addon - Demonstrates the simplified addon system.
|
| 3 |
+
|
| 4 |
+
This addon automatically registers itself and requires no manual edits to system files.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import time
|
| 8 |
+
import gradio as gr
|
| 9 |
+
from typing import Dict, Any, Optional
|
| 10 |
+
|
| 11 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class SelfContainedExampleAddon(NPCAddon):
|
| 15 |
+
"""Example self-contained addon that demonstrates auto-registration."""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.interaction_count = 0
|
| 20 |
+
self.last_interaction = None
|
| 21 |
+
|
| 22 |
+
@property
|
| 23 |
+
def addon_id(self) -> str:
|
| 24 |
+
"""Unique identifier for this add-on"""
|
| 25 |
+
return "self_contained_example"
|
| 26 |
+
|
| 27 |
+
@property
|
| 28 |
+
def addon_name(self) -> str:
|
| 29 |
+
"""Display name for this add-on"""
|
| 30 |
+
return "🎯 Self-Contained Example"
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def npc_config(self) -> Dict[str, Any]:
|
| 34 |
+
"""NPC configuration for auto-placement in world"""
|
| 35 |
+
return {
|
| 36 |
+
'id': 'self_contained_npc',
|
| 37 |
+
'name': '🎯 Example Bot',
|
| 38 |
+
'x': 400, 'y': 350,
|
| 39 |
+
'char': '🎯',
|
| 40 |
+
'type': 'example',
|
| 41 |
+
'description': 'Self-contained addon demonstration NPC'
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def ui_tab_name(self) -> str:
|
| 46 |
+
"""UI tab name for this addon"""
|
| 47 |
+
return "🎯 Example Addon"
|
| 48 |
+
|
| 49 |
+
def get_interface(self) -> gr.Component:
|
| 50 |
+
"""Return Gradio interface for this add-on"""
|
| 51 |
+
with gr.Column():
|
| 52 |
+
gr.Markdown("""
|
| 53 |
+
## 🎯 Self-Contained Example Addon
|
| 54 |
+
|
| 55 |
+
This demonstrates the new simplified addon system where:
|
| 56 |
+
- ✅ **Auto-Registration**: No manual file edits needed
|
| 57 |
+
- ✅ **Self-Contained**: All config in one file
|
| 58 |
+
- ✅ **Auto-Positioning**: NPC placed automatically
|
| 59 |
+
- ✅ **UI Integration**: Tab created automatically
|
| 60 |
+
|
| 61 |
+
### Features:
|
| 62 |
+
- Counter for interactions
|
| 63 |
+
- Timestamp of last interaction
|
| 64 |
+
- Example command processing
|
| 65 |
+
- Automatic UI generation
|
| 66 |
+
""")
|
| 67 |
+
|
| 68 |
+
# Status display
|
| 69 |
+
status_display = gr.JSON(
|
| 70 |
+
label="📊 Addon Status",
|
| 71 |
+
value={
|
| 72 |
+
"addon_id": self.addon_id,
|
| 73 |
+
"addon_name": self.addon_name,
|
| 74 |
+
"npc_position": f"({self.npc_config['x']}, {self.npc_config['y']})",
|
| 75 |
+
"interaction_count": self.interaction_count,
|
| 76 |
+
"last_interaction": self.last_interaction or "Never",
|
| 77 |
+
"status": "🟢 Active"
|
| 78 |
+
}
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Test interaction
|
| 82 |
+
with gr.Row():
|
| 83 |
+
test_input = gr.Textbox(
|
| 84 |
+
label="🧪 Test Command",
|
| 85 |
+
placeholder="Type 'status', 'count', 'reset', or 'help'",
|
| 86 |
+
scale=3
|
| 87 |
+
)
|
| 88 |
+
test_btn = gr.Button("Send Command", variant="primary", scale=1)
|
| 89 |
+
|
| 90 |
+
test_result = gr.Textbox(
|
| 91 |
+
label="📤 Response",
|
| 92 |
+
lines=3,
|
| 93 |
+
interactive=False
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
def handle_test_command(command: str):
|
| 97 |
+
"""Handle test commands from the UI"""
|
| 98 |
+
result = self.handle_command("ui_user", command)
|
| 99 |
+
|
| 100 |
+
# Update status display
|
| 101 |
+
updated_status = {
|
| 102 |
+
"addon_id": self.addon_id,
|
| 103 |
+
"addon_name": self.addon_name,
|
| 104 |
+
"npc_position": f"({self.npc_config['x']}, {self.npc_config['y']})",
|
| 105 |
+
"interaction_count": self.interaction_count,
|
| 106 |
+
"last_interaction": self.last_interaction or "Never",
|
| 107 |
+
"status": "🟢 Active"
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return result, updated_status
|
| 111 |
+
|
| 112 |
+
test_btn.click(
|
| 113 |
+
handle_test_command,
|
| 114 |
+
inputs=[test_input],
|
| 115 |
+
outputs=[test_result, status_display]
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
test_input.submit(
|
| 119 |
+
handle_test_command,
|
| 120 |
+
inputs=[test_input],
|
| 121 |
+
outputs=[test_result, status_display]
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
gr.Markdown("""
|
| 125 |
+
### 🔧 Development Notes:
|
| 126 |
+
|
| 127 |
+
**File Location:** `src/addons/self_contained_example_addon.py`
|
| 128 |
+
|
| 129 |
+
**No Manual Edits Required:**
|
| 130 |
+
- ❌ No editing `world.py`
|
| 131 |
+
- ❌ No editing `game_engine.py`
|
| 132 |
+
- ❌ No editing `huggingface_ui.py`
|
| 133 |
+
|
| 134 |
+
**Auto-Registration:**
|
| 135 |
+
1. Inherits from `NPCAddon`
|
| 136 |
+
2. Calls `super().__init__()` to register
|
| 137 |
+
3. Implements required properties and methods
|
| 138 |
+
4. Game engine auto-discovers on startup
|
| 139 |
+
|
| 140 |
+
**Result:** Complete addon with NPC, UI, and commands!
|
| 141 |
+
""")
|
| 142 |
+
|
| 143 |
+
return gr.Column() # Return a container component
|
| 144 |
+
|
| 145 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 146 |
+
"""Handle player commands via private messages"""
|
| 147 |
+
self.interaction_count += 1
|
| 148 |
+
self.last_interaction = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 149 |
+
|
| 150 |
+
cmd = command.strip().lower()
|
| 151 |
+
|
| 152 |
+
if cmd == "help":
|
| 153 |
+
return """🎯 **Example Addon Commands:**
|
| 154 |
+
|
| 155 |
+
**status** - Show addon status
|
| 156 |
+
**count** - Show interaction count
|
| 157 |
+
**reset** - Reset interaction counter
|
| 158 |
+
**info** - Show addon information
|
| 159 |
+
**help** - Show this help message
|
| 160 |
+
|
| 161 |
+
*This is a demonstration of the self-contained addon system!*"""
|
| 162 |
+
|
| 163 |
+
elif cmd == "status":
|
| 164 |
+
return f"""🎯 **Example Addon Status:**
|
| 165 |
+
|
| 166 |
+
📊 **Interaction Count:** {self.interaction_count}
|
| 167 |
+
🕒 **Last Interaction:** {self.last_interaction}
|
| 168 |
+
📍 **NPC Position:** ({self.npc_config['x']}, {self.npc_config['y']})
|
| 169 |
+
🎮 **Player:** {player_id}
|
| 170 |
+
✅ **Status:** Active and responding"""
|
| 171 |
+
|
| 172 |
+
elif cmd == "count":
|
| 173 |
+
return f"🔢 **Interaction Count:** {self.interaction_count}"
|
| 174 |
+
|
| 175 |
+
elif cmd == "reset":
|
| 176 |
+
old_count = self.interaction_count
|
| 177 |
+
self.interaction_count = 1 # Set to 1 because this command counts as an interaction
|
| 178 |
+
return f"🔄 **Counter Reset!** Previous count: {old_count}, New count: {self.interaction_count}"
|
| 179 |
+
|
| 180 |
+
elif cmd == "info":
|
| 181 |
+
return f"""🎯 **Self-Contained Example Addon**
|
| 182 |
+
|
| 183 |
+
**ID:** `{self.addon_id}`
|
| 184 |
+
**Name:** {self.addon_name}
|
| 185 |
+
**Type:** Demonstration addon
|
| 186 |
+
**Features:** Auto-registration, UI integration, command processing
|
| 187 |
+
|
| 188 |
+
This addon demonstrates the new simplified system where all configuration,
|
| 189 |
+
registration, and positioning is contained within the addon file itself!"""
|
| 190 |
+
|
| 191 |
+
else:
|
| 192 |
+
return f"""❓ **Unknown command:** `{command}`
|
| 193 |
+
|
| 194 |
+
Try one of these commands:
|
| 195 |
+
• `help` - Show available commands
|
| 196 |
+
• `status` - Show current status
|
| 197 |
+
• `count` - Show interaction count
|
| 198 |
+
• `info` - Show addon information
|
| 199 |
+
|
| 200 |
+
💡 *Tip: This addon auto-registered itself without any manual file edits!*"""
|
| 201 |
+
|
| 202 |
+
def on_startup(self):
|
| 203 |
+
"""Called when the addon is loaded during game startup"""
|
| 204 |
+
print(f"[{self.addon_id}] Self-contained example addon started successfully!")
|
| 205 |
+
print(f"[{self.addon_id}] NPC positioned at ({self.npc_config['x']}, {self.npc_config['y']})")
|
| 206 |
+
print(f"[{self.addon_id}] UI tab '{self.ui_tab_name}' will be created automatically")
|
| 207 |
+
|
| 208 |
+
def on_shutdown(self):
|
| 209 |
+
"""Called when the addon is unloaded during game shutdown"""
|
| 210 |
+
print(f"[{self.addon_id}] Self-contained example addon shutting down...")
|
| 211 |
+
print(f"[{self.addon_id}] Total interactions during session: {self.interaction_count}")
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# Auto-instantiate the addon (this triggers registration)
|
| 215 |
+
self_contained_example = SelfContainedExampleAddon()
|
src/addons/simple_trader_addon.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple Trader NPC Add-on - Self-contained NPC with automatic registration.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
from typing import Dict, List
|
| 7 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class SimpleTraderAddon(NPCAddon):
|
| 11 |
+
"""Self-contained trader NPC that handles its own registration and positioning."""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
super().__init__()
|
| 15 |
+
self.inventory = {
|
| 16 |
+
"Health Potion": {"price": 50, "stock": 10, "description": "Restores 100 HP"},
|
| 17 |
+
"Magic Scroll": {"price": 150, "stock": 5, "description": "Cast magic spells"},
|
| 18 |
+
"Iron Sword": {"price": 300, "stock": 3, "description": "Sharp iron weapon"},
|
| 19 |
+
"Shield": {"price": 200, "stock": 4, "description": "Protective gear"}
|
| 20 |
+
}
|
| 21 |
+
self.sales_history = []
|
| 22 |
+
|
| 23 |
+
# ===== ADDON INTERFACE =====
|
| 24 |
+
@property
|
| 25 |
+
def addon_id(self) -> str:
|
| 26 |
+
return "simple_trader"
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def addon_name(self) -> str:
|
| 30 |
+
return "🛒 Simple Trader"
|
| 31 |
+
|
| 32 |
+
def get_interface(self) -> gr.Component:
|
| 33 |
+
"""Create the Gradio interface for this addon."""
|
| 34 |
+
with gr.Column() as interface:
|
| 35 |
+
gr.Markdown("### 🛒 Simple Trader Shop")
|
| 36 |
+
gr.Markdown("Browse items and check prices. Use private messages to trade!")
|
| 37 |
+
|
| 38 |
+
# Display inventory
|
| 39 |
+
inventory_data = []
|
| 40 |
+
for item, info in self.inventory.items():
|
| 41 |
+
inventory_data.append([item, f"{info['price']} gold", info['stock'], info['description']])
|
| 42 |
+
|
| 43 |
+
gr.Dataframe(
|
| 44 |
+
headers=["Item", "Price", "Stock", "Description"],
|
| 45 |
+
value=inventory_data,
|
| 46 |
+
interactive=False
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
gr.Markdown("**Commands:** Send private message to Simple Trader")
|
| 50 |
+
gr.Markdown("• `buy <item>` - Purchase an item")
|
| 51 |
+
gr.Markdown("• `inventory` - See available items")
|
| 52 |
+
gr.Markdown("• `help` - Show all commands")
|
| 53 |
+
|
| 54 |
+
return interface
|
| 55 |
+
|
| 56 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 57 |
+
"""Handle commands sent to this NPC."""
|
| 58 |
+
parts = command.strip().split(' ', 1)
|
| 59 |
+
cmd = parts[0].lower()
|
| 60 |
+
|
| 61 |
+
if cmd == "buy" and len(parts) > 1:
|
| 62 |
+
return self._handle_buy(player_id, parts[1])
|
| 63 |
+
elif cmd == "inventory":
|
| 64 |
+
return self._show_inventory()
|
| 65 |
+
elif cmd == "help":
|
| 66 |
+
return self._show_help()
|
| 67 |
+
else:
|
| 68 |
+
return "🛒 I don't understand that command. Try 'help' to see what I can do!"
|
| 69 |
+
|
| 70 |
+
# ===== AUTO-REGISTRATION METHODS =====
|
| 71 |
+
|
| 72 |
+
@classmethod
|
| 73 |
+
def get_npc_config(cls) -> Dict:
|
| 74 |
+
"""Return the NPC configuration for automatic world registration."""
|
| 75 |
+
return {
|
| 76 |
+
'id': 'simple_trader',
|
| 77 |
+
'name': '🛒 Simple Trader',
|
| 78 |
+
'x': 450, # Position on map
|
| 79 |
+
'y': 150,
|
| 80 |
+
'char': '🛒', # Character emoji
|
| 81 |
+
'type': 'addon',
|
| 82 |
+
'personality': 'trader',
|
| 83 |
+
'description': 'A friendly trader selling useful items'
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
@classmethod
|
| 87 |
+
def auto_register(cls, game_engine):
|
| 88 |
+
"""Automatically register this NPC with the game engine."""
|
| 89 |
+
try:
|
| 90 |
+
# Create addon instance
|
| 91 |
+
addon_instance = cls()
|
| 92 |
+
|
| 93 |
+
# Get NPC config
|
| 94 |
+
npc_config = cls.get_npc_config()
|
| 95 |
+
|
| 96 |
+
# Register NPC in world
|
| 97 |
+
npc_service = game_engine.get_npc_service()
|
| 98 |
+
npc_service.register_npc(npc_config['id'], npc_config)
|
| 99 |
+
|
| 100 |
+
# Register addon for command handling
|
| 101 |
+
if not hasattr(game_engine.get_world(), 'addon_npcs'):
|
| 102 |
+
game_engine.get_world().addon_npcs = {}
|
| 103 |
+
game_engine.get_world().addon_npcs[npc_config['id']] = addon_instance
|
| 104 |
+
|
| 105 |
+
print(f"[SimpleTrader] Auto-registered NPC '{npc_config['name']}' at position ({npc_config['x']}, {npc_config['y']})")
|
| 106 |
+
return True
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"[SimpleTrader] Auto-registration failed: {e}")
|
| 110 |
+
return False
|
| 111 |
+
|
| 112 |
+
# ===== PRIVATE METHODS =====
|
| 113 |
+
|
| 114 |
+
def _handle_buy(self, player_id: str, item_name: str) -> str:
|
| 115 |
+
"""Handle buy command."""
|
| 116 |
+
# Find item (case-insensitive)
|
| 117 |
+
item_key = None
|
| 118 |
+
for key in self.inventory.keys():
|
| 119 |
+
if key.lower() == item_name.lower():
|
| 120 |
+
item_key = key
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
if not item_key:
|
| 124 |
+
return f"❌ I don't have '{item_name}' in stock. Try 'inventory' to see what's available."
|
| 125 |
+
|
| 126 |
+
item = self.inventory[item_key]
|
| 127 |
+
|
| 128 |
+
if item['stock'] <= 0:
|
| 129 |
+
return f"❌ Sorry, {item_key} is out of stock!"
|
| 130 |
+
|
| 131 |
+
# Simulate purchase (in real game, you'd check player's gold, etc.)
|
| 132 |
+
item['stock'] -= 1
|
| 133 |
+
self.sales_history.append({
|
| 134 |
+
'player': player_id,
|
| 135 |
+
'item': item_key,
|
| 136 |
+
'price': item['price']
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
return f"✅ **Purchase Successful!**\n\n🛒 **Item:** {item_key}\n💰 **Price:** {item['price']} gold\n📦 **Stock Remaining:** {item['stock']}\n\nThank you for your business!"
|
| 140 |
+
|
| 141 |
+
def _show_inventory(self) -> str:
|
| 142 |
+
"""Show current inventory."""
|
| 143 |
+
if not self.inventory:
|
| 144 |
+
return "📦 My shop is currently empty. Come back later!"
|
| 145 |
+
|
| 146 |
+
result = "🛒 **Simple Trader Shop Inventory:**\n\n"
|
| 147 |
+
for item, info in self.inventory.items():
|
| 148 |
+
stock_status = "✅ In Stock" if info['stock'] > 0 else "❌ Out of Stock"
|
| 149 |
+
result += f"**{item}**\n"
|
| 150 |
+
result += f"💰 Price: {info['price']} gold\n"
|
| 151 |
+
result += f"📦 Stock: {info['stock']}\n"
|
| 152 |
+
result += f"📝 {info['description']}\n"
|
| 153 |
+
result += f"Status: {stock_status}\n\n"
|
| 154 |
+
|
| 155 |
+
result += "💬 Send me 'buy <item name>' to purchase!"
|
| 156 |
+
return result
|
| 157 |
+
|
| 158 |
+
def _show_help(self) -> str:
|
| 159 |
+
"""Show help information."""
|
| 160 |
+
return """🛒 **Simple Trader Commands:**
|
| 161 |
+
|
| 162 |
+
**buy** `<item name>` - Purchase an item
|
| 163 |
+
**inventory** - See all available items
|
| 164 |
+
**help** - Show this help
|
| 165 |
+
|
| 166 |
+
📋 **Example Commands:**
|
| 167 |
+
• buy Health Potion
|
| 168 |
+
• buy magic scroll
|
| 169 |
+
• inventory
|
| 170 |
+
|
| 171 |
+
💰 **How to Trade:**
|
| 172 |
+
1. Check my inventory to see items and prices
|
| 173 |
+
2. Send me a buy command with the item name
|
| 174 |
+
3. Complete your purchase!
|
| 175 |
+
|
| 176 |
+
🏪 I'm here to help you gear up for your adventures!"""
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# Global instance for easy access
|
| 180 |
+
simple_trader_addon = SimpleTraderAddon()
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# Auto-registration function that can be called from game engine
|
| 184 |
+
def register_simple_trader(game_engine):
|
| 185 |
+
"""Convenience function to register this addon."""
|
| 186 |
+
return SimpleTraderAddon.auto_register(game_engine)
|
src/addons/weather_oracle_addon.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Weather Oracle Add-on - MCP-powered weather information system.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import time
|
| 6 |
+
import json
|
| 7 |
+
import random
|
| 8 |
+
import string
|
| 9 |
+
import asyncio
|
| 10 |
+
from typing import Dict, List, Optional
|
| 11 |
+
import gradio as gr
|
| 12 |
+
from abc import ABC, abstractmethod
|
| 13 |
+
from mcp import ClientSession
|
| 14 |
+
from mcp.client.sse import sse_client
|
| 15 |
+
from contextlib import AsyncExitStack
|
| 16 |
+
|
| 17 |
+
from ..interfaces.npc_addon import NPCAddon
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class IWeatherService(ABC):
|
| 21 |
+
"""Interface for Weather service operations."""
|
| 22 |
+
|
| 23 |
+
@abstractmethod
|
| 24 |
+
def get_weather(self, location: str) -> str:
|
| 25 |
+
"""Get weather information for a location."""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
@abstractmethod
|
| 29 |
+
def connect_to_mcp(self) -> str:
|
| 30 |
+
"""Connect to MCP weather server."""
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class WeatherOracleService(IWeatherService, NPCAddon):
|
| 35 |
+
"""Service for managing Weather Oracle MCP integration."""
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
super().__init__()
|
| 39 |
+
self.connected = False
|
| 40 |
+
self.last_connection_attempt = 0
|
| 41 |
+
self.connection_cooldown = 30 # 30 seconds between connection attempts
|
| 42 |
+
self.server_url = "https://chris4k-weather.hf.space/gradio_api/mcp/sse"
|
| 43 |
+
self.session = None
|
| 44 |
+
self.tools = []
|
| 45 |
+
self.exit_stack = None
|
| 46 |
+
# Set up event loop for async operations
|
| 47 |
+
try:
|
| 48 |
+
self.loop = asyncio.get_event_loop()
|
| 49 |
+
except RuntimeError:
|
| 50 |
+
self.loop = asyncio.new_event_loop()
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def addon_id(self) -> str:
|
| 54 |
+
"""Unique identifier for this add-on"""
|
| 55 |
+
return "weather_oracle"
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def addon_name(self) -> str:
|
| 59 |
+
"""Display name for this add-on"""
|
| 60 |
+
return "Weather Oracle (MCP)"
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def npc_config(self) -> Dict:
|
| 64 |
+
"""NPC configuration for auto-placement in world"""
|
| 65 |
+
return {
|
| 66 |
+
'id': 'weather_oracle',
|
| 67 |
+
'name': 'Weather Oracle (MCP)',
|
| 68 |
+
'x': 150, 'y': 300,
|
| 69 |
+
'char': '🌤️',
|
| 70 |
+
'type': 'mcp',
|
| 71 |
+
'description': 'MCP-powered weather information service'
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def ui_tab_name(self) -> str:
|
| 76 |
+
"""UI tab name for this addon"""
|
| 77 |
+
return "Weather Oracle"
|
| 78 |
+
|
| 79 |
+
def get_interface(self) -> gr.Component:
|
| 80 |
+
"""Return Gradio interface for this add-on"""
|
| 81 |
+
with gr.Column() as interface:
|
| 82 |
+
gr.Markdown("""
|
| 83 |
+
## 🌤️ Weather Oracle (MCP)
|
| 84 |
+
|
| 85 |
+
*I commune with the weather spirits through the mystical MCP protocol to bring you weather wisdom from across the realms!*
|
| 86 |
+
|
| 87 |
+
**Ask me about weather in any city:**
|
| 88 |
+
- Current conditions and temperature
|
| 89 |
+
- Real-time weather data from global sources
|
| 90 |
+
- Powered by Model Context Protocol (MCP)
|
| 91 |
+
|
| 92 |
+
*Format: "City, Country" (e.g., "Berlin, Germany")*
|
| 93 |
+
""")
|
| 94 |
+
|
| 95 |
+
# Connection status
|
| 96 |
+
connection_status = gr.HTML(
|
| 97 |
+
value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
with gr.Row():
|
| 101 |
+
location_input = gr.Textbox(
|
| 102 |
+
label="Location",
|
| 103 |
+
placeholder="e.g., Berlin, Germany",
|
| 104 |
+
scale=3
|
| 105 |
+
)
|
| 106 |
+
get_weather_btn = gr.Button("🌡️ Consult Weather Spirits", variant="primary", scale=1)
|
| 107 |
+
|
| 108 |
+
weather_output = gr.Textbox(
|
| 109 |
+
label="🌤️ Weather Wisdom",
|
| 110 |
+
lines=8,
|
| 111 |
+
interactive=False,
|
| 112 |
+
placeholder="Enter a location and I will consult the weather spirits..."
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Example locations
|
| 116 |
+
with gr.Row():
|
| 117 |
+
gr.Examples(
|
| 118 |
+
examples=[
|
| 119 |
+
["Berlin, Germany"],
|
| 120 |
+
["Tokyo, Japan"],
|
| 121 |
+
["New York, USA"],
|
| 122 |
+
["London, UK"],
|
| 123 |
+
["Sydney, Australia"],
|
| 124 |
+
["Paris, France"],
|
| 125 |
+
["Moscow, Russia"]
|
| 126 |
+
],
|
| 127 |
+
inputs=[location_input],
|
| 128 |
+
label="🌍 Try These Locations"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# Connection controls
|
| 132 |
+
with gr.Row():
|
| 133 |
+
connect_btn = gr.Button("🔗 Connect to MCP", variant="secondary")
|
| 134 |
+
status_btn = gr.Button("📊 Check Status", variant="secondary")
|
| 135 |
+
|
| 136 |
+
def handle_weather_request(location: str):
|
| 137 |
+
if not location.strip():
|
| 138 |
+
return "❓ Please enter a location to get weather information."
|
| 139 |
+
return self.get_weather(location)
|
| 140 |
+
|
| 141 |
+
def handle_connect():
|
| 142 |
+
result = self.connect_to_mcp()
|
| 143 |
+
# Update connection status
|
| 144 |
+
new_status = f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to weather spirits' if self.connected else '🔴 Disconnected from weather realm'}</div>"
|
| 145 |
+
return result, new_status
|
| 146 |
+
|
| 147 |
+
def handle_status():
|
| 148 |
+
status = "🟢 Connected" if self.connected else "🔴 Disconnected"
|
| 149 |
+
return f"🌤️ **Weather Oracle Status**\n\nConnection: {status}\nLast update: {time.strftime('%H:%M')}"
|
| 150 |
+
|
| 151 |
+
# Wire up events
|
| 152 |
+
get_weather_btn.click(
|
| 153 |
+
handle_weather_request,
|
| 154 |
+
inputs=[location_input],
|
| 155 |
+
outputs=[weather_output]
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
location_input.submit(
|
| 159 |
+
handle_weather_request,
|
| 160 |
+
inputs=[location_input],
|
| 161 |
+
outputs=[weather_output]
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
connect_btn.click(
|
| 165 |
+
handle_connect,
|
| 166 |
+
outputs=[weather_output, connection_status]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
status_btn.click(
|
| 170 |
+
handle_status,
|
| 171 |
+
outputs=[weather_output]
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
return interface
|
| 175 |
+
|
| 176 |
+
def connect_to_mcp(self) -> str:
|
| 177 |
+
"""Connect to MCP weather server."""
|
| 178 |
+
current_time = time.time()
|
| 179 |
+
if current_time - self.last_connection_attempt < self.connection_cooldown:
|
| 180 |
+
return "⏳ Please wait before retrying connection..."
|
| 181 |
+
|
| 182 |
+
self.last_connection_attempt = current_time
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
return self.loop.run_until_complete(self._connect())
|
| 186 |
+
except Exception as e:
|
| 187 |
+
self.connected = False
|
| 188 |
+
return f"❌ Connection failed: {str(e)}"
|
| 189 |
+
|
| 190 |
+
async def _connect(self) -> str:
|
| 191 |
+
"""Async connect to MCP server using SSE."""
|
| 192 |
+
try:
|
| 193 |
+
# Clean up previous connection
|
| 194 |
+
if self.exit_stack:
|
| 195 |
+
await self.exit_stack.aclose()
|
| 196 |
+
|
| 197 |
+
self.exit_stack = AsyncExitStack()
|
| 198 |
+
|
| 199 |
+
# Connect to SSE MCP server
|
| 200 |
+
sse_transport = await self.exit_stack.enter_async_context(
|
| 201 |
+
sse_client(self.server_url)
|
| 202 |
+
)
|
| 203 |
+
read_stream, write_callable = sse_transport
|
| 204 |
+
|
| 205 |
+
self.session = await self.exit_stack.enter_async_context(
|
| 206 |
+
ClientSession(read_stream, write_callable)
|
| 207 |
+
)
|
| 208 |
+
await self.session.initialize()
|
| 209 |
+
|
| 210 |
+
# Get available tools
|
| 211 |
+
response = await self.session.list_tools()
|
| 212 |
+
self.tools = response.tools
|
| 213 |
+
|
| 214 |
+
self.connected = True
|
| 215 |
+
tool_names = [tool.name for tool in self.tools]
|
| 216 |
+
return f"✅ Connected to weather MCP server!\nAvailable tools: {', '.join(tool_names)}"
|
| 217 |
+
|
| 218 |
+
except Exception as e:
|
| 219 |
+
self.connected = False
|
| 220 |
+
return f"❌ Connection failed: {str(e)}"
|
| 221 |
+
|
| 222 |
+
def get_weather(self, location: str) -> str:
|
| 223 |
+
"""Get weather for a location using actual MCP server"""
|
| 224 |
+
if not self.connected:
|
| 225 |
+
# Try to auto-connect
|
| 226 |
+
connect_result = self.connect_to_mcp()
|
| 227 |
+
if not self.connected:
|
| 228 |
+
return f"❌ Failed to connect to weather server. {connect_result}"
|
| 229 |
+
|
| 230 |
+
if not location.strip():
|
| 231 |
+
return "❌ Please enter a location (e.g., 'Berlin, Germany')"
|
| 232 |
+
|
| 233 |
+
try:
|
| 234 |
+
return self.loop.run_until_complete(self._get_weather(location))
|
| 235 |
+
except Exception as e:
|
| 236 |
+
return f"❌ Error getting weather: {str(e)}"
|
| 237 |
+
|
| 238 |
+
async def _get_weather(self, location: str) -> str:
|
| 239 |
+
"""Async get weather using MCP."""
|
| 240 |
+
try:
|
| 241 |
+
# Parse location
|
| 242 |
+
if ',' in location:
|
| 243 |
+
city, country = [part.strip() for part in location.split(',', 1)]
|
| 244 |
+
else:
|
| 245 |
+
city = location.strip()
|
| 246 |
+
country = ""
|
| 247 |
+
|
| 248 |
+
# Find the weather tool
|
| 249 |
+
weather_tool = next((tool for tool in self.tools if 'weather' in tool.name.lower()), None)
|
| 250 |
+
if not weather_tool:
|
| 251 |
+
return "❌ Weather tool not found on server"
|
| 252 |
+
|
| 253 |
+
# Call the tool
|
| 254 |
+
params = {"city": city, "country": country}
|
| 255 |
+
result = await self.session.call_tool(weather_tool.name, params)
|
| 256 |
+
|
| 257 |
+
# Extract content properly
|
| 258 |
+
content_text = ""
|
| 259 |
+
if hasattr(result, 'content') and result.content:
|
| 260 |
+
if isinstance(result.content, list):
|
| 261 |
+
for content_item in result.content:
|
| 262 |
+
if hasattr(content_item, 'text'):
|
| 263 |
+
content_text += content_item.text
|
| 264 |
+
elif hasattr(content_item, 'content'):
|
| 265 |
+
content_text += str(content_item.content)
|
| 266 |
+
else:
|
| 267 |
+
content_text += str(content_item)
|
| 268 |
+
elif hasattr(result.content, 'text'):
|
| 269 |
+
content_text = result.content.text
|
| 270 |
+
else:
|
| 271 |
+
content_text = str(result.content)
|
| 272 |
+
|
| 273 |
+
if not content_text:
|
| 274 |
+
return "❌ No content received from server"
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
# Try to parse as JSON
|
| 278 |
+
parsed = json.loads(content_text)
|
| 279 |
+
if isinstance(parsed, dict):
|
| 280 |
+
if 'error' in parsed:
|
| 281 |
+
return f"❌ Error: {parsed['error']}"
|
| 282 |
+
|
| 283 |
+
# Format weather data nicely
|
| 284 |
+
if 'current_weather' in parsed:
|
| 285 |
+
weather = parsed['current_weather']
|
| 286 |
+
formatted = f"🌍 **{parsed.get('location', location)}**\n\n"
|
| 287 |
+
formatted += f"🌡️ Temperature: {weather.get('temperature_celsius', 'N/A')}°C\n"
|
| 288 |
+
formatted += f"🌤️ Conditions: {weather.get('weather_description', 'N/A')}\n"
|
| 289 |
+
formatted += f"💨 Wind: {weather.get('wind_speed_kmh', 'N/A')} km/h\n"
|
| 290 |
+
formatted += f"💧 Humidity: {weather.get('humidity_percent', 'N/A')}%\n"
|
| 291 |
+
formatted += f"\n⏰ Last updated: {time.strftime('%H:%M')}\n⚡ **Powered by MCP**"
|
| 292 |
+
return formatted
|
| 293 |
+
elif 'temperature (°C)' in parsed:
|
| 294 |
+
# Handle the original format from your server
|
| 295 |
+
formatted = f"🌍 **{parsed.get('location', location)}**\n\n"
|
| 296 |
+
formatted += f"🌡️ Temperature: {parsed.get('temperature (°C)', 'N/A')}°C\n"
|
| 297 |
+
formatted += f"🌤️ Weather Code: {parsed.get('weather_code', 'N/A')}\n"
|
| 298 |
+
formatted += f"🕐 Timezone: {parsed.get('timezone', 'N/A')}\n"
|
| 299 |
+
formatted += f"🕒 Local Time: {parsed.get('local_time', 'N/A')}\n"
|
| 300 |
+
formatted += f"\n⏰ Last updated: {time.strftime('%H:%M')}\n⚡ **Powered by MCP**"
|
| 301 |
+
return formatted
|
| 302 |
+
else:
|
| 303 |
+
return f"🌍 **Weather for {location}**\n\n✅ Weather data:\n```json\n{json.dumps(parsed, indent=2)}\n```\n\n⚡ **Powered by MCP**"
|
| 304 |
+
|
| 305 |
+
except json.JSONDecodeError:
|
| 306 |
+
# If not JSON, return as text
|
| 307 |
+
return f"🌍 **Weather for {location}**\n\n✅ Weather data:\n```\n{content_text}\n```\n\n⚡ **Powered by MCP**"
|
| 308 |
+
|
| 309 |
+
return f"🌍 **Weather for {location}**\n\n✅ Raw result:\n{content_text}\n\n⚡ **Powered by MCP**"
|
| 310 |
+
|
| 311 |
+
except Exception as e:
|
| 312 |
+
return f"❌ Failed to get weather: {str(e)}"
|
| 313 |
+
|
| 314 |
+
def handle_command(self, player_id: str, command: str) -> str:
|
| 315 |
+
"""Handle Weather Oracle commands."""
|
| 316 |
+
parts = command.strip().split(' ', 1)
|
| 317 |
+
cmd = parts[0].lower()
|
| 318 |
+
|
| 319 |
+
if cmd == "weather" and len(parts) > 1:
|
| 320 |
+
return self.get_weather(parts[1])
|
| 321 |
+
elif cmd == "connect":
|
| 322 |
+
return self.connect_to_mcp()
|
| 323 |
+
elif cmd == "status":
|
| 324 |
+
status = "🟢 Connected" if self.connected else "🔴 Disconnected"
|
| 325 |
+
return f"🌤️ **Weather Oracle Status**\n\nConnection: {status}\nLast update: {time.strftime('%H:%M')}"
|
| 326 |
+
elif cmd == "help":
|
| 327 |
+
return """🌤️ **Weather Oracle Commands:**
|
| 328 |
+
|
| 329 |
+
**weather** `location` - Get weather (e.g., 'weather Berlin, Germany')
|
| 330 |
+
**connect** - Connect to MCP weather server
|
| 331 |
+
**status** - Check connection status
|
| 332 |
+
**help** - Show this help
|
| 333 |
+
|
| 334 |
+
🌍 **Example Commands:**
|
| 335 |
+
• weather London, UK
|
| 336 |
+
• weather Tokyo
|
| 337 |
+
• weather New York, USA
|
| 338 |
+
|
| 339 |
+
⚡ **Powered by MCP (Model Context Protocol)**"""
|
| 340 |
+
else:
|
| 341 |
+
return "❓ Invalid command. Try: `weather <location>`, `connect`, `status`, or `help`"
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# Global Weather Oracle service instance
|
| 345 |
+
weather_oracle_service = WeatherOracleService()
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def auto_register(game_engine):
|
| 349 |
+
"""Auto-register the Weather Oracle addon with the game engine.
|
| 350 |
+
|
| 351 |
+
This function makes the addon self-contained by handling its own registration.
|
| 352 |
+
"""
|
| 353 |
+
try:
|
| 354 |
+
# Create the weather oracle NPC definition
|
| 355 |
+
weather_oracle_npc = {
|
| 356 |
+
'id': 'weather_oracle_auto',
|
| 357 |
+
'name': '🌤️ Weather Oracle (Auto)',
|
| 358 |
+
'x': 300, 'y': 150,
|
| 359 |
+
'char': '🌤️',
|
| 360 |
+
'type': 'mcp',
|
| 361 |
+
'personality': 'weather_oracle',
|
| 362 |
+
'description': 'Self-contained MCP-powered weather information service'
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
# Register the NPC with the NPC service
|
| 366 |
+
npc_service = game_engine.get_npc_service()
|
| 367 |
+
npc_service.register_npc('weather_oracle_auto', weather_oracle_npc)
|
| 368 |
+
|
| 369 |
+
# Register the addon for handling private message commands
|
| 370 |
+
world = game_engine.get_world()
|
| 371 |
+
if not hasattr(world, 'addon_npcs'):
|
| 372 |
+
world.addon_npcs = {}
|
| 373 |
+
world.addon_npcs['weather_oracle_auto'] = weather_oracle_service
|
| 374 |
+
|
| 375 |
+
print("[WeatherOracleAddon] Auto-registered successfully as self-contained addon")
|
| 376 |
+
return True
|
| 377 |
+
|
| 378 |
+
except Exception as e:
|
| 379 |
+
print(f"[WeatherOracleAddon] Error during auto-registration: {e}")
|
| 380 |
+
return False
|
src/core/__pycache__/game_engine.cpython-313.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
src/core/__pycache__/player.cpython-313.pyc
ADDED
|
Binary file (4.66 kB). View file
|
|
|