diff --git a/.gitignore b/.gitignore index 0dbf2f2..23f4f70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Custom ignores: +fleetyard_login.ini +uex_api_key + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 781de14..8c4e615 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,71 @@ # SC-Discord-Bot -Using open-webui features and ollama's inference engine to host a LLM Discord bot related to Star Citizen \ No newline at end of file +This project provides a sophisticated Discord bot focused on the game *Star Citizen*. It leverages a Large Language Model (LLM) hosted via an [Open-WebUI](https://github.com/open-webui/open-webui) backend, enhanced with Retrieval-Augmented Generation (RAG) and custom tools to provide accurate, up-to-date information. + +## Features + +- **Discord Integration**: A simple and effective Discord bot that responds to user queries in whitelisted channels. +- **LLM-Powered**: Connects to any OpenAI-compatible API, allowing you to use a variety of powerful language models. +- **Retrieval-Augmented Generation (RAG)**: The bot's knowledge is supplemented by a collection of JSON files in [`llm_rag_knowledge/`](llm_rag_knowledge/) containing detailed information about in-game events, crafting, and vehicle data. +- **Custom Tools**: The LLM can invoke a suite of Python tools located in [`llm_tools/`](llm_tools/) to fetch dynamic data: + - **Ship Information**: Get detailed ship specifications and compare vessels using data from `starcitizen.tools`. + - **Price Lookups**: Query real-time prices for commodities and items from a local database synced with `uexcorp.space`. + - **Fleet Ownership**: Check who in your organization owns which ship using data from `fleetyards.net`. +- **Data Persistence**: Utilizes SQLite databases in [`databases/`](databases/) to store and quickly access commodity, item, and fleet information. +- **Containerized**: Comes with a [`Dockerfile`](discord_connector/Dockerfile) and [`docker-compose.yaml`](discord_connector/docker-compose.yaml) for easy and consistent deployment. + +## Architecture + +The system is composed of several key parts: + +1. **Discord Connector**: The [`open-webui_to_discord.py`](discord_connector/open-webui_to_discord.py) script is the frontend, listening for user messages on Discord. +2. **Open-WebUI & LLM**: The bot forwards queries to an Open-WebUI instance, which in turn uses an LLM (e.g., Llama3, Mixtral) for inference. +3. **Knowledge Base & Tools**: The LLM's responses are enriched by: + - The static JSON files in [`llm_rag_knowledge/`](llm_rag_knowledge/). + - The dynamic Python scripts in [`llm_tools/`](llm_tools/). +4. **Data Sync Scripts**: The scripts [`get_commodities.py`](llm_tools/get_commodities.py), [`get_items.py`](llm_tools/get_items.py), and [`fleetyard.py`](llm_tools/fleetyard.py) run independently to keep the local SQLite databases current. + +## Getting Started + +### Prerequisites + +- Docker and Docker Compose +- A running Open-WebUI instance with a loaded model. +- A Discord Bot Token. + +### Setup + +1. **Clone the repository:** + ```sh + git clone https://gitea.zephyre.one/Pakobbix/SC-Discord-Bot.git + cd SC-Discord-Bot + ``` + +2. **Configure the bot:** + - Navigate to the `discord_connector` directory. + - Copy [`example.config.yml`](discord_connector/example.config.yml) to `config.yml`. + - Edit `config.yml` with your details: + - `discord_token` + - `whitelist_channels` + - `open_webui_url` and `open-webui_api_key` + - `model_name` and any `tools` you have configured in Open-WebUI. + +3. **Populate Databases:** + Before running the bot, you may need to run the data sync scripts to populate the databases. These scripts are designed to be run within the bot's environment or a similar one with the required dependencies. + ```sh + # Example for one script + python llm_tools/get_commodities.py + python llm_tools/get_items.py + python llm_tools/fleetyard.py + ``` + *Note: You may need to adjust paths or run these within the Docker container for them to access the correct database location.* + +4. **Run with Docker Compose:** + From the `discord_connector` directory, run: + ```sh + docker-compose up --build -d + ``` + +## License + +This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for diff --git a/databases/commodities.db b/databases/commodities.db new file mode 100644 index 0000000..b212f33 Binary files /dev/null and b/databases/commodities.db differ diff --git a/databases/fleet.db b/databases/fleet.db new file mode 100644 index 0000000..d45e317 Binary files /dev/null and b/databases/fleet.db differ diff --git a/databases/items.db b/databases/items.db new file mode 100644 index 0000000..75edbeb Binary files /dev/null and b/databases/items.db differ diff --git a/discord_connector/Dockerfile b/discord_connector/Dockerfile new file mode 100644 index 0000000..c26e538 --- /dev/null +++ b/discord_connector/Dockerfile @@ -0,0 +1,17 @@ +# We use an official Python runtime as a parent image +FROM python:3.12-alpine + +# Set the working directory in the container +WORKDIR /app + +# Copy the dependencies file to the working directory +COPY requirements.txt . + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application's code +COPY . . + +# Run the bot when the container launches +CMD ["python", "open-webui_to_discord.py"] diff --git a/discord_connector/docker-compose.yaml b/discord_connector/docker-compose.yaml new file mode 100644 index 0000000..66fdf9b --- /dev/null +++ b/discord_connector/docker-compose.yaml @@ -0,0 +1,7 @@ +services: + discord-bot: + build: . + container_name: discord-sc-answer-bot + restart: unless-stopped + volumes: + - ./config.yml:/app/config.yml diff --git a/discord_connector/example.config.yml b/discord_connector/example.config.yml new file mode 100644 index 0000000..89e4491 --- /dev/null +++ b/discord_connector/example.config.yml @@ -0,0 +1,22 @@ +# ─────────── Discord Settings ─────────── +discord_token: "YOUR_DISCORD_BOT_TOKEN_HERE" + +whitelist_channels: + - 123456789123456789 # <-- channel id + - 987654321987654321 # <-- another channel id + +allow_dms: false # set to true if you want the bot to answer private DMs (Currently not working) + +# ─────────── Open‑WebUI Settings ─────────── +open_webui_url: "http://your_open-webui_ip_or_domain:port" +open-webui_api_key: "user_api_key_from_open_webui" +model_name: "model_id_from_open-webui" +knowledge_base: "knowledge_base_id_from_open-webui" + +# tools map – label → tool‑id that your Open‑WebUI instance recognises +tools: + - Tool_ID_1 + - Tool_ID_2 + +# optional system prompt (you can leave it empty to use the default one or the systemprompt given in open-webui for the specific model) +system_prompt: "" \ No newline at end of file diff --git a/discord_connector/open-webui_to_discord.py b/discord_connector/open-webui_to_discord.py new file mode 100644 index 0000000..dab87e4 --- /dev/null +++ b/discord_connector/open-webui_to_discord.py @@ -0,0 +1,144 @@ +import yaml +import asyncio +import discord +from discord.ext import commands +import aiohttp + +# ──────────────────────────────────────────────── +def load_config(path: str): + """Return a dict loaded from a YAML file.""" + with open(path, "r", encoding="utf‑8") as fh: + return yaml.safe_load(fh) + +config = load_config("config.yml") # <- loads the config file + +DISCORD_TOKEN = config["discord_token"] # Discord bot token +WHITELIST_CHANNELS = set(map(int, config.get("whitelist_channels", []))) # Set of whitelisted channel IDs (int) + +OPENWEBUI_URL = config["open_webui_url"].rstrip('/') # Ensure no trailing slash +OPENWEBUI_API_KEY = config["open-webui_api_key"] # API key for Open-WebUI +MODEL_NAME = config["model_name"] # Model name to use, e.g., "gpt-3.5-turbo" +KNOW_BASE = config["knowledge_base"] # Knowledge base to use, e.g., "knowledge_base_v1" + +TOOLS = config.get("tools", []) # list of tool-ids + +SYSTEM_PROMPT = config.get("system_prompt", None) # Optional system prompt to prepend to user messages +ALLOW_DMS = config.get("allow_dms", False) # Allow DMs to the bot (default: False) + +async def _query_openwebui(user_text: str, channel_id: int, tools_list: list): + """ + Payload structure for the OpenAI-compatible endpoint. + + Args: + user_text (str): The user's message to send to the Open-WebUI. + channel_id (int): The Discord channel ID where the message was sent. + tools_list (list): List of tool IDs to use, if any. + """ + async with aiohttp.ClientSession() as session: + # This payload structure is for the OpenAI-compatible endpoint from open-webui + payload = { + "model": MODEL_NAME, + "stream": False, + "messages": [ + { + "role": "user", + "content": user_text + } + ] + } + # Attach tools if provided in the config file + if tools_list: + payload["tool_ids"] = tools_list + print(f"🔧 Using tools: {payload['tool_ids']}") + + # The endpoint path for your instance appears to be /api/chat/completions + async with session.post(f"{OPENWEBUI_URL}/api/chat/completions", + json=payload, + headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"}) as resp: + + if resp.status != 200: + # If the response is not 200, raise an error with the response text + data = await resp.text() + raise RuntimeError(f"Open‑WebUI responded {resp.status}: {data}") + + # If the response is OK, parse the JSON and return the content + response_data = await resp.json() + return response_data['choices'][0]['message']['content'] + +# --------------------------------------------------------------------------- # +# Discord bot logic – discord.py +# --------------------------------------------------------------------------- # + +intents = discord.Intents.default() # Default intents +intents.message_content = True # Required to read message content +intents.members = True # Required to read member info (if needed) + +bot = commands.Bot(command_prefix='!', intents=intents) # Command prefix is '!' by default to allow commands like !ping + + +@bot.event +async def on_ready(): + print(f"✅ Logged in as {bot.user} (id={bot.user.id})") + +# Only a test for commands, I add later +@bot.command(name="ping") +async def ping(ctx): + await ctx.send("🏓 Pong!") + +# --------------------------------------------------------------------------- # +# Main logic – only respond to allowed channels / DM flag +# --------------------------------------------------------------------------- # + +@bot.event +async def on_message(message): + # Ignore messages from bots (incl. the bot itself) + if message.author.bot: return + + # Allow commands to be processed + await bot.process_commands(message) + + # Skip if we are in a DM and that is disabled + if not ALLOW_DMS and isinstance(message.channel, discord.DMChannel): return + + # --- debugging --- + print(f"ℹ️ Message received in channel: {message.channel.id}") + print(f"📢 Whitelisted channels are: {WHITELIST_CHANNELS}") + # ----------------------------- + + # Allow only the whitelist channels – empty list means “all channels” + if WHITELIST_CHANNELS and message.channel.id not in WHITELIST_CHANNELS: + return + + # ----------------------------------------------------------------------- # + # A. Prepare payload + # ----------------------------------------------------------------------- # + # The OpenAI endpoint works better without the extra context in the prompt + prompt = message.content + if SYSTEM_PROMPT: + # The system prompt is handled differently in the OpenAI-compatible API + # For simplicity, we'll prepend it here. A more robust solution + # would add it as a separate message with the 'system' role. + prompt = f"{SYSTEM_PROMPT}\n\nUser Question: {message.content}" + + # ----------------------------------------------------------------------- # + # B. Query Open‑WebUI and show typing indicator + # ----------------------------------------------------------------------- # + try: + async with message.channel.typing(): + # Query the Open-WebUI API while showing "Bot is typing..." + reply = await _query_openwebui(prompt, message.channel.id, TOOLS) + # Send the reply + await message.reply(reply) + except Exception as e: + await message.reply(f"⚠ Error contacting the Open‑WebUI API: {e}") + # No need to return here as the function ends after this block. + +# --------------------------------------------------------------------------- # +# Start bot +# --------------------------------------------------------------------------- # + +if __name__ == "__main__": + try: + asyncio.run(bot.run(DISCORD_TOKEN)) + except KeyboardInterrupt: + print("🤖 Shutting down…") diff --git a/discord_connector/requirements.txt b/discord_connector/requirements.txt new file mode 100644 index 0000000..2d9bc74 --- /dev/null +++ b/discord_connector/requirements.txt @@ -0,0 +1,3 @@ +PyYAML +discord.py +aiohttp \ No newline at end of file diff --git a/llm_rag_knowledge/align_and_mine_event.json b/llm_rag_knowledge/align_and_mine_event.json new file mode 100644 index 0000000..2aa2d12 --- /dev/null +++ b/llm_rag_knowledge/align_and_mine_event.json @@ -0,0 +1,267 @@ +{ + "event": "Align & Mine", + "game": "Star Citizen", + "introduction": { + "description": "Das Align & Mine Event ist ein hochriskantes Bergbau-Event, bei dem Spieler zusammenarbeiten müssen, um einen Orbital-Mining-Laser zu aktivieren, der Zugang zu wertvollen unterirdischen Ressourcen freischaltet.", + "key_requirements": [ + "Drei Satellitenschüsseln ausrichten", + "Verschiedene Keycards sammeln", + "Batteriepacks sichern", + "Gegner abwehren (NPCs und andere Spieler)" + ], + "objective": "Aktiviere den Orbital-Mining-Laser und sichere wertvolle Ressourcen." + }, + "locations": [ + { + "name": "Daymar", + "description": "Ein felsiger, wüstenähnlicher Mond mit Stickstoff-Methan-Atmosphäre. Temperaturen zwischen -40°C und 78°C.", + "required_equipment": { + "suit": "Standard-Raumanzug", + "additional": "Sauerstoffreserven" + }, + "points_of_interest": [ + { + "name": "Lamina", + "type": "POI", + "description": "Ein POI auf Daymar mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Lamina-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Lamina." + }, + { + "name": "Lamina-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Lamina." + }, + { + "name": "Lamina-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Lamina." + }, + { + "name": "Lamina-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + }, + { + "name": "Attritus", + "type": "POI", + "description": "Ein POI auf Daymar mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Attritus-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Attritus." + }, + { + "name": "Attritus-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Attritus." + }, + { + "name": "Attritus-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Attritus." + }, + { + "name": "Attritus-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + } + ] + }, + { + "name": "Aberdeen", + "description": "Ein tödlich heißer Mond mit Schwefelatmosphäre. Temperaturen zwischen 170°C und 237°C.", + "required_equipment": { + "suit": "Pembroke Exploration Suit", + "additional": "Hitzebeständige Ausrüstung" + }, + "points_of_interest": [ + { + "name": "Ruptura", + "type": "POI", + "description": "Ein POI auf Aberdeen mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Ruptura-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Ruptura." + }, + { + "name": "Ruptura-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Ruptura." + }, + { + "name": "Ruptura-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Ruptura." + }, + { + "name": "Ruptura-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + }, + { + "name": "Vivere", + "type": "POI", + "description": "Ein POI auf Aberdeen mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Vivere-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Vivere." + }, + { + "name": "Vivere-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Vivere." + }, + { + "name": "Vivere-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Vivere." + }, + { + "name": "Vivere-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + } + ] + } + ], + "rewards": { + "description": "Nach dem Bergbau können Ressourcen bei Wikelo's gegen spezielle Gegenstände wie Rüstungen und eine Polaris eingetauscht werden.", + "example_items": [ + "Polaris", + "Spezielle Rüstungen", + "Seltenes Bergbau-Material" + ] + }, + "preparation": { + "equipment": { + "racing_suits": { + "Daymar": "Standard-Raumanzug mit Sauerstoffreserven", + "Aberdeen": "Pembroke Exploration Suit" + }, + "weapons": { + "primary": "Beispiel: FS-9 LMG für Sperrfeuer", + "secondary": "Beispiel: Arclight Pistole für Nahkämpfe", + "ammunition": "Mindestens ein Dutzend Magazine" + }, + "tools": [ + "MaxLift Tractor Beam", + "MedGun mit Nachfüllpatronen", + "MedPens" + ] + }, + "ships": { + "solo": "Avenger Titan", + "small_team": "Cutlass Black", + "multi_crew": [ + "Valkyrie", + "Carrack" + ] + } + }, + "steps": [ + { + "step": 1, + "title": "Aktivierung der Satellitenschüsseln", + "description": "Finde und aktiviere drei Satellitenschüsseln, die um den Orbital-Mining-Laser positioniert sind.", + "substeps": [ + { + "title": "Lokalisierung der Schüsseln", + "description": "Suche nach drei Schüsseln, wobei einige bereits aktiviert sein können." + }, + { + "title": "Keycards finden", + "description": "Finde zwei Keycards: PAF Maintenance Keycard und PAF Security Keycard.", + "spawn_locations": [ + "Tote NPCs/Wachen", + "Schließfächer", + "Kontrollräume" + ] + }, + { + "title": "Terminal-Aktivierung", + "description": "Gehe in den Kontrollraum, füge Alignment Blades in den Terminal ein, warte auf Aktivierung.", + "warnings": [ + "Valakkar-Angriffe werden ausgelöst!", + "Deckung suchen, Spotter einsetzen" + ] + }, + { + "title": "Batteriepacks sichern", + "description": "Nach Aktivierung öffnet sich eine Garagentür und gibt ein Batteriepack frei. Sammle drei Batterien.", + "tools": [ + "MaxLift Tractor Beam", + "Sichere Lagerung" + ] + } + ] + }, + { + "step": 2, + "title": "Stromversorgung des Orbital-Mining-Lasers", + "description": "Bringe die drei Batteriepacks zum Mining-Laser und verbinde sie mit den Power Banks.", + "substeps": [ + { + "title": "Sicherung des Gebiets", + "description": "Bereite dich auf NPC- und Spielergegner vor. Nutze Teamkoordination (Verteidiger, Operateure)." + }, + { + "title": "Batterieeinsetzen", + "description": "Setze die Batterien in die drei Power Banks ein. Der Laser wird hochgefahren." + }, + { + "title": "Keycard holen", + "description": "Entnehme die Laser Activation Keycard vom Terminal.", + "tips": [ + "Wenn Power LEDs nicht grün leuchten, Batterie erneut einsetzen" + ] + }, + { + "title": "Optionale Loots", + "description": "Finde OLP Storage Card und Supervisor Access Card für zusätzliche Beute.", + "supervisor_room": { + "access": "Mit Supervisor Access Card", + "contents": "Hochwertige Gegenstände" + } + } + ] + }, + { + "step": 3, + "title": "Abfeuern des Orbital-Lasers", + "description": "Nutze die Laser Activation Keycard, um den Laser vom Bunker aus abzufeuern.", + "action": "Drücke den großen roten Knopf, um den Höhleneingang zu öffnen." + }, + { + "step": 4, + "title": "Betreten der Mine und Sammeln von Ressourcen", + "description": "Gehe in die neu geöffnete Höhle und sammle wertvolle Ressourcen.", + "warnings": [ + "Diese Phase ist oft umkämpft", + "Bleibe wachsam, bewege dich schnell" + ] + } + ], + "tips": [ + "Nutze einen Spotter für Valakkar-Bedrohungen", + "Plane Rollenverteilung für Teamkoordination", + "Sichere Batterien und Keycards vor Einbrüchen", + "Suche nach mehreren Supervisor Access Cards für schnelleren Zugriff" + ] +} diff --git a/llm_rag_knowledge/map.json b/llm_rag_knowledge/map.json new file mode 100644 index 0000000..6eeccaa --- /dev/null +++ b/llm_rag_knowledge/map.json @@ -0,0 +1,193 @@ +{ + "systems": [ + { + "name": "Stanton", + "type": "System", + "description": "Besteht aus mehreren Planeten und Monden", + "Planeten:": 4, + "Zugehörigkeit:": "UEE", + "locations": [ + { + "name": "ArcCorp", + "type": "Planet", + "description": "Planet: Stanton III - ArcCorp", + "environment": "Sicher", + "landingZone": "Area18 (Start Location, Ignis Borealis Heimatplanet)", + "lagrangeStation": "Baijini Point", + "moons": ["Wala", "Lyria"], + "lagrangianPoints": [ + "ARC-L1 Wide Forest Station", + "ARC-L2 Lively Pathway Station", + "ARC-L3 Modern Express Station", + "ARC-L4 Faint Glen Station", + "ARC-L5 Yellow Core Station" + ] + }, + { + "name": "MicroTech", + "type": "Planet", + "description": "Planet: Stanton IV - MicroTech", + "environment": "Sicher", + "landingZone": "New Babbage (Start Location)", + "lagrangeStation": "Port Tressler", + "moons": ["Calliope", "Clio", "Euterpe"], + "lagrangianPoints": [ + "MIC-L1 Shallow Frontier Station", + "MIC-L2 Long Forest Station", + "MIC-L3 Endless Odyssey Station", + "MIC-L4 Red Crossroads Station", + "MIC-L5 Modern Icarus Station" + ] + }, + { + "name": "Crusader", + "type": "Planet", + "description": "Planet: Stanton II - Crusader", + "environment": "Sicher", + "landingZone": "Orison (Start Location)", + "lagrangeStation": "Seraphim Station", + "moons": ["Cellin", "Daymar", "Yela"], + "lagrangianPoints": [ + "CRU-L1 Ambitious Dream Station", + "CRU-L4 Shallow Fields Station", + "CRU-L5 Beautiful Glen Station" + ] + }, + { + "name": "Hurston", + "type": "Planet", + "description": "Planet: Stanton I - Hurston", + "environment": "Sicher (Bisher)", + "landingZone": "Lorville (Start Location)", + "lagrangeStation": "Everus Harbor", + "moons": ["Arial", "Aberdeen", "Magda", "Ita"], + "lagrangianPoints": [ + "HUR-L1 Green Glade Station", + "HUR-L2 Faithful Dream Station", + "HUR-L3 Thundering Express Station", + "HUR-L4 Melodic Fields Station", + "HUR-L5 High Course Station" + ] + }, + { + "name": "Security Post Kareah", + "type": "Station", + "description": "Typ: Station", + "environment": "Sicher, hostile NPC's", + "location": "Umrundet Cellin" + }, + { + "name": "Grim Hex", + "type": "Station", + "description": "Typ: Station", + "environment": "Sicher", + "location": "In der Nähe von Yela im Asteroiden Gürtel", + "faction": "Unabhängig", + "relations": [ + { + "to": "Stanton", + "relationType": "is located in" + }, + { + "to": "Yela", + "relationType": "is near" + } + ] + }, + { + "name": "Pyro Gateway", + "type": "Station", + "description": "Typ: Station", + "environment": "Sicher", + "purpose": "Gateway um nach Pyro zu gelangen" + }, + { + "name": "Magnus Gateway", + "type": "Station", + "description": "Typ: Station", + "environment": "Sicher", + "purpose": "Gateway um nach Magnus zu gelangen (Noch nicht im Spiel)" + }, + { + "name": "Terra Gateway", + "type": "Station", + "description": "Typ: Station", + "environment": "Sicher", + "purpose": "Gateway um nach Terra zu gelangen (Noch nicht im Spiel)" + } + ] + }, + { + "name": "Pyro", + "type": "System", + "description": "Besteht aus mehreren Planeten und Monden", + "Planeten": 6, + "Zugehörigkeit": "Keine", + "locations": [ + { + "name": "Pyro I", + "type": "Planet", + "description": "Planet: Pyro I", + "environment": "Tödlich", + "moons": "Keine", + "lagrangianPoints": [ + "PYAM-FARSTAT-1-2", + "PYAM-FARSTAT-1-3", + "PYAM-FARSTAT-1-5" + ] + }, + { + "name": "Pyro II - Monox", + "type": "Planet", + "description": "Planet: Pyro II - Monox", + "environment": "Sicher", + "moons": "Keine", + "lagrangianPoints": [ + "Checkmate Station (Start Location)" + ] + }, + { + "name": "Pyro III - Bloom", + "type": "Planet", + "description": "Planet: Pyro III - Bloom", + "environment": "Sicher", + "moons": "Keine", + "lagrangianPoints": [ + "Starlight Service Station", + "Patch City", + "PYAM-FARSTAT-3-5", + "Orbituary (Starting Location)" + ] + }, + { + "name": "Pyro V", + "type": "Planet", + "description": "Planet: Pyro V", + "environment": "Tödlich", + "moons": ["Ignis", "Vatra", "Adir", "Fairo", "Fuego", "Vuur", "Pyro IV"], + "lagrangianPoints": [ + "PYAM-FARSTAT-5-1", + "Gaslight", + "PYAM-FARSTAT-5-3", + "Rod's Fuel 'N Supplies", + "Rat's Nest" + ] + }, + { + "name": "Pyro VI - Terminus", + "type": "Planet", + "description": "Planet: Pyro VI - Terminus", + "environment": "Sicher", + "moons": "Keine", + "lagrangianPoints": [ + "PYAM-FARSTAT-5-2", + "Endgame", + "Dudley & Daughters", + "Megumi Refueling", + "Ruin Station (Starting Location)" + ] + } + ] + } + ] +} diff --git a/llm_rag_knowledge/resource_drive_second_life_event.json b/llm_rag_knowledge/resource_drive_second_life_event.json new file mode 100644 index 0000000..2aa2d12 --- /dev/null +++ b/llm_rag_knowledge/resource_drive_second_life_event.json @@ -0,0 +1,267 @@ +{ + "event": "Align & Mine", + "game": "Star Citizen", + "introduction": { + "description": "Das Align & Mine Event ist ein hochriskantes Bergbau-Event, bei dem Spieler zusammenarbeiten müssen, um einen Orbital-Mining-Laser zu aktivieren, der Zugang zu wertvollen unterirdischen Ressourcen freischaltet.", + "key_requirements": [ + "Drei Satellitenschüsseln ausrichten", + "Verschiedene Keycards sammeln", + "Batteriepacks sichern", + "Gegner abwehren (NPCs und andere Spieler)" + ], + "objective": "Aktiviere den Orbital-Mining-Laser und sichere wertvolle Ressourcen." + }, + "locations": [ + { + "name": "Daymar", + "description": "Ein felsiger, wüstenähnlicher Mond mit Stickstoff-Methan-Atmosphäre. Temperaturen zwischen -40°C und 78°C.", + "required_equipment": { + "suit": "Standard-Raumanzug", + "additional": "Sauerstoffreserven" + }, + "points_of_interest": [ + { + "name": "Lamina", + "type": "POI", + "description": "Ein POI auf Daymar mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Lamina-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Lamina." + }, + { + "name": "Lamina-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Lamina." + }, + { + "name": "Lamina-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Lamina." + }, + { + "name": "Lamina-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + }, + { + "name": "Attritus", + "type": "POI", + "description": "Ein POI auf Daymar mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Attritus-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Attritus." + }, + { + "name": "Attritus-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Attritus." + }, + { + "name": "Attritus-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Attritus." + }, + { + "name": "Attritus-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + } + ] + }, + { + "name": "Aberdeen", + "description": "Ein tödlich heißer Mond mit Schwefelatmosphäre. Temperaturen zwischen 170°C und 237°C.", + "required_equipment": { + "suit": "Pembroke Exploration Suit", + "additional": "Hitzebeständige Ausrüstung" + }, + "points_of_interest": [ + { + "name": "Ruptura", + "type": "POI", + "description": "Ein POI auf Aberdeen mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Ruptura-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Ruptura." + }, + { + "name": "Ruptura-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Ruptura." + }, + { + "name": "Ruptura-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Ruptura." + }, + { + "name": "Ruptura-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + }, + { + "name": "Vivere", + "type": "POI", + "description": "Ein POI auf Aberdeen mit drei PAF-Sites und einem OLP.", + "sublocations": [ + { + "name": "Vivere-PAF I", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Vivere." + }, + { + "name": "Vivere-PAF II", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Vivere." + }, + { + "name": "Vivere-PAF III", + "type": "Satellite Dish", + "description": "Eine der drei Satellitenschüsseln auf Vivere." + }, + { + "name": "Vivere-OLP", + "type": "Orbital Laser Platform", + "description": "Der Orbitallaser befindet sich hier." + } + ] + } + ] + } + ], + "rewards": { + "description": "Nach dem Bergbau können Ressourcen bei Wikelo's gegen spezielle Gegenstände wie Rüstungen und eine Polaris eingetauscht werden.", + "example_items": [ + "Polaris", + "Spezielle Rüstungen", + "Seltenes Bergbau-Material" + ] + }, + "preparation": { + "equipment": { + "racing_suits": { + "Daymar": "Standard-Raumanzug mit Sauerstoffreserven", + "Aberdeen": "Pembroke Exploration Suit" + }, + "weapons": { + "primary": "Beispiel: FS-9 LMG für Sperrfeuer", + "secondary": "Beispiel: Arclight Pistole für Nahkämpfe", + "ammunition": "Mindestens ein Dutzend Magazine" + }, + "tools": [ + "MaxLift Tractor Beam", + "MedGun mit Nachfüllpatronen", + "MedPens" + ] + }, + "ships": { + "solo": "Avenger Titan", + "small_team": "Cutlass Black", + "multi_crew": [ + "Valkyrie", + "Carrack" + ] + } + }, + "steps": [ + { + "step": 1, + "title": "Aktivierung der Satellitenschüsseln", + "description": "Finde und aktiviere drei Satellitenschüsseln, die um den Orbital-Mining-Laser positioniert sind.", + "substeps": [ + { + "title": "Lokalisierung der Schüsseln", + "description": "Suche nach drei Schüsseln, wobei einige bereits aktiviert sein können." + }, + { + "title": "Keycards finden", + "description": "Finde zwei Keycards: PAF Maintenance Keycard und PAF Security Keycard.", + "spawn_locations": [ + "Tote NPCs/Wachen", + "Schließfächer", + "Kontrollräume" + ] + }, + { + "title": "Terminal-Aktivierung", + "description": "Gehe in den Kontrollraum, füge Alignment Blades in den Terminal ein, warte auf Aktivierung.", + "warnings": [ + "Valakkar-Angriffe werden ausgelöst!", + "Deckung suchen, Spotter einsetzen" + ] + }, + { + "title": "Batteriepacks sichern", + "description": "Nach Aktivierung öffnet sich eine Garagentür und gibt ein Batteriepack frei. Sammle drei Batterien.", + "tools": [ + "MaxLift Tractor Beam", + "Sichere Lagerung" + ] + } + ] + }, + { + "step": 2, + "title": "Stromversorgung des Orbital-Mining-Lasers", + "description": "Bringe die drei Batteriepacks zum Mining-Laser und verbinde sie mit den Power Banks.", + "substeps": [ + { + "title": "Sicherung des Gebiets", + "description": "Bereite dich auf NPC- und Spielergegner vor. Nutze Teamkoordination (Verteidiger, Operateure)." + }, + { + "title": "Batterieeinsetzen", + "description": "Setze die Batterien in die drei Power Banks ein. Der Laser wird hochgefahren." + }, + { + "title": "Keycard holen", + "description": "Entnehme die Laser Activation Keycard vom Terminal.", + "tips": [ + "Wenn Power LEDs nicht grün leuchten, Batterie erneut einsetzen" + ] + }, + { + "title": "Optionale Loots", + "description": "Finde OLP Storage Card und Supervisor Access Card für zusätzliche Beute.", + "supervisor_room": { + "access": "Mit Supervisor Access Card", + "contents": "Hochwertige Gegenstände" + } + } + ] + }, + { + "step": 3, + "title": "Abfeuern des Orbital-Lasers", + "description": "Nutze die Laser Activation Keycard, um den Laser vom Bunker aus abzufeuern.", + "action": "Drücke den großen roten Knopf, um den Höhleneingang zu öffnen." + }, + { + "step": 4, + "title": "Betreten der Mine und Sammeln von Ressourcen", + "description": "Gehe in die neu geöffnete Höhle und sammle wertvolle Ressourcen.", + "warnings": [ + "Diese Phase ist oft umkämpft", + "Bleibe wachsam, bewege dich schnell" + ] + } + ], + "tips": [ + "Nutze einen Spotter für Valakkar-Bedrohungen", + "Plane Rollenverteilung für Teamkoordination", + "Sichere Batterien und Keycards vor Einbrüchen", + "Suche nach mehreren Supervisor Access Cards für schnelleren Zugriff" + ] +} diff --git a/llm_rag_knowledge/stormbreaker_part_1.json b/llm_rag_knowledge/stormbreaker_part_1.json new file mode 100644 index 0000000..1cbd00d --- /dev/null +++ b/llm_rag_knowledge/stormbreaker_part_1.json @@ -0,0 +1,69 @@ +{ + "event": "Stormbreaker Event Part 1", + "location": "Pyro IV", + "farro_data_centers": 10, + "goal": "Externe Forschungseinrichtungen für den Lazarus-Komplex", + "escort": { + "security_forces": true, + "scientists": true, + "engineers": true + }, + "optional_units": { + "heavy_units": "Von Fackel & BitZeros" + }, + "optional_quest": { + "offered_by": "Eckhart & BitZeros" + }, + "required_keycard": { + "name": "Probenbehälter-Schlüsselkarte", + "source": "Farro Datenzentren (FDZs)", + "purpose": "Lazarus Komplex (LKs) abschließen" + }, + "security_level_02": { + "access": "Über einen lebenden Körper mit Laborkittel", + "scientist_locations": [ + "Nebengebäude der Klinik", + "Messe", + "Grünbereich", + "Edain-Labor" + ], + "action": "Wissenschaftler bewusstlos zur Sicherheits-Tür bringen", + "room_contents": { + "safe": "In der Wand", + "laptop": { + "code": "4-stellig" + } + }, + "required_item": "Laborkittel" + }, + "security_level_03": { + "required_keycards": [ + { + "name": "Datenverarbeitungs-Schlüsselkarte", + "source": "Safes" + }, + { + "name": "Wartungs-Schlüsselkarte", + "source": "Notunterkunft", + "optional": true, + "purpose": "Kein Laborkittel benötigt oder ATLS-Rad umgehen" + } + ], + "data_processing_room": { + "laptop": { + "code": "5-stellig" + }, + "action": "Probenbehälter-Schlüsselkarte im Turm eingeben (Raummitte)" + }, + "press_action": { + "spawns": "Schwere Einheiten", + "doors": "Datenverarbeitung offen", + "duration": "5 Minuten", + "interruption": "Mindestens eine Unterbrechung muss bestätigt werden" + } + }, + "abbreviations": { + "FDZ": "Farro Datenzentren", + "LK": "Lazarus Komplex" + } +} \ No newline at end of file diff --git a/llm_rag_knowledge/stormbreaker_part_2.json b/llm_rag_knowledge/stormbreaker_part_2.json new file mode 100644 index 0000000..f9b7887 --- /dev/null +++ b/llm_rag_knowledge/stormbreaker_part_2.json @@ -0,0 +1,120 @@ +{ + "event": "Stormbreaker Event Part 2", + "complexes": [ + { + "name": "Phoenix", + "location": "Pyro I", + "storms": "Immerwährende Stürme", + "verzerrungsfeld": true, + "labs": 3, + "transit": { + "method": "Transitknoten am Feldrand", + "distance": "22-30 km", + "shuttle": { + "frequency": "Alle 2 Minuten" + } + }, + "radiation_zone": { + "danger_level": "Tödlich", + "rate": "175 REM pro Sekunde", + "ungeschützt": { + "death_time": "Innerhalb von 5 Sekunden" + }, + "heavy_armor": { + "protection_time": "Ca. 15 Minuten" + }, + "stirling_suit": { + "protection": "Immunität" + } + }, + "safe_approach": { + "method": "Via Orbital Marker", + "markers": [ + "OM-1", + "OM-3", + "OM-6" + ] + } + }, + { + "name": "Tithonus", + "location": "Pyro I", + "storms": "Immerwährende Stürme", + "verzerrungsfeld": true, + "labs": 3, + "transit": { + "method": "Transitknoten am Feldrand", + "distance": "22-30 km", + "shuttle": { + "frequency": "Alle 2 Minuten" + } + }, + "radiation_zone": { + "danger_level": "Tödlich", + "rate": "175 REM pro Sekunde", + "ungeschützt": { + "death_time": "Innerhalb von 5 Sekunden" + }, + "heavy_armor": { + "protection_time": "Ca. 15 Minuten" + }, + "stirling_suit": { + "protection": "Immunität" + } + }, + "safe_approach": { + "method": "Via Orbital Marker", + "markers": [ + "OM-6", + "OM-5", + "OM-1" + ] + } + } + ], + "transit_station": { + "guards": { + "count": "10 - 15", + "armor": "Mittel- bis schwer gepanzert" + }, + "shuttles": { + "frequency": "Alle 2 Minuten" + } + }, + "lab_location": { + "access": { + "from_shuttle": "Zu Eingang 01 zur Krankenstation gehen" + }, + "door_code": { + "source": "Fenster zu Dr. Jorrits Büro", + "method": "Zahlen im Periodensystem hervorgehoben" + }, + "interior": { + "items": [ + "Volt-Schrotflinte", + "Stirling-Anzug ASD-Edition" + ] + }, + "egg_extraction": { + "duration": "3 Minuten plus Pausen", + "interaction": "Eine Interaktion erforderlich", + "security_forces": "In Wellen nach Pausen & Strahlungs-Entlüftung" + }, + "egg_preparation": { + "location": "Rufturm", + "actions": [ + "Ei einlegen & bereiten", + "Apex spawns mit ständig auftauchenden Ausgewachsenen und Jungen", + "Auf Schwachpunkte zielen (im Mund & Grüne Pustel)", + "Schwachpunkt-Treffer haben grünen splatter VFX", + "Zittert alle 20% verlorene Gesundheit", + "Bei 20% Gesundheit schlägt ein Blitz ein", + "Wenn tot sind 6 Zähne & 15 Perlen Extrahierbar" + ] + } + }, + "abbreviations": { + "REM": "roentgen equivalent man", + "ASD": "Advanced Sience and Deployment" + } +} \ No newline at end of file diff --git a/llm_rag_knowledge/verhicle_fitment_index.json b/llm_rag_knowledge/verhicle_fitment_index.json new file mode 100644 index 0000000..9204b7b --- /dev/null +++ b/llm_rag_knowledge/verhicle_fitment_index.json @@ -0,0 +1,319 @@ +{ + "title": "Ground Vehicle Fitment Index", + "author": "ChrisGBG", + "vehicles": [ + { + "name": "Reliant Kore", + "fits": [ + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Technically fits, not recommended" } + ], + "doesNotFit": [ + "HoverQuad", "Nox", "Dragonfly", "PTV", "ROC", "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Caterpillar", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "PTV", "STV", "Mule", "ROC", "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ], + "notes": [ + { "vehicle": "Mule", "note": "Fits, but can't enter/exit comfortably" } + ] + }, + { + "name": "Avenger Titan", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Technically fits, not recommended" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "PTV", "STV", "Mule", "ROC", "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Cutter", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Technically fits, not recommended" } + ], + "doesNotFit": [ + "ROC", "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Freelancer", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Mule", "ROC", "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Nomad", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "ROC", "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "400i", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Technically fits, not recommended" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Cutlass Black", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Cyclone", "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Freelancer MAX", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "ROC-DS", "Ursa", "Ballista", "Nova" + ] + }, + { + "name": "Starfarer", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Technically fits, not recommended" } + ], + "doesNotFit": [ + "ROC-DS", "Ursa", "Ballista", "Nova" + ], + "notes": [ + { "vehicle": "Cyclone", "note": "Not confirmed" } + ] + }, + { + "name": "Hammerhead", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ursa", "Ballista", "Nova" + ], + "notes": [ + { "vehicle": "Cyclone", "note": "Not confirmed" } + ] + }, + { + "name": "600i", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ballista", "Nova" + ] + }, + { + "name": "MSR", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ballista", "Nova" + ] + }, + { + "name": "Corsair", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ballista", "Nova" + ] + }, + { + "name": "Valkyrie", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ballista", "Nova" + ] + }, + { + "name": "Carrack", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ballista", "Nova" + ] + }, + { + "name": "Constellation Series", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Ballista", "Nova" + ] + }, + { + "name": "890j", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" }, + { "vehicle": "Ballista", "fitment": "Comfortable fit" } + ], + "doesNotFit": [ + "Nova" + ] + }, + { + "name": "Starlifter Series", + "fits": [ + { "vehicle": "HoverQuad", "fitment": "Comfortable fit" }, + { "vehicle": "Nox", "fitment": "Comfortable fit" }, + { "vehicle": "Dragonfly", "fitment": "Comfortable fit" }, + { "vehicle": "PTV", "fitment": "Comfortable fit" }, + { "vehicle": "STV", "fitment": "Comfortable fit" }, + { "vehicle": "Mule", "fitment": "Comfortable fit" }, + { "vehicle": "ROC", "fitment": "Comfortable fit" }, + { "vehicle": "Cyclone", "fitment": "Comfortable fit" }, + { "vehicle": "ROC-DS", "fitment": "Comfortable fit" }, + { "vehicle": "Ursa", "fitment": "Comfortable fit" }, + { "vehicle": "Ballista", "fitment": "Comfortable fit" }, + { "vehicle": "Nova", "fitment": "Comfortable fit" } + ] + } + ], + "legend": { + "Comfortable fit": "Fits without blocking access to entry/exit, doesn't require force or tricks.", + "Uncomfortable Fit": "Fits, but blocks access to entry/exit, doesn't require force or tricks.", + "Technically fits, not recommended": "Fits but requires force or tricks.", + "Doesn't fit": "Including unable to close ramp without coin toss of ship exploding.", + "Fits, but can't enter/exit comfortably": "Caterpillar's elevator doors aren't implemented yet.", + "Not confirmed": "Inferred from vehicles of similar dimensions fitting/not fitting." + } +} \ No newline at end of file diff --git a/llm_rag_knowledge/wikelo_crafting_information_part_1.json b/llm_rag_knowledge/wikelo_crafting_information_part_1.json new file mode 100644 index 0000000..1f1ce59 --- /dev/null +++ b/llm_rag_knowledge/wikelo_crafting_information_part_1.json @@ -0,0 +1,435 @@ +{ + "Wikelo Crafting": [ + { + "Mission": "Noxy Mod", + "Fahrzeugname": "Nox", + "Komponenten": [ + { "Name": "IonWave", "Klasse": "Civilian B" }, + { "Name": "Tepilo", "Klasse": "Civilian A" } + ], + "Kosten": { + "Wikelo Favor": 5 + } + }, + { + "Mission": "Pulse Plus", + "Fahrzeugname": "Pulse", + "Komponenten": [ + { "Name": "Radix", "Klasse": "Civilian A" }, + { "Name": "Kelvid", "Klasse": "Civilian B" } + ], + "Kosten": { + "Wikelo Favor": 5 + } + }, + { + "Mission": "Make a Ursa Mod", + "Fahrzeugname": "Ursa Medivac", + "Komponenten": [ + { "Name": "MagnaBloom", "Klasse": "Civilian B" }, + { "Name": "Castra", "Klasse": "Industrial C" }, + { "Name": "Kelvid", "Klasse": "Civilian B" } + ], + "Kosten": { + "Wikelo Favor": 5, + "Saldynium (Erz)": 40, + "Jaclium (Erz)": 40 + } + }, + { + "Mission": "Upgrade Intrepid", + "Fahrzeugname": "Intrepid", + "Komponenten": [ + { "Name": "WhiteRose", "Klasse": "Civilian A" }, + { "Name": "Palisade", "Klasse": "Industrial A" }, + { "Name": "Atlas", "Klasse": "Civilian A" }, + { "Name": "Ultra-Flow", "Klasse": "Industrial A" } + ], + "Kosten": { + "Wikelo Favor": 5, + "Government Cartography Agency Medal (Makellos)": 1 + } + }, + { + "Mission": "Fortune ship for you", + "Fahrzeugname": "Fortune", + "Komponenten": [ + { "Name": "Lotus", "Klasse": "Civilian A" }, + { "Name": "75A Concord", "Klasse": "Civilian A" }, + { "Name": "Atlas", "Klasse": "Civilian A" }, + { "Name": "Aufeis", "Klasse": "Civilian A" } + ], + "Kosten": { + "Wikelo Favor": 6, + "Carinite (Rein)": 1 + } + }, + { + "Mission": "Spirit Cargo mod", + "Fahrzeugname": "C1 Spirit", + "Komponenten": [ + { "Name": "Lotus", "Klasse": "Civilian A" }, + { "Name": "7MA Lorica", "Klasse": "Civilian A" }, + { "Name": "Hemera", "Klasse": "Civilian A" }, + { "Name": "Aufeis", "Klasse": "Civilian A" } + ], + "Kosten": { + "Wikelo Favor": 8, + "Tevarin War Service Marker (Makellos)": 2 + } + }, + { + "Mission": "Zeus Special", + "Fahrzeugname": "Zeus MK II ES", + "Komponenten": [ + { "Name": "Genoa", "Klasse": "Industrial A" }, + { "Name": "Rampart", "Klasse": "Industrial A" }, + { "Name": "Hemera", "Klasse": "Civilian A" }, + { "Name": "Snowpack", "Klasse": "Industrial A" } + ], + "Kosten": { + "Wikelo Favor": 10, + "UEE 6th Platoon Medal (Makellos)": 2 + } + }, + { + "Mission": "Peregine Wikelo Mod", + "Fahrzeugname": "Sabre Peregrine", + "Komponenten": [ + { "Name": "LumaCore", "Klasse": "Competition A" }, + { "Name": "Jaghte", "Klasse": "Competition B" }, + { "Name": "FoxFire", "Klasse": "Competition B" }, + { "Name": "ZeroRush", "Klasse": "Competition B" } + ], + "Kosten": { + "Wikelo Favor": 8, + "DCHS-05 Comp-Board": 4 + } + }, + { + "Mission": "Guardian", + "Fahrzeugname": "Guardian", + "Komponenten": [ + { "Name": "QuadraCell", "Klasse": "Military A" }, + { "Name": "FR-76", "Klasse": "Military A" }, + { "Name": "VK-00", "Klasse": "Military A" }, + { "Name": "Glacier", "Klasse": "Military A" } + ], + "Kosten": null, + "Notiz": "Kosten fehlen in 4.2.1" + }, + { + "Mission": "Firebird Mod", + "Fahrzeugname": "Sabre Firebird", + "Komponenten": [ + { "Name": "QuadraCell", "Klasse": "Military A" }, + { "Name": "FR-66", "Klasse": "Military A" }, + { "Name": "VK-00", "Klasse": "Military A" }, + { "Name": "Glacier", "Klasse": "Military A" } + ], + "Kosten": { + "Wikelo Favor": 20, + "Polaris Bit": 1, + "DCHS-05 Comp-Board": 1, + "Ace Interceptor Helmet": 5 + } + }, + { + "Mission": "Build a Mod Scorpius", + "Fahrzeugname": "Scorpius", + "Komponenten": [ + { "Name": "Slipstream", "Klasse": "Stealth A" }, + { "Name": "Umbra", "Klasse": "Stealth A" }, + { "Name": "Spectre", "Klasse": "Stealth A" }, + { "Name": "SnowBlind", "Klasse": "Stealth A" } + ], + "Kosten": { + "Wikelo Favor": 20, + "Polaris Bit": 1, + "DCHS-05 Comp-Board": 12, + "Carinite": 10 + } + }, + { + "Mission": "Wikelo Navy F7", + "Fahrzeugname": "F7C Super Hornet Mk II", + "Komponenten": [ + { "Name": "JS-400", "Klasse": "Military A" }, + { "Name": "FR-66", "Klasse": "Military A" }, + { "Name": "VK-00", "Klasse": "Military A" }, + { "Name": "Glacier", "Klasse": "Military A" } + ], + "Kosten": { + "Wikelo Favor": 20, + "DCHS-05 Comp-Board": 6, + "Ace Interceptor Helmet": 5, + "Government Medal (Makellos)": 1 + } + }, + { + "Mission": "Guardian take down ship", + "Fahrzeugname": "Guardian Q1", + "Komponenten": [ + { "Name": "LumaCore", "Klasse": "Competition A" }, + { "Name": "Haltur", "Klasse": "Competition B" }, + { "Name": "SunFire", "Klasse": "Competition B" }, + { "Name": "AbsoluteZero", "Klasse": "Competition B" } + ], + "Kosten": { + "Wikelo Favor": 30, + "DCHS-05 Comp-Board": 15, + "Irradiated Valakkar Pearl (Grad AA)": 15, + "UEE 6th Platoon Medal (Makellos)": 5 + } + }, + { + "Mission": "Zeus Cargo Special", + "Fahrzeugname": "Zeus MK II CL", + "Komponenten": [ + { "Name": "Genoa", "Klasse": "Industrial C" }, + { "Name": "Rampart", "Klasse": "Industrial A" }, + { "Name": "Hemera", "Klasse": "Civilian A" }, + { "Name": "Snowpack", "Klasse": "Industrial A" } + ], + "Kosten": { + "Wikelo Favor": 30, + "Carinite": 24, + "Ace Interceptor Helmet": 15, + "Carinite (Rein)": 2 + } + }, + { + "Mission": "More than a Max", + "Fahrzeugname": "Starlancer MAX", + "Komponenten": [ + { "Name": "Lotus", "Klasse": "Civilian A" }, + { "Name": "Parapet", "Klasse": "Industrial A" }, + { "Name": "Hemera", "Klasse": "Civilian A" }, + { "Name": "Aufeis", "Klasse": "Civilian A" } + ], + "Kosten": { + "Wikelo Favor": 30, + "Ace Interceptor Helmet": 15, + "Carinite (Rein)": 5, + "Irradiated Valakkar Pearl (Grad AAA)": 5 + } + }, + { + "Mission": "Want Taurus ship", + "Fahrzeugname": "Constellation Taurus", + "Komponenten": [ + { "Name": "QuadraCell MT", "Klasse": "Military A" }, + { "Name": "FR-86", "Klasse": "Military A" }, + { "Name": "XL-1", "Klasse": "Military A" }, + { "Name": "Avalanche", "Klasse": "Military A" } + ], + "Kosten": { + "Wikelo Favor": 30, + "Carinite (Rein)": 5, + "Irradiated Valakkar Pearl (Grad AAA)": 5, + "Government Cartography Agency Medal (Makellos)": 5 + } + }, + { + "Mission": "F8 War Mod", + "Fahrzeugname": "F8C Lightning Mil", + "Komponenten": [ + { "Name": "LuxCore", "Klasse": "Competition A" }, + { "Name": "FR-76", "Klasse": "Military A" }, + { "Name": "Colossus", "Klasse": "Industrial B" }, + { "Name": "Glacier", "Klasse": "Military A" } + ], + "Kosten": { + "Wikelo Favor": 40, + "Carinite (Rein)": 6, + "Irradiated Valakkar Pearl (Grad AAA)": 6, + "Tevarin War Service Marker (Makellos)": 6, + "Argo ATLS RX11": 1 + } + }, + { + "Mission": "Sneaky Slabber", + "Fahrzeugname": "F8C Lightning Stealth", + "Komponenten": [ + { "Name": "Eclipse", "Klasse": "Stealth A" }, + { "Name": "Umbra", "Klasse": "Stealth A" }, + { "Name": "Colossus", "Klasse": "Industrial B" }, + { "Name": "SnowBlind", "Klasse": "Stealth A" } + ], + "Kosten": { + "Wikelo Favor": 40, + "DCHS-05 Comp-Board": 20, + "Carinite (Rein)": 5, + "Irradiated Valakkar Pearl (Grad AAA)": 5, + "Xanithule Ascension Helmet": 5 + } + }, + { + "Mission": "Now make Polaris. Short Time Deal.", + "Fahrzeugname": "Polaris", + "Komponenten": [], + "Kosten": { + "Wikelo Favor": 50, + "Polaris Bits": 25, + "DCHS-05 Comp-Board": 20, + "Carinite": 20, + "Irradiated Valakkar Fang (Apex)": 20, + "MG Scrip": 20, + "Ace Interceptor Helmet": 15, + "Irradiated Valakkar Pearl (Grade AAA)": 20, + "UEE 6th Platoon Medal (Pristine)": 20, + "Carinite (Pure)": 20, + "Finley The Stormwall Large Plushie": 1, + "Picotball": 1, + "Janalite": 5, + "Wowblast Desperado Toy Pistol Red": 1, + "Atatium": { + "Menge": 6, + "Notiz": "6 Einheiten mit je 8 SCU (48 SCU total)" + }, + "Scourge Railgun": 10 + } + }, + { + "Mission": "Golem Rocks", + "Fahrzeugname": "Golem", + "Komponenten": [], + "Kosten": { + "Wikelo Favor": 5, + "ASD Secure Drive": 1 + } + }, + { + "Mission": "New Move Big Starlancer Ship", + "Fahrzeugname": "Starlancer TAG", + "Komponenten": [], + "Kosten": { + "Wikelo Favor": 50, + "Ace Interceptor Helmet": 15, + "ASD Secure Drive": 5, + "Irradiated Valakkar Pearl (Grad AAA)": 5, + "Tevarin War Service Marker (Makellos)": 5 + } + }, + { + "Mission": "What is Terrapin?", + "Fahrzeugname": "Terrapin Medivac", + "Komponenten": [], + "Kosten": { + "Wikelo Favor": 15, + "ASD Secure Drive": 5, + "Tevarin War Service Marker (Makellos)": 1 + } + } + ], + "Materialien Quellen": [ + { + "Material": "Ace Interceptor Helmet", + "Quelle": "Beute von gefallenen Ace-Piloten in den neuen Foxwell Patrol Missionen / Headhunters Patrol Missionen. Darf nur Soft Death, sonst verschwindet der Ace Pilot. Kann auch in Bräunungsboxen bei Align & Storm gefunden werden." + }, + { + "Material": "Advocacy Badge (Replica)", + "Quelle": "Beute von Security Post Kareah rund um Cellin, Contested Zones, Larzarus locations etc." + }, + { + "Material": "Argo ATLS Ikti", + "Quelle": "Wikelo Mission." + }, + { + "Material": "Atlasium", + "Quelle": "Handelsware, Daten darüber, wo sie verkauft wird: https://sc-trade.tools/commodities/Atlasium." + }, + { + "Material": "Carinite", + "Quelle": "Align & Mine Erz." + }, + { + "Material": "Carinite (Rein)", + "Quelle": "Seltenes Align & Mine Erz. Nur 1% Drop chance." + }, + { + "Material": "DCHS-05 Comp-Board", + "Quelle": "DCHS-05 Orbital Positioning Comp-Board als Beute von Ghost Arena in Ruin Station." + }, + { + "Material": "Finley The Stormwall Large Plushie", + "Quelle": "Orison August Dunlow Spaceport - Gift Shop & Cloudview Center - Stratus - Kel-To ConStore." + }, + { + "Material": "Government Cartography (Makellos)", + "Quelle": "Beute von Ace Piloten (gute Beute), lootbar von blauen Boxen bei Align & Mine und Storm Breaker Locations und kleinen Boxen bei Derelict Outpost Locations (schreckliche Beute)." + }, + { + "Material": "Irradiated Kopion Horn", + "Quelle": "Irradiated Kopion." + }, + { + "Material": "Irradiated Valakkar Fang (Erwachsen)", + "Quelle": "Erwachsener Irradiated Valakkar - Storm Breaker Lazarus Locations." + }, + { + "Material": "Irradiated Valakkar Fang (Juvenile)", + "Quelle": "Juveniler Irradiated Valakkar - Storm Breaker Lazarus Locations." + }, + { + "Material": "Irradiated Valakkar Pearl (Grad AAA)", + "Quelle": "Apex-Irradiated Valakkar - Storm Breaker Lazarus Locations (Ich bin mir nicht sicher, wie das Grad-System funktioniert)." + }, + { + "Material": "Jaclium (Erz)", + "Quelle": "Align & Mine Erz." + }, + { + "Material": "Janalite", + "Quelle": "Seltenes FPS-Minable-Erz." + }, + { + "Material": "MG Scrip", + "Quelle": "Foxwell Patrol / Ambush Missions, Gilly's Combat Gauntlet missionen." + }, + { + "Material": "Picoball", + "Quelle": "Beute von Aid Shelters, Outposts, Farro Datacenter, Larzarus, Align & Mine Paf sites. Spawnt nicht neu." + }, + { + "Material": "Polaris Bits", + "Quelle": "24x Quantanium, abgebaut mit Schiff (Konvertierung mit 'Want Polaris? Need something special' Mission)." + }, + { + "Material": "Saldynium (Erz)", + "Quelle": "Align & Mine Erz." + }, + { + "Material": "Scourge Railgun", + "Quelle": "Beute von roten Boxen in Bunkern, großen grünen Boxen bei Align & Mine und Storm Breaker Locations." + }, + { + "Material": "Tevarin War Service Marker (Makellos)", + "Quelle": "Beute von Ace-Piloten (gute Beute), lootbar von blauen Boxen bei Align & Mine und Storm Breaker Locations und kleinen Boxen bei Derelict Outpost Locations (schreckliche Beute)." + }, + { + "Material": "UEE 6th Platoon Medal (Makellos)", + "Quelle": "Beute von Ace-Piloten (gute Beute), lootbar von blauen Boxen bei Align & Mine und Storm Breaker Locations und kleinen Boxen bei Derelict Outpost Locations (schreckliche Beute)." + }, + { + "Material": "Wikelo Favor", + "Quelle": "50x MG Scrip / 50x Council Scrip / 50x Carinite / 15x Irradiated Valakkar Pearl." + }, + { + "Material": "Wowblast Desperado Toy Pistol Red", + "Quelle": "Allgemeine Beute. Kann überall gefunden werden." + }, + { + "Material": "Xanthule Ascension Helmet", + "Quelle": "Wikelo hat eine Mission, um die Basisversion in die Aufstiegsversion zu konvertieren, auch verkauft im pledge store." + }, + { + "Material": "Xanthule Ascension Suit", + "Quelle": "Wikelo hat eine Mission, um die Basisversion in die Aufstiegsversion zu konvertieren, auch verkauft im pledge store." + }, + { + "Material": "ASD Secure Drive", + "Quelle": "Kann in den Onyx Facilities in Stanton gelootet werden." + } + ] +} diff --git a/llm_rag_knowledge/wikelo_crafting_information_part_2.json b/llm_rag_knowledge/wikelo_crafting_information_part_2.json new file mode 100644 index 0000000..57835db --- /dev/null +++ b/llm_rag_knowledge/wikelo_crafting_information_part_2.json @@ -0,0 +1,136 @@ +{ + "contracts": [ + { + "name": "Armor with Horn and String", + "items_needed": [ + { "quantity": 30, "item": "Saldynium (Ore)" }, + { "quantity": 15, "item": "Carinite" }, + { "quantity": 45, "item": "Jaclium (Ore)" }, + { "quantity": 1, "item": "Carinite (Pure)" } + ], + "reward_items": [ + { "quantity": 1, "item": "Ana Armor Helmet Endro" }, + { "quantity": 1, "item": "Ana Armor Core Endro" }, + { "quantity": 1, "item": "Ana Armor Arms Endro" }, + { "quantity": 1, "item": "Ana Armor Legs Endro" }, + { "quantity": 1, "item": "Ana Armor Core Endro" }, + { "quantity": 1, "item": "Ana Armor Arms Endro" }, + { "quantity": 1, "item": "Ana Armor Legs Endro" } + ] + }, + + { + "name": "Look at desert but don't see you", + "items_needed": [ + { "quantity": 3, "item": "Wikelo Favor" }, + { "quantity": 5, "item": "Ace Interceptor Helmet" }, + { "quantity": 20, "item": "Advocacy Badge (Replica)" }, + { "quantity": 1, "item": "ADP-mk4 Core Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Arms Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Legs Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Helmet Woodland" } + ], + "reward_items": [ + { "quantity": 1, "item": "DCP Armor Helmet Hunter Camo" }, + { "quantity": 1, "item": "DCP Armor Arms Hunter Camo" }, + { "quantity": 1, "item": "DCP Armor Core Hunter Camo" }, + { "quantity": 1, "item": "DCP Armor Legs Hunter Camo" } + ] + }, + + { + "name": "Want armor look like tree?", + "items_needed": [ + { "quantity": 3, "item": "Wikelo Favor" }, + { "quantity": 5, "item": "Ace Interceptor Helmet" }, + { "quantity": 50, "item": "Valakkar Fang (Juvenile)" }, + { "quantity": 1, "item": "ADP-mk4 Core Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Arms Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Legs Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Helmet Woodland" } + ], + "reward_items": [ + { "quantity": 1, "item": "DCP Armor Helmet Jungle Camo" }, + { "quantity": 1, "item": "DCP Armor Arms Jungle Camo" }, + { "quantity": 1, "item": "DCP Armor Core Jungle Camo" }, + { "quantity": 1, "item": "DCP Armor Legs Jungle Camo" } + ] + }, + + { + "name": "Make space navy armor", + "items_needed": [ + { "quantity": 3, "item": "Wikelo Favor" }, + { "quantity": 5, "item": "Ace Interceptor Helmet" }, + { "quantity": 50, "item": "Grassland Quasi Grazer Egg" }, + { "quantity": 1, "item": "ADP-mk4 Core Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Arms Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Legs Woodland" }, + { "quantity": 1, "item": "ADP-mk4 Helmet Woodland" } + ], + "reward_items": [ + { "quantity": 1, "item": "DCP Armor Helmet Cobalt Camo" }, + { "quantity": 1, "item": "DCP Armor Arms Cobalt Camo" }, + { "quantity": 1, "item": "DCP Armor Core Cobalt Camo" }, + { "quantity": 1, "item": "DCP Armor Legs Cobalt Camo" } + ] + }, + + { + "name": "Make glowy armor", + "items_needed": [ + { "quantity": 1, "item": "Irradiated Valakkar Pearl (Grade AAA)" }, + { "quantity": 2, "item": "Irradiated Valakkar Fang (Apex)" }, + { "quantity": 15, "item": "Irradiated Valakkar Fang (Adult)" }, + { "quantity": 20, "item": "Irradiated Valakkar Fang (Juvenile)" } + ], + "reward_items": [ + { "quantity": 1, "item": "Ana Armor Helmet Endro" }, + { "quantity": 1, "item": "Ana Armor Core Endro" }, + { "quantity": 1, "item": "Ana Armor Arms Endro" }, + { "quantity": 1, "item": "Ana Armor Legs Endro" } + ] + }, + + { + "name": "Walk in danger. Look good.", + "items_needed": [ + { "quantity": 30, "item": "MG Scrip" }, + { "quantity": 1, "item": "Novikov Exploration Suit" }, + { "quantity": 1, "item": "Novikov Helmet" }, + { "quantity": 10, "item": "Irradiated Valakkar Fang (Adult)" }, + { "quantity": 20, "item": "Irradiated Valakkar Fang (Juvenile)" } + ], + "reward_items": [ + { "quantity": 1, "item": "Irradiated Valakkar Pearl (Grade AAA)" } + ] + }, + + { + "name": "Xi'an Xanthule Suit made better", + "items_needed": [ + { "quantity": 20, "item": "MG Scrip" }, + { "quantity": 1, "item": "Xanthule Suit" }, + { "quantity": 1, "item": "Xanthule Helmet" }, + { "quantity": 15, "item": "Ace Interceptor Helmet" }, + { "quantity": 1, "item": "Tevarian War Service Marker (Pristine)" } + ], + "reward_items": [] + }, + + { + "name": "Adventure a A-Venture", + "items_needed": [ + { "quantity": 30, "item": "MG Scrip" }, + { "quantity": 1, "item": "Venture Arms" }, + { "quantity": 1, "item": "Venture Core" }, + { "quantity": 1, "item": "Venture Helmet White" }, + { "quantity": 1, "item": "Venture Legs" }, + { "quantity": 10, "item": "Saldynium (Ore)" }, + { "quantity": 10, "item": "Jaclium (Ore)" }, + { "quantity": 1, "item": "Carinite (Pure)" } + ], + "reward_items": [] + } + ] +} diff --git a/llm_tools/fleetyard.py b/llm_tools/fleetyard.py new file mode 100644 index 0000000..44538cb --- /dev/null +++ b/llm_tools/fleetyard.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +import requests +import sqlite3 +import configparser +from collections import defaultdict +import json + +# Testing out the fleetyard API + +class FleetyardAPI: + def __init__(self, base_url): + self.base_url = base_url + self.session = requests.Session() + + def create_session(self, ini_path="fleetyard_login.ini"): + """Create a new session using credentials from an INI file.""" + config = configparser.ConfigParser() + config.read(ini_path) + login = config.get('login', 'username', fallback=None) + password = config.get('login', 'password', fallback=None) + remember_me = config.getboolean('login', 'rememberMe', fallback=True) + if not login or not password: + raise ValueError("Missing login or password in fleetyard_login.ini") + payload = { + "login": login, + "password": password, + "rememberMe": remember_me, + } + response = self.session.post(f"{self.base_url}/sessions", json=payload) + response.raise_for_status() + return response.json() + + def get_igns_fleet(self): + """Get the IGNs fleet.""" + fleet_url = f"{self.base_url}/fleets/igns/vehicles/export" + response = self.session.get(fleet_url) + response.raise_for_status() + return response.json() + +def process_fleet_data(fleet_data): + """Groups fleet data by ship and aggregates owners.""" + ship_owners = defaultdict(list) + for ship in fleet_data: + if "username" in ship: + key = (ship["manufacturerName"], ship["name"]) + ship_owners[key].append(ship["username"]) + return ship_owners + +def store_in_database(ship_owners): + """Stores the processed fleet data in a SQLite database.""" + db_file = "fleet.db" + conn = sqlite3.connect(db_file) + cursor = conn.cursor() + + # Create table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS ship_owner_list ( + manufacturerName TEXT, + name TEXT, + usernames TEXT, + PRIMARY KEY (manufacturerName, name) + ) + ''') + + # Clear existing data to prevent duplicates on re-runs + cursor.execute('DELETE FROM ship_owner_list') + + # Insert new data + for (manufacturer, ship_name), owners in ship_owners.items(): + usernames_str = ", ".join(sorted(owners)) + cursor.execute( + "INSERT INTO ship_owner_list (manufacturerName, name, usernames) VALUES (?, ?, ?)", + (manufacturer, ship_name, usernames_str) + ) + + conn.commit() + conn.close() + print(f"Data successfully stored in {db_file}") + +if __name__ == "__main__": + base_url = "https://api.fleetyards.net/v1" + api = FleetyardAPI(base_url) + + try: + # Create a session + session_response = api.create_session() + print("Session created.") + + # Get the IGNs fleet + if session_response.get("code") == "success": + fleet_json = api.get_igns_fleet() + print("IGNs fleet data retrieved.") + + # Process the data + processed_data = process_fleet_data(fleet_json) + + # Store in database + store_in_database(processed_data) + + else: + print("Failed to create session, cannot retrieve fleet.") + + except requests.exceptions.HTTPError as e: + print(f"An HTTP error occurred: {e}") + print(f"Response body: {e.response.text}") + except requests.exceptions.RequestException as e: + print(f"A request error occurred: {e}") diff --git a/llm_tools/get_commodities.py b/llm_tools/get_commodities.py new file mode 100644 index 0000000..a3a9231 --- /dev/null +++ b/llm_tools/get_commodities.py @@ -0,0 +1,168 @@ +import requests +import sqlite3 +import time +import schedule +from datetime import datetime + +# --- Configuration --- +API_URL = "https://api.uexcorp.space/2.0/commodities_prices_all" +with open("uex_api_key", "r") as f: + BEARER_TOKEN = f.read().strip() + +DB_NAME = "commodities.db" +TABLE_NAME = "commodity_prices" + +def setup_database(): + """ + Sets up the SQLite database and creates the table if it doesn't exist. + The table uses a composite primary key (id_commodity, id_terminal) + to ensure each commodity at each terminal has only one latest entry. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + # Using "IF NOT EXISTS" prevents errors on subsequent runs. + # The schema is derived from your provided image. + # We use INSERT OR REPLACE later, so a primary key is important. + # (id_commodity, id_terminal) is a good candidate for a unique key. + cursor.execute(f''' + CREATE TABLE IF NOT EXISTS {TABLE_NAME} ( + id INTEGER, + id_commodity INTEGER, + id_terminal INTEGER, + price_buy REAL, + price_buy_avg REAL, + price_sell REAL, + price_sell_avg REAL, + scu_buy REAL, + scu_buy_avg REAL, + scu_sell_stock REAL, + scu_sell_stock_avg REAL, + scu_sell REAL, + scu_sell_avg REAL, + status_buy INTEGER, + status_sell INTEGER, + date_added INTEGER, + date_modified INTEGER, + commodity_name TEXT, + commodity_code TEXT, + commodity_slug TEXT, + terminal_name TEXT, + terminal_code TEXT, + terminal_slug TEXT, + PRIMARY KEY (id_commodity, id_terminal) + ) + ''') + + conn.commit() + conn.close() + print("Database setup complete. Table 'commodity_prices' is ready.") + +def fetch_data_from_api(): + """ + Fetches the latest commodity data from the UAX Corp API. + Returns the data as a list of dictionaries or None if an error occurs. + """ + headers = { + "Authorization": f"Bearer {BEARER_TOKEN}" + } + try: + response = requests.get(API_URL, headers=headers) + # Raise an exception for bad status codes (4xx or 5xx) + response.raise_for_status() + + data = response.json() + if 'data' in data: + return data['data'] + else: + # Handle cases where the structure might be flat + return data + + except requests.exceptions.RequestException as e: + print(f"Error fetching data from API: {e}") + return None + +def save_data_to_db(data): + """ + Saves the fetched data into the SQLite database. + Uses 'INSERT OR REPLACE' to update existing records for a + commodity/terminal pair or insert new ones. + """ + if not data: + print("No data to save.") + return + + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + # Prepare data for insertion + records_to_insert = [] + for item in data: + # The order of values must match the table schema + records_to_insert.append(( + item.get('id'), + item.get('id_commodity'), + item.get('id_terminal'), + item.get('price_buy'), + item.get('price_buy_avg'), + item.get('price_sell'), + item.get('price_sell_avg'), + item.get('scu_buy'), + item.get('scu_buy_avg'), + item.get('scu_sell_stock'), + item.get('scu_sell_stock_avg'), + item.get('scu_sell'), + item.get('scu_sell_avg'), + item.get('status_buy'), + item.get('status_sell'), + item.get('date_added'), + item.get('date_modified'), + item.get('commodity_name'), + item.get('commodity_code'), + item.get('commodity_slug'), + item.get('terminal_name'), + item.get('terminal_code'), + item.get('terminal_slug') + )) + + # Using executemany is much more efficient than one by one + sql_statement = f''' + INSERT OR REPLACE INTO {TABLE_NAME} ( + id, id_commodity, id_terminal, price_buy, price_buy_avg, price_sell, + price_sell_avg, scu_buy, scu_buy_avg, scu_sell_stock, scu_sell_stock_avg, + scu_sell, scu_sell_avg, status_buy, status_sell, date_added, date_modified, + commodity_name, commodity_code, commodity_slug, terminal_name, terminal_code, + terminal_slug + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + + cursor.executemany(sql_statement, records_to_insert) + + conn.commit() + conn.close() + + print(f"Successfully saved/updated {len(records_to_insert)} records to the database.") + +def job(): + """The main job function to be scheduled.""" + print(f"--- Running job at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ---") + api_data = fetch_data_from_api() + if api_data: + save_data_to_db(api_data) + print("--- Job finished ---") + +if __name__ == "__main__": + # 1. Set up the database and table on the first run. + setup_database() + + # 2. Run the job immediately once when the script starts. + job() + + # 3. Schedule the job to run every hour. + print(f"Scheduling job to run every hour. Press Ctrl+C to exit.") + schedule.every().hour.do(job) + + # 4. Run the scheduler loop. + while True: + schedule.run_pending() + time.sleep(1) diff --git a/llm_tools/get_items.py b/llm_tools/get_items.py new file mode 100644 index 0000000..f8bed36 --- /dev/null +++ b/llm_tools/get_items.py @@ -0,0 +1,162 @@ +import requests +import sqlite3 +import time +import schedule +from datetime import datetime + +# --- Configuration for Item Prices --- +API_URL = "https://api.uexcorp.space/2.0/items_prices_all" +with open("uex_api_key", "r") as f: + BEARER_TOKEN = f.read().strip() +DB_NAME = "items.db" # Using a dedicated DB file +TABLE_NAME = "item_prices" + +def setup_item_database(): + """ + Sets up the SQLite database and creates the item_prices table if it doesn't exist. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + # Schema is derived from the new API documentation. + cursor.execute(f''' + CREATE TABLE IF NOT EXISTS {TABLE_NAME} ( + id INTEGER, + id_item INTEGER, + id_terminal INTEGER, + id_category INTEGER, + price_buy REAL, + price_sell REAL, + date_added INTEGER, + date_modified INTEGER, + item_name TEXT, + item_uuid TEXT, + terminal_name TEXT, + PRIMARY KEY (id_item, id_terminal) + ) + ''') + + conn.commit() + conn.close() + print(f"Database setup complete. Table '{TABLE_NAME}' is ready.") + +def fetch_item_data_from_api(): + """ + Fetches the latest item price data from the UAX Corp API. + Returns the data as a list of dictionaries or None if an error occurs. + """ + headers = { + "Authorization": f"Bearer {BEARER_TOKEN}" + } + try: + response = requests.get(API_URL, headers=headers) + response.raise_for_status() # Check for HTTP errors + + data = response.json() + if 'data' in data: + return data['data'] + return data + + except requests.exceptions.RequestException as e: + print(f"Error fetching item data from API: {e}") + return None + +def sync_data_with_db(data): + """ + Synchronizes the database with the fetched API data. + It de-duplicates the source data, then deletes all old entries and + inserts the new ones in a single transaction. + """ + if not data: + print("No data received from API. Database will not be changed.") + return + + # --- De-duplication Step --- + # The API is returning duplicates for (id_item, id_terminal). + # We will process the list and keep only the one with the latest 'date_modified'. + unique_items = {} + for item in data: + key = (item.get('id_item'), item.get('id_terminal')) + if key not in unique_items or item.get('date_modified') > unique_items[key].get('date_modified'): + unique_items[key] = item + + # The final list of records to insert is the values of our de-duplicated dictionary. + clean_data = list(unique_items.values()) + print(f"Received {len(data)} records from API. After de-duplication, {len(clean_data)} unique records will be processed.") + + conn = None + try: + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + # --- Start Transaction --- + # 1. Delete all existing records from the table. + cursor.execute(f"DELETE FROM {TABLE_NAME}") + print(f"Cleared all old records from '{TABLE_NAME}'.") + + # 2. Prepare the new, clean records for insertion. + records_to_insert = [] + for item in clean_data: # Use the clean_data list now + records_to_insert.append(( + item.get('id'), + item.get('id_item'), + item.get('id_terminal'), + item.get('id_category'), + item.get('price_buy'), + item.get('price_sell'), + item.get('date_added'), + item.get('date_modified'), + item.get('item_name'), + item.get('item_uuid'), + item.get('terminal_name') + )) + + # 3. Insert all new records. + sql_statement = f''' + INSERT INTO {TABLE_NAME} ( + id, id_item, id_terminal, id_category, price_buy, price_sell, + date_added, date_modified, item_name, item_uuid, terminal_name + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + cursor.executemany(sql_statement, records_to_insert) + + # --- Commit Transaction --- + conn.commit() + print(f"Successfully synchronized {len(records_to_insert)} records into the database.") + + except sqlite3.Error as e: + print(f"Database error: {e}") + if conn: + print("Rolling back changes.") + conn.rollback() + finally: + if conn: + conn.close() + +def item_sync_job(): + """The main job function to be scheduled for syncing item prices.""" + print(f"--- Running item sync job at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ---") + api_data = fetch_item_data_from_api() + sync_data_with_db(api_data) + print("--- Item sync job finished ---") + + +if __name__ == "__main__": + # 1. Set up the database and table on the first run. + setup_item_database() + + # 2. Run the job immediately once when the script starts. + item_sync_job() + + # 3. Schedule the job to run every hour. + print(f"Scheduling item sync job to run every hour. Press Ctrl+C to exit.") + schedule.every().hour.do(item_sync_job) + + # 4. Run the scheduler loop. + while True: + try: + schedule.run_pending() + time.sleep(1) + except KeyboardInterrupt: + print("\nExiting scheduler.") + break diff --git a/llm_tools/sample_fleetyard_login.ini b/llm_tools/sample_fleetyard_login.ini new file mode 100644 index 0000000..0d39606 --- /dev/null +++ b/llm_tools/sample_fleetyard_login.ini @@ -0,0 +1,4 @@ +[login] +username = YOUR_USERNAME +password = YOUR_PASSWORD +rememberMe = true diff --git a/llm_tools/sample_uex_api_key b/llm_tools/sample_uex_api_key new file mode 100644 index 0000000..a2a163d --- /dev/null +++ b/llm_tools/sample_uex_api_key @@ -0,0 +1 @@ +YOUR_API_KEY_HERE diff --git a/llm_tools/star_citizen_info_retrieval.py b/llm_tools/star_citizen_info_retrieval.py new file mode 100644 index 0000000..a1df8a4 --- /dev/null +++ b/llm_tools/star_citizen_info_retrieval.py @@ -0,0 +1,792 @@ +#!/usr/bin/env python3 + +import requests, asyncio, json, sqlite3 +from bs4 import BeautifulSoup +from fuzzywuzzy import process +from typing import Callable, Any + + +class EventEmitter: + def __init__(self, event_emitter: Callable[[dict], Any] = None): + self.event_emitter = event_emitter + + async def progress_update(self, description): + await self.emit(description) + + async def error_update(self, description): + await self.emit(description, "error", True) + + async def success_update(self, description): + await self.emit(description, "success", True) + + async def emit(self, description="Unknown State", status="in_progress", done=False): + if self.event_emitter: + await self.event_emitter( + { + "type": "status", + "data": { + "status": status, + "description": description, + "done": done, + }, + } + ) + + +class get_information: + def __init__(self): + self.base_url = "https://starcitizen.tools" + self.db_path = "/app/sc_databases" + + async def get_all_vehicle_names(self): + """Fetches all vehicle names from the list of pledge vehicles using the MediaWiki API.""" + api_url = f"{self.base_url}/api.php" + vehicle_names = [] + categories = ["Category:Pledge ships", "Category:Pledge vehicles"] + for category in categories: + params = { + "action": "query", + "format": "json", + "list": "categorymembers", + "cmtitle": category, + "cmlimit": "max", # Use max limit (500) + "cmprop": "title", + } + + while True: + try: + response = await asyncio.to_thread( + requests.get, api_url, params=params + ) + response.raise_for_status() + data = response.json() + + if "query" in data and "categorymembers" in data["query"]: + for member in data["query"]["categorymembers"]: + vehicle_names.append(member["title"]) + + # Check for continuation to get the next page of results + if "continue" in data and "cmcontinue" in data["continue"]: + params["cmcontinue"] = data["continue"]["cmcontinue"] + else: + break # No more pages + + except requests.exceptions.RequestException as e: + print(f"Error fetching vehicle list for {category}: {e}") + break # Stop processing this category + except json.JSONDecodeError: + print(f"Error decoding JSON from response for {category}.") + break # Stop processing this category + + if not vehicle_names: + print("No vehicle names found.") + return [] + + # Remove duplicates and sort the list + return sorted(list(set(vehicle_names))) + + async def get_closest_vehicle_name(self, vehicle_name): + """Finds the closest matching vehicle name using fuzzy matching.""" + all_vehicle_names = await self.get_all_vehicle_names() + # print(f"Total vehicle names found: {len(all_vehicle_names)}") + if not all_vehicle_names: + return None + + closest_name, _ = process.extractOne(vehicle_name, all_vehicle_names) + return closest_name + + async def fetch_infos(self, ship_name): + """Fetches ship information from the Star Citizen wiki using the MediaWiki API.""" + closest_name = await self.get_closest_vehicle_name(ship_name) + if not closest_name: + print(f"No matching vehicle found for {ship_name}.") + return None + + # Use the closest name found for the API call + page_title = closest_name.replace(" ", "_") + api_url = f"{self.base_url}/api.php" + params = { + "action": "parse", + "page": page_title, + "format": "json", + "prop": "text", # We only need the parsed HTML content + } + + try: + response = await asyncio.to_thread(requests.get, api_url, params=params) + response.raise_for_status() + data = response.json() + + if "error" in data: + print(f"API Error for {page_title}: {data['error']['info']}") + return None + + html_content = data.get("parse", {}).get("text", {}).get("*", "") + if not html_content: + print(f"No content found for {page_title}.") + return None + + except requests.exceptions.RequestException as e: + print(f"Error fetching data for {page_title}: {e}") + return None + except json.JSONDecodeError: + print(f"Error decoding JSON from response for {page_title}.") + return None + + soup = BeautifulSoup(html_content, "html.parser") + info = {} + + # Extracting ship information from the parsed HTML + info["general"] = await self._extract_infobox_data(soup) + info["specifications"] = await self._extract_specifications(soup) + + return info + + async def _extract_infobox_data(self, soup): + """Extracts data from the infobox.""" + infobox_data = {} + infobox = soup.find("details", class_="infobox") + if not infobox: + return infobox_data + + items = infobox.find_all("div", class_="infobox__item") + for item in items: + label_tag = item.find("div", class_="infobox__label") + data_tag = item.find("div", class_="infobox__data") + + if label_tag and data_tag: + label = label_tag.get_text(strip=True) + # For loaners, get all ship names + if "loaner" in label.lower(): + value = [a.get_text(strip=True) for a in data_tag.find_all("a")] + else: + value = data_tag.get_text(separator=" ", strip=True) + + infobox_data[label] = value + return infobox_data + + async def _extract_specifications(self, soup): + """Extracts data from the specifications tabs.""" + specifications = {} + + # Find all specification tabs like "Avionics & Systems", "Weaponry", etc. + tabs = soup.select("div.tabber > section > article.tabber__panel") + + for panel in tabs: + panel_id = panel.get("id", "") + tab_name_tag = soup.find("a", {"aria-controls": panel_id}) + if not tab_name_tag: + continue + + tab_name = tab_name_tag.get_text(strip=True) + specifications[tab_name] = {} + + # Find all component groups in the panel + component_groups = panel.find_all( + "div", class_="template-components__section" + ) + for group in component_groups: + label_tag = group.find("div", class_="template-components__label") + if not label_tag: + continue + + category = label_tag.get_text(strip=True) + components = [] + + # Find all component cards in the group + component_cards = group.select(".template-component__card") + for card in component_cards: + count_tag = card.select_one(".template-component__count") + size_tag = card.select_one(".template-component__size") + title_tag = card.select_one(".template-component__title") + + if count_tag and size_tag and title_tag: + count = count_tag.get_text(strip=True) + size = size_tag.get_text(strip=True) + title = title_tag.get_text(strip=True) + components.append(f"{count} {size} {title}") + + if components: + # If the category already exists, append to it (for Thrusters) + if category in specifications[tab_name]: + specifications[tab_name][category].extend(components) + else: + specifications[tab_name][category] = components + + return specifications + + async def fetch_all_commodity_names(self): + """ + Fetches all commodity names from the database and sort them uniquely and returns a string. + """ + conn = sqlite3.connect(self.db_path + "/commodities.db") + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT commodity_name FROM commodity_prices") + rows = cursor.fetchall() + conn.close() + return_string = "\n".join([row[0] for row in rows]) + return return_string + + async def fetch_all_item_names(self): + """ + Fetches all item names from the database and sort them uniquely and returns a string. + """ + conn = sqlite3.connect(self.db_path + "/items.db") + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT item_name FROM item_prices") + rows = cursor.fetchall() + conn.close() + return_string = "\n".join([row[0] for row in rows]) + return return_string + + async def get_all_ship_names_from_fleetyard_db(self): + """ + Fetches all ship names from the fleet.db database and returns a string. + """ + conn = sqlite3.connect(self.db_path + "/fleet.db") + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT name FROM ship_owner_list") + rows = cursor.fetchall() + conn.close() + return_string = "\n".join([row[0] for row in rows]) + return return_string + +class Tools: + def __init__(self): + self.db_path = "/app/sc_databases" + + async def get_ship_details( + self, ship_name: str, __event_emitter__: Callable[[dict], Any] = None + ): + emitter = EventEmitter(__event_emitter__) + # The API call in fetch_infos now handles fuzzy matching and name formatting. + # ship_name = await get_information().get_closest_vehicle_name(ship_name) + # ship_name = ship_name.title().replace(" ", "_") + + await emitter.progress_update("Fetching ship information for " + ship_name) + info = await get_information().fetch_infos(ship_name) + + if info: + await emitter.success_update( + "Successfully fetched ship information for " + ship_name + ) + await emitter.progress_update("Processing retrieved information...") + output_lines = [] + # Build the output string + output_lines.append(f"Information for {ship_name}:") + if info.get("general"): + await emitter.progress_update("Processing general information...") + output_lines.append("\n--- General Information ---") + for key, value in info["general"].items(): + if isinstance(value, list): + output_lines.append(f"{key}: {', '.join(value)}") + else: + if "Size" in key: + # Only print the first word for size-related keys + value = value.split()[0] if value else "" + if "Stowage" in key: + # Replace 'Stowage' with 'Storage': + key = key.replace("Stowage", "Storage") + output_lines.append(f"{key}: {value}") + + if info.get("specifications"): + await emitter.progress_update("Processing specifications...") + output_lines.append("\n--- Specifications ---") + for spec_area, details in info["specifications"].items(): + if not details: + continue + output_lines.append(f"\n[{spec_area}]") + for category, items in details.items(): + output_lines.append(f" {category}:") + for item in items: + output_lines.append(f" - {item}") + + final_output = "\n".join(output_lines) + print(final_output) + await emitter.success_update(final_output) + return final_output + else: + error_message = f"No information found for {ship_name}." + print(error_message) + await emitter.error_update(error_message) + return error_message + + async def compare_ships( + self, + ship_name1: str, + ship_name2: str, + __event_emitter__: Callable[[dict], Any] = None, + ): + # ship_name1 = ship_name1.title().replace(" ", "_") + # ship_name2 = ship_name2.title().replace(" ", "_") + + emitter = EventEmitter(__event_emitter__) + await emitter.progress_update( + f"Fetching ship information for {ship_name1} and {ship_name2}" + ) + info1 = await get_information().fetch_infos(ship_name1) + if info1: + await emitter.success_update( + f"Successfully fetched ship information for {ship_name1}" + ) + output_lines = [f"Information for {ship_name1}:"] + if info1.get("general"): + await emitter.progress_update( + "Processing general information for " + ship_name1 + ) + output_lines.append("\n--- General Information ---") + for key, value in info1["general"].items(): + if isinstance(value, list): + output_lines.append(f"{key}: {', '.join(value)}") + else: + if "Size" in key: + value = value.split()[0] if value else "" + if "Stowage" in key: + key = key.replace("Stowage", "Storage") + output_lines.append(f"{key}: {value}") + + if info1.get("specifications"): + await emitter.progress_update( + "Processing specifications for " + ship_name1 + ) + output_lines.append("\n--- Specifications ---") + for spec_area, details in info1["specifications"].items(): + if not details: + continue + output_lines.append(f"\n[{spec_area}]") + for category, items in details.items(): + output_lines.append(f" {category}:") + for item in items: + output_lines.append(f" - {item}") + final_output1 = "\n".join(output_lines) + + info2 = await get_information().fetch_infos(ship_name2) + if info2: + await emitter.success_update( + f"Successfully fetched ship information for {ship_name2}" + ) + output_lines = [f"Information for {ship_name2}:"] + if info2.get("general"): + await emitter.progress_update( + "Processing general information for " + ship_name2 + ) + output_lines.append("\n--- General Information ---") + for key, value in info2["general"].items(): + if isinstance(value, list): + output_lines.append(f"{key}: {', '.join(value)}") + else: + if "Size" in key: + value = value.split()[0] if value else "" + if "Stowage" in key: + key = key.replace("Stowage", "Storage") + output_lines.append(f"{key}: {value}") + if info2.get("specifications"): + await emitter.progress_update( + "Processing specifications for " + ship_name2 + ) + output_lines.append("\n--- Specifications ---") + for spec_area, details in info2["specifications"].items(): + if not details: + continue + output_lines.append(f"\n[{spec_area}]") + for category, items in details.items(): + output_lines.append(f" {category}:") + for item in items: + output_lines.append(f" - {item}") + + final_output2 = "\n".join(output_lines) + await emitter.success_update(final_output2) + print(final_output1 + "\n\n" + final_output2) + return final_output1 + "\n\n" + final_output2 + + async def get_commodity_prices( + self, commodity_name: str, __event_emitter__: Callable[[dict], Any] = None + ): + """ + Fetch commodities from the database by name. + + commodity_name: The name of the commodity to fetch. + """ + emitter = EventEmitter(__event_emitter__) + result_string = f"No information found for commodity '{commodity_name}'." + # First, check for spelling issues and compare it to the list of all commodity names available + try: + await emitter.progress_update( + f"Fetching commodity names from the database to find a match for '{commodity_name}'" + ) + all_names = await get_information().fetch_all_commodity_names() + # The names are returned as a single string, split it into a list + names_list = all_names.splitlines() + best_match = process.extractOne(commodity_name, names_list) + if ( + best_match and best_match[1] > 60 + ): # If the match is above 60% confidence + matched_commodity_name = best_match[0] + await emitter.success_update( + f"Found a close match for '{commodity_name}': {matched_commodity_name}" + ) + conn = sqlite3.connect(self.db_path + "/commodities.db") + cursor = conn.cursor() + await emitter.progress_update( + f"Fetching buy and sell prices for '{matched_commodity_name}'" + ) + cursor.execute( + "SELECT price_buy, price_sell, terminal_name, commodity_name FROM commodity_prices WHERE commodity_name = ?", + (matched_commodity_name,), + ) + await emitter.progress_update( + f"Processing results for '{matched_commodity_name}'" + ) + rows = cursor.fetchall() + conn.close() + if rows: + output_lines = [] + for row in rows: + buy_price = ( + "Not buyable" + if int(row[0]) == 0 + else f"{int(row[0])} aUEC" + ) + sell_price = ( + "not sellable" + if int(row[1]) == 0 + else f"{int(row[1])} aUEC" + ) + output_lines.append( + f"Item: {row[3]}, Buy Price: {buy_price} aUEC, Sell Price: {sell_price} aUEC, Terminal: {row[2]}" + ) + result_string = "\n".join(output_lines) + await emitter.success_update( + f"Successfully fetched buy and sell prices for '{matched_commodity_name}'" + ) + else: + result_string = ( + f"No price data found for '{matched_commodity_name}'." + ) + await emitter.error_update(result_string) + else: + result_string = f"Could not find a confident match for commodity '{commodity_name}'. Best guess was '{best_match[0]}' with {best_match[1]}% confidence." + await emitter.error_update(result_string) + + except Exception as e: + error_message = f"An error occurred while fetching information for {commodity_name}: {str(e)}" + await emitter.error_update(error_message) + result_string = error_message + + print(result_string) + return result_string + + async def get_item_prices( + self, item_name: str, __event_emitter__: Callable[[dict], Any] = None + ): + """ + Fetch item prices from the database by name. + item_name: The name of the item to fetch. + """ + emitter = EventEmitter(__event_emitter__) + result_string = f"No information found for item '{item_name}'." + # First, check for spelling issues and compare it to the list of all item names available + try: + await emitter.progress_update( + f"Fetching item names from the database to find a match for '{item_name}'" + ) + all_names = await get_information().fetch_all_item_names() + # The names are returned as a single string, split it into a list + names_list = all_names.splitlines() + best_match = process.extractOne(item_name, names_list) + if best_match and best_match[1] > 60: + matched_item_name = best_match[0] + await emitter.success_update( + f"Found a close match for '{item_name}': {matched_item_name}" + ) + conn = sqlite3.connect(self.db_path + "/items.db") + cursor = conn.cursor() + await emitter.progress_update( + f"Fetching buy and sell prices for '{matched_item_name}'" + ) + cursor.execute( + "SELECT price_buy, price_sell, terminal_name, item_name FROM item_prices WHERE item_name = ?", + (matched_item_name,), + ) + await emitter.progress_update( + f"Processing results for '{matched_item_name}'" + ) + rows = cursor.fetchall() + conn.close() + if rows: + output_lines = [] + for row in rows: + buy_price = ( + "Not buyable" + if int(row[0]) == 0 + else f"{int(row[0])} aUEC" + ) + sell_price = ( + "not sellable" + if int(row[1]) == 0 + else f"{int(row[1])} aUEC" + ) + output_lines.append( + f"Item: {row[3]}, Buy Price: {buy_price}, Sell Price: {sell_price}, Terminal: {row[2]}" + ) + result_string = "\n".join(output_lines) + await emitter.success_update( + f"Successfully fetched buy and sell prices for '{matched_item_name}'" + ) + else: + result_string = f"No price data found for '{matched_item_name}'." + await emitter.error_update(result_string) + else: + result_string = f"Could not find a confident match for item '{item_name}'. Best guess was '{best_match[0]}' with {best_match[1]}% confidence." + await emitter.error_update(result_string) + except Exception as e: + error_message = f"An error occurred while fetching information for {item_name}: {str(e)}" + await emitter.error_update(error_message) + result_string = error_message + print(result_string) + return result_string + + async def get_ship_owners(self, ship_name: str, __event_emitter__: Callable[[dict], Any] = None + ): + """ + Fetches the owners of a specific ship from the fleet.db sqlite database. + + ship_name: The name of the ship to fetch owners for. + """ + emitter = EventEmitter(__event_emitter__) + result_string = f"No owners found for ship '{ship_name}'." + try: + await emitter.progress_update( + f"Fetching owners for ship '{ship_name}' from the database" + ) + available_ships = await get_information().get_all_ship_names_from_fleetyard_db() + # The names are returned as a single string, split it into a list + ships_list = available_ships.splitlines() + best_match = process.extractOne(ship_name, ships_list) + if best_match and best_match[1] > 60: + matched_ship_name = best_match[0] + await emitter.success_update( + f"Found a close match for '{ship_name}': {matched_ship_name}" + ) + print(f'found a close match for "{ship_name}": {matched_ship_name}') + conn = sqlite3.connect(self.db_path + "/fleet.db") + cursor = conn.cursor() + await emitter.progress_update( + f"Fetching owners for ship '{matched_ship_name}' from the database" + ) + cursor.execute( + "SELECT manufacturerName, name, usernames FROM ship_owner_list WHERE name = ?", + (matched_ship_name,), + ) + rows = cursor.fetchall() + conn.close() + if rows: + owners = [row[2] for row in rows] + manufacturer_name = rows[0][0] + matched_ship_name = rows[0][1] + result_string = f"Please report these to the user in a bulletpoint list:\nOwners of ship {manufacturer_name} {matched_ship_name}: {', '.join(owners)}" + except Exception as e: + error_message = f"An error occurred while fetching owners for {ship_name}: {str(e)}" + await emitter.error_update(error_message) + result_string = error_message + await emitter.progress_update(result_string) + print(result_string) + return result_string + + async def list_purchasable_ships( + self, __event_emitter__: Callable[[dict], Any] = None + ): + """ + Fetches all buyable ships, their prices, and locations from the Star Citizen Tools wiki. + """ + emitter = EventEmitter(__event_emitter__) + api_url = "https://starcitizen.tools/api.php" + ship_data = {} + page_title = "Purchasing_ships" + + await emitter.progress_update(f"Fetching data from {page_title}...") + params = { + "action": "parse", + "page": page_title, + "format": "json", + "prop": "text", + } + try: + response = await asyncio.to_thread(requests.get, api_url, params=params) + response.raise_for_status() + data = response.json() + + if "error" in data: + await emitter.error_update( + f"API Error for {page_title}: {data['error']['info']}" + ) + return + html_content = data.get("parse", {}).get("text", {}).get("*", "") + if not html_content: + await emitter.error_update(f"No content found for {page_title}.") + return + + await emitter.progress_update(f"Parsing data from {page_title}...") + soup = BeautifulSoup(html_content, "html.parser") + tables = soup.find_all("table", class_="wikitable") + + for table in tables: + header_row = table.find("tr") + if not header_row: + continue + headers = [th.get_text(strip=True) for th in header_row.find_all("th")] + + rows = table.find_all("tr")[1:] + for row in rows: + cells = row.find_all("td") + if not cells or len(cells) < 3: + continue + + ship_name_tag = cells[1].find("a") + if not ship_name_tag or not ship_name_tag.get("title"): + continue + ship_name = ship_name_tag.get("title").strip() + price = cells[2].get_text(strip=True) + + if ship_name not in ship_data: + ship_data[ship_name] = [] + + location_headers = headers[3:] + for i, cell in enumerate(cells[3:]): + if "✔" in cell.get_text(): + location = location_headers[i] + ship_data[ship_name].append( + {"price": price + " aUEC (alpha United Earth Credits)", "location": location} + ) + + await emitter.success_update(f"Successfully processed {page_title}.") + + except requests.exceptions.RequestException as e: + await emitter.error_update(f"Error fetching data for {page_title}: {e}") + except json.JSONDecodeError: + await emitter.error_update(f"Error decoding JSON for {page_title}.") + + output_lines = [] + for ship_name, locations in sorted(ship_data.items()): + output_lines.append(f"\n--- {ship_name} ---") + output_lines.append("Buyable at:") + for item in locations: + output_lines.append( + f" - Location: {item['location']}, Price: {item['price']}" + ) + + final_output = "\n".join(output_lines) + await emitter.success_update(f"Found {len(ship_data)} unique buyable ships.") + print(final_output) + return final_output + + async def list_rentable_ships( + self, __event_emitter__: Callable[[dict], Any] = None + ): + """ + Fetches all rentable ships, their prices, and locations from the Star Citizen Tools wiki. + """ + emitter = EventEmitter(__event_emitter__) + api_url = "https://starcitizen.tools/api.php" + ship_prices = {} + ship_locations = {} + page_title = "Ship_renting" + + await emitter.progress_update(f"Fetching data from {page_title}...") + params = { + "action": "parse", + "page": page_title, + "format": "json", + "prop": "text", + } + try: + response = await asyncio.to_thread(requests.get, api_url, params=params) + response.raise_for_status() + data = response.json() + + if "error" in data: + await emitter.error_update( + f"API Error for {page_title}: {data['error']['info']}" + ) + return + html_content = data.get("parse", {}).get("text", {}).get("*", "") + if not html_content: + await emitter.error_update(f"No content found for {page_title}.") + return + + await emitter.progress_update(f"Parsing data from {page_title}...") + soup = BeautifulSoup(html_content, "html.parser") + tables = soup.find_all("table", class_="wikitable") + + for table in tables: + header_row = table.find("tr") + if not header_row: + continue + headers = [th.get_text(strip=True) for th in header_row.find_all("th")] + rows = table.find_all("tr")[1:] + + # Table 1: Ship rental prices + if "1 Day" in headers and "Location" in headers: + for row in rows: + cells = row.find_all("td") + if len(cells) < 8: + continue + ship_name_tag = cells[1].find("a") + if not ship_name_tag or not ship_name_tag.get("title"): + continue + ship_name = ship_name_tag.get("title").strip() + ship_prices[ship_name] = { + "1_day": cells[3].get_text(strip=True), + "3_days": cells[4].get_text(strip=True), + "7_days": cells[5].get_text(strip=True), + "30_days": cells[6].get_text(strip=True), + } + # Table 2: Ship rental locations + elif "Area18" in headers: + location_headers = headers[3:] + for row in rows: + cells = row.find_all("td") + if len(cells) < 4: + continue + ship_name_tag = cells[1].find("a") + if not ship_name_tag or not ship_name_tag.get("title"): + continue + ship_name = ship_name_tag.get("title").strip() + if ship_name not in ship_locations: + ship_locations[ship_name] = [] + for i, cell in enumerate(cells[3:]): + if "✔" in cell.get_text(): + ship_locations[ship_name].append(location_headers[i]) + + await emitter.success_update(f"Successfully processed {page_title}.") + + except requests.exceptions.RequestException as e: + await emitter.error_update(f"Error fetching data for {page_title}: {e}") + except json.JSONDecodeError: + await emitter.error_update(f"Error decoding JSON for {page_title}.") + + output_lines = [] + for ship_name, locations in sorted(ship_locations.items()): + if not locations: + continue + output_lines.append(f"\n--- {ship_name} ---") + output_lines.append("Rentable at:") + prices = ship_prices.get(ship_name, {}) + for location in locations: + output_lines.append(f" - Location: {location}") + if prices: + output_lines.append( + f" - 1 Day: {prices.get('1_day', 'N/A')}, 3 Days: {prices.get('3_days', 'N/A')}, 7 Days: {prices.get('7_days', 'N/A')}, 30 Days: {prices.get('30_days', 'N/A')}" + ) + + final_output = "\n".join(output_lines) + await emitter.success_update( + f"Found {len(ship_locations)} unique rentable ships." + ) + print(final_output) + return final_output + + +if __name__ == "__main__": + info_printer = Tools() + asyncio.run(info_printer.get_ship_owners("Perseus"))