diff --git a/CHANGELOG.md b/CHANGELOG.md index 34799f7..c6de5a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,46 @@ -- fix: bug that disabled fancy tab and hud in Skyblocker when selecting Skyhanni compact tab in welcome wizard \ No newline at end of file +# Changelog + +## v4.0.0 + +### Core Features +- First-launch **Welcome Wizard** for guided setup +- Custom **main menu styles** (Modern, Modern Minimal, Minimal) +- Built-in **performance profile selector** +- Visual customization pages for: + - Tab design + - Item background style + - Storage design +- Optional wizard pages for supported mods: + - **ScaleMe** sword block toggle + - **ScamScreener** alert + ping setup +- **Resource pack selection** during setup +- Final **review + apply** page with per-setting status + +### Config Pack System +- Automatic config pack detection and loading +- Resolution-based best-match config selection +- Safer update behavior (tracks applied pack + version) +- Restart-safe pending apply flow for full preset changes + +### Config Manager UI +- New in-game **modpack config screen** with tabs: + - Configuration + - Export + - Import + - Backups +- Browse config pack contents before applying +- Apply **selected files** or apply **entire preset** + +### Export / Import / Backups +- Export selected files as reusable config pack `.zip` +- Include metadata in exports (name, version, author, description, target resolution, GUI scale) +- Import external config packs from imports folder +- Create and restore backups from in-game UI + +### Commands & Utilities +- Command to reopen wizard: `/packcore wizard` +- Command to open config manager: `/packcore modpack_config` +- Update check commands: + - `/packcore update check` + - `/packcore update reset` +- Performance/design quick commands for advanced users diff --git a/LICENSE b/LICENSE index 088231f..3f09b05 100644 --- a/LICENSE +++ b/LICENSE @@ -1,71 +1,164 @@ -# PolyForm Perimeter License 1.0.1 +# PolyForm Shield License 1.0.0 - + ## Acceptance -In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. ## Copyright License -The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license). +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). ## Distribution License -The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license). +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). ## Notices -You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: > Required Notice: Copyright Yoyodyne, Inc. (http://example.com) ## Changes and New Works License -The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. ## Patent License -The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. ## Noncompete -Any purpose is a permitted purpose, except for providing to others any product that competes with the software. +Any purpose is a permitted purpose, except for providing any +product that competes with the software or any product the +licensor or any of its affiliates provides using the software. ## Competition -If you use this software to market a product as a substitute for the functionality or value of the software, it competes with the software. A product may compete regardless how it is designed or deployed. For example, a product may compete even if it provides its functionality via any kind of interface (including services, libraries or plug-ins), even if it is ported to a different platform or programming language, and even if it is provided free of charge. +Goods and services compete even when they provide functionality +through different kinds of interfaces or for different technical +platforms. Applications can compete with services, libraries +with plugins, frameworks with development tools, and so on, +even if they're written in different programming languages +or for different computer architectures. Goods and services +compete even when provided free of charge. If you market a +product as a practical substitute for the software or another +product, it definitely competes. + +## New Products + +If you are using the software to provide a product that does +not compete, but the licensor or any of its affiliates brings +your product into competition by providing a new version of +the software or another product using the software, you may +continue using versions of the software available under these +terms beforehand to provide your competing product, but not +any later versions. + +## Discontinued Products + +You may begin using the software to compete with a product +or service that the licensor or any of its affiliates has +stopped providing, unless the licensor includes a plain-text +line beginning with `Licensor Line of Business:` with the +software that mentions that line of business. For example: + +> Licensor Line of Business: YoyodyneCMS Content Management +System (http://example.com/cms) + +## Sales of Business + +If the licensor or any of its affiliates sells a line of +business developing the software or using the software +to provide a product, the buyer can also enforce +[Noncompete](#noncompete) for that product. ## Fair Use -You may have "fair use" rights for the software under the law. These terms do not limit them. +You may have "fair use" rights for the software under the +law. These terms do not limit them. ## No Other Rights -These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. ## Patent Defense -If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. ## Violations -The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. ## No Liability -***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** ## Definitions -The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +A **product** can be a good or service, or a combination +of them. + +**You** refers to the individual or entity agreeing to these +terms. -A **product** can be a good or service, or a combination of them. +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +its affiliates. -**You** refers to the individual or entity agreeing to these terms. +**Affiliates** means the other organizations than an +organization has control over, is under the control of, or is +under common control with. -**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. +**Control** means ownership of substantially all the assets of +an entity, or the power to direct its management and policies +by vote, contract, or otherwise. Control can be direct or +indirect. -**Your licenses** are all the licenses granted to you for the software under these terms. +**Your licenses** are all the licenses granted to you for the +software under these terms. -**Use** means anything you do with the software requiring one of your licenses. +**Use** means anything you do with the software requiring one +of your licenses. diff --git a/README.md b/README.md index 61cc500..f0d3335 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,93 @@
- + # PackCore - -[![Download on Modrinth](https://raw.githubusercontent.com/intergrav/devins-badges/c7fd18efdadd1c3f12ae56b49afd834640d2d797/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/mod/packcore) -[![fapi-badge](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/requires/fabric-api_vector.svg)](https://modrinth.com/mod/fabric-api) +[![Download on Modrinth](https://raw.githubusercontent.com/intergrav/devins-badges/c7fd18efdadd1c3f12ae56b49afd834640d2d797/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/mod/packcore) +[![Requires Fabric API](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/requires/fabric-api_vector.svg)](https://modrinth.com/mod/fabric-api) ![Build Status](https://github.com/KdGaming0/PackCore/actions/workflows/build.yml/badge.svg) -[![Modrinth Donwloads](https://img.shields.io/modrinth/dt/packcore?color=00AF5C&label=downloads&logo=modrinth)](https://modrinth.com/mod/packcore) +[![Modrinth Downloads](https://img.shields.io/modrinth/dt/packcore?color=00AF5C&label=downloads&logo=modrinth)](https://modrinth.com/mod/packcore) -PackCore is a companion mod for the **Skyblock Enhanced** modpacks. It enhances the player experience with a seamless and immersive start to the game. +**PackCore is the setup + configuration companion mod for SkyBlock-focused modpacks.** +It helps you get the right settings quickly, keeps your configs organized, and makes switching presets easy.
--- -The mod provides: -- **SkyBlock-themed start menu**, bringing a custom touch to your gameplay. -- A **default configuration setup prompt** on first launch, ensuring the best settings for your modpack. -- In-game **pop-up notifications** in the main menu with information about the newest modpack updates. +## What PackCore Does (v4) -PackCore is essential to the SkyBlock Enhanced Modpacks. Its goal is to make your Hypixel experience feel like you’re stepping directly into a SkyBlock world—not just vanilla Minecraft. +- **First-launch setup wizard** with visual previews +- **Easy style choices** for menu, tab design, storage UI, item backgrounds, and more +- **Performance profile picker** (from max FPS to quality-focused) +- **Optional integrations** that appear automatically when compatible mods are installed +- **Resource pack selection** directly in the wizard +- **One-click apply flow** with a final review page before changes are made --- -## Interested in Modpacks for SkyBlock? +## New in Version 4 + +- Full **multi-page Welcome Wizard** with clearer guided setup +- New **Config Management screen** with tabs: + - **Configuration** (official presets + your exported presets) + - **Export** (build your own config pack from selected files) + - **Import** (load config zips from an imports folder) + - **Backups** (create and restore backups safely) +- Better **preset switching flow** using restart-safe apply logic +- Cleaner UX with progress, status rows, and apply confirmation + +--- + +## Screenshots + +
+Spoiler + +![Welcome Wizard](https://cdn.modrinth.com/data/cached_images/9d5e0aea01e6a172db4fae0f1e5b84298df26e52_0.webp) + +![Main Menu 2](https://cdn.modrinth.com/data/cached_images/ce96d20952d72c9c1b96536371c36b6cd93d64bf.jpeg) + +![Config Management Screen](https://cdn.modrinth.com/data/cached_images/dcc548c5174059c8936e296eb7a85a8a2ce7b89a.png) +![Export Screen](https://cdn.modrinth.com/data/cached_images/131e07c3a30898e626237697868d4819cc1df0a7.png) +![Backups Screen](https://cdn.modrinth.com/data/cached_images/ea80ee5f9faa6c20a14d8e87abe9f45818e60b0d.png) +![Import Screen](https://cdn.modrinth.com/data/cached_images/40ca70bdd1837feee1f475485779a0d5c448161c.png) -### _SkyBlock Enhanced – Modern Edition_ -The newest pack, built for **Minecraft 1.21+**. -Play Hypixel Skyblock on the latest version with improved performance over 1.8.9. +
+--- + +## Quick Commands (Optional) + +For advanced users: + +- `/packcore wizard` — reopen the setup wizard +- `/packcore modpack_config` — open PackCore config manager +- `/packcore update check` — check for updates manually + +--- + +## Made for SkyBlock Enhanced Packs + +### SkyBlock Enhanced – Modern Edition (1.21+) +Play Hypixel SkyBlock on modern Minecraft with better performance and modern UI options. → https://modrinth.com/project/e0oMrxjp -### _SkyBlock Enhanced [Hypixel]_ -The original pack, built for **Minecraft 1.8.9**. -Features strong performance and a large collection of SkyBlock-specific mods, all preconfigured so you can jump right in. +### SkyBlock Enhanced [Hypixel] (1.8.9) +Original edition focused on classic 1.8.9 gameplay and compatibility. → https://modrinth.com/project/9JTbeXjU --- -Support the Project -------------------- +## Support -Want to support my work? You can do this on Ko‑fi. All donations are highly appreciated and help me continue providing support and updates. Thank you to everyone who wants to help! +If you want to support development: → [☕ Support on Ko-fi](https://ko-fi.com/kdgaming1) -Server Hosting Partner ----------------------- +--- -In need of your own server? I partner with Bisect Hosting to bring you reliable game servers. Whether you play Minecraft or another title, they deliver high‑performance hardware and fast support. +## Server Hosting Partner -Use code **SBE** at checkout for **25% off** your first purchase. +Need a server? I partner with Bisect Hosting. +Use code **SBE** for **25% off** your first purchase. → [🎮 Get 25% Off with Bisect Hosting](https://www.bisecthosting.com/SBE?r=SkyblockEnhancedModrinthPage) - -![Bisect Hosting promotional banner advertising 'Use code SBE for 25% off' to start your adventure, featuring the Bisect Hosting logo and pixelated Minecraft-style text with golden decorative borders](https://cdn.modrinth.com/data/cached_images/01502d9d41e784dfa18a3a1903a3e906cde1af1f.webp) diff --git a/README_2.md b/README_2.md deleted file mode 100644 index 98b4472..0000000 --- a/README_2.md +++ /dev/null @@ -1,601 +0,0 @@ -# ScamShield User Guide - -## 📋 Table of Contents -1. [How It Works](#how-it-works) -2. [File Structure](#file-structure) -3. [Customizing Detection](#customizing-detection) -4. [Understanding Scores](#understanding-scores) -5. [Adding New Patterns](#adding-new-patterns) -6. [Commands](#commands) -7. [Troubleshooting](#troubleshooting) - ---- - -## 🔍 How It Works - -ScamShield uses a **multi-layered detection system** with **automatic scam type discovery** to identify scam attempts in Hypixel chat: - -### Auto-Discovery System - -**NEW:** ScamShield automatically finds and loads scam types! - -- Drop any `scamtype-*.json` file into `packcore/scamshield/` -- Run `/scamshield reload` -- The file is automatically detected and loaded -- No code editing required! - -**Example:** Create `scamtype-crypto.json` → Run `/scamshield reload` → Instantly active! - -### Detection Layers - -1. **Psychological Tactics** (`phishing-language.json`) - - Detects *HOW* scammers manipulate (urgency, authority, trust-building) - - Reusable across all scam types - - Examples: "quick!", "trust me", "i'm staff" - -2. **Scam-Specific Actions** (Individual scam type files) - - Detects *WHAT* scammers ask you to do - - Unique to each scam type - - Examples: "/coopadd me", "verify your account", "flex your items" - -3. **Conversation Progression** (Built into code) - - Tracks conversation stages (Initial → Setup → Exploitation) - - Detects multi-message patterns - - Identifies escalation and dangerous sequences - -4. **Context Awareness** (Built into code) - - Considers your current activity (dungeons, lobby, etc.) - - Reduces false positives for legitimate trades - - Adjusts sensitivity based on situation - -### Detection Flow - -``` -Message Received - ↓ -Normalize & Check Cache - ↓ -Run All Detectors in Parallel: - • Phishing Language Analyzer - • Discord Verify Detector - • Island Theft Detector - • Trade Manipulation Detector - • Free Rank Bait Detector - • Command Instruction Detector - ↓ -Calculate Total Score - ↓ -Check Conversation History - ↓ -Apply Context Multipliers - ↓ -Compare to Threshold (100 points) - ↓ -Trigger Warning if Exceeded -``` - ---- - -## 📁 File Structure - -All configuration files are in `packcore/scamshield/`: - -``` -packcore/scamshield/ -├── phishing-language.json # Pure psychological tactics -├── scamtype-discord-verify.json # Discord verification scams -├── scamtype-island-theft.json # Island/co-op theft scams -├── scamtype-trade-manipulation.json # Trade & auction scams -├── scamtype-free-rank.json # Free reward bait scams -└── detections.json # Your detection history (auto-generated) -``` - -### File Responsibilities - -| File | Detects | Example Phrases | -|------|---------|-----------------| -| `phishing-language.json` | Manipulation tactics | "quick!", "trust me", "i'm staff" | -| `discord-verify` | Discord → verification → credentials | "join discord", "verify account", "send code" | -| `island-theft` | Co-op/visit commands + giveaways | "/coopadd", "i'm quitting", "all my items" | -| `trade-manipulation` | Trade scams & auction tricks | "flex your", "pay you 10%", "accidentally put" | -| `free-rank` | Free reward baits | "free rank", "/visit me to claim" | - ---- - -## ⚙️ Customizing Detection - -### Adjusting Sensitivity - -1. **Lower Detection Threshold** (detect more, but more false positives) - ``` - /scamshield config - Set threshold: 80 (default: 100) - ``` - -2. **Raise Detection Threshold** (detect less, fewer false positives) - ``` - Set threshold: 150 - ``` - -### Whitelisting Friends - -If a friend triggers false positives: - -``` -/scamshield whitelist add PlayerName -``` - -View whitelisted players: -``` -/scamshield whitelist list -``` - -Remove from whitelist: -``` -/scamshield whitelist remove PlayerName -``` - -### Editing JSON Files - -**Location:** `packcore/scamshield/` - -**After editing:** Run `/scamshield reload` to apply changes - -#### Adding Phrases to Existing Patterns - -Open any scam type file and find the pattern group: - -```json -{ - "pattern_groups": { - "verification_request": { - "description": "Requesting account verification", - "score": 35, - "phrases": [ - "verify", - "verification", - "verify your account", - "YOUR NEW PHRASE HERE" // ← Add here - ] - } - } -} -``` - -#### Adjusting Scores - -**Pattern Group Score:** Base points for detecting this pattern - -```json -"verification_request": { - "score": 35, // ← Higher = more suspicious - "phrases": [...] -} -``` - -**Combination Bonus:** Extra points when multiple patterns match together - -```json -"combination_rules": [ - { - "description": "Discord + Verification = scam", - "requires": ["discord_invitation", "verification_request"], - "bonus": 60 // ← Adjust this - } -] -``` - -#### Creating New Pattern Groups - -Add to the `pattern_groups` object: - -```json -"your_new_pattern": { - "description": "What this detects", - "score": 25, - "phrases": [ - "phrase 1", - "phrase 2", - "phrase 3" - ] -} -``` - -Then add combination rules to link it with other patterns: - -```json -"combination_rules": [ - { - "description": "New pattern + existing pattern", - "requires": ["your_new_pattern", "existing_pattern"], - "bonus": 50 - } -] -``` - ---- - -## 📊 Understanding Scores - -### Score Ranges - -| Score | Confidence | Warning Type | Response | -|-------|-----------|--------------|----------| -| 0-99 | None | No warning | Message passes through | -| 100-149 | LOW | Gentle warning | Yellow text, suggest whitelist | -| 150-249 | MEDIUM | Strong warning | Orange text, education link | -| 250+ | HIGH | Critical warning | Red text, force warning screen | - -### How Scores Are Calculated - -**Base Score** = Sum of all matching patterns - -**Example:** -``` -Message: "quick! join my discord to verify your account" - -Phishing Language: - • urgency ("quick"): +15 - -Discord Verify: - • discord_invitation: +20 - • verification_request: +35 - -Combination Bonuses: - • Discord + Verification: +60 - -Total: 130 points → LOW confidence warning -``` - -**Progression Bonus** = Multi-message patterns (+0 to +100) - -**Context Multiplier** = Based on your activity (0.5x to 2.0x) - -**Final Score** = (Base Score + Progression Bonus) × Context Multiplier - -### Conversation Stages - -Messages become more suspicious as conversations progress: - -| Stage | Description | Score Multiplier | -|-------|-------------|-----------------| -| INITIAL | Normal chat | 0.5x (reduced) | -| SETUP | Introducing scam | 1.0x (normal) | -| TRANSITION | Moving off-platform | 1.5x (increased) | -| EXPLOITATION | Asking for credentials | 2.5x (high alert) | -| PRESSURE | Creating urgency | 3.0x (critical) | - ---- - -## ➕ Adding New Patterns - -### Adding to Phishing Language - -**When to use:** If it's a general manipulation tactic (not specific to one scam type) - -1. Open `phishing-language.json` -2. Add a new tactic: - -```json -"new_tactic_name": { - "description": "What this detects", - "baseScore": 15, - "combinationBonus": 10, - "patterns": [ - { - "description": "Pattern category", - "weight": 1.0, - "phrases": [ - "phrase 1", - "phrase 2" - ] - } - ] -} -``` - -**Weight System:** -- `1.0` = Normal weight -- `1.5` = High priority (e.g., "i'm staff" is worse than "trust me") -- `0.7` = Lower priority (e.g., casual phrases) - -### Adding to Scam Types - -**When to use:** If it's specific to one scam type - -1. Open the appropriate `scamtype-*.json` file -2. Add to `pattern_groups`: - -```json -"new_pattern_group": { - "description": "Specific action or request", - "score": 30, - "phrases": [ - "specific phrase 1", - "specific phrase 2" - ] -} -``` - -3. Add combination rule: - -```json -{ - "description": "New pattern + existing", - "requires": ["new_pattern_group", "existing_group"], - "bonus": 55 -} -``` - -### Creating a New Scam Type File - -**NEW: Auto-Discovery!** Just create the file and reload - no code editing needed! - -1. Copy an existing scam type file as a template -2. Rename: `scamtype-your-name.json` (MUST start with `scamtype-`) -3. Update the `id` and `name` fields: - -```json -{ - "id": "your_scam_id", - "name": "Your Scam Display Name", - "description": "What this scam type detects", - "base_score": 30, - "pattern_groups": { - "pattern_1": { - "description": "What this pattern detects", - "score": 25, - "phrases": [ - "phrase 1", - "phrase 2" - ] - } - }, - "combination_rules": [ - { - "description": "Pattern 1 + another = bonus", - "requires": ["pattern_1", "another_pattern"], - "bonus": 50 - } - ] -} -``` - -4. Save file to `packcore/scamshield/` directory - -5. Reload: `/scamshield reload` - -6. Test: `/scamshield test ` - -**That's it!** The system automatically discovers and loads all `scamtype-*.json` files. - -**Important:** -- ✅ File name MUST start with `scamtype-` -- ✅ File name MUST end with `.json` -- ✅ Valid examples: `scamtype-crypto.json`, `scamtype-my-custom.json` -- ❌ Invalid: `custom-scam.json`, `scamtype.json`, `myscam.json` - ---- - -## 🎮 Commands - -### Main Commands - -| Command | Description | -|---------|-------------| -| `/scamshield toggle` | Enable/disable ScamShield | -| `/scamshield reload` | Reload all patterns & discover new files | -| `/scamshield stats` | View detection statistics | -| `/scamshield clear` | Clear detection history | - -**What `/scamshield reload` does:** -- 🔄 Reloads all existing `scamtype-*.json` files (picks up edits) -- 🔍 Scans for NEW `scamtype-*.json` files and loads them -- 🗑️ Removes deleted scam types from memory -- 💾 Clears the analysis cache for fresh analysis -- 📋 Shows summary of loaded detectors in logs - -### Testing - -| Command | Description | -|---------|-------------| -| `/scamshield test ` | Test a single message | -| `/scamshield debug` | Run full test suite (~30 seconds) | - -**Example:** -``` -/scamshield test quick! join my discord to verify your account - -Output: -⚠ SCAM DETECTED -Category: Discord Verification -Score: 130 -Patterns: urgency, discord_invitation, verification_request -``` - -### Whitelist Management - -| Command | Description | -|---------|-------------| -| `/scamshield whitelist add ` | Add player to whitelist | -| `/scamshield whitelist remove ` | Remove from whitelist | -| `/scamshield whitelist list` | Show all whitelisted players | -| `/scamshield whitelist clear` | Clear entire whitelist | - ---- - -## 🔧 Troubleshooting - -### "Friend's message triggered warning" - -**Solution:** Whitelist them -``` -/scamshield whitelist add FriendName -``` - -### "Too many false positives" - -**Solutions:** -1. Raise threshold: Edit config, set to 120-150 -2. Remove overly aggressive phrases from JSON files -3. Add legitimate trade phrases to `LegitimateTradeContext.java` - -### "Missed an obvious scam" - -**Solutions:** -1. Test the message: `/scamshield test ` -2. Check which patterns matched (if any) -3. Add missing phrases to appropriate JSON file -4. `/scamshield reload` - -### "Changes not taking effect" - -Always run after editing files: -``` -/scamshield reload -``` - -This command now: -- ✅ Reloads all existing scam type files -- ✅ Discovers and loads new scam type files -- ✅ Removes deleted scam type files -- ✅ Clears the analysis cache - -### "New scam type file not loading" - -Check the file name: -- ✅ Must start with `scamtype-` -- ✅ Must end with `.json` -- ✅ Must be in `packcore/scamshield/` directory - -**Example valid names:** -- `scamtype-crypto.json` -- `scamtype-my-custom-scam.json` -- `scamtype-phishing-v2.json` - -**Invalid names:** -- `crypto-scam.json` (doesn't start with scamtype-) -- `scamtype.json` (missing name part) -- `my-scam.json` (wrong prefix) - -After creating the file, check logs: -``` -[ScamShield] ✓ Loaded: Your Scam Name (scamtype-your-file.json) -``` - -Or for errors: -``` -[ScamShield] ✗ Failed to load: scamtype-your-file.json -``` - -### "Pattern not matching" - -**Common issues:** -- Message is normalized (lowercase, no special chars) -- Pattern must be substring match -- Check for typos in phrases array - -**Debug:** -``` -/scamshield test -``` - -### "Score too high/low" - -**Adjust in JSON:** -- Pattern group `score` values -- Combination rule `bonus` values -- Or change detection threshold in config - ---- - -## 📝 Best Practices - -### DO: -✅ Test changes with `/scamshield test` before using -✅ Keep backups of JSON files before major edits -✅ Use descriptive names for new pattern groups -✅ Add comments in `description` fields -✅ Run `/scamshield reload` after every change - -### DON'T: -❌ Set scores too high (causes false positives) -❌ Duplicate patterns across files (causes inflated scores) -❌ Add generic words like "the" or "you" as patterns -❌ Edit `detections.json` manually (auto-generated) -❌ Forget to test after adding new patterns - ---- - -## 🆘 Getting Help - -- **Discord:** [Your Support Server] -- **GitHub Issues:** [Your Repo] -- **In-game:** `/scamshield debug` for diagnostic info - ---- - -## 📜 Example Use Cases - -### Reducing False Positives for Lowballers - -Lowballers legitimately use `/visit` commands. The system already handles this, but you can adjust: - -Edit `LegitimateTradeContext.java` (requires Java knowledge) or lower visit command scores in `island-theft.json`. - -### Adding a New Crypto Scam Pattern - -If scammers start using cryptocurrency phrases: - -1. Open `scamtype-discord-verify.json` (most likely category) -2. Add new pattern group: - -```json -"crypto_mention": { -"description": "Cryptocurrency-related scams", -"score": 35, -"phrases": [ -"bitcoin", -"btc", -"cryptocurrency", -"send crypto", -"wallet address" -] -} -``` - -3. Add combination: - -```json -{ - "description": "Discord + Crypto = crypto scam", - "requires": ["discord_invitation", "crypto_mention"], - "bonus": 65 -} -``` - -4. Save and `/scamshield reload` - ---- - -## 🎯 Quick Reference - -**File to edit depends on what you're adding:** - -| What to Add | File to Edit | -|-------------|--------------| -| General manipulation tactic | `phishing-language.json` | -| Discord/verification phrase | `scamtype-discord-verify.json` | -| Island/co-op phrase | `scamtype-island-theft.json` | -| Trade/auction phrase | `scamtype-trade-manipulation.json` | -| Free reward phrase | `scamtype-free-rank.json` | - -**After ANY edit:** `/scamshield reload` - -**Testing:** `/scamshield test your message here` - -**View results:** `/scamshield stats` - ---- - -*Last updated: [Date] | Version: 4.0.0* \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5042e03..5596e18 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,56 +7,47 @@ version = "${property("mod.version")}+${stonecutter.current.version}" base.archivesName = property("mod.id") as String repositories { - /** - * Restricts dependency search of the given [groups] to the [maven URL][url], - * improving the setup speed. - */ + mavenCentral() fun strictMaven(url: String, alias: String, vararg groups: String) = exclusiveContent { forRepository { maven(url) { name = alias } } filter { groups.forEach(::includeGroup) } } - strictMaven("https://www.cursemaven.com", "CurseForge", "curse.maven") strictMaven("https://api.modrinth.com/maven", "Modrinth", "maven.modrinth") - maven("https://maven.terraformersmc.com/") - maven("https://maven.wispforest.io/releases/") - maven("https://jitpack.io") - maven("https://maven.midnightdust.eu/releases") - maven("https://repo.hypixel.net/repository/Hypixel/") + strictMaven("https://maven.daqem.com/releases", "DAQEM Studios", "com.daqem", "com.daqem.uilib") + maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1") exclusiveContent { forRepository { maven("https://maven.azureaaron.net/releases") } filter { includeGroup("net.azureaaron") } } - maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1") - mavenLocal() } dependencies { minecraft("com.mojang:minecraft:${stonecutter.current.version}") - mappings("net.fabricmc:yarn:${property("deps.yarn_mappings")}:v2") + mappings(loom.officialMojangMappings()) modImplementation("net.fabricmc:fabric-loader:${property("deps.fabric_loader")}") modImplementation("net.fabricmc.fabric-api:fabric-api:${property("deps.fabric_api")}") - modImplementation("eu.midnightdust:midnightlib:${property("deps.midnightlib_version")}") - include("eu.midnightdust:midnightlib:${property("deps.midnightlib_version")}") + modImplementation("maven.modrinth:midnightlib:${property("deps.midnightlib_version")}") + include("maven.modrinth:midnightlib:${property("deps.midnightlib_version")}") + + modImplementation("com.daqem.uilib:uilib-fabric:${property("deps.uilib_version")}") + + modImplementation("maven.modrinth:modmenu:${property("deps.modmenu_version")}") + modCompileOnly("maven.modrinth:scamscreener:${property("deps.scamscreener_version")}+${stonecutter.current.version}") + modCompileOnly("maven.modrinth:scaleme:${property("deps.scaleme_version")}") - modImplementation("com.terraformersmc:modmenu:${property("deps.modmenu_version")}") - modRuntimeOnly("com.terraformersmc:modmenu:${property("deps.modmenu_version")}") - modImplementation("io.wispforest:owo-lib:${property("deps.owo_version")}") - modImplementation("io.wispforest.lavender-md:core:${property("deps.lavender_md_version")}") - include("io.wispforest.lavender-md:core:${property("deps.lavender_md_version")}") - modImplementation("io.wispforest.lavender-md:owo-ui:${property("deps.lavender_md_version")}") - include("io.wispforest.lavender-md:owo-ui:${property("deps.lavender_md_version")}") - modImplementation("net.azureaaron:hm-api:1.0.1+1.21.2") - include("net.azureaaron:hm-api:1.0.1+1.21.2") + modImplementation("net.azureaaron:hm-api:${property("deps.hm_api_version")}") + include("net.azureaaron:hm-api:${property("deps.hm_api_version")}") modImplementation("maven.modrinth:sodium:${property("deps.sodium_version")}") modImplementation("maven.modrinth:iris:${property("deps.iris_version")}") - modRuntimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.1") - implementation("org.apache.httpcomponents:httpclient:4.5.13") + modRuntimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.2") + modRuntimeOnly("maven.modrinth:modmenu:${property("deps.modmenu_version")}") + + modRuntimeOnly("maven.modrinth:scamscreener:${property("deps.scamscreener_version")}+${stonecutter.current.version}") } -// Add this mixin configuration block loom { decompilerOptions.named("vineflower") { options.put("mark-corresponding-synthetics", "1") // Adds names to lambdas - useful for mixins @@ -82,26 +73,28 @@ tasks { inputs.property("name", project.property("mod.name")) inputs.property("version", project.property("mod.version")) inputs.property("minecraft", project.property("mod.mc_dep")) - inputs.property("owo_version", project.property("deps.owo_version")) + inputs.property("ui_lib", project.property("deps.uilib_version")) + inputs.property("hm_api", project.property("deps.hm_api_version")) + inputs.property("fabric_api", project.property("deps.fabric_api")) inputs.property("iris_version", project.property("deps.iris_version")) - inputs.property("fabric_loader", project.property("deps.fabric_loader")) + inputs.property("fabricloader", project.property("deps.fabric_loader")) + inputs.property("uilib_version", project.property("deps.uilib_version")) inputs.property("sodium_version", project.property("deps.sodium_version")) inputs.property("modmenu_version", project.property("deps.modmenu_version")) - inputs.property("midnightlib_version", project.property("deps.midnightlib_version")) - inputs.property("lavender_md_version", project.property("deps.lavender_md_version")) val props = mapOf( "id" to project.property("mod.id"), "name" to project.property("mod.name"), "version" to project.property("mod.version"), "minecraft" to project.property("mod.mc_dep"), - "owo_version" to project.property("deps.owo_version"), + "ui_lib" to project.property("deps.uilib_version"), + "hm_api" to project.property("deps.hm_api_version"), + "fabric_api" to project.property("deps.fabric_api"), "iris_version" to project.property("deps.iris_version"), - "fabric_loader" to project.property("deps.fabric_loader"), + "fabricloader" to project.property("deps.fabric_loader"), + "uilib_version" to project.property("deps.uilib_version"), "sodium_version" to project.property("deps.sodium_version"), - "modmenu_version" to project.property("deps.modmenu_version"), - "lavender_md_version" to project.property("deps.lavender_md_version"), - "midnightlib_version" to project.property("deps.midnightlib_version") + "modmenu_version" to project.property("deps.modmenu_version") ) filesMatching("fabric.mod.json") { @@ -127,18 +120,6 @@ tasks { } } -stonecutter { - replacements.string(current.parsed >= "1.21.11") { - replace("ParentComponent", "ParentUIComponent") - } - - replacements.regex(current.parsed >= "1.21.11") { - replace("\\bComponent\\b" to "UIComponent", "\\bUIComponent\\b" to "Component") - replace("\\bComponents\\b" to "UIComponents", "\\bUIComponents\\b" to "Components") - replace("\\bContainers\\b" to "UIContainers", "\\bUIContainers\\b" to "Containers") - } -} - publishMods { file = tasks.remapJar.map { it.archiveFile.get() } additionalFiles.from(tasks.remapSourcesJar.map { it.archiveFile.get() }) @@ -159,11 +140,14 @@ publishMods { slug = "P7dR8mSH" // Fabric API } requires { - slug = "ccKDOlHs" // OwO Lib + slug = "AOEDs9Al" // UI Lib } optional { slug = "mOgUt4GM" // ModMenu } + optional { + slug = "scamscreener" + } } curseforge { @@ -174,10 +158,10 @@ publishMods { slug = "fabric-api" } requires { - slug = "owo-lib" + slug = "ui" } optional { slug = "modmenu" } } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index c5eb55e..e1721b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,23 +4,25 @@ org.gradle.parallel=true org.gradle.configuration-cache=false # Mod properties -mod.name=PackCore -mod.id=packcore -mod.version=3.3.2 +mod.version=4.0.0 mod.group=com.github.kd_gaming1 +mod.id=packcore +mod.name=Pack Core # Global dependencies deps.fabric_loader=0.18.4 -deps.yarn_mappings=[VERSIONED] +deps.scamscreener_version=2.1.2 + +# Versioned dependencies deps.fabric_api=[VERSIONED] +deps.uilib_version=[VERSIONED] deps.midnightlib_version=[VERSIONED] -deps.lavender_md_version=[VERSIONED] +deps.hm_api_version=[VERSIONED] deps.modmenu_version=[VERSIONED] -deps.owo_version=[VERSIONED] +deps.scaleme_version=[VERSIONED] deps.sodium_version=[VERSIONED] deps.iris_version=[VERSIONED] - publish.modrinth=V3FnL7QV -publish.curseforge=1375477 \ No newline at end of file +publish.curseforge=1375477 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d205b54..bc79d44 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 3448cff..ba504d5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,12 +8,12 @@ pluginManagement { } plugins { - id("dev.kikugie.stonecutter") version "0.8.3" + id("dev.kikugie.stonecutter") version "0.9-beta.1" } stonecutter { create(rootProject) { - versions("1.21.10", "1.21.11") - vcsVersion = "1.21.10" + versions("1.21.11") + vcsVersion = "1.21.11" } } \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/PackCore.java b/src/main/java/com/github/kd_gaming1/packcore/PackCore.java index 14adf00..6d159a7 100644 --- a/src/main/java/com/github/kd_gaming1/packcore/PackCore.java +++ b/src/main/java/com/github/kd_gaming1/packcore/PackCore.java @@ -1,25 +1,22 @@ package com.github.kd_gaming1.packcore; -import com.github.kd_gaming1.packcore.command.packcore.PackCoreCommand; +import com.github.kd_gaming1.packcore.command.PackCoreCommands; import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.config.backup.ScheduledBackupManager; -import com.github.kd_gaming1.packcore.crash.CrashBrandingLogger; -import com.github.kd_gaming1.packcore.integration.bobby.BobbyConfigModifier; -import com.github.kd_gaming1.packcore.ui.screen.wizard.pages.WelcomeWizardPage; -import com.github.kd_gaming1.packcore.ui.screen.title.SBEStyledTitleScreen; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; -import com.github.kd_gaming1.packcore.util.HypixelEventUtil; -import com.github.kd_gaming1.packcore.util.io.zip.UnzipAsyncTask; -import com.github.kd_gaming1.packcore.util.io.zip.ZipAsyncTask; -import com.github.kd_gaming1.packcore.util.update.modrinth.UpdateCache; -import eu.midnightdust.lib.config.MidnightConfig; +import com.github.kd_gaming1.packcore.gui.screen.PackCoreTitleScreen; +import com.github.kd_gaming1.packcore.gui.screen.SBETitleScreen; +import com.github.kd_gaming1.packcore.gui.screen.WelcomeWizardScreen; +import com.github.kd_gaming1.packcore.playtime.PlaytimeTracker; +import com.github.kd_gaming1.packcore.update.UpdateChecker; +import com.github.kd_gaming1.packcore.util.RamWarningHelper; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,73 +26,67 @@ public class PackCore implements ClientModInitializer { public static final String MOD_ID = "packcore"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - private static ModpackInfo modpackInfo; - private static UpdateCache updateManager; - private static final Path packcoreDir = FabricLoader.getInstance().getGameDir().resolve("packcore"); + public static final Path PACKCORE_DIR = FabricLoader.getInstance().getGameDir().resolve("packcore"); + + public static boolean migratedFromV3 = false; + private static boolean replacingTitleScreen = false; @Override public void onInitializeClient() { - LOGGER.info("PackCore initialized!"); + LOGGER.info("[PackCore] Initialized"); - HypixelEventUtil.init(); + RamWarningHelper.init(); + UpdateChecker.checkAsync(); - // Cleanup on shutdown - ClientLifecycleEvents.CLIENT_STOPPING.register(client -> { - BackupManager.shutdown(); - ZipAsyncTask.shutdown(); - UnzipAsyncTask.shutdown(); - }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> PackCoreCommands.register(dispatcher)); - try { - modpackInfo = ModpackInfo.loadFromFile(packcoreDir); - updateManager = new UpdateCache(); + ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (!(screen instanceof TitleScreen)) return; + if (screen instanceof PackCoreTitleScreen) return; - LOGGER.info("Loaded modpack info for: {}", modpackInfo.getName()); - } catch (Exception e) { - LOGGER.error("Failed to load modpack info: {}", e.getMessage()); - } + RamWarningHelper.onMainMenu(); - // Add modpack information to logs - CrashBrandingLogger.logBrandingInfo(); + if (PackCoreConfig.menuStyle != PackCoreConfig.MenuStyle.MINIMAL) { + scheduleConfiguredTitleScreen(client, screen); + } + }); - MidnightConfig.init(MOD_ID, PackCoreConfig.class); + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (!(screen instanceof TitleScreen)) return; + if (screen instanceof PackCoreTitleScreen) return; + if (!PackCoreConfig.successfulWelcomeWizard) return; + if (PackCoreConfig.menuStyle != PackCoreConfig.MenuStyle.MINIMAL) return; - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { - PackCoreCommand.registerCommands(dispatcher); + PackCoreTitleScreen.decorateExisting((TitleScreen) screen, scaledWidth, scaledHeight); }); - // Initialize scheduled backups - if (PackCoreConfig.enableScheduledBackups) { - ScheduledBackupManager.initialize(); - } - - // try catch just in case something goes wrong with title screen - try { - if (PackCoreConfig.enableCustomMenu) { - ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - if (screen instanceof TitleScreen) { - client.execute(() -> client.setScreen(PackCoreConfig.haveShownWelcomeWizard - ? new SBEStyledTitleScreen() - : new WelcomeWizardPage()) - ); - } - }); - } - } catch (Exception e) { - LOGGER.error("Failed to show custom title screen: {}", e.getMessage()); - } - - if (!PackCoreConfig.haveSetBobbyConfig) { - BobbyConfigModifier.enableDynamicMultiWorld(); - PackCoreConfig.haveSetBobbyConfig = true; - PackCoreConfig.write(MOD_ID); - } - } - public static ModpackInfo getModpackInfo() { - return modpackInfo; + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> client.execute(RamWarningHelper::onWorldJoin)); + + ClientLifecycleEvents.CLIENT_STARTED.register(client -> PlaytimeTracker.onSessionStart()); + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> PlaytimeTracker.onSessionEnd()); } - public static UpdateCache getUpdateManager() { - return updateManager; + private static void scheduleConfiguredTitleScreen(Minecraft client, Screen screen) { + if (!(screen instanceof TitleScreen) || screen instanceof PackCoreTitleScreen || replacingTitleScreen) return; + + replacingTitleScreen = true; + client.execute(() -> { + try { + if (client.screen != screen) return; + + if (!PackCoreConfig.successfulWelcomeWizard) { + client.setScreen(new WelcomeWizardScreen(screen)); + return; + } + + switch (PackCoreConfig.menuStyle) { + case MODERN -> client.setScreen(new SBETitleScreen()); + case MODERN_MINIMAL -> client.setScreen(new SBETitleScreen(false)); + case MINIMAL -> client.setScreen(new PackCoreTitleScreen()); + } + } finally { + replacingTitleScreen = false; + } + }); } -} \ No newline at end of file +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/PackCorePreLaunch.java b/src/main/java/com/github/kd_gaming1/packcore/PackCorePreLaunch.java new file mode 100644 index 0000000..5d780ed --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/PackCorePreLaunch.java @@ -0,0 +1,326 @@ +package com.github.kd_gaming1.packcore; + +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.configpack.ConfigPackExtractor; +import com.github.kd_gaming1.packcore.configpack.ConfigPackScanner; +import com.github.kd_gaming1.packcore.update.UpdateChecker; +import com.github.kd_gaming1.packcore.util.ScreenResolution; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import eu.midnightdust.lib.config.MidnightConfig; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class PackCorePreLaunch implements PreLaunchEntrypoint { + + private static final String MOD_ID = "packcore"; + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/PreLaunch"); + private static final String PACK_META_FILE = "pack.json"; + + @Override + public void onPreLaunch() { + Path gameDir = FabricLoader.getInstance().getGameDir(); + Path packcoreDir = gameDir.resolve(MOD_ID); + Path configsDir = packcoreDir.resolve("configs"); + + MidnightConfig.init("packcore", PackCoreConfig.class); + + if (!PackCoreConfig.pendingRestoreBackup.isBlank()) { + applyPendingRestore(packcoreDir, gameDir); + return; + } + + if (!PackCoreConfig.pendingConfigPack.isBlank()) { + applyPendingConfig(gameDir, configsDir); + return; + } + + ScreenResolution.ScreenSize screen = ScreenResolution.detect(); + + List scannedPacks; + try { + scannedPacks = new ConfigPackScanner().scanFolder(configsDir); + } catch (IOException e) { + LOGGER.error("Failed to scan configs directory: {}", e.getMessage()); + return; + } + + if (scannedPacks.isEmpty()) { + LOGGER.warn("No valid config packs found in: {}", configsDir); + return; + } + + ConfigPackEntry selectedPack = findBestMatch(scannedPacks, screen.width(), screen.height()); + if (selectedPack == null) { + LOGGER.warn("No packs had valid resolution fields, aborting."); + return; + } + + if (isUpgradeFromV3()) { + migrateFromV3(selectedPack, gameDir); + return; + } + + extractIfNeeded(selectedPack, gameDir); + } + + + /** + * Applies the pack whose filename is stored in {@link PackCoreConfig#pendingConfigPack}. + * Always uses REPLACE_EXISTING because the user explicitly asked to switch. + * The pending flag is cleared regardless of success or failure. + */ + private void applyPendingConfig(Path gameDir, Path configsDir) { + String pendingFileName = PackCoreConfig.pendingConfigPack; + Path zipPath = configsDir.resolve(pendingFileName); + + LOGGER.info("Pending config switch requested: {}", pendingFileName); + + if (!Files.isRegularFile(zipPath)) { + LOGGER.error("Pending config zip not found at: {}", zipPath); + clearPending(); + return; + } + + Optional configOptional = readPackConfig(zipPath); + if (configOptional.isEmpty()) { + LOGGER.error("Pending config zip is missing a valid {}: {}", PACK_META_FILE, zipPath); + clearPending(); + return; + } + + try { + ConfigPackExtractor.extractAll( + zipPath, + gameDir, + ConfigPackExtractor.OverwriteMode.REPLACE_EXISTING + ); + } catch (IOException e) { + LOGGER.error("Failed to extract pending config pack '{}': {}", pendingFileName, e.getMessage()); + clearPending(); + return; + } + + JsonObject config = configOptional.get(); + String packVersion = config.has("version") ? config.get("version").getAsString() : ""; + + PackCoreConfig.lastAppliedVersion = packVersion; + PackCoreConfig.lastAppliedPackFile = pendingFileName; + PackCoreConfig.pendingConfigPack = ""; + MidnightConfig.write(MOD_ID); + + LOGGER.info("Successfully applied pending config: {} (version: {})", pendingFileName, packVersion); + } + + private static Optional readPackConfig(Path zipPath) { + try (ZipFile zipFile = new ZipFile(zipPath.toFile())) { + ZipEntry metaEntry = zipFile.getEntry(PACK_META_FILE); + if (metaEntry == null) { + return Optional.empty(); + } + + try (InputStream stream = zipFile.getInputStream(metaEntry); + InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + return Optional.of(JsonParser.parseReader(reader).getAsJsonObject()); + } + } catch (IOException | JsonParseException | IllegalStateException e) { + LOGGER.warn("Failed to read {} from '{}': {}", PACK_META_FILE, zipPath.getFileName(), e.getMessage()); + return Optional.empty(); + } + } + + private void applyPendingRestore(Path packcoreDir, Path gameDir) { + String backupFile = PackCoreConfig.pendingRestoreBackup; + Path backupPath = packcoreDir.resolve("backups").resolve(backupFile); + + LOGGER.info("Pending backup restore requested: {}", backupFile); + + if (!Files.exists(backupPath)) { + LOGGER.error("Pending restore backup not found: {}", backupPath); + clearPendingRestore(); + return; + } + + try { + ConfigPackExtractor.extractAll( + backupPath, + gameDir, + ConfigPackExtractor.OverwriteMode.REPLACE_EXISTING + ); + LOGGER.info("Successfully restored backup: {}", backupFile); + } catch (IOException e) { + LOGGER.error("Failed to restore backup '{}': {}", backupFile, e.getMessage()); + } + + clearPendingRestore(); + } + + private static void clearPendingRestore() { + PackCoreConfig.pendingRestoreBackup = ""; + MidnightConfig.write(MOD_ID); + } + + private static void clearPending() { + PackCoreConfig.pendingConfigPack = ""; + MidnightConfig.write(MOD_ID); + } + + /** + * Extracts {@code selectedPack} into the game directory if: + * - No version has been applied yet, or + * - The selected pack's filename matches the last applied file and its version is newer. + * If filenames differ, extraction is skipped to preserve user-selected config. + */ + private void extractIfNeeded(ConfigPackEntry selectedPack, Path gameDir) { + JsonObject config = selectedPack.config(); + String packVersion = config.has("version") ? config.get("version").getAsString() : ""; + String installedVersion = PackCoreConfig.lastAppliedVersion; + String installedPackFile = PackCoreConfig.lastAppliedPackFile; + String selectedPackFile = selectedPack.zipPath().getFileName().toString(); + + LOGGER.info("Best match: {} (version: {})", selectedPackFile, packVersion); + + try { + if (installedVersion.isEmpty()) { + LOGGER.info("No config applied yet, performing full extraction."); + ConfigPackExtractor.extractAll( + selectedPack.zipPath(), + gameDir, + ConfigPackExtractor.OverwriteMode.REPLACE_EXISTING + ); + } else if (!installedPackFile.equals(selectedPackFile)) { + LOGGER.info( + "Selected pack '{}' differs from last applied '{}', skipping.", + selectedPackFile, + installedPackFile + ); + return; + } else if (UpdateChecker.isNewerVersion(packVersion, installedVersion)) { + LOGGER.info( + "Newer config available ({} -> {}), applying with SKIP_EXISTING.", + installedVersion, + packVersion + ); + ConfigPackExtractor.extractAll( + selectedPack.zipPath(), + gameDir, + ConfigPackExtractor.OverwriteMode.SKIP_EXISTING + ); + } else { + LOGGER.info("Config up to date (version: {}), skipping.", installedVersion); + return; + } + } catch (IOException e) { + LOGGER.error("Failed to extract config pack: {}", e.getMessage()); + return; + } + + PackCoreConfig.lastAppliedVersion = packVersion; + PackCoreConfig.lastAppliedPackFile = selectedPackFile; + MidnightConfig.write(MOD_ID); + + LOGGER.info("Successfully applied config version: {}", packVersion); + } + + private void migrateFromV3(ConfigPackEntry selectedPack, Path gameDir) { + PackCore.migratedFromV3 = true; + + JsonObject config = selectedPack.config(); + String selectedPackFile = selectedPack.zipPath().getFileName().toString(); + String packVersion = readPackVersionOrFallback(config); + + LOGGER.info( + "Detected v3 upgrade. Backfilling PackCore v4 metadata using '{}' (version: {}) with SKIP_EXISTING.", + selectedPackFile, + packVersion + ); + + try { + ConfigPackExtractor.extractAll( + selectedPack.zipPath(), + gameDir, + ConfigPackExtractor.OverwriteMode.SKIP_EXISTING + ); + } catch (IOException e) { + LOGGER.error("Failed to migrate v3 install using '{}': {}", selectedPackFile, e.getMessage()); + return; + } + + PackCoreConfig.lastAppliedVersion = packVersion; + PackCoreConfig.lastAppliedPackFile = selectedPackFile; + PackCoreConfig.isFirstStartup = false; + MidnightConfig.write(MOD_ID); + + LOGGER.info( + "Successfully migrated v3 install. Stored applied pack '{}' at version '{}'.", + selectedPackFile, + packVersion + ); + } + + + /** + * Returns the pack whose target resolution is closest to the current screen + * using squared Euclidean distance. Ties are resolved by higher guiScale. + */ + private ConfigPackEntry findBestMatch(List packs, int screenWidth, int screenHeight) { + ConfigPackEntry bestPack = null; + long bestDistanceSquared = Long.MAX_VALUE; + int bestGuiScale = -1; + + for (ConfigPackEntry pack : packs) { + JsonObject config = pack.config(); + + if (!config.has("targetWidth") || !config.has("targetHeight")) { + LOGGER.warn("Pack missing resolution fields, skipping: {}", pack.zipPath().getFileName()); + continue; + } + + long widthDiff = config.get("targetWidth").getAsInt() - screenWidth; + long heightDiff = config.get("targetHeight").getAsInt() - screenHeight; + long distanceSquared = widthDiff * widthDiff + heightDiff * heightDiff; + int guiScale = config.has("guiScale") ? config.get("guiScale").getAsInt() : 0; + + if (distanceSquared < bestDistanceSquared + || (distanceSquared == bestDistanceSquared && guiScale > bestGuiScale)) { + bestPack = pack; + bestDistanceSquared = distanceSquared; + bestGuiScale = guiScale; + } + } + + return bestPack; + } + + private static boolean isUpgradeFromV3() { + return !PackCoreConfig.isFirstStartup + && PackCoreConfig.lastAppliedPackFile.isBlank() + && PackCoreConfig.lastAppliedVersion.isBlank(); + } + + private static String readPackVersionOrFallback(JsonObject config) { + if (config.has("version")) { + String version = config.get("version").getAsString(); + if (!version.isBlank()) { + return version; + } + } + return "0.0.0"; + } + +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/CommandHelper.java b/src/main/java/com/github/kd_gaming1/packcore/command/CommandHelper.java deleted file mode 100644 index cd63fb7..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/CommandHelper.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.kd_gaming1.packcore.command; - -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.text.ClickEvent; -import net.minecraft.text.HoverEvent; -import net.minecraft.text.MutableText; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -public class CommandHelper { - // Send a command that can be clicked to copy - public static void sendCopyCommand(FabricClientCommandSource source, String message, String command) { - MutableText commandText = Text.literal(" ").append(Text.literal(message)) - .styled(style -> style - .withClickEvent(new ClickEvent.SuggestCommand(command)) - .withHoverEvent(new HoverEvent.ShowText( - Text.literal("Click to copy command").formatted(Formatting.YELLOW)))); - source.sendFeedback(commandText); - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java b/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java new file mode 100644 index 0000000..3898399 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/command/PackCoreCommands.java @@ -0,0 +1,199 @@ +package com.github.kd_gaming1.packcore.command; + +import com.github.kd_gaming1.packcore.gui.screen.WelcomeWizardScreen; +import com.github.kd_gaming1.packcore.gui.screen.config.ConfigScreen; +import com.github.kd_gaming1.packcore.integration.ItemBackgroundManager; +import com.github.kd_gaming1.packcore.integration.PerformanceProfileService; +import com.github.kd_gaming1.packcore.integration.StorageDesignManager; +import com.github.kd_gaming1.packcore.integration.TabDesignManager; +import com.github.kd_gaming1.packcore.update.UpdateCache; +import com.github.kd_gaming1.packcore.update.UpdateChecker; +import com.github.kd_gaming1.packcore.update.UpdateStatus; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public class PackCoreCommands { + + private PackCoreCommands() {} + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + literal("packcore") + .then(literal("update") + .then(literal("check").executes(ctx -> { + checkUpdate(ctx.getSource()); + return 1; + })) + .then(literal("reset").executes(ctx -> { + resetUpdateCache(ctx.getSource()); + return 1; + })) + ) + .then(literal("performance") + .then(argument("profile", StringArgumentType.word()) + .suggests((ctx, builder) -> { + Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) + .map(PerformanceProfileService.PerformanceProfile::id) + .forEach(builder::suggest); + return builder.buildFuture(); + }) + .executes(ctx -> { + String id = StringArgumentType.getString(ctx, "profile"); + applyPerformanceProfile(ctx.getSource(), id); + return 1; + }) + ) + ) + .then(literal("tabdesign") + .then(literal("compact").executes(ctx -> { + applyTabDesign(ctx.getSource(), TabDesignManager.TabDesign.COMPACT); + return 1; + })) + .then(literal("fancy").executes(ctx -> { + applyTabDesign(ctx.getSource(), TabDesignManager.TabDesign.FANCY); + return 1; + })) + ) + .then(literal("itembg") + .then(literal("none").executes(ctx -> { + applyItemBackground(ctx.getSource(), ItemBackgroundManager.ItemBackground.NONE); + return 1; + })) + .then(literal("circle").executes(ctx -> { + applyItemBackground(ctx.getSource(), ItemBackgroundManager.ItemBackground.CIRCLE); + return 1; + })) + .then(literal("square").executes(ctx -> { + applyItemBackground(ctx.getSource(), ItemBackgroundManager.ItemBackground.SQUARE); + return 1; + })) + ) + .then(literal("storagedesign") + .then(literal("overlay").executes(ctx -> { + applyStorageDesign(ctx.getSource(), StorageDesignManager.StorageDesign.OVERLAY); + return 1; + })) + .then(literal("vanilla").executes(ctx -> { + applyStorageDesign(ctx.getSource(), StorageDesignManager.StorageDesign.VANILLA); + return 1; + })) + ) + .then(literal("wizard") + .executes(ctx -> { + Minecraft.getInstance().execute(() -> + Minecraft.getInstance().setScreen(new WelcomeWizardScreen(Minecraft.getInstance().screen)) + ); + return 1; + }) + ) + .then(literal("modpack_config") + .executes(ctx -> { + Minecraft.getInstance().execute(() -> + Minecraft.getInstance().setScreen(new ConfigScreen()) + ); + return 1; + }) + ) + ); + } + + private static void checkUpdate(FabricClientCommandSource source) { + send(source, "Checking for updates..."); + + CompletableFuture future = UpdateChecker.checkAsync(); + + future.thenAccept(status -> Minecraft.getInstance().execute(() -> { + switch (status.state()) { + case UP_TO_DATE -> send(source, "You are up to date! Version: " + status.installedVersion()); + case UPDATE_AVAILABLE -> { + send(source, "Update available!"); + send(source, "Installed: " + status.installedVersion()); + send(source, "Latest: " + status.latestVersion()); + + if (status.changelog() != null && !status.changelog().isBlank()) { + send(source, "Changelog:"); + send(source, status.changelog()); + } + } + case UNKNOWN -> sendError(source, "Could not determine update status."); + } + })); + } + + private static void resetUpdateCache(FabricClientCommandSource source) { + UpdateCache.invalidate(); + send(source, "Update cache cleared. Next check will fetch from Modrinth."); + } + + private static void applyStorageDesign(FabricClientCommandSource source, StorageDesignManager.StorageDesign design) { + send(source, "Applying storage design: " + design.name().toLowerCase() + "..."); + boolean success = StorageDesignManager.apply(design); + if (success) { + send(source, "Storage design applied: " + design.name().toLowerCase() + + ". If not in a world yet, the change will take effect on next world join."); + } else { + sendError(source, "Failed to apply storage design: " + design.name().toLowerCase() + ". Firmament may not be loaded — check logs."); + } + } + + private static void applyItemBackground(FabricClientCommandSource source, ItemBackgroundManager.ItemBackground background) { + send(source, "Applying item background: " + background.name().toLowerCase() + "..."); + boolean success = ItemBackgroundManager.apply(background); + if (success) { + send(source, "Item background applied: " + background.name().toLowerCase()); + } else { + sendError(source, "Failed to apply item background: " + background.name().toLowerCase() + ". Skyblocker may not be loaded — check logs."); + } + } + + private static void applyTabDesign(FabricClientCommandSource source, TabDesignManager.TabDesign design) { + send(source, "Applying tab design: " + design.name().toLowerCase() + "..."); + boolean success = TabDesignManager.apply(design); + if (success) { + send(source, "Tab design applied: " + design.name().toLowerCase()); + } else { + sendError(source, "Failed to apply tab design: " + design.name().toLowerCase() + ". Check logs for details."); + } + } + + private static void applyPerformanceProfile(FabricClientCommandSource source, String id) { + PerformanceProfileService.PerformanceProfile profile = Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) + .filter(p -> p.id().equals(id)) + .findFirst() + .orElse(null); + + if (profile == null) { + sendError(source, "Unknown performance profile: \"" + id + "\". Valid options: " + + Arrays.stream(PerformanceProfileService.PerformanceProfile.values()) + .map(PerformanceProfileService.PerformanceProfile::id) + .reduce((a, b) -> a + ", " + b).orElse("")); + return; + } + + send(source, "Applying performance profile: " + profile.getDisplayName() + "..."); + boolean success = PerformanceProfileService.applyAll(profile); + + if (success) { + send(source, "Performance profile applied: " + profile.getDisplayName()); + } else { + sendError(source, "One or more integrations failed for profile: " + profile.getDisplayName() + ". Check logs for details."); + } + } + + private static void send(FabricClientCommandSource source, String message) { + source.sendFeedback(Component.literal("[PackCore] " + message)); + } + + private static void sendError(FabricClientCommandSource source, String message) { + source.sendError(Component.literal("[PackCore] " + message)); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/ConfigManagerCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/ConfigManagerCommand.java deleted file mode 100644 index b8718a3..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/ConfigManagerCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.configmanager.ConfigManagerScreen; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; - -public class ConfigManagerCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("configmanager").executes(ConfigManagerCommand::execute); - } - - private static int execute(CommandContext context) { - MinecraftClient client = context.getSource().getClient(); - - if (client == null) { - context.getSource().sendError(Text.literal("Unable to access Minecraft client")); - return 0; - } - - /* - After executing a command, the current screen will be closed (the chat hud). - And if you open a new screen in a command, that new screen will be closed - instantly along with the chat hud. Slightly delaying the opening of the - screen fixes this issue. - */ - client.send(() -> { - try { - client.setScreen(new ConfigManagerScreen()); - } catch (Exception e) { - PackCore.LOGGER.error("Failed to open config: {}", e.getMessage()); - } - }); - - return 1; - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/GuideCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/GuideCommand.java deleted file mode 100644 index 7f10860..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/GuideCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.help.guide.GuideListScreen; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; - -public class GuideCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("guide").executes(GuideCommand::execute); - } - - private static int execute(CommandContext context) { - MinecraftClient client = context.getSource().getClient(); - - if (client == null) { - context.getSource().sendError(Text.literal("Unable to access Minecraft client")); - return 0; - } - - /* - After executing a command, the current screen will be closed (the chat hud). - And if you open a new screen in a command, that new screen will be closed - instantly along with the chat hud. Slightly delaying the opening of the - screen fixes this issue. - */ - client.send(() -> { - try { - client.setScreen(new GuideListScreen()); - } catch (Exception e) { - PackCore.LOGGER.error("Failed to open guide: {}", e.getMessage()); - } - }); - - return 1; - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/HelpCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/HelpCommand.java deleted file mode 100644 index 26967a7..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/HelpCommand.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import static com.github.kd_gaming1.packcore.command.CommandHelper.sendCopyCommand; - -public class HelpCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("help").executes(HelpCommand::execute); - } - - private static int execute(CommandContext context) { - var source = context.getSource(); - - source.sendFeedback(Text.literal("═══════════════════════════════════").formatted(Formatting.GOLD)); - source.sendFeedback(Text.literal(" PackCore Commands Help").formatted(Formatting.GOLD, Formatting.BOLD)); - source.sendFeedback(Text.literal("═══════════════════════════════════").formatted(Formatting.GOLD)); - source.sendFeedback(Text.literal("")); - - // Setup & Configuration - source.sendFeedback(Text.literal("⚙ Setup & Configuration:").formatted(Formatting.YELLOW, Formatting.BOLD)); - sendCopyCommand(source, "§a/packcore wizard §7- Open the setup wizard", "/packcore wizard"); - sendCopyCommand(source, "§a/packcore configmanager §7- Open config manager GUI", "/packcore configmanager"); - sendCopyCommand(source, "§a/packcore menu toggle §7- Enable/disable custom menu", "/packcore menu toggle"); - sendCopyCommand(source, "§a/packcore menu enable §7- Enable custom menu", "/packcore menu enable"); - sendCopyCommand(source, "§a/packcore menu disable §7- Disable custom menu", "/packcore menu disable"); - source.sendFeedback(Text.literal("")); - - // Performance & Design - source.sendFeedback(Text.literal("🚀 Performance & Design:").formatted(Formatting.YELLOW, Formatting.BOLD)); - sendCopyCommand(source, "§a/packcore performance list §7- List performance profiles", "/packcore performance list"); - sendCopyCommand(source, "§a/packcore performance apply §7- Apply performance profile", "/packcore performance apply"); - sendCopyCommand(source, "§a/packcore tabdesign list §7- List available tab designs", "/packcore tabdesign list"); - sendCopyCommand(source, "§a/packcore tabdesign apply §7- Apply tab design", "/packcore tabdesign apply "); - source.sendFeedback(Text.literal("")); - - // Information - source.sendFeedback(Text.literal("ℹ Information:").formatted(Formatting.YELLOW, Formatting.BOLD)); - sendCopyCommand(source, "§a/packcore status §7- Show current status", "/packcore status"); - sendCopyCommand(source, "§a/packcore guide §7- Open guide system", "/packcore guide"); - sendCopyCommand(source, "§a/packcore help §7- Show this help message", "/packcore help"); - - source.sendFeedback(Text.literal("")); - source.sendFeedback(Text.literal("═══════════════════════════════════").formatted(Formatting.GOLD)); - source.sendFeedback(Text.literal("💡 Tip: Click any command to copy it!").formatted(Formatting.GRAY, Formatting.ITALIC)); - - return 1; - } - -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/MenuCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/MenuCommand.java deleted file mode 100644 index 96b8889..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/MenuCommand.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -public class MenuCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("menu") - .then(ClientCommandManager.literal("enable") - .executes(ctx -> setMenuEnabled(ctx, true))) - .then(ClientCommandManager.literal("disable") - .executes(ctx -> setMenuEnabled(ctx, false))) - .then(ClientCommandManager.literal("toggle") - .executes(MenuCommand::toggleMenu)) - .executes(MenuCommand::showMenuStatus); - } - - private static int setMenuEnabled(CommandContext context, boolean enabled) { - PackCoreConfig.enableCustomMenu = enabled; - PackCoreConfig.write(PackCore.MOD_ID); - - String status = enabled ? "enabled" : "disabled"; - Formatting color = enabled ? Formatting.GREEN : Formatting.RED; - - context.getSource().sendFeedback( - Text.literal("✓ Custom menu " + status + "!") - .formatted(color) - ); - - context.getSource().sendFeedback( - Text.literal("ℹ Restart the game for changes to take effect.") - .formatted(Formatting.YELLOW) - ); - - return 1; - } - - private static int toggleMenu(CommandContext context) { - boolean newState = !PackCoreConfig.enableCustomMenu; - return setMenuEnabled(context, newState); - } - - private static int showMenuStatus(CommandContext context) { - boolean enabled = PackCoreConfig.enableCustomMenu; - String status = enabled ? "Enabled" : "Disabled"; - Formatting color = enabled ? Formatting.GREEN : Formatting.RED; - - context.getSource().sendFeedback( - Text.literal("Custom Menu Status: ") - .formatted(Formatting.YELLOW) - .append(Text.literal(status).formatted(color)) - ); - - context.getSource().sendFeedback(Text.literal("/packcore menu toggle") - .formatted(Formatting.YELLOW)); - - return 1; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/PackCoreCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/PackCoreCommand.java deleted file mode 100644 index 015689c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/PackCoreCommand.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import static com.github.kd_gaming1.packcore.command.CommandHelper.sendCopyCommand; - -public class PackCoreCommand { - - public static void registerCommands(CommandDispatcher dispatcher) { - dispatcher.register(ClientCommandManager.literal("packcore") - .executes(PackCoreCommand::showQuickHelp) - .then(HelpCommand.register()) - .then(WizardCommand.register()) - .then(MenuCommand.register()) - .then(GuideCommand.register()) - .then(ConfigManagerCommand.register()) - .then(StatusCommand.register()) - .then(PerformanceCommand.register()) - .then(TabDesignCommand.register()) - ); - } - - private static int showQuickHelp(CommandContext context) { - var source = context.getSource(); - - source.sendFeedback(Text.literal("PackCore Commands").formatted(Formatting.GOLD, Formatting.BOLD)); - - sendCopyCommand(source, "§7Type §a/packcore help §7for full command list", "/packcore help"); - - source.sendFeedback(Text.literal("")); - source.sendFeedback(Text.literal("Quick Commands:").formatted(Formatting.YELLOW)); - - sendCopyCommand(source, "§7 • §a/packcore wizard §7- Open setup wizard", "/packcore wizard"); - sendCopyCommand(source, "§7 • §a/packcore menu toggle §7- Toggle custom menu", "/packcore menu toggle"); - sendCopyCommand(source, "§7 • §a/packcore configmanager §7- Open config manager", "/packcore configmanager"); - - - return 1; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/PerformanceCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/PerformanceCommand.java deleted file mode 100644 index 0bfbd18..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/PerformanceCommand.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.integration.minecraft.PerformanceProfileService; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.util.concurrent.CompletableFuture; - -import static com.github.kd_gaming1.packcore.command.CommandHelper.sendCopyCommand; - -public class PerformanceCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("performance") - .executes(context -> { - context.getSource().sendFeedback(Text.literal("Available types: list, apply") - .formatted(Formatting.YELLOW)); - return 0; - }) - .then(ClientCommandManager.literal("list") - .executes(PerformanceCommand::listPerformanceProfiles)) - .then(ClientCommandManager.literal("apply") - .executes(context -> { - context.getSource().sendFeedback(Text.literal("Available performance options: performance, balanced, quality, shaders") - .formatted(Formatting.YELLOW)); - return 0; - }) - .then(ClientCommandManager.argument("profile", StringArgumentType.word()) - .suggests((context, builder) -> { - builder.suggest("performance"); - builder.suggest("balanced"); - builder.suggest("quality"); - builder.suggest("shaders"); - return builder.buildFuture(); - }) - .executes(PerformanceCommand::applyPerformanceProfile))); - } - - private static int applyPerformanceProfile(CommandContext context) { - String profileName = StringArgumentType.getString(context, "profile").toLowerCase(); - - PerformanceProfileService.PerformanceProfile profile; - - // Map string to enum - switch (profileName) { - case "performance" -> profile = PerformanceProfileService.PerformanceProfile.PERFORMANCE; - case "balanced" -> profile = PerformanceProfileService.PerformanceProfile.BALANCED; - case "quality" -> profile = PerformanceProfileService.PerformanceProfile.QUALITY; - case "shaders" -> profile = PerformanceProfileService.PerformanceProfile.SHADERS; - default -> { - context.getSource().sendError(Text.literal("Unknown performance profile: " + profileName) - .formatted(Formatting.RED)); - context.getSource().sendFeedback(Text.literal("Available profiles: performance, balanced, quality, shaders") - .formatted(Formatting.YELLOW)); - return 0; - } - } - - context.getSource().sendFeedback(Text.literal("Applying performance profile: " + profile.getDisplayName() + "...") - .formatted(Formatting.YELLOW)); - - // Apply the profile asynchronously to avoid blocking the main thread - CompletableFuture.runAsync(() -> { - try { - PerformanceProfileService.ProfileResult result = PerformanceProfileService.applyPerformanceProfile(profile); - - // Send feedback on main thread - MinecraftClient.getInstance().execute(() -> { - if (result.isFullySuccessful()) { - context.getSource().sendFeedback(Text.literal("✓ Performance profile '" + profile.getDisplayName() + "' applied successfully!") - .formatted(Formatting.GREEN)); - - // Show what was applied - if (result.isVanillaApplied()) { - context.getSource().sendFeedback(Text.literal(" ✓ Minecraft settings applied") - .formatted(Formatting.GRAY)); - } - if (result.isSodiumAvailable() && result.isSodiumApplied()) { - context.getSource().sendFeedback(Text.literal(" ✓ Sodium settings applied") - .formatted(Formatting.GRAY)); - } - if (result.isIrisAvailable() && result.isIrisApplied()) { - context.getSource().sendFeedback(Text.literal(" ✓ Iris/Shader settings applied") - .formatted(Formatting.GRAY)); - } - } else { - context.getSource().sendError(Text.literal("⚠ Performance profile applied with some issues:") - .formatted(Formatting.YELLOW)); - - if (!result.isVanillaApplied()) { - context.getSource().sendError(Text.literal(" ✗ Failed to apply Minecraft settings") - .formatted(Formatting.RED)); - } - if (result.isSodiumAvailable() && !result.isSodiumApplied()) { - context.getSource().sendError(Text.literal(" ✗ Failed to apply Sodium settings") - .formatted(Formatting.RED)); - } - if (result.isIrisAvailable() && !result.isIrisApplied()) { - context.getSource().sendError(Text.literal(" ✗ Failed to apply Iris/Shader settings") - .formatted(Formatting.RED)); - } - } - }); - - } catch (Exception e) { - MinecraftClient.getInstance().execute(() -> - context.getSource().sendError(Text.literal("✗ Failed to apply performance profile: " + e.getMessage()) - .formatted(Formatting.RED))); - } - }); - - return 1; - } - - private static int listPerformanceProfiles(CommandContext context) { - var source = context.getSource(); - var availability = PerformanceProfileService.getSystemAvailability(); - - source.sendFeedback(Text.literal("=== PackCore Performance Profiles ===") - .formatted(Formatting.GOLD)); - - // Available systems - source.sendFeedback(Text.literal("Available Systems:") - .formatted(Formatting.YELLOW)); - source.sendFeedback(Text.literal(" • Minecraft: ✓") - .formatted(Formatting.GREEN)); - source.sendFeedback(Text.literal(" • Sodium: " + (availability.sodiumAvailable() ? "✓" : "✗")) - .formatted(availability.sodiumAvailable() ? Formatting.GREEN : Formatting.RED)); - source.sendFeedback(Text.literal(" • Iris/Shaders: " + (availability.irisAvailable() ? "✓" : "✗")) - .formatted(availability.irisAvailable() ? Formatting.GREEN : Formatting.RED)); - - source.sendFeedback(Text.literal("")); - - // Profiles - source.sendFeedback(Text.literal("Available Profiles:") - .formatted(Formatting.YELLOW)); - - for (PerformanceProfileService.PerformanceProfile profile - : PerformanceProfileService.PerformanceProfile.values()) { - - String command = "/packcore performance apply " + profile.name().toLowerCase(); - - source.sendFeedback(Text.literal(" • " + profile.getDisplayName()) - .formatted(Formatting.WHITE, Formatting.BOLD)); - source.sendFeedback(Text.literal(" " + profile.getDescription()) - .formatted(Formatting.GRAY)); - - sendCopyCommand( - source, - " §a" + command + " §7- Apply this profile", - command - ); - } - - return 1; - } - -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/StatusCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/StatusCommand.java deleted file mode 100644 index 5dfb820..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/StatusCommand.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -public class StatusCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("status").executes(StatusCommand::execute); - } - - private static int execute(CommandContext context) { - var source = context.getSource(); - var modpackInfo = PackCore.getModpackInfo(); - var currentConfig = ConfigFileRepository.getCurrentConfig(); - - source.sendFeedback(Text.literal("=== PackCore Status ===").formatted(Formatting.GOLD)); - source.sendFeedback(Text.literal("Modpack: " + modpackInfo.getName() + " v" + modpackInfo.getVersion())); - source.sendFeedback(Text.literal("Active Config: " + currentConfig.getName() + " v" + currentConfig.getVersion())); - source.sendFeedback(Text.literal("Custom Menu: " + (PackCoreConfig.enableCustomMenu ? "Enabled" : "Disabled"))); - source.sendFeedback(Text.literal("Config Applied: " + (PackCoreConfig.defaultConfigSuccessfullyApplied ? "Yes" : "No"))); - - return 1; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/TabDesignCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/TabDesignCommand.java deleted file mode 100644 index b1a987c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/TabDesignCommand.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.integration.tabdesign.TabDesignManager; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.util.concurrent.CompletableFuture; - -import static com.github.kd_gaming1.packcore.command.CommandHelper.sendCopyCommand; - -public class TabDesignCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("tabdesign") - .executes(context -> { - context.getSource().sendFeedback(Text.literal("Available types: list, apply") - .formatted(Formatting.YELLOW)); - return 0; - }) - .then(ClientCommandManager.literal("list") - .executes(TabDesignCommand::listTabDesigns)) - .then(ClientCommandManager.literal("apply") - .executes(context -> { - context.getSource().sendFeedback(Text.literal("Available designs: skyhanni, skyblocker") - .formatted(Formatting.YELLOW)); - return 0; - }) - .then(ClientCommandManager.argument("design", StringArgumentType.word()) - .suggests((context, builder) -> { - builder.suggest("skyhanni"); - builder.suggest("skyblocker"); - return builder.buildFuture(); - }) - .executes(TabDesignCommand::applyTabDesign))); - } - - private static int applyTabDesign(CommandContext context) { - String designName = StringArgumentType.getString(context, "design").toLowerCase(); - - // Validate the design name - if (!designName.equals("skyhanni") && !designName.equals("skyblocker")) { - context.getSource().sendError(Text.literal("Unknown tab design: " + designName) - .formatted(Formatting.RED)); - context.getSource().sendFeedback(Text.literal("Available designs: skyhanni, skyblocker") - .formatted(Formatting.YELLOW)); - return 0; - } - - // Check mod availability - TabDesignManager.TabDesignAvailability availability = TabDesignManager.getAvailability(); - - if (designName.equals("skyhanni") && !availability.isSkyHanniAvailable()) { - context.getSource().sendError(Text.literal("✗ SkyHanni mod is not installed!") - .formatted(Formatting.RED)); - return 0; - } - - if (designName.equals("skyblocker") && !availability.isSkyblockerAvailable()) { - context.getSource().sendError(Text.literal("✗ Skyblocker mod is not installed!") - .formatted(Formatting.RED)); - return 0; - } - - String displayName = designName.equals("skyhanni") ? "SkyHanni Compact" : "Skyblocker Fancy"; - context.getSource().sendFeedback(Text.literal("Applying tab design: " + displayName + "...") - .formatted(Formatting.YELLOW)); - - // Apply the tab design asynchronously - CompletableFuture.runAsync(() -> { - try { - boolean success = TabDesignManager.applyTabDesign(designName); - - // Send feedback on main thread - MinecraftClient.getInstance().execute(() -> { - if (success) { - context.getSource().sendFeedback(Text.literal("✓ Tab design '" + displayName + "' applied successfully!") - .formatted(Formatting.GREEN)); - - if (designName.equals("skyhanni")) { - context.getSource().sendFeedback(Text.literal(" ℹ SkyHanni Compact tab list is now active") - .formatted(Formatting.GRAY)); - } else { - context.getSource().sendFeedback(Text.literal(" ℹ Skyblocker Fancy tab HUD is now active") - .formatted(Formatting.GRAY)); - } - } else { - context.getSource().sendError(Text.literal("⚠ Failed to apply tab design") - .formatted(Formatting.YELLOW)); - context.getSource().sendFeedback(Text.literal(" The mod may not be loaded properly") - .formatted(Formatting.GRAY)); - } - }); - - } catch (Exception e) { - MinecraftClient.getInstance().execute(() -> - context.getSource().sendError(Text.literal("✗ Failed to apply tab design: " + e.getMessage()) - .formatted(Formatting.RED))); - } - }); - - return 1; - } - - private static int listTabDesigns(CommandContext context) { - TabDesignManager.TabDesignAvailability availability = TabDesignManager.getAvailability(); - - context.getSource().sendFeedback(Text.literal("=== PackCore Tab Designs ===") - .formatted(Formatting.GOLD)); - - // Show available mods - context.getSource().sendFeedback(Text.literal("Available Mods:") - .formatted(Formatting.YELLOW)); - context.getSource().sendFeedback(Text.literal(" • SkyHanni: " + (availability.isSkyHanniAvailable() ? "✓" : "✗")) - .formatted(availability.isSkyHanniAvailable() ? Formatting.GREEN : Formatting.RED)); - context.getSource().sendFeedback(Text.literal(" • Skyblocker: " + (availability.isSkyblockerAvailable() ? "✓" : "✗")) - .formatted(availability.isSkyblockerAvailable() ? Formatting.GREEN : Formatting.RED)); - - context.getSource().sendFeedback(Text.literal("")); - - // Show available designs - context.getSource().sendFeedback(Text.literal("Available Designs:") - .formatted(Formatting.YELLOW)); - - if (availability.isSkyHanniAvailable()) { - context.getSource().sendFeedback(Text.literal(" • SkyHanni Compact - Minimalist compact tab list") - .formatted(Formatting.WHITE)); - context.getSource().sendFeedback(Text.literal(" Command: /packcore tabdesign apply skyhanni") - .formatted(Formatting.GRAY)); - } - - if (availability.isSkyblockerAvailable()) { - context.getSource().sendFeedback(Text.literal(" • Skyblocker Fancy - Feature-rich tab HUD") - .formatted(Formatting.WHITE)); - context.getSource().sendFeedback(Text.literal(" Command: /packcore tabdesign apply skyblocker") - .formatted(Formatting.GRAY)); - } - - if (!availability.isSkyHanniAvailable() && !availability.isSkyblockerAvailable()) { - context.getSource().sendFeedback(Text.literal(" ⚠ No compatible tab design mods found") - .formatted(Formatting.RED)); - } - - return 1; - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/WizardCommand.java b/src/main/java/com/github/kd_gaming1/packcore/command/packcore/WizardCommand.java deleted file mode 100644 index c64c439..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/command/packcore/WizardCommand.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.kd_gaming1.packcore.command.packcore; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.pages.WelcomeWizardPage; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -public class WizardCommand { - - public static LiteralArgumentBuilder register() { - return ClientCommandManager.literal("wizard") - .executes(WizardCommand::openWizard); - } - - private static int openWizard(CommandContext context) { - MinecraftClient client = context.getSource().getClient(); - - if (client == null) { - context.getSource().sendError(Text.literal("Unable to access Minecraft client")); - return 0; - } - - context.getSource().sendFeedback(Text.literal("Opening setup wizard...") - .formatted(Formatting.GREEN)); - - client.send(() -> { - try { - client.setScreen(new WelcomeWizardPage()); - } catch (Exception e) { - PackCore.LOGGER.error("Failed to open wizard: {}", e.getMessage()); - } - }); - - return 1; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/PackCoreConfig.java b/src/main/java/com/github/kd_gaming1/packcore/config/PackCoreConfig.java index 9bd945a..116de77 100644 --- a/src/main/java/com/github/kd_gaming1/packcore/config/PackCoreConfig.java +++ b/src/main/java/com/github/kd_gaming1/packcore/config/PackCoreConfig.java @@ -3,79 +3,76 @@ import eu.midnightdust.lib.config.MidnightConfig; public class PackCoreConfig extends MidnightConfig { - public static final String CATEGORY_INTERFACE = "interface"; - public static final String CATEGORY_BACKUPS = "backups"; - public static final String CATEGORY_CUSTOMIZATION = "customization"; - public static final String CATEGORY_ADVANCED = "advanced"; - // --------------------- - // Interface - // --------------------- - @Comment(category = CATEGORY_INTERFACE, name = "packcore.midnightconfig.interface_info") - public static Comment interfaceInfo; + public static final String MENU = "menu"; + public static final String BACKUP = "backup"; + public static final String TOAST = "toast"; + public static final String META = "meta"; - @Entry(category = CATEGORY_INTERFACE, name = "packcore.midnightconfig.enable_custom_menu") - public static boolean enableCustomMenu = true; + // ── Toast ───────────────────────────────────────────────────────────────── - // --------------------- - // Backups - // --------------------- - @Comment(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.backup_info") - public static Comment backupInfo; + @Entry(category = TOAST) + public static boolean showRamWarningToast = true; - @Entry(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.enable_auto_backups") - public static boolean enableAutoBackups = true; + @Entry(category = TOAST) + public static boolean showUpdateToast = true; - @Entry(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.enable_scheduled_backups") - public static boolean enableScheduledBackups = true; + @Entry(category = TOAST) + public static boolean showBackupToast = true; - @Entry(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.max_backups", min = 1, max = 20) - public static int maxBackups = 5; + // ── Menu ────────────────────────────────────────────────────────────────── - // the key says days; variable name reflects that - @Entry(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.backup_interval_days", min = 1, max = 14) - public static int backupIntervalDays = 3; + @Entry(category = MENU) + public static MenuStyle menuStyle = MenuStyle.MODERN_MINIMAL; - @Entry(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.enable_backup_debug_logging") - public static boolean enableBackupDebugLogging = false; + @Entry(category = MENU) + public static String serverAddressForQuickJoinButton = "mc.hypixel.net"; - @Comment(category = CATEGORY_BACKUPS, name = "packcore.midnightconfig.backup_spacer_1") - public static Comment backupSpacer; + public enum MenuStyle { + MODERN, + MODERN_MINIMAL, + MINIMAL + } - // --------------------- - // Customization & Updates - // --------------------- - @Entry(category = CATEGORY_CUSTOMIZATION, name = "packcore.midnightconfig.server_address") - public static String serverAddressForQuickJoinButton = "mc.hypixel.net"; + // ── Backup ──────────────────────────────────────────────────────────────── - @Entry(category = CATEGORY_CUSTOMIZATION, name = "packcore.midnightconfig.enable_update_notifications") - public static boolean enableUpdateNotifications = true; + @Entry(category = BACKUP) + public static boolean autoBackupEnabled = true; - @Entry(category = CATEGORY_CUSTOMIZATION, name = "packcore.midnightconfig.show_update_notifications_title") - public static boolean showUpdateNotificationsOnTitleScreen = true; + @Entry(category = BACKUP, min = 1, max = 90) + public static int autoBackupIntervalDays = 3; - @Comment(category = CATEGORY_CUSTOMIZATION, name = "packcore.midnightconfig.customization_spacer_1") - public static Comment customizationSpacer; + // ── Meta (hidden) ───────────────────────────────────────────────────────── - @Entry(category = CATEGORY_CUSTOMIZATION, name = "packcore.midnightconfig.show_low_memory_warning") - public static boolean showLowMemoryWarning = true; + @Hidden + @Entry(category = META) + public static String lastAppliedVersion = ""; - @Entry(category = CATEGORY_CUSTOMIZATION, name = "packcore.midnightconfig.minimum_ram_gb", min = 2, max = 8) - public static int minimumRamGB = 3; + @Hidden + @Entry(category = META) + public static String lastAppliedPackFile = ""; - // --------------------- - // Advanced / telemetry-like trackers - // --------------------- - @Entry(category = CATEGORY_ADVANCED, name = "packcore.midnightconfig.first_startup") - public static boolean isFirstStartup = true; + @Hidden + @Entry(category = META) + public static String pendingConfigPack = ""; - @Entry(category = CATEGORY_ADVANCED, name = "packcore.midnightconfig.welcome_wizard_shown") - public static boolean haveShownWelcomeWizard = false; + @Hidden + @Entry(category = META) + public static String pendingRestoreBackup = ""; - @Entry(category = CATEGORY_ADVANCED, name = "packcore.midnightconfig.have_set_bobby_config") - public static boolean haveSetBobbyConfig = false; + @Hidden + @Entry(category = META) + public static long lastBackupEpochMs = 0L; + + @Hidden + @Entry(category = META) + public static long lastSeenEpochMs = 0L; @Hidden - @Entry(category = CATEGORY_ADVANCED, name = "packcore.midnightconfig.setup_wizard_completed") - public static boolean defaultConfigSuccessfullyApplied = false; + @Entry(category = META) + public static boolean successfulWelcomeWizard = false; + + @Hidden + @Entry(category = META) + public static boolean isFirstStartup = true; } \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/apply/ConfigApplyService.java b/src/main/java/com/github/kd_gaming1/packcore/config/apply/ConfigApplyService.java deleted file mode 100644 index 626945e..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/apply/ConfigApplyService.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.github.kd_gaming1.packcore.config.apply; - -import com.github.kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.github.kd_gaming1.packcore.util.io.zip.UnzipService; -import com.google.gson.Gson; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; - -/** - * Manages config application on game restart. - * Handles the pending config system for in-game config switching. - */ -public class ConfigApplyService { - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigApplyService.class); - private static final String PENDING_CONFIG_FILE = "packcore_pending_config.json"; - private static final Gson GSON = GsonUtils.GSON; - - /** - * Schedule a config to be applied on next game start - */ - public static void scheduleConfigApplication(ConfigFileRepository.ConfigFile config) { - try { - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path pendingFile = gameDir.resolve(PENDING_CONFIG_FILE); - - // Create pending config info - PendingConfig pending = new PendingConfig( - config.path().toString(), - config.getDisplayName(), - config.metadata() - ); - - // Write to file - String json = GSON.toJson(pending); - Files.writeString(pendingFile, json, StandardCharsets.UTF_8, - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - LOGGER.info("Scheduled config for application: {}", config.getDisplayName()); - - // Schedule game shutdown - MinecraftClient.getInstance().scheduleStop(); - - } catch (IOException e) { - LOGGER.error("Failed to schedule config application", e); - throw new RuntimeException("Failed to prepare config application", e); - } - } - - /** - * Check and apply pending config during pre-launch - * - * @return true if a config was applied - */ - public static boolean checkAndApplyPendingConfig(Path gameDir) { - Path pendingFile = gameDir.resolve(PENDING_CONFIG_FILE); - - if (!Files.exists(pendingFile)) { - return false; - } - - try { - // Read pending config info - String json = Files.readString(pendingFile, StandardCharsets.UTF_8); - PendingConfig pending = GSON.fromJson(json, PendingConfig.class); - - if (pending == null || pending.configPath == null) { - LOGGER.warn("Invalid pending config file"); - Files.deleteIfExists(pendingFile); - return false; - } - - LOGGER.info("Found pending config: {}", pending.configName); - - // Create backup using new backup manager - Path backup = BackupManager.createAutoBackup(); - if (backup != null) { - LOGGER.info("Created auto-backup before applying config: {}", backup); - } - - // Apply the config - boolean success = applyConfig(Path.of(pending.configPath), gameDir); - - if (success) { - // Save the metadata as current config - ConfigFileRepository.saveCurrentConfig(pending.metadata); - LOGGER.info("Successfully applied config: {}", pending.configName); - } else { - LOGGER.error("Failed to apply config: {}", pending.configName); - } - - // Clean up pending file - Files.deleteIfExists(pendingFile); - - return success; - - } catch (Exception e) { - LOGGER.error("Error processing pending config", e); - try { - Files.deleteIfExists(pendingFile); - } catch (IOException ex) { - LOGGER.warn("Failed to clean up pending file", ex); - } - return false; - } - } - - /** - * Apply a config by extracting its ZIP file - */ - private static boolean applyConfig(Path configZipPath, Path gameDir) { - try { - if (!Files.exists(configZipPath)) { - LOGGER.error("Config file not found: {}", configZipPath); - return false; - } - - // Extract config zip to game directory - final int[] lastLogged = {-1}; - UnzipService unzipper = new UnzipService(); - unzipper.unzip( - configZipPath.toString(), - gameDir.toString(), - (bytesProcessed, totalBytes, percentage) -> { - if (percentage % 25 == 0 && percentage != lastLogged[0]) { - LOGGER.info("Extraction progress: {}%", percentage); - lastLogged[0] = (int) percentage; - } - } - ); - - LOGGER.info("Config extraction completed"); - return true; - - } catch (IOException e) { - LOGGER.error("Failed to extract config", e); - return false; - } - } - - /** - * Data class for pending config info - */ - private static class PendingConfig { - String configPath; - String configName; - ConfigMetadata metadata; - - PendingConfig(String configPath, String configName, ConfigMetadata metadata) { - this.configPath = configPath; - this.configName = configName; - this.metadata = metadata; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/apply/ConfigAutoApplier.java b/src/main/java/com/github/kd_gaming1/packcore/config/apply/ConfigAutoApplier.java deleted file mode 100644 index f4f6d6c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/apply/ConfigAutoApplier.java +++ /dev/null @@ -1,244 +0,0 @@ -package com.github.kd_gaming1.packcore.config.apply; - -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.util.io.zip.UnzipService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.List; - -/** - * Automatically applies the best matching configuration based on screen resolution. - * Uses actual pixel-based distance calculation to find the closest matching config. - */ -public class ConfigAutoApplier { - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigAutoApplier.class); - - /** - * Detect screen resolution and apply the best matching config - * - * @param gameDir The game directory - * @return true if a config was successfully applied - */ - public static boolean applyBestMatchingConfig(Path gameDir) { - LOGGER.info("Starting automatic config application..."); - - // Detect screen resolution - Dimension screenSize = detectResolution(); - if (screenSize == null) { - LOGGER.error("Failed to detect screen resolution"); - return false; - } - - LOGGER.info("Detected screen resolution: {}x{}", screenSize.width, screenSize.height); - - // Get all available configs - List allConfigs = ConfigFileRepository.getAllConfigs(); - - if (allConfigs.isEmpty()) { - LOGGER.warn("No configs available for automatic application"); - return false; - } - - LOGGER.info("Found {} available configs", allConfigs.size()); - - // Find best match - ConfigFileRepository.ConfigFile bestMatch = findBestMatch(screenSize, allConfigs); - - if (bestMatch == null) { - LOGGER.error("Could not find suitable config to apply"); - return false; - } - - LOGGER.info("Selected config: {} ({})", bestMatch.getDisplayName(), - bestMatch.official() ? "Official" : "Custom"); - - // Apply the config using shared logic - return applyConfigDirect(bestMatch, gameDir); - } - - private static boolean applyConfigDirect(ConfigFileRepository.ConfigFile config, Path gameDir) { - LOGGER.info("Applying config: {}", config.getDisplayName()); - - try { - // Create zip backup (pre-launch) with a filename that reflects why it's made - if (PackCoreConfig.enableAutoBackups) { - String title = "Auto backup before pre-launch auto-apply: " + config.getDisplayName(); - String description = "Safety backup created before automatically applying a config based on screen resolution."; - String backupIdHint = "prelaunch_auto_apply_" + config.getDisplayName(); - - Path backupZip = BackupManager.createBackupAsync( - gameDir, - BackupManager.BackupType.AUTO, - title, - description, - backupIdHint, - msg -> {} - ).join(); - - if (backupZip != null) { - LOGGER.info("Backup ZIP created at: {}", backupZip); - } - } else { - LOGGER.debug("Auto-backups disabled; skipping pre-launch safety backup"); - } - - // Extract config using existing UnzipFiles utility - UnzipService unzipper = new UnzipService(); - unzipper.unzip( - config.path().toString(), - gameDir.toString(), - (bytesProcessed, totalBytes, percentage) -> { - if (percentage % 25 == 0) { - LOGGER.info("Extraction progress: {}%", percentage); - } - } - ); - - ConfigFileRepository.saveCurrentConfig(config.metadata()); - - LOGGER.info("Config applied successfully: {}", config.getDisplayName()); - return true; - - } catch (IOException e) { - LOGGER.error("Failed to apply config", e); - return false; - } - } - - /** - * Detect the current screen resolution - * - * @return Dimension with screen width and height, or null if detection fails - */ - private static Dimension detectResolution() { - try { - Toolkit toolkit = Toolkit.getDefaultToolkit(); - Dimension screenSize = toolkit.getScreenSize(); - LOGGER.debug("Screen dimensions: {}x{}", screenSize.width, screenSize.height); - return screenSize; - } catch (Exception e) { - LOGGER.error("Failed to detect resolution", e); - return null; - } - } - - /** - * Find the best matching config for the detected resolution - * Uses actual pixel distance calculation - */ - private static ConfigFileRepository.ConfigFile findBestMatch( - Dimension detectedResolution, - List configs) { - - LOGGER.info("Available configs:"); - for (ConfigFileRepository.ConfigFile config : configs) { - if (config.metadata() == null) { - LOGGER.warn("Config {} has no metadata, skipping", config.getDisplayName()); - continue; - } - String targetRes = config.metadata().getTargetResolution(); - Dimension configRes = parseResolution(targetRes); - double distance = calculateDistance(detectedResolution, configRes); - - LOGGER.info(" - {} | Resolution: {} | Official: {} | Distance: {}", - config.getDisplayName(), - targetRes, - config.official(), - configRes != null ? String.format("%.0f", distance) : "N/A"); - } - - ConfigFileRepository.ConfigFile selected = configs.stream() - .filter(c -> c.metadata() != null) - .min(createConfigComparator(detectedResolution)) - .orElse(null); - - if (selected != null) { - Dimension selectedRes = parseResolution(selected.metadata().getTargetResolution()); - double distance = calculateDistance(detectedResolution, selectedRes); - LOGGER.info("Best match selected: {} (distance: {} pixels)", - selected.getDisplayName(), String.format("%.0f", distance)); - } - - return selected; - } - - /** - * Creates a comparator that prioritizes configs by: - * 1. Official status (official configs first) - * 2. Pixel distance from target resolution (closer is better) - * 3. Name (alphabetically for consistency) - */ - private static Comparator createConfigComparator(Dimension targetResolution) { - return Comparator - // Prioritize official configs - .comparing((ConfigFileRepository.ConfigFile c) -> !c.official()) - // Then by how close the resolution matches (pixel distance) - .thenComparing(c -> { - Dimension configRes = parseResolution(c.metadata().getTargetResolution()); - return calculateDistance(targetResolution, configRes); - }) - // Then by name for consistency - .thenComparing(ConfigFileRepository.ConfigFile::getDisplayName); - } - - /** - * Calculate Euclidean distance between two resolutions in pixels - * - * @param target The target resolution - * @param candidate The candidate resolution - * @return Distance in pixels, or Double.MAX_VALUE if either is null - */ - private static double calculateDistance(Dimension target, Dimension candidate) { - if (target == null || candidate == null) { - return Double.MAX_VALUE; - } - - double widthDiff = target.width - candidate.width; - double heightDiff = target.height - candidate.height; - - return Math.sqrt(widthDiff * widthDiff + heightDiff * heightDiff); - } - - /** - * Parse resolution strings to extract width and height - * Handles formats like "1920x1080", "2560×1440", "1920 x 1080", etc. - * Supports both 'x' and '×' (multiplication sign) as separators - * - * @param resolution Resolution string to parse - * @return Dimension with width and height, or null if parsing fails - */ - private static Dimension parseResolution(String resolution) { - if (resolution == null || resolution.trim().isEmpty()) { - return null; - } - - String cleaned = resolution.trim().toLowerCase(); - - // Replace multiplication sign (×) with regular x for consistent parsing - cleaned = cleaned.replace('×', 'x'); - - // Match pattern: digits, optional whitespace, x, optional whitespace, digits - // Example: "1920x1080", "1920 x 1080", "2560x1440" - if (cleaned.matches("\\d+\\s*x\\s*\\d+")) { - String[] parts = cleaned.split("x"); - try { - int width = Integer.parseInt(parts[0].trim()); - int height = Integer.parseInt(parts[1].trim()); - return new Dimension(width, height); - } catch (NumberFormatException e) { - LOGGER.warn("Could not parse resolution numbers: {}", resolution); - return null; - } - } - - LOGGER.warn("Resolution format not recognized: {}", resolution); - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/apply/FileDescriptionRegistry.java b/src/main/java/com/github/kd_gaming1/packcore/config/apply/FileDescriptionRegistry.java deleted file mode 100644 index 1a27e6c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/apply/FileDescriptionRegistry.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.github.kd_gaming1.packcore.config.apply; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.reflect.TypeToken; -import net.minecraft.client.MinecraftClient; -import net.minecraft.resource.ResourceManager; -import net.minecraft.util.Identifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -public class FileDescriptionRegistry { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - private static final Gson GSON = new Gson(); - private static final String DESCRIPTIONS_FILE = "file-descriptions.json"; - private static final String CONFIG_PREFIX = "config/"; - private static final String RESOURCEPACKS_PREFIX = "resourcepacks/"; - - private static Map masterDescriptions = new HashMap<>(); - - static { - loadMasterDescriptions(); - } - - public record FileDescription( - String displayName, - String description, - String icon, - boolean isImportant - ) {} - - /** - * Loads master descriptions from bundled resource. - */ - private static void loadMasterDescriptions() { - try { - ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager(); - Identifier resourceId = Identifier.of(MOD_ID, DESCRIPTIONS_FILE); - - try (InputStream stream = resourceManager.getResource(resourceId) - .orElseThrow() - .getInputStream()) { - - Map descriptions = parseDescriptionsJson(stream); - masterDescriptions = descriptions; - LOGGER.info("Loaded {} master file descriptions", descriptions.size()); - } - } catch (Exception e) { - LOGGER.error("Failed to load master descriptions", e); - masterDescriptions = new HashMap<>(); - } - } - - /** - * Loads descriptions from a ZIP file (for config packages). - * Falls back to master descriptions if not found. - */ - public static Map loadFromZip(ZipFile zipFile) { - ZipEntry entry = zipFile.getEntry(DESCRIPTIONS_FILE); - - if (entry != null) { - try (InputStream stream = zipFile.getInputStream(entry)) { - Map customDescriptions = parseDescriptionsJson(stream); - LOGGER.info("Loaded {} custom file descriptions from ZIP", customDescriptions.size()); - - // Merge with master (custom overrides master) - Map merged = new HashMap<>(masterDescriptions); - merged.putAll(customDescriptions); - return merged; - } catch (IOException e) { - LOGGER.warn("Failed to load descriptions from ZIP, using master", e); - } - } - - return new HashMap<>(masterDescriptions); - } - - /** - * Parses the descriptions JSON structure from an input stream. - */ - private static Map parseDescriptionsJson(InputStream stream) { - JsonObject root = JsonParser.parseReader( - new InputStreamReader(stream) - ).getAsJsonObject(); - - if (root.has("descriptions")) { - Type typeToken = new TypeToken>(){}.getType(); - return GSON.fromJson(root.get("descriptions"), typeToken); - } - - return new HashMap<>(); - } - - /** - * Gets description from a specific description map. - */ - public static Optional getDescription(String path, Map descriptions) { - String normalizedPath = normalizePath(path); - - // Exact match - FileDescription exact = descriptions.get(normalizedPath); - if (exact != null) { - return Optional.of(exact); - } - - // Partial match - for (Map.Entry entry : descriptions.entrySet()) { - if (normalizedPath.endsWith(entry.getKey().toLowerCase())) { - return Optional.of(entry.getValue()); - } - } - - return inferDescription(normalizedPath); - } - - /** - * Gets description using master registry (for general use). - */ - public static Optional getDescription(String path) { - return getDescription(path, masterDescriptions); - } - - /** - * Normalizes a file path to lowercase with forward slashes. - */ - private static String normalizePath(String path) { - return path.replace("\\", "/").toLowerCase(); - } - - /** - * Infers a description based on the file path. - */ - private static Optional inferDescription(String path) { - if (path.startsWith(CONFIG_PREFIX)) { - return inferConfigDescription(path); - } - - if (path.startsWith(RESOURCEPACKS_PREFIX)) { - return inferResourcePackDescription(path); - } - - return Optional.empty(); - } - - /** - * Infers description for configuration files. - */ - private static Optional inferConfigDescription(String path) { - String fileName = path.substring(CONFIG_PREFIX.length()); - int slashIndex = fileName.indexOf('/'); - - if (slashIndex > 0) { - String folderName = fileName.substring(0, slashIndex); - return Optional.of(new FileDescription( - formatModName(folderName) + " Configuration", - "Settings and options for " + formatModName(folderName), - "⚙", - false - )); - } - - String baseName = getBaseName(fileName); - return Optional.of(new FileDescription( - formatModName(baseName) + " Config", - "Configuration file for " + formatModName(baseName), - "⚙", - false - )); - } - - /** - * Infers description for resource pack files. - */ - private static Optional inferResourcePackDescription(String path) { - String packName = path.substring(RESOURCEPACKS_PREFIX.length()); - int slashIndex = packName.indexOf('/'); - - if (slashIndex > 0) { - packName = packName.substring(0, slashIndex); - } - - return Optional.of(new FileDescription( - "Resource Pack: " + packName, - "Texture and sound resources", - "🎨", - false - )); - } - - /** - * Extracts the base name of a file (without extension). - */ - private static String getBaseName(String fileName) { - int dotIndex = fileName.lastIndexOf('.'); - return dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName; - } - - /** - * Formats a mod name by capitalizing words and replacing separators with spaces. - */ - private static String formatModName(String modName) { - modName = modName.replace("-", " ").replace("_", " "); - String[] words = modName.split(" "); - StringBuilder formatted = new StringBuilder(); - - for (String word : words) { - if (!word.isEmpty()) { - formatted.append(Character.toUpperCase(word.charAt(0))) - .append(word.substring(1).toLowerCase()) - .append(" "); - } - } - - return formatted.toString().trim(); - } - - /** - * Checks if a file is marked as important. - */ - public static boolean isImportantFile(String path) { - return getDescription(path) - .map(FileDescription::isImportant) - .orElse(false); - } - - /** - * Gets the icon for a file path. - */ - public static String getIcon(String path) { - return getDescription(path) - .map(FileDescription::icon) - .orElse("📄"); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/apply/SelectiveConfigApplyService.java b/src/main/java/com/github/kd_gaming1/packcore/config/apply/SelectiveConfigApplyService.java deleted file mode 100644 index a6ef4c3..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/apply/SelectiveConfigApplyService.java +++ /dev/null @@ -1,507 +0,0 @@ -package com.github.kd_gaming1.packcore.config. apply; - -import com.github.kd_gaming1.packcore. PackCore; -import com.github. kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore. util.GsonUtils; -import com.google.gson.Gson; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent. CompletableFuture; -import java.util.function.Consumer; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -/** - * Service for applying specific files from a config package - * instead of the entire configuration. - */ -public class SelectiveConfigApplyService { - private static final Gson GSON = GsonUtils. GSON; - private static final String PENDING_SELECTIVE_CONFIG_FILE = "packcore_pending_selective_config.json"; - - /** - * Represents a file that can be selectively applied from a config - */ - public record SelectableFile( - String path, // Path within the zip - String displayName, // User-friendly name - FileType type, // Type of file - long size, // File size in bytes - String description, // What this file does - boolean isDirectory - ) { - public enum FileType { - MOD_CONFIG("Mod Configuration"), - GAME_OPTIONS("Game Settings"), - KEYBINDINGS("Keybindings"), - RESOURCE_PACK("Resource Pack"), - SERVER_LIST("Server List"), - OTHER("Other"); - - private final String displayName; - FileType(String displayName) { this.displayName = displayName; } - public String getDisplayName() { return displayName; } - } - } - - /** - * Data class for pending selective config info - */ - private static class PendingSelectiveConfig { - String configPath; - String configName; - Set selectedPaths; - - PendingSelectiveConfig(String configPath, String configName, Set selectedPaths) { - this.configPath = configPath; - this.configName = configName; - this.selectedPaths = selectedPaths; - } - } - - /** - * Scan a config zip file and build a tree of selectable files - */ - public static CompletableFuture> scanConfigFiles( - ConfigFileRepository. ConfigFile config) { - return CompletableFuture.supplyAsync(() -> { - List files = new ArrayList<>(); - - try (ZipFile zipFile = new ZipFile(config. path(). toFile())) { - Enumeration entries = zipFile.entries(); - - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String entryName = entry.getName(); - - // Skip metadata file - if (entryName.equals(ConfigFileRepository.METADATA_FILE)) { - continue; - } - - SelectableFile. FileType type = determineFileType(entryName); - String description = getFileDescription(entryName, type); - String displayName = getDisplayName(entryName); - - files.add(new SelectableFile( - entryName, - displayName, - type, - entry.getSize(), - description, - entry.isDirectory() - )); - } - - // Sort by type and then name - files.sort(Comparator - .comparing((SelectableFile f) -> f.type.ordinal()) - .thenComparing(f -> f.displayName)); - - } catch (IOException e) { - PackCore.LOGGER.error("[Selective Config Apply Service] Failed to scan config files", e); - } - - return files; - }); - } - - /** - * Schedule selected files to be applied on next game start - */ - public static CompletableFuture scheduleSelectiveConfigApplication( - ConfigFileRepository. ConfigFile config, - Set selectedPaths) { - return CompletableFuture. supplyAsync(() -> { - try { - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path pendingFile = gameDir.resolve(PENDING_SELECTIVE_CONFIG_FILE); - - // Create pending config info - PendingSelectiveConfig pending = new PendingSelectiveConfig( - config.path(). toString(), - config.getDisplayName(), - selectedPaths - ); - - // Write to file - String json = GSON.toJson(pending); - Files.writeString(pendingFile, json, StandardCharsets.UTF_8, - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - PackCore.LOGGER.info("[Selective Config Apply Service] Scheduled {} files for application: {}", - selectedPaths.size(), config.getDisplayName()); - - // Schedule game shutdown - MinecraftClient.getInstance().scheduleStop(); - - return true; - - } catch (IOException e) { - PackCore.LOGGER.error("[Selective Config Apply Service] Failed to schedule selective config application", e); - return false; - } - }); - } - - /** - * Check and apply pending selective config during pre-launch - * - * @return true if a selective config was applied - */ - public static boolean checkAndApplyPendingSelectiveConfig(Path gameDir) { - Path pendingFile = gameDir.resolve(PENDING_SELECTIVE_CONFIG_FILE); - - if (!Files.exists(pendingFile)) { - return false; - } - - try { - // Read pending config info - String json = Files.readString(pendingFile, StandardCharsets.UTF_8); - PendingSelectiveConfig pending = GSON.fromJson(json, PendingSelectiveConfig.class); - - if (pending == null || pending.configPath == null || pending.selectedPaths == null) { - PackCore.LOGGER.warn("[Selective Config Apply Service] Invalid pending selective config file"); - Files.deleteIfExists(pendingFile); - return false; - } - - PackCore.LOGGER.info("[Selective Config Apply Service] Found pending selective config: {} ({} files)", - pending.configName, pending.selectedPaths.size()); - - // Create backup using backup manager - Path backup = BackupManager.createAutoBackup(); - if (backup != null) { - PackCore.LOGGER.info("[Selective Config Apply Service] Created auto-backup before applying: {}", backup); - } - - // Apply the selected files - boolean success = applySelectedFilesSync( - Path.of(pending.configPath), - pending.selectedPaths, - gameDir, - msg -> PackCore.LOGGER.info("[Selective Config Apply Service] {}", msg) - ); - - if (success) { - PackCore.LOGGER.info("[Selective Config Apply Service] Successfully applied {} selected files", - pending. selectedPaths.size()); - } else { - PackCore. LOGGER.error("[Selective Config Apply Service] Failed to apply selected files"); - } - - // Clean up pending file - Files.deleteIfExists(pendingFile); - - return success; - - } catch (Exception e) { - PackCore. LOGGER.error("[Selective Config Apply Service] Error processing pending selective config", e); - try { - Files.deleteIfExists(pendingFile); - } catch (IOException ex) { - PackCore.LOGGER.warn("[Selective Config Apply Service] Failed to clean up pending file", ex); - } - return false; - } - } - - /** - * Apply selected files synchronously (for pre-launch) - */ - private static boolean applySelectedFilesSync( - Path zipPath, - Set selectedPaths, - Path gameDir, - Consumer progressCallback) { - - try { - progressCallback.accept("Extracting selected files.. ."); - - // Extract only selected files - Path tempDir = Files.createTempDirectory("packcore_selective"); - try { - extractSelectedFiles(zipPath, selectedPaths, tempDir, progressCallback); - - progressCallback.accept("Applying files..."); - - // Copy extracted files to game directory - copySelectedFilesToGameDir(tempDir, gameDir); - - PackCore.LOGGER.info("[Selective Config Apply Service] Successfully applied {} selected files", - selectedPaths.size()); - progressCallback.accept("Complete!"); - return true; - - } finally { - // Cleanup temp directory - deleteDirectory(tempDir); - } - - } catch (Exception e) { - PackCore.LOGGER. error("[Selective Config Apply Service] Failed to apply selected files", e); - progressCallback.accept("Error: " + e.getMessage()); - return false; - } - } - - /** - * Extract only the selected files from the zip - */ - private static void extractSelectedFiles( - Path zipPath, - Set selectedPaths, - Path tempDir, - Consumer progressCallback) throws IOException { - - try (ZipFile zipFile = new ZipFile(zipPath.toFile())) { - int processed = 0; - int total = selectedPaths.size(); - - for (String selectedPath : selectedPaths) { - ZipEntry entry = zipFile.getEntry(selectedPath); - if (entry == null) { - PackCore.LOGGER.warn("[Selective Config Apply Service] Entry not found in zip: {}", selectedPath); - continue; - } - - Path targetPath = tempDir.resolve(selectedPath); - - if (entry.isDirectory()) { - Files.createDirectories(targetPath); - - // Extract all files in this directory - extractDirectory(zipFile, selectedPath, tempDir); - } else { - Files.createDirectories(targetPath.getParent()); - - try (var is = zipFile.getInputStream(entry)) { - Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } - - processed++; - int percentage = (processed * 100) / total; - progressCallback.accept(String. format("Extracting: %d%%", percentage)); - } - } - } - - /** - * Extract all files in a directory from the zip - */ - private static void extractDirectory(ZipFile zipFile, String dirPath, Path tempDir) - throws IOException { - Enumeration entries = zipFile.entries(); - - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String entryName = entry.getName(); - - if (entryName.startsWith(dirPath) && !entry.isDirectory()) { - Path targetPath = tempDir.resolve(entryName); - Files.createDirectories(targetPath. getParent()); - - try (var is = zipFile.getInputStream(entry)) { - Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } - } - } - - /** - * Copy extracted files to game directory - */ - private static void copySelectedFilesToGameDir(Path tempDir, Path gameDir) - throws IOException { - try (var paths = Files.walk(tempDir)) { - paths.filter(Files::isRegularFile). forEach(sourcePath -> { - try { - Path relativePath = tempDir.relativize(sourcePath); - Path targetPath = gameDir.resolve(relativePath); - - Files.createDirectories(targetPath.getParent()); - Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); - - PackCore.LOGGER.debug("[Selective Config Apply Service] Copied: {}", relativePath); - } catch (IOException e) { - PackCore.LOGGER.warn("[Selective Config Apply Service] Failed to copy file: {}", sourcePath, e); - } - }); - } - } - - /** - * Determine the file type based on path - */ - private static SelectableFile.FileType determineFileType(String path) { - path = path.toLowerCase(); - - if (path.equals("options.txt")) { - return SelectableFile.FileType.GAME_OPTIONS; - } else if (path.equals("servers.dat")) { - return SelectableFile.FileType.SERVER_LIST; - } else if (path. startsWith("config/")) { - return SelectableFile.FileType.MOD_CONFIG; - } else if (path.startsWith("resourcepacks/")) { - return SelectableFile.FileType.RESOURCE_PACK; - } - - return SelectableFile.FileType.OTHER; - } - - /** - * Get display name from path - */ - private static String getDisplayName(String path) { - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - - int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - return path.substring(lastSlash + 1); - } - - return path; - } - - /** - * Get description for a file - */ - private static String getFileDescription(String path, SelectableFile.FileType type) { - // Check for well-known files first - String description = KNOWN_FILE_DESCRIPTIONS.get(path. toLowerCase()); - if (description != null) { - return description; - } - - // Extract mod name from config path - if (path.startsWith("config/")) { - String fileName = path.substring("config/".length()); - int dotIndex = fileName.indexOf('.'); - if (dotIndex > 0) { - String modName = fileName.substring(0, dotIndex); - // Capitalize first letter - modName = modName.substring(0, 1).toUpperCase() + modName.substring(1); - return "Configuration for " + modName; - } - } - - // Fall back to generic description based on type - return switch (type) { - case MOD_CONFIG -> "Mod configuration file"; - case GAME_OPTIONS -> "Minecraft game settings"; - case KEYBINDINGS -> "Control bindings"; - case RESOURCE_PACK -> "Resource pack"; - case SERVER_LIST -> "Multiplayer server list"; - case OTHER -> "Configuration file"; - }; - } - - /** - * Known file descriptions for common configs - * This map can be expanded as you add more mods - */ - private static final Map KNOWN_FILE_DESCRIPTIONS = Map.ofEntries( - // Game files - Map.entry("options. txt", "Minecraft video settings, controls, and preferences"), - Map.entry("servers.dat", "Multiplayer server list"), - - // Popular mod configs - Map.entry("config/skyblocker.json", "SkyBlocker - Item highlighting, macro features"), - Map.entry("config/firmament.json", "Firmament - Repository integration, features"), - Map.entry("config/skyhanni.json", "SkyHanni - Events, diana helper, farming"), - Map.entry("config/dungeons-guide.json", "Dungeons Guide - Dungeon secrets and routing"), - Map.entry("config/neu.json", "NotEnoughUpdates - Auction house, recipes, overlays"), - Map.entry("config/skytils.json", "Skytils - Dungeons, kuudra, slayer features"), - - // UI mods - Map.entry("config/owo-ui.json", "owo-ui library configuration"), - Map.entry("config/isxander-main-menu-credits.json", "Main menu credits display"), - - // Performance mods - Map.entry("config/sodium-options.json", "Sodium - Performance optimization settings"), - Map.entry("config/iris. json5", "Iris Shaders - Shader pack settings") - ); - - /** - * Delete directory recursively - */ - private static void deleteDirectory(Path directory) { - try (var paths = Files.walk(directory)) { - paths.sorted(Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - PackCore.LOGGER. debug("[Selective Config Apply Service] Could not delete: {}", path); - } - }); - } catch (IOException e) { - PackCore.LOGGER.warn("[Selective Config Apply Service] Failed to delete temp directory: {}", directory); - } - } - - /** - * Get groups of related files (e.g., all files for a specific mod) - */ - public static Map> groupFilesByMod( - List files) { - Map> groups = new LinkedHashMap<>(); - - for (SelectableFile file : files) { - String groupName = determineGroup(file.path); - groups.computeIfAbsent(groupName, k -> new ArrayList<>()).add(file); - } - - return groups; - } - - /** - * Determine which mod group a file belongs to - */ - private static String determineGroup(String path) { - if (path.equals("options.txt")) return "Minecraft Settings"; - if (path.equals("servers.dat")) return "Server List"; - if (path. startsWith("config/")) { - String fileName = path.substring("config/".length()); - int dotIndex = fileName.indexOf('.'); - if (dotIndex > 0) { - String modName = fileName.substring(0, dotIndex); - // Capitalize and format - return formatModName(modName); - } - } - return "Other Files"; - } - - /** - * Format mod name for display - */ - private static String formatModName(String modName) { - // Handle common patterns - modName = modName. replace("-", " "); - modName = modName.replace("_", " "); - - // Capitalize each word - String[] words = modName.split(" "); - StringBuilder formatted = new StringBuilder(); - for (String word : words) { - if (! word.isEmpty()) { - formatted.append(Character.toUpperCase(word.charAt(0))) - .append(word. substring(1).toLowerCase()) - .append(" "); - } - } - - return formatted.toString().trim(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/apply/WizardOptionApplyService.java b/src/main/java/com/github/kd_gaming1/packcore/config/apply/WizardOptionApplyService.java deleted file mode 100644 index 6373fbd..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/apply/WizardOptionApplyService.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.github.kd_gaming1.packcore.config.apply; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import com.github.kd_gaming1.packcore.integration.resourcepack.ResourcePackManager; -import com.github.kd_gaming1.packcore.integration.minecraft.PerformanceProfileService; -import com.github.kd_gaming1.packcore.integration.tabdesign.TabDesignManager; -import com.github.kd_gaming1.packcore.integration.itembackground.ItemBackgroundManager; - -import java.util.List; -import java.util.Set; -import java.util.Map; -import java.util.LinkedHashMap; -import java.util.concurrent.CompletableFuture; - -/** - * Service responsible for applying all wizard configurations - */ -public class WizardOptionApplyService { - - public interface ProgressCallback { - void updateProgress(String stepKey, String status, String errorMessage); - } - - public record ConfigurationResult(boolean overallSuccess, Map failedSteps, - Map successfulSteps) { - } - - public static CompletableFuture applyAllConfigurations() { - return applyAllConfigurationsWithProgress(null).thenApply(ConfigurationResult::overallSuccess); - } - - public static CompletableFuture applyAllConfigurationsWithProgress(ProgressCallback progressCallback) { - WizardDataStore dataManager = WizardDataStore.getInstance(); - - return CompletableFuture.supplyAsync(() -> { - Map failedSteps = new LinkedHashMap<>(); - Map successfulSteps = new LinkedHashMap<>(); - - try { - PackCore.LOGGER.info("Starting comprehensive configuration application"); - - // Step 1: Apply Performance Profile (Optimization Profile) - String optimizationProfile = dataManager.getOptimizationProfile(); - if (!optimizationProfile.isEmpty()) { - if (progressCallback != null) { - progressCallback.updateProgress("performance", "running", null); - } - - boolean performanceApplied = applyPerformanceProfile(optimizationProfile); - if (!performanceApplied) { - String error = "Could not apply the '" + optimizationProfile + "' performance profile. The settings may be incompatible or already active."; - PackCore.LOGGER.warn(error); - failedSteps.put("Performance Settings", error); - if (progressCallback != null) { - progressCallback.updateProgress("performance", "error", "Setup failed"); - } - } else { - String success = "Successfully configured performance to '" + optimizationProfile + "' profile"; - PackCore.LOGGER.info(success); - successfulSteps.put("Performance Settings", success); - if (progressCallback != null) { - progressCallback.updateProgress("performance", "success", null); - } - } - } - - // Step 2: Apply Tab Design - String tabDesign = dataManager.getTabDesign(); - if (!tabDesign.isEmpty()) { - if (progressCallback != null) { - progressCallback.updateProgress("tabdesign", "running", null); - } - - boolean tabDesignApplied = TabDesignManager.applyTabDesignFromWizard(); - if (!tabDesignApplied) { - String error = "Could not apply the '" + tabDesign + "' tab menu style. The theme may be unavailable."; - PackCore.LOGGER.warn(error); - failedSteps.put("Tab Menu Style", error); - if (progressCallback != null) { - progressCallback.updateProgress("tabdesign", "error", "Theme unavailable"); - } - } else { - String success = "Successfully applied '" + tabDesign + "' tab menu style"; - PackCore.LOGGER.info(success); - successfulSteps.put("Tab Menu Style", success); - if (progressCallback != null) { - progressCallback.updateProgress("tabdesign", "success", null); - } - } - } - - // Step 3: Apply Item Background - String itemBackground = dataManager.getItemBackground(); - if (!itemBackground.isEmpty()) { - if (progressCallback != null) { - progressCallback.updateProgress("itembackground", "running", null); - } - - boolean itemBackgroundApplied = ItemBackgroundManager.applyItemBackgroundFromWizard(); - if (!itemBackgroundApplied) { - String error = "Could not apply the '" + itemBackground + "' item background style. Skyblocker may not be installed or the config structure changed."; - PackCore.LOGGER.warn(error); - failedSteps.put("Item Background Style", error); - if (progressCallback != null) { - progressCallback.updateProgress("itembackground", "error", "Style unavailable"); - } - } else { - String success = "Successfully applied '" + itemBackground + "' item background style"; - PackCore.LOGGER.info(success); - successfulSteps.put("Item Background Style", success); - if (progressCallback != null) { - progressCallback.updateProgress("itembackground", "success", null); - } - } - } - - // Step 4: Apply Resource Packs - List resourcePacks = dataManager.getResourcePacksOrdered(); - if (!resourcePacks.isEmpty()) { - if (progressCallback != null) { - progressCallback.updateProgress("resourcepacks", "running", null); - } - - try { - boolean resourcePacksApplied = ResourcePackManager.applyResourcePacksOrdered(resourcePacks) - .exceptionally(ex -> { - PackCore.LOGGER.error("Exception while applying resource packs", ex); - String userFriendlyError = "Failed to enable resource packs. They may be missing or corrupted."; - failedSteps.put("Resource Packs", userFriendlyError); - return false; - }).join(); - - if (!resourcePacksApplied) { - String error = "Could not enable the selected resource packs: " + String.join(", ", resourcePacks) + ". They may already be active or unavailable."; - PackCore.LOGGER.warn(error); - failedSteps.put("Resource Packs", error); - if (progressCallback != null) { - progressCallback.updateProgress("resourcepacks", "error", "Activation failed"); - } - } else { - String success = "Successfully enabled " + resourcePacks.size() + " resource pack(s)"; - PackCore.LOGGER.info(success); - successfulSteps.put("Resource Packs", success); - if (progressCallback != null) { - progressCallback.updateProgress("resourcepacks", "success", null); - } - } - } catch (Exception e) { - String error = "An error occurred while trying to enable resource packs. Check that they're properly installed."; - PackCore.LOGGER.error(error, e); - failedSteps.put("Resource Packs", error); - if (progressCallback != null) { - progressCallback.updateProgress("resourcepacks", "error", "Error occurred"); - } - } - } - - // Step 5: Apply Additional Settings - Set additionalSettings = dataManager.getAdditionalSettings(); - if (!additionalSettings.isEmpty()) { - if (progressCallback != null) { - progressCallback.updateProgress("additional", "running", null); - } - - boolean additionalSettingsApplied = applyAdditionalSettings(additionalSettings); - if (!additionalSettingsApplied) { - String error = "Some extra settings could not be applied. They may conflict with existing settings."; - PackCore.LOGGER.warn(error); - failedSteps.put("Extra Settings", error); - if (progressCallback != null) { - progressCallback.updateProgress("additional", "error", "Some failed"); - } - } else { - String success = "Successfully applied all extra settings"; - PackCore.LOGGER.info(success); - successfulSteps.put("Extra Settings", success); - if (progressCallback != null) { - progressCallback.updateProgress("additional", "success", null); - } - } - } - - boolean overallSuccess = failedSteps.isEmpty(); - PackCore.LOGGER.info("Configuration application completed with overall success: {}", overallSuccess); - - if (!overallSuccess) { - PackCore.LOGGER.warn("Failed steps: {}", failedSteps); - } - - return new ConfigurationResult(overallSuccess, failedSteps, successfulSteps); - - } catch (Exception e) { - PackCore.LOGGER.error("Fatal error during configuration application", e); - failedSteps.put("Setup Error", "An unexpected problem occurred during setup. Please try again or skip this step."); - return new ConfigurationResult(false, failedSteps, successfulSteps); - } - }); - } - - private static boolean applyPerformanceProfile(String optimizationProfile) { - try { - PerformanceProfileService.PerformanceProfile profile = mapToPerformanceProfile(optimizationProfile); - if (profile != null) { - PerformanceProfileService.ProfileResult result = PerformanceProfileService.applyPerformanceProfile(profile); - return result.isFullySuccessful(); - } else { - PackCore.LOGGER.warn("Unrecognized performance profile: {}", optimizationProfile); - return false; - } - } catch (Exception e) { - PackCore.LOGGER.error("Failed to apply performance profile: {}", optimizationProfile, e); - return false; - } - } - - private static boolean applyAdditionalSettings(Set additionalSettings) { - try { - boolean allSuccessful = true; - for (String setting : additionalSettings) { - boolean applied = applySingleAdditionalSetting(setting); - if (!applied) { - PackCore.LOGGER.warn("Could not apply extra setting: {}", setting); - allSuccessful = false; - } else { - PackCore.LOGGER.info("Successfully applied extra setting: {}", setting); - } - } - return allSuccessful; - } catch (Exception e) { - PackCore.LOGGER.error("Error while applying extra settings", e); - return false; - } - } - - private static boolean applySingleAdditionalSetting(String setting) { - // Implement specific setting application logic here - PackCore.LOGGER.info("Applying additional setting: {}", setting); - - // Example implementations: - switch (setting.toLowerCase()) { - case "enable_chat_timestamps" -> { - // Apply chat timestamp setting - return true; - } - case "auto_reconnect" -> { - // Apply auto-reconnect setting - return true; - } - case "performance_mode" -> { - // Apply performance mode setting - return true; - } - default -> { - PackCore.LOGGER.warn("Unknown extra setting: {}", setting); - return false; - } - } - } - - private static PerformanceProfileService.PerformanceProfile mapToPerformanceProfile(String optimizationProfile) { - return switch (optimizationProfile.toLowerCase()) { - case "max fps" -> PerformanceProfileService.PerformanceProfile.PERFORMANCE; - case "balanced" -> PerformanceProfileService.PerformanceProfile.BALANCED; - case "quality" -> PerformanceProfileService.PerformanceProfile.QUALITY; - case "shaders" -> PerformanceProfileService.PerformanceProfile.SHADERS; - default -> null; - }; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/backup/BackupManager.java b/src/main/java/com/github/kd_gaming1/packcore/config/backup/BackupManager.java deleted file mode 100644 index fdf9a68..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/backup/BackupManager.java +++ /dev/null @@ -1,568 +0,0 @@ -package com.github.kd_gaming1.packcore.config.backup; - -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.github.kd_gaming1.packcore.util.io.file.ExclusionPatterns; -import com.github.kd_gaming1.packcore.util.io.file.FileUtils; -import com.github.kd_gaming1.packcore.util.io.zip.UnzipAsyncTask; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.google.gson.Gson; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.text.DecimalFormat; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -/** - * Enhanced backup manager with async operations and progress reporting. - * Optimized to stream directly to ZIP to avoid double-copy overhead. - */ -public class BackupManager { - private static final Logger LOGGER = LoggerFactory.getLogger(BackupManager.class); - private static final Gson GSON = GsonUtils.GSON; - private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); - - private static final String BACKUPS_DIR = "packcore/backups"; - private static final String METADATA_FILE = "backup_metadata.json"; - - // Async executor for background operations - private static final ExecutorService BACKUP_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread thread = new Thread(r); - thread.setName("BackupManager-" + thread.threadId()); - thread.setDaemon(true); - return thread; - }); - - // Config-related paths to backup - private static final Set CONFIG_PATHS = Set.of( - "config", - "options.txt", - "servers.dat", - "packcore/current_config.json" - ); - - public enum BackupType { - AUTO("Auto"), - MANUAL("Manual"); - - private final String displayName; - - BackupType(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - } - - public record BackupInfo(String backupId, String timestamp, BackupType type, String configName, - String configVersion, long sizeBytes, String title, String description) { - - public String getDisplayName() { - return String.format("[%s] %s - %s", - type.getDisplayName(), - title != null ? title : (configName != null ? configName : "Unknown Config"), - formatTimestamp()); - } - - private String formatTimestamp() { - try { - LocalDateTime dateTime = LocalDateTime.parse(timestamp, DateTimeFormatter.ISO_LOCAL_DATE_TIME); - return dateTime.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm")); - } catch (Exception e) { - return timestamp; - } - } - } - - /** - * Create an automatic backup before config changes (async) - */ - public static CompletableFuture createAutoBackupAsync(Consumer progressCallback) { - if (!PackCoreConfig.enableAutoBackups) { - LOGGER.debug("Auto-backups are disabled"); - return CompletableFuture.completedFuture(null); - } - - ConfigMetadata currentConfig = ConfigFileRepository.getCurrentConfig(); - String title = "Auto backup before applying: " + - (currentConfig != null ? currentConfig.getName() : "Unknown Config"); - - return createBackupAsync(BackupType.AUTO, title, null, progressCallback); - } - - /** - * Create an automatic backup (blocking fallback) - */ - public static Path createAutoBackup() { - try { - return createAutoBackupAsync(msg -> {}).get(); - } catch (Exception e) { - LOGGER.error("Failed to create auto backup", e); - return null; - } - } - - /** - * Create a manual backup asynchronously - */ - public static CompletableFuture createManualBackupAsync( - String title, String description, Consumer progressCallback) { - return createBackupAsync(BackupType.MANUAL, title, description, progressCallback); - } - - /** - * Create a backup with metadata (async) - */ - static CompletableFuture createBackupAsync( - BackupType type, String title, String description, Consumer progressCallback) { - return createBackupAsync(type, title, description, null, progressCallback); - } - - /** - * Create a backup with metadata (async) with an optional hint that becomes part of the zip filename. - */ - static CompletableFuture createBackupAsync( - BackupType type, String title, String description, String backupIdHint, Consumer progressCallback) { - - return CompletableFuture.supplyAsync(() -> { - try { - progressCallback.accept("Preparing backup..."); - Path gameDir = getGameDirectory(); - return createBackupAsyncInternal(gameDir, type, title, description, backupIdHint, progressCallback).join(); - } catch (Exception e) { - LOGGER.error("Failed to create backup", e); - progressCallback.accept("Backup failed: " + e.getMessage()); - throw new RuntimeException("Backup creation failed", e); - } - }, BACKUP_EXECUTOR); - } - - /** - * Create a backup with explicit game directory - */ - public static CompletableFuture createBackupAsync( - Path gameDir, BackupType type, String title, String description, Consumer progressCallback) { - return createBackupAsyncInternal(gameDir, type, title, description, null, progressCallback); - } - - public static CompletableFuture createBackupAsync( - Path gameDir, BackupType type, String title, String description, String backupIdHint, Consumer progressCallback) { - return createBackupAsyncInternal(gameDir, type, title, description, backupIdHint, progressCallback); - } - - private static String formatBytes(long bytes) { - if (bytes <= 0) return "0 B"; - final String[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB"}; - int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024)); - double value = bytes / Math.pow(1024, digitGroups); - DecimalFormat df = new DecimalFormat("#,##0.#"); - return df.format(value) + " " + units[digitGroups]; - } - - /** - * Internal backup creation method. - * OPTIMIZED: Uses direct streaming to avoid temporary files. - */ - private static CompletableFuture createBackupAsyncInternal( - Path gameDir, BackupType type, String title, String description, String backupIdHint, Consumer progressCallback) { - - return CompletableFuture.supplyAsync(() -> { - long startTime = System.currentTimeMillis(); - boolean debug = PackCoreConfig.enableBackupDebugLogging; - - try { - if (debug) { - LOGGER.info("╔══════════════════════════════════════════════════════════════╗"); - LOGGER.info("║ BACKUP STARTED (Direct Streaming Mode) ║"); - LOGGER.info("╚══════════════════════════════════════════════════════════════╝"); - } - progressCallback.accept("Preparing backup..."); - - // Phase 1: Create directories - long phaseStart = System.currentTimeMillis(); - if (debug) LOGGER.info("[Backup] Phase 1: Creating backup directory..."); - Path backupsDir = gameDir.resolve(BACKUPS_DIR); - Files.createDirectories(backupsDir); - if (debug) LOGGER.info("[Backup] Phase 1 complete: {}ms", System.currentTimeMillis() - phaseStart); - - String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); - String backupId = (sanitizeForBackupId(backupIdHint) != null ? backupIdHint : type.name().toLowerCase()) + "_" + timestamp; - Path backupZip = backupsDir.resolve(backupId + ".zip"); - - // Prepare Metadata - ConfigMetadata currentConfig = ConfigFileRepository.getCurrentConfig(); - BackupInfo backupInfo = new BackupInfo( - backupId, - LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), - type, - currentConfig != null ? currentConfig.getName() : "Unknown", - currentConfig != null ? currentConfig.getVersion() : "1.0.0", - -1, // Size calculated later or on read - title != null ? title : "Manual backup", - description - ); - String metadataJson = GSON.toJson(backupInfo); - - // Phase 2: Create ZIP archive directly from source - phaseStart = System.currentTimeMillis(); - if (debug) LOGGER.info("[Backup] Phase 2: Streaming files directly to ZIP"); - progressCallback.accept("Creating backup archive..."); - - try (FileOutputStream fos = new FileOutputStream(backupZip.toFile()); - BufferedOutputStream bos = new BufferedOutputStream(fos); - ZipOutputStream zos = new ZipOutputStream(bos)) { - - zos.setLevel(3); // Moderate compression for speed - - // 1. Write Metadata - ZipEntry metaEntry = new ZipEntry(METADATA_FILE); - zos.putNextEntry(metaEntry); - zos.write(metadataJson.getBytes(StandardCharsets.UTF_8)); - zos.closeEntry(); - - // 2. Stream config files directly - byte[] buffer = new byte[32768]; - int pathsProcessed = 0; - - for (String configPath : CONFIG_PATHS) { - Path sourcePath = gameDir.resolve(configPath); - if (!Files.exists(sourcePath)) continue; - - progressCallback.accept("Backing up: " + configPath); - - if (Files.isDirectory(sourcePath)) { - // Walk directory and zip - Files.walkFileTree(sourcePath, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (ExclusionPatterns.shouldExclude(gameDir, file)) return FileVisitResult.CONTINUE; - - // Relativize path for ZIP entry - String relative = gameDir.relativize(file).toString().replace(File.separatorChar, '/'); - ZipEntry entry = new ZipEntry(relative); - entry.setTime(attrs.lastModifiedTime().toMillis()); - zos.putNextEntry(entry); - - try (InputStream is = Files.newInputStream(file)) { - int read; - while ((read = is.read(buffer)) != -1) { - zos.write(buffer, 0, read); - } - } - zos.closeEntry(); - return FileVisitResult.CONTINUE; - } - }); - } else { - // Zip single file - String relative = configPath.replace(File.separatorChar, '/'); - ZipEntry entry = new ZipEntry(relative); - entry.setTime(Files.getLastModifiedTime(sourcePath).toMillis()); - zos.putNextEntry(entry); - try (InputStream is = Files.newInputStream(sourcePath)) { - int read; - while ((read = is.read(buffer)) != -1) { - zos.write(buffer, 0, read); - } - } - zos.closeEntry(); - } - pathsProcessed++; - int percentage = (pathsProcessed * 100) / CONFIG_PATHS.size(); - progressCallback.accept(String.format("Zipping: %d%%", percentage)); - } - } - - if (debug) LOGGER.info("[Backup] Phase 2 complete: {}ms", System.currentTimeMillis() - phaseStart); - - long totalTime = System.currentTimeMillis() - startTime; - if (debug) { - long zipSize = Files.size(backupZip); - LOGGER.info("╔══════════════════════════════════════════════════════════════╗"); - LOGGER.info("║ BACKUP COMPLETE (Direct streaming: 2 phases) ║"); - LOGGER.info("║ Total time: {}ms - Size: {}", totalTime, formatBytes(zipSize)); - LOGGER.info("╚══════════════════════════════════════════════════════════════╝"); - } else { - LOGGER.info("Backup created in {}ms: {}", totalTime, backupZip.getFileName()); - } - - CompletableFuture.runAsync(() -> cleanupOldBackups(backupsDir), BACKUP_EXECUTOR); - - progressCallback.accept("Backup complete!"); - return backupZip; - - } catch (Exception e) { - long totalTime = System.currentTimeMillis() - startTime; - LOGGER.error("[Backup] FAILED after {}ms: {}", totalTime, e.getMessage(), e); - progressCallback.accept("Backup failed: " + e.getMessage()); - throw new RuntimeException("Backup creation failed", e); - } - }, BACKUP_EXECUTOR); - } - - private static String sanitizeForBackupId(String input) { - if (input == null) return null; - String s = input.trim().toLowerCase(Locale.ROOT); - if (s.isEmpty()) return null; - s = s.replaceAll("[^a-z0-9]+", "_").replaceAll("^_+|_+$", ""); - if (s.isEmpty()) return null; - int maxLen = 40; - if (s.length() > maxLen) s = s.substring(0, maxLen).replaceAll("_+$", ""); - return s.isEmpty() ? null : s; - } - - /** - * Get list of all backups with metadata (async) - */ - public static CompletableFuture> getBackupsAsync(Path gameDir) { - return CompletableFuture.supplyAsync(() -> { - try { - Path backupsDir = gameDir.resolve(BACKUPS_DIR); - if (!Files.exists(backupsDir)) return new ArrayList<>(); - List backups = new ArrayList<>(); - try (Stream backupFiles = Files.list(backupsDir)) { - backupFiles.filter(path -> path.toString().endsWith(".zip")) - .forEach(backupZip -> { - BackupInfo info = readBackupMetadata(backupZip); - if (info != null) backups.add(info); - }); - } - backups.sort((a, b) -> b.timestamp.compareTo(a.timestamp)); - return backups; - } catch (IOException e) { - LOGGER.error("Failed to list backups", e); - return new ArrayList<>(); - } - }, BACKUP_EXECUTOR); - } - - public static CompletableFuture> getBackupsAsync() { - return getBackupsAsync(getGameDirectory()); - } - - public static List getBackups() { - try { - return getBackupsAsync().get(); - } catch (Exception e) { - LOGGER.error("Failed to get backups", e); - return new ArrayList<>(); - } - } - - private static BackupInfo readBackupMetadata(Path backupZip) { - try (ZipFile zip = new ZipFile(backupZip.toFile())) { - ZipEntry metadataEntry = zip.getEntry(METADATA_FILE); - if (metadataEntry == null) return createLegacyBackupInfo(backupZip); - - try (InputStream inputStream = zip.getInputStream(metadataEntry)) { - String json = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - BackupInfo info = GSON.fromJson(json, BackupInfo.class); - if (info != null && info.sizeBytes() <= 0) { - long zipSize = Files.size(backupZip); - return new BackupInfo(info.backupId(), info.timestamp(), info.type(), info.configName(), info.configVersion(), zipSize, info.title(), info.description()); - } - return info; - } - } catch (Exception e) { - return createLegacyBackupInfo(backupZip); - } - } - - private static BackupInfo createLegacyBackupInfo(Path backupZip) { - try { - String fileName = backupZip.getFileName().toString(); - String backupId = fileName.replace(".zip", ""); - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - // Primitive parsing attempt - if (fileName.contains("auto_")) timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - long size = Files.size(backupZip); - return new BackupInfo(backupId, timestamp, BackupType.MANUAL, "Legacy", "Unknown", size, "Legacy Backup", "No metadata"); - } catch (IOException e) { - return null; - } - } - - /** - * Restore a backup asynchronously with progress - */ - public static CompletableFuture restoreBackupAsync( - BackupInfo backupInfo, Consumer progressCallback) { - - return CompletableFuture.supplyAsync(() -> { - try { - Path gameDir = getGameDirectory(); - Path backupsDir = gameDir.resolve(BACKUPS_DIR); - Path backupZip = backupsDir.resolve(backupInfo.backupId + ".zip"); - - if (!Files.exists(backupZip)) { - throw new FileNotFoundException("Backup file not found: " + backupInfo.backupId); - } - - // Safety backup before restore - createBackupAsync(BackupType.AUTO, "Pre-restore safety backup", "Created before restoring " + backupInfo.backupId, progressCallback).join(); - - progressCallback.accept("Extracting backup..."); - - // Extract backup to temp - Path tempDir = Files.createTempDirectory("packcore_restore"); - try { - UnzipAsyncTask unzipTask = new UnzipAsyncTask(); - unzipTask.unzipAsync(backupZip.toString(), tempDir.toString(), (p, t, percent) -> - progressCallback.accept("Extracting: " + percent + "%") - ).join(); - - progressCallback.accept("Applying restored files..."); - copyRestoredFilesAsync(tempDir, gameDir, progressCallback).join(); - - return true; - } finally { - CompletableFuture.runAsync(() -> { - try { - FileUtils.deleteDirectory(tempDir); - } catch (Exception ignored) {} - }, BACKUP_EXECUTOR); - } - - } catch (Exception e) { - LOGGER.error("Failed to restore backup", e); - progressCallback.accept("Restore failed: " + e.getMessage()); - return false; - } - }, BACKUP_EXECUTOR); - } - - public static boolean restoreBackup(BackupInfo backupInfo) { - try { - return restoreBackupAsync(backupInfo, msg -> {}).get(); - } catch (Exception e) { - LOGGER.error("Failed to restore backup", e); - return false; - } - } - - private static CompletableFuture copyRestoredFilesAsync( - Path sourceDir, Path gameDir, Consumer progressCallback) { - - return CompletableFuture.runAsync(() -> { - try { - List pathsToRestore; - try (Stream paths = Files.walk(sourceDir)) { - pathsToRestore = paths.filter(Files::isRegularFile) - .filter(p -> !p.getFileName().toString().equals(METADATA_FILE)) - .toList(); - } - - int total = pathsToRestore.size(); - AtomicLong processed = new AtomicLong(0); - - for (Path sourcePath : pathsToRestore) { - Path relativePath = sourceDir.relativize(sourcePath); - Path targetPath = gameDir.resolve(relativePath); - - Files.createDirectories(targetPath.getParent()); - Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); - - long current = processed.incrementAndGet(); - if (current % Math.max(1, total / 100) == 0 || current == total) { - int percent = (int) ((current * 100) / total); - progressCallback.accept("Restoring files: " + percent + "%"); - } - } - } catch (IOException e) { - throw new RuntimeException("Failed to restore files", e); - } - }, BACKUP_EXECUTOR); - } - - public static boolean deleteBackup(BackupInfo backupInfo) { - try { - Path gameDir = getGameDirectory(); - Path backupsDir = gameDir.resolve(BACKUPS_DIR); - Path backupZip = backupsDir.resolve(backupInfo.backupId + ".zip"); - - if (Files.exists(backupZip)) { - Files.delete(backupZip); - LOGGER.info("Deleted backup: {}", backupInfo.getDisplayName()); - return true; - } - return false; - } catch (IOException e) { - LOGGER.error("Failed to delete backup", e); - return false; - } - } - - private static void cleanupOldBackups(Path backupsDir) { - try { - Path gameDir = backupsDir.getParent().getParent(); - List backups = getBackupsAsync(gameDir).get(); - List autoBackups = backups.stream() - .filter(backup -> backup.type == BackupType.AUTO) - .toList(); - - if (autoBackups.size() > PackCoreConfig.maxBackups) { - List toDelete = autoBackups.subList(PackCoreConfig.maxBackups, autoBackups.size()); - for (BackupInfo backup : toDelete) { - try { - Path backupZip = backupsDir.resolve(backup.backupId + ".zip"); - if (Files.exists(backupZip)) { - Files.delete(backupZip); - LOGGER.info("Deleted old auto backup: {}", backup.backupId); - } - } catch (IOException e) { - LOGGER.warn("Failed to delete clean up backup", e); - } - } - } - } catch (Exception e) { - LOGGER.error("Failed to cleanup old backups", e); - } - } - - public static void openBackupsFolder() { - CompletableFuture.runAsync(() -> { - try { - Path backupsDir = getGameDirectory().resolve(BACKUPS_DIR); - Files.createDirectories(backupsDir); - if (java.awt.Desktop.isDesktopSupported()) { - java.awt.Desktop.getDesktop().open(backupsDir.toFile()); - } - } catch (Exception e) { - LOGGER.error("Failed to open backups folder", e); - } - }, BACKUP_EXECUTOR); - } - - private static Path getGameDirectory() { - MinecraftClient client = MinecraftClient.getInstance(); - if (client != null && client.runDirectory != null) { - return client.runDirectory.toPath(); - } - return FabricLoader.getInstance().getGameDir(); - } - - public static void shutdown() { - ScheduledBackupManager.shutdown(); - BACKUP_EXECUTOR.shutdown(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/backup/ScheduledBackupManager.java b/src/main/java/com/github/kd_gaming1/packcore/config/backup/ScheduledBackupManager.java deleted file mode 100644 index c94f354..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/backup/ScheduledBackupManager.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.github.kd_gaming1.packcore.config.backup; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.ui.toast.PackCoreToast; -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.google.gson.Gson; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -/** - * Manages automatic scheduled backups every 3 days when user is active - */ -public class ScheduledBackupManager { - private static final Gson GSON = GsonUtils.GSON; - - private static final String LAST_BACKUP_FILE = "packcore/last_auto_backup.json"; - private static final String LAST_ACTIVE_FILE = "packcore/last_active.json"; - private static final long THREE_DAYS_MILLIS = 3 * 24 * 60 * 60 * 1000L; - - private static ScheduledExecutorService scheduledExecutor; - - /** - * Tracking data for scheduled backups - */ - private record BackupTracking(long lastBackupTimestamp, long lastActiveTimestamp) { - public static BackupTracking loadOrCreate(Path filePath) { - try { - if (Files.exists(filePath)) { - String json = Files.readString(filePath, StandardCharsets.UTF_8); - return GSON.fromJson(json, BackupTracking.class); - } - } catch (Exception e) { - PackCore.LOGGER.warn("[ScheduledBackupManager] Failed to load backup tracking", e); - } - return new BackupTracking(0, 0); - } - - public void save(Path filePath) { - try { - Files.createDirectories(filePath.getParent()); - Files.writeString(filePath, GSON.toJson(this), StandardCharsets.UTF_8); - } catch (IOException e) { - PackCore.LOGGER.error("[ScheduledBackupManager] Failed to save backup tracking", e); - } - } - } - - /** - * Initialize the scheduled backup system - * Call this when the game starts - */ - public static void initialize() { - if (scheduledExecutor != null) { - PackCore.LOGGER.debug("[ScheduledBackupManager] Scheduled backup manager already initialized"); - return; - } - - scheduledExecutor = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r); - thread.setName("ScheduledBackupManager"); - thread.setDaemon(true); - return thread; - }); - - // Update last active timestamp on startup - updateLastActive(); - - // Schedule periodic backup check (check every hour) - scheduledExecutor.scheduleAtFixedRate( - ScheduledBackupManager::checkAndPerformScheduledBackup, - 1, // Initial delay: 1 hour - 1, // Period: 1 hour - TimeUnit.HOURS - ); - - PackCore.LOGGER.info("[ScheduledBackupManager] Scheduled backup manager initialized (checks every hour, backups every 3 days)"); - } - - /** - * Update the last active timestamp - * Call this periodically (e.g., on game startup) - */ - public static void updateLastActive() { - try { - Path gameDir = getGameDirectorySafe(); - Path activeFile = gameDir.resolve(LAST_ACTIVE_FILE); - - long currentTime = System.currentTimeMillis(); - Files.createDirectories(activeFile.getParent()); - Files.writeString(activeFile, String.valueOf(currentTime), StandardCharsets.UTF_8); - - PackCore.LOGGER.debug("[ScheduledBackupManager] Updated last active timestamp: {}", currentTime); - } catch (Exception e) { - PackCore. LOGGER.error("[ScheduledBackupManager] Failed to update last active timestamp", e); - } - } - - /** - * Check if a scheduled backup should be performed and execute it - */ - private static void checkAndPerformScheduledBackup() { - if (! PackCoreConfig.enableAutoBackups) { - PackCore.LOGGER.debug("[ScheduledBackupManager] Auto backups disabled, skipping scheduled backup check"); - return; - } - - try { - Path gameDir = getGameDirectorySafe(); - Path trackingFile = gameDir.resolve(LAST_BACKUP_FILE); - - BackupTracking tracking = BackupTracking.loadOrCreate(trackingFile); - long currentTime = System.currentTimeMillis(); - long timeSinceLastBackup = currentTime - tracking.lastBackupTimestamp; - - // Check if 3 days have passed since last backup - if (timeSinceLastBackup < THREE_DAYS_MILLIS) { - long hoursRemaining = (THREE_DAYS_MILLIS - timeSinceLastBackup) / (1000 * 60 * 60); - PackCore.LOGGER.debug("[ScheduledBackupManager] Not time for scheduled backup yet. Hours remaining: {}", hoursRemaining); - return; - } - - // Check if user has been active in the past 3 days - if (! hasBeenActiveInPastThreeDays()) { - PackCore.LOGGER.debug("[ScheduledBackupManager] User has not been active in past 3 days, skipping scheduled backup"); - return; - } - - PackCore.LOGGER.info("[ScheduledBackupManager] Performing scheduled automatic backup"); - - String title = "Scheduled backup - " + - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); - String description = "Automatic backup created after 3 days of activity"; - - BackupManager.createBackupAsync( - gameDir, - BackupManager.BackupType.AUTO, - title, - description, - msg -> PackCore.LOGGER.debug("[ScheduledBackupManager] Scheduled backup: {}", msg) - ).thenAccept(backupPath -> { - if (backupPath != null) { - // Update tracking with new backup timestamp - BackupTracking newTracking = new BackupTracking( - System.currentTimeMillis(), - tracking.lastActiveTimestamp - ); - newTracking.save(trackingFile); - PackCore.LOGGER.info("[ScheduledBackupManager] Scheduled backup completed: {}", backupPath.getFileName()); - - // Show toast notification on the main thread (only if client is available) - MinecraftClient client = MinecraftClient.getInstance(); - if (client != null) { - client.execute(() -> { - PackCoreToast.showBackupComplete( - "Scheduled Config Backup", - backupPath.getFileName().toString(), - false - ); - }); - } - } - }). exceptionally(ex -> { - PackCore.LOGGER.error("[ScheduledBackupManager] Scheduled backup failed", ex); - - // Show error toast on the main thread (only if client is available) - MinecraftClient client = MinecraftClient.getInstance(); - if (client != null) { - client.execute(() -> { - PackCoreToast.showError( - "Scheduled Config Backup Failed", - "Check logs for details" - ); - }); - } - return null; - }); - - } catch (Exception e) { - PackCore.LOGGER. error("[ScheduledBackupManager] Failed to check/perform scheduled backup", e); - } - } - - /** - * Check if user has been active in the past 3 days - */ - private static boolean hasBeenActiveInPastThreeDays() { - try { - Path gameDir = getGameDirectorySafe(); - Path activeFile = gameDir.resolve(LAST_ACTIVE_FILE); - - if (!Files.exists(activeFile)) { - PackCore.LOGGER.debug("[ScheduledBackupManager] No activity file found, considering user active"); - return true; - } - - String content = Files.readString(activeFile, StandardCharsets.UTF_8); - long lastActive = Long.parseLong(content. trim()); - long currentTime = System.currentTimeMillis(); - long daysSinceActive = (currentTime - lastActive) / (1000 * 60 * 60 * 24); - - PackCore.LOGGER.debug("[ScheduledBackupManager] Days since last active: {}", daysSinceActive); - return daysSinceActive <= 3; - - } catch (Exception e) { - PackCore.LOGGER.warn("[ScheduledBackupManager] Failed to check activity, assuming user is active", e); - return true; - } - } - - /** - * Get time until next scheduled backup (in milliseconds) - * Returns -1 if no backup is scheduled - */ - public static long getTimeUntilNextBackup() { - try { - Path gameDir = getGameDirectorySafe(); - Path trackingFile = gameDir.resolve(LAST_BACKUP_FILE); - - BackupTracking tracking = BackupTracking.loadOrCreate(trackingFile); - long currentTime = System.currentTimeMillis(); - long timeSinceLastBackup = currentTime - tracking.lastBackupTimestamp; - - if (timeSinceLastBackup >= THREE_DAYS_MILLIS) { - return 0; // Backup is due now - } - - return THREE_DAYS_MILLIS - timeSinceLastBackup; - - } catch (Exception e) { - PackCore. LOGGER.warn("[ScheduledBackupManager] Failed to calculate time until next backup", e); - return -1; - } - } - - /** - * Safely get game directory - works during pre-launch and post-launch - */ - private static Path getGameDirectorySafe() { - // Try to get from MinecraftClient first (post-launch) - MinecraftClient client = MinecraftClient. getInstance(); - if (client != null && client.runDirectory != null) { - return client.runDirectory. toPath(); - } - - // Fallback to FabricLoader for pre-launch - return FabricLoader.getInstance().getGameDir(); - } - - /** - * Shutdown the scheduled backup manager - * Call this when the game closes - */ - public static void shutdown() { - if (scheduledExecutor != null) { - PackCore.LOGGER.info("[ScheduledBackupManager] Shutting down scheduled backup manager"); - scheduledExecutor.shutdown(); - try { - if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - scheduledExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - scheduledExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - scheduledExecutor = null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/backup/SelectiveBackupRestoreService.java b/src/main/java/com/github/kd_gaming1/packcore/config/backup/SelectiveBackupRestoreService.java deleted file mode 100644 index e15b329..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/backup/SelectiveBackupRestoreService.java +++ /dev/null @@ -1,428 +0,0 @@ -package com.github.kd_gaming1.packcore.config. backup; - -import com.github. kd_gaming1.packcore.config.apply.SelectiveConfigApplyService.SelectableFile; -import com.github.kd_gaming1.packcore.config.apply.FileDescriptionRegistry; -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.google.gson.Gson; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -/** - * Service for selectively restoring specific files from a backup - */ -public class SelectiveBackupRestoreService { - private static final Logger LOGGER = LoggerFactory.getLogger(SelectiveBackupRestoreService.class); - private static final Gson GSON = GsonUtils. GSON; - private static final String PENDING_SELECTIVE_RESTORE_FILE = "packcore_pending_selective_restore.json"; - - /** - * Data class for pending selective restore info - */ - private static class PendingSelectiveRestore { - String backupId; - String backupName; - Set selectedPaths; - - PendingSelectiveRestore(String backupId, String backupName, Set selectedPaths) { - this.backupId = backupId; - this.backupName = backupName; - this.selectedPaths = selectedPaths; - } - } - - /** - * Scan a backup file and build a tree of restorable files - */ - public static CompletableFuture> scanBackupFiles( - BackupManager. BackupInfo backup) { - return CompletableFuture. supplyAsync(() -> { - List files = new ArrayList<>(); - - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path backupsDir = gameDir.resolve("packcore/backups"); - Path backupZip = backupsDir.resolve(backup.backupId() + ".zip"); - - if (! Files.exists(backupZip)) { - LOGGER.error("Backup file not found: {}", backupZip); - return files; - } - - try (ZipFile zipFile = new ZipFile(backupZip.toFile())) { - Enumeration entries = zipFile.entries(); - - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String entryName = entry.getName(); - - // Skip metadata file - if (entryName.equals("backup_metadata.json")) { - continue; - } - - SelectableFile. FileType type = determineFileType(entryName); - - // Get description from registry - var description = FileDescriptionRegistry.getDescription(entryName); - String displayName = description - .map(FileDescriptionRegistry. FileDescription::displayName) - .orElseGet(() -> getDisplayName(entryName)); - - String desc = description - .map(FileDescriptionRegistry.FileDescription::description) - .orElseGet(() -> getFileDescription(entryName, type)); - - files.add(new SelectableFile( - entryName, - displayName, - type, - entry.getSize(), - desc, - entry.isDirectory() - )); - } - - // Sort by type and then name - files.sort(Comparator - .comparing((SelectableFile f) -> f.type(). ordinal()) - .thenComparing(f -> f.displayName())); - - } catch (IOException e) { - LOGGER. error("Failed to scan backup files", e); - } - - return files; - }); - } - - /** - * Schedule selected files to be restored on next game start - */ - public static CompletableFuture scheduleSelectiveRestore( - BackupManager.BackupInfo backup, - Set selectedPaths) { - return CompletableFuture. supplyAsync(() -> { - try { - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path pendingFile = gameDir.resolve(PENDING_SELECTIVE_RESTORE_FILE); - - // Create pending restore info - PendingSelectiveRestore pending = new PendingSelectiveRestore( - backup.backupId(), - backup.getDisplayName(), - selectedPaths - ); - - // Write to file - String json = GSON.toJson(pending); - Files.writeString(pendingFile, json, StandardCharsets. UTF_8, - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - LOGGER.info("[Selective Backup Restore] Scheduled {} files for restoration from: {}", - selectedPaths.size(), backup.getDisplayName()); - - // Schedule game shutdown - MinecraftClient.getInstance().scheduleStop(); - - return true; - - } catch (IOException e) { - LOGGER.error("[Selective Backup Restore] Failed to schedule selective restore", e); - return false; - } - }); - } - - /** - * Check and apply pending selective restore during pre-launch - * - * @return true if a selective restore was performed - */ - public static boolean checkAndApplyPendingSelectiveRestore(Path gameDir) { - Path pendingFile = gameDir.resolve(PENDING_SELECTIVE_RESTORE_FILE); - - if (!Files.exists(pendingFile)) { - return false; - } - - try { - // Read pending restore info - String json = Files.readString(pendingFile, StandardCharsets.UTF_8); - PendingSelectiveRestore pending = GSON.fromJson(json, PendingSelectiveRestore.class); - - if (pending == null || pending.backupId == null || pending.selectedPaths == null) { - LOGGER.warn("[Selective Backup Restore] Invalid pending selective restore file"); - Files.deleteIfExists(pendingFile); - return false; - } - - LOGGER.info("[Selective Backup Restore] Found pending selective restore: {} ({} files)", - pending.backupName, pending.selectedPaths.size()); - - // Get backup zip path - Path backupsDir = gameDir.resolve("packcore/backups"); - Path backupZip = backupsDir.resolve(pending.backupId + ".zip"); - - if (!Files.exists(backupZip)) { - LOGGER.error("[Selective Backup Restore] Backup file not found: {}", backupZip); - Files. deleteIfExists(pendingFile); - return false; - } - - // Create a safety backup before restoring - LOGGER.info("[Selective Backup Restore] Creating safety backup before restore.. ."); - BackupManager.createBackupAsync( - gameDir, - BackupManager.BackupType.AUTO, - "Safety backup before selective restore", - "Auto backup created before restoring files from: " + pending.backupId, - msg -> {} - ).join(); - - // Restore the selected files - boolean success = restoreSelectedFilesSync( - backupZip, - pending.selectedPaths, - gameDir, - msg -> LOGGER.info("[Selective Backup Restore] {}", msg) - ); - - if (success) { - LOGGER.info("[Selective Backup Restore] Successfully restored {} selected files", - pending. selectedPaths.size()); - } else { - LOGGER. error("[Selective Backup Restore] Failed to restore selected files"); - } - - // Clean up pending file - Files.deleteIfExists(pendingFile); - - return success; - - } catch (Exception e) { - LOGGER. error("[Selective Backup Restore] Error processing pending selective restore", e); - try { - Files.deleteIfExists(pendingFile); - } catch (IOException ex) { - LOGGER. warn("[Selective Backup Restore] Failed to clean up pending file", ex); - } - return false; - } - } - - /** - * Restore selected files synchronously (for pre-launch) - */ - private static boolean restoreSelectedFilesSync( - Path backupZip, - Set selectedPaths, - Path gameDir, - Consumer progressCallback) { - - try { - progressCallback.accept("Extracting selected files..."); - - // Extract only selected files - Path tempDir = Files.createTempDirectory("packcore_selective_restore"); - try { - extractSelectedFiles(backupZip, selectedPaths, tempDir, progressCallback); - - progressCallback.accept("Restoring files..."); - - // Copy extracted files to game directory - copyFilesToGameDir(tempDir, gameDir, progressCallback); - - LOGGER.info("[Selective Backup Restore] Successfully restored {} selected files", - selectedPaths.size()); - progressCallback.accept("Restore complete!"); - return true; - - } finally { - // Cleanup temp directory - deleteDirectory(tempDir); - } - - } catch (Exception e) { - LOGGER.error("[Selective Backup Restore] Failed to restore selected files", e); - progressCallback.accept("Error: " + e.getMessage()); - return false; - } - } - - /** - * Extract only the selected files from the backup - */ - private static void extractSelectedFiles( - Path backupZip, - Set selectedPaths, - Path tempDir, - Consumer progressCallback) throws IOException { - - try (ZipFile zipFile = new ZipFile(backupZip.toFile())) { - int processed = 0; - int total = selectedPaths.size(); - - for (String selectedPath : selectedPaths) { - ZipEntry entry = zipFile.getEntry(selectedPath); - if (entry == null) { - LOGGER.warn("Entry not found in backup: {}", selectedPath); - continue; - } - - Path targetPath = tempDir.resolve(selectedPath); - - if (entry.isDirectory()) { - Files.createDirectories(targetPath); - - // Extract all files in this directory - extractDirectory(zipFile, selectedPath, tempDir); - } else { - Files.createDirectories(targetPath. getParent()); - - try (var is = zipFile.getInputStream(entry)) { - Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } - - processed++; - int percentage = (processed * 100) / total; - progressCallback.accept(String. format("Extracting: %d%%", percentage)); - } - } - } - - /** - * Extract all files in a directory from the backup - */ - private static void extractDirectory(ZipFile zipFile, String dirPath, Path tempDir) - throws IOException { - Enumeration entries = zipFile.entries(); - - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String entryName = entry. getName(); - - if (entryName.startsWith(dirPath) && !entry.isDirectory()) { - Path targetPath = tempDir.resolve(entryName); - Files. createDirectories(targetPath. getParent()); - - try (var is = zipFile.getInputStream(entry)) { - Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } - } - } - - /** - * Copy extracted files to game directory - */ - private static void copyFilesToGameDir(Path tempDir, Path gameDir, Consumer progressCallback) - throws IOException { - List filesToCopy = new ArrayList<>(); - - Files.walk(tempDir). forEach(filesToCopy::add); - - int total = filesToCopy.size(); - int processed = 0; - - for (Path sourcePath : filesToCopy) { - if (Files.isRegularFile(sourcePath)) { - Path relativePath = tempDir.relativize(sourcePath); - Path targetPath = gameDir.resolve(relativePath); - - Files.createDirectories(targetPath.getParent()); - Files.copy(sourcePath, targetPath, StandardCopyOption. REPLACE_EXISTING); - - LOGGER.debug("Restored: {}", relativePath); - } - - processed++; - if (processed % 10 == 0) { - int percentage = (processed * 100) / total; - progressCallback. accept(String.format("Restoring files: %d%%", percentage)); - } - } - } - - /** - * Delete directory recursively - */ - private static void deleteDirectory(Path directory) { - try { - Files.walk(directory) - .sorted(Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - LOGGER.debug("Could not delete: {}", path); - } - }); - } catch (IOException e) { - LOGGER.warn("Failed to delete temp directory: {}", directory); - } - } - - // Helper methods - - private static SelectableFile.FileType determineFileType(String path) { - path = path.toLowerCase(); - - if (path.equals("options.txt")) { - return SelectableFile.FileType. GAME_OPTIONS; - } else if (path.equals("servers.dat")) { - return SelectableFile.FileType.SERVER_LIST; - } else if (path.startsWith("config/")) { - return SelectableFile.FileType.MOD_CONFIG; - } else if (path.startsWith("resourcepacks/")) { - return SelectableFile.FileType. RESOURCE_PACK; - } - - return SelectableFile.FileType.OTHER; - } - - private static String getDisplayName(String path) { - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - - int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - return path.substring(lastSlash + 1); - } - - return path; - } - - private static String getFileDescription(String path, SelectableFile.FileType type) { - // Extract mod name from config path - if (path.startsWith("config/")) { - String fileName = path.substring("config/".length()); - int dotIndex = fileName.indexOf('.'); - if (dotIndex > 0) { - String modName = fileName.substring(0, dotIndex); - modName = modName.substring(0, 1).toUpperCase() + modName.substring(1); - return "Configuration for " + modName; - } - } - - return switch (type) { - case MOD_CONFIG -> "Mod configuration file"; - case GAME_OPTIONS -> "Minecraft game settings"; - case KEYBINDINGS -> "Control bindings"; - case RESOURCE_PACK -> "Resource pack"; - case SERVER_LIST -> "Multiplayer server list"; - case OTHER -> "Configuration file"; - }; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/export/ConfigExportService.java b/src/main/java/com/github/kd_gaming1/packcore/config/export/ConfigExportService.java deleted file mode 100644 index 705fefd..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/export/ConfigExportService.java +++ /dev/null @@ -1,408 +0,0 @@ -package com.github.kd_gaming1.packcore.config.export; - -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.github.kd_gaming1.packcore.util.io.file.ExclusionPatterns; -import com.github.kd_gaming1.packcore.util.io.file.FileUtils; -import com.github.kd_gaming1.packcore.ui.component.tree.FileTreeNode; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.util.io.zip.ZipAsyncTask; -import com.google.gson.Gson; -import net.minecraft.client.MinecraftClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.*; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.stream.Stream; - -/** - * Optimized ConfigExportManager with async operations and progress reporting - */ -public class ConfigExportService { - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigExportService.class); - private static final Gson GSON = GsonUtils.GSON; - - private static final int MAX_TREE_DEPTH = 10; - private static final int MAX_CHILDREN_PER_NODE = 100; - private static final int BATCH_SIZE = 50; // Process files in batches - - private final Path gameDir; - private final Path exportDir; - private final Map sizeCache = new ConcurrentHashMap<>(); - - public ConfigExportService() { - this.gameDir = MinecraftClient.getInstance().runDirectory.toPath(); - this.exportDir = gameDir.resolve(ConfigFileRepository.CUSTOM_CONFIGS_PATH); - - try { - Files.createDirectories(exportDir); - } catch (IOException e) { - LOGGER.error("Failed to create export directory", e); - } - } - - /** - * Build initial file tree structure (only top level, lazy load the rest) - */ - public FileTreeNode buildFileTree() { - FileTreeNode root = new FileTreeNode(gameDir, "Game Directory", true); - root.setExpanded(true); - - try (Stream entries = Files.list(gameDir)) { - sortEntries(root, entries); - } catch (IOException e) { - LOGGER.error("Failed to build file tree", e); - } - - return root; - } - - private void sortEntries(FileTreeNode root, Stream entries) { - List sortedEntries = entries - .filter(Files::exists) - .filter(path -> !isHidden(path)) - .sorted(comparePaths()) - .limit(MAX_CHILDREN_PER_NODE) - .toList(); - - for (Path path : sortedEntries) { - FileTreeNode node = createLazyNode(path); - if (!node.isHidden()) { - root.addChild(node); - } - } - } - - /** - * Create a lazy node that doesn't load children immediately - */ - private FileTreeNode createLazyNode(Path path) { - String fileName = path.getFileName().toString(); - boolean isDirectory = Files.isDirectory(path); - - FileTreeNode node = new FileTreeNode(path, fileName, isDirectory); - - if (isHidden(path)) { - node.setHidden(true); - return node; - } - - if (isDirectory) { - // Just check if it has children without loading them - try (Stream children = Files.list(path)) { - boolean hasChildren = children - .anyMatch(child -> !isHidden(child)); - node.setHasUnloadedChildren(hasChildren); - } catch (IOException e) { - LOGGER.debug("Could not check directory: {}", path); - } - } - - return node; - } - - /** - * Load children for a directory node asynchronously - */ - public void loadNodeChildren(FileTreeNode node) { - if (!node.isDirectory() || node.isChildrenLoaded()) { - return; - } - - try (Stream children = Files.list(node.getPath())) { - sortEntries(node, children); - - node.setChildrenLoaded(true); - node.setHasUnloadedChildren(false); - } catch (IOException e) { - LOGGER.debug("Could not list directory: {}", node.getPath()); - } - } - - private boolean isHidden(Path path) { - String name = path.getFileName().toString().toLowerCase(); - - // Use centralized hidden folders list - if (ExclusionPatterns.HIDDEN_FOLDERS.contains(name) || name.startsWith(".")) { - return true; - } - - // Use centralized exclusion check - return ExclusionPatterns.shouldExclude(gameDir, path); - } - - private Comparator comparePaths() { - return Comparator.comparing((Path p) -> !Files.isDirectory(p)) - .thenComparing(p -> p.getFileName().toString().toLowerCase()); - } - - /** - * Get preset paths for common configuration combinations - */ - public Set getPresetPaths(PresetType presetType) { - Set paths = new HashSet<>(); - - switch (presetType) { - case MODS_ONLY -> addIfExists(paths, "config"); - case MINECRAFT_ONLY -> { - addIfExists(paths, "options.txt"); - addIfExists(paths, "servers.dat"); - } - case ALL_CONFIGS -> { - addIfExists(paths, "config"); - addIfExists(paths, "options.txt"); - addIfExists(paths, "servers.dat"); - } - case CLEAR -> paths.clear(); - } - - return paths; - } - - public enum PresetType { - MODS_ONLY("Mod Configs Only"), - MINECRAFT_ONLY("MC Configs Only"), - ALL_CONFIGS("Both Configs"), - CLEAR("Clear All"); - - private final String displayName; - - PresetType(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - } - - private void addIfExists(Set paths, String relativePath) { - Path path = gameDir.resolve(relativePath); - if (Files.exists(path)) { - paths.add(path); - } - } - - /** - * Calculate total size with caching for performance - */ - public long calculateSelectionSize(Set selectedPaths) { - return selectedPaths.parallelStream() - .mapToLong(this::getCachedSize) - .sum(); - } - - private long getCachedSize(Path path) { - return sizeCache.computeIfAbsent(path, p -> { - try { - if (Files.isRegularFile(p)) { - return Files.size(p); - } else if (Files.isDirectory(p)) { - try (Stream paths = Files.walk(p)) { - return paths - .filter(Files::isRegularFile) - .mapToLong(file -> { - try { - return Files.size(file); - } catch (IOException e) { - return 0L; - } - }) - .sum(); - } - } - } catch (IOException e) { - LOGGER.debug("Could not calculate size for: {}", p); - } - return 0L; - }); - } - - /** - * Scan mods folder and return list of mod names - */ - public List scanInstalledMods() { - List mods = new ArrayList<>(); - Path modsDir = gameDir.resolve("mods"); - - if (Files.exists(modsDir) && Files.isDirectory(modsDir)) { - try (Stream stream = Files.list(modsDir)) { - stream.filter(Files::isRegularFile) - .filter(p -> { - String name = p.getFileName().toString().toLowerCase(); - return name.endsWith(".jar") || name.endsWith(".zip"); - }) - .map(p -> p.getFileName().toString() - .replaceAll("\\.(jar|zip)$", "")) - .sorted() - .forEach(mods::add); - } catch (IOException e) { - LOGGER.error("Failed to scan mods folder", e); - } - } - - return mods; - } - - /** - * Export configuration asynchronously with progress reporting - */ - public Path exportConfigAsync(ExportRequest request, Consumer progressCallback) throws IOException { - validateExportRequest(request); - - Path tempDir = Files.createTempDirectory("packcore_export"); - - try { - LOGGER.info("Starting async export for {} selected paths", request.selectedPaths.size()); - - // Copy selected paths to temp directory - progressCallback.accept("Copying files..."); - copySelectedPathsAsync(request.selectedPaths, tempDir, progressCallback); - - // Create metadata - progressCallback.accept("Creating metadata..."); - ConfigMetadata metadata = ConfigMetadata.builder() - .name(request.name) - .description(request.description) - .version(request.version) - .author(request.author) - .targetResolution(request.targetResolution) - .mods(request.includedMods) - .source("Community") - .createdNow() - .build(); - - // Write metadata file - Path metadataPath = tempDir.resolve(ConfigFileRepository.METADATA_FILE); - Files.writeString(metadataPath, GSON.toJson(metadata), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - // Create zip file - progressCallback.accept("Creating zip archive..."); - String zipFileName = generateZipFileName(request.name); - Path zipPath = exportDir.resolve(zipFileName); - - // Use async zip utility - ZipAsyncTask zipFiles = new ZipAsyncTask(); - CompletableFuture zipFuture = zipFiles.zipDirectoryAsync( - tempDir.toFile(), - zipPath.toString(), - (bytesProcessed, totalBytes, percentage) -> progressCallback.accept(String.format("Zipping: %d%%", percentage)) - ); - - // Wait for zipping to complete - zipFuture.join(); - - progressCallback.accept("Export complete!"); - LOGGER.info("Config exported successfully to: {}", zipPath); - return zipPath; - - } finally { - // Clean up temp directory in background - CompletableFuture.runAsync(() -> { - try { - FileUtils.deleteDirectory(tempDir); - } catch (Exception e) { - LOGGER.warn("Failed to clean up temp directory", e); - } - }); - } - } - - /** - * Export configuration synchronously (fallback method) - */ - public Path exportConfig(ExportRequest request) throws IOException { - return exportConfigAsync(request, message -> { - }); - } - - private void validateExportRequest(ExportRequest request) { - if (request.selectedPaths == null || request.selectedPaths.isEmpty()) { - throw new IllegalArgumentException("No paths selected for export"); - } - if (request.name == null || request.name.isBlank()) { - throw new IllegalArgumentException("Config name is required"); - } - } - - private void copySelectedPathsAsync(Set selectedPaths, Path targetDir, - Consumer progressCallback) { - int total = selectedPaths.size(); - int processed; - - // Process in batches for better performance - List pathList = new ArrayList<>(selectedPaths); - for (int i = 0; i < pathList.size(); i += BATCH_SIZE) { - int end = Math.min(i + BATCH_SIZE, pathList.size()); - List batch = pathList.subList(i, end); - - // Process batch in parallel - batch.parallelStream().forEach(selectedPath -> { - try { - if (!Files.exists(selectedPath)) { - LOGGER.warn("Selected path does not exist: {}", selectedPath); - return; - } - - Path relativePath = gameDir.relativize(selectedPath); - Path targetPath = targetDir.resolve(relativePath); - - if (Files.isDirectory(selectedPath)) { - // Use exclusion-aware copy for directories - FileUtils.copyDirectoryWithExclusions(selectedPath, targetPath, gameDir); - } else { - Files.createDirectories(targetPath.getParent()); - Files.copy(selectedPath, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - LOGGER.error("Failed to copy: {}", selectedPath, e); - } - }); - - processed = end; - int percentage = (processed * 100) / total; - progressCallback.accept(String.format("Copying files: %d%%", percentage)); - } - } - - private String generateZipFileName(String configName) { - String sanitized = configName.replaceAll("[^a-zA-Z0-9\\-_]", "_"); - String timestamp = LocalDateTime.now() - .format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); - return sanitized + "_" + timestamp + ".zip"; - } - - public void openExportFolder() { - try { - java.awt.Desktop.getDesktop().open(exportDir.toFile()); - } catch (Exception e) { - LOGGER.error("Failed to open export folder", e); - } - } - - /** - * Export request data class - */ - public record ExportRequest(Set selectedPaths, String name, String description, String version, String author, - String targetResolution, List includedMods) { - public ExportRequest(Set selectedPaths, String name, String description, - String version, String author, String targetResolution, - List includedMods) { - this.selectedPaths = selectedPaths; - this.name = name; - this.description = description; - this.version = version; - this.author = author; - this.targetResolution = targetResolution; - this.includedMods = includedMods != null ? includedMods : new ArrayList<>(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/imports/ConfigImportService.java b/src/main/java/com/github/kd_gaming1/packcore/config/imports/ConfigImportService.java deleted file mode 100644 index 354271e..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/imports/ConfigImportService.java +++ /dev/null @@ -1,455 +0,0 @@ -package com.github.kd_gaming1.packcore.config.imports; - -import com.github.kd_gaming1.packcore.config.apply.ConfigApplyService; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.util.task.ProgressListener; -import net.minecraft.client.MinecraftClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.Desktop; -import java.io.IOException; -import java.nio.file.*; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -/** - * Manages importing configuration ZIP files with validation. - * Uses a folder-based approach to avoid Swing/AWT headless issues. - */ -public class ConfigImportService { - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigImportService.class); - private static final DateTimeFormatter TIMESTAMP_FORMAT = - DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); - - public static final String IMPORTS_FOLDER = "packcore/imports"; - - /** - * Validation result with details - */ - public record ValidationResult(boolean isValid, String errorMessage) { - - public static ValidationResult valid() { - return new ValidationResult(true, null); - } - - public static ValidationResult invalid(String message) { - return new ValidationResult(false, message); - } - } - - /** - * Represents a file available for import - */ - public record ImportableFile( - Path path, - String fileName, - long fileSize, - LocalDateTime lastModified, - ConfigMetadata metadata, - ValidationResult validation - ) { - public boolean isValid() { - return validation.isValid(); - } - - public String getDisplayName() { - if (metadata != null && metadata.getName() != null) { - return metadata.getName(); - } - return fileName; - } - - public String getFileSizeFormatted() { - if (fileSize < 1024) return fileSize + " B"; - if (fileSize < 1024 * 1024) return String.format("%.1f KB", fileSize / 1024.0); - return String.format("%.1f MB", fileSize / (1024.0 * 1024.0)); - } - } - - /** - * Get the imports folder path, creating it if necessary - */ - public static Path getImportsFolder() { - Path gameDir = MinecraftClient.getInstance().runDirectory.toPath(); - Path importsDir = gameDir.resolve(IMPORTS_FOLDER); - - try { - Files.createDirectories(importsDir); - } catch (IOException e) { - LOGGER.error("Failed to create imports directory", e); - } - - return importsDir; - } - - /** - * Open the imports folder in the system file browser - */ - public static CompletableFuture openImportsFolder() { - return CompletableFuture.supplyAsync(() -> { - try { - Path importsFolder = getImportsFolder(); - - if (!Files.exists(importsFolder)) { - Files.createDirectories(importsFolder); - } - - if (Desktop.isDesktopSupported()) { - Desktop desktop = Desktop.getDesktop(); - if (desktop.isSupported(Desktop.Action.OPEN)) { - desktop.open(importsFolder.toFile()); - LOGGER.info("Opened imports folder: {}", importsFolder); - return true; - } - } - - LOGGER.warn("Desktop operations not supported on this system"); - return false; - - } catch (IOException e) { - LOGGER.error("Failed to open imports folder", e); - return false; - } - }); - } - - /** - * Scan the imports folder for available config files - */ - public static CompletableFuture> scanImportsFolder() { - return CompletableFuture.supplyAsync(() -> { - List importableFiles = new ArrayList<>(); - Path importsFolder = getImportsFolder(); - - if (!Files.exists(importsFolder)) { - LOGGER.warn("Imports folder does not exist: {}", importsFolder); - return importableFiles; - } - - try (Stream files = Files.list(importsFolder)) { - files.filter(path -> path.toString().toLowerCase().endsWith(".zip")) - .forEach(path -> { - try { - // Get file info - long size = Files.size(path); - LocalDateTime modified = LocalDateTime.ofInstant( - Files.getLastModifiedTime(path).toInstant(), - java.time.ZoneId.systemDefault() - ); - - // Validate the file - ValidationResult validation = validateConfigZip(path); - - // Try to read metadata (null if invalid) - ConfigMetadata metadata = null; - if (validation.isValid()) { - metadata = ConfigFileRepository.readMetadataFromZip(path); - } - - ImportableFile importableFile = new ImportableFile( - path, - path.getFileName().toString(), - size, - modified, - metadata, - validation - ); - - importableFiles.add(importableFile); - - } catch (IOException e) { - LOGGER.error("Failed to process file: {}", path, e); - } - }); - - } catch (IOException e) { - LOGGER.error("Failed to scan imports folder", e); - } - - // Sort by last modified (newest first) - importableFiles.sort((a, b) -> b.lastModified().compareTo(a.lastModified())); - - return importableFiles; - }); - } - - /** - * Preview config metadata without importing - includes validation - */ - public static ConfigMetadata previewConfig(Path configPath) { - if (configPath == null || !Files.exists(configPath)) { - LOGGER.error("Config file does not exist: {}", configPath); - return null; - } - - if (!configPath.toString().toLowerCase().endsWith(".zip")) { - LOGGER.error("File is not a zip: {}", configPath); - return null; - } - - // Validate before reading metadata - ValidationResult validation = validateConfigZip(configPath); - if (!validation.isValid) { - LOGGER.error("Config validation failed: {}", validation.errorMessage); - return null; - } - - return ConfigFileRepository.readMetadataFromZip(configPath); - } - - /** - * Validates that a ZIP file is a valid config: - * - Must contain packcore_metadata.json - * - Must NOT contain any .jar files - */ - public static ValidationResult validateConfigZip(Path zipPath) { - if (zipPath == null || !Files.exists(zipPath)) { - return ValidationResult.invalid("File does not exist"); - } - - if (!zipPath.toString().toLowerCase().endsWith(".zip")) { - return ValidationResult.invalid("File must be a .zip file"); - } - - try { - if (Files.size(zipPath) == 0) { - return ValidationResult.invalid("File is empty"); - } - } catch (IOException e) { - return ValidationResult.invalid("Cannot read file size"); - } - - try (ZipFile zipFile = new ZipFile(zipPath.toFile())) { - boolean hasMetadata = false; - boolean hasJarFiles = false; - - var entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String entryName = entry.getName(); - - // Check for metadata file - if (entryName.equals(ConfigFileRepository.METADATA_FILE) || - entryName.endsWith("/" + ConfigFileRepository.METADATA_FILE)) { - hasMetadata = true; - } - - // Check for .jar files (forbidden) - if (entryName.toLowerCase().endsWith(".jar")) { - hasJarFiles = true; - LOGGER.warn("Found .jar file in config: {}", entryName); - } - } - - if (!hasMetadata) { - return ValidationResult.invalid( - """ - Invalid config file: Missing packcore_metadata.json - - This ZIP must contain configuration metadata to be imported.""" - ); - } - - if (hasJarFiles) { - return ValidationResult.invalid( - """ - Invalid config file: Contains .jar files - - Configuration files should not contain mod .jar files. - Please use config files only.""" - ); - } - - return ValidationResult.valid(); - - } catch (IOException e) { - LOGGER.error("Failed to validate config zip", e); - return ValidationResult.invalid( - "Cannot read ZIP file: " + e.getMessage() - ); - } - } - - /** - * Import config file to custom configs directory - */ - public static void importConfig(Path sourceFile, boolean applyImmediately, - ProgressListener callback) { - if (callback == null) { - callback = new ProgressListener() { - @Override - public void onProgress(String msg, int pct) {} - @Override - public void onComplete(boolean success, String msg) {} - }; - } - - final ProgressListener finalCallback = callback; - - CompletableFuture.runAsync(() -> { - try { - finalCallback.onProgress("Validating file...", 10); - - // Perform comprehensive validation - ValidationResult validation = validateConfigZip(sourceFile); - if (!validation.isValid) { - finalCallback.onComplete(false, validation.errorMessage); - return; - } - - finalCallback.onProgress("Reading metadata...", 30); - ConfigMetadata metadata = ConfigFileRepository.readMetadataFromZip(sourceFile); - if (metadata == null || !metadata.isValid()) { - finalCallback.onComplete(false, - "Could not read config metadata or metadata is invalid"); - return; - } - - finalCallback.onProgress("Copying file...", 50); - Path destination = copyToCustomConfigs(sourceFile, metadata); - - if (destination == null) { - finalCallback.onComplete(false, "Failed to copy config file"); - return; - } - - finalCallback.onProgress("Import complete", 80); - - if (applyImmediately) { - finalCallback.onProgress("Scheduling restart...", 90); - - // Create ConfigFile for application - ConfigFileRepository.ConfigFile configFile = - new ConfigFileRepository.ConfigFile( - destination.getFileName().toString(), - destination, - false, - metadata - ); - - // Schedule application on restart - ConfigApplyService.scheduleConfigApplication(configFile); - - finalCallback.onComplete(true, - "Config imported and will be applied on restart."); - } else { - finalCallback.onComplete(true, - "Config imported successfully: " + metadata.getName()); - } - - } catch (Exception e) { - LOGGER.error("Failed to import config", e); - finalCallback.onComplete(false, "Import failed: " + e.getMessage()); - } - }); - } - - /** - * Import and optionally delete the source file from imports folder - */ - public static void importConfigAndCleanup(Path sourceFile, boolean applyImmediately, - boolean deleteAfterImport, ProgressListener callback) { - ProgressListener wrappedCallback = new ProgressListener() { - @Override - public void onProgress(String msg, int pct) { - if (callback != null) callback.onProgress(msg, pct); - } - - @Override - public void onComplete(boolean success, String msg) { - if (success && deleteAfterImport) { - try { - Files.deleteIfExists(sourceFile); - LOGGER.info("Deleted imported file from imports folder: {}", sourceFile); - } catch (IOException e) { - LOGGER.warn("Failed to delete imported file: {}", sourceFile, e); - } - } - if (callback != null) callback.onComplete(success, msg); - } - }; - - importConfig(sourceFile, applyImmediately, wrappedCallback); - } - - private static Path copyToCustomConfigs(Path sourceFile, ConfigMetadata metadata) { - try { - Path gameDir = MinecraftClient.getInstance().runDirectory.toPath(); - Path customConfigsDir = gameDir.resolve(ConfigFileRepository.CUSTOM_CONFIGS_PATH); - Files.createDirectories(customConfigsDir); - - // Generate unique filename - String baseName = metadata.getName() - .replaceAll("[^a-zA-Z0-9\\-_]", "_"); - String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); - String fileName = baseName + "_" + timestamp + ".zip"; - - Path destination = customConfigsDir.resolve(fileName); - - // Ensure unique name - int counter = 1; - while (Files.exists(destination)) { - fileName = baseName + "_" + timestamp + "_" + counter + ".zip"; - destination = customConfigsDir.resolve(fileName); - counter++; - } - - // Copy file - Files.copy(sourceFile, destination, StandardCopyOption.COPY_ATTRIBUTES); - LOGGER.info("Config imported to: {}", destination); - - return destination; - - } catch (IOException e) { - LOGGER.error("Failed to copy config file", e); - return null; - } - } - - /** - * Check if a config with similar name already exists - */ - public static boolean configExists(String configName) { - if (configName == null || configName.isBlank()) { - return false; - } - - String sanitizedName = configName - .replaceAll("[^a-zA-Z0-9\\-_]", "_") - .toLowerCase(); - - return ConfigFileRepository.getAllConfigs().stream() - .anyMatch(config -> config.fileName() - .toLowerCase() - .contains(sanitizedName)); - } - - /** - * Delete a file from the imports folder - */ - public static boolean deleteImportFile(Path filePath) { - try { - Files.deleteIfExists(filePath); - LOGGER.info("Deleted import file: {}", filePath); - return true; - } catch (IOException e) { - LOGGER.error("Failed to delete import file: {}", filePath, e); - return false; - } - } - - /** - * Get the imports folder path as a string for display - */ - public static String getImportsFolderPath() { - return getImportsFolder().toAbsolutePath().toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/model/ConfigMetadata.java b/src/main/java/com/github/kd_gaming1/packcore/config/model/ConfigMetadata.java deleted file mode 100644 index 9c88806..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/model/ConfigMetadata.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.github.kd_gaming1.packcore.config.model; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -/** - * Represents metadata information for a configuration, such as name, description, - * version, author, creation date, target resolution, associated mods, and source. - *

- * Instances are intended to be immutable after creation. - */ -public class ConfigMetadata { - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE_TIME; - - private String name; - private String description; - private String version; - private String author; - private String createdDate; - private String targetResolution; - private List mods; - private String source; // "Official", "Community", or "System" - - // Default constructor for Gson - public ConfigMetadata() { - this.name = "Unknown Config"; - this.description = ""; - this.version = "1.0.0"; - this.author = "Unknown"; - this.createdDate = LocalDateTime.now().format(DATE_FORMAT); - this.targetResolution = "Any"; - this.mods = new ArrayList<>(); - this.source = "Community"; - } - - // Builder pattern for cleaner creation - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private final ConfigMetadata metadata = new ConfigMetadata(); - - public Builder name(String name) { - metadata.name = name != null && !name.isBlank() ? name : "Unknown Config"; - return this; - } - - public Builder description(String description) { - metadata.description = description != null ? description : ""; - return this; - } - - public Builder version(String version) { - metadata.version = version != null && !version.isBlank() ? version : "1.0.0"; - return this; - } - - public Builder author(String author) { - metadata.author = author != null && !author.isBlank() ? author : "Unknown"; - return this; - } - - public Builder targetResolution(String resolution) { - metadata.targetResolution = resolution != null && !resolution.isBlank() ? resolution : "Any"; - return this; - } - - public Builder mods(List mods) { - metadata.mods = mods != null ? new ArrayList<>(mods) : new ArrayList<>(); - return this; - } - - public Builder source(String source) { - metadata.source = source != null ? source : "Community"; - return this; - } - - public Builder createdNow() { - metadata.createdDate = LocalDateTime.now().format(DATE_FORMAT); - return this; - } - - public ConfigMetadata build() { - return metadata; - } - } - - // Getters only - make immutable after creation - public String getName() { return name; } - public String getDescription() { return description; } - public String getVersion() { return version; } - public String getAuthor() { return author; } - public String getCreatedDate() { return createdDate; } - public String getTargetResolution() { return targetResolution; } - public List getMods() { return new ArrayList<>(mods); } // Return copy for safety - public String getSource() { return source; } - - // Convenience method for display - public String getDisplayName() { - return name + " v" + version; - } - - // Validation helper - public boolean isValid() { - return name != null && !name.isBlank(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/storage/ConfigFileRepository.java b/src/main/java/com/github/kd_gaming1/packcore/config/storage/ConfigFileRepository.java deleted file mode 100644 index ffa657a..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/storage/ConfigFileRepository.java +++ /dev/null @@ -1,339 +0,0 @@ -package com.github.kd_gaming1.packcore.config.storage; - -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; -import net.fabricmc.loader.api.FabricLoader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.stream.Stream; - -/** - * Utility class for managing configuration files and their metadata. - * Handles reading, writing, and listing config zip files and their associated metadata. - */ -public class ConfigFileRepository { - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigFileRepository.class); - private static final Gson GSON = GsonUtils.GSON; - - // Standard paths and filenames - /** - * Name of the metadata file stored in config zips and game directory. - */ - public static final String METADATA_FILE = "packcore_metadata.json"; - /** - * Path to official config zips relative to the game directory. - */ - public static final String OFFICIAL_CONFIGS_PATH = "packcore/modpack_config/official_configs"; - /** - * Path to custom config zips relative to the game directory. - */ - public static final String CUSTOM_CONFIGS_PATH = "packcore/modpack_config/custom_configs"; - - /** - * Represents a config file with its metadata. - */ - public record ConfigFile(String fileName, Path path, boolean official, ConfigMetadata metadata) { - /** - * Constructs a ConfigFile instance. - * - * @param fileName Name of the config file. - * @param path Path to the config file. - * @param official Whether the config is official. - * @param metadata Metadata associated with the config. - */ - public ConfigFile(String fileName, Path path, boolean official, ConfigMetadata metadata) { - this.fileName = fileName; - this.path = path; - this.official = official; - this.metadata = metadata != null ? metadata : new ConfigMetadata(); - } - - /** - * @return The file name of the config. - */ - @Override - public String fileName() { - return fileName; - } - - /** - * @return The path to the config file. - */ - @Override - public Path path() { - return path; - } - - /** - * @return True if the config is official, false otherwise. - */ - @Override - public boolean official() { - return official; - } - - /** - * @return The metadata associated with the config. - */ - @Override - public ConfigMetadata metadata() { - return metadata; - } - - /** - * Gets a display name for the config, using metadata if available. - * Falls back to the file name without extension. - * - * @return Display name for the config. - */ - public String getDisplayName() { - if (metadata != null && metadata.isValid()) { - return metadata.getDisplayName(); - } - // Fallback to filename without extension - return fileName.endsWith(".zip") - ? fileName.substring(0, fileName.length() - 4) - : fileName; - } - } - - /** - * Gets the currently applied configuration metadata. - * - * @return Current config metadata, or a default if none is applied. - */ - public static ConfigMetadata getCurrentConfig() { - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path metadataPath = gameDir.resolve(METADATA_FILE); - - if (!Files.exists(metadataPath)) { - return createDefaultConfig(); - } - - try { - String content = Files.readString(metadataPath, StandardCharsets.UTF_8); - ConfigMetadata metadata = GSON.fromJson(content, ConfigMetadata.class); - return metadata != null ? metadata : createDefaultConfig(); - } catch (IOException | JsonSyntaxException e) { - LOGGER.error("Failed to read current config metadata", e); - return createDefaultConfig(); - } - } - - /** - * Creates a default configuration metadata instance. - * - * @return Default ConfigMetadata. - */ - private static ConfigMetadata createDefaultConfig() { - return ConfigMetadata.builder() - .name("Default Configuration") - .description("Stock Minecraft configuration") - .version("1.0.0") - .author("System") - .source("System") - .targetResolution("Any") - .build(); - } - - /** - * Saves the current config metadata to the game directory. - * - * @param metadata The metadata to save. - * @throws IOException If writing fails. - */ - public static void saveCurrentConfig(ConfigMetadata metadata) throws IOException { - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path metadataPath = gameDir.resolve(METADATA_FILE); - - String json = GSON.toJson(metadata); - Files.writeString(metadataPath, json, StandardCharsets.UTF_8); - } - - /** - * Reads metadata from a zip file. - * - * @param zipPath Path to the zip file. - * @return ConfigMetadata if found, otherwise a fallback metadata. - */ - public static ConfigMetadata readMetadataFromZip(Path zipPath) { - if (!Files.exists(zipPath) || !zipPath.toString().endsWith(".zip")) { - LOGGER.warn("Invalid zip path: {}", zipPath); - return null; - } - - try (ZipFile zipFile = new ZipFile(zipPath.toFile())) { - ZipEntry metadataEntry = zipFile.getEntry(METADATA_FILE); - - if (metadataEntry == null) { - LOGGER.debug("No metadata found in zip: {}", zipPath); - return createFallbackMetadata(zipPath); - } - - try (InputStreamReader reader = new InputStreamReader( - zipFile.getInputStream(metadataEntry), StandardCharsets.UTF_8)) { - ConfigMetadata metadata = GSON.fromJson(reader, ConfigMetadata.class); - return metadata != null ? metadata : createFallbackMetadata(zipPath); - } - - } catch (IOException | JsonSyntaxException e) { - LOGGER.error("Failed to read metadata from zip: {}", zipPath, e); - return createFallbackMetadata(zipPath); - } - } - - /** - * Creates fallback metadata for a config zip if no metadata is found. - * - * @param zipPath Path to the zip file. - * @return Fallback ConfigMetadata. - */ - private static ConfigMetadata createFallbackMetadata(Path zipPath) { - String fileName = zipPath.getFileName().toString(); - String displayName = fileName.endsWith(".zip") - ? fileName.substring(0, fileName.length() - 4) - : fileName; - - return ConfigMetadata.builder() - .name(displayName) - .description("No description available") - .version("Unknown") - .author("Unknown") - .source("Unknown") - .build(); - } - - /** - * Gets all available configs (official and custom). - * - * @return List of all ConfigFile instances. - */ - public static List getAllConfigs() { - List configs = new ArrayList<>(); - configs.addAll(getConfigs(OFFICIAL_CONFIGS_PATH, true)); - configs.addAll(getConfigs(CUSTOM_CONFIGS_PATH, false)); - return configs; - } - - /** - * Gets official configs only. - * - * @return List of official ConfigFile instances. - */ - public static List getOfficialConfigs() { - return getConfigs(OFFICIAL_CONFIGS_PATH, true); - } - - /** - * Gets custom configs only. - * - * @return List of custom ConfigFile instances. - */ - public static List getCustomConfigs() { - return getConfigs(CUSTOM_CONFIGS_PATH, false); - } - - /** - * Gets configs from a specific directory. - * - * @param relativePath Relative path to the config directory. - * @param official Whether the configs are official. - * @return List of ConfigFile instances found in the directory. - */ - private static List getConfigs(String relativePath, boolean official) { - List configs = new ArrayList<>(); - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path configDir = gameDir.resolve(relativePath); - - // Create directory if it doesn't exist - if (!Files.exists(configDir)) { - try { - Files.createDirectories(configDir); - LOGGER.info("Created config directory: {}", configDir); - } catch (IOException e) { - LOGGER.error("Failed to create config directory: {}", configDir, e); - } - return configs; - } - - // Read all zip files from directory - try (Stream files = Files.list(configDir)) { - files.filter(Files::isRegularFile) - .filter(path -> path.toString().toLowerCase().endsWith(".zip")) - .forEach(path -> { - String fileName = path.getFileName().toString(); - ConfigMetadata metadata = readMetadataFromZip(path); - - if (metadata != null && official) { - metadata = ConfigMetadata.builder() - .name(metadata.getName()) - .description(metadata.getDescription()) - .version(metadata.getVersion()) - .author(metadata.getAuthor()) - .targetResolution(metadata.getTargetResolution()) - .mods(metadata.getMods()) - .source("Official") - .build(); - } - - configs.add(new ConfigFile(fileName, path, official, metadata)); - }); - - } catch (IOException e) { - LOGGER.error("Failed to read configs from: {}", configDir, e); - } - - return configs; - } - - /** - * Checks if a config file exists in either official or custom directories. - * - * @param fileName Name of the config file (with or without .zip extension). - * @return True if the config exists, false otherwise. - */ - public static boolean configExists(String fileName) { - if (!fileName.endsWith(".zip")) { - fileName += ".zip"; - } - - Path gameDir = FabricLoader.getInstance().getGameDir(); - Path officialPath = gameDir.resolve(OFFICIAL_CONFIGS_PATH).resolve(fileName); - Path customPath = gameDir.resolve(CUSTOM_CONFIGS_PATH).resolve(fileName); - - return Files.exists(officialPath) || Files.exists(customPath); - } - - /** - * Deletes a config file. Only custom configs can be deleted. - * - * @param config The ConfigFile to delete. - * @return True if deletion was successful, false otherwise. - */ - public static boolean deleteConfig(ConfigFile config) { - if (config == null || config.official()) { - return false; // Don't delete official configs - } - - try { - Files.deleteIfExists(config.path()); - LOGGER.info("Deleted config: {}", config.path()); - return true; - } catch (IOException e) { - LOGGER.error("Failed to delete config: {}", config.path(), e); - return false; - } - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/config/update/ConfigUpdateService.java b/src/main/java/com/github/kd_gaming1/packcore/config/update/ConfigUpdateService.java deleted file mode 100644 index c63f218..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/config/update/ConfigUpdateService.java +++ /dev/null @@ -1,449 +0,0 @@ -package com.github.kd_gaming1.packcore.config.update; - -import com.github.kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.github.kd_gaming1.packcore.util.io.zip.UnzipService; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -/** - * Manages automatic config updates from the modpack update distribution folder. - * This system is separate from user imports and handles developer-controlled config updates. - */ -public class ConfigUpdateService { - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigUpdateService.class); - private static final Gson GSON = GsonUtils.GSON; - - // Update system paths - private static final String UPDATES_FOLDER = "packcore/updates"; - private static final String APPLIED_UPDATES_FILE = "packcore/applied_updates.json"; - private static final String UPDATE_MANIFEST_FILE = "update_manifest.json"; - - /** - * Represents metadata for a config update package - */ - public record UpdateManifest( - String updateId, // Unique ID for this update (e.g., "1.2.0_coolnewmod") - String version, // Version string (e.g., "1.2.0") - String description, // What this update contains - String configFileName, // Name of the zip file containing configs - boolean createBackup, // Whether to create backup before applying - List affectedMods // List of mods this update affects - ) { - public UpdateManifest { - if (updateId == null || updateId.isBlank()) { - throw new IllegalArgumentException("updateId cannot be null or blank"); - } - if (configFileName == null || configFileName.isBlank()) { - throw new IllegalArgumentException("configFileName cannot be null or blank"); - } - affectedMods = affectedMods != null ? new ArrayList<>(affectedMods) : new ArrayList<>(); - } - - public boolean isValid() { - return updateId != null && !updateId.isBlank() && - configFileName != null && !configFileName.isBlank(); - } - } - - /** - * Tracks which updates have been applied - */ - private record AppliedUpdatesRecord( - List appliedUpdateIds, - String lastAppliedVersion, - String lastAppliedDate - ) { - public AppliedUpdatesRecord { - appliedUpdateIds = appliedUpdateIds != null ? new ArrayList<>(appliedUpdateIds) : new ArrayList<>(); - } - - public static AppliedUpdatesRecord empty() { - return new AppliedUpdatesRecord(new ArrayList<>(), null, null); - } - - public boolean hasApplied(String updateId) { - return appliedUpdateIds.contains(updateId); - } - - public AppliedUpdatesRecord withNewUpdate(String updateId, String version) { - List newList = new ArrayList<>(appliedUpdateIds); - if (!newList.contains(updateId)) { - newList.add(updateId); - } - String date = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - return new AppliedUpdatesRecord(newList, version, date); - } - } - - /** - * Check for and apply any pending config updates - * - * @param gameDir The game directory - */ - public static void checkAndApplyUpdates(Path gameDir) { - LOGGER.info("Checking for config updates..."); - - Path updatesFolder = gameDir.resolve(UPDATES_FOLDER); - - // Create updates folder if it doesn't exist - if (!Files.exists(updatesFolder)) { - try { - Files.createDirectories(updatesFolder); - LOGGER.info("Created updates folder: {}", updatesFolder); - createReadmeFile(updatesFolder); - } catch (IOException e) { - LOGGER.error("Failed to create updates folder", e); - } - return; - } - - // Load record of applied updates - AppliedUpdatesRecord appliedRecord = loadAppliedUpdates(gameDir); - - // Scan for available updates - List availableUpdates = scanForUpdates(updatesFolder); - - if (availableUpdates.isEmpty()) { - LOGGER.info("No config updates found"); - return; - } - - LOGGER.info("Found {} update package(s)", availableUpdates.size()); - - // Filter out already applied updates - List pendingUpdates = availableUpdates.stream() - .filter(pkg -> !appliedRecord.hasApplied(pkg.manifest.updateId())) - .toList(); - - if (pendingUpdates.isEmpty()) { - LOGGER.info("All updates have already been applied"); - return; - } - - LOGGER.info("Found {} pending update(s) to apply", pendingUpdates.size()); - - // Apply each pending update - boolean anyApplied = false; - AppliedUpdatesRecord currentRecord = appliedRecord; - - for (UpdatePackage updatePkg : pendingUpdates) { - LOGGER.info("Applying update: {} ({})", updatePkg.manifest.updateId(), updatePkg.manifest.description()); - - boolean success = applyUpdate(gameDir, updatePkg); - - if (success) { - // Mark as applied - currentRecord = currentRecord.withNewUpdate( - updatePkg.manifest.updateId(), - updatePkg.manifest.version() - ); - anyApplied = true; - LOGGER.info("Successfully applied update: {}", updatePkg.manifest.updateId()); - - // Archive the update package - archiveUpdate(updatePkg); - } else { - LOGGER.error("Failed to apply update: {}", updatePkg.manifest.updateId()); - } - } - - // Save updated record - if (anyApplied) { - saveAppliedUpdates(gameDir, currentRecord); - LOGGER.info("Config updates applied successfully. Last version: {}", currentRecord.lastAppliedVersion); - } - } - - /** - * Represents an update package ready to be applied - */ - private record UpdatePackage( - Path manifestPath, - Path configZipPath, - UpdateManifest manifest - ) {} - - /** - * Scan the updates folder for available update packages - */ - private static List scanForUpdates(Path updatesFolder) { - List updates = new ArrayList<>(); - - try (Stream files = Files.list(updatesFolder)) { - files.filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().equals(UPDATE_MANIFEST_FILE)) - .forEach(manifestPath -> { - try { - // Read manifest - String json = Files.readString(manifestPath, StandardCharsets.UTF_8); - UpdateManifest manifest = GSON.fromJson(json, UpdateManifest.class); - - if (manifest == null || !manifest.isValid()) { - LOGGER.warn("Invalid manifest at: {}", manifestPath); - return; - } - - // Find corresponding config zip - Path configZip = manifestPath.getParent().resolve(manifest.configFileName()); - - if (!Files.exists(configZip)) { - LOGGER.warn("Config file not found for manifest: {} (expected: {})", - manifestPath, manifest.configFileName()); - return; - } - - updates.add(new UpdatePackage(manifestPath, configZip, manifest)); - LOGGER.debug("Found update package: {} at {}", manifest.updateId(), manifestPath.getParent()); - - } catch (IOException | JsonSyntaxException e) { - LOGGER.error("Failed to read manifest: {}", manifestPath, e); - } - }); - } catch (IOException e) { - LOGGER.error("Failed to scan updates folder", e); - } - - return updates; - } - - /** - * Apply an update package to the game directory - */ - private static boolean applyUpdate(Path gameDir, UpdatePackage updatePkg) { - try { - UpdateManifest manifest = updatePkg. manifest; - - // Create backup if requested - if (manifest.createBackup()) { - LOGGER.info("Creating backup before applying update.. ."); - - // Use pre-launch safe backup method with explicit gameDir - Path backup = BackupManager.createBackupAsync( - gameDir, - BackupManager.BackupType.AUTO, - "Auto backup before update: " + manifest.updateId(), - "Backup created before applying config update", - msg -> LOGGER.debug("Backup progress: {}", msg) - ).join(); - - if (backup != null) { - LOGGER.info("Backup created: {}", backup. getFileName()); - } else { - LOGGER.warn("Failed to create backup, but continuing with update"); - } - } - - // Extract the config zip to game directory - LOGGER.info("Extracting update configs from: {}", updatePkg.configZipPath. getFileName()); - - UnzipService unzipper = new UnzipService(); - unzipper.unzip( - updatePkg.configZipPath.toString(), - gameDir.toString(), - (bytesProcessed, totalBytes, percentage) -> { - if (percentage % 25 == 0) { - LOGGER.info("Update extraction progress: {}%", percentage); - } - } - ); - - LOGGER.info("Update configs extracted successfully"); - return true; - - } catch (IOException e) { - LOGGER. error("Failed to apply update", e); - return false; - } - } - - /** - * Archive an applied update by moving it to an archive subfolder - */ - private static void archiveUpdate(UpdatePackage updatePkg) { - try { - Path archiveFolder = updatePkg.manifestPath.getParent().resolve("applied"); - Files.createDirectories(archiveFolder); - - // Create timestamped archive folder - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); - Path archiveSubfolder = archiveFolder.resolve(updatePkg.manifest.updateId() + "_" + timestamp); - Files.createDirectories(archiveSubfolder); - - // Move manifest - Path archivedManifest = archiveSubfolder.resolve(updatePkg.manifestPath.getFileName()); - Files.move(updatePkg.manifestPath, archivedManifest, StandardCopyOption.REPLACE_EXISTING); - - // Move config zip - Path archivedZip = archiveSubfolder.resolve(updatePkg.configZipPath.getFileName()); - Files.move(updatePkg.configZipPath, archivedZip, StandardCopyOption.REPLACE_EXISTING); - - LOGGER.info("Archived update to: {}", archiveSubfolder); - - } catch (IOException e) { - LOGGER.warn("Failed to archive update package", e); - // Non-critical error, continue - } - } - - /** - * Load the record of applied updates - */ - private static AppliedUpdatesRecord loadAppliedUpdates(Path gameDir) { - Path recordPath = gameDir.resolve(APPLIED_UPDATES_FILE); - - if (!Files.exists(recordPath)) { - return AppliedUpdatesRecord.empty(); - } - - try { - String json = Files.readString(recordPath, StandardCharsets.UTF_8); - AppliedUpdatesRecord record = GSON.fromJson(json, AppliedUpdatesRecord.class); - return record != null ? record : AppliedUpdatesRecord.empty(); - } catch (IOException | JsonSyntaxException e) { - LOGGER.error("Failed to load applied updates record", e); - return AppliedUpdatesRecord.empty(); - } - } - - /** - * Save the record of applied updates - */ - private static void saveAppliedUpdates(Path gameDir, AppliedUpdatesRecord record) { - Path recordPath = gameDir.resolve(APPLIED_UPDATES_FILE); - - try { - Files.createDirectories(recordPath.getParent()); - String json = GSON.toJson(record); - Files.writeString(recordPath, json, StandardCharsets.UTF_8); - LOGGER.info("Saved applied updates record"); - } catch (IOException e) { - LOGGER.error("Failed to save applied updates record", e); - } - } - - /** - * Create a helpful README in the updates folder - */ - private static void createReadmeFile(Path updatesFolder) { - Path readmePath = updatesFolder.resolve("README.txt"); - - try { - String content = """ - ═══════════════════════════════════════════════════════════ - PackCore Automatic Config Updates Folder - ═══════════════════════════════════════════════════════════ - - 📦 What is this folder? - - This folder is used for automatic config updates when you - release a modpack update. It allows you to ship config files - for new mods without overwriting users' existing configs. - - ═══════════════════════════════════════════════════════════ - 🔧 How to use (for modpack developers): - ═══════════════════════════════════════════════════════════ - - 1. Create your config update zip file - - Include ONLY the new/changed config files - - Example: config/newmod/settings.json - - 2. Create update_manifest.json with this structure: - { - "updateId": "1.2.0_newmod", - "version": "1.2.0", - "description": "Added NewMod configuration", - "configFileName": "newmod_config.zip", - "createBackup": true, - "affectedMods": ["newmod"] - } - - 3. Place both files in this folder for distribution - - 4. When users update the modpack, configs apply automatically - - ═══════════════════════════════════════════════════════════ - 📋 Update Manifest Fields: - ═══════════════════════════════════════════════════════════ - - updateId: Unique identifier (prevents re-applying) - version: Modpack version (e.g., "1.2.0") - description: What this update contains - configFileName: Name of the zip file to extract - createBackup: Whether to backup before applying - affectedMods: List of affected mod names - - ═══════════════════════════════════════════════════════════ - ⚠️ Important Notes: - ═══════════════════════════════════════════════════════════ - - - Each updateId can only be applied once - - Applied updates are moved to the 'applied' subfolder - - Users on fresh installs get full configs (updates skipped) - - Only existing users will receive these updates - - ═══════════════════════════════════════════════════════════ - 📂 Folder Structure After Updates: - ═══════════════════════════════════════════════════════════ - - packcore/updates/ - ├── update_manifest.json ← Pending update - ├── newmod_config.zip ← Pending update - ├── README.txt ← This file - └── applied/ ← Archive of applied updates - └── 1.2.0_newmod_20250101_120000/ - ├── update_manifest.json - └── newmod_config.zip - - ═══════════════════════════════════════════════════════════ - """; - - Files.writeString(readmePath, content); - LOGGER.info("Created updates README: {}", readmePath); - } catch (IOException e) { - LOGGER.error("Failed to create updates README", e); - } - } - - /** - * Manually trigger update check (for debugging/testing) - */ - public static void forceUpdateCheck(Path gameDir) { - LOGGER.info("Forcing update check..."); - checkAndApplyUpdates(gameDir); - } - - /** - * Get list of applied updates (for UI display) - */ - public static List getAppliedUpdateIds(Path gameDir) { - AppliedUpdatesRecord record = loadAppliedUpdates(gameDir); - return new ArrayList<>(record.appliedUpdateIds); - } - - /** - * Reset applied updates (for testing - use with caution!) - */ - public static void resetAppliedUpdates(Path gameDir) { - Path recordPath = gameDir.resolve(APPLIED_UPDATES_FILE); - try { - Files.deleteIfExists(recordPath); - LOGGER.warn("Reset applied updates record"); - } catch (IOException e) { - LOGGER.error("Failed to reset applied updates", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/BackupEntry.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/BackupEntry.java new file mode 100644 index 0000000..db27cca --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/BackupEntry.java @@ -0,0 +1,6 @@ +package com.github.kd_gaming1.packcore.configpack; + +import java.nio.file.Path; +import java.time.Instant; + +public record BackupEntry(Path zipPath, String displayName, Instant timestamp) {} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/BackupManager.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/BackupManager.java new file mode 100644 index 0000000..1778278 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/BackupManager.java @@ -0,0 +1,144 @@ +package com.github.kd_gaming1.packcore.configpack; + +import com.github.kd_gaming1.packcore.PackCore; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public final class BackupManager { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/BackupManager"); + private static final Path BACKUP_DIR = PackCore.PACKCORE_DIR.resolve("backups"); + private static final DateTimeFormatter FILE_FORMAT = + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(ZoneId.systemDefault()); + private static final DateTimeFormatter DISPLAY_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); + + private static final Set BACKUP_ROOTS = Set.of( + "config", "options.txt", "servers.dat" + ); + + private static final Set BACKUP_EXCLUDED = Set.of( + "config/skyhanni/repo", + "config/skyhanni/logs", + "config/skyhanni/backup", + "config/skyblocker/item-repo", + "config/skyblocker/config_backups", + "config/skyblocker/backpack-preview", + "config/SBO", + "config/notenoughupdates", + "config/firmament/profiles", + "config/skyocean/data" + ); + + private BackupManager() {} + + private record BackupFile(Path path, Instant modifiedAt) {} + + public static List listBackups() throws IOException { + if (!Files.exists(BACKUP_DIR)) { + return List.of(); + } + + List backupFiles = new ArrayList<>(); + try (Stream stream = Files.list(BACKUP_DIR)) { + stream.filter(path -> path.toString().endsWith(".zip")) + .forEach(path -> { + try { + backupFiles.add(new BackupFile(path, Files.getLastModifiedTime(path).toInstant())); + } catch (IOException ignored) { + } + }); + } + + backupFiles.sort(Comparator.comparing(BackupFile::modifiedAt).reversed()); + + List result = new ArrayList<>(backupFiles.size()); + for (BackupFile backupFile : backupFiles) { + result.add(new BackupEntry( + backupFile.path(), + DISPLAY_FORMAT.format(backupFile.modifiedAt()), + backupFile.modifiedAt() + )); + } + return result; + } + + public static void createBackup(Path gameDir) throws IOException { + String timestamp = FILE_FORMAT.format(Instant.now()); + String zipName = "backup_" + timestamp + ".zip"; + + List paths = collectBackupPaths(gameDir); + if (paths.isEmpty()) { + LOGGER.warn("No files found to back up."); + return; + } + + int guiScale = Minecraft.getInstance().options.guiScale().get(); + + ConfigPackMeta meta = ConfigPackMeta.builder("backup", 0, 0, guiScale) + .name("Manual backup " + timestamp) + .build(); + + ConfigPackBuilder.zipFiles(gameDir, paths, BACKUP_DIR, zipName, meta); + LOGGER.info("Backup created: {}", zipName); + } + + private static List collectBackupPaths(Path gameDir) throws IOException { + List paths = new ArrayList<>(); + + for (String root : BACKUP_ROOTS) { + Path rootPath = gameDir.resolve(root); + if (!Files.exists(rootPath)) { + continue; + } + + if (Files.isRegularFile(rootPath)) { + paths.add(root); + continue; + } + + Files.walkFileTree(rootPath, new SimpleFileVisitor<>() { + @Override + public @NonNull FileVisitResult preVisitDirectory(@NonNull Path dir, @NonNull BasicFileAttributes attrs) { + String relativePath = gameDir.relativize(dir).toString().replace('\\', '/'); + if (BACKUP_EXCLUDED.stream().anyMatch(ex -> relativePath.equals(ex) || relativePath.startsWith(ex + "/"))) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public @NonNull FileVisitResult visitFile(@NonNull Path file, @NonNull BasicFileAttributes attrs) { + String relativePath = gameDir.relativize(file).toString().replace('\\', '/'); + paths.add(relativePath); + return FileVisitResult.CONTINUE; + } + + @Override + public @NonNull FileVisitResult visitFileFailed(@NonNull Path file, @NonNull IOException e) { + LOGGER.warn("Could not read file during backup: {}", file); + return FileVisitResult.CONTINUE; + } + }); + } + + return paths; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackBuilder.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackBuilder.java new file mode 100644 index 0000000..fdb9a16 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackBuilder.java @@ -0,0 +1,109 @@ +package com.github.kd_gaming1.packcore.configpack; + +import com.github.kd_gaming1.packcore.PackCore; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ConfigPackBuilder { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ConfigPackBuilder"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path OUTPUT_DIR = PackCore.PACKCORE_DIR.resolve("user_configs"); + + private ConfigPackBuilder() {} + + public static void zipFolder(Path sourceFolder, String outputZipName, ConfigPackMeta meta) throws IOException { + if (!Files.isDirectory(sourceFolder)) { + throw new IllegalArgumentException("Expected a directory: " + sourceFolder); + } + + List files; + try (var stream = Files.walk(sourceFolder)) { + files = stream.filter(Files::isRegularFile).toList(); + } + + Files.createDirectories(OUTPUT_DIR); + Path outputZip = OUTPUT_DIR.resolve(outputZipName); + + try (ZipOutputStream zip = openZip(outputZip)) { + writeMetaEntry(zip, meta); + for (Path file : files) { + writeEntry(zip, file, sourceFolder.relativize(file).toString()); + } + } + + LOGGER.info("Created zip '{}' from folder '{}'", outputZip.getFileName(), sourceFolder.getFileName()); + } + + public static void zipFiles(Path root, Collection relativePaths, String outputZipName, ConfigPackMeta meta) throws IOException { + zipFiles(root, relativePaths, OUTPUT_DIR, outputZipName, meta); + } + + public static void zipFiles(Path root, Collection relativePaths, Path outputDir, String outputZipName, ConfigPackMeta meta) throws IOException { + Files.createDirectories(outputDir); + Path outputZip = outputDir.resolve(outputZipName); + + try (ZipOutputStream zip = openZip(outputZip)) { + writeMetaEntry(zip, meta); + for (String relative : relativePaths) { + Path file = root.resolve(relative); + if (!Files.isRegularFile(file)) { + LOGGER.warn("Skipping '{}' — not a regular file or does not exist", relative); + continue; + } + writeEntry(zip, file, relative); + } + } + + LOGGER.info("Created zip '{}' with {} file(s)", outputZip.getFileName(), relativePaths.size()); + } + + private static void writeMetaEntry(ZipOutputStream zip, ConfigPackMeta meta) throws IOException { + JsonObject json = new JsonObject(); + json.addProperty("version", meta.version()); + json.addProperty("targetWidth", meta.targetWidth()); + json.addProperty("targetHeight", meta.targetHeight()); + json.addProperty("guiScale", meta.guiScale()); + json.addProperty("createdDate", meta.createdDate()); + + if (meta.name() != null) json.addProperty("name", meta.name()); + if (meta.description() != null) json.addProperty("description", meta.description()); + if (meta.author() != null) json.addProperty("author", meta.author()); + + if (meta.mods() != null && !meta.mods().isEmpty()) { + JsonArray modsArray = new JsonArray(); + meta.mods().forEach(modsArray::add); + json.add("mods", modsArray); + } + + byte[] bytes = GSON.toJson(json).getBytes(StandardCharsets.UTF_8); + zip.putNextEntry(new ZipEntry("pack.json")); + zip.write(bytes); + zip.closeEntry(); + } + + private static ZipOutputStream openZip(Path zipPath) throws IOException { + OutputStream fileOut = Files.newOutputStream(zipPath); + return new ZipOutputStream(fileOut); + } + + private static void writeEntry(ZipOutputStream zip, Path file, String entryName) throws IOException { + zip.putNextEntry(new ZipEntry(entryName.replace('\\', '/'))); + Files.copy(file, zip); + zip.closeEntry(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackEntry.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackEntry.java new file mode 100644 index 0000000..83361a9 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackEntry.java @@ -0,0 +1,14 @@ +package com.github.kd_gaming1.packcore.configpack; + +import com.google.gson.JsonObject; + +import java.nio.file.Path; + +/** + * Holds the result of a successful scan for a single config pack zip. + * + * @param zipPath Absolute path to the zip file on disk. + * @param config Parsed contents of pack.json found inside the zip. + */ +public record ConfigPackEntry(Path zipPath, JsonObject config) { +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackExtractor.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackExtractor.java new file mode 100644 index 0000000..29d10d1 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackExtractor.java @@ -0,0 +1,98 @@ +package com.github.kd_gaming1.packcore.configpack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.Collection; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ConfigPackExtractor { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ConfigPackExtractor"); + + private ConfigPackExtractor() {} + + public static void extractAll(Path zipPath, Path extractionRoot, OverwriteMode overwriteMode) throws IOException { + extractFromZip(zipPath, extractionRoot, overwriteMode, null); + } + + public static void extractSelective(Path zipPath, Path extractionRoot, OverwriteMode overwriteMode, Collection targets) throws IOException { + extractFromZip(zipPath, extractionRoot, overwriteMode, targets); + } + + private static void extractFromZip(Path zipPath, Path extractionRoot, OverwriteMode overwriteMode, Collection targetPaths) throws IOException { + Path absoluteRoot = extractionRoot.toAbsolutePath().normalize(); + Files.createDirectories(absoluteRoot); + + try (ZipFile zipFile = new ZipFile(zipPath.toFile())) { + Enumeration entries = zipFile.entries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + + if (targetPaths != null && !isTargetedEntry(entry.getName(), targetPaths)) { + continue; + } + + extractEntry(zipFile, entry, absoluteRoot, overwriteMode); + } + } + + LOGGER.info("Finished extraction from '{}'", zipPath.getFileName()); + } + + private static boolean isTargetedEntry(String entryName, Collection targetPaths) { + for (String target : targetPaths) { + if (entryName.equals(target) || entryName.startsWith(target + "/")) { + return true; + } + } + return false; + } + + private static void extractEntry(ZipFile zipFile, ZipEntry entry, Path extractionRoot, OverwriteMode overwriteMode) throws IOException { + Path targetPath = extractionRoot.resolve(entry.getName()).normalize(); + + // Prevent zip slip attacks + if (!targetPath.startsWith(extractionRoot)) { + throw new IOException("Zip slip detected: " + entry.getName()); + } + + if (entry.isDirectory()) { + Files.createDirectories(targetPath); + } else { + Files.createDirectories(targetPath.getParent()); + writeEntry(zipFile, entry, targetPath, overwriteMode); + } + } + + private static void writeEntry(ZipFile zipFile, ZipEntry entry, Path targetPath, OverwriteMode overwriteMode) throws IOException { + if (overwriteMode == OverwriteMode.SKIP_EXISTING && Files.exists(targetPath)) return; + + if (overwriteMode == OverwriteMode.FAIL_IF_EXISTS && Files.exists(targetPath)) { + throw new FileAlreadyExistsException(targetPath.toString()); + } + + CopyOption[] options = (overwriteMode == OverwriteMode.REPLACE_EXISTING) + ? new CopyOption[]{ StandardCopyOption.REPLACE_EXISTING } + : new CopyOption[0]; + + try (InputStream in = zipFile.getInputStream(entry)) { + Files.copy(in, targetPath, options); + } + } + + public enum OverwriteMode { + /** Skip extraction if the target file already exists. */ + SKIP_EXISTING, + /** Replace the target file if it already exists. */ + REPLACE_EXISTING, + /** Fail with FileAlreadyExistsException if the target file already exists. */ + FAIL_IF_EXISTS + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackMeta.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackMeta.java new file mode 100644 index 0000000..1b1cc72 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackMeta.java @@ -0,0 +1,86 @@ +package com.github.kd_gaming1.packcore.configpack; + +import java.time.Instant; +import java.util.List; + +/** + * Metadata written into pack.json inside a config pack zip. + * Use {@link Builder} to construct instances. + */ +public final class ConfigPackMeta { + + // Required + private final String version; + private final int targetWidth; + private final int targetHeight; + private final int guiScale; + + // Optional + private final String name; + private final String description; + private final String author; + private final List mods; + + // Auto-generated — caller never sets this + private final String createdDate; + + private ConfigPackMeta(Builder builder) { + this.version = builder.version; + this.targetWidth = builder.targetWidth; + this.targetHeight = builder.targetHeight; + this.guiScale = builder.guiScale; + this.name = builder.name; + this.description = builder.description; + this.author = builder.author; + this.mods = builder.mods != null ? List.copyOf(builder.mods) : List.of(); + this.createdDate = Instant.now().toString(); + } + + public String version() { return version; } + public int targetWidth() { return targetWidth; } + public int targetHeight() { return targetHeight; } + public int guiScale() { return guiScale; } + public String name() { return name; } + public String description() { return description; } + public String author() { return author; } + public List mods() { return mods; } + public String createdDate() { return createdDate; } + + public static Builder builder(String version, int targetWidth, int targetHeight, int guiScale) { + return new Builder(version, targetWidth, targetHeight, guiScale); + } + + public static final class Builder { + + // Required + private final String version; + private final int targetWidth; + private final int targetHeight; + private final int guiScale; + + // Optional + private String name; + private String description; + private String author; + private List mods = List.of(); + + private Builder(String version, int targetWidth, int targetHeight, int guiScale) { + this.version = version; + this.targetWidth = targetWidth; + this.targetHeight = targetHeight; + this.guiScale = guiScale; + } + + public Builder name(String name) { this.name = name; return this; } + public Builder description(String description) { this.description = description; return this; } + public Builder author(String author) { this.author = author; return this; } + public Builder mods(List mods) { + this.mods = mods != null ? mods : List.of(); + return this; + } + + public ConfigPackMeta build() { + return new ConfigPackMeta(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackScanner.java b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackScanner.java new file mode 100644 index 0000000..7a415d1 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/configpack/ConfigPackScanner.java @@ -0,0 +1,66 @@ +package com.github.kd_gaming1.packcore.configpack; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ConfigPackScanner { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ConfigPackScanner"); + private static final String CONFIG_FILE_NAME = "pack.json"; + + public List scanFolder(Path folderPath) throws IOException { + if (!Files.exists(folderPath)) { + Files.createDirectories(folderPath); + } + + if (!Files.isDirectory(folderPath)) { + throw new IllegalArgumentException("Expected a directory: " + folderPath); + } + + List results = new ArrayList<>(); + + try (Stream entries = Files.list(folderPath)) { + entries.filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".zip")) + .forEach(zipPath -> + readConfigFromZip(zipPath).ifPresent(json -> + results.add(new ConfigPackEntry(zipPath, json)) + )); + } + + return results; + } + + private Optional readConfigFromZip(Path zipPath) { + try (ZipFile zipFile = new ZipFile(zipPath.toFile())) { + ZipEntry configEntry = zipFile.getEntry(CONFIG_FILE_NAME); + if (configEntry == null) { + return Optional.empty(); + } + + try (InputStream stream = zipFile.getInputStream(configEntry); + InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + return Optional.of(JsonParser.parseReader(reader).getAsJsonObject()); + } + } catch (IOException | JsonParseException e) { + LOGGER.warn("Failed to read zip: {} - {}", zipPath, e.getMessage()); + return Optional.empty(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/crash/CrashBrandingHandler.java b/src/main/java/com/github/kd_gaming1/packcore/crash/CrashBrandingHandler.java deleted file mode 100644 index 8fa4b9c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/crash/CrashBrandingHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.kd_gaming1.packcore.crash; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; -import net.minecraft.util.crash.CrashReportSection; - -/** - * Handles adding modpack branding information to crash reports. - * This makes it easier for users and support teams to identify the exact - * modpack version and get support resources. - */ -public class CrashBrandingHandler { - - /** - * Adds modpack branding information to a crash report section. - * - * @param section The crash report section to add information to - */ - public static void addBranding(CrashReportSection section) { - ModpackInfo info = PackCore.getModpackInfo(); - - if (info == null) { - section.add("Status", "Modpack information not available"); - return; - } - - // Add modpack identification - String name = info.getName(); - String version = info. getVersion(); - - if (name != null && ! name.equals("YOUR_MODPACK_NAME_HERE") && ! name.isBlank()) { - section. add("Name", name); - } - - if (version != null && !version.isBlank()) { - section. add("Version", version); - } - - // Add Minecraft version - String mcVersion = info.getMinecraftVersion(); - if (mcVersion != null && !mcVersion.isBlank()) { - section. add("Minecraft Version", mcVersion); - } - - // Add author info - String author = info.getAuthor(); - if (author != null && !author.equals("YOUR_NAME_HERE") && !author.isBlank()) { - section.add("Author", author); - } - - // Add support resources (most important for users!) - String discord = info.getDiscord(); - if (discord != null && ! discord.contains("your-invite") && !discord.isBlank()) { - section.add("Discord Support", discord); - } - - String issueTracker = info.getIssueTracker(); - if (issueTracker != null && !issueTracker.contains("yourname/yourmod") && !issueTracker.isBlank()) { - section.add("Issue Tracker", issueTracker); - } - - String website = info.getWebsite(); - if (website != null && ! website.contains("your-website") && !website.isBlank()) { - section.add("Website", website); - } - - String wiki = info.getWiki(); - if (wiki != null && !wiki.contains("your-wiki") && !wiki.isBlank()) { - section.add("Wiki", wiki); - } - - // Add description if available - String description = info. getDescription(); - if (description != null && !description.equals("A brief description of your modpack") && !description.isBlank()) { - section.add("Description", description); - } - - // Add Modrinth project info - String modrinthId = info.getModrinthProjectId(); - if (modrinthId != null && !modrinthId.equals("YOUR_PROJECT_ID_FROM_MODRINTH_URL") && !modrinthId.isBlank()) { - section.add("Modrinth Project", "https://modrinth.com/modpack/" + modrinthId); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/crash/CrashBrandingLogger.java b/src/main/java/com/github/kd_gaming1/packcore/crash/CrashBrandingLogger.java deleted file mode 100644 index 09979da..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/crash/CrashBrandingLogger.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.github.kd_gaming1.packcore.crash; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github. kd_gaming1.packcore.modpack.ModpackInfo; -import net.fabricmc.loader.api.FabricLoader; - -/** - * Logs modpack branding information at startup. - * Helps with log-based debugging when crash reports aren't available. - */ -public class CrashBrandingLogger { - - /** - * Print modpack branding information to logs on startup. - * Call this from PreLaunchEntrypoint or ClientModInitializer. - */ - public static void logBrandingInfo() { - ModpackInfo info = PackCore.getModpackInfo(); - - if (info == null) { - PackCore.LOGGER.warn("╔══════════════════════════════════════════════════════════════╗"); - PackCore. LOGGER.warn("║ Modpack Information - Not Available ║"); - PackCore. LOGGER.warn("╚══════════════════════════════════════════════════════════════╝"); - return; - } - - // Build the branding info display - StringBuilder sb = new StringBuilder(); - sb.append("\n"); - sb.append("╔══════════════════════════════════════════════════════════════╗\n"); - sb.append("║ MODPACK INFORMATION ║\n"); - sb.append("╠══════════════════════════════════════════════════════════════╣\n"); - - // Add modpack identification - appendField(sb, "Name", info.getName(), "YOUR_MODPACK_NAME_HERE"); - appendField(sb, "Version", info.getVersion(), null); - appendField(sb, "Minecraft", info.getMinecraftVersion(), null); - appendField(sb, "Author", info.getAuthor(), "YOUR_NAME_HERE"); - - // Add separator if we have support links - boolean hasLinks = hasValidField(info. getDiscord(), "your-invite") || - hasValidField(info.getIssueTracker(), "yourname/yourmod") || - hasValidField(info.getWebsite(), "your-website"); - - if (hasLinks) { - sb.append("╟──────────────────────────────────────────────────────────────╢\n"); - appendField(sb, "Discord", info.getDiscord(), "your-invite"); - appendField(sb, "Issue Tracker", info.getIssueTracker(), "yourname/yourmod"); - appendField(sb, "Website", info.getWebsite(), "your-website"); - appendField(sb, "Wiki", info.getWiki(), "your-wiki"); - } - - // Add technical info - sb.append("╟──────────────────────────────────────────────────────────────╢\n"); - - // Fabric Loader version - String loaderVersion = FabricLoader. getInstance() - .getModContainer("fabricloader") - .map(modContainer -> modContainer.getMetadata().getVersion().getFriendlyString()) - .orElse("Unknown"); - appendInfoLine(sb, "Fabric Loader", loaderVersion); - - // PackCore version - String packcoreVersion = FabricLoader.getInstance() - .getModContainer("packcore") - .map(modContainer -> modContainer.getMetadata().getVersion().getFriendlyString()) - .orElse("Unknown"); - appendInfoLine(sb, "PackCore", packcoreVersion); - - // Total mods - int modCount = FabricLoader.getInstance().getAllMods().size(); - appendInfoLine(sb, "Total Mods", String.valueOf(modCount)); - - // Java version - String javaVersion = System.getProperty("java.version"); - appendInfoLine(sb, "Java", javaVersion); - - // Add description if available - String description = info.getDescription(); - if (description != null && ! description.equals("A brief description of your modpack") && !description.isBlank()) { - sb.append("╟────────────────────────────��────────────────────────────────╢\n"); - // Wrap description to 60 chars - String wrappedDesc = wrapText(description, 58); - for (String line : wrappedDesc.split("\n")) { - sb.append(String.format("║ %-60s ║\n", line)); - } - } - - sb.append("╚══════════════════════════════════════════════════════════════╝"); - - PackCore.LOGGER.info(sb.toString()); - } - - /** - * Append a field to the output if it has a valid value. - */ - private static void appendField(StringBuilder sb, String label, String value, String invalidPattern) { - if (hasValidField(value, invalidPattern)) { - appendInfoLine(sb, label, value); - } - } - - /** - * Append a line with proper formatting. - */ - private static void appendInfoLine(StringBuilder sb, String label, String value) { - String line = String.format("%s: %s", label, value); - if (line.length() > 58) { - line = line.substring(0, 55) + "..."; - } - sb.append(String.format("║ %-60s ║\n", line)); - } - - /** - * Check if a field has a valid (non-default) value. - */ - private static boolean hasValidField(String value, String invalidPattern) { - if (value == null || value.isBlank()) { - return false; - } - if (invalidPattern != null && value.contains(invalidPattern)) { - return false; - } - return true; - } - - /** - * Wrap text to a maximum line length. - */ - private static String wrapText(String text, int maxLength) { - if (text. length() <= maxLength) { - return text; - } - - StringBuilder result = new StringBuilder(); - String[] words = text.split(" "); - StringBuilder currentLine = new StringBuilder(); - - for (String word : words) { - if (currentLine. length() + word.length() + 1 > maxLength) { - if (currentLine.length() > 0) { - result. append(currentLine).append("\n"); - currentLine = new StringBuilder(); - } - } - - if (currentLine.length() > 0) { - currentLine. append(" "); - } - currentLine.append(word); - } - - if (currentLine.length() > 0) { - result.append(currentLine); - } - - return result.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigPackCard.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigPackCard.java new file mode 100644 index 0000000..6d35c95 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigPackCard.java @@ -0,0 +1,123 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.text.TruncatedTextComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.google.gson.JsonObject; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NonNull; + +import java.util.function.Consumer; + +public class ConfigPackCard extends AbstractComponent { + + private static final int ACCENT_BAR_WIDTH = 3; + private static final int PADDING_VERTICAL = 8; + private static final int PADDING_HORIZONTAL = 10; + private static final int ROW_GAP = 3; + private static final int BADGE_PADDING = 4; + + private static final int COLOR_ACCENT = 0xFF2196F3; + private static final int COLOR_BORDER = 0x552196F3; + private static final int COLOR_BACKGROUND = 0xCC0A1520; + private static final int COLOR_HOVER = 0xCC0D2035; + + private static final int COLOR_ACTIVE_ACCENT = 0xFF4CAF50; + private static final int COLOR_ACTIVE_BORDER = 0x554CAF50; + private static final int COLOR_ACTIVE_BACKGROUND = 0xCC081408; + + private static final int COLOR_BADGE_BACKGROUND = 0x554CAF50; + private static final int COLOR_BADGE_TEXT = 0xFF4CAF50; + + private static final int COLOR_TEXT_MAIN = 0xFFFFFFFF; + private static final int COLOR_TEXT_SUB = 0xFFAAAAAA; + + private final ConfigPackEntry entry; + private final boolean isActive; + private final String badgeText; + private final Consumer onSelect; + + public ConfigPackCard(int x, int y, int width, ConfigPackEntry entry, boolean isActive, String badgeText, Consumer onSelect) { + super(x, y, width, 0); + this.entry = entry; + this.isActive = isActive; + this.badgeText = badgeText; + this.onSelect = onSelect; + this.build(); + } + + private void build() { + JsonObject config = entry.config(); + String name = config.has("name") ? config.get("name").getAsString() : entry.zipPath().getFileName().toString(); + String meta = (config.has("author") ? config.get("author").getAsString() : "Unknown") + + " · v" + (config.has("version") ? config.get("version").getAsString() : "?.?.?"); + + int fontHeight = Minecraft.getInstance().font.lineHeight; + int textX = ACCENT_BAR_WIDTH + PADDING_HORIZONTAL; + int textWidth = getWidth() - textX - PADDING_HORIZONTAL; + + addComponent(new TruncatedTextComponent(textX, PADDING_VERTICAL, textWidth, Component.literal(name), COLOR_TEXT_MAIN)); + + int metaRowY = PADDING_VERTICAL + fontHeight + ROW_GAP; + addComponent(new TextComponent(textX, metaRowY, Component.literal(meta), COLOR_TEXT_SUB)); + + int resolutionRowY = metaRowY + fontHeight + ROW_GAP; + addComponent(new TextComponent(textX, resolutionRowY, Component.literal(formatResolution(config)), COLOR_TEXT_SUB)); + + setHeight(resolutionRowY + fontHeight + PADDING_VERTICAL); + + if (!isActive) { + addWidget(new ButtonWidget(0, 0, getWidth(), getHeight(), Component.empty(), button -> onSelect.accept(entry)) { + @Override + protected void renderContents(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {} + }); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + boolean isHovered = !isActive + && mouseX >= getTotalX() && mouseX <= getTotalX() + getWidth() + && mouseY >= getTotalY() && mouseY <= getTotalY() + getHeight(); + + int cardX = getTotalX(); + int cardY = getTotalY(); + int cardWidth = getWidth(); + int cardHeight = getHeight(); + + int accentColor = isActive ? COLOR_ACTIVE_ACCENT : COLOR_ACCENT; + int borderColor = isActive ? COLOR_ACTIVE_BORDER : COLOR_BORDER; + int backgroundColor = isActive ? COLOR_ACTIVE_BACKGROUND : (isHovered ? COLOR_HOVER : COLOR_BACKGROUND); + + graphics.fill(cardX, cardY, cardX + cardWidth, cardY + cardHeight, backgroundColor); + drawOutline(graphics, cardX, cardY, cardWidth, cardHeight, borderColor); + graphics.fill(cardX, cardY, cardX + ACCENT_BAR_WIDTH, cardY + cardHeight, accentColor); + + if (isActive && badgeText != null && !badgeText.isBlank()) { + var font = Minecraft.getInstance().font; + + int badgeWidth = font.width(badgeText) + (BADGE_PADDING * 2); + int badgeHeight = font.lineHeight + BADGE_PADDING; + int badgeX = cardX + cardWidth - badgeWidth - PADDING_HORIZONTAL; + int badgeY = cardY + (cardHeight - badgeHeight) / 2; + + graphics.fill(badgeX, badgeY, badgeX + badgeWidth, badgeY + badgeHeight, COLOR_BADGE_BACKGROUND); + graphics.drawString(font, badgeText, badgeX + BADGE_PADDING, badgeY + (BADGE_PADDING / 2), COLOR_BADGE_TEXT, false); + } + } + + private void drawOutline(GuiGraphics graphics, int x, int y, int width, int height, int color) { + GuiHelper.drawBorder(graphics, x, y, width, height, color); + } + + private String formatResolution(JsonObject config) { + if (!config.has("targetWidth") || !config.has("targetHeight")) return "Unknown resolution"; + String resolution = config.get("targetWidth").getAsInt() + "×" + config.get("targetHeight").getAsInt(); + return config.has("guiScale") ? resolution + " · GUI Scale: " + config.get("guiScale").getAsString() : resolution; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigStatusCard.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigStatusCard.java new file mode 100644 index 0000000..27c9119 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigStatusCard.java @@ -0,0 +1,88 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.text.ScrollingTextComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.google.gson.JsonObject; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +public class ConfigStatusCard extends AbstractComponent { + + private static final int ACCENT_BAR_WIDTH = 3; + private static final int PADDING_V = 8; + private static final int PADDING_H = 10; + private static final int ROW_GAP = 3; + + private final boolean isApplied; + + public ConfigStatusCard(int x, int y, int width, ConfigPackEntry appliedPack, boolean showMigrationHint) { + super(x, y, width, 0); + isApplied = appliedPack != null; + build(appliedPack, showMigrationHint); + } + + private void build(ConfigPackEntry appliedPack, boolean showMigrationHint) { + int fontHeight = Minecraft.getInstance().font.lineHeight; + int textX = ACCENT_BAR_WIDTH + PADDING_H; + int textWidth = getWidth() - textX - PADDING_H; + + if (isApplied) { + JsonObject config = appliedPack.config(); + String packName = config.has("name") + ? config.get("name").getAsString() + : appliedPack.zipPath().getFileName().toString(); + + ScrollingTextComponent statusLine = new ScrollingTextComponent( + textX, PADDING_V, textWidth, + Component.translatable("gui.packcore.wizard.card.config.applied", packName), + GuiColors.TEXT_PRIMARY + ); + statusLine.setDrawShadow(true); + addComponent(statusLine); + if (showMigrationHint) { + addComponent(new TextComponent( + textX, PADDING_V + fontHeight + ROW_GAP, + Component.translatable("gui.packcore.wizard.card.config.applied.hint"), + GuiColors.TEXT_SECONDARY + )); + setHeight(PADDING_V + fontHeight + ROW_GAP + fontHeight + PADDING_V); + } else { + setHeight(PADDING_V + fontHeight + PADDING_V); + } + } else { + ScrollingTextComponent errorLine = new ScrollingTextComponent( + textX, PADDING_V, textWidth, + Component.translatable("gui.packcore.wizard.card.config.error"), + GuiColors.TEXT_PRIMARY + ); + errorLine.setDrawShadow(true); + addComponent(errorLine); + addComponent(new TextComponent( + textX, PADDING_V + fontHeight + ROW_GAP, + Component.translatable("gui.packcore.wizard.card.config.error.hint"), + GuiColors.TEXT_SECONDARY + )); + setHeight(PADDING_V + fontHeight + ROW_GAP + fontHeight + PADDING_V); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + + int accentColor = isApplied ? GuiColors.SUCCESS : GuiColors.WARNING; + int borderColor = isApplied ? GuiColors.SUCCESS_BORDER : 0x55FFAA00; + + graphics.fill(x, y, x + w, y + h, GuiColors.PANEL_BACKGROUND); + GuiHelper.drawBorder(graphics, x, y, w, h, borderColor); + graphics.fill(x, y, x + ACCENT_BAR_WIDTH, y + h, accentColor); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigSwitchOverlay.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigSwitchOverlay.java new file mode 100644 index 0000000..f40f7d4 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/ConfigSwitchOverlay.java @@ -0,0 +1,211 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.util.ScreenResolution; +import eu.midnightdust.lib.config.MidnightConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class ConfigSwitchOverlay extends AbstractComponent { + + private static final int PANEL_WIDTH = 310; + private static final int PADDING = 14; + + private static final int BUTTON_WIDTH = 130; + private static final int BUTTON_HEIGHT = 20; + private static final int BUTTON_GAP = 10; + + private static final int RESOLUTION_TOLERANCE = 100; + + private static final int COLOR_DIM = 0xBB000000; + private static final int COLOR_BACKGROUND = 0xF0080F1A; + private static final int COLOR_BORDER = 0x882196F3; + private static final int COLOR_ACCENT = 0xFF2196F3; + + private static final int COLOR_TEXT = 0xFFFFFFFF; + private static final int COLOR_SUBTEXT = 0xFFAAAAAA; + + private static final int COLOR_WARNING_BACKGROUND = 0x33FFAA00; + private static final int COLOR_WARNING = 0xFFFFAA00; + + private boolean visible = false; + + private ConfigPackEntry currentPack; + private ConfigPackEntry newPack; + private ScreenResolution.ScreenSize screenSize; + + private final ButtonWidget cancelButton; + private final ButtonWidget applyButton; + + private Runnable onClose; + + // Cached layout values computed when the overlay is shown + private int panelX; + private int panelY; + private int panelHeight; + + public ConfigSwitchOverlay(int width, int height) { + super(0, 0, width, height); + + cancelButton = new CustomButtonWidget(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.literal("Cancel"), GuiHelper.BLANK_BUTTON_SPRITES, button -> setVisible(false)); + + applyButton = new CustomButtonWidget(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.literal("Yes, apply on restart"), GuiHelper.BLANK_BUTTON_SPRITES, button -> applyConfig()); + + addWidgets(List.of(cancelButton, applyButton)); + setVisible(false); + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + public void show(ConfigPackEntry current, ConfigPackEntry next) { + this.currentPack = current; + this.newPack = next; + setVisible(true); + layoutPanel(); + } + + public void setVisible(boolean visible) { + this.visible = visible; + + cancelButton.visible = applyButton.visible = visible; + cancelButton.active = applyButton.active = visible; + + if (!visible && onClose != null) { + onClose.run(); + } + } + + private void layoutPanel() { + if (screenSize == null) screenSize = ScreenResolution.detect(); + + boolean hasMismatch = hasResolutionMismatch(); + + var font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + int warningHeight = hasMismatch ? (lineHeight * 2 + 18) : 0; + + panelHeight = (PADDING * 2) + lineHeight + 10 + (lineHeight * 3 + 10) + warningHeight + BUTTON_HEIGHT + lineHeight + 25; + + panelX = getTotalX() + (getWidth() - PANEL_WIDTH) / 2; + panelY = getTotalY() + (getHeight() - panelHeight) / 2; + + int buttonsX = panelX + (PANEL_WIDTH - (BUTTON_WIDTH * 2 + BUTTON_GAP)) / 2; + int buttonsY = panelY + PADDING + lineHeight + 10 + (lineHeight * 3 + 20) + (hasMismatch ? (lineHeight * 2 + 18) : 0); + + cancelButton.uilib$updateParentPosition(buttonsX, buttonsY); + applyButton.uilib$updateParentPosition(buttonsX + BUTTON_WIDTH + BUTTON_GAP, buttonsY); + } + + private void applyConfig() { + if (newPack == null) return; + + PackCoreConfig.pendingConfigPack = newPack.zipPath().getFileName().toString(); + MidnightConfig.write(MOD_ID); + Minecraft.getInstance().stop(); + } + + private boolean hasResolutionMismatch() { + if (newPack == null || !newPack.config().has("targetWidth")) return false; + + if (screenSize == null) screenSize = ScreenResolution.detect(); + + var config = newPack.config(); + int targetWidth = config.get("targetWidth").getAsInt(); + int targetHeight = config.get("targetHeight").getAsInt(); + + return Math.abs(targetWidth - screenSize.width()) > RESOLUTION_TOLERANCE + || Math.abs(targetHeight - screenSize.height()) > RESOLUTION_TOLERANCE; + } + + @Override + public void renderBase(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + if (visible) { + super.renderBase(graphics, mouseX, mouseY, partialTick, parentWidth, parentHeight); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + if (!visible || newPack == null) return; + + var font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + + boolean hasMismatch = hasResolutionMismatch(); + int warningHeight = hasMismatch ? (lineHeight * 2 + 18) : 0; + + graphics.fill(getTotalX(), getTotalY(), getTotalX() + getWidth(), getTotalY() + getHeight(), COLOR_DIM); + graphics.fill(panelX, panelY, panelX + PANEL_WIDTH, panelY + panelHeight, COLOR_BACKGROUND); + drawBorder(graphics, panelX, panelY, panelHeight); + + int currentY = panelY + PADDING; + + graphics.drawCenteredString(font, "Switch Active Config?", panelX + PANEL_WIDTH / 2, currentY, COLOR_TEXT); + currentY += lineHeight + 10; + + int columnWidth = (PANEL_WIDTH - PADDING * 2 - 20) / 2; + + drawPackInfo(graphics, font, currentPack, panelX + PADDING, currentY, columnWidth, "Current"); + graphics.drawCenteredString(font, "→", panelX + PANEL_WIDTH / 2, currentY + lineHeight, COLOR_ACCENT); + drawPackInfo(graphics, font, newPack, panelX + PANEL_WIDTH - PADDING - columnWidth, currentY, columnWidth, "New"); + + currentY += lineHeight * 3 + 20; + + if (hasMismatch) { + var config = newPack.config(); + String packResolution = config.get("targetWidth").getAsInt() + "×" + config.get("targetHeight").getAsInt(); + String screenResolution = screenSize.width() + "×" + screenSize.height(); + + graphics.fill(panelX + PADDING, currentY, panelX + PANEL_WIDTH - PADDING, currentY + warningHeight - 6, COLOR_WARNING_BACKGROUND); + graphics.drawString(font, "⚠ Resolution Mismatch", panelX + PADDING + 8, currentY + 5, COLOR_WARNING, false); + graphics.drawString(font, "Pack: " + packResolution + " | Screen: " + screenResolution, + panelX + PADDING + 8, currentY + 5 + lineHeight + 2, COLOR_WARNING, false); + + currentY += warningHeight + 4; + } + + graphics.drawCenteredString(font, + "The game will close to apply the config.", + panelX + PANEL_WIDTH / 2, + currentY + BUTTON_HEIGHT + 8, + 0xFF666666); + } + + private void drawPackInfo(GuiGraphics graphics, net.minecraft.client.gui.Font font, ConfigPackEntry pack, int x, int y, int width, String label) { + int lineHeight = font.lineHeight; + + graphics.drawString(font, label, x, y, 0xFF555555, false); + + if (pack == null) { + graphics.drawString(font, "None", x, y + lineHeight + 2, COLOR_SUBTEXT, false); + return; + } + + var config = pack.config(); + String name = config.has("name") ? config.get("name").getAsString() : pack.zipPath().getFileName().toString(); + String author = config.has("author") ? config.get("author").getAsString() : "Unknown"; + String version = config.has("version") ? config.get("version").getAsString() : "?"; + + graphics.drawString(font, font.plainSubstrByWidth(name, width), x, y + lineHeight + 2, COLOR_TEXT, false); + graphics.drawString(font, font.plainSubstrByWidth(author + " v" + version, width), + x, y + lineHeight * 2 + 3, COLOR_SUBTEXT, false); + } + + private void drawBorder(GuiGraphics graphics, int x, int y, int height) { + GuiHelper.drawBorder(graphics, x, y, PANEL_WIDTH, height, COLOR_BORDER); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeBuilder.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeBuilder.java new file mode 100644 index 0000000..bb16872 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeBuilder.java @@ -0,0 +1,153 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import org.jspecify.annotations.NonNull; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipFile; + +public final class FileTreeBuilder { + + private FileTreeBuilder() {} + + public static FileTreeNode fromZip(Path zipPath) throws IOException { + FileTreeNode root = new FileTreeNode("root", "", true); + root.setExpanded(true); + + Map> childIndex = new IdentityHashMap<>(); + childIndex.put(root, new HashMap<>()); + + try (ZipFile zip = new ZipFile(zipPath.toFile())) { + zip.entries().asIterator().forEachRemaining(entry -> { + if (!entry.getName().equals("pack.json")) { + insertPath(root, entry.getName(), entry.isDirectory(), childIndex); + } + }); + } + + sortTree(root); + return root; + } + + public static FileTreeNode fromDirectory(Path dir, Set hidden) throws IOException { + String dirName = dir.getFileName() != null ? dir.getFileName().toString() : "root"; + FileTreeNode root = new FileTreeNode(dirName, "", true); + root.setExpanded(true); + + if (!Files.exists(dir)) { + return root; + } + + Map> childIndex = new IdentityHashMap<>(); + childIndex.put(root, new HashMap<>()); + + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + + @Override + public @NonNull FileVisitResult preVisitDirectory(@NonNull Path path, @NonNull BasicFileAttributes attrs) { + if (path.equals(dir)) { + return FileVisitResult.CONTINUE; + } + + String rel = dir.relativize(path).toString().replace('\\', '/'); + String topLevel = rel.split("/")[0]; + + if (topLevel.startsWith(".")) { + return FileVisitResult.SKIP_SUBTREE; + } + if (hidden != null && hidden.stream().anyMatch(h -> rel.equals(h) || rel.startsWith(h + "/"))) { + return FileVisitResult.SKIP_SUBTREE; + } + + insertPath(root, rel, true, childIndex); + return FileVisitResult.CONTINUE; + } + + @Override + public @NonNull FileVisitResult visitFile(@NonNull Path path, @NonNull BasicFileAttributes attrs) { + String rel = dir.relativize(path).toString().replace('\\', '/'); + String topLevel = rel.split("/")[0]; + + if (topLevel.startsWith(".")) { + return FileVisitResult.CONTINUE; + } + if (hidden != null && hidden.stream().anyMatch(h -> rel.equals(h) || rel.startsWith(h + "/"))) { + return FileVisitResult.CONTINUE; + } + + insertPath(root, rel, false, childIndex); + return FileVisitResult.CONTINUE; + } + + @Override + public @NonNull FileVisitResult visitFileFailed(@NonNull Path path, @NonNull IOException e) { + return FileVisitResult.CONTINUE; + } + }); + + sortTree(root); + return root; + } + + private static void insertPath( + FileTreeNode root, + String path, + boolean isDirectory, + Map> childIndex + ) { + String[] parts = path.split("/"); + FileTreeNode current = root; + StringBuilder accumulatedPath = new StringBuilder(); + + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + if (part.isEmpty()) { + continue; + } + + if (!accumulatedPath.isEmpty()) { + accumulatedPath.append('/'); + } + accumulatedPath.append(part); + + boolean isLastPart = i == parts.length - 1; + boolean nodeIsDirectory = !isLastPart || isDirectory; + String fullPath = accumulatedPath.toString(); + + Map indexForCurrent = + childIndex.computeIfAbsent(current, ignored -> new HashMap<>()); + FileTreeNode existing = indexForCurrent.get(part); + + if (existing == null) { + FileTreeNode created = new FileTreeNode(part, fullPath, nodeIsDirectory); + current.addChild(created); + indexForCurrent.put(part, created); + childIndex.put(created, new HashMap<>()); + current = created; + } else { + current = existing; + } + } + } + + private static void sortTree(FileTreeNode node) { + node.children().sort((a, b) -> { + if (a.isDirectory() != b.isDirectory()) { + return a.isDirectory() ? -1 : 1; + } + return a.name().compareToIgnoreCase(b.name()); + }); + + for (FileTreeNode child : node.children()) { + sortTree(child); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeComponent.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeComponent.java new file mode 100644 index 0000000..daeb8a3 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeComponent.java @@ -0,0 +1,201 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.api.widget.IWidget; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractContainerWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Scrollable file tree with expandable directories and selectable files. + */ +public class FileTreeComponent extends EmptyComponent { + + private static final int ROW_HEIGHT = 16; + private static final int INDENT_WIDTH = 12; + private static final int SCROLL_BAR_ROOM = 8; + + private static final int COLOR_HOVER = 0x22FFFFFF; + private static final int COLOR_TEXT_FILE = 0xFFAAAAAA; + private static final int COLOR_TEXT_FILE_SELECTED = 0xFFFFFFFF; + private static final int COLOR_TEXT_DIR = 0xFFCCCCCC; + private static final int COLOR_CHECKBOX_BORDER = 0xFF555555; + private static final int COLOR_CHECKBOX_FILL = 0xFF2196F3; + + private final FileTreeNode root; + private ScrollContainerWidget scrollContainer; + private Runnable onSelectionChanged; + + public FileTreeComponent(int x, int y, int width, int height, FileTreeNode root) { + super(x, y, width, height); + this.root = root; + build(); + } + + private void build() { + double savedScroll = scrollContainer != null ? scrollContainer.scrollAmount() : 0; + clearComponents(); + + List visibleNodes = new ArrayList<>(); + collectVisible(root, 0, visibleNodes); + + int rowWidth = getWidth() - SCROLL_BAR_ROOM; + EmptyComponent rows = new EmptyComponent(0, 0, rowWidth, visibleNodes.size() * ROW_HEIGHT); + + int nodeY = 0; + for (VisibleNode visibleNode : visibleNodes) { + rows.addWidget(new FileTreeRowWidget(0, nodeY, rowWidth, ROW_HEIGHT, visibleNode.node, visibleNode.depth)); + nodeY += ROW_HEIGHT; + } + + scrollContainer = new ScrollContainerWidget(getWidth(), getHeight()); + scrollContainer.addComponent(rows); + scrollContainer.setScrollAmount(savedScroll); + + EmptyComponent wrapper = new EmptyComponent(0, 0, getWidth(), getHeight()); + wrapper.addWidget(scrollContainer); + addComponent(wrapper); + updateParentPosition(getParentX(), getParentY(), getWidth(), getHeight()); + } + + private void collectVisible(FileTreeNode node, int depth, List result) { + for (FileTreeNode child : node.children()) { + result.add(new VisibleNode(child, depth)); + if (child.isDirectory() && child.isExpanded()) { + collectVisible(child, depth + 1, result); + } + } + } + + public List getSelectedPaths() { + return root.collectSelectedPaths(); + } + + public void setOnSelectionChanged(Runnable callback) { + this.onSelectionChanged = callback; + } + + private record VisibleNode(FileTreeNode node, int depth) {} + + private class FileTreeRowWidget extends AbstractContainerWidget implements IWidget { + + private final FileTreeNode node; + private final int depth; + + FileTreeRowWidget(int x, int y, int width, int height, FileTreeNode node, int depth) { + super(x, y, width, height, Component.empty()); + this.node = node; + this.depth = depth; + } + + @Override + protected void renderWidget(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + var font = Minecraft.getInstance().font; + int x = getX(); + int y = getY(); + int width = getWidth(); + int height = getHeight(); + + int indent = INDENT_WIDTH * depth + 4; + int checkboxSize = 8; + int checkboxX = x + indent; + int checkboxY = y + (height - checkboxSize) / 2; + int textY = y + (height - font.lineHeight) / 2; + + if (isHovered()) { + graphics.fill(x, y, x + width, y + height, COLOR_HOVER); + } + + GuiHelper.drawBorder(graphics, checkboxX, checkboxY, checkboxSize, checkboxSize, COLOR_CHECKBOX_BORDER); + + if (node.isDirectory()) { + boolean allSelected = node.isAllSelected(); + boolean anySelected = node.isAnySelected(); + + if (allSelected) { + graphics.fill(checkboxX + 1, checkboxY + 1, checkboxX + checkboxSize - 1, checkboxY + checkboxSize - 1, COLOR_CHECKBOX_FILL); + } else if (anySelected) { + graphics.fill(checkboxX + 2, checkboxY + 2, checkboxX + checkboxSize - 2, checkboxY + checkboxSize - 2, COLOR_CHECKBOX_FILL); + } + + String prefix = node.isExpanded() ? "v " : "> "; + graphics.drawString(font, prefix + node.name(), checkboxX + checkboxSize + 3, textY, COLOR_TEXT_DIR, false); + } else { + if (node.isSelected()) { + graphics.fill(checkboxX + 1, checkboxY + 1, checkboxX + checkboxSize - 1, checkboxY + checkboxSize - 1, COLOR_CHECKBOX_FILL); + } + + int textColor = node.isSelected() ? COLOR_TEXT_FILE_SELECTED : COLOR_TEXT_FILE; + graphics.drawString(font, node.name(), checkboxX + checkboxSize + 3, textY, textColor, false); + } + } + + @Override + public boolean mouseClicked(@NotNull MouseButtonEvent event, boolean bl) { + if (event.button() != 0 || !isMouseOver(event.x(), event.y())) { + return false; + } + + int indent = INDENT_WIDTH * depth + 4; + int checkboxX = getX() + indent; + int checkboxSize = 8; + + if (node.isDirectory()) { + if (event.x() >= checkboxX && event.x() < checkboxX + checkboxSize) { + node.setSelectedRecursive(!node.isAllSelected()); + if (onSelectionChanged != null) onSelectionChanged.run(); + } else { + node.setExpanded(!node.isExpanded()); + FileTreeComponent.this.build(); + } + } else { + node.setSelected(!node.isSelected()); + if (onSelectionChanged != null) onSelectionChanged.run(); + } + + return true; + } + + @Override + protected int contentHeight() { + return 0; + } + + @Override + protected double scrollRate() { + return 0; + } + + @Override + protected void updateWidgetNarration(@NotNull NarrationElementOutput output) {} + + @Override + public @NotNull ScreenRectangle getBorderForArrowNavigation(@NotNull ScreenDirection direction) { + return getRectangle(); + } + + @Override + public @NotNull List children() { + return List.of(); + } + + @Override + public @NotNull Collection getNarratables() { + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeNode.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeNode.java new file mode 100644 index 0000000..368ff30 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/FileTreeNode.java @@ -0,0 +1,158 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import java.util.ArrayList; +import java.util.List; + +public final class FileTreeNode { + + private enum SelectionState { + NONE, + PARTIAL, + ALL + } + + private final String name; + private final String path; + private final boolean directory; + private final List children = new ArrayList<>(); + + private FileTreeNode parent; + private boolean expanded = false; + private boolean selected = false; + private SelectionState selectionState = SelectionState.NONE; + + public FileTreeNode(String name, String path, boolean directory) { + this.name = name; + this.path = path; + this.directory = directory; + } + + public void addChild(FileTreeNode child) { + child.parent = this; + children.add(child); + } + + public String name() { + return name; + } + + public String path() { + return path; + } + + public boolean isDirectory() { + return directory; + } + + public List children() { + return children; + } + + public boolean isExpanded() { + return expanded; + } + + public void setExpanded(boolean expanded) { + this.expanded = expanded; + } + + public boolean isSelected() { + return selected; + } + + public void setSelected(boolean selected) { + if (directory) { + setSelectedRecursive(selected); + return; + } + + if (this.selected == selected) { + return; + } + + this.selected = selected; + recalculateSelectionStateUpward(); + } + + public List collectSelectedPaths() { + List result = new ArrayList<>(); + collectInto(result); + return result; + } + + private void collectInto(List result) { + if (!directory && selected) { + result.add(path); + } + for (FileTreeNode child : children) { + child.collectInto(result); + } + } + + public void setSelectedRecursive(boolean selected) { + if (directory) { + for (FileTreeNode child : children) { + child.setSelectedRecursive(selected); + } + } else { + this.selected = selected; + } + + recalculateSelectionState(); + if (parent != null) { + parent.recalculateSelectionStateUpward(); + } + } + + public boolean isAllSelected() { + return selectionState == SelectionState.ALL; + } + + public boolean isAnySelected() { + return selectionState != SelectionState.NONE; + } + + private void recalculateSelectionStateUpward() { + FileTreeNode current = this; + while (current != null) { + SelectionState before = current.selectionState; + current.recalculateSelectionState(); + if (before == current.selectionState) { + break; + } + current = current.parent; + } + } + + private void recalculateSelectionState() { + if (!directory) { + selectionState = selected ? SelectionState.ALL : SelectionState.NONE; + return; + } + + if (children.isEmpty()) { + selectionState = SelectionState.NONE; + return; + } + + boolean anySelected = false; + boolean allSelected = true; + + for (FileTreeNode child : children) { + SelectionState state = child.selectionState; + if (state != SelectionState.NONE) { + anySelected = true; + } + if (state != SelectionState.ALL) { + allSelected = false; + } + + if (anySelected && !allSelected) { + selectionState = SelectionState.PARTIAL; + return; + } + } + + selectionState = allSelected ? SelectionState.ALL : SelectionState.NONE; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/MarkdownComponent.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/MarkdownComponent.java new file mode 100644 index 0000000..0558b57 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/MarkdownComponent.java @@ -0,0 +1,623 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.api.component.IComponent; +import com.daqem.uilib.api.widget.IWidget; +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.mojang.blaze3d.platform.cursor.CursorTypes; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +/** + * Lightweight Markdown renderer built on UILib's component system. + *

+ * Supported syntax: + * Headings # through ###### + * Bullet list - or * + * Bold **text** + * Italic *text* + * Link [label](url) + * Image ![alt](namespace:path/to/texture.png WxH) + * Blockquote > text + * Horizontal --- + * Blank line (adds vertical spacing) + */ +public class MarkdownComponent extends AbstractComponent { + + // Spacing constants + private static final int SPACING_HEADING = 4; + private static final int SPACING_PARAGRAPH = 2; + private static final int SPACING_EMPTY_LINE = 5; + private static final int SPACING_IMAGE = 6; + private static final int SPACING_RULE = 6; + private static final int SPACING_BLOCKQUOTE = 3; + + // Layout constants + private static final int BULLET_INDENT = 8; + private static final int BLOCKQUOTE_INDENT = 10; + private static final int BLOCKQUOTE_BAR_WIDTH = 2; + private static final int BLOCKQUOTE_BAR_GAP = 4; + private static final int RULE_HEIGHT = 1; + private static final int DEFAULT_IMAGE_HEIGHT = 80; + + // Colors + private static final int COLOR_RULE = 0xFF444444; + private static final int COLOR_LINK = 0xFF55AAFF; + private static final int COLOR_BLOCKQUOTE = 0xFFAAAAAA; + private static final int COLOR_HEADING_1 = 0xFFFFFF55; + private static final int COLOR_HEADING_2 = 0xFFFFAA55; + private static final int COLOR_HEADING_3 = 0xFFFF5555; + + private record ImageInfo(Identifier location, int renderWidth, int renderHeight, int texWidth, int texHeight) {} + private record ParsedLink(int startCharIndex, int endCharIndex, String url) {} + + private final String markdown; + private final int maxWidth; + private final int defaultColor; + + private final List blockquoteBars = new ArrayList<>(); + private final List horizontalRules = new ArrayList<>(); + private final List images = new ArrayList<>(); + private final List imagePositions = new ArrayList<>(); + + public MarkdownComponent(int x, int y, int maxWidth, String markdown) { + this(x, y, maxWidth, markdown, 0xFFFFFFFF); + } + + public MarkdownComponent(int x, int y, int maxWidth, String markdown, int defaultColor) { + super(x, y, maxWidth, 0); + this.markdown = markdown != null ? markdown : ""; + this.maxWidth = maxWidth; + this.defaultColor = defaultColor; + rebuild(); + } + + public void rebuild() { + clearComponents(); + clearOnlyWidgets(); + blockquoteBars.clear(); + horizontalRules.clear(); + images.clear(); + imagePositions.clear(); + + int currentY = 0; + + for (String rawLine : markdown.split("\n", -1)) { + String line = rawLine.trim(); + + if (line.isEmpty()) { + currentY += SPACING_EMPTY_LINE; + continue; + } + + if (isHorizontalRule(line)) { + currentY += buildHorizontalRule(currentY); + continue; + } + + int headingLevel = getHeadingLevel(line); + if (headingLevel > 0) { + currentY += buildHeading(line, headingLevel, currentY); + continue; + } + + if (isBlockquote(line)) { + currentY += buildBlockquote(line, currentY); + continue; + } + + if (isBullet(line)) { + currentY += buildBullet(line, currentY); + continue; + } + + ImageInfo image = tryParseStandaloneImage(line); + if (image != null) { + currentY += buildImage(image, currentY); + continue; + } + + currentY += buildParagraph(line, currentY); + } + + setHeight(currentY); + } + + private int buildHeading(String line, int level, int y) { + String content = line.substring(level + 1).trim(); + MutableComponent text = parseInline(content); + text.withStyle(s -> s.withBold(true).withColor(getHeadingColor(level))); + + float scale = getHeadingScale(level); + int unscaledWidth = (int) (maxWidth / scale); + ScaledTextComponent comp = new ScaledTextComponent(0, y, unscaledWidth, text, scale); + addComponent(comp); + + return comp.getScaledHeight() + SPACING_HEADING; + } + + private int buildBullet(String line, int y) { + String content = line.substring(2).trim(); + List links = new ArrayList<>(); + + MutableComponent bulletPrefix = Component.literal("• ").withStyle(Style.EMPTY.withColor(defaultColor)); + MutableComponent body = parseInlineWithLinks(content, links, bulletPrefix, defaultColor); + MutableComponent combined = bulletPrefix.copy().append(body); + + int textWidth = maxWidth - BULLET_INDENT; + MultiLineTextComponent comp = new MultiLineTextComponent(BULLET_INDENT, y, textWidth, combined, defaultColor); + addComponent(comp); + + registerLinkWidgets(links, combined, BULLET_INDENT, y, textWidth); + return comp.getHeight() + SPACING_PARAGRAPH; + } + + private int buildParagraph(String line, int y) { + List links = new ArrayList<>(); + MutableComponent text = parseInlineWithLinks(line, links, Component.empty(), defaultColor); + + MultiLineTextComponent comp = new MultiLineTextComponent(0, y, maxWidth, text, defaultColor); + addComponent(comp); + + registerLinkWidgets(links, text, 0, y, maxWidth); + return comp.getHeight() + SPACING_PARAGRAPH; + } + + private int buildBlockquote(String line, int y) { + String content = line.substring(1).trim(); + int textX = BLOCKQUOTE_BAR_WIDTH + BLOCKQUOTE_BAR_GAP + BLOCKQUOTE_INDENT; + int textWidth = maxWidth - textX; + + List links = new ArrayList<>(); + Style italicBaseStyle = Style.EMPTY.withColor(defaultColor).withItalic(true); + MutableComponent text = parseInlineWithLinks(content, links, Component.empty(), defaultColor, italicBaseStyle); + text.withStyle(s -> s.withColor(COLOR_BLOCKQUOTE).withItalic(true)); + + MultiLineTextComponent comp = new MultiLineTextComponent(textX, y, textWidth, text, COLOR_BLOCKQUOTE); + addComponent(comp); + blockquoteBars.add(new int[]{BLOCKQUOTE_INDENT, y, comp.getHeight()}); + + registerLinkWidgets(links, text, textX, y, textWidth); + return comp.getHeight() + SPACING_BLOCKQUOTE; + } + + private int buildHorizontalRule(int y) { + horizontalRules.add(new int[]{y}); + return RULE_HEIGHT + SPACING_RULE; + } + + private int buildImage(ImageInfo info, int y) { + images.add(info); + imagePositions.add(new int[]{0, y, info.renderWidth(), info.renderHeight()}); + return info.renderHeight() + SPACING_IMAGE; + } + + // Link hit-region registration + private void registerLinkWidgets(List links, MutableComponent fullText, int columnX, int blockY, int columnWidth) { + if (links.isEmpty()) return; + + Font font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + String flatText = fullText.getString(); + + List wrappedLines = font.getSplitter().splitLines(fullText, columnWidth, Style.EMPTY); + if (wrappedLines.isEmpty()) return; + + // Map each wrapped line back to a char range in the flat string by searching forward. + int[] lineStartCharIndex = new int[wrappedLines.size()]; + int[] lineEndCharIndex = new int[wrappedLines.size()]; + + int searchFrom = 0; + for (int lineIndex = 0; lineIndex < wrappedLines.size(); lineIndex++) { + String lineText = wrappedLines.get(lineIndex).getString(); + int found = flatText.indexOf(lineText, searchFrom); + if (found == -1) found = searchFrom; // shouldn't happen, but safe fallback + + lineStartCharIndex[lineIndex] = found; + lineEndCharIndex[lineIndex] = found + lineText.length(); + searchFrom = found + lineText.length(); + } + + // The splitter may drop trailing chars on the last line + lineEndCharIndex[wrappedLines.size() - 1] = flatText.length(); + + final int leftTolerancePx = 1; + + for (ParsedLink link : links) { + int linkStart = link.startCharIndex(); + int linkEnd = link.endCharIndex(); + if (linkEnd <= linkStart) continue; + + for (int lineIndex = 0; lineIndex < wrappedLines.size(); lineIndex++) { + int lineStart = lineStartCharIndex[lineIndex]; + int lineEnd = lineEndCharIndex[lineIndex]; + + int overlapStart = Math.max(linkStart, lineStart); + int overlapEnd = Math.min(linkEnd, lineEnd); + if (overlapEnd <= overlapStart) continue; + + String lineText = wrappedLines.get(lineIndex).getString(); + int localStart = overlapStart - lineStart; + int localEnd = Math.min(overlapEnd - lineStart, lineText.length()); + if (localEnd <= localStart) continue; + + int startPx = font.width(lineText.substring(0, localStart)); + int endPx = font.width(lineText.substring(0, localEnd)); + + int hitX = columnX + Math.max(0, startPx - leftTolerancePx); + int hitWidth = (endPx - startPx) + leftTolerancePx; + if (hitWidth <= 0) continue; + + int hitY = blockY + lineIndex * lineHeight; + addWidget(new LinkWidget(this, hitX, hitY, hitWidth, lineHeight, link.url())); + } + } + } + + /** + * Wraps a MultiLineTextComponent and applies a pose-matrix scale when rendering, + * so heading text appears visually larger. The logical height accounts for the scale, so the layout is correct. + */ + private static class ScaledTextComponent extends AbstractComponent { + + private final MultiLineTextComponent inner; + private final float scale; + private final int scaledHeight; + + ScaledTextComponent(int x, int y, int unscaledWidth, MutableComponent text, float scale) { + super(x, y, (int) (unscaledWidth * scale), 0); + this.scale = scale; + this.inner = new MultiLineTextComponent(0, 0, unscaledWidth, text); + this.scaledHeight = (int) (inner.getHeight() * scale); + setHeight(scaledHeight); + } + + int getScaledHeight() { + return scaledHeight; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, + float partialTick, int parentWidth, int parentHeight) { + int drawX = getTotalX(); + int drawY = getTotalY(); + + graphics.pose().pushMatrix(); + graphics.pose().translate((float) drawX, (float) drawY); + graphics.pose().scale(scale, scale); + + int scaledParentWidth = (int) (parentWidth / scale); + int scaledParentHeight = (int) (parentHeight / scale); + + inner.updateParentPosition(0, 0, scaledParentWidth, scaledParentHeight); + inner.render(graphics, (int) (mouseX / scale), (int) (mouseY / scale), + partialTick, scaledParentWidth, scaledParentHeight); + + graphics.pose().popMatrix(); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, + float partialTick, int parentWidth, int parentHeight) { + int baseX = getTotalX(); + int baseY = getTotalY(); + + for (int[] bar : blockquoteBars) { + graphics.fill( + baseX + bar[0], baseY + bar[1], + baseX + bar[0] + BLOCKQUOTE_BAR_WIDTH, baseY + bar[1] + bar[2], + COLOR_RULE + ); + } + + for (int[] rule : horizontalRules) { + graphics.fill(baseX, baseY + rule[0], baseX + maxWidth, baseY + rule[0] + RULE_HEIGHT, COLOR_RULE); + } + + for (int i = 0; i < images.size(); i++) { + ImageInfo info = images.get(i); + int[] pos = imagePositions.get(i); + graphics.blit( + RenderPipelines.GUI_TEXTURED, + info.location(), + baseX + pos[0], baseY + pos[1], + 0.0f, 0.0f, + pos[2], pos[3], + info.texWidth(), info.texHeight(), + info.texWidth(), info.texHeight() + ); + } + + for (IComponent child : getComponents()) { + child.render(graphics, mouseX, mouseY, partialTick, parentWidth, parentHeight); + } + } + + // LinkWidget + private static class LinkWidget extends AbstractWidget implements IWidget { + + private final MarkdownComponent parent; + private final int localX; + private final int localY; + private final int localWidth; + private final int localHeight; + private final String url; + + LinkWidget(MarkdownComponent parent, int localX, int localY, int width, int height, String url) { + super(0, 0, width, height, Component.empty()); + this.parent = parent; + this.localX = localX; + this.localY = localY; + this.localWidth = width; + this.localHeight = height; + this.url = url; + } + + @Override public int getX() { return parent.getTotalX() + localX; } + @Override public int getY() { return parent.getTotalY() + localY; } + @Override public int getWidth() { return localWidth; } + @Override public int getHeight() { return localHeight; } + + @Override + public @NotNull ScreenRectangle getRectangle() { + return new ScreenRectangle(getX(), getY(), localWidth, localHeight); + } + + @Override + public boolean mouseClicked(@NotNull MouseButtonEvent event, boolean bl) { + if (event.button() != 0 || !isMouseOver(event.x(), event.y())) return false; + openUrl(url); + return true; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + int absX = getX(); + int absY = getY(); + return mouseX >= absX && mouseX < absX + localWidth + && mouseY >= absY && mouseY < absY + localHeight; + } + + @Override + protected void renderWidget(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + if (isMouseOver(mouseX, mouseY)) { + guiGraphics.requestCursor(CursorTypes.POINTING_HAND); + } + } + + @Override + protected void updateWidgetNarration(@NotNull NarrationElementOutput output) {} + + private static void openUrl(String url) { + try { + Util.getPlatform().openUri(new URI(url)); + } catch (Exception ignored) {} + } + } + + // Inline parser + private MutableComponent parseInlineWithLinks(String text, List links, MutableComponent prefix, int color) { + return parseInlineWithLinks(text, links, prefix, color, Style.EMPTY.withColor(color)); + } + + private MutableComponent parseInlineWithLinks( + String text, + List links, + MutableComponent prefix, + int color, + Style baseStyle + ) { + MutableComponent root = Component.empty(); + StringBuilder buffer = new StringBuilder(); + boolean bold = false; + boolean italic = false; + + int plainCharIndex = prefix.getString().length(); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + + if (c == '!' && i + 1 < text.length() && text.charAt(i + 1) == '[') { + int closeBracket = text.indexOf(']', i + 2); + if (closeBracket != -1 && closeBracket + 1 < text.length() && text.charAt(closeBracket + 1) == '(') { + int closeParen = text.indexOf(')', closeBracket + 2); + if (closeParen != -1) { + MutableComponent flushed = flushBuffer(buffer, bold, italic, baseStyle, color); + if (flushed != null) { + root.append(flushed); + plainCharIndex += flushed.getString().length(); + } + + String alt = "[" + text.substring(i + 2, closeBracket) + "]"; + MutableComponent altText = Component.literal(alt) + .withStyle(Style.EMPTY.withItalic(true).withColor(COLOR_BLOCKQUOTE)); + root.append(altText); + plainCharIndex += alt.length(); + + i = closeParen; + continue; + } + } + } + + if (c == '[') { + int closeBracket = text.indexOf(']', i + 1); + if (closeBracket != -1 && closeBracket + 1 < text.length() && text.charAt(closeBracket + 1) == '(') { + int closeParen = text.indexOf(')', closeBracket + 2); + if (closeParen != -1) { + MutableComponent flushed = flushBuffer(buffer, bold, italic, baseStyle, color); + if (flushed != null) { + root.append(flushed); + plainCharIndex += flushed.getString().length(); + } + + String label = text.substring(i + 1, closeBracket); + String url = text.substring(closeBracket + 2, closeParen); + + int startIndex = plainCharIndex; + int endIndex = startIndex + label.length(); + + MutableComponent linkSpan = buildLinkSpan(label, bold, italic); + root.append(linkSpan); + links.add(new ParsedLink(startIndex, endIndex, url)); + + plainCharIndex = endIndex; + i = closeParen; + continue; + } + } + } + + if (c == '*' && i + 1 < text.length() && text.charAt(i + 1) == '*') { + MutableComponent flushed = flushBuffer(buffer, bold, italic, baseStyle, color); + if (flushed != null) { + root.append(flushed); + plainCharIndex += flushed.getString().length(); + } + bold = !bold; + i++; + continue; + } + + if (c == '*') { + MutableComponent flushed = flushBuffer(buffer, bold, italic, baseStyle, color); + if (flushed != null) { + root.append(flushed); + plainCharIndex += flushed.getString().length(); + } + italic = !italic; + continue; + } + + buffer.append(c); + } + + MutableComponent flushed = flushBuffer(buffer, bold, italic, baseStyle, color); + if (flushed != null) { + root.append(flushed); + } + + return root; + } + + private MutableComponent parseInline(String text) { + return parseInlineWithLinks(text, new ArrayList<>(), Component.empty(), defaultColor); + } + + private MutableComponent buildLinkSpan(String label, boolean bold, boolean italic) { + return Component.literal(label).setStyle(Style.EMPTY + .withColor(COLOR_LINK) + .withUnderlined(true) + .withBold(bold) + .withItalic(italic)); + } + + private MutableComponent flushBuffer(StringBuilder buffer, boolean bold, boolean italic, + Style baseStyle, int color) { + if (buffer.isEmpty()) return null; + Style style = baseStyle.withColor(color); + if (bold) style = style.withBold(true); + if (italic) style = style.withItalic(true); + MutableComponent comp = Component.literal(buffer.toString()).setStyle(style); + buffer.setLength(0); + return comp; + } + + // Helpers + private int getHeadingLevel(String line) { + int level = 0; + while (level < 6 && level < line.length() && line.charAt(level) == '#') level++; + return (level > 0 && level < line.length() && line.charAt(level) == ' ') ? level : 0; + } + + private boolean isBullet(String line) { + return line.startsWith("- ") || line.startsWith("* "); + } + + private boolean isBlockquote(String line) { + return line.startsWith("> "); + } + + private boolean isHorizontalRule(String line) { + return line.equals("---") || line.equals("***") || line.equals("___"); + } + + private ImageInfo tryParseStandaloneImage(String line) { + if (!line.startsWith("![")) return null; + int closeBracket = line.indexOf(']', 2); + if (closeBracket == -1 || closeBracket + 1 >= line.length()) return null; + if (line.charAt(closeBracket + 1) != '(') return null; + int closeParen = line.indexOf(')', closeBracket + 2); + if (closeParen != line.length() - 1) return null; + + String inner = line.substring(closeBracket + 2, closeParen).trim(); + String resourcePath = inner; + int renderWidth = maxWidth; + int renderHeight = DEFAULT_IMAGE_HEIGHT; + int texWidth = maxWidth; + int texHeight = DEFAULT_IMAGE_HEIGHT; + + int dimSepIdx = inner.lastIndexOf(' '); + if (dimSepIdx != -1) { + String maybeDims = inner.substring(dimSepIdx + 1); + int xSepIdx = maybeDims.indexOf('x'); + if (xSepIdx > 0) { + try { + int w = Integer.parseInt(maybeDims.substring(0, xSepIdx)); + int h = Integer.parseInt(maybeDims.substring(xSepIdx + 1)); + texWidth = w; + texHeight = h; + renderWidth = w; + renderHeight = h; + if (renderWidth > maxWidth) { + renderHeight = renderHeight * maxWidth / renderWidth; + renderWidth = maxWidth; + } + resourcePath = inner.substring(0, dimSepIdx).trim(); + } catch (NumberFormatException ignored) {} + } + } + + try { + return new ImageInfo(Identifier.parse(resourcePath), renderWidth, renderHeight, texWidth, texHeight); // ← add texWidth, texHeight + } catch (Exception e) { + return null; + } + } + + private float getHeadingScale(int level) { + return switch (level) { + case 1 -> 1.75f; + case 2 -> 1.35f; + case 3 -> 1.15f; + default -> 1.0f; + }; + } + + private int getHeadingColor(int level) { + return switch (level) { + case 1 -> COLOR_HEADING_1; + case 2 -> COLOR_HEADING_2; + case 3 -> COLOR_HEADING_3; + default -> defaultColor; + }; + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/MultiSelectList.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/MultiSelectList.java new file mode 100644 index 0000000..e4a1369 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/MultiSelectList.java @@ -0,0 +1,274 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.api.component.IComponent; +import com.daqem.uilib.api.widget.IWidget; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.color.ColorComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractContainerWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A reusable scrollable vertical list that supports selecting multiple rows simultaneously. + * Each row shows a name and an optional description. + * + *

Clicking a selected row deselects it; clicking an unselected row selects it. + * The scroll position is preserved across rebuilds. + * + * @param The option type (record, class, enum, etc.) + */ +public class MultiSelectList extends EmptyComponent { + + private static final int ROW_GAP = 4; + private static final int ROW_PADDING_X = 10; + private static final int ROW_PADDING_Y = 8; + private static final int SCROLL_BAR_WIDTH = 8; + private static final int INDICATOR_WIDTH = 3; + private static final int INDICATOR_GAP = 8; + private static final int CHECKMARK_RIGHT_MARGIN = 8; + private static final int CHECKMARK_SIZE = 9; + + private final List options; + private final RowDescriptor descriptor; + private final Consumer onSelect; + private final Consumer onDeselect; + private final Set selectedIds; + private ScrollContainerWidget scrollContainer; + + public MultiSelectList( + int x, int y, + int width, int height, + List options, + RowDescriptor descriptor, + Set selectedIds, + Consumer onSelect, + Consumer onDeselect + ) { + super(x, y, width, height); + this.options = options; + this.descriptor = descriptor; + this.selectedIds = new HashSet<>(selectedIds); + this.onSelect = onSelect; + this.onDeselect = onDeselect; + + buildList(); + } + + private void buildList() { + double savedScroll = scrollContainer != null ? scrollContainer.scrollAmount() : 0; + this.clearComponents(); + + int listWidth = getWidth(); + int listHeight = getHeight(); + int rowWidth = listWidth - SCROLL_BAR_WIDTH; + int textWidth = rowWidth - ROW_PADDING_X * 2 - INDICATOR_WIDTH - INDICATOR_GAP - CHECKMARK_SIZE - CHECKMARK_RIGHT_MARGIN; + int lineHeight = Minecraft.getInstance().font.lineHeight; + + scrollContainer = new ScrollContainerWidget(listWidth, listHeight); + EmptyComponent rowContainer = new EmptyComponent(0, 0, rowWidth, 0); + + int currentY = 0; + + for (T option : options) { + boolean isSelected = selectedIds.contains(descriptor.id(option)); + + int descHeight = 0; + Component desc = descriptor.description(option); + if (desc != null) { + MultiLineTextComponent probe = new MultiLineTextComponent(0, 0, textWidth, desc, GuiColors.DESCRIPTION); + descHeight = probe.getHeight(); + } + + int rowHeight = ROW_PADDING_Y + lineHeight + (descHeight > 0 ? 2 + descHeight : 0) + ROW_PADDING_Y; + + SelectRow row = new SelectRow<>( + 0, currentY, + rowWidth, rowHeight, + option, + descriptor, + isSelected, + clicked -> { + String clickedId = descriptor.id(clicked); + if (selectedIds.contains(clickedId)) { + selectedIds.remove(clickedId); + onDeselect.accept(clicked); + } else { + selectedIds.add(clickedId); + onSelect.accept(clicked); + } + buildList(); + } + ); + rowContainer.addWidget(row); + currentY += rowHeight + ROW_GAP; + } + + rowContainer.setHeight(currentY); + scrollContainer.addComponent(rowContainer); + scrollContainer.setScrollAmount(savedScroll); + + EmptyComponent scrollWrapper = new EmptyComponent(0, 0, listWidth, listHeight); + scrollWrapper.addWidget(scrollContainer); + this.addComponent(scrollWrapper); + + this.updateParentPosition(getParentX(), getParentY(), listWidth, listHeight); + } + + /** + * Tells the list how to read each field from your option type. + * Build one with {@link #of} using method references. + * Description may return null for name-only rows. + */ + public interface RowDescriptor { + String id(T option); + Component name(T option); + Component description(T option); + + static RowDescriptor of( + Function id, + Function name, + Function description + ) { + return new RowDescriptor<>() { + @Override public String id(T o) { return id.apply(o); } + @Override public Component name(T o) { return name.apply(o); } + @Override public Component description(T o) { return description.apply(o); } + }; + } + } + + private static class SelectRow extends AbstractContainerWidget implements IWidget { + + private final T option; + private final boolean isSelected; + private final Consumer onClick; + private final List childComponents = new ArrayList<>(); + + SelectRow( + int x, int y, + int width, int height, + T option, + RowDescriptor descriptor, + boolean isSelected, + Consumer onClick + ) { + super(x, y, width, height, Component.empty()); + this.option = option; + this.isSelected = isSelected; + this.onClick = onClick; + + int lineHeight = Minecraft.getInstance().font.lineHeight; + int textX = ROW_PADDING_X + INDICATOR_WIDTH + INDICATOR_GAP; + int textWidth = width - textX - ROW_PADDING_X - CHECKMARK_SIZE - CHECKMARK_RIGHT_MARGIN; + + childComponents.add(new ColorComponent( + 0, 0, width, height, + isSelected ? GuiColors.ROW_SELECTED : GuiColors.ROW_BACKGROUND + )); + + if (isSelected) { + childComponents.add(new ColorComponent(0, 0, INDICATOR_WIDTH, height, GuiColors.INDICATOR_SELECTED)); + } + + TextComponent nameText = new TextComponent( + textX, ROW_PADDING_Y, + descriptor.name(option), + isSelected ? GuiColors.NAME_SELECTED : GuiColors.NAME_DEFAULT + ); + nameText.setDrawShadow(true); + childComponents.add(nameText); + + Component desc = descriptor.description(option); + if (desc != null) { + childComponents.add(new MultiLineTextComponent( + textX, ROW_PADDING_Y + lineHeight + 2, + textWidth, desc, GuiColors.DESCRIPTION + )); + } + } + + @Override + protected void renderWidget(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + int rowLeft = getX(); + int rowTop = getY(); + int rowWidth = getWidth(); + int rowHeight = getHeight(); + + int borderColor = isSelected ? GuiColors.BORDER_SELECTED + : isHovered() ? GuiColors.BORDER_HOVERED + : GuiColors.BORDER_IDLE; + GuiHelper.drawBorder(graphics, rowLeft, rowTop, rowWidth, rowHeight, borderColor); + + for (IComponent component : childComponents) { + component.updateParentPosition(rowLeft, rowTop, rowWidth, rowHeight); + component.renderBase(graphics, mouseX, mouseY, partialTick, rowWidth, rowHeight); + } + + int checkboxX = rowLeft + rowWidth - CHECKMARK_SIZE - CHECKMARK_RIGHT_MARGIN; + int checkboxY = rowTop + (rowHeight - CHECKMARK_SIZE) / 2; + drawCheckbox(graphics, checkboxX, checkboxY, isSelected); + } + + @Override + public boolean mouseClicked(@NotNull MouseButtonEvent event, boolean bl) { + if (event.button() == 0 && isMouseOver(event.x(), event.y())) { + onClick.accept(option); + return true; + } + return false; + } + + private static void drawCheckbox(GuiGraphics graphics, int x, int y, boolean checked) { + GuiHelper.drawBorder(graphics, x, y, CHECKMARK_SIZE, CHECKMARK_SIZE, GuiColors.BORDER_IDLE); + + if (checked) { + graphics.fill(x + 1, y + 1, x + CHECKMARK_SIZE - 1, y + CHECKMARK_SIZE - 1, GuiColors.CHECKMARK_BOX); + graphics.drawCenteredString( + Minecraft.getInstance().font, + "✓", + x + CHECKMARK_SIZE / 2, + y + (CHECKMARK_SIZE - Minecraft.getInstance().font.lineHeight) / 2, + GuiColors.CHECKMARK_TICK + ); + } + } + + @Override protected int contentHeight() { return 0; } + @Override protected double scrollRate() { return 0; } + @Override protected void updateWidgetNarration(@NotNull NarrationElementOutput narrationOutput) { } + + @Override + public @NotNull ScreenRectangle getBorderForArrowNavigation(@NotNull ScreenDirection direction) { + return getRectangle(); + } + + @Override + public @NotNull List children() { return List.of(); } + + @Override + public @NotNull Collection getNarratables() { + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/OptionCardGrid.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/OptionCardGrid.java new file mode 100644 index 0000000..7e50b78 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/OptionCardGrid.java @@ -0,0 +1,270 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.api.component.IComponent; +import com.daqem.uilib.api.widget.IWidget; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.color.ColorComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractContainerWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +public class OptionCardGrid extends EmptyComponent { + + private static final int SCROLL_BAR_WIDTH = 8; + private static final int PREVIEW_ASPECT_W = 16; + private static final int PREVIEW_ASPECT_H = 9; + private static final int BORDER_THICKNESS = 1; + private static final int LABEL_PADDING = 6; + private static final int BADGE_SIZE = 10; + + private final int columns; + private final int cardGap; + private final List options; + private final CardDescriptor descriptor; + private final Consumer onSelect; + private String selectedId; + + public OptionCardGrid(int x, int y, int width, int height, int columns, int cardGap, List options, CardDescriptor descriptor, String selectedId, Consumer onSelect) { + super(x, y, width, height); + this.columns = columns; + this.cardGap = cardGap; + this.options = options; + this.descriptor = descriptor; + this.selectedId = selectedId; + this.onSelect = onSelect; + + buildGrid(); + } + + private void buildGrid() { + this.clearComponents(); + + int gridWidth = getWidth(); + int gridHeight = getHeight(); + + int cardWidth = (gridWidth - SCROLL_BAR_WIDTH - (cardGap * (columns - 1))) / columns; + int texW = descriptor.previewTextureWidth(options.getFirst()); + int texH = descriptor.previewTextureHeight(options.getFirst()); + int previewHeight = cardWidth * texH / texW; + int textWidth = cardWidth - (LABEL_PADDING * 2); + int lineHeight = Minecraft.getInstance().font.lineHeight; + + int maxDescriptionHeight = calculateMaxDescriptionHeight(textWidth); + int cardLabelHeight = LABEL_PADDING + lineHeight + 2 + maxDescriptionHeight + LABEL_PADDING; + int cardHeight = previewHeight + cardLabelHeight; + + EmptyComponent cardGrid = new EmptyComponent(0, 0, gridWidth - SCROLL_BAR_WIDTH, 0); + + for (int i = 0; i < options.size(); i++) { + T option = options.get(i); + int col = i % columns; + int row = i / columns; + + int cardX = col * (cardWidth + cardGap); + int cardY = row * (cardHeight + cardGap); + boolean isSelected = descriptor.id(option).equals(selectedId); + + OptionCard card = new OptionCard<>( + cardX, cardY, cardWidth, cardHeight, previewHeight, + option, descriptor, isSelected, + clicked -> { + String clickedId = descriptor.id(clicked); + if (clickedId.equals(this.selectedId)) { + this.selectedId = null; + this.onSelect.accept(null); + } else { + this.selectedId = clickedId; + this.onSelect.accept(clicked); + } + this.buildGrid(); + } + ); + cardGrid.addWidget(card); + } + + int totalRows = (int) Math.ceil((double) options.size() / columns); + cardGrid.setHeight(totalRows * (cardHeight + cardGap)); + + this.addComponent(GuiHelper.scrollWrapped(0, 0, gridWidth, gridHeight, + scroll -> scroll.addComponent(cardGrid))); + + this.updateParentPosition(getParentX(), getParentY(), gridWidth, gridHeight); + } + + private int calculateMaxDescriptionHeight(int textWidth) { + int maxHeight = 0; + for (T option : options) { + MultiLineTextComponent probe = new MultiLineTextComponent( + 0, 0, textWidth, descriptor.description(option), GuiColors.DESCRIPTION + ); + maxHeight = Math.max(maxHeight, probe.getHeight()); + } + return maxHeight; + } + + public interface CardDescriptor { + String id(T option); + Component name(T option); + Component description(T option); + Identifier previewTexture(T option); + int previewTextureWidth(T option); + int previewTextureHeight(T option); + + static CardDescriptor of( + Function id, + Function name, + Function description, + Function previewTexture, + ToIntFunction previewTexWidth, + ToIntFunction previewTexHeight + ) { + return new CardDescriptor<>() { + @Override public String id(T o) { return id.apply(o); } + @Override public Component name(T o) { return name.apply(o); } + @Override public Component description(T o) { return description.apply(o); } + @Override public Identifier previewTexture(T o) { return previewTexture.apply(o); } + @Override public int previewTextureWidth(T o) { return previewTexWidth.applyAsInt(o); } + @Override public int previewTextureHeight(T o) { return previewTexHeight.applyAsInt(o); } + }; + } + } + + private static class OptionCard extends AbstractContainerWidget implements IWidget { + private final T option; + private final CardDescriptor descriptor; + private final boolean isSelected; + private final int previewHeight; + private final Consumer onClick; + private final List childComponents = new ArrayList<>(); + + OptionCard(int x, int y, int width, int height, int previewHeight, T option, CardDescriptor descriptor, boolean isSelected, Consumer onClick) { + super(x, y, width, height, Component.empty()); + this.previewHeight = previewHeight; + this.option = option; + this.descriptor = descriptor; + this.isSelected = isSelected; + this.onClick = onClick; + + setupChildComponents(width, height); + } + + private void setupChildComponents(int width, int height) { + int labelAreaY = BORDER_THICKNESS + previewHeight; + int labelAreaHeight = height - labelAreaY - BORDER_THICKNESS; + childComponents.add(new ColorComponent( + BORDER_THICKNESS, + labelAreaY, + width - BORDER_THICKNESS * 2, + labelAreaHeight, + GuiColors.ROW_BACKGROUND + )); + + int textWidth = width - (LABEL_PADDING * 2); + int labelsStartY = BORDER_THICKNESS + previewHeight + LABEL_PADDING; + int lineHeight = Minecraft.getInstance().font.lineHeight; + + TextComponent nameText = new TextComponent( + LABEL_PADDING, labelsStartY, + descriptor.name(option), + isSelected ? GuiColors.NAME_SELECTED : GuiColors.NAME_DEFAULT + ); + nameText.setDrawShadow(true); + childComponents.add(nameText); + + childComponents.add(new MultiLineTextComponent( + LABEL_PADDING, labelsStartY + lineHeight + 2, + textWidth, descriptor.description(option), GuiColors.DESCRIPTION + )); + } + + @Override + protected void renderWidget(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + int cardLeft = getX(); + int cardTop = getY(); + int cardWidth = getWidth(); + int cardHeight = getHeight(); + + int borderColor = isSelected ? GuiColors.BORDER_SELECTED + : isHovered() ? GuiColors.BORDER_HOVERED + : GuiColors.BORDER_IDLE; + drawBorder(graphics, cardLeft, cardTop, cardWidth, cardHeight, borderColor); + + graphics.blit( + RenderPipelines.GUI_TEXTURED, + descriptor.previewTexture(option), + cardLeft + BORDER_THICKNESS, cardTop + BORDER_THICKNESS, + 0f, 0f, + cardWidth - BORDER_THICKNESS * 2, previewHeight, + descriptor.previewTextureWidth(option), descriptor.previewTextureHeight(option), + descriptor.previewTextureWidth(option), descriptor.previewTextureHeight(option) + ); + + if (isSelected) { + drawCheckmarkBadge(graphics, cardLeft + cardWidth - BADGE_SIZE - 4, cardTop + 4); + } + + for (IComponent component : childComponents) { + component.updateParentPosition(cardLeft, cardTop, cardWidth, cardHeight); + component.renderBase(graphics, mouseX, mouseY, partialTick, cardWidth, cardHeight); + } + } + + @Override + public boolean mouseClicked(@NotNull MouseButtonEvent event, boolean bl) { + if (event.button() == 0 && isMouseOver(event.x(), event.y())) { + onClick.accept(option); + return true; + } + return false; + } + + private void drawCheckmarkBadge(GuiGraphics graphics, int badgeX, int badgeY) { + graphics.fill(badgeX, badgeY, badgeX + BADGE_SIZE, badgeY + BADGE_SIZE, GuiColors.BORDER_SELECTED); + graphics.drawCenteredString( + Minecraft.getInstance().font, + "✓", + badgeX + BADGE_SIZE / 2, + badgeY + 1, + GuiColors.CHECKMARK_TICK + ); + } + + private static void drawBorder(GuiGraphics graphics, int x, int y, int width, int height, int color) { + GuiHelper.drawBorder(graphics, x, y, width, height, color); + } + + @Override protected int contentHeight() { return 0; } + + @Override protected double scrollRate() { return 0; } + + @Override protected void updateWidgetNarration(@NotNull NarrationElementOutput narrationOutput) { } + + @Override public @NotNull ScreenRectangle getBorderForArrowNavigation(@NotNull ScreenDirection direction) { return getRectangle(); } + + @Override public @NotNull List children() { return List.of(); } + + @Override public @NotNull Collection getNarratables() { return List.of(); } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/OptionSelectList.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/OptionSelectList.java new file mode 100644 index 0000000..d01a1ba --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/OptionSelectList.java @@ -0,0 +1,234 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.api.component.IComponent; +import com.daqem.uilib.api.widget.IWidget; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.color.ColorComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractContainerWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A reusable scrollable vertical list of selectable text rows. + * Each row shows a name and an optional description. + * + * @param The option type (record, class, enum, etc.) + */ +public class OptionSelectList extends EmptyComponent { + + private static final int ROW_GAP = 4; + private static final int ROW_PADDING_X = 10; + private static final int ROW_PADDING_Y = 8; + private static final int SCROLL_BAR_WIDTH = 8; + private static final int INDICATOR_WIDTH = 3; + private static final int INDICATOR_GAP = 8; + + private final List options; + private final RowDescriptor descriptor; + private final Consumer onSelect; + private String selectedId; + private ScrollContainerWidget scrollContainer; + + public OptionSelectList(int x, int y, int width, int height, List options, RowDescriptor descriptor, String selectedId, Consumer onSelect) { + super(x, y, width, height); + this.options = options; + this.descriptor = descriptor; + this.selectedId = selectedId; + this.onSelect = onSelect; + + buildList(); + } + + private void buildList() { + double savedScroll = scrollContainer != null ? scrollContainer.scrollAmount() : 0; + this.clearComponents(); + + int listWidth = getWidth(); + int listHeight = getHeight(); + int rowWidth = listWidth - SCROLL_BAR_WIDTH; + int textWidth = rowWidth - ROW_PADDING_X * 2 - INDICATOR_WIDTH - INDICATOR_GAP; + int lineHeight = Minecraft.getInstance().font.lineHeight; + + scrollContainer = new ScrollContainerWidget(listWidth, listHeight); + EmptyComponent rowContainer = new EmptyComponent(0, 0, rowWidth, 0); + + int currentY = 0; + + for (T option : options) { + boolean isSelected = descriptor.id(option).equals(selectedId); + + int descHeight = 0; + Component desc = descriptor.description(option); + if (desc != null) { + MultiLineTextComponent probe = new MultiLineTextComponent(0, 0, textWidth, desc, GuiColors.DESCRIPTION); + descHeight = probe.getHeight(); + } + + int rowHeight = ROW_PADDING_Y + lineHeight + (descHeight > 0 ? 2 + descHeight : 0) + ROW_PADDING_Y; + + SelectRow row = new SelectRow<>( + 0, currentY, + rowWidth, rowHeight, + option, + descriptor, + isSelected, + clicked -> { + String clickedId = descriptor.id(clicked); + selectedId = clickedId.equals(selectedId) ? null : clickedId; + onSelect.accept(clicked); + buildList(); + } + ); + rowContainer.addWidget(row); + currentY += rowHeight + ROW_GAP; + } + + rowContainer.setHeight(currentY); + scrollContainer.addComponent(rowContainer); + scrollContainer.setScrollAmount(savedScroll); + + EmptyComponent scrollWrapper = new EmptyComponent(0, 0, listWidth, listHeight); + scrollWrapper.addWidget(scrollContainer); + this.addComponent(scrollWrapper); + + this.updateParentPosition(getParentX(), getParentY(), listWidth, listHeight); + } + + /** + * Tells the list how to read each field from your option type. + * Build one with {@link #of} using method references. + * Description may return null if you don't need one. + */ + public interface RowDescriptor { + String id(T option); + Component name(T option); + Component description(T option); + + static RowDescriptor of( + Function id, + Function name, + Function description + ) { + return new RowDescriptor<>() { + @Override public String id(T o) { return id.apply(o); } + @Override public Component name(T o) { return name.apply(o); } + @Override public Component description(T o) { return description.apply(o); } + }; + } + } + + private static class SelectRow extends AbstractContainerWidget implements IWidget { + private final T option; + private final boolean isSelected; + private final Consumer onClick; + private final List childComponents = new ArrayList<>(); + + SelectRow( + int x, int y, + int width, int height, + T option, + RowDescriptor descriptor, + boolean isSelected, + Consumer onClick + ) { + super(x, y, width, height, Component.empty()); + this.option = option; + this.isSelected = isSelected; + this.onClick = onClick; + + int lineHeight = Minecraft.getInstance().font.lineHeight; + int textX = ROW_PADDING_X + INDICATOR_WIDTH + INDICATOR_GAP; + int textWidth = width - textX - ROW_PADDING_X; + + childComponents.add(new ColorComponent( + 0, 0, width, height, + isSelected ? GuiColors.ROW_SELECTED : GuiColors.ROW_BACKGROUND + )); + + if (isSelected) { + childComponents.add(new ColorComponent(0, 0, INDICATOR_WIDTH, height, GuiColors.INDICATOR_SELECTED)); + } + + TextComponent nameText = new TextComponent( + textX, ROW_PADDING_Y, + descriptor.name(option), + isSelected ? GuiColors.NAME_SELECTED : GuiColors.NAME_DEFAULT + ); + nameText.setDrawShadow(true); + childComponents.add(nameText); + + Component desc = descriptor.description(option); + if (desc != null) { + childComponents.add(new MultiLineTextComponent( + textX, ROW_PADDING_Y + lineHeight + 2, + textWidth, desc, GuiColors.DESCRIPTION + )); + } + } + + @Override + protected void renderWidget(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + int rowLeft = getX(); + int rowTop = getY(); + int rowWidth = getWidth(); + int rowHeight = getHeight(); + + int borderColor = isSelected ? GuiColors.BORDER_SELECTED : isHovered() ? GuiColors.BORDER_HOVERED : GuiColors.BORDER_IDLE; + drawBorder(graphics, rowLeft, rowTop, rowWidth, rowHeight, borderColor); + + for (IComponent component : childComponents) { + component.updateParentPosition(rowLeft, rowTop, rowWidth, rowHeight); + component.renderBase(graphics, mouseX, mouseY, partialTick, rowWidth, rowHeight); + } + } + + @Override + public boolean mouseClicked(@NotNull MouseButtonEvent event, boolean bl) { + if (event.button() == 0 && isMouseOver(event.x(), event.y())) { + onClick.accept(option); + return true; + } + return false; + } + + private static void drawBorder(GuiGraphics graphics, int x, int y, int width, int height, int color) { + GuiHelper.drawBorder(graphics, x, y, width, height, color); + } + + @Override protected int contentHeight() { return 0; } + @Override protected double scrollRate() { return 0; } + @Override protected void updateWidgetNarration(@NotNull NarrationElementOutput narrationOutput) { } + + @Override + public @NotNull ScreenRectangle getBorderForArrowNavigation(@NotNull ScreenDirection direction) { + return getRectangle(); + } + + @Override + public @NotNull List children() { return List.of(); } + + @Override + public @NotNull Collection getNarratables() { + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/OverlayComponent.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/OverlayComponent.java new file mode 100644 index 0000000..f4378f2 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/OverlayComponent.java @@ -0,0 +1,107 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** + * A toggleable overlay panel with a title, scrollable Markdown body, and a close button. + */ +public class OverlayComponent extends AbstractComponent { + + private static final int PADDING = 8; + private static final int CLOSE_BTN_SIZE = 16; + private static final int HEADER_BOTTOM_GAP = 20; + + private static final int COLOR_DIM = 0x99000000; + private static final int COLOR_TITLE = 0xFFFFFFFF; + + private static final WidgetSprites X_BUTTON_SPRITES = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/x"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/x"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/xhover") + ); + + private boolean shown = false; + + private final ScrollContainerWidget scrollContainer; + private final ButtonWidget closeButton; + + private Runnable onClose; + + public OverlayComponent(int x, int y, int width, int height, Component title, String markdown) { + super(x, y, width, height); + + int fontHeight = Minecraft.getInstance().font.lineHeight; + int headerHeight = fontHeight + HEADER_BOTTOM_GAP; + int innerWidth = width - PADDING * 2; + int scrollHeight = height - headerHeight; + + TextComponent titleComp = new TextComponent(PADDING, PADDING, title, COLOR_TITLE); + titleComp.setDrawShadow(true); + this.addComponent(titleComp); + + MarkdownComponent markdownComponent = new MarkdownComponent(0, 0, innerWidth - 8, markdown); + scrollContainer = new ScrollContainerWidget(innerWidth, scrollHeight); + scrollContainer.addComponent(markdownComponent); + + EmptyComponent scrollWrapper = new EmptyComponent(PADDING, headerHeight, innerWidth, scrollHeight); + scrollWrapper.addWidget(scrollContainer); + this.addComponent(scrollWrapper); + + closeButton = new CustomButtonWidget( + width - CLOSE_BTN_SIZE - PADDING, PADDING, + CLOSE_BTN_SIZE, CLOSE_BTN_SIZE, + Component.literal(""), + X_BUTTON_SPRITES, + btn -> setShown(false) + ); + this.addWidget(closeButton); + + setShown(false); + } + + public void setShown(boolean shown) { + this.shown = shown; + scrollContainer.visible = shown; + scrollContainer.active = shown; + closeButton.visible = shown; + closeButton.active = shown; + if (!shown && onClose != null) { + onClose.run(); + } + } + + public void toggle() { + setShown(!shown); + } + + public boolean isShown() { + return shown; + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + @Override + public void renderBase(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + if (!shown) return; + super.renderBase(graphics, mouseX, mouseY, partialTick, parentWidth, parentHeight); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + graphics.fill(0, 0, parentWidth, parentHeight, COLOR_DIM); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/component/RestoreConfirmOverlay.java b/src/main/java/com/github/kd_gaming1/packcore/gui/component/RestoreConfirmOverlay.java new file mode 100644 index 0000000..f565e2e --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/component/RestoreConfirmOverlay.java @@ -0,0 +1,168 @@ +package com.github.kd_gaming1.packcore.gui.component; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.BackupEntry; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import eu.midnightdust.lib.config.MidnightConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class RestoreConfirmOverlay extends AbstractComponent { + + private static final int PANEL_WIDTH = 310; + private static final int PADDING = 14; + private static final int BUTTON_WIDTH = 120; + private static final int BUTTON_HEIGHT = 18; + private static final int BUTTON_GAP = 10; + + private static final int COLOR_DIM = 0xBB000000; + private static final int COLOR_BACKGROUND = 0xF0080F1A; + private static final int COLOR_BORDER = 0x882196F3; + private static final int COLOR_TEXT = 0xFFFFFFFF; + private static final int COLOR_SUBTEXT = 0xFFAAAAAA; + private static final int COLOR_WARNING = 0xFFFFAA00; + private static final int COLOR_WARNING_BG = 0x33FFAA00; + private static final int COLOR_NOTE = 0xFF666666; + + private boolean visible = false; + private BackupEntry backup; + private Runnable onClose; + + private final CustomButtonWidget cancelButton; + private final CustomButtonWidget confirmButton; + + private int panelX; + private int panelY; + private int panelHeight; + + public RestoreConfirmOverlay(int width, int height) { + super(0, 0, width, height); + + cancelButton = new CustomButtonWidget(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.overlay.restore.button.cancel"), + GuiHelper.BLANK_BUTTON_SPRITES, btn -> setVisible(false)); + + confirmButton = new CustomButtonWidget(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.overlay.restore.button.confirm"), + GuiHelper.BLANK_BUTTON_SPRITES, btn -> confirmRestore()); + + addWidgets(List.of(cancelButton, confirmButton)); + setVisible(false); + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + public void show(BackupEntry backup) { + this.backup = backup; + setVisible(true); + layoutPanel(); + } + + public void setVisible(boolean visible) { + this.visible = visible; + cancelButton.visible = confirmButton.visible = visible; + cancelButton.active = confirmButton.active = visible; + + if (!visible && onClose != null) { + onClose.run(); + } + } + + public boolean isVisible() { + return visible; + } + + private void layoutPanel() { + var font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + int warningHeight = lineHeight * 2 + 12; + + // Sum every content row so panelHeight and buttonsY share one source of truth + int contentHeight = lineHeight + 10 // title + + lineHeight + 4 // filename + + lineHeight + 14 // timestamp + + warningHeight + 8 // warning box + + lineHeight + 12 // closing note + + BUTTON_HEIGHT; + + panelHeight = PADDING * 2 + contentHeight; + + panelX = getTotalX() + (getWidth() - PANEL_WIDTH) / 2; + panelY = getTotalY() + (getHeight() - panelHeight) / 2; + + int buttonsX = panelX + (PANEL_WIDTH - (BUTTON_WIDTH * 2 + BUTTON_GAP)) / 2; + int buttonsY = panelY + PADDING + contentHeight - BUTTON_HEIGHT; + + cancelButton.uilib$updateParentPosition(buttonsX, buttonsY); + confirmButton.uilib$updateParentPosition(buttonsX + BUTTON_WIDTH + BUTTON_GAP, buttonsY); + } + + private void confirmRestore() { + if (backup == null) return; + PackCoreConfig.pendingRestoreBackup = backup.zipPath().getFileName().toString(); + MidnightConfig.write(MOD_ID); + Minecraft.getInstance().stop(); + } + + @Override + public void renderBase(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + if (visible) { + super.renderBase(graphics, mouseX, mouseY, partialTick, parentWidth, parentHeight); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + if (!visible || backup == null) return; + + var font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + int warningHeight = lineHeight * 2 + 12; + + graphics.fill(getTotalX(), getTotalY(), getTotalX() + getWidth(), getTotalY() + getHeight(), COLOR_DIM); + graphics.fill(panelX, panelY, panelX + PANEL_WIDTH, panelY + panelHeight, COLOR_BACKGROUND); + drawBorder(graphics); + + int currentY = panelY + PADDING; + + graphics.drawCenteredString(font, + Component.translatable("gui.packcore.overlay.restore.title"), + panelX + PANEL_WIDTH / 2, currentY, COLOR_TEXT); + currentY += lineHeight + 10; + + String truncatedName = font.plainSubstrByWidth( + backup.zipPath().getFileName().toString(), PANEL_WIDTH - PADDING * 2); + graphics.drawCenteredString(font, truncatedName, panelX + PANEL_WIDTH / 2, currentY, COLOR_TEXT); + currentY += lineHeight + 4; + + graphics.drawCenteredString(font, backup.displayName(), panelX + PANEL_WIDTH / 2, currentY, COLOR_SUBTEXT); + currentY += lineHeight + 14; + + graphics.fill(panelX + PADDING, currentY, + panelX + PANEL_WIDTH - PADDING, currentY + warningHeight, COLOR_WARNING_BG); + graphics.drawString(font, + Component.translatable("gui.packcore.overlay.restore.warning1"), + panelX + PADDING + 8, currentY + 5, COLOR_WARNING, false); + graphics.drawString(font, + Component.translatable("gui.packcore.overlay.restore.warning2"), + panelX + PADDING + 8, currentY + 5 + lineHeight + 2, COLOR_WARNING, false); + currentY += warningHeight + 8; + + graphics.drawCenteredString(font, + Component.translatable("gui.packcore.overlay.restore.note"), + panelX + PANEL_WIDTH / 2, currentY, COLOR_NOTE); + } + + private void drawBorder(GuiGraphics graphics) { + GuiHelper.drawBorder(graphics, panelX, panelY, PANEL_WIDTH, panelHeight, COLOR_BORDER); + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/ChangelogScreen.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/ChangelogScreen.java new file mode 100644 index 0000000..692a474 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/ChangelogScreen.java @@ -0,0 +1,196 @@ +package com.github.kd_gaming1.packcore.gui.screen; + +import com.github.kd_gaming1.packcore.update.UpdateStatus; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.Style; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.List; + +public class ChangelogScreen extends Screen { + + private static final int PADDING = 16; + private static final int BOX_PADDING = 12; + private static final int CLOSE_BUTTON_HEIGHT = 20; + private static final int CLOSE_BUTTON_WIDTH = 120; + private static final int LINE_SPACING = 2; + + private static final int COLOR_BACKGROUND = 0xDD101010; + private static final int COLOR_BORDER = 0x88FFD700; + private static final int COLOR_TITLE = 0xFFFFFFFF; + private static final int COLOR_HEADING = 0xFFFFDD44; + private static final int COLOR_CONTENT = 0xFFCCCCCC; + + private final Screen parent; + private final String rawChangelog; + + private final List lines = new ArrayList<>(); + private int scrollOffset = 0; + private int maxScroll = 0; + private int totalContentHeight = 0; + + public ChangelogScreen(Screen parent, UpdateStatus status) { + super(Component.translatable( + "gui.packcore.overlay.changelog.title", + status.latestVersion() != null ? status.latestVersion() : "" + )); + this.parent = parent; + this.rawChangelog = status.changelog() != null ? status.changelog() : ""; + } + + @Override + protected void init() { + lines.clear(); + scrollOffset = 0; + totalContentHeight = 0; + + int contentWidth = this.width - PADDING * 2 - BOX_PADDING * 2; + + for (String raw : rawChangelog.split("\n")) { + parseAndAddLine(raw.stripTrailing(), contentWidth); + } + + totalContentHeight = lines.stream().mapToInt(RenderedLine::height).sum(); + maxScroll = Math.max(0, totalContentHeight - getViewHeight()); + + int closeX = this.width / 2 - CLOSE_BUTTON_WIDTH / 2; + int closeY = this.height - PADDING * 2 - CLOSE_BUTTON_HEIGHT; + addRenderableWidget(Button.builder( + Component.translatable("gui.done"), + button -> onClose() + ).bounds(closeX, closeY, CLOSE_BUTTON_WIDTH, CLOSE_BUTTON_HEIGHT).build()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + int boxX = PADDING; + int boxY = PADDING; + int boxWidth = this.width - PADDING * 2; + int boxHeight = this.height - PADDING * 2; + + graphics.fill(boxX, boxY, boxX + boxWidth, boxY + boxHeight, COLOR_BACKGROUND); + graphics.renderOutline(boxX, boxY, boxWidth, boxHeight, COLOR_BORDER); + + int titleY = boxY + BOX_PADDING; + graphics.drawCenteredString(this.font, this.getTitle(), this.width / 2, titleY, COLOR_TITLE); + + int contentX = boxX + BOX_PADDING; + int scrollbarTop = titleY + this.font.lineHeight + BOX_PADDING; + int scrollbarBottom = boxY + boxHeight - PADDING - CLOSE_BUTTON_HEIGHT - BOX_PADDING; + + graphics.enableScissor(contentX, scrollbarTop, contentX + boxWidth - BOX_PADDING * 2, scrollbarBottom); + + int drawY = scrollbarTop - scrollOffset; + for (RenderedLine line : lines) { + if (drawY + line.height >= scrollbarTop && drawY <= scrollbarBottom) { + graphics.drawString(this.font, line.text, contentX, drawY, line.color, false); + } + drawY += line.height; + } + + graphics.disableScissor(); + + if (maxScroll > 0 && totalContentHeight > 0) { + int scrollbarX = boxX + boxWidth - BOX_PADDING / 2 - 2; + int scrollbarHeight = scrollbarBottom - scrollbarTop; + + graphics.fill(scrollbarX, scrollbarTop, scrollbarX + 3, scrollbarBottom, 0x44FFFFFF); + + int viewHeight = getViewHeight(); + float thumbRatio = (float) viewHeight / totalContentHeight; + int thumbHeight = Math.max(20, (int) (scrollbarHeight * thumbRatio)); + int thumbY = scrollbarTop + (int) ((scrollbarHeight - thumbHeight) * ((float) scrollOffset / maxScroll)); + + graphics.fill(scrollbarX, thumbY, scrollbarX + 3, thumbY + thumbHeight, 0xCCFFD700); + } + + super.render(graphics, mouseX, mouseY, delta); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + scrollOffset = (int) Math.max(0, Math.min(maxScroll, scrollOffset - scrollY * 12)); + return true; + } + + @Override + public boolean keyPressed(KeyEvent keyEvent) { + if (keyEvent.key() == GLFW.GLFW_KEY_ESCAPE) { + onClose(); + return true; + } + return super.keyPressed(keyEvent); + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(parent); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private void parseAndAddLine(String raw, int maxWidth) { + int lineHeight = this.font.lineHeight + LINE_SPACING; + + if (raw.isBlank()) { + lines.add(new RenderedLine("", COLOR_CONTENT, lineHeight / 2)); + return; + } + + String text; + int color; + + if (raw.startsWith("### ")) { + text = raw.substring(4); + color = COLOR_HEADING; + } else if (raw.startsWith("## ")) { + text = raw.substring(3); + color = COLOR_HEADING; + } else if (raw.startsWith("# ")) { + text = raw.substring(2); + color = COLOR_HEADING; + } else if (raw.startsWith("- ") || raw.startsWith("* ")) { + text = "• " + raw.substring(2); + color = COLOR_CONTENT; + } else { + text = raw; + color = COLOR_CONTENT; + } + + text = text + .replaceAll("\\*\\*(.+?)\\*\\*", "$1") + .replaceAll("\\*(.+?)\\*", "$1") + .replaceAll("`(.+?)`", "$1"); + + if (this.font.width(text) > maxWidth) { + for (FormattedText segment : this.font.getSplitter().splitLines( + FormattedText.of(text), + maxWidth, + Style.EMPTY + )) { + lines.add(new RenderedLine(segment.getString(), color, lineHeight)); + } + } else { + lines.add(new RenderedLine(text, color, lineHeight)); + } + } + + private int getViewHeight() { + int boxHeight = this.height - PADDING * 2; + int titleSpace = BOX_PADDING + this.font.lineHeight + BOX_PADDING; + int buttonSpace = PADDING + CLOSE_BUTTON_HEIGHT + BOX_PADDING; + return boxHeight - titleSpace - buttonSpace; + } + + private record RenderedLine(String text, int color, int height) {} +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java new file mode 100644 index 0000000..9911a42 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java @@ -0,0 +1,241 @@ +package com.github.kd_gaming1.packcore.gui.screen; + +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.gui.screen.config.ConfigScreen; +import com.github.kd_gaming1.packcore.gui.util.ToastHelper; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import com.github.kd_gaming1.packcore.update.UpdateChecker; +import com.github.kd_gaming1.packcore.update.UpdateStatus; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ImageButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; +import org.jspecify.annotations.NonNull; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** + * Vanilla title screen extended with PackCore-specific buttons. + * Extends TitleScreen so other mods hooking ScreenEvents still see it. + */ +public class PackCoreTitleScreen extends TitleScreen { + + private static final int ICON_SIZE = 20; + private static final int MARGIN = 5; + private static final int ICON_SPACING = 4; + private static final int BUTTON_HEIGHT = 20; + private static final int BUTTON_STRIDE = 24; + private static final String JOIN_HYPIXEL_LABEL = Component.translatable("gui.packcore.button.join_hypixel").getString(); + + private static boolean updateToastShown = false; + private static final Set VERSION_HOOKED_SCREENS = Collections.newSetFromMap(new WeakHashMap<>()); + + @Override + protected void init() { + super.init(); + + UpdateStatus status = UpdateChecker.getCachedStatus(); + showUpdateToastIfNeeded(); + + // Join Hypixel — one row above vanilla singleplayer + int hypixelY = this.height / 4 + 48 - (BUTTON_STRIDE * 2); + addRenderableWidget(Button.builder( + Component.translatable("gui.packcore.button.join_hypixel"), + btn -> connectToHypixel() + ).bounds(this.width / 2 - 100, hypixelY, 200, BUTTON_HEIGHT).build()); + + int vanillaTwoLinesY = this.height - 10 - this.font.lineHeight - 2; + int yourVersionY = vanillaTwoLinesY - this.font.lineHeight - 2; + + int githubY = yourVersionY - MARGIN - ICON_SIZE; + int modrinthY = githubY - ICON_SPACING - ICON_SIZE; + int discordY = modrinthY - ICON_SPACING - ICON_SIZE; + + addIconButton(MARGIN, discordY, "menu/discord_icon", + Component.translatable("gui.packcore.tooltip.discord"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getDiscordUrl())); + + addIconButton(MARGIN, modrinthY, "menu/modrinth_icon", + Component.translatable("gui.packcore.tooltip.modrinth"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getWebsiteUrl())); + + addIconButton(MARGIN, githubY, "menu/github_icon", + Component.translatable("gui.packcore.tooltip.github"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getIssueTrackerUrl())); + + // Config — bottom-right + int settingsY = this.height - ICON_SIZE - MARGIN - (this.font.lineHeight * 2) - 4; + addIconButton(this.width - ICON_SIZE - MARGIN, settingsY, "menu/settings_icon", + Component.translatable("gui.packcore.tooltip.modpack_config"), + btn -> Minecraft.getInstance().setScreen(new ConfigScreen())); + + // Changelog/update + boolean hasUpdate = status.isUpdateAvailable(); + String updateIcon = hasUpdate ? "menu/update_icon_available" : "menu/update_icon"; + Component updateTooltip = hasUpdate + ? Component.translatable("gui.packcore.tooltip.update_available", status.latestVersion()) + : Component.translatable("gui.packcore.tooltip.changelog"); + + addIconButton(this.width - ICON_SIZE - MARGIN, MARGIN, updateIcon, updateTooltip, + btn -> Minecraft.getInstance().setScreen(new ChangelogScreen(this, status))); + } + + public static void decorateExisting(TitleScreen screen, int scaledWidth, int scaledHeight) { + showUpdateToastIfNeeded(); + Screens.getButtons(screen).removeIf(button -> + button instanceof PackCoreDecoratedWidget + || JOIN_HYPIXEL_LABEL.equals(button.getMessage().getString()) + ); + + int hypixelY = scaledHeight / 4 + 48 - (BUTTON_STRIDE * 2); + Screens.getButtons(screen).add(Button.builder( + Component.translatable("gui.packcore.button.join_hypixel"), + btn -> connectToHypixel(screen) + ).bounds(scaledWidth / 2 - 100, hypixelY, 200, BUTTON_HEIGHT).build()); + + int vanillaTwoLinesY = scaledHeight - 10 - Minecraft.getInstance().font.lineHeight - 2; + int yourVersionY = vanillaTwoLinesY - Minecraft.getInstance().font.lineHeight - 2; + + int githubY = yourVersionY - MARGIN - ICON_SIZE; + int modrinthY = githubY - ICON_SPACING - ICON_SIZE; + int discordY = modrinthY - ICON_SPACING - ICON_SIZE; + + Screens.getButtons(screen).add(createDecoratedIconButton( + MARGIN, discordY, "menu/discord_icon", + Component.translatable("gui.packcore.tooltip.discord"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getDiscordUrl()) + )); + + Screens.getButtons(screen).add(createDecoratedIconButton( + MARGIN, modrinthY, "menu/modrinth_icon", + Component.translatable("gui.packcore.tooltip.modrinth"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getWebsiteUrl()) + )); + + Screens.getButtons(screen).add(createDecoratedIconButton( + MARGIN, githubY, "menu/github_icon", + Component.translatable("gui.packcore.tooltip.github"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getIssueTrackerUrl()) + )); + + int settingsY = scaledHeight - ICON_SIZE - MARGIN - (Minecraft.getInstance().font.lineHeight * 2) - 4; + Screens.getButtons(screen).add(createDecoratedIconButton( + scaledWidth - ICON_SIZE - MARGIN, settingsY, "menu/settings_icon", + Component.translatable("gui.packcore.tooltip.modpack_config"), + btn -> Minecraft.getInstance().setScreen(new ConfigScreen()) + )); + + UpdateStatus status = UpdateChecker.getCachedStatus(); + boolean hasUpdate = status.isUpdateAvailable(); + String updateIcon = hasUpdate ? "menu/update_icon_available" : "menu/update_icon"; + Component updateTooltip = hasUpdate + ? Component.translatable("gui.packcore.tooltip.update_available", status.latestVersion()) + : Component.translatable("gui.packcore.tooltip.changelog"); + + Screens.getButtons(screen).add(createDecoratedIconButton( + scaledWidth - ICON_SIZE - MARGIN, MARGIN, updateIcon, updateTooltip, + btn -> Minecraft.getInstance().setScreen(new ChangelogScreen(screen, status)) + )); + + registerVersionHook(screen); + } + + @Override + public void render(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.render(graphics, mouseX, mouseY, delta); + + int yourVersionY = (this.height - MARGIN - ICON_SIZE) + (ICON_SIZE - this.font.lineHeight) / 2; + graphics.drawString(this.font, buildVersionText(UpdateChecker.getCachedStatus()), MARGIN, yourVersionY, 0xFFFFFFFF, false); + } + + private void connectToHypixel() { + connectToHypixel(this); + } + + private static void connectToHypixel(TitleScreen screen) { + ServerData serverData = new ServerData( + "Hypixel", + PackCoreConfig.serverAddressForQuickJoinButton, + ServerData.Type.OTHER + ); + ConnectScreen.startConnecting( + screen, + Minecraft.getInstance(), + ServerAddress.parseString(PackCoreConfig.serverAddressForQuickJoinButton), + serverData, + false, + null + ); + } + + private void addIconButton(int x, int y, String spritePath, Component tooltip, Button.OnPress onPress) { + Identifier icon = Identifier.fromNamespaceAndPath(MOD_ID, spritePath); + WidgetSprites sprites = new WidgetSprites(icon, icon, icon); + ImageButton button = new ImageButton(x, y, ICON_SIZE, ICON_SIZE, sprites, onPress); + button.setTooltip(Tooltip.create(tooltip)); + addRenderableWidget(button); + } + + private static PackCoreImageButton createDecoratedIconButton(int x, int y, String spritePath, Component tooltip, Button.OnPress onPress) { + Identifier icon = Identifier.fromNamespaceAndPath(MOD_ID, spritePath); + WidgetSprites sprites = new WidgetSprites(icon, icon, icon); + PackCoreImageButton button = new PackCoreImageButton(x, y, ICON_SIZE, ICON_SIZE, sprites, onPress); + button.setTooltip(Tooltip.create(tooltip)); + return button; + } + + private static void registerVersionHook(TitleScreen screen) { + if (!VERSION_HOOKED_SCREENS.add(screen)) return; + + ScreenEvents.afterRender(screen).register((screen1, graphics, mouseX, mouseY, tickDelta) -> { + Minecraft client = Minecraft.getInstance(); + int height = client.getWindow().getGuiScaledHeight(); + int yourVersionY = (height - MARGIN - ICON_SIZE) + (ICON_SIZE - client.font.lineHeight) / 2; + graphics.drawString(client.font, buildVersionText(UpdateChecker.getCachedStatus()), MARGIN, yourVersionY, 0xFFFFFFFF, false); + }); + } + + private static void showUpdateToastIfNeeded() { + if (updateToastShown) return; + + UpdateStatus status = UpdateChecker.getCachedStatus(); + if (status.isUpdateAvailable()) { + ToastHelper.showUpdateAvailable(status.latestVersion()); + } + updateToastShown = true; + } + + private static Component buildVersionText(UpdateStatus status) { + String installed = status.installedVersion() != null + ? status.installedVersion() + : ModpackMetadata.getInstance().getModpackVersion(); + + if (status.isUpdateAvailable()) { + return Component.literal("v" + installed + " → v" + status.latestVersion()); + } + return Component.literal("v" + installed); + } + + private interface PackCoreDecoratedWidget {} + + private static final class PackCoreImageButton extends ImageButton implements PackCoreDecoratedWidget { + private PackCoreImageButton(int x, int y, int width, int height, WidgetSprites sprites, OnPress onPress) { + super(x, y, width, height, sprites, onPress); + } + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/SBETitleScreen.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/SBETitleScreen.java new file mode 100644 index 0000000..7f599a8 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/SBETitleScreen.java @@ -0,0 +1,423 @@ +package com.github.kd_gaming1.packcore.gui.screen; + +import com.daqem.uilib.gui.AbstractScreen; +import com.daqem.uilib.gui.background.PanoramaBackground; +import com.daqem.uilib.gui.component.sprite.SpriteComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.gui.component.OverlayComponent; +import com.github.kd_gaming1.packcore.gui.screen.config.ConfigScreen; +import com.github.kd_gaming1.packcore.gui.util.ImageBackground; +import com.github.kd_gaming1.packcore.gui.util.SpriteHelper; +import com.github.kd_gaming1.packcore.gui.util.ToastHelper; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import com.github.kd_gaming1.packcore.update.UpdateChecker; +import com.github.kd_gaming1.packcore.update.UpdateStatus; +import com.terraformersmc.modmenu.api.ModMenuApi; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; +import net.minecraft.client.gui.screens.options.OptionsScreen; +import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; +import org.lwjgl.glfw.GLFW; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** + * Custom title screen for the modpack with branded UI and quick-access buttons. + */ +public class SBETitleScreen extends AbstractScreen { + + // Logo dimensions and positioning + private static final int LOGO_ORIGINAL_WIDTH = 1020; + private static final int LOGO_ORIGINAL_HEIGHT = 77; + private static final double LOGO_SCALE = 0.5; + private static final int LOGO_Y_POSITION = 20; + + // Main menu button dimensions + private static final int BUTTON_WIDTH = 200; + private static final int BUTTON_HEIGHT = 20; + private static final int BUTTON_SPACING = 6; + private static final int LOGO_TO_BUTTONS_GAP = 40; + + // Icon button dimensions + private static final int ICON_BUTTON_SIZE = 20; + + // Screen margins and spacing + private static final int SCREEN_MARGIN = 5; + private static final int VERSION_TEXT_HEIGHT = 15; + private static final int SOCIAL_TO_VERSION_GAP = 5; + + // Instance fields + private OverlayComponent changelogOverlay; + private ButtonWidget joinHypixelButton; + private ButtonWidget singleplayerButton; + private ButtonWidget multiplayerButton; + private ButtonWidget modmenuButton; + private ButtonWidget optionsButton; + private ButtonWidget quitButton; + + private static boolean updateToastShown = false; + + public SBETitleScreen() { + this(true); + } + + public SBETitleScreen(boolean useCustomBackground) { + super(Component.literal("Title Screen")); + if (useCustomBackground) { + this.setBackground(new ImageBackground( + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/title_menu_background.png"), + 1920, + 1080, + ImageBackground.BackgroundMode.STRETCH + )); + } else { + this.setBackground(new PanoramaBackground()); + } + } + + @Override + protected void init() { + if (!updateToastShown) { + UpdateStatus status = UpdateChecker.getCachedStatus(); + if (status.isUpdateAvailable()) { + ToastHelper.showUpdateAvailable(status.latestVersion()); + } + updateToastShown = true; + } + + // === LOGO === + SpriteComponent titleSprite = createLogo(); + + // === MAIN MENU BUTTONS === + int buttonX = this.width / 2 - BUTTON_WIDTH / 2; + int buttonStartY = calculateButtonStartY(); + + joinHypixelButton = createJoinHypixelButton(buttonX, buttonStartY); + singleplayerButton = createSingleplayerButton(buttonX, buttonStartY + (BUTTON_HEIGHT + BUTTON_SPACING)); + multiplayerButton = createMultiplayerButton(buttonX, buttonStartY + (BUTTON_HEIGHT + BUTTON_SPACING) * 2); + modmenuButton = createModMenuButton(buttonX, buttonStartY + (BUTTON_HEIGHT + BUTTON_SPACING) * 3); + optionsButton = createOptionsButton(buttonX, buttonStartY + (BUTTON_HEIGHT + BUTTON_SPACING) * 4); + quitButton = createQuitButton(buttonX, buttonStartY + (BUTTON_HEIGHT + BUTTON_SPACING) * 5); + + // === SOCIAL BUTTONS (Bottom-left, stacked vertically) === + int socialButtonX = SCREEN_MARGIN; + int versionY = this.height - VERSION_TEXT_HEIGHT; + int githubY = versionY - SOCIAL_TO_VERSION_GAP - ICON_BUTTON_SIZE; + int modrinthY = githubY - BUTTON_SPACING - ICON_BUTTON_SIZE; + int discordY = modrinthY - BUTTON_SPACING - ICON_BUTTON_SIZE; + + ButtonWidget discordButton = createDiscordButton(socialButtonX, discordY); + ButtonWidget modrinthButton = createModrinthButton(socialButtonX, modrinthY); + ButtonWidget githubButton = createGithubButton(socialButtonX, githubY); + + // === VERSION TEXT === + UpdateStatus updateStatus = UpdateChecker.getCachedStatus(); + + Component versionText = buildVersionText(updateStatus); + + TextComponent modpackVersion = new TextComponent( + SCREEN_MARGIN, versionY, + versionText, + 0xFFFFFFFF + ); + + // === CHANGELOG OVERLAY === + int overlayW = Math.min((int) (this.width * 0.80), 800); + int overlayH = Math.min((int) (this.height * 0.75), 500); + int overlayX = (this.width - overlayW) / 2; + int overlayY = (this.height - overlayH) / 2 + 25; + + String changelogVersion = resolveChangelogVersion(updateStatus); + String changelogMarkdown = resolveChangelogMarkdown(updateStatus); + + changelogOverlay = new OverlayComponent( + overlayX, overlayY, overlayW, overlayH, + Component.translatable("gui.packcore.overlay.changelog.title", changelogVersion), + changelogMarkdown + ); + changelogOverlay.setOnClose(() -> setMenuButtonsVisible(true)); + + // === CORNER BUTTONS === + ButtonWidget modpackConfigButton = createModpackConfigButton(); + ButtonWidget modpackUpdateButton = createModpackUpdateButton(changelogOverlay); + + // === ADD ALL COMPONENTS AND WIDGETS === + this.addComponent(titleSprite); + this.addComponent(modpackVersion); + + this.addWidget(joinHypixelButton); + this.addWidget(singleplayerButton); + this.addWidget(multiplayerButton); + this.addWidget(modmenuButton); + this.addWidget(optionsButton); + this.addWidget(quitButton); + + this.addWidget(discordButton); + this.addWidget(modrinthButton); + this.addWidget(githubButton); + + this.addWidget(modpackUpdateButton); + this.addWidget(modpackConfigButton); + + this.addComponent(changelogOverlay); + + super.init(); + } + + // === LOGO CREATION === + + private SpriteComponent createLogo() { + return SpriteHelper.createScaledCenteredSprite( + this.width, + this.height, + LOGO_ORIGINAL_WIDTH, + LOGO_ORIGINAL_HEIGHT, + LOGO_SCALE, + LOGO_Y_POSITION, + Identifier.fromNamespaceAndPath(MOD_ID, "title/SkyBlockEnhanced_title1") + ); + } + + private int calculateButtonStartY() { + SpriteHelper.SpriteDimensions logoDims = SpriteHelper.scaleAndCenter( + this.width, this.height, + LOGO_ORIGINAL_WIDTH, LOGO_ORIGINAL_HEIGHT, + LOGO_SCALE, LOGO_Y_POSITION + ); + return logoDims.y() + logoDims.height() + LOGO_TO_BUTTONS_GAP; + } + + // === MAIN MENU BUTTONS === + + private ButtonWidget createJoinHypixelButton(int x, int y) { + return new CustomButtonWidget( + x, y, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.button.join_hypixel"), + MAIN_BUTTON_SPRITES, + btn -> { + ServerData serverData = new ServerData( + "Hypixel", + PackCoreConfig.serverAddressForQuickJoinButton, + ServerData.Type.OTHER + ); + ConnectScreen.startConnecting( + this, + Minecraft.getInstance(), + ServerAddress.parseString(PackCoreConfig.serverAddressForQuickJoinButton), + serverData, + false, + null + ); + } + ); + } + + private ButtonWidget createSingleplayerButton(int x, int y) { + return new CustomButtonWidget( + x, y, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("menu.singleplayer"), + MAIN_BUTTON_SPRITES, + btn -> Minecraft.getInstance().setScreen(new SelectWorldScreen(this)) + ); + } + + private ButtonWidget createMultiplayerButton(int x, int y) { + return new CustomButtonWidget( + x, y, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("menu.multiplayer"), + MAIN_BUTTON_SPRITES, + btn -> Minecraft.getInstance().setScreen(new JoinMultiplayerScreen(this)) + ); + } + + private ButtonWidget createModMenuButton(int x, int y) { + return new CustomButtonWidget( + x, y, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.button.modmenu"), + MAIN_BUTTON_SPRITES, + btn -> Minecraft.getInstance().setScreen(ModMenuApi.createModsScreen(this)) + ); + } + + private ButtonWidget createOptionsButton(int x, int y) { + return new CustomButtonWidget( + x, y, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("menu.options"), + MAIN_BUTTON_SPRITES, + btn -> Minecraft.getInstance().setScreen(new OptionsScreen(this, Minecraft.getInstance().options)) + ); + } + + private ButtonWidget createQuitButton(int x, int y) { + return new CustomButtonWidget( + x, y, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("menu.quit"), + MAIN_BUTTON_SPRITES, + btn -> Minecraft.getInstance().stop() + ); + } + + // === CORNER BUTTONS === + + private ButtonWidget createModpackUpdateButton(OverlayComponent changelogOverlay) { + UpdateStatus status = UpdateChecker.getCachedStatus(); + ButtonWidget button = new CustomButtonWidget( + this.width - ICON_BUTTON_SIZE - SCREEN_MARGIN, + SCREEN_MARGIN, + ICON_BUTTON_SIZE, + ICON_BUTTON_SIZE, + Component.empty(), + // Tint the icon green when up to date, yellow when update available + status.isUpdateAvailable() + ? createIconSprites("menu/update_icon_available") + : createIconSprites("menu/update_icon"), + btn -> { + changelogOverlay.toggle(); + setMenuButtonsVisible(!changelogOverlay.isShown()); + } + ); + button.setTooltip(Tooltip.create( + status.isUpdateAvailable() + ? Component.translatable("gui.packcore.tooltip.update_available", status.latestVersion()) + : Component.translatable("gui.packcore.tooltip.changelog") + )); + return button; + } + + private ButtonWidget createModpackConfigButton() { + ButtonWidget button = new CustomButtonWidget( + this.width - ICON_BUTTON_SIZE - SCREEN_MARGIN, + this.height - ICON_BUTTON_SIZE - SCREEN_MARGIN, + ICON_BUTTON_SIZE, + ICON_BUTTON_SIZE, + Component.empty(), + createIconSprites("menu/settings_icon"), + btn -> Minecraft.getInstance().setScreen(new ConfigScreen()) + ); + button.setTooltip(Tooltip.create(Component.translatable("gui.packcore.tooltip.modpack_config"))); + return button; + } + + // === SOCIAL BUTTONS === + + private ButtonWidget createDiscordButton(int x, int y) { + ButtonWidget button = new CustomButtonWidget( + x, y, ICON_BUTTON_SIZE, ICON_BUTTON_SIZE, + Component.empty(), + createIconSprites("menu/discord_icon"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getDiscordUrl()) + ); + button.setTooltip(Tooltip.create(Component.translatable("gui.packcore.tooltip.discord"))); + return button; + } + + private ButtonWidget createModrinthButton(int x, int y) { + ButtonWidget button = new CustomButtonWidget( + x, y, ICON_BUTTON_SIZE, ICON_BUTTON_SIZE, + Component.empty(), + createIconSprites("menu/modrinth_icon"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getWebsiteUrl()) + ); + button.setTooltip(Tooltip.create(Component.translatable("gui.packcore.tooltip.modrinth"))); + return button; + } + + private ButtonWidget createGithubButton(int x, int y) { + ButtonWidget button = new CustomButtonWidget( + x, y, ICON_BUTTON_SIZE, ICON_BUTTON_SIZE, + Component.empty(), + createIconSprites("menu/github_icon"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getIssueTrackerUrl()) + ); + button.setTooltip(Tooltip.create(Component.translatable("gui.packcore.tooltip.github"))); + return button; + } + + private static Component buildVersionText(UpdateStatus status) { + String installed = status.installedVersion() != null + ? status.installedVersion() + : ModpackMetadata.getInstance().getModpackVersion(); + + if (status.isUpdateAvailable()) { + return Component.literal("v" + installed + " → ") + .append(Component.literal("v" + status.latestVersion()) + .withStyle(s -> s.withColor(0xFFFFFFFF))); // green + } + + return Component.literal("v" + installed); + } + + private static String resolveChangelogVersion(UpdateStatus status) { + if (status.latestVersion() != null && !status.latestVersion().isBlank()) { + return status.latestVersion(); + } + if (status.installedVersion() != null && !status.installedVersion().isBlank()) { + return status.installedVersion(); + } + return ModpackMetadata.getInstance().getModpackVersion(); + } + + private static String resolveChangelogMarkdown(UpdateStatus status) { + if (status.changelog() != null && !status.changelog().isBlank()) { + return status.changelog(); + } + + return Component.translatable("gui.packcore.overlay.changelog.empty").getString() + + "\n\n" + + Component.translatable("gui.packcore.overlay.changelog.empty.hint").getString(); + } + + private void setMenuButtonsVisible(boolean visible) { + joinHypixelButton.visible = visible; + joinHypixelButton.active = visible; + singleplayerButton.visible = visible; + singleplayerButton.active = visible; + multiplayerButton.visible = visible; + multiplayerButton.active = visible; + modmenuButton.visible = visible; + modmenuButton.active = visible; + optionsButton.visible = visible; + optionsButton.active = visible; + quitButton.visible = visible; + quitButton.active = visible; + } + + // === SPRITE HELPERS === + + private static final WidgetSprites MAIN_BUTTON_SPRITES = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/blank_red_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/disabled_red_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/hover_red_button") + ); + + private WidgetSprites createIconSprites(String basePath) { + Identifier icon = Identifier.fromNamespaceAndPath(MOD_ID, basePath); + return new WidgetSprites(icon, icon, icon); + } + + @Override + public boolean shouldCloseOnEsc() { + return false; + } + + @Override + public boolean keyPressed(KeyEvent keyEvent) { + if (keyEvent.key() == GLFW.GLFW_KEY_ESCAPE && changelogOverlay.isShown()) { + changelogOverlay.setShown(false); + return true; + } + return super.keyPressed(keyEvent); + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/WelcomeWizardScreen.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/WelcomeWizardScreen.java new file mode 100644 index 0000000..815d882 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/WelcomeWizardScreen.java @@ -0,0 +1,175 @@ +package com.github.kd_gaming1.packcore.gui.screen; + +import com.daqem.uilib.gui.AbstractScreen; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.gui.wizard.*; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.gui.wizard.page.*; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import eu.midnightdust.lib.config.MidnightConfig; +import com.github.kd_gaming1.packcore.gui.wizard.page.ConfirmApplyPage; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.chat.Component; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class WelcomeWizardScreen extends AbstractScreen { + + private static final int HEADER_HEIGHT = 30; + private static final int FOOTER_HEIGHT = 26; + private static final int PANEL_PADDING = 8; + + private WizardState wizardState; + private WizardNavigator navigator; + + private WizardHeaderComponent headerComponent; + private WizardButtonBar buttonBar; + private ConfirmApplyPage confirmApplyPage; + + private final Screen lastScreen; + + public WelcomeWizardScreen(Screen lastScreen) { + super(Component.translatable("gui.packcore.wizard.title", ModpackMetadata.getInstance().getModpackName())); + this.lastScreen = lastScreen; + } + + @Override + protected void init() { + boolean firstOpen = wizardState == null; + + if (firstOpen) { + wizardState = new WizardState(); + wizardState.migratedFromV3 = PackCore.migratedFromV3; + navigator = new WizardNavigator(wizardState); + registerPages(); + navigator.initialize(); + } else { + resizePages(); + } + + buildLayout(); + wireEvents(); + + super.init(); + } + + private void registerPages() { + int contentWidth = width - PANEL_PADDING * 2; + int contentHeight = height - HEADER_HEIGHT - FOOTER_HEIGHT; + + navigator.addPage(new WelcomePage(wizardState, navigator, contentWidth, contentHeight)); + navigator.addPage(new MainMenuDesignPage(wizardState, navigator, contentWidth, contentHeight)); + navigator.addPage(new PerformancePage(wizardState, navigator, contentWidth, contentHeight)); + navigator.addPage(new TabDesignPage(wizardState, navigator, contentWidth, contentHeight)); + navigator.addPage(new ItemBackgroundPage(wizardState, navigator, contentWidth, contentHeight)); + navigator.addPage(new StorageDesignPage(wizardState, navigator, contentWidth, contentHeight)); + if (FabricLoader.getInstance().isModLoaded("scaleme")) { + navigator.addPage(new SwordBlockPage(wizardState, navigator, contentWidth, contentHeight)); + } + if (FabricLoader.getInstance().isModLoaded("scamscreener")) { + navigator.addPage(new ScamScreenerPage(wizardState, navigator, contentWidth, contentHeight)); + } + navigator.addPage(new ResourcePackPage(wizardState, navigator, contentWidth, contentHeight)); + + confirmApplyPage = new ConfirmApplyPage(wizardState, navigator, contentWidth, contentHeight); + navigator.addPage(confirmApplyPage); + + for (BaseWizardPage page : navigator.getPages()) { + page.preloadAssets(); + } + } + + /** Updates the size of each existing page after a window resize, then re-enters the current page. */ + private void resizePages() { + int contentWidth = width - PANEL_PADDING * 2; + int contentHeight = height - HEADER_HEIGHT - FOOTER_HEIGHT; + + for (BaseWizardPage page : navigator.getPages()) { + page.setWidth(contentWidth); + page.setHeight(contentHeight); + } + + // Re-enter the current page so it rebuilds its child components at the new size + navigator.getCurrentPage().onEnter(); + } + + private void buildLayout() { + headerComponent = new WizardHeaderComponent(0, 0, width, HEADER_HEIGHT, navigator); + addComponent(headerComponent); + + WizardContentPanel contentPanel = new WizardContentPanel( + PANEL_PADDING, HEADER_HEIGHT, + width - PANEL_PADDING * 2, + height - HEADER_HEIGHT - FOOTER_HEIGHT, + navigator + ); + addComponent(contentPanel); + + buttonBar = new WizardButtonBar(navigator, width, FOOTER_HEIGHT); + + EmptyComponent footerWrapper = new EmptyComponent(0, height - FOOTER_HEIGHT, width, FOOTER_HEIGHT); + footerWrapper.addComponent(buttonBar); + addComponent(footerWrapper); + } + + private void wireEvents() { + // When apply succeeds, unlock the Finish button + confirmApplyPage.setOnApplySucceeded(() -> buttonBar.setFinishEnabled(true)); + // Re-apply persisted confirm state after screen init/reload. + buttonBar.setFinishEnabled(confirmApplyPage.isApplyCompleted()); + + // Finish — settings have been applied; mark complete and close + buttonBar.setOnFinish(() -> { + markWizardComplete(); + boolean openedFromTitle = lastScreen instanceof TitleScreen; + if (openedFromTitle) { + Minecraft.getInstance().setScreen(resolvePostWizardScreen()); + } else { + Minecraft.getInstance().setScreen(lastScreen); + } + }); + + // Skip on the last page — close without applying; still marks complete + buttonBar.setOnSkipFinish(() -> { + markWizardComplete(); + Minecraft.getInstance().setScreen(lastScreen); + }); + + navigator.setOnPageChange(event -> { + headerComponent.onPageChanged(); + buttonBar.refresh(); + }); + } + + /** Writes the wizard-complete flag to the config and saves it. */ + private void markWizardComplete() { + PackCoreConfig.successfulWelcomeWizard = true; + MidnightConfig.write(MOD_ID); + } + + private Screen resolvePostWizardScreen() { + return switch (PackCoreConfig.menuStyle) { + case MODERN -> new SBETitleScreen(); + case MODERN_MINIMAL -> new SBETitleScreen(false); + case MINIMAL -> new PackCoreTitleScreen(); + }; + } + + @Override + public boolean shouldCloseOnEsc() { + if (navigator.hasPrevious()) { + navigator.previousPage(); + return false; + } + return true; + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/BackupsPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/BackupsPage.java new file mode 100644 index 0000000..88ae2c8 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/BackupsPage.java @@ -0,0 +1,185 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.configpack.BackupEntry; +import com.github.kd_gaming1.packcore.configpack.BackupManager; +import com.github.kd_gaming1.packcore.gui.component.RestoreConfirmOverlay; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class BackupsPage extends BaseConfigPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/BackupsPage"); + + private static final Executor IO_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + + private static final int PADDING = 10; + private static final int BUTTON_HEIGHT = 18; + private static final int BUTTON_WIDTH = 120; + private static final int CARD_GAP = 6; + private static final int CARD_PADDING = 8; + private static final int RESTORE_BTN_WIDTH = 70; + + private RestoreConfirmOverlay restoreOverlay; + private ScrollContainerWidget backupListScroll; + private CustomButtonWidget createBackupButton; + + public BackupsPage(int width, int height) { + super(width, height); + } + + @Override + public void onEnter() { + clearComponents(); + + var font = Minecraft.getInstance().font; + + addComponent(new TextComponent(PADDING, PADDING, + Component.translatable("gui.packcore.backups.heading"), GuiColors.NAME_DEFAULT)); + + int createBtnX = (getWidth() - BUTTON_WIDTH) / 2; + int createBtnY = getHeight() - BUTTON_HEIGHT - PADDING; + createBackupButton = new CustomButtonWidget(createBtnX, createBtnY, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.backups.button.create"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> createBackupAsync()); + addWidget(createBackupButton); + + restoreOverlay = new RestoreConfirmOverlay(getWidth(), getHeight()); + restoreOverlay.setOnClose(() -> { + if (backupListScroll != null) backupListScroll.active = true; + if (createBackupButton != null) createBackupButton.visible = true; + }); + + buildBackupList(PADDING + font.lineHeight + PADDING); + addComponent(restoreOverlay); + } + + private void buildBackupList(int startY) { + List backups; + try { + backups = BackupManager.listBackups(); + } catch (IOException e) { + LOGGER.error("Failed to list backups: {}", e.getMessage()); + addComponent(new TextComponent(PADDING, startY, + Component.literal("Error reading backups."), GuiColors.ERROR)); + return; + } + + int listHeight = getHeight() - startY - BUTTON_HEIGHT - PADDING * 2; + int listWidth = getWidth() - PADDING * 3; + + backupListScroll = new ScrollContainerWidget(listWidth, listHeight, CARD_GAP); + EmptyComponent container = new EmptyComponent(0, 0, listWidth - 8, 0); + + var font = Minecraft.getInstance().font; + int cardHeight = CARD_PADDING * 2 + font.lineHeight; + int containerHeight = 0; + + if (backups.isEmpty()) { + container.addComponent(new TextComponent(0, 0, + Component.translatable("gui.packcore.backups.empty"), GuiColors.TEXT_HINT)); + containerHeight = font.lineHeight; + } + + for (int i = 0; i < backups.size(); i++) { + BackupEntry backup = backups.get(i); + int cardY = i * (cardHeight + CARD_GAP); + + container.addComponent(new BackupCard(0, cardY, listWidth - 8, cardHeight, backup)); + container.addWidget(new CustomButtonWidget( + listWidth - 8 - RESTORE_BTN_WIDTH - CARD_PADDING, + cardY + (cardHeight - BUTTON_HEIGHT) / 2, + RESTORE_BTN_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.backups.button.restore"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> { + if (backupListScroll != null) backupListScroll.active = false; + if (createBackupButton != null) createBackupButton.visible = false; + restoreOverlay.show(backup); + })); + containerHeight = cardY + cardHeight; + } + + container.setHeight(containerHeight); + backupListScroll.addComponent(container); + + EmptyComponent scrollWrapper = new EmptyComponent(PADDING, startY, listWidth, listHeight); + scrollWrapper.addWidget(backupListScroll); + addComponent(scrollWrapper); + } + + /** Disables the button immediately, runs the backup on a background thread, then refreshes the page. */ + private void createBackupAsync() { + createBackupButton.active = false; + + CompletableFuture.runAsync( + () -> { + try { + BackupManager.createBackup(FabricLoader.getInstance().getGameDir()); + } catch (IOException e) { + LOGGER.error("Failed to create backup: {}", e.getMessage()); + } + }, + IO_EXECUTOR + ).thenRun(() -> Minecraft.getInstance().execute(() -> { + onEnter(); + updateParentPosition(getParentX(), getParentY(), getWidth(), getHeight()); + })); + } + + public boolean handleEsc() { + if (restoreOverlay != null && restoreOverlay.isVisible()) { + restoreOverlay.setVisible(false); + return true; + } + return false; + } + + private static class BackupCard extends AbstractComponent { + + private final BackupEntry backup; + + BackupCard(int x, int y, int width, int height, BackupEntry backup) { + super(x, y, width, height); + this.backup = backup; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + + graphics.fill(x, y, x + w, y + h, GuiColors.ROW_BACKGROUND); + GuiHelper.drawBorder(graphics, x, y, w, h, GuiColors.BORDER_IDLE); + + var font = Minecraft.getInstance().font; + int textY = y + (h - font.lineHeight) / 2; + + graphics.drawString(font, backup.zipPath().getFileName().toString(), + x + CARD_PADDING, textY, GuiColors.NAME_DEFAULT, false); + + String timestamp = backup.displayName(); + int timestampX = x + w - RESTORE_BTN_WIDTH - CARD_PADDING - font.width(timestamp) - 8; + graphics.drawString(font, timestamp, timestampX, textY, GuiColors.TEXT_SECONDARY, false); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/BaseConfigPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/BaseConfigPage.java new file mode 100644 index 0000000..a234e01 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/BaseConfigPage.java @@ -0,0 +1,16 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.EmptyComponent; + +public abstract class BaseConfigPage extends EmptyComponent { + + public BaseConfigPage(int width, int height) { + super(0, 0, width, height); + } + + /** Called when this page becomes visible. Build all child components here. */ + public abstract void onEnter(); + + /** Called when navigating away. Override for cleanup if needed. */ + public void onExit() {} +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigContentPanel.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigContentPanel.java new file mode 100644 index 0000000..7203eeb --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigContentPanel.java @@ -0,0 +1,30 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.minecraft.client.gui.GuiGraphics; + +public class ConfigContentPanel extends AbstractComponent { + + public ConfigContentPanel(int x, int y, int width, int height) { + super(x, y, width, height); + } + + public void setPage(BaseConfigPage page) { + clearComponents(); + addComponent(page); + updateParentPosition(getParentX(), getParentY(), getWidth(), getHeight()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + + graphics.fill(x, y, x + w, y + h, GuiColors.PANEL_BACKGROUND); + GuiHelper.drawBorder(graphics, x, y, w, h, GuiColors.PANEL_BORDER); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigScreen.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigScreen.java new file mode 100644 index 0000000..a177938 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigScreen.java @@ -0,0 +1,104 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.AbstractScreen; +import com.github.kd_gaming1.packcore.gui.util.ImageBackground; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class ConfigScreen extends AbstractScreen { + + private static final int HEADER_HEIGHT = 40; + private static final int NAV_HEIGHT = 24; + private static final int PANEL_PADDING = 8; + private static final int CONTENT_MARGIN = 4; + + private ConfigTab activeTab = ConfigTab.CONFIGURATION; + + private ConfigurationPage configPage; + private ExportPage exportPage; + private BackupsPage backupsPage; + private ImportPage importPage; + + private ConfigContentPanel contentPanel; + private TabNavBar navBar; + + public ConfigScreen() { + super(Component.translatable("gui.packcore.config.title")); + setBackground(new ImageBackground( + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/welcome_bg.png"), + 1920, 1080, + ImageBackground.BackgroundMode.STRETCH + )); + } + + @Override + protected void init() { + int contentWidth = width; + int contentHeight = height - HEADER_HEIGHT - NAV_HEIGHT - CONTENT_MARGIN * 2; + + boolean firstInit = configPage == null; + + if (firstInit) { + configPage = new ConfigurationPage(contentWidth, contentHeight); + exportPage = new ExportPage(contentWidth, contentHeight); + backupsPage = new BackupsPage(contentWidth, contentHeight); + importPage = new ImportPage(contentWidth, contentHeight); + getCurrentPage().onEnter(); + } else { + resizePages(contentWidth, contentHeight); + } + + addComponent(new ConfigScreenHeader(0, 0, width, HEADER_HEIGHT, () -> Minecraft.getInstance().setScreen(null))); + + contentPanel = new ConfigContentPanel( + PANEL_PADDING, HEADER_HEIGHT, + width - PANEL_PADDING * 2, + height - HEADER_HEIGHT - NAV_HEIGHT - CONTENT_MARGIN + ); + contentPanel.setPage(getCurrentPage()); + addComponent(contentPanel); + + navBar = new TabNavBar(PANEL_PADDING, height - NAV_HEIGHT, width - PANEL_PADDING * 2, NAV_HEIGHT, activeTab, this::switchTab); + addComponent(navBar); + + super.init(); + } + + private void resizePages(int width, int height) { + for (BaseConfigPage page : allPages()) { + page.setWidth(width); + page.setHeight(height); + } + getCurrentPage().onEnter(); + } + + private void switchTab(ConfigTab tab) { + if (tab == activeTab) return; + getCurrentPage().onExit(); + activeTab = tab; + navBar.setActiveTab(tab); + getCurrentPage().onEnter(); + contentPanel.setPage(getCurrentPage()); + } + + private BaseConfigPage getCurrentPage() { + return switch (activeTab) { + case CONFIGURATION -> configPage; + case EXPORT -> exportPage; + case BACKUPS -> backupsPage; + case IMPORT -> importPage; + }; + } + + private BaseConfigPage[] allPages() { + return new BaseConfigPage[]{ configPage, exportPage, backupsPage, importPage }; + } + + @Override + public boolean shouldCloseOnEsc() { + return activeTab != ConfigTab.BACKUPS || !backupsPage.handleEsc(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigScreenHeader.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigScreenHeader.java new file mode 100644 index 0000000..81432ca --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigScreenHeader.java @@ -0,0 +1,90 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.sprite.SpriteComponent; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class ConfigScreenHeader extends AbstractComponent { + + private static final int LOGO_ORIGINAL_SIZE = 640; + private static final int LOGO_PADDING_X = 10; + private static final int LOGO_PADDING_Y = 6; + + private static final int COLOR_LABEL = 0xFFCCCCCC; + private static final int COLOR_ACCENT = 0xFF2196F3; + private static final int COLOR_VALUE = 0xFFAAAAAA; + + private static final int CLOSE_BUTTON_SIZE = 16; + private static final int CLOSE_BUTTON_MARGIN = 8; + private static final int TEXT_PADDING_RIGHT = CLOSE_BUTTON_SIZE + CLOSE_BUTTON_MARGIN * 2; + + private static final WidgetSprites CLOSE_SPRITES = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/x"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/x"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/xhover") + ); + + public ConfigScreenHeader(int x, int y, int width, int height, Runnable onClose) { + super(x, y, width, height); + + int maxLogoHeight = height - LOGO_PADDING_Y * 2; + int scaledLogoWidth = (LOGO_ORIGINAL_SIZE * maxLogoHeight) / LOGO_ORIGINAL_SIZE; + addComponent(new SpriteComponent( + LOGO_PADDING_X, LOGO_PADDING_Y, + scaledLogoWidth, maxLogoHeight, + Identifier.fromNamespaceAndPath(MOD_ID, "assets/sbe_logo") + )); + + int closeBtnX = width - CLOSE_BUTTON_SIZE - CLOSE_BUTTON_MARGIN; + int closeBtnY = (height - CLOSE_BUTTON_SIZE) / 2; + addWidget(new CustomButtonWidget( + closeBtnX, closeBtnY, + CLOSE_BUTTON_SIZE, CLOSE_BUTTON_SIZE, + Component.empty(), + CLOSE_SPRITES, + b -> onClose.run() + )); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + + var font = Minecraft.getInstance().font; + + String packName = resolvePackName(); + String version = PackCoreConfig.lastAppliedVersion.isBlank() ? "—" : "v" + PackCoreConfig.lastAppliedVersion; + String modpackVersion = "v" + ModpackMetadata.getInstance().getModpackVersion(); + String labelText = "Active Config"; + String detailText = " · " + version + " · Modpack " + modpackVersion; + + int totalTextHeight = font.lineHeight * 2 + 2; + int firstLineY = y + (h - totalTextHeight) / 2; + int secondLineY = firstLineY + font.lineHeight + 2; + int rightEdge = x + w - TEXT_PADDING_RIGHT; + + graphics.drawString(font, labelText, rightEdge - font.width(labelText), firstLineY, COLOR_LABEL, false); + + int packNameX = rightEdge - font.width(packName + detailText); + graphics.drawString(font, packName, packNameX, secondLineY, COLOR_ACCENT, false); + graphics.drawString(font, detailText, packNameX + font.width(packName), secondLineY, COLOR_VALUE, false); + } + + private static String resolvePackName() { + String file = PackCoreConfig.lastAppliedPackFile; + if (file == null || file.isBlank()) return "None"; + return file.endsWith(".zip") ? file.substring(0, file.length() - 4) : file; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigTab.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigTab.java new file mode 100644 index 0000000..ad65257 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigTab.java @@ -0,0 +1,22 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import net.minecraft.network.chat.Component; + +public enum ConfigTab { + CONFIGURATION(0, "gui.packcore.config.tab.configuration"), + EXPORT(1, "gui.packcore.config.tab.export"), + BACKUPS(2, "gui.packcore.config.tab.backups"), + IMPORT(3, "gui.packcore.config.tab.import"); + + private final int index; + private final String translationKey; + + ConfigTab(int index, String translationKey) { + this.index = index; + this.translationKey = translationKey; + } + + public int index() { return index; } + public Component label() { return Component.translatable(translationKey); } + public static ConfigTab[] ordered() { return values(); } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigurationPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigurationPage.java new file mode 100644 index 0000000..047b46a --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ConfigurationPage.java @@ -0,0 +1,286 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.configpack.ConfigPackExtractor; +import com.github.kd_gaming1.packcore.configpack.ConfigPackScanner; +import com.github.kd_gaming1.packcore.gui.component.*; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import eu.midnightdust.lib.config.MidnightConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class ConfigurationPage extends BaseConfigPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ConfigurationPage"); + + private static final int PANEL_GAP = 12; + private static final int PADDING = 10; + private static final int BUTTON_HEIGHT = 18; + private static final int BUTTON_WIDTH = 110; + private static final int BUTTON_GAP = 8; + private static final int LABEL_GAP = 6; + private static final int SUB_TAB_HEIGHT = 22; + + private static final int COLOR_LABEL = 0xFFCCCCCC; + private static final int COLOR_HINT = 0xFF666666; + private static final int COLOR_ERROR = 0xFFFF5555; + + private ConfigPackEntry selectedPreset; + private EmptyComponent leftPanel; + private int panelWidth; + private double rightPanelScrollAmount = 0; + + private enum PresetsSource { OFFICIAL, MY_EXPORTS } + private PresetsSource presetsSource = PresetsSource.OFFICIAL; + + // Cached per page-open session; invalidated on enter and on source switch + private List cachedPacks = null; + private PresetsSource cachedPacksSource = null; + + public ConfigurationPage(int width, int height) { + super(width, height); + } + + @Override + public void onEnter() { + this.clearComponents(); + cachedPacks = null; + cachedPacksSource = null; + + panelWidth = (getWidth() - PANEL_GAP) / 2; + int panelHeight = getHeight(); + + leftPanel = new EmptyComponent(0, 0, panelWidth, panelHeight); + EmptyComponent rightPanel = new EmptyComponent(panelWidth + PANEL_GAP, 0, panelWidth, panelHeight); + + buildLeftPanel(); + buildRightPanel(rightPanel); + + this.addComponent(leftPanel); + this.addComponent(rightPanel); + } + + private void buildLeftPanel() { + leftPanel.clearComponents(); + var font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + int currentY = PADDING; + + leftPanel.addComponent(new TextComponent(PADDING, currentY, + Component.translatable("gui.packcore.config.files.heading"), COLOR_LABEL)); + currentY += lineHeight + LABEL_GAP; + + if (selectedPreset == null) { + leftPanel.addComponent(new MultiLineTextComponent(PADDING, currentY, (getWidth() - PANEL_GAP) / 2, + Component.translatable("gui.packcore.config.files.hint"), COLOR_HINT)); + return; + } + + int buttonsAreaHeight = BUTTON_HEIGHT + PADDING * 2; + int treeHeight = getHeight() - currentY - buttonsAreaHeight; + + FileTreeNode treeRoot; + try { + treeRoot = FileTreeBuilder.fromZip(selectedPreset.zipPath()); + } catch (IOException e) { + LOGGER.error("Failed to read zip for file tree: {}", e.getMessage()); + leftPanel.addComponent(new TextComponent(PADDING, currentY, + Component.literal("Error reading preset."), COLOR_ERROR)); + return; + } + + FileTreeComponent fileTree = new FileTreeComponent(PADDING, currentY, panelWidth - PADDING * 2, treeHeight, treeRoot); + leftPanel.addComponent(fileTree); + + int panelHeight = leftPanel.getHeight(); + int buttonsY = panelHeight - PADDING - BUTTON_HEIGHT; + int totalBtnWidth = BUTTON_WIDTH * 2 + BUTTON_GAP; + int buttonsStartX = (panelWidth - totalBtnWidth) / 2; + + CustomButtonWidget applySelectedBtn = new CustomButtonWidget( + buttonsStartX, buttonsY, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.config.button.apply_selected"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> applyFiles(fileTree.getSelectedPaths())); + applySelectedBtn.active = false; + applySelectedBtn.setTooltip(Tooltip.create( + Component.translatable("gui.packcore.config.tooltip.apply_selected"))); + + CustomButtonWidget applyAllBtn = new CustomButtonWidget( + buttonsStartX + BUTTON_WIDTH + BUTTON_GAP, buttonsY, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.config.button.apply_all"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> applyAll()); + applyAllBtn.setTooltip(Tooltip.create( + Component.translatable("gui.packcore.config.tooltip.apply_all"))); + + fileTree.setOnSelectionChanged(() -> + applySelectedBtn.active = !fileTree.getSelectedPaths().isEmpty()); + + leftPanel.addWidget(applySelectedBtn); + leftPanel.addWidget(applyAllBtn); + } + + private void buildRightPanel(EmptyComponent panel) { + panel.clearComponents(); + + var font = Minecraft.getInstance().font; + int tabWidth = (panelWidth - PADDING * 4) / 2; + + // Sub-tab bar + panel.addComponent(new AbstractComponent(PADDING, PADDING, panelWidth - PADDING * 2, SUB_TAB_HEIGHT) { + @Override + public void render(GuiGraphics g, int mx, int my, float pt, int pw, int ph) { + int bx = getTotalX(), by = getTotalY(), bh = getHeight(); + for (int i = 0; i < 2; i++) { + boolean active = (i == 0) == (presetsSource == PresetsSource.OFFICIAL); + String label = i == 0 + ? Component.translatable("gui.packcore.config.source.official").getString() + : Component.translatable("gui.packcore.config.source.my_exports").getString(); + int tx = bx + i * tabWidth; + int color = active ? 0xFFFFFFFF : 0xFF888888; + g.drawString(font, label, tx + (tabWidth - font.width(label)) / 2, + by + (bh - font.lineHeight) / 2, color, false); + if (active) + g.fill(tx, by + bh - 2, tx + tabWidth, by + bh, 0xFF2196F3); + } + } + }); + + // Tab click hit-areas + for (int i = 0; i < 2; i++) { + final PresetsSource src = (i == 0) ? PresetsSource.OFFICIAL : PresetsSource.MY_EXPORTS; + int btnX = PADDING + i * tabWidth; + panel.addWidget(new ButtonWidget(btnX, PADDING, tabWidth, SUB_TAB_HEIGHT, + Component.empty(), b -> { + if (presetsSource != src) { + presetsSource = src; + selectedPreset = null; + cachedPacks = null; + cachedPacksSource = null; + buildLeftPanel(); + buildRightPanel(panel); + leftPanel.updateParentPosition(leftPanel.getParentX(), leftPanel.getParentY(), getWidth(), getHeight()); + panel.updateParentPosition(panel.getParentX(), panel.getParentY(), getWidth(), getHeight()); + } + }) { + @Override protected void renderContents(@NonNull GuiGraphics graphics, int mx, int my, float pt) {} + }); + } + + int listY = PADDING + SUB_TAB_HEIGHT + LABEL_GAP; + int listHeight = getHeight() - listY - PADDING; + + List packs = getScannedPacks(); + + ScrollContainerWidget scroll = new ScrollContainerWidget(panelWidth - PADDING * 3, listHeight); + EmptyComponent scrollWrapper = new EmptyComponent(PADDING, listY, panelWidth - PADDING * 2, listHeight); + EmptyComponent container = new EmptyComponent(0, 0, panelWidth - PADDING * 2, 0); + + int y = 0; + + for (ConfigPackEntry pack : packs) { + boolean isActive = selectedPreset != null && selectedPreset.zipPath().equals(pack.zipPath()); + ConfigPackCard card = new ConfigPackCard( + 0, y, panelWidth - PADDING * 4, pack, isActive, + null, + clicked -> { + rightPanelScrollAmount = scroll.scrollAmount(); + selectedPreset = clicked; + buildLeftPanel(); + buildRightPanel(panel); + leftPanel.updateParentPosition( + leftPanel.getParentX(), leftPanel.getParentY(), + getWidth(), getHeight()); + panel.updateParentPosition( + panel.getParentX(), panel.getParentY(), + getWidth(), getHeight()); + } + ); + container.addComponent(card); + y += card.getHeight() + 6; + } + + container.setHeight(y); + scroll.addComponent(container); + scroll.setScrollAmount(rightPanelScrollAmount); + scrollWrapper.addWidget(scroll); + panel.addComponent(scrollWrapper); + } + + /** Returns cached packs for the current source, scanning from disk only when the cache is stale. */ + private List getScannedPacks() { + if (cachedPacks == null || cachedPacksSource != presetsSource) { + cachedPacks = scanPacks(); + cachedPacksSource = presetsSource; + } + return cachedPacks; + } + + private void applyFiles(List paths) { + if (selectedPreset == null || paths.isEmpty()) return; + try { + ConfigPackExtractor.extractSelective(selectedPreset.zipPath(), PackCore.PACKCORE_DIR, + ConfigPackExtractor.OverwriteMode.REPLACE_EXISTING, paths); + saveAppliedState(); + } catch (IOException e) { + LOGGER.error("Failed to apply selected files: {}", e.getMessage()); + } + } + + private void applyAll() { + if (selectedPreset == null) { + LOGGER.warn("Apply All clicked without a selected preset (source={})", presetsSource); + return; + } + + String pendingFile = selectedPreset.zipPath().getFileName().toString(); + LOGGER.info("Scheduling config apply on restart: file='{}', source={}", pendingFile, presetsSource); + + PackCoreConfig.pendingConfigPack = pendingFile; + MidnightConfig.write(MOD_ID); + + LOGGER.info("Pending config saved, stopping client to apply on next launch."); + Minecraft.getInstance().stop(); + } + + private void saveAppliedState() { + if (selectedPreset.config().has("version")) { + PackCoreConfig.lastAppliedVersion = selectedPreset.config().get("version").getAsString(); + } + PackCoreConfig.lastAppliedPackFile = selectedPreset.zipPath().getFileName().toString(); + MidnightConfig.write(MOD_ID); + } + + private List scanPacks() { + Path dir = presetsSource == PresetsSource.OFFICIAL + ? PackCore.PACKCORE_DIR.resolve("configs") + : PackCore.PACKCORE_DIR.resolve("user_configs"); + try { + return new ConfigPackScanner().scanFolder(dir); + } catch (IOException e) { + LOGGER.error("Failed to scan packs: {}", e.getMessage()); + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ExportPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ExportPage.java new file mode 100644 index 0000000..3eb96a3 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ExportPage.java @@ -0,0 +1,365 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.daqem.uilib.gui.widget.EditBoxWidget; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.daqem.uilib.util.ValidationErrors; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.configpack.ConfigPackBuilder; +import com.github.kd_gaming1.packcore.configpack.ConfigPackMeta; +import com.github.kd_gaming1.packcore.gui.component.FileTreeBuilder; +import com.github.kd_gaming1.packcore.gui.component.FileTreeComponent; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import com.github.kd_gaming1.packcore.util.ScreenResolution; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Function; + +public class ExportPage extends BaseConfigPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ExportPage"); + + private static final Path EXPORTS_DIR = PackCore.PACKCORE_DIR.resolve("user_configs"); + + private static final Executor IO_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + + private static final int PANEL_GAP = 12; + private static final int PADDING = 10; + private static final int FIELD_HEIGHT = 20; + private static final int FIELD_GAP = 6; + private static final int LABEL_GAP = 3; + private static final int BUTTON_WIDTH = 120; + private static final int OPEN_BTN_WIDTH = 160; + private static final int BUTTON_HEIGHT = 18; + + private static final int COLOR_LABEL = 0xFFCCCCCC; + private static final int COLOR_SECTION = 0xFF888888; + private static final int COLOR_HINT = 0xFF666666; + private static final int COLOR_ERROR = 0xFFFF5555; + + /** + * Top-level folders and files hidden from the export file tree. + * Dot-folders (e.g. .fabric, .git) are hidden automatically by FileTreeBuilder. + */ + private static final Set HIDDEN_PATHS = Set.of( + "logs", "crash-reports", "screenshots", "saves", "packcore", + "replay_recordings", "debug", "resourcepacks", "shaderpacks", + "cache", "data", "datapacks", "downloads", "meowdding-repo-cache", + "moddata", "mods", "skyblock-repo-cache", "command_history.txt", "icon.png", + "usercache", "realms_persistence.json", "config/firmament/profiles", "config/skyocean/data", + "config/skyhanni/repo", "config/skyhanni/logs", "config/skyhanni/backup", + "config/skyblocker/item-repo", "config/skyblocker/config_backups", + "config/skyblocker/backpack-preview", "config/SBO", "config/notenoughupdates" + ); + + private EditBoxWidget nameField; + private EditBoxWidget versionField; + private EditBoxWidget authorField; + private EditBoxWidget descriptionField; + private EditBoxWidget resolutionField; + private EditBoxWidget guiScaleField; + private FileTreeComponent fileTree; + private CustomButtonWidget exportBtn; + + public ExportPage(int width, int height) { + super(width, height); + } + + @Override + public void onEnter() { + this.clearComponents(); + fileTree = null; + + int panelWidth = (getWidth() - PANEL_GAP) / 2; + + EmptyComponent leftPanel = new EmptyComponent(0, 0, panelWidth, getHeight()); + EmptyComponent rightPanel = new EmptyComponent(panelWidth + PANEL_GAP, 0, panelWidth, getHeight()); + + buildLeftPanel(leftPanel, panelWidth); + buildRightPanel(rightPanel, panelWidth); + + this.addComponent(leftPanel); + this.addComponent(rightPanel); + } + + private void buildLeftPanel(EmptyComponent panel, int width) { + var font = Minecraft.getInstance().font; + int fieldWidth = width - PADDING * 2; + int fieldStride = font.lineHeight + LABEL_GAP + FIELD_HEIGHT + FIELD_GAP; + + panel.addComponent(new TextComponent(PADDING, PADDING, + Component.translatable("gui.packcore.export.metadata.heading"), COLOR_LABEL)); + + int scrollY = PADDING + font.lineHeight + FIELD_GAP; + int scrollHeight = getHeight() - scrollY - BUTTON_HEIGHT - PADDING * 2; + + EmptyComponent container = new EmptyComponent(0, 0, width, 0); + int currentY = PADDING; + + nameField = addValidatedField(container, fieldWidth, currentY, + "gui.packcore.export.field.name", "", input -> { + if (input.isBlank()) return List.of(ValidationErrors.minLength(1)); + return List.of(); + }); + currentY += fieldStride; + + versionField = addValidatedField(container, fieldWidth, currentY, + "gui.packcore.export.field.version", "1.0.0", input -> { + if (!input.matches("\\d+\\.\\d+(\\.\\d+)?(-.*)?")) + return List.of(ValidationErrors.pattern("e.g. 1.0.0")); + return List.of(); + }); + currentY += fieldStride; + + authorField = addPlainField(container, fieldWidth, currentY, + "gui.packcore.export.field.author", ModpackMetadata.getInstance().getAuthor()); + currentY += fieldStride; + + descriptionField = addPlainField(container, fieldWidth, currentY, + "gui.packcore.export.field.description", ""); + currentY += fieldStride; + + ScreenResolution.ScreenSize screenSize = ScreenResolution.detect(); + resolutionField = addValidatedField(container, fieldWidth, currentY, + "gui.packcore.export.field.resolution", screenSize.width() + "x" + screenSize.height(), input -> { + if (!input.matches("\\d+[x×]\\d+")) + return List.of(ValidationErrors.pattern("e.g. 1920x1080")); + return List.of(); + }); + currentY += fieldStride; + + int currentGuiScale = Minecraft.getInstance().options.guiScale().get(); + guiScaleField = addValidatedField(container, fieldWidth, currentY, + "gui.packcore.export.field.gui_scale", String.valueOf(currentGuiScale), input -> { + try { + int value = Integer.parseInt(input.trim()); + if (value < 0) return List.of(ValidationErrors.pattern("Must be 0 or greater")); + } catch (NumberFormatException e) { + return List.of(ValidationErrors.pattern("Must be a whole number")); + } + return List.of(); + }); + currentY += fieldStride; + + container.setHeight(currentY + PADDING); + + ScrollContainerWidget scroll = new ScrollContainerWidget(width - PADDING * 2, scrollHeight); + scroll.addComponent(container); + EmptyComponent scrollWrapper = new EmptyComponent(PADDING, scrollY, width - PADDING, scrollHeight); + scrollWrapper.addWidget(scroll); + panel.addComponent(scrollWrapper); + + int exportBtnX = (fieldWidth - BUTTON_WIDTH) / 2; + int exportBtnY = scrollY + scrollHeight + PADDING; + exportBtn = new CustomButtonWidget(exportBtnX, exportBtnY, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.export.button.export"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> doExport()); + exportBtn.active = false; + panel.addWidget(exportBtn); + } + + /** Builds the right panel immediately with a loading placeholder, then fills the tree async. */ + private void buildRightPanel(EmptyComponent panel, int width) { + var font = Minecraft.getInstance().font; + int currentY = PADDING; + + panel.addComponent(new TextComponent(PADDING, currentY, + Component.translatable("gui.packcore.export.files.heading"), COLOR_LABEL)); + currentY += font.lineHeight + FIELD_GAP; + + int treeHeight = getHeight() - currentY - BUTTON_HEIGHT - PADDING * 2; + + // Slot that will be swapped out once the async scan finishes + EmptyComponent treeSlot = new EmptyComponent(PADDING, currentY, width - PADDING * 2, treeHeight); + treeSlot.addComponent(new TextComponent(0, 0, Component.literal("Loading files…"), COLOR_HINT)); + panel.addComponent(treeSlot); + + int openBtnX = PADDING + (width - PADDING * 2 - OPEN_BTN_WIDTH) / 2; + int openBtnY = currentY + treeHeight + PADDING; + panel.addWidget(new CustomButtonWidget(openBtnX, openBtnY, OPEN_BTN_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.export.button.open_folder"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> openExportsFolder())); + + final int capturedTreeWidth = width - PADDING * 3; + final int capturedTreeHeight = treeHeight; + + CompletableFuture.supplyAsync( + () -> { + try { + return FileTreeBuilder.fromDirectory(FabricLoader.getInstance().getGameDir(), HIDDEN_PATHS); + } catch (IOException e) { + LOGGER.error("Failed to build directory tree: {}", e.getMessage()); + return null; + } + }, + IO_EXECUTOR + ).thenAccept(root -> Minecraft.getInstance().execute(() -> { + treeSlot.clearComponents(); + treeSlot.clearOnlyWidgets(); + + if (root == null) { + treeSlot.addComponent(new TextComponent(0, 0, + Component.literal("Error reading game folder."), COLOR_ERROR)); + } else { + fileTree = new FileTreeComponent(0, 0, capturedTreeWidth, capturedTreeHeight, root); + fileTree.setOnSelectionChanged(this::updateExportButton); + treeSlot.addComponent(fileTree); + updateExportButton(); + } + + treeSlot.updateParentPosition( + treeSlot.getParentX(), treeSlot.getParentY(), + treeSlot.getWidth(), treeSlot.getHeight()); + })); + } + + private EditBoxWidget addValidatedField(EmptyComponent panel, int fieldWidth, int y, + String labelKey, String defaultValue, + Function> validator) { + var font = Minecraft.getInstance().font; + panel.addComponent(new TextComponent(PADDING, y, Component.translatable(labelKey), COLOR_SECTION)); + + EditBoxWidget field = new EditBoxWidget(font, PADDING, y + font.lineHeight + LABEL_GAP, + fieldWidth - PADDING * 2, FIELD_HEIGHT, Component.translatable(labelKey)) { + @Override + public List validateInput(String input) { + List errors = new ArrayList<>(validator.apply(input)); + updateExportButton(); + return errors; + } + }; + field.setValue(defaultValue); + panel.addWidget(field); + return field; + } + + private EditBoxWidget addPlainField(EmptyComponent panel, int fieldWidth, int y, + String labelKey, String defaultValue) { + var font = Minecraft.getInstance().font; + panel.addComponent(new TextComponent(PADDING, y, Component.translatable(labelKey), COLOR_SECTION)); + + EditBoxWidget field = new EditBoxWidget(font, PADDING, y + font.lineHeight + LABEL_GAP, + fieldWidth - PADDING * 2, FIELD_HEIGHT, Component.translatable(labelKey)); + field.setValue(defaultValue); + panel.addWidget(field); + return field; + } + + private void updateExportButton() { + if (exportBtn == null) return; + + boolean nameOk = nameField != null && !nameField.getValue().isBlank(); + boolean versionOk = versionField != null && versionField.getValue().matches("\\d+\\.\\d+(\\.\\d+)?(-.*)?"); + boolean resOk = resolutionField != null && resolutionField.getValue().matches("\\d+[x×]\\d+"); + boolean guiScaleOk = guiScaleField != null && isValidGuiScale(guiScaleField.getValue()); + boolean filesSelected = fileTree != null && !fileTree.getSelectedPaths().isEmpty(); + + exportBtn.active = nameOk && versionOk && resOk && guiScaleOk && filesSelected; + + if (exportBtn.active) { + exportBtn.setTooltip(null); + return; + } + + List reasons = new ArrayList<>(); + if (!nameOk) reasons.add(Component.translatable("gui.packcore.export.tooltip.missing_name")); + if (!versionOk) reasons.add(Component.translatable("gui.packcore.export.tooltip.missing_version")); + if (!resOk) reasons.add(Component.translatable("gui.packcore.export.tooltip.missing_resolution")); + if (!guiScaleOk) reasons.add(Component.translatable("gui.packcore.export.tooltip.missing_gui_scale")); + if (!filesSelected) reasons.add(Component.translatable("gui.packcore.export.tooltip.missing_files")); + + var tooltipText = Component.empty(); + for (int i = 0; i < reasons.size(); i++) { + if (i > 0) tooltipText.append("\n"); + tooltipText.append(reasons.get(i)); + } + + exportBtn.setTooltip(Tooltip.create(tooltipText)); + } + + private static boolean isValidGuiScale(String input) { + try { + return Integer.parseInt(input.trim()) >= 0; + } catch (NumberFormatException e) { + return false; + } + } + + private void doExport() { + if (nameField == null || nameField.getValue().isBlank()) return; + + String name = nameField.getValue().trim(); + String version = versionField != null ? versionField.getValue().trim() : "1.0.0"; + String author = authorField != null ? authorField.getValue().trim() : ""; + String description = descriptionField != null ? descriptionField.getValue().trim() : ""; + String resolution = resolutionField != null ? resolutionField.getValue().trim() : "1920x1080"; + + int targetWidth = 1920; + int targetHeight = 1080; + try { + String[] parts = resolution.split("[x×]"); + if (parts.length == 2) { + targetWidth = Integer.parseInt(parts[0].trim()); + targetHeight = Integer.parseInt(parts[1].trim()); + } + } catch (NumberFormatException ignored) {} + + int guiScale = 0; + if (guiScaleField != null) { + try { + guiScale = Integer.parseInt(guiScaleField.getValue().trim()); + } catch (NumberFormatException ignored) {} + } + + ConfigPackMeta meta = ConfigPackMeta.builder(version, targetWidth, targetHeight, guiScale) + .name(name) + .description(description.isEmpty() ? null : description) + .author(author.isEmpty() ? null : author) + .build(); + + List selectedPaths = fileTree != null ? fileTree.getSelectedPaths() : List.of(); + if (selectedPaths.isEmpty()) { + LOGGER.warn("No files selected for export."); + return; + } + + String zipName = name.replaceAll("[^a-zA-Z0-9_\\-]", "_") + ".zip"; + Path gameDir = FabricLoader.getInstance().getGameDir(); + + try { + ConfigPackBuilder.zipFiles(gameDir, selectedPaths, zipName, meta); + LOGGER.info("Exported config pack: {}", zipName); + } catch (IOException e) { + LOGGER.error("Export failed: {}", e.getMessage()); + } + } + + private void openExportsFolder() { + try { + Files.createDirectories(EXPORTS_DIR); + Util.getPlatform().openUri(EXPORTS_DIR.toUri()); + } catch (IOException e) { + LOGGER.error("Failed to open exports folder: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ImportPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ImportPage.java new file mode 100644 index 0000000..da614f6 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/ImportPage.java @@ -0,0 +1,267 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.configpack.ConfigPackExtractor; +import com.github.kd_gaming1.packcore.configpack.ConfigPackScanner; +import com.github.kd_gaming1.packcore.gui.component.ConfigPackCard; +import com.github.kd_gaming1.packcore.gui.component.FileTreeBuilder; +import com.github.kd_gaming1.packcore.gui.component.FileTreeComponent; +import com.github.kd_gaming1.packcore.gui.component.FileTreeNode; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import eu.midnightdust.lib.config.MidnightConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class ImportPage extends BaseConfigPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ImportPage"); + + private static final Path IMPORTS_DIR = PackCore.PACKCORE_DIR.resolve("imports"); + + private static final int PANEL_GAP = 12; + private static final int PADDING = 10; + private static final int BUTTON_HEIGHT = 18; + private static final int OPEN_BTN_WIDTH = 140; + private static final int ACTION_BTN_WIDTH = 110; + private static final int BUTTON_GAP = 8; + private static final int LABEL_GAP = 6; + + private static final int COLOR_LABEL = 0xFFCCCCCC; + private static final int COLOR_HINT = 0xFF666666; + private static final int COLOR_ERROR = 0xFFFF5555; + + private ConfigPackEntry selectedImport; + private EmptyComponent leftPanel; + private int panelWidth; + private double rightPanelScrollAmount = 0; + + private List cachedImports = null; + + public ImportPage(int width, int height) { + super(width, height); + } + + @Override + public void onEnter() { + this.clearComponents(); + cachedImports = null; // Force fresh scan on each page entry + + ensureImportsDirExists(); + + panelWidth = (getWidth() - PANEL_GAP) / 2; + + leftPanel = new EmptyComponent(0, 0, panelWidth, getHeight()); + EmptyComponent rightPanel = new EmptyComponent(panelWidth + PANEL_GAP, 0, panelWidth, getHeight()); + + buildLeftPanel(); + buildRightPanel(rightPanel); + + this.addComponent(leftPanel); + this.addComponent(rightPanel); + } + + private void buildLeftPanel() { + leftPanel.clearComponents(); + + var font = Minecraft.getInstance().font; + int panelHeight = leftPanel.getHeight(); + int currentY = PADDING; + + leftPanel.addComponent(new TextComponent(PADDING, currentY, + Component.translatable("gui.packcore.import.files.heading"), COLOR_LABEL)); + currentY += font.lineHeight + LABEL_GAP; + + if (selectedImport == null) { + leftPanel.addComponent(new TextComponent(PADDING, currentY, + Component.translatable("gui.packcore.import.files.hint"), COLOR_HINT)); + return; + } + + int buttonsAreaHeight = BUTTON_HEIGHT + PADDING * 2; + int treeHeight = panelHeight - currentY - buttonsAreaHeight; + + FileTreeNode treeRoot; + try { + treeRoot = FileTreeBuilder.fromZip(selectedImport.zipPath()); + } catch (IOException e) { + LOGGER.error("Failed to read import zip: {}", e.getMessage()); + leftPanel.addComponent(new TextComponent(PADDING, currentY, + Component.literal("Error reading import."), COLOR_ERROR)); + return; + } + + FileTreeComponent fileTree = new FileTreeComponent(PADDING, currentY, panelWidth - PADDING * 2, treeHeight, treeRoot); + leftPanel.addComponent(fileTree); + + int buttonsY = panelHeight - PADDING - BUTTON_HEIGHT; + int totalBtnWidth = ACTION_BTN_WIDTH * 2 + BUTTON_GAP; + int buttonsStartX = (panelWidth - totalBtnWidth) / 2; + + CustomButtonWidget applySelectedBtn = new CustomButtonWidget( + buttonsStartX, buttonsY, ACTION_BTN_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.config.button.apply_selected"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> applyFiles(fileTree.getSelectedPaths())); + applySelectedBtn.active = false; + applySelectedBtn.setTooltip(Tooltip.create( + Component.translatable("gui.packcore.config.tooltip.apply_selected"))); + + CustomButtonWidget applyAllBtn = new CustomButtonWidget( + buttonsStartX + ACTION_BTN_WIDTH + BUTTON_GAP, buttonsY, ACTION_BTN_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.config.button.apply_all"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> applyAll()); + applyAllBtn.setTooltip(Tooltip.create( + Component.translatable("gui.packcore.config.tooltip.apply_all"))); + + fileTree.setOnSelectionChanged(() -> + applySelectedBtn.active = !fileTree.getSelectedPaths().isEmpty()); + + leftPanel.addWidget(applySelectedBtn); + leftPanel.addWidget(applyAllBtn); + } + + private void buildRightPanel(EmptyComponent panel) { + panel.clearComponents(); + + var font = Minecraft.getInstance().font; + int currentY = PADDING; + + panel.addComponent(new TextComponent(PADDING, currentY, + Component.translatable("gui.packcore.import.list.heading"), COLOR_LABEL)); + currentY += font.lineHeight + LABEL_GAP; + + // Use cached imports — avoids a full disk scan on every card click + if (cachedImports == null) { + cachedImports = scanImports(); + } + List imports = cachedImports; + + int buttonAreaHeight = BUTTON_HEIGHT + PADDING * 2; + int listHeight = getHeight() - currentY - buttonAreaHeight; + + ScrollContainerWidget scroll = new ScrollContainerWidget(panelWidth - PADDING * 3, listHeight); + EmptyComponent container = new EmptyComponent(0, 0, panelWidth - PADDING * 2, 0); + + int y = 0; + if (imports.isEmpty()) { + container.addComponent(new TextComponent(0, 0, + Component.translatable("gui.packcore.import.list.empty"), COLOR_HINT)); + container.addComponent(new MultiLineTextComponent(0, font.lineHeight + 4, container.getWidth(), + Component.translatable("gui.packcore.import.list.empty.hint"), COLOR_HINT)); + y = font.lineHeight * 2 + 4; + } else { + for (ConfigPackEntry entry : imports) { + boolean isActive = selectedImport != null && selectedImport.zipPath().equals(entry.zipPath()); + ConfigPackCard card = new ConfigPackCard( + 0, y, panelWidth - PADDING * 4, entry, isActive, + null, + clicked -> { + rightPanelScrollAmount = scroll.scrollAmount(); + selectedImport = clicked; + buildLeftPanel(); + buildRightPanel(panel); + leftPanel.updateParentPosition( + leftPanel.getParentX(), leftPanel.getParentY(), + getWidth(), getHeight()); + panel.updateParentPosition( + panel.getParentX(), panel.getParentY(), + getWidth(), getHeight()); + } + ); + container.addComponent(card); + y += card.getHeight() + 6; + } + } + + container.setHeight(y); + scroll.addComponent(container); + scroll.setScrollAmount(rightPanelScrollAmount); + + EmptyComponent scrollWrapper = new EmptyComponent(PADDING, currentY, panelWidth - PADDING * 2, listHeight); + scrollWrapper.addWidget(scroll); + panel.addComponent(scrollWrapper); + + int openBtnX = PADDING + (panelWidth - PADDING * 2 - OPEN_BTN_WIDTH) / 2; + int openBtnY = currentY + listHeight + PADDING; + panel.addWidget(new CustomButtonWidget(openBtnX, openBtnY, OPEN_BTN_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.import.button.open_folder"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> openImportsFolder())); + } + + private void applyFiles(List paths) { + if (selectedImport == null || paths.isEmpty()) return; + try { + ConfigPackExtractor.extractSelective(selectedImport.zipPath(), PackCore.PACKCORE_DIR, + ConfigPackExtractor.OverwriteMode.REPLACE_EXISTING, paths); + saveAppliedState(); + } catch (IOException e) { + LOGGER.error("Failed to apply imported files: {}", e.getMessage()); + } + } + + private void applyAll() { + if (selectedImport == null) { + LOGGER.warn("Apply All clicked in ImportPage without a selected import."); + return; + } + + String pendingFile = selectedImport.zipPath().getFileName().toString(); + LOGGER.info("Scheduling imported config apply on restart: file='{}'", pendingFile); + + PackCoreConfig.pendingConfigPack = pendingFile; + MidnightConfig.write(MOD_ID); + + LOGGER.info("Pending imported config saved, stopping client to apply on next launch."); + Minecraft.getInstance().stop(); + } + + private void saveAppliedState() { + if (selectedImport.config().has("version")) { + PackCoreConfig.lastAppliedVersion = selectedImport.config().get("version").getAsString(); + } + PackCoreConfig.lastAppliedPackFile = selectedImport.zipPath().getFileName().toString(); + MidnightConfig.write(MOD_ID); + } + + private void openImportsFolder() { + try { + Files.createDirectories(IMPORTS_DIR); + Util.getPlatform().openUri(IMPORTS_DIR.toUri()); + } catch (IOException e) { + LOGGER.error("Failed to open imports folder: {}", e.getMessage()); + } + } + + private void ensureImportsDirExists() { + try { Files.createDirectories(IMPORTS_DIR); } + catch (IOException ignored) {} + } + + private List scanImports() { + try { return new ConfigPackScanner().scanFolder(IMPORTS_DIR); } + catch (IOException e) { + LOGGER.error("Failed to scan imports: {}", e.getMessage()); + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/TabNavBar.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/TabNavBar.java new file mode 100644 index 0000000..6155c0e --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/config/TabNavBar.java @@ -0,0 +1,77 @@ +package com.github.kd_gaming1.packcore.gui.screen.config; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import org.jspecify.annotations.NonNull; + +import java.util.function.Consumer; + +public class TabNavBar extends AbstractComponent { + + private static final int COLOR_ACTIVE_INDICATOR = 0xFF2196F3; + private static final int COLOR_ACTIVE_BG = 0x551A3A5C; + private static final int COLOR_HOVER_BG = 0x441A3A5C; + private static final int COLOR_TEXT_ACTIVE = 0xFFFFFFFF; + private static final int COLOR_TEXT_INACTIVE = 0xFFCCCCCC; + private static final int COLOR_SEPARATOR = 0x44FFFFFF; + private static final int INDICATOR_HEIGHT = 2; + + private ConfigTab activeTab; + + public TabNavBar(int x, int y, int width, int height, ConfigTab activeTab, Consumer onTabClick) { + super(x, y, width, height); + this.activeTab = activeTab; + buildTabWidgets(width, height, onTabClick); + } + + private void buildTabWidgets(int width, int height, Consumer onTabClick) { + ConfigTab[] tabs = ConfigTab.ordered(); + int tabWidth = width / tabs.length; + + for (ConfigTab tab : tabs) { + ButtonWidget btn = new ButtonWidget(tab.index() * tabWidth, 0, tabWidth, height, tab.label(), b -> onTabClick.accept(tab)) { + @Override + public void renderContents(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {} + }; + this.addWidget(btn); + } + } + + public void setActiveTab(ConfigTab tab) { + this.activeTab = tab; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + + var font = Minecraft.getInstance().font; + ConfigTab[] tabs = ConfigTab.ordered(); + int tabWidth = w / tabs.length; + + graphics.fill(x, y, x + w, y + 1, COLOR_SEPARATOR); + + for (ConfigTab tab : tabs) { + int tabX = x + tab.index() * tabWidth; + boolean active = tab == activeTab; + boolean hovered = mouseX >= tabX && mouseX < tabX + tabWidth && mouseY >= y && mouseY < y + h; + + if (active) { + graphics.fill(tabX, y + 1, tabX + tabWidth, y + h, COLOR_ACTIVE_BG); + graphics.fill(tabX, y + h - INDICATOR_HEIGHT, tabX + tabWidth, y + h, COLOR_ACTIVE_INDICATOR); + } else if (hovered) { + graphics.fill(tabX, y + 1, tabX + tabWidth, y + h, COLOR_HOVER_BG); + } + + String label = tab.label().getString(); + int labelX = tabX + (tabWidth - font.width(label)) / 2; + int labelY = y + (h - font.lineHeight) / 2; + graphics.drawString(font, label, labelX, labelY, active ? COLOR_TEXT_ACTIVE : COLOR_TEXT_INACTIVE, false); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/util/GuiColors.java b/src/main/java/com/github/kd_gaming1/packcore/gui/util/GuiColors.java new file mode 100644 index 0000000..3a0fdca --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/util/GuiColors.java @@ -0,0 +1,53 @@ +package com.github.kd_gaming1.packcore.gui.util; + +/** Design token palette for all GUI components. Edit here to restyle globally. */ +public final class GuiColors { + + private GuiColors() {} + + // Accent + public static final int ACCENT = 0xFF2196F3; + public static final int ACCENT_BORDER = 0x552196F3; + + // Semantic + public static final int SUCCESS = 0xFF4CAF50; + public static final int SUCCESS_BORDER = 0x554CAF50; + public static final int WARNING = 0xFFFFAA00; + public static final int WARNING_BACKGROUND = 0x33FFAA00; + public static final int ERROR = 0xFFFF5555; + + // Panels + public static final int PANEL_BACKGROUND = 0xCC0A1520; + public static final int PANEL_BACKGROUND_HOVER = 0xCC0D2035; + public static final int PANEL_BORDER = 0xFFD4A017; + + // Overlays + public static final int OVERLAY_DIM = 0xBB000000; + public static final int OVERLAY_BACKGROUND = 0xF0080F1A; + public static final int OVERLAY_BORDER = 0x882196F3; + + // Rows / cards + public static final int ROW_BACKGROUND = 0x22FFFFFF; + public static final int ROW_SELECTED = 0x33FFFFFF; + + // Borders + public static final int BORDER_SELECTED = 0xFF2196F3; + public static final int BORDER_HOVERED = 0xFFFFAA00; + public static final int BORDER_IDLE = 0xFF333333; + + // Selection indicator bar + public static final int INDICATOR_SELECTED = 0xFF2196F3; + + // Text + public static final int TEXT_PRIMARY = 0xFFFFFFFF; + public static final int TEXT_SECONDARY = 0xFFAAAAAA; + public static final int TEXT_HINT = 0xFF777777; + public static final int TEXT_NOTE = 0xFF666666; + public static final int NAME_SELECTED = 0xFF2196F3; + public static final int NAME_DEFAULT = 0xFFCCCCCC; + public static final int DESCRIPTION = 0xFFAAAAAA; + + // Checkbox + public static final int CHECKMARK_BOX = 0xFF2196F3; + public static final int CHECKMARK_TICK = 0xFFFFFFFF; +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/util/GuiHelper.java b/src/main/java/com/github/kd_gaming1/packcore/gui/util/GuiHelper.java new file mode 100644 index 0000000..842ef43 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/util/GuiHelper.java @@ -0,0 +1,54 @@ +package com.github.kd_gaming1.packcore.gui.util; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public final class GuiHelper { + + private GuiHelper() {} + + public static void drawBorder(GuiGraphics graphics, int x, int y, int width, int height, int color) { + graphics.fill(x, y, x + width, y + 1, color); + graphics.fill(x, y + height - 1, x + width, y + height, color); + graphics.fill(x, y, x + 1, y + height, color); + graphics.fill(x + width - 1, y, x + width, y + height, color); + } + + public static final WidgetSprites BLANK_BUTTON_SPRITES = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/blank_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/disabled_blank_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/hover_blank_gray_button") + ); + + public static EmptyComponent scrollWrapped(int x, int y, int width, int height, Consumer configure) { + ScrollContainerWidget scroll = new ScrollContainerWidget(width, height); + configure.accept(scroll); + EmptyComponent wrapper = new EmptyComponent(x, y, width, height); + wrapper.addWidget(scroll); + return wrapper; + } + + public static String loadMarkdown(Path path, String fallback, Logger logger) { + if (!Files.exists(path)) { + logger.warn("Markdown file not found: {}", path); + return fallback; + } + try { + return Files.readString(path); + } catch (IOException e) { + logger.error("Failed to read markdown file {}: {}", path.getFileName(), e.getMessage()); + return fallback; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/util/ImageBackground.java b/src/main/java/com/github/kd_gaming1/packcore/gui/util/ImageBackground.java new file mode 100644 index 0000000..6fcfe2a --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/util/ImageBackground.java @@ -0,0 +1,136 @@ +package com.github.kd_gaming1.packcore.gui.util; + +import com.daqem.uilib.gui.background.AbstractBackground; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.resources.Identifier; + +/** + * Custom background_old that renders a texture using one of four layout modes. + */ +public class ImageBackground extends AbstractBackground { + + public enum BackgroundMode { + /** Stretch the image to fill the entire screen. */ + STRETCH, + /** Tile the image across the screen. */ + TILE, + /** Center image at its natural size. */ + CENTER, + /** Scale to fit the screen while maintaining an aspect ratio. */ + FIT + } + + private final Identifier texture; + private final int textureWidth; + private final int textureHeight; + private final BackgroundMode mode; + + // Cached layout — invalidated when screen size changes + private int cachedScreenWidth = -1; + private int cachedScreenHeight = -1; + private int cachedX, cachedY, cachedW, cachedH; + + public ImageBackground(Identifier texture, int textureWidth, int textureHeight, BackgroundMode mode) { + this.texture = texture; + this.textureWidth = textureWidth; + this.textureHeight = textureHeight; + this.mode = mode; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + int screenWidth = graphics.guiWidth(); + int screenHeight = graphics.guiHeight(); + + switch (mode) { + case STRETCH -> renderStretched(graphics, screenWidth, screenHeight); + case TILE -> renderTiled(graphics, screenWidth, screenHeight); + case CENTER -> renderCentered(graphics, screenWidth, screenHeight); + case FIT -> renderFit(graphics, screenWidth, screenHeight); + } + } + + private boolean cacheIsStale(int screenWidth, int screenHeight) { + return screenWidth != cachedScreenWidth || screenHeight != cachedScreenHeight; + } + + private void updateCache(int screenWidth, int screenHeight, int x, int y, int w, int h) { + cachedScreenWidth = screenWidth; + cachedScreenHeight = screenHeight; + cachedX = x; + cachedY = y; + cachedW = w; + cachedH = h; + } + + private void renderStretched(GuiGraphics graphics, int screenWidth, int screenHeight) { + graphics.blit( + RenderPipelines.GUI_TEXTURED, + texture, + 0, 0, + 0.0F, 0.0F, + screenWidth, screenHeight, + textureWidth, textureHeight, + textureWidth, textureHeight + ); + } + + private void renderTiled(GuiGraphics graphics, int screenWidth, int screenHeight) { + for (int tx = 0; tx * textureWidth < screenWidth; tx++) { + for (int ty = 0; ty * textureHeight < screenHeight; ty++) { + graphics.blit( + RenderPipelines.GUI_TEXTURED, + texture, + tx * textureWidth, ty * textureHeight, + 0.0F, 0.0F, + textureWidth, textureHeight, + textureWidth, textureHeight, + textureWidth, textureHeight + ); + } + } + } + + private void renderCentered(GuiGraphics graphics, int screenWidth, int screenHeight) { + if (cacheIsStale(screenWidth, screenHeight)) { + int x = (screenWidth - textureWidth) / 2; + int y = (screenHeight - textureHeight) / 2; + updateCache(screenWidth, screenHeight, x, y, textureWidth, textureHeight); + } + + graphics.blit( + RenderPipelines.GUI_TEXTURED, + texture, + cachedX, cachedY, + 0.0F, 0.0F, + textureWidth, textureHeight, + textureWidth, textureHeight, + textureWidth, textureHeight + ); + } + + private void renderFit(GuiGraphics graphics, int screenWidth, int screenHeight) { + if (cacheIsStale(screenWidth, screenHeight)) { + float scale = Math.min( + (float) screenWidth / textureWidth, + (float) screenHeight / textureHeight + ); + int w = (int) (textureWidth * scale); + int h = (int) (textureHeight * scale); + int x = (screenWidth - w) / 2; + int y = (screenHeight - h) / 2; + updateCache(screenWidth, screenHeight, x, y, w, h); + } + + graphics.blit( + RenderPipelines.GUI_TEXTURED, + texture, + cachedX, cachedY, + 0.0F, 0.0F, + cachedW, cachedH, + textureWidth, textureHeight, + textureWidth, textureHeight + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/util/PackCoreToast.java b/src/main/java/com/github/kd_gaming1/packcore/gui/util/PackCoreToast.java new file mode 100644 index 0000000..b2b9a75 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/util/PackCoreToast.java @@ -0,0 +1,73 @@ +package com.github.kd_gaming1.packcore.gui.util; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.toasts.Toast; +import net.minecraft.client.gui.components.toasts.ToastManager; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import org.jspecify.annotations.NonNull; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** + * A branded toast notification showing a title and message line. + * Use {@link ToastHelper} to display one — don't instantiate directly. + */ +class PackCoreToast implements Toast { + + private static final long DISPLAY_TIME_MS = 5_000L; + + private static final int COLOR_BACKGROUND = 0xF0080F1A; + private static final int COLOR_BORDER = 0xFFFFAA00; + private static final int COLOR_TITLE = 0xFFFFFFFF; + private static final int COLOR_MESSAGE = 0xFFAAAAAA; + + private static final int ICON_SIZE = 20; + private static final int ICON_MARGIN = 6; + private static final int TEXT_MARGIN = 6; + + private static final Identifier ICON = Identifier.fromNamespaceAndPath(MOD_ID, "assets/sbe_logo"); + + private final Component title; + private final Component message; + private Visibility visibility = Visibility.SHOW; + + PackCoreToast(Component title, Component message) { + this.title = title; + this.message = message; + } + + @Override + public @NonNull Visibility getWantedVisibility() { + return visibility; + } + + @Override + public void update(ToastManager manager, long fullyVisibleFor) { + if (fullyVisibleFor >= (long) (DISPLAY_TIME_MS * manager.getNotificationDisplayTimeMultiplier())) { + visibility = Visibility.HIDE; + } + } + + @Override + public void render(GuiGraphics graphics, Font font, long fullyVisibleFor) { + int w = width(); + int h = height(); + + graphics.fill(0, 0, w, h, COLOR_BACKGROUND); + GuiHelper.drawBorder(graphics, 0, 0, w, h, COLOR_BORDER); + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, ICON, ICON_MARGIN, (h - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE); + + int textX = ICON_MARGIN + ICON_SIZE + TEXT_MARGIN; + int titleY = h / 2 - font.lineHeight - 1; + int messageY = h / 2 + 1; + + graphics.drawString(font, title, textX, titleY, COLOR_TITLE, false); + graphics.drawString(font, message, textX, messageY, COLOR_MESSAGE, false); + } + + @Override public int width() { return 200; } + @Override public int height() { return 40; } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/util/SpriteHelper.java b/src/main/java/com/github/kd_gaming1/packcore/gui/util/SpriteHelper.java new file mode 100644 index 0000000..ea55bd0 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/util/SpriteHelper.java @@ -0,0 +1,98 @@ +package com.github.kd_gaming1.packcore.gui.util; + +import com.daqem.uilib.gui.component.sprite.SpriteComponent; +import net.minecraft.resources.Identifier; + +/** + * Utility for creating and positioning sprites with responsive scaling. + */ +@SuppressWarnings("unused") +public class SpriteHelper { + + /** Holds the calculated position and size of a sprite. */ + public record SpriteDimensions(int x, int y, int width, int height) { } + + /** Scale to a percentage of screen width, maintaining aspect ratio. */ + public static SpriteDimensions scale( + int screenWidth, int screenHeight, + int originalWidth, int originalHeight, + double scalePercent, + int x, int y) { + + int scaledWidth = (int) (screenWidth * scalePercent); + int scaledHeight = (originalHeight * scaledWidth) / originalWidth; + return new SpriteDimensions(x, y, scaledWidth, scaledHeight); + } + + /** Scale and center horizontally on the screen. */ + public static SpriteDimensions scaleAndCenter( + int screenWidth, int screenHeight, + int originalWidth, int originalHeight, + double scalePercent, + int y) { + + int scaledWidth = (int) (screenWidth * scalePercent); + int scaledHeight = (originalHeight * scaledWidth) / originalWidth; + int centeredX = (screenWidth - scaledWidth) / 2; + return new SpriteDimensions(centeredX, y, scaledWidth, scaledHeight); + } + + /** Scale with a maximum width cap. */ + public static SpriteDimensions scaleWithMax( + int screenWidth, int screenHeight, + int originalWidth, int originalHeight, + int maxWidth, + double scalePercent, + int x, int y) { + + int scaledWidth = Math.min(maxWidth, (int) (screenWidth * scalePercent)); + int scaledHeight = (originalHeight * scaledWidth) / originalWidth; + return new SpriteDimensions(x, y, scaledWidth, scaledHeight); + } + + /** Scale with a maximum width cap and center horizontally. */ + public static SpriteDimensions scaleWithMaxAndCenter( + int screenWidth, int screenHeight, + int originalWidth, int originalHeight, + int maxWidth, + double scalePercent, + int y) { + + int scaledWidth = Math.min(maxWidth, (int) (screenWidth * scalePercent)); + int scaledHeight = (originalHeight * scaledWidth) / originalWidth; + int centeredX = (screenWidth - scaledWidth) / 2; + return new SpriteDimensions(centeredX, y, scaledWidth, scaledHeight); + } + + public static SpriteComponent createSprite(SpriteDimensions dims, Identifier spriteLocation) { + return new SpriteComponent(dims.x(), dims.y(), dims.width(), dims.height(), spriteLocation); + } + + public static SpriteComponent createScaledSprite( + int screenWidth, int screenHeight, + int originalWidth, int originalHeight, + double scalePercent, + int x, int y, + Identifier spriteLocation) { + + return createSprite(scale(screenWidth, screenHeight, originalWidth, originalHeight, scalePercent, x, y), spriteLocation); + } + + public static SpriteComponent createScaledCenteredSprite( + int screenWidth, int screenHeight, + int originalWidth, int originalHeight, + double scalePercent, + int y, + Identifier spriteLocation) { + + return createSprite(scaleAndCenter(screenWidth, screenHeight, originalWidth, originalHeight, scalePercent, y), spriteLocation); + } + + public static int centerX(int screenWidth, int componentWidth) { + return (screenWidth - componentWidth) / 2; + } + + public static int centerY(int screenHeight, int componentHeight) { + return (screenHeight - componentHeight) / 2; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/util/ToastHelper.java b/src/main/java/com/github/kd_gaming1/packcore/gui/util/ToastHelper.java new file mode 100644 index 0000000..7b6a955 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/util/ToastHelper.java @@ -0,0 +1,39 @@ +package com.github.kd_gaming1.packcore.gui.util; + +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +/** Shows PackCore toast notifications. */ +public final class ToastHelper { + + private ToastHelper() {} + + public static void showUpdateAvailable(String latestVersion) { + if (!PackCoreConfig.showUpdateToast) return; + show( + Component.translatable("gui.packcore.toast.update.title"), + Component.translatable("gui.packcore.toast.update.message", latestVersion) + ); + } + + public static void showBackupCreated(String backupName) { + if (!PackCoreConfig.showBackupToast) return; + show( + Component.translatable("gui.packcore.toast.backup.title"), + Component.translatable("gui.packcore.toast.backup.message", backupName) + ); + } + + public static void showLowRam() { + if (!PackCoreConfig.showRamWarningToast) return; + show( + Component.translatable("gui.packcore.toast.ram.title"), + Component.translatable("gui.packcore.toast.ram.message") + ); + } + + public static void show(Component title, Component message) { + Minecraft.getInstance().getToastManager().addToast(new PackCoreToast(title, message)); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/BaseCardGridPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/BaseCardGridPage.java new file mode 100644 index 0000000..9a3ef18 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/BaseCardGridPage.java @@ -0,0 +1,68 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.github.kd_gaming1.packcore.gui.component.OptionCardGrid; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +import java.util.List; + +/** + * Base class for wizard pages that show an explanation and an OptionCardGrid below it. + */ +public abstract class BaseCardGridPage extends BaseWizardPage { + + private static final int OUTER_PADDING = 16; + private static final int CARD_GAP = 10; + private static final int EXPLANATION_GAP = 8; + private static final int COLOR_EXPLANATION = 0xFFAAAAAA; + + protected BaseCardGridPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + protected abstract String stateKey(); + protected abstract int columns(); + protected abstract Component explanation(); + protected abstract List options(); + protected abstract OptionCardGrid.CardDescriptor descriptor(); + + @Override public boolean validate() { return true; } + @Override public void onExit() {} + + @Override + public void onEnter() { + this.clearComponents(); + + int availableWidth = getWidth() - OUTER_PADDING * 2; + + MultiLineTextComponent explanationText = new MultiLineTextComponent( + OUTER_PADDING, OUTER_PADDING, availableWidth, explanation(), COLOR_EXPLANATION + ); + this.addComponent(explanationText); + + int gridTop = OUTER_PADDING + explanationText.getHeight() + EXPLANATION_GAP; + int gridHeight = getHeight() - gridTop - OUTER_PADDING; + + OptionCardGrid.CardDescriptor desc = descriptor(); + + OptionCardGrid grid = new OptionCardGrid<>( + OUTER_PADDING, gridTop, + availableWidth, gridHeight, + columns(), CARD_GAP, + options(), desc, + state.getSelection(stateKey()), + selected -> state.setSelection(stateKey(), selected != null ? desc.id(selected) : null) + ); + this.addComponent(grid); + } + + @Override + public void preloadAssets() { + // Touch the texture so TextureManager loads it now instead of on first render + for (T option : options()) { + Minecraft.getInstance().getTextureManager().getTexture(descriptor().previewTexture(option)); + } + } + +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/BaseWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/BaseWizardPage.java new file mode 100644 index 0000000..3509bd0 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/BaseWizardPage.java @@ -0,0 +1,48 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import com.daqem.uilib.gui.component.EmptyComponent; +import net.minecraft.network.chat.Component; + +/** + * Abstract base class for all wizard pages. + * Each step extends this and implements the abstract methods below. + */ +public abstract class BaseWizardPage extends EmptyComponent { + + protected final WizardState state; + protected final WizardNavigator navigator; + + public BaseWizardPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(0, 0, width, height); + this.state = state; + this.navigator = navigator; + } + + /** The title shown at the top of this page. */ + public abstract Component getTitle(); + + /** + * Returns true if the user can proceed from this page. + */ + public abstract boolean validate(); + + /** + * Called when this page becomes active. Build your UI components here. + */ + public abstract void onEnter(); + + /** Called when the user navigates away. Use for cleanup or saving state. */ + public abstract void onExit(); + + // Visited state + public void markAsVisited() { + } + + // Navigation flags + public boolean canGoBack() { + return true; + } + + /** Called once at wizard init to pre-load any GPU resources. Default no-op. */ + public void preloadAssets() {} +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardButtonBar.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardButtonBar.java new file mode 100644 index 0000000..cfc2e43 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardButtonBar.java @@ -0,0 +1,195 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.widget.ButtonWidget; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** + * Bottom navigation bar for the Welcome Wizard. + */ +public class WizardButtonBar extends EmptyComponent { + + private static final Logger LOGGER = LogManager.getLogger(); + + private static final int BTN_WIDTH = 90; + private static final int BTN_HEIGHT = 18; + private static final int ICON_SIZE = 20; + private static final int BTN_GAP = 6; + + private static final WidgetSprites CONTINUE_BUTTON = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/continue_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/disabled_continue_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/hover_continue_gray_button") + ); + + private static final WidgetSprites PREVIOUS_BUTTON = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/previous_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/disabled_previous_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/hover_previous_gray_button") + ); + + private static final WidgetSprites DISCORD_BUTTON = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/discord_icon"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/discord_icon"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/discord_icon") + ); + + private static final WidgetSprites GITHUB_BUTTON = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/github_icon"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/github_icon"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/github_icon") + ); + + private static final WidgetSprites MODRINTH_BUTTON = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/modrinth_icon"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/modrinth_icon"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/modrinth_icon") + ); + + private final WizardNavigator navigator; + private Runnable onFinish; + private Runnable onSkipFinish; + + private final ButtonWidget backButton; + private final ButtonWidget skipButton; + private final ButtonWidget continueButton; + private final ButtonWidget finishButton; + + public WizardButtonBar(WizardNavigator navigator, int width, int height) { + super(0, 0, width, height); + this.navigator = navigator; + + int btnY = (height - BTN_HEIGHT) / 2; + + ButtonWidget discord = new CustomButtonWidget( + BTN_GAP, btnY, ICON_SIZE, ICON_SIZE, + Component.literal(""), + DISCORD_BUTTON, + btn -> openUrlSafely(ModpackMetadata.getInstance().getDiscordUrl()) + ); + + ButtonWidget modrinth = new CustomButtonWidget( + BTN_GAP * 2 + ICON_SIZE, btnY, ICON_SIZE, ICON_SIZE, + Component.literal(""), + MODRINTH_BUTTON, + btn -> openUrlSafely(ModpackMetadata.getInstance().getWebsiteUrl()) + ); + + ButtonWidget github = new CustomButtonWidget( + BTN_GAP * 3 + ICON_SIZE + ICON_SIZE, btnY, ICON_SIZE, ICON_SIZE, + Component.literal(""), + GITHUB_BUTTON, + btn -> openUrlSafely(ModpackMetadata.getInstance().getIssueTrackerUrl()) + ); + + continueButton = new CustomButtonWidget( + width - BTN_WIDTH - BTN_GAP, btnY, BTN_WIDTH, BTN_HEIGHT, + Component.translatable("gui.packcore.wizard.button.continue"), + CONTINUE_BUTTON, + btn -> navigator.nextPage() + ); + + backButton = new CustomButtonWidget( + width - BTN_WIDTH * 2 - BTN_GAP * 2, btnY, BTN_WIDTH, BTN_HEIGHT, + Component.translatable("gui.packcore.wizard.button.back"), + PREVIOUS_BUTTON, + btn -> navigator.previousPage() + ); + + // Skip is only shown on the last page as a "skip applying" escape hatch. + // It marks the wizard complete without applying settings. + skipButton = new CustomButtonWidget( + width - BTN_WIDTH * 2 - BTN_GAP * 2, btnY, BTN_WIDTH, BTN_HEIGHT, + Component.translatable("gui.packcore.wizard.button.skip"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> { if (onSkipFinish != null) onSkipFinish.run(); } + ); + + finishButton = new CustomButtonWidget( + width - BTN_WIDTH - BTN_GAP, btnY, BTN_WIDTH, BTN_HEIGHT, + Component.translatable("gui.packcore.wizard.button.finish"), + GuiHelper.BLANK_BUTTON_SPRITES, + btn -> { if (onFinish != null) onFinish.run(); } + ); + finishButton.active = false; + finishButton.setTooltip(Tooltip.create( + Component.translatable("gui.packcore.wizard.button.finish.locked_tooltip"))); + + this.addWidget(discord); + this.addWidget(modrinth); + this.addWidget(github); + this.addWidget(skipButton); + this.addWidget(backButton); + this.addWidget(continueButton); + this.addWidget(finishButton); + + refresh(); + } + + /** Syncs button visibility, position, and active state with the current navigator position. */ + public void refresh() { + boolean hasBack = navigator.hasPrevious(); + boolean isLastPage = navigator.isOnLastPage(); + + backButton.visible = hasBack; + backButton.active = hasBack; + + continueButton.visible = !isLastPage; + continueButton.active = !isLastPage; + + skipButton.visible = isLastPage; + skipButton.active = isLastPage; + + finishButton.visible = isLastPage; + + if (isLastPage) { + skipButton.setX(getTotalX() + getWidth() - BTN_WIDTH * 3 - BTN_GAP * 3); + backButton.setX(getTotalX() + getWidth() - BTN_WIDTH * 2 - BTN_GAP * 2); + } else { + backButton.setX(getTotalX() + getWidth() - BTN_WIDTH * 2 - BTN_GAP * 2); + } + } + + /** + * Enables or disables the Finish button. + * Called by the screen once ConfirmApplyPage reports a successful applying. + */ + public void setFinishEnabled(boolean enabled) { + finishButton.active = enabled; + finishButton.setTooltip(enabled ? null : Tooltip.create( + Component.translatable("gui.packcore.wizard.button.finish.locked_tooltip"))); + } + + /** Registers a callback invoked when the user presses Finish. */ + public void setOnFinish(Runnable callback) { + this.onFinish = callback; + } + + /** Registers a callback invoked when the user presses Skip on the last page. */ + public void setOnSkipFinish(Runnable callback) { + this.onSkipFinish = callback; + } + + private static void openUrlSafely(String url) { + if (url == null || url.isBlank()) { + LOGGER.warn("Attempted to open an empty or null URL, skipping."); + return; + } + try { + Util.getPlatform().openUri(url); + } catch (Exception e) { + LOGGER.warn("Couldn't open uri '{}' ", url, e); + } + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardContentPanel.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardContentPanel.java new file mode 100644 index 0000000..a47d58e --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardContentPanel.java @@ -0,0 +1,42 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import net.minecraft.client.gui.GuiGraphics; + +/** Main content area of the Welcome Wizard. Swaps pages on navigation. */ +public class WizardContentPanel extends AbstractComponent { + + private final WizardNavigator navigator; + private int lastRenderedPageIndex = -1; + + public WizardContentPanel(int x, int y, int width, int height, WizardNavigator navigator) { + super(x, y, width, height); + this.navigator = navigator; + swapToCurrentPage(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int currentIndex = navigator.getCurrentIndex(); + if (currentIndex != lastRenderedPageIndex) { + lastRenderedPageIndex = currentIndex; + swapToCurrentPage(); + updateParentPosition(getParentX(), getParentY(), parentWidth, parentHeight); + } + + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + + graphics.fill(x, y, x + w, y + h, GuiColors.PANEL_BACKGROUND); + GuiHelper.drawBorder(graphics, x, y, w, h, GuiColors.PANEL_BORDER); + } + + private void swapToCurrentPage() { + clearComponents(); + addComponent(navigator.getCurrentPage()); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardHeaderComponent.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardHeaderComponent.java new file mode 100644 index 0000000..a4988b0 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardHeaderComponent.java @@ -0,0 +1,87 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import com.daqem.uilib.gui.component.AbstractComponent; +import com.daqem.uilib.gui.component.sprite.SpriteComponent; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +public class WizardHeaderComponent extends AbstractComponent { + + private final WizardNavigator navigator; + + private static final Identifier SPRITE_ACTIVE = Identifier.fromNamespaceAndPath("packcore", "wizard/node_active"); + private static final Identifier SPRITE_VISITED = Identifier.fromNamespaceAndPath("packcore", "wizard/node_visited"); + private static final Identifier SPRITE_LOCKED = Identifier.fromNamespaceAndPath("packcore", "wizard/node_locked"); + + private static final int COLOR_TITLE = 0xFFFFFFFF; + private static final int COLOR_TRACK_DONE = 0xFF4A90D9; + private static final int COLOR_TRACK_AHEAD = 0x55FFFFFF; + + private static final int ACTIVE_NODE_SIZE = 14; + private static final int SMALL_NODE_SIZE = 10; + private static final int TRACK_HALF_HEIGHT = 1; + private static final int PADDING = 14; + + public WizardHeaderComponent(int x, int y, int width, int height, WizardNavigator navigator) { + super(x, y, width, height); + this.navigator = navigator; + } + + /** Called by the navigator when the active page changes. */ + public void onPageChanged() { } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + var font = Minecraft.getInstance().font; + int pageCount = navigator.getPages().size(); + int activeIndex = navigator.getCurrentIndex(); + + int originX = getTotalX(); + int originY = getTotalY(); + + Component title = navigator.getPages().get(activeIndex).getTitle(); + graphics.drawCenteredString(font, title, originX + getWidth() / 2, originY + 4, COLOR_TITLE); + + int nodeRowY = originY + getHeight() - 10; + int available = getWidth() - PADDING * 2; + int spacing = available / (pageCount - 1); + int startX = originX + PADDING + (available - spacing * (pageCount - 1)) / 2; + + for (int i = 0; i < pageCount; i++) { + int nodeCenterX = startX + i * spacing; + boolean isActive = (i == activeIndex); + boolean isVisited = (i < activeIndex); + + int nodeSize = isActive ? ACTIVE_NODE_SIZE : SMALL_NODE_SIZE; + int halfNode = nodeSize / 2; + + // Draw the connecting track segment before this node + if (i > 0) { + int prevNodeSize = (i - 1 == activeIndex) ? ACTIVE_NODE_SIZE : SMALL_NODE_SIZE; + int prevNodeCenterX = startX + (i - 1) * spacing; + int segmentX1 = prevNodeCenterX + prevNodeSize / 2; + int segmentX2 = nodeCenterX - halfNode; + int trackColor = (i <= activeIndex) ? COLOR_TRACK_DONE : COLOR_TRACK_AHEAD; + graphics.fill(segmentX1, nodeRowY - TRACK_HALF_HEIGHT, segmentX2, nodeRowY + TRACK_HALF_HEIGHT + 1, trackColor); + } + + Identifier sprite = isActive ? SPRITE_ACTIVE : isVisited ? SPRITE_VISITED : SPRITE_LOCKED; + + SpriteComponent node = new SpriteComponent(nodeCenterX - halfNode, nodeRowY - halfNode, nodeSize, nodeSize, sprite); + node.updateParentPosition(originX, originY, getWidth(), getHeight()); + node.render(graphics, mouseX, mouseY, partialTick, getWidth(), getHeight()); + + if (isActive) { + graphics.drawCenteredString( + font, + Component.literal(String.valueOf(i + 1)), + nodeCenterX, + nodeRowY - font.lineHeight / 2, + COLOR_TITLE + ); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardNavigator.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardNavigator.java new file mode 100644 index 0000000..ffbaad1 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardNavigator.java @@ -0,0 +1,136 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * Navigation state machine for the setup wizard. + * + *

Pages are registered with {@link #addPage(BaseWizardPage)} in order, then + * {@link #initialize()} activates the first page. {@link #nextPage()} and + * {@link #previousPage()} guard every transition: they check + * {@link BaseWizardPage#validate()}, {@link BaseWizardPage#canGoBack()}, and bounds + * before calling {@link BaseWizardPage#onExit()} / {@link BaseWizardPage#onEnter()} + * on the outgoing and incoming pages respectively. A {@link PageChangeEvent} is + * fired after every successful navigation so the host screen can update its + * header and button bar. + */ +public class WizardNavigator { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/WizardNavigator"); + + private final List pages = new ArrayList<>(); + private final WizardState state; + + private int currentPageIndex = 0; + private Consumer onPageChange; + + public WizardNavigator(WizardState state) { + this.state = state; + } + + public void addPage(BaseWizardPage page) { + pages.add(page); + LOGGER.debug("Added wizard page: {} (total pages: {})", page.getClass().getSimpleName(), pages.size()); + } + + public List getPages() { + return Collections.unmodifiableList(pages); + } + + public int getPageCount() { + return pages.size(); + } + + public BaseWizardPage getCurrentPage() { + validateIndex(currentPageIndex); + return pages.get(currentPageIndex); + } + + public int getCurrentIndex() { + return currentPageIndex; + } + + public int getCurrentPageNumber() { + return currentPageIndex + 1; + } + + private void validateIndex(int index) { + if (index < 0 || index >= pages.size()) { + throw new IllegalStateException("Invalid page index: " + index); + } + } + + public void nextPage() { + if (!canProceed()) { + LOGGER.warn("Cannot proceed from page {}", getCurrentPageNumber()); + return; + } + navigateTo(currentPageIndex + 1, NavigationDirection.FORWARD); + } + + public void previousPage() { + if (!hasPrevious()) { + LOGGER.warn("Already on first page"); + return; + } + if (!getCurrentPage().canGoBack()) { + LOGGER.warn("Back navigation disabled on page {}", getCurrentPageNumber()); + return; + } + navigateTo(currentPageIndex - 1, NavigationDirection.BACKWARD); + } + + private void navigateTo(int newIndex, NavigationDirection direction) { + validateIndex(newIndex); + + int previousIndex = currentPageIndex; + getCurrentPage().onExit(); + currentPageIndex = newIndex; + + BaseWizardPage newPage = getCurrentPage(); + newPage.onEnter(); + newPage.markAsVisited(); + + firePageChangeEvent(previousIndex, newIndex, direction); + LOGGER.debug("Navigated to page {}/{}", getCurrentPageNumber(), getPageCount()); + } + + public boolean hasPrevious() { return currentPageIndex > 0; } + public boolean hasNext() { return currentPageIndex < pages.size() - 1; } + public boolean isOnLastPage() { return currentPageIndex == pages.size() - 1; } + public boolean canProceed() { return hasNext() && getCurrentPage().validate(); } + public WizardState getState() { return state; } + + public void initialize() { + if (pages.isEmpty()) throw new IllegalStateException("Cannot initialize wizard with no pages"); + + currentPageIndex = 0; + BaseWizardPage firstPage = getCurrentPage(); + firstPage.onEnter(); + firstPage.markAsVisited(); + LOGGER.info("Wizard initialized with {} pages", pages.size()); + } + + public void setOnPageChange(Consumer callback) { + this.onPageChange = callback; + } + + private void firePageChangeEvent(int from, int to, NavigationDirection direction) { + if (onPageChange != null) { + onPageChange.accept(new PageChangeEvent(from, to, direction)); + } + } + + public record PageChangeEvent(int fromIndex, int toIndex, NavigationDirection direction) { } + + public enum NavigationDirection { + FORWARD, + BACKWARD + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardState.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardState.java new file mode 100644 index 0000000..f30cfd8 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/WizardState.java @@ -0,0 +1,67 @@ +package com.github.kd_gaming1.packcore.gui.wizard; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public class WizardState { + + private final Map selections = new HashMap<>(); + private final Map> multiSelections = new HashMap<>(); + private final Set selectedResourcePacks = new HashSet<>(); + + public boolean migratedFromV3 = false; + + // Selections + public void setSelection(String key, String value) { + selections.put(key, value); + } + + public String getSelection(String key) { + return selections.get(key); + } + + // Generic multi-select state + public Set getMultiSelection(String key) { + return Collections.unmodifiableSet(multiSelections.getOrDefault(key, Set.of())); + } + + public void addMultiSelection(String key, String value) { + multiSelections.computeIfAbsent(key, ignored -> new LinkedHashSet<>()).add(value); + } + + public void removeMultiSelection(String key, String value) { + Set values = multiSelections.get(key); + if (values == null) return; + + values.remove(value); + if (values.isEmpty()) { + multiSelections.remove(key); + } + } + + // Resource Packs + public Set getSelectedResourcePacks() { + return Collections.unmodifiableSet(selectedResourcePacks); + } + + public void addResourcePack(String packId) { + selectedResourcePacks.add(packId); + } + + public void removeResourcePack(String packId) { + selectedResourcePacks.remove(packId); + } + + @Override + public String toString() { + return "WizardState{" + + "selections=" + selections + + ", multiSelections=" + multiSelections + + ", resourcePacks=" + selectedResourcePacks + + '}'; + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ConfirmApplyPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ConfirmApplyPage.java new file mode 100644 index 0000000..cfbd1d1 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ConfirmApplyPage.java @@ -0,0 +1,482 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.daqem.uilib.gui.widget.CustomButtonWidget; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.gui.util.GuiColors; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.gui.wizard.BaseWizardPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import com.github.kd_gaming1.packcore.integration.ItemBackgroundManager; +import com.github.kd_gaming1.packcore.integration.PerformanceProfileService; +import com.github.kd_gaming1.packcore.integration.ResourcePackManager; +import com.github.kd_gaming1.packcore.integration.ScamScreenerConfigurator; +import com.github.kd_gaming1.packcore.integration.StorageDesignManager; +import com.github.kd_gaming1.packcore.integration.TabDesignManager; +import com.github.kd_gaming1.packcore.util.JvmArgs; +import com.github.kd_gaming1.scaleme.config.ScaleMeConfig; +import eu.midnightdust.lib.config.MidnightConfig; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** Final wizard step — review selections and apply them. */ +public class ConfirmApplyPage extends BaseWizardPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ConfirmApplyPage"); + + private static final Component PAGE_TITLE = Component.translatable("gui.packcore.wizard.page.confirm.title"); + + private static final int PADDING = 16; + private static final int ROW_HEIGHT = 30; + private static final int ROW_GAP = 6; + private static final int BUTTON_WIDTH = 120; + private static final int BUTTON_HEIGHT = 20; + private static final int BUTTON_GAP = 8; + private static final int SCROLL_BAR_WIDTH = 8; + + private static final int COLOR_VALUE_SELECTED = GuiColors.ACCENT; + private static final int COLOR_VALUE_SKIPPED = 0xFF555555; + private static final int COLOR_PACK_SUBROW = 0xFF777777; + + private static final WidgetSprites APPLY_BUTTON_SPRITES = new WidgetSprites( + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/blank_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/disabled_blank_gray_button"), + Identifier.fromNamespaceAndPath(MOD_ID, "menu/buttons/hover_blank_gray_button") + ); + + private static final String RESOURCE_PACKS_KEY = "resourcePacks"; + private static final String SCAM_PINGS_KEY = "scamScreenerPings"; + + private record SummaryEntry(String selectionKey, String statusKey, String label, String translationPrefix) {} + + private static final List SUMMARY_ENTRIES = List.of( + new SummaryEntry(MainMenuDesignPage.STATE_KEY, MainMenuDesignPage.STATE_KEY, "Main Menu Design", "gui.packcore.wizard.menu_design."), + new SummaryEntry(PerformancePage.STATE_KEY, PerformancePage.STATE_KEY, "Performance Profile", "gui.packcore.wizard.performance."), + new SummaryEntry(TabDesignPage.STATE_KEY, TabDesignPage.STATE_KEY, "Tab Design", "gui.packcore.wizard.tab_design."), + new SummaryEntry(ItemBackgroundPage.STATE_KEY, ItemBackgroundPage.STATE_KEY, "Item Background", "gui.packcore.wizard.item_background."), + new SummaryEntry(StorageDesignPage.STATE_KEY, StorageDesignPage.STATE_KEY, "Storage Design", "gui.packcore.wizard.storage_design."), + new SummaryEntry(SwordBlockPage.STATE_KEY, SwordBlockPage.STATE_KEY, "Sword Block", "gui.packcore.wizard.sword_block."), + new SummaryEntry(ScamScreenerPage.ALERT_LEVEL_KEY, ScamScreenerPage.ALERT_LEVEL_KEY, "ScamScreener Alerts", "gui.packcore.wizard.scamscreener.minimum_risk.") + ); + + private enum RowStatus { SUCCESS, ERROR } + + private final Map rowStatuses = new HashMap<>(); + private final Map rowErrors = new HashMap<>(); + private final List summaryRows = new ArrayList<>(); + private final List packRows = new ArrayList<>(); + private final List scamPingRows = new ArrayList<>(); + + private CustomButtonWidget applyButton; + private static String globalErrorMessage; + private Runnable onApplySucceeded; + private boolean applyCompleted; + + private static String resourcePackWarningMessage; + + public void setOnApplySucceeded(Runnable callback) { + onApplySucceeded = callback; + } + + public boolean isApplyCompleted() { + return applyCompleted; + } + + public ConfirmApplyPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return PAGE_TITLE; } + @Override public boolean validate() { return true; } + @Override public void onExit() { + applyCompleted = false; + rowStatuses.clear(); + rowErrors.clear(); + globalErrorMessage = null; + resourcePackWarningMessage = null; + } + + @Override + public void onEnter() { + clearComponents(); + applyButton = null; + summaryRows.clear(); + packRows.clear(); + scamPingRows.clear(); + globalErrorMessage = null; + resourcePackWarningMessage = null; + + if (!applyCompleted) { + rowStatuses.clear(); + rowErrors.clear(); + } + + var font = Minecraft.getInstance().font; + int rowWidth = getWidth() - PADDING * 2 - SCROLL_BAR_WIDTH; + boolean scamLoaded = FabricLoader.getInstance().isModLoaded("scamscreener"); + + addComponent(new TextComponent(PADDING, PADDING, + Component.translatable("gui.packcore.wizard.confirm.title"), GuiColors.NAME_DEFAULT)); + + int buttonY = getHeight() - PADDING - BUTTON_HEIGHT; + boolean showWarning = requiresWorldJoin(); + int warningY = buttonY - (showWarning ? font.lineHeight + BUTTON_GAP : 0); + + if (showWarning) { + addComponent(new MultiLineTextComponent(PADDING, warningY, getWidth() - PADDING * 2 - SCROLL_BAR_WIDTH, + Component.translatable("gui.packcore.wizard.confirm.world_join_required"), GuiColors.WARNING)); + } + + int scrollTop = PADDING + font.lineHeight + PADDING; + int scrollHeight = (showWarning ? warningY - BUTTON_GAP : buttonY - BUTTON_GAP) - scrollTop; + + EmptyComponent rowContainer = new EmptyComponent(0, 0, rowWidth, 0); + int currentY = 0; + + for (SummaryEntry entry : SUMMARY_ENTRIES) { + if (!scamLoaded && entry.selectionKey().equals(ScamScreenerPage.ALERT_LEVEL_KEY)) continue; + + String selectedId = state.getSelection(entry.selectionKey()); + boolean skipped = selectedId == null; + + Component valueText = skipped + ? Component.literal("Skipped") + : entry.selectionKey().equals(ScamScreenerPage.ALERT_LEVEL_KEY) + ? ScamScreenerPage.labelForAlertLevel(selectedId) + : Component.translatable(entry.translationPrefix() + selectedId + ".name"); + + SummaryRowComponent row = new SummaryRowComponent( + 0, currentY, rowWidth, ROW_HEIGHT, + entry.statusKey(), entry.label(), valueText, + skipped ? COLOR_VALUE_SKIPPED : COLOR_VALUE_SELECTED, false); + summaryRows.add(row); + rowContainer.addComponent(row); + currentY += ROW_HEIGHT + ROW_GAP; + } + + if (scamLoaded) { + Set pingOptions = state.getMultiSelection(ScamScreenerPage.PING_OPTIONS_KEY); + SummaryRowComponent pingHeader = new SummaryRowComponent( + 0, currentY, rowWidth, ROW_HEIGHT, SCAM_PINGS_KEY, + "ScamScreener Pings", + pingOptions.isEmpty() ? Component.literal("None selected") : Component.literal(pingOptions.size() + " selected"), + pingOptions.isEmpty() ? COLOR_VALUE_SKIPPED : COLOR_VALUE_SELECTED, false); + scamPingRows.add(pingHeader); + rowContainer.addComponent(pingHeader); + currentY += ROW_HEIGHT + ROW_GAP; + + for (String optionId : pingOptions.stream().sorted(Comparator.naturalOrder()).toList()) { + SummaryRowComponent pingRow = new SummaryRowComponent( + 0, currentY, rowWidth, ROW_HEIGHT, SCAM_PINGS_KEY, + "", ScamScreenerPage.labelForPingOption(optionId), COLOR_PACK_SUBROW, true); + scamPingRows.add(pingRow); + rowContainer.addComponent(pingRow); + currentY += ROW_HEIGHT + ROW_GAP; + } + } + + Set selectedPacks = state.getSelectedResourcePacks(); + SummaryRowComponent packHeader = new SummaryRowComponent( + 0, currentY, rowWidth, ROW_HEIGHT, RESOURCE_PACKS_KEY, + "Resource Packs", + selectedPacks.isEmpty() ? Component.literal("None selected") : Component.literal(selectedPacks.size() + " selected"), + selectedPacks.isEmpty() ? COLOR_VALUE_SKIPPED : COLOR_VALUE_SELECTED, false); + packRows.add(packHeader); + rowContainer.addComponent(packHeader); + currentY += ROW_HEIGHT + ROW_GAP; + + for (String packId : selectedPacks) { + SummaryRowComponent packRow = new SummaryRowComponent( + 0, currentY, rowWidth, ROW_HEIGHT, RESOURCE_PACKS_KEY + ":" + packId, + "", Component.literal(packId), COLOR_PACK_SUBROW, true); + packRows.add(packRow); + rowContainer.addComponent(packRow); + currentY += ROW_HEIGHT + ROW_GAP; + } + + rowContainer.setHeight(currentY); + addComponent(GuiHelper.scrollWrapped(PADDING, scrollTop, getWidth() - PADDING * 2, scrollHeight, + scroll -> scroll.addComponent(rowContainer))); + + applyButton = new CustomButtonWidget( + (getWidth() - BUTTON_WIDTH) / 2, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, + Component.translatable("gui.packcore.wizard.confirm.apply_all_configs"), + APPLY_BUTTON_SPRITES, btn -> applyAll()); + addWidget(applyButton); + + if (applyCompleted) { + applyButton.active = false; + refreshRowStatuses(); + } + } + + private void applyAll() { + LOGGER.info("Applying wizard selections..."); + globalErrorMessage = null; + boolean anyError = false; + + anyError |= runStep(MainMenuDesignPage.STATE_KEY, () -> applyMainMenuDesign(state.getSelection(MainMenuDesignPage.STATE_KEY))); + anyError |= runStep(PerformancePage.STATE_KEY, () -> applyPerformanceProfile(state.getSelection(PerformancePage.STATE_KEY))); + anyError |= runStep(TabDesignPage.STATE_KEY, () -> applyTabDesign(state.getSelection(TabDesignPage.STATE_KEY))); + anyError |= runStep(ItemBackgroundPage.STATE_KEY, () -> applyItemBackground(state.getSelection(ItemBackgroundPage.STATE_KEY))); + anyError |= runStep(StorageDesignPage.STATE_KEY, () -> applyStorageDesign(state.getSelection(StorageDesignPage.STATE_KEY))); + if (FabricLoader.getInstance().isModLoaded("scaleme")) { + anyError |= runStep(SwordBlockPage.STATE_KEY, () -> applySwordBlock(state.getSelection(SwordBlockPage.STATE_KEY))); + } + + if (FabricLoader.getInstance().isModLoaded("scamscreener")) { + anyError |= runStep(ScamScreenerPage.ALERT_LEVEL_KEY, () -> applyScamScreener( + state.getSelection(ScamScreenerPage.ALERT_LEVEL_KEY), + state.getMultiSelection(ScamScreenerPage.PING_OPTIONS_KEY))); + } + + anyError |= runStep(RESOURCE_PACKS_KEY, () -> applyResourcePacksGuarded(state.getSelectedResourcePacks())); + + applyCompleted = !anyError; + + if (!anyError) { + applyButton.active = false; + if (onApplySucceeded != null) onApplySucceeded.run(); + } else { + globalErrorMessage = "Some settings failed to apply — see highlighted rows and check logs."; + } + + refreshRowStatuses(); + } + + private boolean runStep(String key, Runnable step) { + try { + step.run(); + rowStatuses.put(key, RowStatus.SUCCESS); + return false; + } catch (Exception e) { + rowStatuses.put(key, RowStatus.ERROR); + rowErrors.put(key, e.getMessage()); + LOGGER.error("Failed to apply \"{}\": {}", key, e.getMessage(), e); + return true; + } + } + + private void refreshRowStatuses() { + for (SummaryRowComponent row : summaryRows) { + row.setStatus(rowStatuses.get(row.getKey()), rowErrors.get(row.getKey())); + } + RowStatus scamStatus = rowStatuses.get(ScamScreenerPage.ALERT_LEVEL_KEY); + String scamError = rowErrors.get(ScamScreenerPage.ALERT_LEVEL_KEY); + for (SummaryRowComponent row : scamPingRows) { + row.setStatus(scamStatus, scamError); + } + RowStatus packStatus = rowStatuses.get(RESOURCE_PACKS_KEY); + String packError = rowErrors.get(RESOURCE_PACKS_KEY); + for (SummaryRowComponent row : packRows) { + row.setStatus(packStatus, packError); + } + } + + private void applyMainMenuDesign(String selectedId) { + if (selectedId == null) return; + PackCoreConfig.menuStyle = switch (selectedId) { + case "modern" -> PackCoreConfig.MenuStyle.MODERN; + case "modern_minimal" -> PackCoreConfig.MenuStyle.MODERN_MINIMAL; + case "minimal" -> PackCoreConfig.MenuStyle.MINIMAL; + default -> throw new RuntimeException("Unknown menu design ID: " + selectedId); + }; + MidnightConfig.write(MOD_ID); + } + + private void applyPerformanceProfile(String selectedId) { + if (selectedId == null) return; + PerformanceProfileService.PerformanceProfile profile = switch (selectedId) { + case "max_fps" -> PerformanceProfileService.PerformanceProfile.PERFORMANCE; + case "balanced" -> PerformanceProfileService.PerformanceProfile.BALANCED; + case "quality" -> PerformanceProfileService.PerformanceProfile.QUALITY; + case "quality_performance_shaders" -> PerformanceProfileService.PerformanceProfile.SHADERS_PERFORMANCE; + case "quality_quality_shaders" -> PerformanceProfileService.PerformanceProfile.SHADERS_QUALITY; + default -> throw new RuntimeException("Unknown profile ID: " + selectedId); + }; + if (!PerformanceProfileService.applyAll(profile)) { + throw new RuntimeException("One or more integrations failed for profile: " + profile.getDisplayName()); + } + } + + private void applyTabDesign(String selectedId) { + if (selectedId == null) return; + TabDesignManager.TabDesign design = switch (selectedId) { + case "compact" -> TabDesignManager.TabDesign.COMPACT; + case "fancy" -> TabDesignManager.TabDesign.FANCY; + default -> throw new RuntimeException("Unknown tab design ID: " + selectedId); + }; + if (!TabDesignManager.apply(design)) throw new RuntimeException("Failed to apply tab design: " + selectedId); + } + + private void applyItemBackground(String selectedId) { + if (selectedId == null) return; + ItemBackgroundManager.ItemBackground background = switch (selectedId) { + case "none" -> ItemBackgroundManager.ItemBackground.NONE; + case "circle" -> ItemBackgroundManager.ItemBackground.CIRCLE; + case "square" -> ItemBackgroundManager.ItemBackground.SQUARE; + default -> throw new RuntimeException("Unknown item background ID: " + selectedId); + }; + if (!ItemBackgroundManager.apply(background)) throw new RuntimeException("Failed to apply item background: " + selectedId); + } + + private void applyStorageDesign(String selectedId) { + if (selectedId == null) return; + StorageDesignManager.StorageDesign design = switch (selectedId) { + case "overlay" -> StorageDesignManager.StorageDesign.OVERLAY; + case "vanilla" -> StorageDesignManager.StorageDesign.VANILLA; + default -> throw new RuntimeException("Unknown storage design ID: " + selectedId); + }; + if (!StorageDesignManager.apply(design)) throw new RuntimeException("Failed to apply storage design: " + selectedId); + } + + private void applySwordBlock(String selectedId) { + if (selectedId == null) return; + ScaleMeConfig.enableSwordBlock = selectedId.equals("enabled"); + MidnightConfig.write("scaleme"); + } + + private void applyScamScreener(String selectedId, Set pingOptions) { + if (selectedId == null && pingOptions.isEmpty()) return; + String riskLevel = selectedId != null ? selectedId : ScamScreenerConfigurator.defaultSettings().minimumRiskLevel(); + if (!ScamScreenerConfigurator.apply(riskLevel, + pingOptions.contains("risk_warning"), + pingOptions.contains("blacklist_warning"))) { + throw new RuntimeException("Failed to update ScamScreener settings"); + } + } + + private void applyResourcePacksGuarded(Set packIds) { + if (packIds.isEmpty()) return; + + Set hypixel = packIds.stream() + .filter(this::isHypixelPlusId) + .collect(Collectors.toSet()); + + boolean missingXss = !JvmArgs.hasXssAtLeast(4L * 1024 * 1024); + + if (missingXss && !hypixel.isEmpty()) { + // Build a detailed error that names the launcher and gives fix steps. + JvmArgs.Launcher launcher = JvmArgs.detectLauncher(); + String instructions = JvmArgs.xss4MInstructions(launcher); + + Set filtered = new LinkedHashSet<>(packIds); + filtered.removeAll(hypixel); + + if (!filtered.isEmpty()) { + ResourcePackManager.apply(filtered); + // Partial apply — warn but don't throw; other packs succeeded. + resourcePackWarningMessage = + "Hypixel+ was skipped because -Xss4M is not set.\n" + + instructions; + } else { + // Nothing was applied — surface as a hard error so the row goes red. + throw new RuntimeException( + "Hypixel+ could not be applied — -Xss4M JVM argument is missing.\n" + + instructions + ); + } + return; + } + + ResourcePackManager.apply(packIds); + } + + private boolean requiresWorldJoin() { + return (state.getSelection(TabDesignPage.STATE_KEY) != null && FabricLoader.getInstance().isModLoaded("skyhanni")) + || (state.getSelection(StorageDesignPage.STATE_KEY) != null && FabricLoader.getInstance().isModLoaded("firmament")); + } + + private boolean isHypixelPlusId(String packId) { + return packId != null && packId.toLowerCase(Locale.ROOT).contains("hypixel"); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + if (globalErrorMessage == null) return; + var font = Minecraft.getInstance().font; + int errorY = getTotalY() + getHeight() - PADDING - BUTTON_HEIGHT + BUTTON_HEIGHT + 4; + graphics.drawCenteredString(font, globalErrorMessage, getTotalX() + getWidth() / 2, errorY, GuiColors.ERROR); + } + + private static class SummaryRowComponent extends EmptyComponent { + + private final String key; + private final String label; + private final Component value; + private final int valueColor; + private final boolean isSubRow; + + private RowStatus status; + private String cachedRightText; + private int cachedRightColor; + private int cachedRightWidth; + + SummaryRowComponent(int x, int y, int width, int height, + String key, String label, Component value, int valueColor, boolean isSubRow) { + super(x, y, width, height); + this.key = key; + this.label = label; + this.value = value; + this.valueColor = valueColor; + this.isSubRow = isSubRow; + cacheRightSide(value.getString(), valueColor); + } + + String getKey() { return key; } + + void setStatus(RowStatus newStatus, String error) { + status = newStatus; + if (newStatus == RowStatus.ERROR && error != null && !isSubRow) { + cacheRightSide("Error: " + error, GuiColors.ERROR); + } else { + cacheRightSide(value.getString(), valueColor); + } + } + + private void cacheRightSide(String text, int color) { + cachedRightText = text; + cachedRightColor = color; + cachedRightWidth = Minecraft.getInstance().font.width(text); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + int x = getTotalX(); + int y = getTotalY(); + int w = getWidth(); + int h = getHeight(); + int leftInset = isSubRow ? 20 : 0; + + int borderColor = status == RowStatus.SUCCESS ? GuiColors.SUCCESS + : status == RowStatus.ERROR ? GuiColors.ERROR + : GuiColors.BORDER_IDLE; + + graphics.fill(x + leftInset, y, x + w, y + h, GuiColors.ROW_BACKGROUND); + GuiHelper.drawBorder(graphics, x + leftInset, y, w - leftInset, h, borderColor); + + var font = Minecraft.getInstance().font; + int textY = y + (h - font.lineHeight) / 2; + + if (!label.isEmpty()) { + graphics.drawString(font, label, x + leftInset + 8, textY, GuiColors.NAME_DEFAULT, false); + } + + graphics.drawString(font, cachedRightText, x + w - cachedRightWidth - 8, textY, cachedRightColor, false); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ItemBackgroundPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ItemBackgroundPage.java new file mode 100644 index 0000000..734f5b3 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ItemBackgroundPage.java @@ -0,0 +1,55 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.github.kd_gaming1.packcore.gui.component.OptionCardGrid; +import com.github.kd_gaming1.packcore.gui.wizard.BaseCardGridPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** Step 4 — Item Background Design chooser. */ +public class ItemBackgroundPage extends BaseCardGridPage { + + public static final String STATE_KEY = "itemBackground"; + + public ItemBackgroundPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return Component.translatable("gui.packcore.wizard.page.item_background.title"); } + @Override protected String stateKey() { return STATE_KEY; } + @Override protected int columns() { return 3; } + @Override protected Component explanation() { return Component.translatable("gui.packcore.wizard.page.item_background.explanation"); } + @Override protected List options() { return ItemBackgroundOption.all(); } + + @Override + protected OptionCardGrid.CardDescriptor descriptor() { + return OptionCardGrid.CardDescriptor.of( + ItemBackgroundOption::id, ItemBackgroundOption::name, ItemBackgroundOption::description, + ItemBackgroundOption::previewTexture, ItemBackgroundOption::previewTextureWidth, ItemBackgroundOption::previewTextureHeight + ); + } + + public record ItemBackgroundOption( + String id, Component name, Component description, + Identifier previewTexture, int previewTextureWidth, int previewTextureHeight + ) { + public static List all() { + return List.of(fromId("none"), fromId("circle"), fromId("square")); + } + + private static ItemBackgroundOption fromId(String id) { + return new ItemBackgroundOption( + id, + Component.translatable("gui.packcore.wizard.item_background." + id + ".name"), + Component.translatable("gui.packcore.wizard.item_background." + id + ".desc"), + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/sprites/wizard/item_background_preview/" + id + ".png"), + 555, 666 + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/MainMenuDesignPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/MainMenuDesignPage.java new file mode 100644 index 0000000..bf5fa4f --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/MainMenuDesignPage.java @@ -0,0 +1,58 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.github.kd_gaming1.packcore.gui.component.OptionCardGrid; +import com.github.kd_gaming1.packcore.gui.wizard.BaseCardGridPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** Step 1 — Main Menu Design chooser. */ +public class MainMenuDesignPage extends BaseCardGridPage { + + public static final String STATE_KEY = "mainMenuDesign"; + + private static final int PREVIEW_WIDTH = 320; + private static final int PREVIEW_HEIGHT = 180; + + public MainMenuDesignPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return Component.translatable("gui.packcore.wizard.page.main_menu_design.title"); } + @Override protected String stateKey() { return STATE_KEY; } + @Override protected int columns() { return 3; } + @Override protected Component explanation() { return Component.translatable("gui.packcore.wizard.page.main_menu_design.explanation"); } + @Override protected List options() { return MenuDesignOption.all(); } + + @Override + protected OptionCardGrid.CardDescriptor descriptor() { + return OptionCardGrid.CardDescriptor.of( + MenuDesignOption::id, MenuDesignOption::name, MenuDesignOption::description, + MenuDesignOption::previewTexture, MenuDesignOption::previewTextureWidth, MenuDesignOption::previewTextureHeight + ); + } + + public record MenuDesignOption( + String id, Component name, Component description, + Identifier previewTexture, int previewTextureWidth, int previewTextureHeight + ) { + public static List all() { + return List.of(fromId("modern"), fromId("modern_minimal"), fromId("minimal")); + } + + private static MenuDesignOption fromId(String id) { + return new MenuDesignOption( + id, + Component.translatable("gui.packcore.wizard.menu_design." + id + ".name"), + Component.translatable("gui.packcore.wizard.menu_design." + id + ".desc"), + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/sprites/wizard/menu_preview/" + id + ".png"), + PREVIEW_WIDTH, PREVIEW_HEIGHT + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/PerformancePage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/PerformancePage.java new file mode 100644 index 0000000..74a9d1f --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/PerformancePage.java @@ -0,0 +1,106 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.gui.component.MarkdownComponent; +import com.github.kd_gaming1.packcore.gui.component.OptionSelectList; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.gui.wizard.BaseWizardPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.List; + +/** + * Step 2 — Performance profile selection. + * Left column: scrollable Markdown guide. Right column: selectable profile list. + */ +public class PerformancePage extends BaseWizardPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/PerformancePage"); + + private static final Component PAGE_TITLE = Component.translatable("gui.packcore.wizard.page.performance.title"); + + public static final String STATE_KEY = "performanceProfile"; + + private static final int PADDING = 16; + private static final int COLUMN_GAP = 14; + private static final int SCROLL_BAR_WIDTH = 8; + + private static final String FALLBACK_MARKDOWN = "*No performance guide found.*"; + private static final Path MARKDOWN_PATH = PackCore.PACKCORE_DIR.resolve("markdown").resolve("performance.md"); + + public PerformancePage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return PAGE_TITLE; } + @Override public boolean validate() { return true; } + @Override public void onExit() { } + + @Override + public void onEnter() { + this.clearComponents(); + + int availableWidth = getWidth() - PADDING * 2; + int availableHeight = getHeight() - PADDING * 2; + int columnWidth = (availableWidth - COLUMN_GAP) / 2; + + EmptyComponent leftColumn = new EmptyComponent(PADDING, PADDING, columnWidth, availableHeight); + EmptyComponent rightColumn = new EmptyComponent(PADDING + columnWidth + COLUMN_GAP, PADDING, columnWidth, availableHeight); + + buildLeftColumn(leftColumn, columnWidth, availableHeight); + buildRightColumn(rightColumn, columnWidth, availableHeight); + + this.addComponent(leftColumn); + this.addComponent(rightColumn); + } + + private void buildLeftColumn(EmptyComponent column, int columnWidth, int columnHeight) { + MarkdownComponent markdownComp = new MarkdownComponent( + 0, 0, columnWidth - SCROLL_BAR_WIDTH - (PADDING / 2), GuiHelper.loadMarkdown(MARKDOWN_PATH, FALLBACK_MARKDOWN, LOGGER) + ); + column.addComponent(GuiHelper.scrollWrapped(0, 0, columnWidth, columnHeight, + scroll -> scroll.addComponent(markdownComp))); + } + + private void buildRightColumn(EmptyComponent column, int columnWidth, int columnHeight) { + OptionSelectList list = new OptionSelectList<>( + 0, 0, columnWidth, columnHeight, + PerformanceProfile.all(), + OptionSelectList.RowDescriptor.of( + PerformanceProfile::id, + PerformanceProfile::name, + PerformanceProfile::description + ), + state.getSelection(STATE_KEY), + selected -> state.setSelection(STATE_KEY, selected.id()) + ); + column.addComponent(list); + } + + public record PerformanceProfile(String id, Component name, Component description) { + + public static List all() { + return List.of( + fromId("max_fps"), + fromId("balanced"), + fromId("quality"), + fromId("quality_performance_shaders"), + fromId("quality_quality_shaders") + ); + } + + private static PerformanceProfile fromId(String id) { + return new PerformanceProfile( + id, + Component.translatable("gui.packcore.wizard.performance." + id + ".name"), + Component.translatable("gui.packcore.wizard.performance." + id + ".desc") + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ResourcePackPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ResourcePackPage.java new file mode 100644 index 0000000..7097220 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ResourcePackPage.java @@ -0,0 +1,193 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.gui.component.MultiSelectList; +import com.github.kd_gaming1.packcore.gui.component.MarkdownComponent; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.gui.wizard.BaseWizardPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import com.github.kd_gaming1.packcore.util.JvmArgs; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * Step 6 — Resource Pack chooser. + */ +public class ResourcePackPage extends BaseWizardPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ResourcePackPage"); + + private static final Component PAGE_TITLE = Component.translatable("gui.packcore.wizard.page.resource_pack.title"); + + private static final int PADDING = 16; + private static final int COLUMN_GAP = 14; + private static final int SCROLL_BAR_WIDTH = 8; + + /** Minimum thread stack size required by Hypixel+ (4 MB). */ + private static final long XSS_THRESHOLD_BYTES = 4L * 1024 * 1024; + + private static final String FALLBACK_MARKDOWN = "*No resource pack guide found.*"; + private static final Path MARKDOWN_PATH = PackCore.PACKCORE_DIR.resolve("markdown").resolve("resource_packs.md"); + + public ResourcePackPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return PAGE_TITLE; } + @Override public boolean validate() { return true; } + @Override public void onExit() { } + + @Override + public void onEnter() { + this.clearComponents(); + + int availableWidth = getWidth() - (PADDING * 2); + int availableHeight = getHeight() - (PADDING * 2); + int columnWidth = (availableWidth - COLUMN_GAP) / 2; + + EmptyComponent leftColumn = new EmptyComponent(PADDING, PADDING, columnWidth, availableHeight); + EmptyComponent rightColumn = new EmptyComponent(PADDING + columnWidth + COLUMN_GAP, PADDING, columnWidth, availableHeight); + + setupMarkdownColumn(leftColumn, columnWidth, availableHeight); + setupSelectionColumn(rightColumn, columnWidth, availableHeight); + + this.addComponent(leftColumn); + this.addComponent(rightColumn); + } + + private void setupMarkdownColumn(EmptyComponent column, int width, int height) { + MarkdownComponent markdownComp = new MarkdownComponent( + 0, 0, width - SCROLL_BAR_WIDTH - (PADDING / 2), + GuiHelper.loadMarkdown(MARKDOWN_PATH, FALLBACK_MARKDOWN, LOGGER) + ); + column.addComponent(GuiHelper.scrollWrapped(0, 0, width, height, + scroll -> scroll.addComponent(markdownComp))); + } + + private void setupSelectionColumn(EmptyComponent column, int width, int height) { + boolean missingXss = !JvmArgs.hasXssAtLeast(XSS_THRESHOLD_BYTES); + List packs = discoverUserPacks(missingXss); + + if (packs.isEmpty()) { + column.addComponent(new MultiLineTextComponent( + 0, 0, width, + Component.translatable("gui.packcore.wizard.resource_pack.none_found"), + 0xFF777777 + )); + return; + } + + // If -Xss4M is missing, show a banner above the list explaining how to fix it. + int listOffsetY = 0; + if (missingXss && packs.stream().anyMatch(ResourcePackEntry::requiresXss)) { + JvmArgs.Launcher launcher = JvmArgs.detectLauncher(); + Component banner = buildXssBanner(launcher); + + // MultiLineTextComponent calculates its own height based on wrapped lines — + // read it back after construction rather than manually counting newlines. + MultiLineTextComponent bannerComp = new MultiLineTextComponent(0, 0, width, banner, 0xFFFFAA00); + column.addComponent(bannerComp); + listOffsetY = bannerComp.getHeight() + 6; + height -= listOffsetY; + } + + MultiSelectList list = new MultiSelectList<>( + 0, listOffsetY, width, height, + packs, + MultiSelectList.RowDescriptor.of( + ResourcePackEntry::id, + ResourcePackEntry::name, + ResourcePackEntry::description + ), + state.getSelectedResourcePacks(), + selected -> state.addResourcePack(selected.id()), + deselected -> state.removeResourcePack(deselected.id()) + ); + column.addComponent(list); + } + + /** + * Builds a multi-line warning banner using translatable components so the + * text is localisation-friendly. The launcher name is injected as a %s arg. + */ + private static Component buildXssBanner(JvmArgs.Launcher launcher) { + String fixKey = switch (launcher) { + case PRISM_POLYMC -> "gui.packcore.wizard.resource_pack.xss_fix.prism"; + case CURSEFORGE -> "gui.packcore.wizard.resource_pack.xss_fix.curseforge"; + case ATLAUNCHER -> "gui.packcore.wizard.resource_pack.xss_fix.atlauncher"; + case MODRINTH -> "gui.packcore.wizard.resource_pack.xss_fix.modrinth"; + case OFFICIAL -> "gui.packcore.wizard.resource_pack.xss_fix.official"; + default -> "gui.packcore.wizard.resource_pack.xss_fix.unknown"; + }; + + return Component.empty() + .append(Component.translatable("gui.packcore.wizard.resource_pack.xss_banner_header")) + .append(Component.literal("\n")) + .append(Component.translatable("gui.packcore.wizard.resource_pack.xss_banner_launcher", + launcher.displayName())) + .append(Component.literal("\n")) + .append(Component.translatable(fixKey)) + .append(Component.literal("\n")) + .append(Component.translatable("gui.packcore.wizard.resource_pack.xss_fix.restart")); + } + + private List discoverUserPacks(boolean missingXss) { + return Minecraft.getInstance() + .getResourcePackRepository() + .getAvailablePacks() + .stream() + .filter(this::isUserSelectablePack) + .sorted(Comparator.comparing(pack -> pack.getTitle().getString())) + .map(pack -> { + boolean isHypixel = isHypixelPlusPack(pack); + return ResourcePackEntry.fromPack(pack, isHypixel, missingXss && isHypixel); + }) + .toList(); + } + + private boolean isUserSelectablePack(Pack pack) { + return pack.getPackSource() == PackSource.DEFAULT && !pack.getId().equals("vanilla"); + } + + /** Returns {@code true} if the pack appears to be Hypixel+. */ + private static boolean isHypixelPlusPack(Pack pack) { + String title = pack.getTitle().getString().toLowerCase(Locale.ROOT); + String id = pack.getId().toLowerCase(Locale.ROOT); + return title.contains("hypixel") || id.contains("hypixel"); + } + + public record ResourcePackEntry(String id, Component name, Component description, boolean requiresXss) { + + static ResourcePackEntry fromPack(Pack pack, boolean isHypixel, boolean warnXss) { + Component desc = pack.getDescription(); + if (desc.getString().isBlank()) { + desc = Component.translatable("gui.packcore.wizard.resource_pack.no_description"); + } + + if (warnXss) { + // Per-row warning is intentionally short; the banner above gives full instructions. + MutableComponent warning = Component.translatable("gui.packcore.wizard.resource_pack.xss_warning") + .withStyle(s -> s.withColor(0xFFAA00)); + desc = Component.empty() + .append(desc) + .append(Component.literal("\n")) + .append(warning); + } + + return new ResourcePackEntry(pack.getId(), pack.getTitle(), desc, isHypixel); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ScamScreenerPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ScamScreenerPage.java new file mode 100644 index 0000000..738e93d --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/ScamScreenerPage.java @@ -0,0 +1,223 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.component.text.multiline.MultiLineTextComponent; +import com.github.kd_gaming1.packcore.gui.component.MultiSelectList; +import com.github.kd_gaming1.packcore.gui.component.OptionSelectList; +import com.github.kd_gaming1.packcore.gui.wizard.BaseWizardPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import com.github.kd_gaming1.packcore.integration.ScamScreenerConfigurator; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +import java.util.List; +import java.util.Locale; + +public class ScamScreenerPage extends BaseWizardPage { + + public static final String ALERT_LEVEL_KEY = "scamScreenerMinimumRiskLevel"; + public static final String PING_OPTIONS_KEY = "scamScreenerPingOptions"; + + private static final Component PAGE_TITLE = Component.translatable("gui.packcore.wizard.page.scamscreener.title"); + + private static final int PADDING = 16; + private static final int COLUMN_GAP = 14; + private static final int SECTION_GAP = 10; + private static final int LABEL_GAP = 6; + private static final int COLOR_LABEL = 0xFFCCCCCC; + private static final int COLOR_HINT = 0xFF777777; + + public ScamScreenerPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return PAGE_TITLE; } + @Override public boolean validate() { return true; } + @Override public void onExit() {} + + @Override + public void onEnter() { + this.clearComponents(); + seedInitialState(); + + int availableWidth = getWidth() - PADDING * 2; + int availableHeight = getHeight() - PADDING * 2; + + MultiLineTextComponent intro = new MultiLineTextComponent( + PADDING, PADDING, availableWidth, + Component.translatable("gui.packcore.wizard.page.scamscreener.explanation"), + COLOR_HINT + ); + this.addComponent(intro); + + int columnsY = PADDING + intro.getHeight() + SECTION_GAP; + int columnHeight = availableHeight - intro.getHeight() - SECTION_GAP; + int columnWidth = (availableWidth - COLUMN_GAP) / 2; + + EmptyComponent leftColumn = new EmptyComponent(PADDING, columnsY, columnWidth, columnHeight); + EmptyComponent rightColumn = new EmptyComponent(PADDING + columnWidth + COLUMN_GAP, columnsY, columnWidth, columnHeight); + + buildAlertLevelColumn(leftColumn, columnWidth, columnHeight); + buildPingColumn(rightColumn, columnWidth, columnHeight); + + this.addComponent(leftColumn); + this.addComponent(rightColumn); + } + + public static Component labelForPingOption(String optionId) { + return pingOptions().stream() + .filter(option -> option.id().equals(optionId)) + .findFirst() + .map(PingOption::name) + .orElse(Component.literal(optionId)); + } + + public static Component labelForAlertLevel(String optionId) { + return AlertLevelOption.fromId(optionId).name(); + } + + private void seedInitialState() { + ScamScreenerConfigurator.RuntimeSettings settings = ScamScreenerConfigurator.loadSettings(); + + if (state.getSelection(ALERT_LEVEL_KEY) == null) { + state.setSelection(ALERT_LEVEL_KEY, settings.minimumRiskLevel()); + } + + if (state.getMultiSelection(PING_OPTIONS_KEY).isEmpty()) { + if (settings.pingOnRiskWarning()) { + state.addMultiSelection(PING_OPTIONS_KEY, "risk_warning"); + } + if (settings.pingOnBlacklistWarning()) { + state.addMultiSelection(PING_OPTIONS_KEY, "blacklist_warning"); + } + } + } + + private void buildAlertLevelColumn(EmptyComponent column, int width, int height) { + var font = Minecraft.getInstance().font; + List alertLevels = ScamScreenerConfigurator.availableAlertLevels().stream() + .map(AlertLevelOption::fromId) + .toList(); + + String selectedAlertLevel = state.getSelection(ALERT_LEVEL_KEY); + String selectedAlertLevelCandidate = selectedAlertLevel; + boolean selectedValueExists = alertLevels.stream().anyMatch(option -> option.id().equals(selectedAlertLevelCandidate)); + if (!selectedValueExists && !alertLevels.isEmpty()) { + selectedAlertLevel = alertLevels.getFirst().id(); + state.setSelection(ALERT_LEVEL_KEY, selectedAlertLevel); + } + + column.addComponent(new TextComponent( + 0, 0, + Component.translatable("gui.packcore.wizard.scamscreener.alerts.heading"), + COLOR_LABEL + )); + + int contentY = font.lineHeight + LABEL_GAP; + int contentHeight = height - contentY; + + OptionSelectList list = new OptionSelectList<>( + 0, contentY, width, contentHeight, + alertLevels, + OptionSelectList.RowDescriptor.of( + AlertLevelOption::id, + AlertLevelOption::name, + AlertLevelOption::description + ), + selectedAlertLevel, + selected -> state.setSelection(ALERT_LEVEL_KEY, selected.id()) + ); + column.addComponent(list); + } + + private void buildPingColumn(EmptyComponent column, int width, int height) { + var font = Minecraft.getInstance().font; + int currentY = 0; + + column.addComponent(new TextComponent( + 0, currentY, + Component.translatable("gui.packcore.wizard.scamscreener.pings.heading"), + COLOR_LABEL + )); + currentY += font.lineHeight + LABEL_GAP; + + MultiLineTextComponent pingsHint = new MultiLineTextComponent( + 0, currentY, + width, + Component.translatable("gui.packcore.wizard.scamscreener.pings.hint"), + COLOR_HINT + ); + column.addComponent(pingsHint); + currentY += pingsHint.getHeight() + LABEL_GAP; + + int pingListHeight = Math.max(100, height - currentY); + MultiSelectList pingList = new MultiSelectList<>( + 0, currentY, width, pingListHeight, + pingOptions(), + MultiSelectList.RowDescriptor.of( + PingOption::id, + PingOption::name, + PingOption::description + ), + state.getMultiSelection(PING_OPTIONS_KEY), + selected -> state.addMultiSelection(PING_OPTIONS_KEY, selected.id()), + deselected -> state.removeMultiSelection(PING_OPTIONS_KEY, deselected.id()) + ); + column.addComponent(pingList); + } + + private static List pingOptions() { + return List.of( + new PingOption( + "risk_warning", + Component.translatable("gui.packcore.wizard.scamscreener.pings.risk.name"), + Component.translatable("gui.packcore.wizard.scamscreener.pings.risk.desc") + ), + new PingOption( + "blacklist_warning", + Component.translatable("gui.packcore.wizard.scamscreener.pings.blacklist.name"), + Component.translatable("gui.packcore.wizard.scamscreener.pings.blacklist.desc") + ) + ); + } + + public record AlertLevelOption(String id, Component name, Component description) { + private static AlertLevelOption fromId(String id) { + String normalizedId = id.toUpperCase(Locale.ROOT); + String baseKey = "gui.packcore.wizard.scamscreener.minimum_risk." + normalizedId; + return new AlertLevelOption( + normalizedId, + switch (normalizedId) { + case "LOW", "MEDIUM", "HIGH", "CRITICAL" -> Component.translatable(baseKey + ".name"); + default -> Component.literal(prettyLabel(normalizedId)); + }, + switch (normalizedId) { + case "LOW", "MEDIUM", "HIGH", "CRITICAL" -> Component.translatable(baseKey + ".desc"); + default -> Component.literal("Use ScamScreener's " + prettyLabel(normalizedId) + " warning threshold."); + } + ); + } + + private static String prettyLabel(String id) { + String[] parts = id.split("_"); + StringBuilder builder = new StringBuilder(); + for (String s : parts) { + if (s.isEmpty()) { + continue; + } + + if (!builder.isEmpty()) { + builder.append(' '); + } + + String part = s.toLowerCase(Locale.ROOT); + builder.append(Character.toUpperCase(part.charAt(0))).append(part.substring(1)); + } + return builder.toString(); + } + } + + public record PingOption(String id, Component name, Component description) {} +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/StorageDesignPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/StorageDesignPage.java new file mode 100644 index 0000000..0a9fad5 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/StorageDesignPage.java @@ -0,0 +1,55 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.github.kd_gaming1.packcore.gui.component.OptionCardGrid; +import com.github.kd_gaming1.packcore.gui.wizard.BaseCardGridPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** Step 5 — Storage Design chooser. */ +public class StorageDesignPage extends BaseCardGridPage { + + public static final String STATE_KEY = "storageDesign"; + + public StorageDesignPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return Component.translatable("gui.packcore.wizard.page.storage_design.title"); } + @Override protected String stateKey() { return STATE_KEY; } + @Override protected int columns() { return 2; } + @Override protected Component explanation() { return Component.translatable("gui.packcore.wizard.page.storage_design.explanation"); } + @Override protected List options() { return StorageDesignOption.all(); } + + @Override + protected OptionCardGrid.CardDescriptor descriptor() { + return OptionCardGrid.CardDescriptor.of( + StorageDesignOption::id, StorageDesignOption::name, StorageDesignOption::description, + StorageDesignOption::previewTexture, StorageDesignOption::previewTextureWidth, StorageDesignOption::previewTextureHeight + ); + } + + public record StorageDesignOption( + String id, Component name, Component description, + Identifier previewTexture, int previewTextureWidth, int previewTextureHeight + ) { + public static List all() { + return List.of(fromId("overlay"), fromId("vanilla")); + } + + private static StorageDesignOption fromId(String id) { + return new StorageDesignOption( + id, + Component.translatable("gui.packcore.wizard.storage_design." + id + ".name"), + Component.translatable("gui.packcore.wizard.storage_design." + id + ".desc"), + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/sprites/wizard/storage_preview/" + id + ".png"), + 320, 180 + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/SwordBlockPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/SwordBlockPage.java new file mode 100644 index 0000000..e9dc0cb --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/SwordBlockPage.java @@ -0,0 +1,55 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.github.kd_gaming1.packcore.gui.component.OptionCardGrid; +import com.github.kd_gaming1.packcore.gui.wizard.BaseCardGridPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** Sword Block toggle — lets the user enable or disable ScaleMe's sword block animation. */ +public class SwordBlockPage extends BaseCardGridPage { + + public static final String STATE_KEY = "swordBlock"; + + public SwordBlockPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return Component.translatable("gui.packcore.wizard.page.sword_block.title"); } + @Override protected String stateKey() { return STATE_KEY; } + @Override protected int columns() { return 2; } + @Override protected Component explanation() { return Component.translatable("gui.packcore.wizard.page.sword_block.explanation"); } + @Override protected List options() { return SwordBlockOption.all(); } + + @Override + protected OptionCardGrid.CardDescriptor descriptor() { + return OptionCardGrid.CardDescriptor.of( + SwordBlockOption::id, SwordBlockOption::name, SwordBlockOption::description, + SwordBlockOption::previewTexture, SwordBlockOption::previewTextureWidth, SwordBlockOption::previewTextureHeight + ); + } + + public record SwordBlockOption( + String id, Component name, Component description, + Identifier previewTexture, int previewTextureWidth, int previewTextureHeight + ) { + public static List all() { + return List.of(fromId("enabled"), fromId("disabled")); + } + + private static SwordBlockOption fromId(String id) { + return new SwordBlockOption( + id, + Component.translatable("gui.packcore.wizard.sword_block." + id + ".name"), + Component.translatable("gui.packcore.wizard.sword_block." + id + ".desc"), + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/sprites/wizard/sword_block_preview/" + id + ".png"), + 320, 180 + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/TabDesignPage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/TabDesignPage.java new file mode 100644 index 0000000..ddd29e8 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/TabDesignPage.java @@ -0,0 +1,55 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.github.kd_gaming1.packcore.gui.component.OptionCardGrid; +import com.github.kd_gaming1.packcore.gui.wizard.BaseCardGridPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import java.util.List; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +/** Step 3 — Tab Design chooser. */ +public class TabDesignPage extends BaseCardGridPage { + + public static final String STATE_KEY = "tabDesign"; + + public TabDesignPage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return Component.translatable("gui.packcore.wizard.page.tab_design.title"); } + @Override protected String stateKey() { return STATE_KEY; } + @Override protected int columns() { return 2; } + @Override protected Component explanation() { return Component.translatable("gui.packcore.wizard.page.tab_design.explanation"); } + @Override protected List options() { return TabDesignOption.all(); } + + @Override + protected OptionCardGrid.CardDescriptor descriptor() { + return OptionCardGrid.CardDescriptor.of( + TabDesignOption::id, TabDesignOption::name, TabDesignOption::description, + TabDesignOption::previewTexture, TabDesignOption::previewTextureWidth, TabDesignOption::previewTextureHeight + ); + } + + public record TabDesignOption( + String id, Component name, Component description, + Identifier previewTexture, int previewTextureWidth, int previewTextureHeight + ) { + public static List all() { + return List.of(fromId("compact"), fromId("fancy")); + } + + private static TabDesignOption fromId(String id) { + return new TabDesignOption( + id, + Component.translatable("gui.packcore.wizard.tab_design." + id + ".name"), + Component.translatable("gui.packcore.wizard.tab_design." + id + ".desc"), + Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/sprites/wizard/tab_preview/" + id + ".png"), + 320, 180 + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/WelcomePage.java b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/WelcomePage.java new file mode 100644 index 0000000..22e9ab1 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/wizard/page/WelcomePage.java @@ -0,0 +1,194 @@ +package com.github.kd_gaming1.packcore.gui.wizard.page; + +import com.daqem.uilib.gui.component.EmptyComponent; +import com.daqem.uilib.gui.component.text.TextComponent; +import com.daqem.uilib.gui.widget.ScrollContainerWidget; +import com.github.kd_gaming1.packcore.PackCore; +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.ConfigPackEntry; +import com.github.kd_gaming1.packcore.configpack.ConfigPackScanner; +import com.github.kd_gaming1.packcore.gui.component.*; +import com.github.kd_gaming1.packcore.gui.util.GuiHelper; +import com.github.kd_gaming1.packcore.gui.wizard.BaseWizardPage; +import com.github.kd_gaming1.packcore.gui.wizard.WizardNavigator; +import com.github.kd_gaming1.packcore.gui.wizard.WizardState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +/** + * Step 0 — Welcome page. + */ +public class WelcomePage extends BaseWizardPage { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/WelcomePage"); + + private static final Component TITLE = Component.translatable("gui.packcore.wizard.page.welcome.title"); + + private static final int PADDING = 16; + private static final int COLUMN_GAP = 14; + private static final int LABEL_GAP = 5; + private static final int CARD_GAP = 8; + private static final int SCROLL_BAR_ROOM = 8; + private static final int DIVIDER_GAP = 10; + private static final int HINT_GAP = 6; + + private static final int COLOR_LABEL_PRIMARY = 0xFFCCCCCC; + private static final int COLOR_HINT = 0xFF777777; + private static final int COLOR_DIVIDER = 0x44FFFFFF; + + private static final String FALLBACK_MARKDOWN = "*No welcome message found.*"; + private static final Path MARKDOWN_PATH = PackCore.PACKCORE_DIR.resolve("markdown").resolve("welcome.md"); + + private ConfigPackEntry activePack; + private ConfigSwitchOverlay overlay; + + private ScrollContainerWidget leftScroll; + private ScrollContainerWidget rightScroll; + + public WelcomePage(WizardState state, WizardNavigator navigator, int width, int height) { + super(state, navigator, width, height); + } + + @Override public Component getTitle() { return TITLE; } + @Override public boolean validate() { return true; } + @Override public void onExit() { } + + @Override + public void onEnter() { + this.clearComponents(); + + int innerWidth = getWidth() - (PADDING * 2); + int innerHeight = getHeight() - (PADDING * 2); + int columnWidth = (innerWidth - COLUMN_GAP) / 2; + + List packs = scanAvailablePacks(); + this.activePack = findActivePack(packs); + + EmptyComponent leftColumn = new EmptyComponent(PADDING, PADDING, columnWidth, innerHeight); + EmptyComponent rightColumn = new EmptyComponent(PADDING + columnWidth + COLUMN_GAP, PADDING, columnWidth, innerHeight); + + buildLeftColumn(leftColumn, columnWidth, innerHeight); + buildRightColumn(rightColumn, columnWidth, innerHeight, packs); + + this.addComponent(leftColumn); + this.addComponent(rightColumn); + + overlay = new ConfigSwitchOverlay(getWidth(), getHeight()); + overlay.setOnClose(() -> { + if (leftScroll != null) leftScroll.active = true; + if (rightScroll != null) rightScroll.active = true; + }); + this.addComponent(overlay); + } + + private void buildLeftColumn(EmptyComponent column, int columnWidth, int columnHeight) { + MarkdownComponent markdownComp = new MarkdownComponent( + 0, 0, columnWidth - SCROLL_BAR_ROOM - (PADDING / 2), GuiHelper.loadMarkdown(MARKDOWN_PATH, FALLBACK_MARKDOWN, LOGGER) + ); + leftScroll = new ScrollContainerWidget(columnWidth, columnHeight); + leftScroll.addComponent(markdownComp); + + EmptyComponent scrollWrapper = new EmptyComponent(0, 0, columnWidth, columnHeight); + scrollWrapper.addWidget(leftScroll); + column.addComponent(scrollWrapper); + } + + private void buildRightColumn(EmptyComponent column, int columnWidth, int columnHeight, List packs) { + var font = Minecraft.getInstance().font; + int lineHeight = font.lineHeight; + int currentY = 0; + + ConfigStatusCard statusCard = new ConfigStatusCard(0, currentY, columnWidth, activePack, state.migratedFromV3); + column.addComponent(statusCard); + currentY += statusCard.getHeight() + DIVIDER_GAP; + + final int dividerY = currentY; + column.addComponent(new EmptyComponent(0, dividerY, columnWidth, 1) { + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, int parentWidth, int parentHeight) { + graphics.fill(getTotalX(), getTotalY(), getTotalX() + getWidth(), getTotalY() + 1, COLOR_DIVIDER); + } + }); + currentY += 1 + DIVIDER_GAP; + + column.addComponent(new TextComponent(0, currentY, + Component.translatable("gui.packcore.wizard.card.configs.heading"), COLOR_LABEL_PRIMARY)); + currentY += lineHeight + LABEL_GAP; + + column.addComponent(new TextComponent(0, currentY, + Component.translatable("gui.packcore.wizard.card.configs.hint"), COLOR_HINT)); + currentY += lineHeight + HINT_GAP; + + int scrollHeight = columnHeight - currentY; + if (scrollHeight <= 0) return; + + if (packs.isEmpty()) { + column.addComponent(new TextComponent(0, currentY, + Component.literal("No configs found in the configs folder."), COLOR_HINT)); + return; + } + + rightScroll = new ScrollContainerWidget(columnWidth, scrollHeight); + EmptyComponent listContainer = new EmptyComponent(0, 0, columnWidth - SCROLL_BAR_ROOM, 0); + + int cardY = 0; + for (ConfigPackEntry pack : packs) { + boolean isActivePack = isSamePack(activePack, pack); + + ConfigPackCard card = new ConfigPackCard( + 0, cardY, + columnWidth - SCROLL_BAR_ROOM, + pack, isActivePack, + "✓ Active", + clickedPack -> { + if (isSamePack(activePack, clickedPack)) return; + overlay.show(activePack, clickedPack); + if (leftScroll != null) leftScroll.active = false; + if (rightScroll != null) rightScroll.active = false; + } + ); + + listContainer.addComponent(card); + cardY += card.getHeight() + CARD_GAP; + } + + listContainer.setHeight(cardY); + rightScroll.addComponent(listContainer); + + EmptyComponent scrollWrapper = new EmptyComponent(0, currentY, columnWidth, scrollHeight); + scrollWrapper.addWidget(rightScroll); + column.addComponent(scrollWrapper); + } + + /** Returns true if both packs are non-null and refer to the same zip file. */ + private static boolean isSamePack(ConfigPackEntry a, ConfigPackEntry b) { + return a != null && b != null + && a.zipPath().getFileName().toString().equals(b.zipPath().getFileName().toString()); + } + + private static ConfigPackEntry findActivePack(List packs) { + String appliedFile = PackCoreConfig.lastAppliedPackFile; + if (appliedFile == null || appliedFile.isBlank()) return null; + + return packs.stream() + .filter(pack -> pack.zipPath().getFileName().toString().equals(appliedFile)) + .findFirst() + .orElse(null); + } + + private static List scanAvailablePacks() { + try { + return new ConfigPackScanner().scanFolder(PackCore.PACKCORE_DIR.resolve("configs")); + } catch (IOException e) { + LOGGER.error("Failed to scan config packs: {}", e.getMessage()); + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/IrisConfigurator.java b/src/main/java/com/github/kd_gaming1/packcore/integration/IrisConfigurator.java new file mode 100644 index 0000000..e75f38f --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/IrisConfigurator.java @@ -0,0 +1,71 @@ +package com.github.kd_gaming1.packcore.integration; + +import net.irisshaders.iris.Iris; +import net.irisshaders.iris.config.IrisConfig; +import net.minecraft.client.Minecraft; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class IrisConfigurator { + private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + public static boolean setShaderPack(String prefix) { + String foundPack = findShaderPack(prefix); + if (foundPack == null) { + LOGGER.warn("Shader pack not found with prefix: {}", prefix); + return false; + } + return applyAndReload(foundPack, true); + } + + public static boolean disableShaders() { + return applyAndReload(null, false); + } + + private static boolean applyAndReload(String packName, boolean enabled) { + try { + IrisConfig config = Iris.getIrisConfig(); + config.setShadersEnabled(enabled); + if (enabled && packName != null) { + config.setShaderPackName(packName); + } + config.save(); + + Minecraft.getInstance().execute(() -> { + LOGGER.info("Iris: Reloading with shaders {}", enabled ? "ON (" + packName + ")" : "OFF"); + try { + Iris.reload(); + } catch (IOException e) { + LOGGER.error("Iris: Failed to reload shaders", e); + } + }); + return true; + } catch (Exception e) { + LOGGER.error("Iris: Failed to update configuration", e); + return false; + } + } + + private static String findShaderPack(String prefix) { + Path dir = Minecraft.getInstance().gameDirectory.toPath().resolve("shaderpacks"); + if (!Files.exists(dir)) return null; + + try (Stream paths = Files.list(dir)) { + return paths + .filter(Files::isRegularFile) + .map(p -> p.getFileName().toString()) + .filter(name -> name.toLowerCase().startsWith(prefix.toLowerCase()) && name.toLowerCase().endsWith(".zip")) + .findFirst() + .orElse(null); + } catch (Exception e) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/ItemBackgroundManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/ItemBackgroundManager.java new file mode 100644 index 0000000..f9e694a --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/ItemBackgroundManager.java @@ -0,0 +1,74 @@ +package com.github.kd_gaming1.packcore.integration; + +import com.github.kd_gaming1.packcore.PackCore; +import net.fabricmc.loader.api.FabricLoader; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +public class ItemBackgroundManager { + + private static final String CONFIG_MANAGER_CLASS = "de.hysky.skyblocker.config.SkyblockerConfigManager"; + private static final String ITEM_BACKGROUND_ENUM_CLASS = "de.hysky.skyblocker.config.configs.GeneralConfig$ItemBackgroundStyle"; + private static final float DEFAULT_OPACITY = 0.5f; + + public enum ItemBackground { + NONE, CIRCLE, SQUARE + } + + public static boolean apply(ItemBackground background) { + if (!FabricLoader.getInstance().isModLoaded("skyblocker")) { + PackCore.LOGGER.warn("ItemBackground: Skyblocker not loaded, cannot apply"); + return false; + } + + // NONE means disable rarity backgrounds entirely; others map to Skyblocker enum values + String skyblockerStyle = switch (background) { + case NONE -> null; + case CIRCLE -> "CIRCULAR"; + case SQUARE -> "SQUARE"; + }; + + try { + Class configManager = Class.forName(CONFIG_MANAGER_CLASS); + Method update = configManager.getDeclaredMethod("update", Consumer.class); + update.invoke(null, (Consumer) config -> updateConfig(config, skyblockerStyle)); + PackCore.LOGGER.info("ItemBackground: applied {}", background); + return true; + } catch (ClassNotFoundException e) { + PackCore.LOGGER.warn("ItemBackground: Skyblocker config manager not found"); + return false; + } catch (NoSuchMethodException e) { + PackCore.LOGGER.warn("ItemBackground: update method not found, Skyblocker API may have changed"); + return false; + } catch (Exception e) { + PackCore.LOGGER.error("ItemBackground: failed to apply", e); + return false; + } + } + + private static void updateConfig(Object config, String skyblockerStyle) { + try { + Object general = config.getClass().getField("general").get(config); + Object itemInfoDisplay = general.getClass().getField("itemInfoDisplay").get(general); + + itemInfoDisplay.getClass().getField("itemRarityBackgrounds").setBoolean(itemInfoDisplay, skyblockerStyle != null); + + if (skyblockerStyle != null) { + Class styleEnum = Class.forName(ITEM_BACKGROUND_ENUM_CLASS); + Method valueOfMethod = styleEnum.getMethod("valueOf", String.class); + Object enumValue = valueOfMethod.invoke(null, skyblockerStyle); + itemInfoDisplay.getClass().getField("itemBackgroundStyle").set(itemInfoDisplay, enumValue); + itemInfoDisplay.getClass().getField("itemBackgroundOpacity").setFloat(itemInfoDisplay, DEFAULT_OPACITY); + } + } catch (NoSuchFieldException e) { + PackCore.LOGGER.error("ItemBackground: config field not found, Skyblocker structure may have changed: {}", e.getMessage()); + } catch (ClassNotFoundException e) { + PackCore.LOGGER.error("ItemBackground: style enum not found"); + } catch (IllegalAccessException e) { + PackCore.LOGGER.error("ItemBackground: cannot access config fields"); + } catch (Exception e) { + PackCore.LOGGER.error("ItemBackground: unexpected error updating config", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/MinecraftIntegration.java b/src/main/java/com/github/kd_gaming1/packcore/integration/MinecraftIntegration.java new file mode 100644 index 0000000..85a58a6 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/MinecraftIntegration.java @@ -0,0 +1,69 @@ +package com.github.kd_gaming1.packcore.integration; + +import net.minecraft.client.GraphicsPreset; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; +import net.minecraft.server.level.ParticleStatus; +import net.minecraft.client.CloudStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class MinecraftIntegration { + private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + public static boolean applyProfile(PerformanceProfileService.PerformanceProfile profile) { + try { + Options options = getOptions(); + if (options == null) return false; + + applyCommonSettings(options); + + switch (profile) { + case PERFORMANCE -> { + options.graphicsPreset().set(GraphicsPreset.FAST); + options.particles().set(ParticleStatus.DECREASED); + options.cloudStatus().set(CloudStatus.FAST); + options.renderDistance().set(10); + options.entityShadows().set(false); + } + case BALANCED, SHADERS_PERFORMANCE -> { + options.graphicsPreset().set(GraphicsPreset.FANCY); + options.particles().set(ParticleStatus.ALL); + options.cloudStatus().set(CloudStatus.FANCY); + options.renderDistance().set(16); + options.entityShadows().set(true); + } + case QUALITY, SHADERS_QUALITY -> { + options.graphicsPreset().set(GraphicsPreset.FABULOUS); + options.particles().set(ParticleStatus.ALL); + options.cloudStatus().set(CloudStatus.FANCY); + options.renderDistance().set(20); + options.entityShadows().set(true); + options.entityDistanceScaling().set(1.25); + } + } + + options.save(); + return true; + } catch (Exception e) { + LOGGER.error("Vanilla: Failed to apply profile", e); + return false; + } + } + + private static void applyCommonSettings(Options options) { + options.ambientOcclusion().set(true); + options.biomeBlendRadius().set(2); + options.enableVsync().set(false); + options.framerateLimit().set(260); + options.mipmapLevels().set(4); + options.simulationDistance().set(12); + options.entityDistanceScaling().set(1.0); + } + + private static Options getOptions() { + return Minecraft.getInstance().options; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/PerformanceProfileService.java b/src/main/java/com/github/kd_gaming1/packcore/integration/PerformanceProfileService.java new file mode 100644 index 0000000..0392b48 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/PerformanceProfileService.java @@ -0,0 +1,52 @@ +package com.github.kd_gaming1.packcore.integration; + +import net.fabricmc.loader.api.FabricLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class PerformanceProfileService { + private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + public enum PerformanceProfile { + PERFORMANCE("performance", "Maximum Performance"), + BALANCED("balanced", "Balanced"), + QUALITY("quality", "Best Quality"), + SHADERS_PERFORMANCE("shaders_performance", "Shaders (Performance)"), + SHADERS_QUALITY("shaders_quality", "Shaders (Quality)"); + + private final String id; + private final String displayName; + + PerformanceProfile(String id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + public String id() { return id; } + public String getDisplayName() { return displayName; } + } + + public static boolean applyAll(PerformanceProfile profile) { + LOGGER.info("Applying global performance profile: {}", profile.getDisplayName()); + + boolean vanilla = MinecraftIntegration.applyProfile(profile); + + boolean sodium = true; + if (FabricLoader.getInstance().isModLoaded("sodium")) { + sodium = SodiumConfigurator.applyProfile(profile); + } + + boolean iris = true; + if (FabricLoader.getInstance().isModLoaded("iris")) { + iris = switch (profile) { + case SHADERS_PERFORMANCE -> IrisConfigurator.setShaderPack("MakeUp-UltraFast"); + case SHADERS_QUALITY -> IrisConfigurator.setShaderPack("ComplementaryUnbound"); + default -> IrisConfigurator.disableShaders(); + }; + } + + return vanilla && sodium && iris; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/ResourcePackManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/ResourcePackManager.java new file mode 100644 index 0000000..0625bb2 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/ResourcePackManager.java @@ -0,0 +1,65 @@ +package com.github.kd_gaming1.packcore.integration; + +import com.github.kd_gaming1.packcore.PackCore; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.repository.PackRepository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class ResourcePackManager { + + /** + * Applies the given pack IDs on top of any currently enabled non-wizard packs, + * then triggers a full resource reload. + *

+ * Pack order: existing user packs first, wizard-selected packs on top (last = highest priority). + * + * @throws RuntimeException if the reload fails + */ + public static void apply(Set packIds) { + Minecraft client = Minecraft.getInstance(); + PackRepository repo = client.getResourcePackRepository(); + + repo.reload(); + + Collection availableIds = repo.getAvailableIds(); + + // Validate — warn about any selected IDs that no longer exist on disk + for (String id : packIds) { + if (!availableIds.contains(id)) { + PackCore.LOGGER.warn("ResourcePack: selected pack '{}' is not available, skipping", id); + } + } + + // Keep existing enabled packs that we didn't touch, then append wizard packs on top + List finalOrder = new ArrayList<>(); + + for (String id : client.options.resourcePacks) { + if (!packIds.contains(id)) { + finalOrder.add(id); + } + } + + for (String id : packIds) { + if (availableIds.contains(id) && !finalOrder.contains(id)) { + finalOrder.add(id); + } + } + + PackCore.LOGGER.info("ResourcePack: applying order: {}", finalOrder); + + repo.setSelected(finalOrder); + client.options.resourcePacks.clear(); + client.options.resourcePacks.addAll(finalOrder); + client.options.save(); + + client.reloadResourcePacks().whenComplete((res, ex) -> { + if (ex != null) { + PackCore.LOGGER.error("ResourcePack: reload failed", ex); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/ScamScreenerApiBridge.java b/src/main/java/com/github/kd_gaming1/packcore/integration/ScamScreenerApiBridge.java new file mode 100644 index 0000000..9a73680 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/ScamScreenerApiBridge.java @@ -0,0 +1,74 @@ +package com.github.kd_gaming1.packcore.integration; + +import eu.tango.scamscreener.api.ScamScreenerAlertLevel; +import eu.tango.scamscreener.api.ScamScreenerApi; +import eu.tango.scamscreener.api.ScamScreenerSettingsApi; +import net.fabricmc.loader.api.FabricLoader; + +import java.util.Arrays; +import java.util.List; + +final class ScamScreenerApiBridge { + + private ScamScreenerApiBridge() {} + + static boolean isAvailable() { + return !apis().isEmpty(); + } + + static ScamScreenerConfigurator.RuntimeSettings loadSettings() { + ScamScreenerSettingsApi settings = api().settings(); + return new ScamScreenerConfigurator.RuntimeSettings( + settings.alertMinimumRiskLevel().name(), + settings.pingOnRiskWarning(), + settings.pingOnBlacklistWarning() + ); + } + + static List availableAlertLevels() { + return Arrays.stream(ScamScreenerAlertLevel.values()) + .map(Enum::name) + .toList(); + } + + static boolean apply(String minimumRiskLevel, boolean pingOnRiskWarning, boolean pingOnBlacklistWarning) { + ScamScreenerApi api = api(); + ScamScreenerSettingsApi settings = api.settings(); + + ScamScreenerAlertLevel alertLevel = ScamScreenerAlertLevel.valueOf(minimumRiskLevel); + boolean changed = false; + + if (settings.alertMinimumRiskLevel() != alertLevel) { + settings.setAlertMinimumRiskLevel(alertLevel); + changed = true; + } + + if (settings.pingOnRiskWarning() != pingOnRiskWarning) { + settings.setPingOnRiskWarning(pingOnRiskWarning); + changed = true; + } + + if (settings.pingOnBlacklistWarning() != pingOnBlacklistWarning) { + settings.setPingOnBlacklistWarning(pingOnBlacklistWarning); + changed = true; + } + + if (changed) { + api.reload(); + } + + return true; + } + + private static ScamScreenerApi api() { + List apis = apis(); + if (apis.isEmpty()) { + throw new IllegalStateException("ScamScreener API entrypoint not found"); + } + return apis.getFirst(); + } + + private static List apis() { + return FabricLoader.getInstance().getEntrypoints(ScamScreenerApi.ENTRYPOINT_KEY, ScamScreenerApi.class); + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/ScamScreenerConfigurator.java b/src/main/java/com/github/kd_gaming1/packcore/integration/ScamScreenerConfigurator.java new file mode 100644 index 0000000..1bb1b1f --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/ScamScreenerConfigurator.java @@ -0,0 +1,271 @@ +package com.github.kd_gaming1.packcore.integration; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.fabricmc.loader.api.FabricLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public final class ScamScreenerConfigurator { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ScamScreenerConfigurator"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private static final Path CONFIG_DIR = FabricLoader.getInstance().getConfigDir(); + private static final Path PRIMARY_RUNTIME_PATH = CONFIG_DIR.resolve("scamscreener").resolve("runtime.json"); + private static final Path LEGACY_RUNTIME_PATH = CONFIG_DIR.resolve("runtime.json"); + + private static final String DEFAULT_MINIMUM_RISK_LEVEL = "MEDIUM"; + private static final boolean DEFAULT_PING_ON_RISK_WARNING = true; + private static final boolean DEFAULT_PING_ON_BLACKLIST_WARNING = true; + private static final List DEFAULT_ALERT_LEVELS = List.of("LOW", "MEDIUM", "HIGH", "CRITICAL"); + private static final List DEFAULT_MUTE_PATTERNS = List.of(); + + private ScamScreenerConfigurator() {} + + public record RuntimeSettings( + String minimumRiskLevel, + boolean pingOnRiskWarning, + boolean pingOnBlacklistWarning + ) {} + + public static RuntimeSettings loadSettings() { + if (FabricLoader.getInstance().isModLoaded("scamscreener")) { + try { + if (ScamScreenerApiBridge.isAvailable()) { + return ScamScreenerApiBridge.loadSettings(); + } + } catch (RuntimeException | LinkageError e) { + LOGGER.warn("Failed to load ScamScreener settings from API, falling back to runtime.json: {}", e.getMessage()); + } + } + + return loadSettingsFromJson(); + } + + public static List availableAlertLevels() { + if (FabricLoader.getInstance().isModLoaded("scamscreener")) { + try { + if (ScamScreenerApiBridge.isAvailable()) { + return ScamScreenerApiBridge.availableAlertLevels(); + } + } catch (RuntimeException | LinkageError e) { + LOGGER.warn("Failed to read ScamScreener alert levels from API, falling back to defaults: {}", e.getMessage()); + } + } + + return DEFAULT_ALERT_LEVELS; + } + + private static RuntimeSettings loadSettingsFromJson() { + Path path = resolveExistingPath(); + if (path == null) { + return defaultSettings(); + } + + try (Reader reader = Files.newBufferedReader(path)) { + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + return settingsFromJson(root); + } catch (Exception e) { + LOGGER.warn("Failed to read ScamScreener runtime config '{}': {}", path, e.getMessage()); + return defaultSettings(); + } + } + + public static RuntimeSettings defaultSettings() { + return new RuntimeSettings( + DEFAULT_MINIMUM_RISK_LEVEL, + DEFAULT_PING_ON_RISK_WARNING, + DEFAULT_PING_ON_BLACKLIST_WARNING + ); + } + + public static boolean apply(String minimumRiskLevel, boolean pingOnRiskWarning, boolean pingOnBlacklistWarning) { + if (FabricLoader.getInstance().isModLoaded("scamscreener")) { + try { + if (ScamScreenerApiBridge.isAvailable()) { + return ScamScreenerApiBridge.apply(minimumRiskLevel, pingOnRiskWarning, pingOnBlacklistWarning); + } + } catch (RuntimeException | LinkageError e) { + LOGGER.warn("Failed to apply ScamScreener settings through API, falling back to runtime.json: {}", e.getMessage()); + } + } + + return applyViaJson(minimumRiskLevel, pingOnRiskWarning, pingOnBlacklistWarning); + } + + private static boolean applyViaJson(String minimumRiskLevel, boolean pingOnRiskWarning, boolean pingOnBlacklistWarning) { + Path targetPath = resolveWritePath(); + + try { + JsonObject root = loadOrCreateConfig(targetPath); + applySettings(root, minimumRiskLevel, readMutePatterns(root), pingOnRiskWarning, pingOnBlacklistWarning); + + Files.createDirectories(targetPath.getParent()); + try (Writer writer = Files.newBufferedWriter(targetPath)) { + GSON.toJson(root, writer); + } + + LOGGER.info("Updated ScamScreener runtime config at '{}'", targetPath); + return true; + } catch (IOException e) { + LOGGER.error("Failed to write ScamScreener runtime config: {}", e.getMessage(), e); + return false; + } + } + + private static Path resolveExistingPath() { + if (Files.exists(PRIMARY_RUNTIME_PATH)) return PRIMARY_RUNTIME_PATH; + if (Files.exists(LEGACY_RUNTIME_PATH)) return LEGACY_RUNTIME_PATH; + return null; + } + + private static Path resolveWritePath() { + Path existing = resolveExistingPath(); + return existing != null ? existing : PRIMARY_RUNTIME_PATH; + } + + private static JsonObject loadOrCreateConfig(Path targetPath) throws IOException { + if (!Files.exists(targetPath)) { + return createDefaultConfig(); + } + + try (Reader reader = Files.newBufferedReader(targetPath)) { + JsonElement parsed = JsonParser.parseReader(reader); + if (parsed != null && parsed.isJsonObject()) { + return parsed.getAsJsonObject(); + } + } catch (Exception e) { + LOGGER.warn("Existing ScamScreener config is invalid, recreating '{}': {}", targetPath, e.getMessage()); + } + + return createDefaultConfig(); + } + + private static RuntimeSettings settingsFromJson(JsonObject root) { + JsonObject alerts = getOrCreateObject(root, "alerts"); + JsonObject output = getOrCreateObject(root, "output"); + + String minimumRiskLevel = getString(alerts, "minimumRiskLevel", DEFAULT_MINIMUM_RISK_LEVEL); + boolean pingOnRiskWarning = getBoolean(output, "pingOnRiskWarning", DEFAULT_PING_ON_RISK_WARNING); + boolean pingOnBlacklistWarning = getBoolean(output, "pingOnBlacklistWarning", DEFAULT_PING_ON_BLACKLIST_WARNING); + + return new RuntimeSettings(minimumRiskLevel, pingOnRiskWarning, pingOnBlacklistWarning); + } + + private static List readMutePatterns(JsonObject root) { + JsonObject safety = getOrCreateObject(root, "safety"); + if (!safety.has("mutePatterns") || !safety.get("mutePatterns").isJsonArray()) { + return DEFAULT_MUTE_PATTERNS; + } + + return safety.getAsJsonArray("mutePatterns").asList().stream() + .filter(JsonElement::isJsonPrimitive) + .filter(element -> element.getAsJsonPrimitive().isString()) + .map(JsonElement::getAsString) + .toList(); + } + + private static void applySettings(JsonObject root, String minimumRiskLevel, List mutePatterns, + boolean pingOnRiskWarning, boolean pingOnBlacklistWarning) { + JsonObject alerts = getOrCreateObject(root, "alerts"); + JsonObject safety = getOrCreateObject(root, "safety"); + JsonObject output = getOrCreateObject(root, "output"); + + alerts.addProperty("minimumRiskLevel", minimumRiskLevel); + + JsonArray mutePatternsJson = new JsonArray(); + for (String pattern : mutePatterns) { + mutePatternsJson.add(pattern); + } + + safety.addProperty("muteFilterEnabled", !mutePatterns.isEmpty()); + safety.add("mutePatterns", mutePatternsJson); + output.addProperty("pingOnRiskWarning", pingOnRiskWarning); + output.addProperty("pingOnBlacklistWarning", pingOnBlacklistWarning); + } + + private static JsonObject createDefaultConfig() { + JsonObject root = new JsonObject(); + root.addProperty("version", 3); + root.addProperty("enabled", true); + + JsonObject pipeline = new JsonObject(); + pipeline.addProperty("reviewThreshold", 1); + root.add("pipeline", pipeline); + + JsonObject alerts = new JsonObject(); + alerts.addProperty("minimumRiskLevel", DEFAULT_MINIMUM_RISK_LEVEL); + alerts.addProperty("autoCaptureLevel", "MEDIUM"); + root.add("alerts", alerts); + + JsonObject output = new JsonObject(); + output.addProperty("showRiskWarningMessage", true); + output.addProperty("pingOnRiskWarning", DEFAULT_PING_ON_RISK_WARNING); + output.addProperty("showBlacklistWarningMessage", true); + output.addProperty("pingOnBlacklistWarning", DEFAULT_PING_ON_BLACKLIST_WARNING); + output.addProperty("showAutoLeaveMessage", true); + output.addProperty("debugLogging", true); + root.add("output", output); + + JsonObject review = new JsonObject(); + review.addProperty("captureEnabled", true); + review.addProperty("maxEntries", 200); + root.add("review", review); + + JsonObject safety = new JsonObject(); + safety.addProperty("autoLeaveOnBlacklist", false); + safety.addProperty("muteFilterEnabled", false); + JsonArray mutePatterns = new JsonArray(); + for (String pattern : DEFAULT_MUTE_PATTERNS) { + mutePatterns.add(pattern); + } + safety.add("mutePatterns", mutePatterns); + root.add("safety", safety); + + JsonObject profiler = new JsonObject(); + profiler.addProperty("hudEnabled", false); + root.add("profiler", profiler); + + JsonObject debug = new JsonObject(); + debug.add("flags", new JsonObject()); + root.add("debug", debug); + + return root; + } + + private static JsonObject getOrCreateObject(JsonObject parent, String key) { + if (parent.has(key) && parent.get(key).isJsonObject()) { + return parent.getAsJsonObject(key); + } + + JsonObject child = new JsonObject(); + parent.add(key, child); + return child; + } + + private static String getString(JsonObject json, String key, String fallback) { + if (json.has(key) && json.get(key).isJsonPrimitive() && json.get(key).getAsJsonPrimitive().isString()) { + return json.get(key).getAsString(); + } + return fallback; + } + + private static boolean getBoolean(JsonObject json, String key, boolean fallback) { + if (json.has(key) && json.get(key).isJsonPrimitive() && json.get(key).getAsJsonPrimitive().isBoolean()) { + return json.get(key).getAsBoolean(); + } + return fallback; + } +} diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/SodiumConfigurator.java b/src/main/java/com/github/kd_gaming1/packcore/integration/SodiumConfigurator.java new file mode 100644 index 0000000..672095a --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/SodiumConfigurator.java @@ -0,0 +1,44 @@ +package com.github.kd_gaming1.packcore.integration; + +import com.github.kd_gaming1.packcore.integration.PerformanceProfileService.PerformanceProfile; +import net.caffeinemc.mods.sodium.client.SodiumClientMod; +import net.caffeinemc.mods.sodium.client.gui.SodiumOptions; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public class SodiumConfigurator { + private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + public static boolean applyProfile(PerformanceProfile profile) { + try { + SodiumOptions options = SodiumClientMod.options(); + + if (options.isReadOnly()) return false; + + applyCommonSettings(options); + + SodiumOptions.writeToDisk(options); + + LOGGER.info("Sodium: Applied {} profile", profile); + return true; + } catch (Exception e) { + LOGGER.error("Sodium: Failed to apply profile", e); + return false; + } + } + + private static void applyCommonSettings(SodiumOptions options) { + options.advanced.useAdvancedStagingBuffers = true; + options.advanced.cpuRenderAheadLimit = 3; + + options.performance.chunkBuilderThreads = 0; + options.performance.chunkBuildDeferMode = DeferMode.ALWAYS; + options.performance.animateOnlyVisibleTextures = true; + options.performance.useEntityCulling = true; + options.performance.useFogOcclusion = true; + options.performance.useBlockFaceCulling = true; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/StorageDesignManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/StorageDesignManager.java new file mode 100644 index 0000000..1274840 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/StorageDesignManager.java @@ -0,0 +1,64 @@ +package com.github.kd_gaming1.packcore.integration; + +import com.github.kd_gaming1.packcore.PackCore; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; + +import java.util.concurrent.atomic.AtomicReference; + +public class StorageDesignManager { + + private static final AtomicReference pendingOverlayState = new AtomicReference<>(null); + + static { + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { + Boolean state = pendingOverlayState.getAndSet(null); + if (state != null) scheduleCommand(state); + }); + } + + public enum StorageDesign { + OVERLAY, VANILLA + } + + public static boolean apply(StorageDesign design) { + if (!FabricLoader.getInstance().isModLoaded("firmament")) { + PackCore.LOGGER.warn("StorageDesign: Firmament not loaded, cannot apply"); + return false; + } + + boolean enableOverlay = design == StorageDesign.OVERLAY; + + Minecraft client = Minecraft.getInstance(); + if (client.player != null) { + scheduleCommand(enableOverlay); + } else { + pendingOverlayState.set(enableOverlay); + PackCore.LOGGER.info("StorageDesign: queued command for next world join"); + } + + return true; + } + + private static void scheduleCommand(boolean enable) { + Minecraft client = Minecraft.getInstance(); + new Thread(() -> { + try { + Thread.sleep(2000); + client.execute(() -> { + LocalPlayer player = client.player; + if (player == null) return; + // /firm config toggle storage-overlay always-replace + // The toggle command flips the value, so we need to check the current state. + String command = "firm config toggle storage-overlay always-replace"; + player.connection.sendCommand(command); + PackCore.LOGGER.info("StorageDesign: executed /{}", command); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "packcore-firmament-config").start(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/TabDesignManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/TabDesignManager.java new file mode 100644 index 0000000..ff06028 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/integration/TabDesignManager.java @@ -0,0 +1,107 @@ +package com.github.kd_gaming1.packcore.integration; + +import com.github.kd_gaming1.packcore.PackCore; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; + +import java.util.concurrent.atomic.AtomicReference; + +public class TabDesignManager { + + private static final AtomicReference pendingSkyHanniEnable = new AtomicReference<>(null); + + static { + net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.JOIN.register( + (handler, sender, client) -> { + Boolean state = pendingSkyHanniEnable.getAndSet(null); + if (state != null) scheduleSkyHanniCommand(state); + } + ); + } + + public enum TabDesign { + COMPACT, FANCY + } + + public static boolean apply(TabDesign design) { + boolean skyblockerLoaded = FabricLoader.getInstance().isModLoaded("skyblocker"); + boolean skyhanniLoaded = FabricLoader.getInstance().isModLoaded("skyhanni"); + + return switch (design) { + case COMPACT -> { + // SkyHanni tab on, Skyblocker defers to vanilla + boolean ok = true; + if (skyhanniLoaded) ok = setSkyHanniEnabled(true); + if (skyblockerLoaded) setSkyblockerEnabled(false); + yield ok; + } + case FANCY -> { + // Skyblocker tab on, SkyHanni off + boolean ok = true; + if (skyblockerLoaded) ok = setSkyblockerEnabled(true); + if (skyhanniLoaded) setSkyHanniEnabled(false); + yield ok; + } + }; + } + + // --- Skyblocker (reflection) --- + + private static boolean setSkyblockerEnabled(boolean enabled) { + try { + Class configManager = Class.forName("de.hysky.skyblocker.config.SkyblockerConfigManager"); + java.lang.reflect.Method update = configManager.getDeclaredMethod("update", java.util.function.Consumer.class); + update.invoke(null, (java.util.function.Consumer) config -> { + try { + Object uiAndVisuals = config.getClass().getField("uiAndVisuals").get(config); + Object tabHud = uiAndVisuals.getClass().getField("tabHud").get(uiAndVisuals); + tabHud.getClass().getField("tabHudEnabled").setBoolean(tabHud, true); + // showVanillaTabByDefault=true lets vanilla/SkyHanni show; false means Skyblocker renders + tabHud.getClass().getField("showVanillaTabByDefault").setBoolean(tabHud, !enabled); + } catch (Exception e) { + PackCore.LOGGER.warn("Skyblocker: failed to update TabHud config", e); + } + }); + PackCore.LOGGER.info("Skyblocker: tabHudEnabled=true showVanillaTabByDefault={}", !enabled); + return true; + } catch (ClassNotFoundException e) { + PackCore.LOGGER.info("Skyblocker not present"); + return false; + } catch (Exception e) { + PackCore.LOGGER.warn("Skyblocker: config update failed", e); + return false; + } + } + + // --- SkyHanni (chat command) --- + + private static boolean setSkyHanniEnabled(boolean enabled) { + Minecraft client = Minecraft.getInstance(); + if (client.player != null) { + scheduleSkyHanniCommand(enabled); + } else { + pendingSkyHanniEnable.set(enabled); + PackCore.LOGGER.info("SkyHanni: queued command for next world join"); + } + return true; + } + + private static void scheduleSkyHanniCommand(boolean enabled) { + Minecraft client = Minecraft.getInstance(); + new Thread(() -> { + try { + Thread.sleep(2000); + client.execute(() -> { + LocalPlayer player = client.player; + if (player == null) return; + String command = "shconfig set config.gui.compactTabList.enabled " + enabled; + player.connection.sendCommand(command); + PackCore.LOGGER.info("SkyHanni: executed /{}", command); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "packcore-skyhanni-config").start(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/bobby/BobbyConfigModifier.java b/src/main/java/com/github/kd_gaming1/packcore/integration/bobby/BobbyConfigModifier.java deleted file mode 100644 index d86e262..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/bobby/BobbyConfigModifier.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.bobby; - -import com.github.kd_gaming1.packcore.PackCore; -import net.fabricmc.loader.api.FabricLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -public class BobbyConfigModifier { - - /** - * Enables Bobby's dynamicMultiWorld option by modifying its config file. - * If Bobby is not installed or the config file is missing, it does nothing. - */ - public static void enableDynamicMultiWorld() { - if (!FabricLoader.getInstance().isModLoaded("bobby")) { - PackCore.LOGGER.info("Bobby mod not found, skipping config modification"); - return; - } - - try { - // Bobby stores its config in config/bobby.conf - Path configPath = FabricLoader.getInstance().getConfigDir().resolve("bobby.conf"); - - if (!Files.exists(configPath)) { - PackCore.LOGGER.info("Bobby config not found yet, skipping modification (will use defaults)"); - return; - } - - // Read the config file - List lines = Files.readAllLines(configPath); - List newLines = new ArrayList<>(); - boolean modified = false; - boolean foundDynamicMultiWorld = false; - - // Process each line - for (String line : lines) { - String trimmed = line.trim(); - - // Check if this is the dynamic-multi-world setting - if (trimmed.startsWith("dynamic-multi-world=") || trimmed.startsWith("dynamic-multi-world ")) { - foundDynamicMultiWorld = true; - - // Check if it's already set to true - if (trimmed.contains("=true")) { - PackCore.LOGGER.info("Bobby's dynamicMultiWorld is already enabled"); - return; // Already set, no need to modify - } - - // Replace with true, preserving any indentation - String indentation = line.substring(0, line.indexOf(trimmed)); - newLines.add(indentation + "dynamic-multi-world=true"); - modified = true; - } else { - newLines.add(line); - } - } - - if (!foundDynamicMultiWorld) { - PackCore.LOGGER.warn("Could not find dynamic-multi-world setting in Bobby config"); - return; - } - - if (modified) { - Files.write(configPath, newLines); - PackCore.LOGGER.info("Successfully enabled Bobby's dynamicMultiWorld"); - } - - } catch (Exception e) { - PackCore.LOGGER.error("Failed to modify Bobby config", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/iris/IrisConfigurator.java b/src/main/java/com/github/kd_gaming1/packcore/integration/iris/IrisConfigurator.java deleted file mode 100644 index 266feb4..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/iris/IrisConfigurator.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.iris; - -import net.irisshaders.iris.Iris; -import net.irisshaders.iris.config.IrisConfig; -import net.minecraft.client.MinecraftClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Integration class for Iris-specific settings. - * This class is only loaded when Iris is present. - */ -public class IrisConfigurator { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - /** - * Set a shader pack by finding the first pack that starts with the given name and ends with .zip - * - * @param shaderPackPrefix The prefix to search for (e.g., "ComplementaryUnbound") - * @return true if successful - */ - public static boolean setShaderPack(String shaderPackPrefix) { - try { - // Find the shader pack - String foundPack = findShaderPack(shaderPackPrefix); - if (foundPack == null) { - LOGGER.warn("No shader pack found starting with: {}", shaderPackPrefix); - return false; - } - - // Get the Iris configuration instance - IrisConfig irisConfig = Iris.getIrisConfig(); - - // Enable shaders and set the pack - irisConfig.setShadersEnabled(true); - irisConfig.setShaderPackName(foundPack); - - LOGGER.info("Setting shader pack in method setShaderPack to: {}", foundPack); - - // Save the configuration - irisConfig.save(); - Thread.sleep(200); // Brief delay for file I/O - - // Schedule reload on main thread - CompletableFuture reloadFuture = new CompletableFuture<>(); - MinecraftClient.getInstance().execute(() -> { - try { - LOGGER.info("Reloading Iris with shader pack: {}", foundPack); - Iris.reload(); - reloadFuture.complete(true); - } catch (Exception e) { - LOGGER.error("Failed to reload Iris", e); - reloadFuture.complete(false); - } - }); - - return reloadFuture.get(30, TimeUnit.SECONDS); - - } catch (Exception e) { - LOGGER.error("Failed to set shader pack", e); - return false; - } - } - - /** - * Disable shaders - * - * @return true if successful - */ - public static boolean disableShaders() { - try { - return configureAndReload(null, false); - } catch (Exception e) { - LOGGER.error("Failed to disable shaders", e); - return false; - } - } - - private static boolean configureAndReload(String shaderPack, boolean enabled) throws Exception { - IrisConfig irisConfig = Iris.getIrisConfig(); - irisConfig.setShadersEnabled(enabled); - - if (enabled && shaderPack != null) { - irisConfig.setShaderPackName(shaderPack); - LOGGER.info("Setting shader pack in method configureAndReload to: {}", shaderPack); - } else { - LOGGER.info("Disabling Iris shaders"); - } - - irisConfig.save(); - Thread.sleep(enabled ? 200 : 100); - - CompletableFuture reloadFuture = new CompletableFuture<>(); - MinecraftClient.getInstance().execute(() -> { - try { - LOGGER.info("Reloading Iris with shaders {}", enabled ? "enabled" : "disabled"); - Iris.reload(); - reloadFuture.complete(true); - } catch (Exception e) { - LOGGER.error("Failed to reload Iris", e); - reloadFuture.complete(false); - } - }); - - return reloadFuture.get(30, TimeUnit.SECONDS); - } - - /** - * Find a shader pack that starts with the given prefix and ends with .zip - * - * @param prefix The prefix to search for - * @return The full shader pack filename, or null if not found - */ - private static String findShaderPack(String prefix) { - try { - Path shaderPacksDir = MinecraftClient.getInstance() - .runDirectory.toPath().resolve("shaderpacks"); - - if (!Files.exists(shaderPacksDir)) { - LOGGER.warn("Shaderpacks directory does not exist: {}", shaderPacksDir); - return null; - } - - try (Stream paths = Files.list(shaderPacksDir)) { - return paths - .filter(Files::isRegularFile) - .map(path -> path.getFileName().toString()) - .filter(name -> name.toLowerCase().startsWith(prefix.toLowerCase()) - && name.toLowerCase().endsWith(".zip")) - .findFirst() - .orElse(null); - } - - } catch (Exception e) { - LOGGER.error("Failed to search for shader pack with prefix: {}", prefix, e); - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/itembackground/ItemBackgroundManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/itembackground/ItemBackgroundManager.java deleted file mode 100644 index 4407e95..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/itembackground/ItemBackgroundManager.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.itembackground; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import net.fabricmc.loader.api.FabricLoader; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.function.Consumer; - -/** - * Manager for applying item background styles by modifying Skyblocker's configuration at runtime. - * Uses reflection to interact with Skyblocker's config system. - */ -public class ItemBackgroundManager { - - private static final String SKYBLOCKER_MOD_ID = "skyblocker"; - private static final String CONFIG_MANAGER_CLASS = "de.hysky.skyblocker.config.SkyblockerConfigManager"; - private static final String ITEM_BACKGROUND_ENUM_CLASS = "de.hysky.skyblocker.config.configs.GeneralConfig$ItemBackgroundStyle"; - private static final float DEFAULT_OPACITY = 0.5f; - - /** - * Applies the item background style selected in the wizard. - * - * @return true if successfully applied or no action needed, false on failure - */ - public static boolean applyItemBackgroundFromWizard() { - String itemBackground = WizardDataStore.getInstance().getItemBackground(); - - if (shouldSkipApplication(itemBackground)) { - return true; - } - - if (!isSkyblockerLoaded()) { - PackCore.LOGGER.warn("Skyblocker not loaded; cannot apply item background"); - return false; - } - - // mapper returns null for "none" meaning "disable backgrounds" - String style = mapToSkyblockerStyle(itemBackground); - return applyItemBackgroundStyle(style); - } - - /** - * Applies a specific item background style or disables backgrounds when style is null. - * - * @param style the style enum name (CIRCULAR, SQUARE) or null to disable rarity backgrounds - * @return true if successfully applied, false otherwise - */ - private static boolean applyItemBackgroundStyle(String style) { - try { - Class configManager = Class.forName(CONFIG_MANAGER_CLASS); - Method updateMethod = configManager.getDeclaredMethod("update", Consumer.class); - - Consumer configUpdater = config -> updateItemBackgroundConfig(config, style); - updateMethod.invoke(null, configUpdater); - - PackCore.LOGGER.info("Successfully applied Skyblocker item background change: {}", style == null ? "DISABLE_BACKGROUNDS" : style); - return true; - - } catch (ClassNotFoundException e) { - PackCore.LOGGER.warn("Skyblocker config manager not found - mod may not be loaded correctly"); - return false; - } catch (NoSuchMethodException e) { - PackCore.LOGGER.warn("Skyblocker config update method not found - API may have changed"); - return false; - } catch (Exception e) { - PackCore.LOGGER.error("Unexpected error while applying item background config", e); - return false; - } - } - - /** - * Updates the Skyblocker config object with the new item background settings. - * - * If *style* is null, this will disable item rarity backgrounds. Otherwise it will set the enum, - * enable rarity backgrounds, and set default opacity. - * - * @param config the Skyblocker config instance - * @param style the background style to apply, or null to disable backgrounds - */ - private static void updateItemBackgroundConfig(Object config, String style) { - try { - // Navigate to itemInfoDisplay config - Field generalField = config.getClass().getField("general"); - Object general = generalField.get(config); - - Field itemInfoDisplayField = general.getClass().getField("itemInfoDisplay"); - Object itemInfoDisplay = itemInfoDisplayField.get(general); - - // Toggle itemRarityBackgrounds based on whether we have a style or are disabling - Field rarityBackgroundsField = itemInfoDisplay.getClass().getField("itemRarityBackgrounds"); - rarityBackgroundsField.setBoolean(itemInfoDisplay, style != null); - - if (style != null) { - // Set the background style enum - Class styleEnum = Class.forName(ITEM_BACKGROUND_ENUM_CLASS); - Object enumValue = Enum.valueOf((Class) styleEnum, style); - - Field styleField = itemInfoDisplay.getClass().getField("itemBackgroundStyle"); - styleField.set(itemInfoDisplay, enumValue); - - // Set default opacity - Field opacityField = itemInfoDisplay.getClass().getField("itemBackgroundOpacity"); - opacityField.setFloat(itemInfoDisplay, DEFAULT_OPACITY); - - PackCore.LOGGER.debug("Item background config updated: style={}, opacity={}", style, DEFAULT_OPACITY); - } else { - PackCore.LOGGER.debug("Item rarity backgrounds disabled via wizard selection"); - } - - } catch (NoSuchFieldException e) { - PackCore.LOGGER.error("Required config field not found - Skyblocker structure may have changed: {}", e.getMessage()); - } catch (ClassNotFoundException e) { - PackCore.LOGGER.error("Item background style enum not found - Skyblocker may have changed"); - } catch (IllegalAccessException e) { - PackCore.LOGGER.error("Cannot access config fields - security restrictions may apply"); - } catch (Exception e) { - PackCore.LOGGER.error("Unexpected error updating item background config", e); - } - } - - /** - * Maps wizard style names to Skyblocker enum values. - * - * Returns null for "none"/"no background" to indicate disabling backgrounds. - * - * @param wizardStyle the style name from the wizard - * @return the corresponding Skyblocker enum name or null to disable backgrounds - */ - private static String mapToSkyblockerStyle(String wizardStyle) { - if (wizardStyle == null) return "SQUARE"; - return switch (wizardStyle.toLowerCase().trim()) { - case "circular" -> "CIRCULAR"; - case "square" -> "SQUARE"; - case "no background", "none" -> null; // signal to disable itemRarityBackgrounds - default -> { - PackCore.LOGGER.warn("Unknown item background style '{}', defaulting to SQUARE", wizardStyle); - yield "SQUARE"; - } - }; - } - - /** - * Checks if the application should be skipped. - * Do not skip when the user explicitly chose "None" — that indicates they want to disable backgrounds. - */ - private static boolean shouldSkipApplication(String itemBackground) { - return itemBackground == null || itemBackground.isEmpty(); - } - - /** - * Checks if Skyblocker is loaded. - */ - private static boolean isSkyblockerLoaded() { - return FabricLoader.getInstance().isModLoaded(SKYBLOCKER_MOD_ID); - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/minecraft/MinecraftIntegration.java b/src/main/java/com/github/kd_gaming1/packcore/integration/minecraft/MinecraftIntegration.java deleted file mode 100644 index c3ee140..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/minecraft/MinecraftIntegration.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.minecraft; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.option.GameOptions; -import net.minecraft.client.option.GraphicsMode; -import net.minecraft.client.option.CloudRenderMode; -import net.minecraft.particle.ParticlesMode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Integration class for vanilla Minecraft settings. - * Handles GameOptions modifications for performance profiles. - */ -public class MinecraftIntegration { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - public static boolean applyProfile(PerformanceProfileService.PerformanceProfile profile) { - try { - GameOptions options = getValidatedOptions(); - if (options == null) return false; - - switch (profile) { - case PERFORMANCE -> applyPerformanceSettings(options); - case BALANCED -> applyBalancedSettings(options); - case QUALITY, SHADERS -> applyQualitySettings(options); - default -> { - LOGGER.warn("Unknown profile: {}", profile); - return false; - } - } - - options.write(); - LOGGER.debug("Minecraft profile '{}' applied successfully", profile.name()); - return true; - } catch (Exception e) { - LOGGER.error("Failed to apply Minecraft profile", e); - return false; - } - } - - public static boolean restoreDefaults() { - try { - GameOptions options = getValidatedOptions(); - if (options == null) return false; - - // Reset to default values using getter methods - //? if >=1.21.11 { - /*options.applyGraphicsMode(GraphicsMode.FANCY); - *///?} else { - options.getGraphicsMode().setValue(GraphicsMode.FANCY); - //?} - options.getViewDistance().setValue(12); - options.getSimulationDistance().setValue(12); - options.getEntityDistanceScaling().setValue(1.0); - options.getMaxFps().setValue(120); - options.getEnableVsync().setValue(false); - options.getBiomeBlendRadius().setValue(2); - options.getEntityShadows().setValue(true); - options.getCloudRenderMode().setValue(CloudRenderMode.FANCY); - options.getAo().setValue(true); // Ambient occlusion - - options.write(); - LOGGER.debug("Minecraft default settings restored"); - return true; - } catch (Exception e) { - LOGGER.error("Failed to restore Minecraft defaults", e); - return false; - } - } - - private static GameOptions getValidatedOptions() { - MinecraftClient client = MinecraftClient.getInstance(); - if (client == null) { - LOGGER.warn("MinecraftClient is null, cannot apply profile"); - return null; - } - - GameOptions options = client.options; - if (options == null) { - LOGGER.warn("GameOptions is null, cannot apply profile"); - return null; - } - - return options; - } - - private static void applyPerformanceSettings(GameOptions options) { - // Performance profile settings from options.txt - options.getAo().setValue(true); - options.getBiomeBlendRadius().setValue(2); - options.getEnableVsync().setValue(false); - options.getEntityDistanceScaling().setValue(1.0); - options.getEntityShadows().setValue(false); - //? if >=1.21.11 { - /*options.applyGraphicsMode(GraphicsMode.FAST); - *///?} else { - options.getGraphicsMode().setValue(GraphicsMode.FAST); - //?} - options.getMaxFps().setValue(260); - options.getMipmapLevels().setValue(2); - options.getParticles().setValue(ParticlesMode.MINIMAL); // particles:2 = MINIMAL - options.getCloudRenderMode().setValue(CloudRenderMode.FAST); // renderClouds:"fast" - options.getViewDistance().setValue(10); // renderDistance:10 - options.getSimulationDistance().setValue(10); - - LOGGER.debug("Applied Minecraft performance settings"); - } - - private static void applyBalancedSettings(GameOptions options) { - // Balanced profile settings from options.txt - options.getAo().setValue(true); - options.getBiomeBlendRadius().setValue(2); - options.getEnableVsync().setValue(false); - options.getEntityDistanceScaling().setValue(1.0); - options.getEntityShadows().setValue(true); - //? if >=1.21.11 { - /*options.applyGraphicsMode(GraphicsMode.FANCY); - *///?} else { - options.getGraphicsMode().setValue(GraphicsMode.FANCY); - //?} - options.getMaxFps().setValue(260); - options.getMipmapLevels().setValue(4); - options.getParticles().setValue(ParticlesMode.ALL); // particles:0 = ALL - options.getCloudRenderMode().setValue(CloudRenderMode.FANCY); // renderClouds:"true" - options.getViewDistance().setValue(14); // renderDistance:14 - options.getSimulationDistance().setValue(12); - - LOGGER.debug("Applied Minecraft balanced settings"); - } - - private static void applyQualitySettings(GameOptions options) { - // Quality profile settings from options.txt - options.getAo().setValue(true); - options.getBiomeBlendRadius().setValue(2); - options.getEnableVsync().setValue(false); - options.getEntityDistanceScaling().setValue(1.25); - options.getEntityShadows().setValue(true); - //? if >=1.21.11 { - /*options.applyGraphicsMode(GraphicsMode.FABULOUS); - *///?} else { - options.getGraphicsMode().setValue(GraphicsMode.FABULOUS); - //?} - options.getMaxFps().setValue(260); - options.getMipmapLevels().setValue(4); - options.getParticles().setValue(ParticlesMode.ALL); // particles:0 = ALL - options.getCloudRenderMode().setValue(CloudRenderMode.FANCY); // renderClouds:"true" - options.getViewDistance().setValue(20); // renderDistance:20 - options.getSimulationDistance().setValue(12); - - LOGGER.debug("Applied Minecraft quality settings"); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/minecraft/PerformanceProfileService.java b/src/main/java/com/github/kd_gaming1/packcore/integration/minecraft/PerformanceProfileService.java deleted file mode 100644 index a1bda83..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/minecraft/PerformanceProfileService.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.minecraft; - -import com.github.kd_gaming1.packcore.integration.iris.IrisConfigurator; -import com.github.kd_gaming1.packcore.integration.sodium.SodiumConfigurator; -import net.fabricmc.loader.api.FabricLoader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Objects; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Central utility class for managing performance profiles across different systems. - * This class coordinates between Sodium, Minecraft vanilla settings, and Iris shaders. - */ -public class PerformanceProfileService { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - private static final String SODIUM_MOD_ID = "sodium"; - private static final String IRIS_MOD_ID = "iris"; - - /** - * Applies a performance profile to all available systems (Sodium + Vanilla + Iris) - * - * @param profile The performance profile to apply - * @return ProfileResult indicating what was applied and any failures - */ - public static ProfileResult applyPerformanceProfile(PerformanceProfile profile) { - LOGGER.info("Applying performance profile: {}", profile.getDisplayName()); - ProfileResult result = new ProfileResult(); - - result.setSodiumApplied(applySystemProfile(() -> SodiumConfigurator.applyProfile(profile), "Sodium", isSodiumAvailable())); - result.setVanillaApplied(applySystemProfile(() -> MinecraftIntegration.applyProfile(profile), "Vanilla Minecraft", true)); - result.setIrisApplied(applySystemProfile(() -> applyIrisSettings(profile), "Iris shader", isIrisAvailable() || profile == PerformanceProfile.SHADERS)); - - return result; - } - - private static boolean applySystemProfile(SystemProfileApplier applier, String systemName, boolean shouldApply) { - if (!shouldApply) { - LOGGER.debug("{} not available, skipping profile", systemName); - return true; - } - - try { - boolean success = applier.apply(); - if (success) { - LOGGER.info("{} profile applied successfully", systemName); - } else { - LOGGER.warn("Failed to apply {} profile", systemName); - } - return success; - } catch (Throwable t) { - LOGGER.error("Error applying {} profile", systemName, t); - return false; - } - } - - private static boolean applyIrisSettings(PerformanceProfile profile) { - if (!isIrisAvailable() && profile == PerformanceProfile.SHADERS) { - LOGGER.warn("Shaders profile selected but Iris is not available"); - return false; - } - - if (Objects.requireNonNull(profile) == PerformanceProfile.SHADERS) { - return IrisConfigurator.setShaderPack("ComplementaryUnbound"); - } - return IrisConfigurator.disableShaders(); - } - - /** - * Restores default settings for all available systems - * - * @return ProfileResult indicating what was restored - */ - public static ProfileResult restoreDefaults() { - LOGGER.info("Restoring default settings"); - ProfileResult result = new ProfileResult(); - - result.setSodiumApplied(applySystemProfile(SodiumConfigurator::restoreDefaults, "Sodium", isSodiumAvailable())); - result.setVanillaApplied(applySystemProfile(MinecraftIntegration::restoreDefaults, "Vanilla", true)); - result.setIrisApplied(applySystemProfile(IrisConfigurator::disableShaders, "Iris", isIrisAvailable())); - - return result; - } - - /** - * Gets information about available performance systems - * - * @return SystemAvailability indicating what systems are present - */ - public static SystemAvailability getSystemAvailability() { - return new SystemAvailability(isSodiumAvailable(), true, isIrisAvailable()); - } - - private static boolean isSodiumAvailable() { - return FabricLoader.getInstance().isModLoaded(SODIUM_MOD_ID); - } - - private static boolean isIrisAvailable() { - return FabricLoader.getInstance().isModLoaded(IRIS_MOD_ID); - } - - @FunctionalInterface - private interface SystemProfileApplier { - boolean apply() throws Exception; - } - - public enum PerformanceProfile { - PERFORMANCE("Maximum Performance", "Optimized for highest FPS"), - BALANCED("Balanced", "Good balance between performance and quality"), - QUALITY("Best Quality", "Optimized for visual quality"), - SHADERS("Shaders", "Ultimate visual experience with shaders enabled"); - - private final String displayName; - private final String description; - - PerformanceProfile(String displayName, String description) { - this.displayName = displayName; - this.description = description; - } - - public String getDisplayName() { return displayName; } - public String getDescription() { return description; } - } - - /** - * Result of applying a performance profile - */ - public static class ProfileResult { - private boolean sodiumApplied = false; - private boolean vanillaApplied = false; - private boolean irisApplied = false; - private final boolean sodiumAvailable; - private final boolean irisAvailable; - - public ProfileResult() { - this.sodiumAvailable = isSodiumAvailable(); - this.irisAvailable = isIrisAvailable(); - } - - public boolean isSodiumApplied() { return sodiumApplied; } - public boolean isVanillaApplied() { return vanillaApplied; } - public boolean isIrisApplied() { return irisApplied; } - public boolean isSodiumAvailable() { return sodiumAvailable; } - public boolean isIrisAvailable() { return irisAvailable; } - - void setSodiumApplied(boolean applied) { this.sodiumApplied = applied; } - void setVanillaApplied(boolean applied) { this.vanillaApplied = applied; } - void setIrisApplied(boolean applied) { this.irisApplied = applied; } - - public boolean isFullySuccessful() { - return vanillaApplied && - (!sodiumAvailable || sodiumApplied) && - (!irisAvailable || irisApplied); - } - - public boolean hasAnyFailures() { - return !vanillaApplied || - (sodiumAvailable && !sodiumApplied) || - (irisAvailable && !irisApplied); - } - } - - /** - * Information about what performance systems are available - * */ - public record SystemAvailability(boolean sodiumAvailable, boolean vanillaAvailable, boolean irisAvailable) { - public boolean hasAnySystem() { - return sodiumAvailable || vanillaAvailable || irisAvailable; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/resourcepack/ResourcePackManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/resourcepack/ResourcePackManager.java deleted file mode 100644 index 8a61877..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/resourcepack/ResourcePackManager.java +++ /dev/null @@ -1,242 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.resourcepack; - -import com.github.kd_gaming1.packcore.PackCore; -import net.minecraft.client.MinecraftClient; -import net.minecraft.resource.ResourcePackProfile; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -public class ResourcePackManager { - private static final Map MULTI_PACK_KEYWORDS = Map.of( - "HypixelPlus", new String[]{"hypixel"}, - "FurfSkyOverlay", new String[]{"overlay", "furfsky"}, - "FurfSkyFull", new String[]{"full", "furfsky"}, - "SkyBlockDarkUI", new String[]{"skyblock", "dark", "ui"}, - "SkyBlockDarkMode", new String[]{"dark", "skyblock", "mode"}, - "SophieHypixelEnchants", new String[]{"sophie's", "enchants"}, - "Defrosted", new String[]{"defrosted"}, - "Looshy", new String[]{"looshy"} - ); - - private static final Map PACK_KEYWORDS = Map.of( - "HypixelPlus", "hypixel", - "FurfSkyOverlay", "overlay", - "FurfSkyFull", "full", - "SkyBlockDarkUI", "skyblock dark ui", - "SkyBlockDarkMode", "dark skyblock", - "SophieHypixelEnchants", "sophie's hypixel enchants", - "Defrosted", "defrosted", - "Looshy", "looshy" - ); - - public static CompletableFuture applyResourcePacksOrdered(List packKeysOrdered) { - CompletableFuture result = new CompletableFuture<>(); - - try { - MinecraftClient client = MinecraftClient.getInstance(); - if (client == null) { - return CompletableFuture.completedFuture(false); - } - - // The mixin handles sanitization automatically when scanPacks() is called - client.execute(() -> applyPacksOnMainThread(packKeysOrdered, result)); - - } catch (Exception e) { - PackCore.LOGGER.error("Failed to schedule resource packs application", e); - return CompletableFuture.completedFuture(false); - } - - return result.completeOnTimeout(false, 10, TimeUnit.SECONDS); - } - - private static void applyPacksOnMainThread(List packKeysOrdered, CompletableFuture result) { - try { - MinecraftClient client = MinecraftClient.getInstance(); - net.minecraft.resource.ResourcePackManager packManager = client.getResourcePackManager(); - - // Scan packs - mixin will sanitize filenames before this processes them - packManager.scanPacks(); - - List foundPackIds = findAvailablePackIdsOrderedList(packKeysOrdered); - - if (foundPackIds.isEmpty()) { - PackCore.LOGGER.warn("No matching resource packs found for keys: {}", packKeysOrdered); - result.complete(true); - return; - } - - PackCore.LOGGER.info("Found {} packs in order: {}", foundPackIds.size(), foundPackIds); - - List currentPacks = new ArrayList<>(client.options.resourcePacks); - List newPacks = new ArrayList<>(); - Set allKnownPackIds = getAllKnownPackIds(); - - PackCore.LOGGER.info("Current packs from options.txt: {}", currentPacks); - PackCore.LOGGER.info("All known managed pack IDs: {}", allKnownPackIds); - PackCore.LOGGER.info("Packs to add (in order): {}", foundPackIds); - - for (String pack : currentPacks) { - if (!allKnownPackIds.contains(pack)) { - newPacks.add(pack); - PackCore.LOGGER.info("Keeping non-managed pack: {}", pack); - } else { - PackCore.LOGGER.info("Removing previously enabled managed pack: {}", pack); - } - } - - PackCore.LOGGER.info("Base packs (after removing managed): {}", newPacks); - - Collections.reverse(foundPackIds); - - for (int i = 0; i < foundPackIds.size(); i++) { - String packId = foundPackIds.get(i); - if (!newPacks.contains(packId)) { - newPacks.add(packId); - PackCore.LOGGER.info("Adding resource pack: {} (reversed index: {}, final position: {})", - packId, i + 1, newPacks.size() - 1); - } else { - PackCore.LOGGER.warn("Pack {} was already in the list, skipping duplicate", packId); - } - } - - PackCore.LOGGER.info("Final pack order for options.txt: {}", newPacks); - - packManager.setEnabledProfiles(newPacks); - client.options.resourcePacks.clear(); - client.options.resourcePacks.addAll(newPacks); - client.options.write(); - - client.reloadResources().thenRun(() -> { - PackCore.LOGGER.info("Resource reload completed successfully"); - result.complete(true); - }).exceptionally(e -> { - PackCore.LOGGER.error("Resource reload failed", e); - result.complete(false); - return null; - }); - - } catch (Exception e) { - PackCore.LOGGER.error("Failed to apply packs", e); - result.complete(false); - } - } - - private static List findAvailablePackIdsOrderedList(List packKeysOrdered) { - List found = new ArrayList<>(); - MinecraftClient client = MinecraftClient.getInstance(); - if (client == null) return found; - - net.minecraft.resource.ResourcePackManager packManager = client.getResourcePackManager(); - Collection allProfiles = packManager.getProfiles(); - - for (int index = 0; index < packKeysOrdered.size(); index++) { - String key = packKeysOrdered.get(index).trim(); - if (key.isEmpty()) continue; - - String[] keywords = getKeywordsForPack(key); - if (keywords == null) { - PackCore.LOGGER.warn("No keywords found for pack key: {}", key); - continue; - } - - PackCore.LOGGER.info("Looking for pack '{}' (selection #{}) with keywords: {}", - key, index + 1, Arrays.toString(keywords)); - - ResourcePackProfile bestMatch = findBestMatch(allProfiles, keywords); - - if (bestMatch != null) { - found.add(bestMatch.getId()); - PackCore.LOGGER.info("MATCHED '{}' -> '{}' (display: '{}') [Selection #{} -> Pack position #{}]", - key, bestMatch.getId(), bestMatch.getDisplayName().getString(), - index + 1, found.size()); - } else { - PackCore.LOGGER.warn("No match found for pack '{}' (selection #{}) with keywords: {}", - key, index + 1, Arrays.toString(keywords)); - } - } - - return found; - } - - private static String[] getKeywordsForPack(String key) { - String[] keywords = MULTI_PACK_KEYWORDS.get(key); - if (keywords == null) { - String keyword = PACK_KEYWORDS.get(key); - if (keyword != null) { - keywords = new String[]{keyword}; - } - } - return keywords; - } - - private static ResourcePackProfile findBestMatch(Collection allProfiles, String[] keywords) { - ResourcePackProfile bestMatch = null; - int bestScore = -1; - - for (ResourcePackProfile profile : allProfiles) { - String name = stripMinecraftColors(profile.getDisplayName().getString().toLowerCase()); - String id = stripMinecraftColors(profile.getId().toLowerCase()); - String desc = stripMinecraftColors(profile.getDescription().getString().toLowerCase()); - - int totalScore = 0; - int matched = 0; - for (String kw : keywords) { - int score = getMatchScore(id, name, desc, kw.toLowerCase()); - if (score > 0) { - totalScore += score; - matched++; - } - } - totalScore += matched > 1 ? matched * 2 : 0; - - if (totalScore > bestScore) { - bestScore = totalScore; - bestMatch = profile; - } - } - return bestMatch; - } - - private static int getMatchScore(String id, String name, String desc, String keyword) { - if (id.equals(keyword)) return 5; - if (id.startsWith(keyword)) return 4; - if (id.contains(keyword)) return 3; - if (name.contains(keyword)) return 2; - if (desc.contains(keyword)) return 1; - return 0; - } - - private static Set getAllKnownPackIds() { - Set allKnown = new HashSet<>(); - MinecraftClient client = MinecraftClient.getInstance(); - if (client == null) return allKnown; - - net.minecraft.resource.ResourcePackManager packManager = client.getResourcePackManager(); - - Set allKeywords = new HashSet<>(PACK_KEYWORDS.values()); - for (String[] keywords : MULTI_PACK_KEYWORDS.values()) { - allKeywords.addAll(Arrays.asList(keywords)); - } - - for (String keyword : allKeywords) { - for (ResourcePackProfile profile : packManager.getProfiles()) { - String name = stripMinecraftColors(profile.getDisplayName().getString().toLowerCase()); - String id = stripMinecraftColors(profile.getId().toLowerCase()); - String desc = stripMinecraftColors(profile.getDescription().getString().toLowerCase()); - - if (name.contains(keyword) || id.contains(keyword) || desc.contains(keyword)) { - allKnown.add(profile.getId()); - } - } - } - - PackCore.LOGGER.debug("All known managed pack IDs: {}", allKnown); - return allKnown; - } - - private static String stripMinecraftColors(String text) { - return text.replaceAll("§[0-9a-fk-or]", ""); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/sodium/SodiumConfigurator.java b/src/main/java/com/github/kd_gaming1/packcore/integration/sodium/SodiumConfigurator.java deleted file mode 100644 index 016d832..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/sodium/SodiumConfigurator.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.sodium; - -import com.github.kd_gaming1.packcore.integration.minecraft.PerformanceProfileService; -import net.caffeinemc.mods.sodium.client.SodiumClientMod; -//? if >=1.21.11 { -/*import net.caffeinemc.mods.sodium.client.gui.SodiumOptions; -*///?} else { -import net.caffeinemc.mods.sodium.client.gui.SodiumGameOptions; - //?} -import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.io.IOException; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Integration class for Sodium-specific settings. - * This class is only loaded when Sodium is present. - */ -public class SodiumConfigurator { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - public static boolean applyProfile(PerformanceProfileService.PerformanceProfile profile) { - try { - //? if >=1.21.11 { - /*SodiumOptions options = SodiumClientMod.options(); - *///?} else { - SodiumGameOptions options = SodiumClientMod.options(); - //?} - - if (options.isReadOnly()) { - LOGGER.warn("Sodium config is read-only, cannot apply profile"); - return false; - } - - switch (profile) { - case PERFORMANCE -> applyPerformanceSettings(options); - case BALANCED -> applyBalancedSettings(options); - case QUALITY, SHADERS -> applyQualitySettings(options); - default -> { - LOGGER.warn("Unknown profile: {}", profile); - return false; - } - } - - //? if >=1.21.11 { - /*SodiumOptions.writeToDisk(options); - *///?} else { - SodiumGameOptions.writeToDisk(options); - //?} - LOGGER.debug("Sodium profile '{}' applied successfully", profile.name()); - return true; - - } catch (IOException e) { - LOGGER.error("Failed to save Sodium configuration", e); - return false; - } catch (Exception e) { - LOGGER.error("Unexpected error applying Sodium profile", e); - return false; - } - } - - public static boolean restoreDefaults() { - try { - SodiumClientMod.restoreDefaultOptions(); - LOGGER.debug("Sodium default settings restored"); - return true; - } catch (Exception e) { - LOGGER.error("Failed to restore Sodium defaults", e); - return false; - } - } - - //? if >=1.21.11 { - /*private static void applyPerformanceSettings(SodiumOptions options) { - *///?} else { - private static void applyPerformanceSettings(SodiumGameOptions options) { - //?} - // Advanced settings - options.advanced.enableMemoryTracing = false; - options.advanced.useAdvancedStagingBuffers = true; - options.advanced.cpuRenderAheadLimit = 3; - - // Performance settings - options.performance.chunkBuilderThreads = 0; - options.performance.chunkBuildDeferMode = DeferMode.ALWAYS; - options.performance.animateOnlyVisibleTextures = true; - options.performance.useEntityCulling = true; - options.performance.useFogOcclusion = true; - options.performance.useBlockFaceCulling = true; - options.performance.useNoErrorGLContext = true; - - //? if <1.21.11 { - options.quality.weatherQuality = SodiumGameOptions.WeatherQuality.FAST; - options.quality.leavesQuality = SodiumGameOptions.LeavesQuality.FAST; - options.quality.enableVignette = true; - //?} - - LOGGER.debug("Applied Sodium performance settings"); - } - - //? if >=1.21.11 { - /*private static void applyBalancedSettings(SodiumOptions options) { - *///?} else { - private static void applyBalancedSettings(SodiumGameOptions options) { - //?} - // Advanced settings - options.advanced.enableMemoryTracing = false; - options.advanced.useAdvancedStagingBuffers = true; - options.advanced.cpuRenderAheadLimit = 3; - - // Performance settings - options.performance.chunkBuilderThreads = 0; - options.performance.chunkBuildDeferMode = DeferMode.ALWAYS; - options.performance.animateOnlyVisibleTextures = true; - options.performance.useEntityCulling = true; - options.performance.useFogOcclusion = true; - options.performance.useBlockFaceCulling = true; - options.performance.useNoErrorGLContext = true; - - //? if <1.21.11 { - options.quality.weatherQuality = SodiumGameOptions.WeatherQuality.DEFAULT; - options.quality.leavesQuality = SodiumGameOptions.LeavesQuality.DEFAULT; - options.quality.enableVignette = true; - //?} - - LOGGER.debug("Applied Sodium balanced settings"); - } - - //? if >=1.21.11 { - /*private static void applyQualitySettings(SodiumOptions options) { - *///?} else { - private static void applyQualitySettings(SodiumGameOptions options) { - //?} - // Advanced settings - options.advanced.enableMemoryTracing = false; - options.advanced.useAdvancedStagingBuffers = true; - options.advanced.cpuRenderAheadLimit = 3; - - // Performance settings - options.performance.chunkBuilderThreads = 0; - options.performance.chunkBuildDeferMode = DeferMode.ALWAYS; - options.performance.animateOnlyVisibleTextures = true; - options.performance.useEntityCulling = true; - options.performance.useFogOcclusion = true; - options.performance.useBlockFaceCulling = true; - options.performance.useNoErrorGLContext = true; - - //? if <1.21.11 { - options.quality.weatherQuality = SodiumGameOptions.WeatherQuality.FANCY; - options.quality.leavesQuality = SodiumGameOptions.LeavesQuality.FANCY; - options.quality.enableVignette = true; - //?} - - LOGGER.debug("Applied Sodium quality settings"); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/integration/tabdesign/TabDesignManager.java b/src/main/java/com/github/kd_gaming1/packcore/integration/tabdesign/TabDesignManager.java deleted file mode 100644 index cf4912d..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/integration/tabdesign/TabDesignManager.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.github.kd_gaming1.packcore.integration.tabdesign; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Utility for applying the selected Tab Design (SkyHanni or Skyblocker) - * by changing the config of the respective mod at runtime. - */ -public class TabDesignManager { - - // Store the pending enable state atomically to avoid race conditions - private static final AtomicReference pendingSkyHanniState = new AtomicReference<>(null); - - // Register the join listener once on class initialization - static { - ClientPlayConnectionEvents.JOIN.register((handler, sender, clientPlayNetworkHandler) -> { - Boolean state = pendingSkyHanniState.getAndSet(null); - if (state != null) { - scheduleDelayedCommand(state); - } - }); - } - - public static boolean applyTabDesignFromWizard() { - String tabDesign = WizardDataStore.getInstance().getTabDesign(); - - if (tabDesign == null || tabDesign.isEmpty() || "None".equals(tabDesign)) { - return false; - } - - return applyTabDesign(tabDesign); - } - - private static boolean isModLoaded(String modId) { - return FabricLoader.getInstance().isModLoaded(modId); - } - - // ===== Skyblocker (using reflection) ===== - - private static boolean enableSkyblockerTabList(boolean skyblockerSelected) { - try { - Class configManager = Class.forName("de.hysky.skyblocker.config.SkyblockerConfigManager"); - java.lang.reflect.Method updateMethod = configManager.getDeclaredMethod("update", java.util.function.Consumer.class); - - java.util.function.Consumer consumer = config -> updateSkyblockerConfig(config, skyblockerSelected); - - updateMethod.invoke(null, consumer); - PackCore.LOGGER.info("Set Skyblocker TabHud: tabHudEnabled=true, showVanillaTabByDefault={}", !skyblockerSelected); - return true; - - } catch (ClassNotFoundException e) { - PackCore.LOGGER.info("Skyblocker not present"); - return false; - } catch (Exception e) { - PackCore.LOGGER.warn("Could not update Skyblocker config", e); - return false; - } - } - - private static void updateSkyblockerConfig(Object config, boolean skyblockerSelected) { - try { - Object uiAndVisuals = config.getClass().getField("uiAndVisuals").get(config); - Object tabHud = uiAndVisuals.getClass().getField("tabHud").get(uiAndVisuals); - - // tabHudEnabled is ALWAYS true — Skyblocker's tab must stay enabled regardless. - // When SkyHanni is selected, showVanillaTabByDefault=true lets vanilla (SkyHanni) show through. - // When Skyblocker is selected, showVanillaTabByDefault=false so Skyblocker renders its own tab. - tabHud.getClass().getField("tabHudEnabled").setBoolean(tabHud, true); - tabHud.getClass().getField("showVanillaTabByDefault").setBoolean(tabHud, !skyblockerSelected); - } catch (Exception e) { - PackCore.LOGGER.warn("Failed to update Skyblocker TabHud config", e); - } - } - - // ===== SkyHanni (using command) ===== - - private static boolean enableSkyHanniTabList(boolean enable) { - try { - MinecraftClient client = MinecraftClient.getInstance(); - - if (client.player != null) { - scheduleDelayedCommand(enable); - return true; - } - - // Reset so the JOIN listener will fire again - pendingSkyHanniState.set(enable); - PackCore.LOGGER.info("Queued SkyHanni command to run on world join"); - return true; - - } catch (Exception e) { - PackCore.LOGGER.warn("Could not queue SkyHanni config command", e); - return false; - } - } - - private static void scheduleDelayedCommand(boolean enable) { - MinecraftClient client = MinecraftClient.getInstance(); - - new Thread(() -> { - try { - Thread.sleep(2000); - client.execute(() -> { - try { - executeSkyHanniCommand(enable); - PackCore.LOGGER.info("Executed SkyHanni command after delay"); - } catch (Exception e) { - PackCore.LOGGER.warn("Failed to execute SkyHanni command", e); - } - }); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }, "SkyHanni-Config-Delay").start(); - } - - private static void executeSkyHanniCommand(boolean enable) { - String command = "shconfig set config.gui.compactTabList.enabled " + enable; - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (player == null) return; - player.networkHandler.sendChatCommand(command); - PackCore.LOGGER.info("Executed SkyHanni command: /{}", command); - } - - /** - * Availability information for tab design mods - */ - public static class TabDesignAvailability { - private final boolean skyhanniAvailable; - private final boolean skyblockerAvailable; - - public TabDesignAvailability(boolean skyhanniAvailable, boolean skyblockerAvailable) { - this.skyhanniAvailable = skyhanniAvailable; - this.skyblockerAvailable = skyblockerAvailable; - } - - public boolean isSkyHanniAvailable() { - return skyhanniAvailable; - } - - public boolean isSkyblockerAvailable() { - return skyblockerAvailable; - } - } - - /** - * Get the availability of tab design mods - */ - public static TabDesignAvailability getAvailability() { - return new TabDesignAvailability( - isModLoaded("skyhanni"), - isModLoaded("skyblocker") - ); - } - - /** - * Apply a specific tab design by name - * - * @param design "skyhanni" or "skyblocker" - * @return true if successfully applied - */ - public static boolean applyTabDesign(String design) { - if (design == null || design.isEmpty()) { - return false; - } - - boolean skyblockerPresent = isModLoaded("skyblocker"); - boolean skyhanniPresent = isModLoaded("skyhanni"); - - if ("skyblocker".equalsIgnoreCase(design) && skyblockerPresent) { - // skyblockerSelected=true → tabHudEnabled=true, showVanillaTabByDefault=false - boolean changed = enableSkyblockerTabList(true); - if (skyhanniPresent) enableSkyHanniTabList(false); - return changed; - } else if ("skyhanni".equalsIgnoreCase(design) && skyhanniPresent) { - // skyblockerSelected=false → tabHudEnabled=true, showVanillaTabByDefault=true - boolean changed = enableSkyHanniTabList(true); - if (skyblockerPresent) enableSkyblockerTabList(false); - return changed; - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/lavendermd/CustomLavenderCompiler.java b/src/main/java/com/github/kd_gaming1/packcore/lavendermd/CustomLavenderCompiler.java deleted file mode 100644 index dfdf864..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/lavendermd/CustomLavenderCompiler.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.kd_gaming1.packcore.lavendermd; - -import io.wispforest.lavendermd.compiler.OwoUICompiler; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.core.HorizontalAlignment; -import io.wispforest.owo.ui.core.Sizing; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -public class CustomLavenderCompiler extends OwoUICompiler { - - public CustomLavenderCompiler() { - super(); - } - - @Override - public void visitImage(Identifier image, String description, boolean fit) { - if (fit) { - this.append(Containers.stack(Sizing.fill(100), Sizing.content()) - .child(Components.texture(image, 0, 0, 256, 256, 256, 256) - .blend(true) - .tooltip(Text.literal(description)) - .sizing(Sizing.fill(100), Sizing.content())) - .horizontalAlignment(HorizontalAlignment.CENTER)); - } else { - super.visitImage(image, description, fit); - } - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/metadata/ModpackMetadata.java b/src/main/java/com/github/kd_gaming1/packcore/metadata/ModpackMetadata.java new file mode 100644 index 0000000..89e8160 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/metadata/ModpackMetadata.java @@ -0,0 +1,116 @@ +package com.github.kd_gaming1.packcore.metadata; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.fabricmc.loader.api.FabricLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ModpackMetadata { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ModpackMetadata"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final JsonObject DEFAULTS = buildDefaults(); + private static final String FILE_NAME = "modpack.json"; + private static final Path CONFIG_PATH = FabricLoader.getInstance().getGameDir().resolve("packcore").resolve(FILE_NAME); + + private static final class Holder { + static final ModpackMetadata INSTANCE = new ModpackMetadata(); + } + + public static ModpackMetadata getInstance() { + return Holder.INSTANCE; + } + + private final String modpackName; + private final String modpackVersion; + private final String minecraftVersion; + private final String author; + private final String description; + private final String modrinthProjectId; + private final String websiteUrl; + private final String discordUrl; + private final String issueTrackerUrl; + private final String wikiUrl; + + private ModpackMetadata() { + ensureFileExists(); + + try (Reader reader = Files.newBufferedReader(CONFIG_PATH)) { + JsonObject json = JsonParser.parseReader(reader).getAsJsonObject(); + + modpackName = get(json, "modpackName"); + modpackVersion = get(json, "modpackVersion"); + minecraftVersion = get(json, "minecraftVersion"); + author = get(json, "author"); + description = get(json, "description"); + modrinthProjectId = get(json, "modrinthProjectId"); + websiteUrl = get(json, "websiteUrl"); + discordUrl = get(json, "discordUrl"); + issueTrackerUrl = get(json, "issueTrackerUrl"); + wikiUrl = get(json, "wikiUrl"); + + } catch (IOException e) { + throw new RuntimeException("Failed to load " + FILE_NAME, e); + } + } + + private void ensureFileExists() { + if (Files.exists(CONFIG_PATH)) return; + + try { + Files.createDirectories(CONFIG_PATH.getParent()); + + try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) { + GSON.toJson(DEFAULTS, writer); + } + + LOGGER.info("Created {} with default values", FILE_NAME); + } catch (IOException e) { + throw new RuntimeException("Failed to create " + FILE_NAME, e); + } + } + + private static JsonObject buildDefaults() { + JsonObject json = new JsonObject(); + json.addProperty("modpackName", "Unknown Modpack"); + json.addProperty("modpackVersion", "Unknown"); + json.addProperty("minecraftVersion", "Unknown"); + json.addProperty("author", "Unknown Author"); + json.addProperty("description", "Unknown Description"); + json.addProperty("modrinthProjectId", "Unknown Modrinth ID"); + json.addProperty("websiteUrl", "Unknown Website URL"); + json.addProperty("discordUrl", "Unknown Discord URL"); + json.addProperty("issueTrackerUrl", "Unknown Issue URL"); + json.addProperty("wikiUrl", "Unknown Wiki URL"); + return json; + } + + /** Returns the value for the given key, falling back to the default if missing. */ + private String get(JsonObject json, String key) { + if (json.has(key) && !json.get(key).isJsonNull()) { + return json.get(key).getAsString(); + } + LOGGER.warn("{} missing field '{}', using default", FILE_NAME, key); + return DEFAULTS.get(key).getAsString(); + } + + public String getModpackName() { return modpackName; } + public String getModpackVersion() { return modpackVersion; } + public String getMinecraftVersion() { return minecraftVersion; } + public String getAuthor() { return author; } + public String getDescription() { return description; } + public String getModrinthProjectId(){ return modrinthProjectId; } + public String getWebsiteUrl() { return websiteUrl; } + public String getDiscordUrl() { return discordUrl; } + public String getIssueTrackerUrl() { return issueTrackerUrl; } + public String getWikiUrl() { return wikiUrl; } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/mixin/CrashReportMixin.java b/src/main/java/com/github/kd_gaming1/packcore/mixin/CrashReportMixin.java deleted file mode 100644 index 1263836..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/mixin/CrashReportMixin.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.kd_gaming1.packcore.mixin; - -import com.github.kd_gaming1.packcore.crash.CrashBrandingHandler; -import net.minecraft.util.crash.CrashReport; -import net.minecraft.util.crash.CrashReportSection; -import net.minecraft.util.crash.ReportType; -import org.spongepowered. asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered. asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm. mixin.injection.Inject; -import org.spongepowered.asm.mixin. injection.callback.CallbackInfoReturnable; - -import java.nio.file.Path; -import java.util.List; - -/** - * Injects modpack branding information into crash reports. - * This helps users and support teams quickly identify the modpack version. - * - * Compatible with Minecraft 1.21.10+ (updated writeToFile signature) - */ -@Mixin(CrashReport.class) -public class CrashReportMixin { - @Shadow @Final private List otherSections; - - /** - * Inject at HEAD of writeToFile to add modpack branding section - * before the crash report is written to disk. - */ - @Inject(method = "writeToFile", at = @At("HEAD")) - private void packcore$addModpackBranding( - Path path, - ReportType reportType, - List list, - CallbackInfoReturnable cir - ) { - try { - CrashReportSection section = new CrashReportSection("Modpack Information"); - CrashBrandingHandler.addBranding(section); - otherSections.add(section); - } catch (Exception e) { - // Don't let branding injection cause crashes - System.err.println("PackCore: Failed to add modpack branding to crash report: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/modpack/ModpackInfo.java b/src/main/java/com/github/kd_gaming1/packcore/modpack/ModpackInfo.java deleted file mode 100644 index 344db22..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/modpack/ModpackInfo.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.github.kd_gaming1.packcore.modpack; - -import com.github.kd_gaming1.packcore.util.GsonUtils; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.SerializedName; -import net.fabricmc.loader.api.FabricLoader; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ModpackInfo { - // Using a special comment field that gets ignored by our code but helps users - @SerializedName("_comment") - private String comment = "Edit the values below for your modpack. Remove this _comment field when done."; - - @SerializedName("mod_name") - private String modName = "YOUR_MODPACK_NAME_HERE"; - - @SerializedName("mod_version") - private String modVersion = "1.0.0"; - - @SerializedName("minecraft_version") - private String minecraftVersion = "1.21.+"; - - @SerializedName("author") - private String author = "YOUR_NAME_HERE"; - - @SerializedName("description") - private String description = "A brief description of your modpack"; - - @SerializedName("modrinth_project_id") - private String modrinthProjectId = "YOUR_PROJECT_ID_FROM_MODRINTH_URL"; - - @SerializedName("update_channel") - private String updateChannel = "release"; // release/beta/alpha - - @SerializedName("website") - private String website = "https://your-website.com"; - - @SerializedName("discord") - private String discord = "https://discord.gg/your-invite"; - - @SerializedName("issue_tracker") - private String issueTracker = "https://github.com/yourname/yourmod/issues"; - - @SerializedName("wiki") - private String wiki = "https://your-wiki-url.com"; - - // File handling - private static final String CONFIG_FILE_NAME = "modpack-info.json"; - private static final Gson gson = GsonUtils.GSON; - - // File save location - private static final Path runDir = FabricLoader.getInstance().getGameDir(); - private static final Path packcoreDir = runDir.resolve("packcore"); - - // Constructor - public ModpackInfo() {} - - // Save to file - public void saveToFile(Path packcoreDir) throws IOException { - Path filePath = packcoreDir.resolve(CONFIG_FILE_NAME); - try (FileWriter writer = new FileWriter(filePath.toFile())) { - gson.toJson(this, writer); - } - } - - // Load from file - public static ModpackInfo loadFromFile(Path packcoreDir) throws IOException { - Path filePath = packcoreDir.resolve(CONFIG_FILE_NAME); - - if (!Files.exists(filePath)) { - // Create default file - ModpackInfo defaultInfo = new ModpackInfo(); - defaultInfo.setConfigDirectory(packcoreDir); - defaultInfo.saveToFile(packcoreDir); - return defaultInfo; - } - - try (FileReader reader = new FileReader(filePath.toFile())) { - ModpackInfo info = gson.fromJson(reader, ModpackInfo.class); - info.setConfigDirectory(packcoreDir); - return info; - } - } - - // Store the config directory for auto-saving - private transient Path configDirectory; - - // Method to set the config directory (call this after loading) - public void setConfigDirectory(Path configDir) { - this.configDirectory = configDir; - } - - // Auto-save helper method - private void autoSave() { - if (configDirectory != null) { - try { - saveToFile(configDirectory); - } catch (IOException e) { - System.err.println("Failed to auto-save modpack info: " + e.getMessage()); - } - } - } - - public boolean isConfigurationValid() { - return modrinthProjectId.equals("YOUR_PROJECT_ID_FROM_MODRINTH_URL") || - modrinthProjectId.trim().isEmpty() || - modName.equals("YOUR_MODPACK_NAME_HERE") || - author.equals("YOUR_NAME_HERE"); - } - - public String getValidationError() { - if (modrinthProjectId.equals("YOUR_PROJECT_ID_FROM_MODRINTH_URL")) { - return "Modrinth project ID is not configured. Please set a valid project ID."; - } - if (modrinthProjectId.trim().isEmpty()) { - return "Modrinth project ID is empty."; - } - if (modName.equals("YOUR_MODPACK_NAME_HERE")) { - return "Mod name is not configured."; - } - if (author.equals("YOUR_NAME_HERE")) { - return "Author is not configured."; - } - return null; - } - - // Getters - public String getName() { return modName; } - public String getVersion() { return modVersion; } - public String getMinecraftVersion() { return minecraftVersion; } - public String getAuthor() { return author; } - public String getDescription() { return description; } - public String getModrinthProjectId() { return modrinthProjectId; } - public String getUpdateChannel() { return updateChannel; } - public String getWebsite() { return website; } - public String getDiscord() { return discord; } - public String getIssueTracker() { return issueTracker; } - public String getWiki() { return wiki; } - - // Setters with auto-save - public void setName(String name) { this.modName = name; autoSave(); } - public void setVersion(String version) { this.modVersion = version; autoSave(); } - public void setMinecraftVersion(String version) { this.minecraftVersion = version; autoSave(); } - public void setAuthor(String author) { this.author = author; autoSave(); } - public void setDescription(String description) { this.description = description; autoSave(); } - public void setModrinthProjectId(String id) { this.modrinthProjectId = id; autoSave(); } - public void setUpdateChannel(String channel) { this.updateChannel = channel; autoSave(); } - public void setWebsite(String website) { this.website = website; autoSave(); } - public void setDiscord(String discord) { this.discord = discord; autoSave(); } - public void setIssueTracker(String tracker) { this.issueTracker = tracker; autoSave(); } - public void setWiki(String wiki) { this.wiki = wiki; autoSave(); } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/notification/BackupNotifications.java b/src/main/java/com/github/kd_gaming1/packcore/notification/BackupNotifications.java deleted file mode 100644 index bff5d1c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/notification/BackupNotifications.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.kd_gaming1.packcore.notification; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.toast.PackCoreToast; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.text.ClickEvent; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.nio.file.Path; - -/** - * Notifies users when backup operations complete - */ -public class BackupNotifications { - public static void notifyBackupComplete(String backupName, Path backupPath, boolean isRestore) { - MinecraftClient client = MinecraftClient.getInstance(); - ClientPlayerEntity player = client.player; - - // If player is in-game, send chat message - if (player != null && client.world != null) { - sendChatNotification(player, backupName, backupPath, isRestore); - } else { - // If not in-game, show toast - showToastNotification(backupName, backupPath, isRestore); - } - } - - private static void sendChatNotification(ClientPlayerEntity player, String backupName, - Path backupPath, boolean isRestore) { - try { - ClickEvent clickEvent = new ClickEvent.OpenFile(backupPath.getParent()); - - Text message = Text.literal("[PackCore] ").formatted(Formatting.GOLD, Formatting.BOLD) - .append(Text.literal(isRestore ? "Restore completed! " : "Backup created! ") - .formatted(Formatting.GREEN)) - .append(Text.literal("\"" + backupName + "\"").formatted(Formatting.YELLOW)) - .append(Text.literal(" [Click to open folder]") - .formatted(Formatting.AQUA, Formatting.UNDERLINE) - .styled(style -> style.withClickEvent(clickEvent))); - - player.sendMessage(message, false); - } catch (Exception e) { - PackCore.LOGGER.warn("[Backup] Failed to send chat notification", e); - // Fallback to toast - showToastNotification(backupName, backupPath, isRestore); - } - } - - private static void showToastNotification(String backupName, Path backupPath, boolean isRestore) { - PackCoreToast.showBackupComplete(backupName, String.valueOf(backupPath), isRestore); - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/notification/ExportNotifications.java b/src/main/java/com/github/kd_gaming1/packcore/notification/ExportNotifications.java deleted file mode 100644 index 577e018..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/notification/ExportNotifications.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.kd_gaming1.packcore.notification; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.toast.PackCoreToast; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.text.ClickEvent; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.nio.file.Path; - -public class ExportNotifications { - public static void notifyExportComplete(String configName, Path exportPath) { - MinecraftClient client = MinecraftClient.getInstance(); - ClientPlayerEntity player = client.player; - - // If player is in-game, send chat message - if (player != null && client.world != null) { - sendChatNotification(player, configName, exportPath); - } else { - // If not in-game, show toast - showToastNotification(configName, exportPath); - } - } - - private static void sendChatNotification(ClientPlayerEntity player, String configName, Path exportPath) { - try { - String folderPath = exportPath.getParent().toUri().toString(); - ClickEvent clickEvent = new ClickEvent.OpenFile(exportPath.getParent()); - - Text message = Text.literal("[PackCore] ").formatted(Formatting.GOLD, Formatting.BOLD) - .append(Text.literal("Export completed! ").formatted(Formatting.GREEN)) - .append(Text.literal("\"" + configName + "\"").formatted(Formatting.YELLOW)) - .append(Text.literal(" [Click to open folder]").formatted(Formatting.AQUA, Formatting.UNDERLINE) - .styled(style -> style.withClickEvent(clickEvent))); - - player.sendMessage(message, false); - } catch (Exception e) { - PackCore.LOGGER.warn("[Export] Failed to send chat notification", e); - // Fallback to toast - showToastNotification(configName, exportPath); - } - } - - private static void showToastNotification(String configName, Path exportPath) { - PackCoreToast.showExportComplete(configName, String.valueOf(exportPath)); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/notification/UpdateNotifier.java b/src/main/java/com/github/kd_gaming1/packcore/notification/UpdateNotifier.java deleted file mode 100644 index 743dc64..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/notification/UpdateNotifier.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.github.kd_gaming1.packcore.notification; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.ui.toast.PackCoreToast; -import com.github.kd_gaming1.packcore.util.update.UpdateResult; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.text.ClickEvent; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.net.URI; -import java.util.HashSet; -import java.util.Set; - -public class UpdateNotifier { - private static final long MAIN_MENU_TOAST_COOLDOWN_MS = 300_000; // 5 minutes for main menu - private static final Set shownVersionsThisSession = new HashSet<>(); - private static long lastMainMenuToastTime = 0; - private static boolean hasShownInGameNotificationThisSession = false; - - static { - // Register event for in-game notifications - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { - if (!hasShownInGameNotificationThisSession) { - checkAndShowInGameNotification(); - } - }); - } - - public static boolean shouldShowMainMenuToast(String newVersion) { - long now = System.currentTimeMillis(); - return !shownVersionsThisSession.contains(newVersion) && - (now - lastMainMenuToastTime > MAIN_MENU_TOAST_COOLDOWN_MS); - } - - public static void showMainMenuToast(String currentVersion, String newVersion, String modpackName) { - PackCoreToast.showUpdateAvailable(currentVersion, newVersion, modpackName); - shownVersionsThisSession.add(newVersion); - lastMainMenuToastTime = System.currentTimeMillis(); - } - - private static void checkAndShowInGameNotification() { - ModpackInfo info = PackCore.getModpackInfo(); - if (info == null || !PackCoreConfig.enableUpdateNotifications) return; - - UpdateResult result = PackCore.getUpdateManager().checkForUpdates(info); - if (result.isSuccess() && result.isUpdateAvailable()) { - showInGameChatMessage(info.getVersion(), result.getVersionNumber(), - result.getModrinthUrl(), info.getName()); - hasShownInGameNotificationThisSession = true; - } - } - - private static void showInGameChatMessage(String currentVersion, String newVersion, - String modrinthUrl, String modpackName) { - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (player == null) return; - - try { - URI uri = URI.create(modrinthUrl); - ClickEvent clickEvent = new ClickEvent.OpenUrl(uri); - - Text updateMessage = Text.literal("[" + modpackName + "] ") - .formatted(Formatting.GOLD, Formatting.BOLD) - .append(Text.literal("Update available! ").formatted(Formatting.YELLOW)) - .append(Text.literal(currentVersion + " → " + newVersion + " ").formatted(Formatting.WHITE)) - .append(Text.literal("[Click to view]").formatted(Formatting.AQUA, Formatting.UNDERLINE) - .styled(style -> style.withClickEvent(clickEvent))); - - player.sendMessage(updateMessage, false); - } catch (IllegalArgumentException e) { - PackCore.LOGGER.error("Invalid Modrinth URL: {}", modrinthUrl, e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/playtime/PlaytimeTracker.java b/src/main/java/com/github/kd_gaming1/packcore/playtime/PlaytimeTracker.java new file mode 100644 index 0000000..236d7fb --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/playtime/PlaytimeTracker.java @@ -0,0 +1,82 @@ +package com.github.kd_gaming1.packcore.playtime; + +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.configpack.BackupManager; +import com.github.kd_gaming1.packcore.gui.util.ToastHelper; +import eu.midnightdust.lib.config.MidnightConfig; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; + +public final class PlaytimeTracker { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/PlaytimeTracker"); + + private static final long BACKUP_INTERVAL_MS = 3L * 24 * 60 * 60 * 1_000; + + private static final DateTimeFormatter BACKUP_NAME_FORMAT = + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(ZoneId.systemDefault()); + + private static final Executor BACKUP_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + + private PlaytimeTracker() {} + + public static void onSessionStart() { + long now = System.currentTimeMillis(); + + if (!PackCoreConfig.autoBackupEnabled) { + PackCoreConfig.lastSeenEpochMs = now; + MidnightConfig.write(MOD_ID); + return; + } + + long intervalMs = PackCoreConfig.autoBackupIntervalDays * 24L * 60 * 60 * 1_000; + long lastBackup = PackCoreConfig.lastBackupEpochMs; + long lastSeen = PackCoreConfig.lastSeenEpochMs; + long msSinceBackup = now - lastBackup; + long msSinceLastSeen = now - lastSeen; + + boolean shouldAutoBackup = lastBackup > 0 + && msSinceBackup >= intervalMs + && msSinceLastSeen < intervalMs; + + PackCoreConfig.lastSeenEpochMs = now; + if (shouldAutoBackup) { + PackCoreConfig.lastBackupEpochMs = now; + } + MidnightConfig.write(MOD_ID); + + if (shouldAutoBackup) { + triggerAutoBackupAsync(); + } + } + + public static void onSessionEnd() { + PackCoreConfig.lastSeenEpochMs = System.currentTimeMillis(); + MidnightConfig.write(MOD_ID); + } + + private static void triggerAutoBackupAsync() { + String backupName = "auto_" + BACKUP_NAME_FORMAT.format(Instant.now()) + ".zip"; + LOGGER.info("Scheduling automatic backup: {}", backupName); + + BACKUP_EXECUTOR.execute(() -> { + try { + BackupManager.createBackup(FabricLoader.getInstance().getGameDir()); + Minecraft.getInstance().execute(() -> ToastHelper.showBackupCreated(backupName)); + } catch (IOException e) { + LOGGER.error("Automatic backup failed: {}", e.getMessage()); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/prelaunch/PreLaunchWizardInitializer.java b/src/main/java/com/github/kd_gaming1/packcore/prelaunch/PreLaunchWizardInitializer.java deleted file mode 100644 index cc0533c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/prelaunch/PreLaunchWizardInitializer.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.github.kd_gaming1.packcore.prelaunch; - -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.config.apply.ConfigAutoApplier; -import com.github. kd_gaming1.packcore.config.apply.ConfigApplyService; -import com.github.kd_gaming1.packcore.config.apply.SelectiveConfigApplyService; -import com.github.kd_gaming1.packcore.config.backup.SelectiveBackupRestoreService; -import com.github.kd_gaming1.packcore.config. update.ConfigUpdateService; -import com.github.kd_gaming1.packcore.util.io.file.FileLayoutInitializer; -import eu.midnightdust.lib.config.MidnightConfig; -import net.fabricmc.loader.api. FabricLoader; -import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Files; -import java.nio.file.Path; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -public class PreLaunchWizardInitializer implements PreLaunchEntrypoint { - - private static final Logger LOGGER = LoggerFactory.getLogger(PreLaunchWizardInitializer.class); - - @Override - public void onPreLaunch() { - LOGGER.info("PackCore pre-launch initializer started"); - - MidnightConfig.init("packcore", PackCoreConfig.class); - Path runDir = FabricLoader.getInstance().getGameDir(); - - FileLayoutInitializer.initializeFileStructure(); - - // CHECK FOR PENDING CONFIG APPLICATION FIRST - boolean configApplied = ConfigApplyService.checkAndApplyPendingConfig(runDir); - if (configApplied) { - LOGGER.info("Applied pending config during pre-launch"); - PackCoreConfig.defaultConfigSuccessfullyApplied = true; - PackCoreConfig.write(MOD_ID); - } - - // CHECK FOR PENDING SELECTIVE CONFIG APPLICATION - boolean selectiveConfigApplied = SelectiveConfigApplyService.checkAndApplyPendingSelectiveConfig(runDir); - if (selectiveConfigApplied) { - LOGGER.info("Applied pending selective config during pre-launch"); - // Note: We don't set defaultConfigSuccessfullyApplied for selective applies - // since they're partial applications - } - - // CHECK FOR PENDING SELECTIVE BACKUP RESTORE - boolean selectiveRestoreApplied = SelectiveBackupRestoreService.checkAndApplyPendingSelectiveRestore(runDir); - if (selectiveRestoreApplied) { - LOGGER.info("Applied pending selective backup restore during pre-launch"); - } - - // Handle first-time setup - if (isUpgradeFromOldVersion(runDir)) { - LOGGER.info("Upgrade from old pre 2.0 version detected"); - PackCoreConfig.isFirstStartup = false; - PackCoreConfig. write(MOD_ID); - // Update configs will apply below for upgrade users - } else if (shouldApplyConfigAutomatically()) { - LOGGER.info("First launch detected - applying config automatically.. ."); - boolean success = ConfigAutoApplier.applyBestMatchingConfig(runDir); - - if (success) { - LOGGER.info("Config applied successfully on first launch"); - PackCoreConfig.defaultConfigSuccessfullyApplied = true; - PackCoreConfig.isFirstStartup = false; - PackCoreConfig.write(MOD_ID); - // Skip update configs - we just applied the latest full config - LOGGER.info("Skipping update configs - full config already applied"); - return; - } - } - - // Apply update configs (only if full config was applied previously) - if (PackCoreConfig.defaultConfigSuccessfullyApplied) { - LOGGER.info("Checking for config updates..."); - ConfigUpdateService.checkAndApplyUpdates(runDir); - } - - LOGGER.info("PackCore pre-launch initialization complete"); - } - - private boolean shouldApplyConfigAutomatically() { - return PackCoreConfig.isFirstStartup && - !PackCoreConfig. defaultConfigSuccessfullyApplied; - } - - /** - * Checks if this is an upgrade from the old version by detecting the - * "SkyBlock Enhanced" folder in the root game directory. - * - * @param gameDir The game directory - * @return true if "SkyBlock Enhanced" folder exists (old install), false otherwise - */ - private boolean isUpgradeFromOldVersion(Path gameDir) { - Path oldFolder = gameDir.resolve("SkyBlock Enhanced"); - boolean exists = Files.exists(oldFolder) && Files.isDirectory(oldFolder); - - if (exists) { - LOGGER. info("Detected 'SkyBlock Enhanced' folder - this is an upgrade from old version"); - } - - return exists; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/component/PlaceholderTextArea.java b/src/main/java/com/github/kd_gaming1/packcore/ui/component/PlaceholderTextArea.java deleted file mode 100644 index 6794619..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/component/PlaceholderTextArea.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.component; - -import io.wispforest.owo.ui.component.TextAreaComponent; -import io.wispforest.owo.ui.core.Sizing; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.text.Text; -import org.jetbrains.annotations.Nullable; - -/** - * Extended TextAreaUIComponent that supports placeholder text - */ -public class PlaceholderTextArea extends TextAreaComponent { - @Nullable - private Text placeholder; - private int placeholderColor = 0x808080; // Gray color for placeholder text - - protected PlaceholderTextArea(Sizing horizontalSizing, Sizing verticalSizing) { - super(horizontalSizing, verticalSizing); - } - - /** - * Sets the placeholder text that will be displayed when the text area is empty and not focused - */ - public PlaceholderTextArea placeholder(Text placeholder) { - this.placeholder = placeholder; - return this; - } - - /** - * Sets the color of the placeholder text - */ - public PlaceholderTextArea placeholderColor(int color) { - this.placeholderColor = color; - return this; - } - - @Override - protected void renderOverlay(DrawContext context) { - // Call the parent render method first - super.renderOverlay(context); - - // Render placeholder if text is empty and component is not focused - if (this.placeholder != null && this.getText().isEmpty() && !this.isFocused()) { - renderPlaceholder(context); - } - } - - private void renderPlaceholder(DrawContext context) { - var textRenderer = MinecraftClient.getInstance().textRenderer; - - // Calculate position similar to how TextFieldWidget does it - int x = this.getX() + 4; // Add padding - int y = this.getY() + 4; // Add padding - - // Draw the placeholder text - context.drawTextWithShadow(textRenderer, this.placeholder, x, y, this.placeholderColor); - } - - /** - * Factory method to create a new PlaceholderTextAreaUIComponent - */ - public static PlaceholderTextArea create(Sizing horizontalSizing, Sizing verticalSizing) { - return new PlaceholderTextArea(horizontalSizing, verticalSizing); - } - - /** - * Factory method to create a new PlaceholderTextAreaUIComponent with placeholder text - */ - public static PlaceholderTextArea create(Sizing horizontalSizing, Sizing verticalSizing, Text placeholder) { - return new PlaceholderTextArea(horizontalSizing, verticalSizing).placeholder(placeholder); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/component/tree/FileTreeNode.java b/src/main/java/com/github/kd_gaming1/packcore/ui/component/tree/FileTreeNode.java deleted file mode 100644 index abe8d05..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/component/tree/FileTreeNode.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.component.tree; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -public class FileTreeNode { - private final Path path; - private final String name; - private final boolean isDirectory; - private boolean expanded = false; - private boolean hidden = false; - private final List children = new ArrayList<>(); - private boolean childrenLoaded = false; - private boolean hasUnloadedChildren = false; - - public FileTreeNode(Path path, String name, boolean isDirectory) { - this.path = path; - this.name = name; - this.isDirectory = isDirectory; - } - - public Path getPath() { return path; } - public String getName() { return name; } - public boolean isDirectory() { return isDirectory; } - public boolean isExpanded() { return expanded; } - public void setExpanded(boolean expanded) { this.expanded = expanded; } - public boolean isHidden() { return hidden; } - public void setHidden(boolean hidden) { this.hidden = hidden; } - public List getChildren() { return children; } - public void addChild(FileTreeNode child) { this.children.add(child); } - public boolean isChildrenLoaded() { return childrenLoaded; } - public void setChildrenLoaded(boolean loaded) { this.childrenLoaded = loaded; } - public boolean hasUnloadedChildren() { return hasUnloadedChildren; } - public void setHasUnloadedChildren(boolean hasChildren) { this.hasUnloadedChildren = hasChildren; } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/component/tree/FileTreeUIHelper.java b/src/main/java/com/github/kd_gaming1/packcore/ui/component/tree/FileTreeUIHelper.java deleted file mode 100644 index 25e5a26..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/component/tree/FileTreeUIHelper.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.component.tree; - -import com.github.kd_gaming1.packcore.config.apply.FileDescriptionRegistry; -import io.wispforest.owo.ui.component.BoxComponent; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.CheckboxComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Text; - -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Shared helper for rendering file tree UI components. - * Promotes DRY by centralizing tree rendering logic. - */ -public class FileTreeUIHelper { - - /** - * Create a tree node row with expand/collapse, checkbox, and label - * - * @param node The tree node to render - * @param depth Indentation depth - * @param isSelected Whether this node is currently selected - * @param onExpand Callback when expand button is clicked - * @param onSelect Callback when checkbox is changed (node, checked) - * @param onHover Optional callback when node is hovered - * @return Configured FlowLayout for the tree row - */ - public static FlowLayout createTreeNodeRow( - FileTreeNode node, - int depth, - boolean isSelected, - Runnable onExpand, - BiConsumer onSelect, - Consumer onHover) { - - FlowLayout row = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .padding(Insets.left(depth * 16)) - .verticalAlignment(VerticalAlignment.CENTER); - - // Expand/collapse button for directories - if (node.isDirectory() && (!node.getChildren().isEmpty() || node.hasUnloadedChildren())) { - ButtonComponent expandBtn = (ButtonComponent) Components.button( - Text.literal(node.isExpanded() ? "▼" : "▶"), - btn -> onExpand.run() - ).renderer(ButtonComponent.Renderer.flat(ENTRY_BACKGROUND, ACCENT_SECONDARY, ENTRY_BORDER)) - .sizing(Sizing.fixed(16), Sizing.fixed(16)); - row.child(expandBtn); - } else { - // Spacer for alignment - BoxComponent spacer = Components.box(Sizing.fixed(16), Sizing.fixed(16)); - spacer.fill(true); - spacer.color(Color.ofArgb(0x00000000)); - row.child(spacer); - } - - // Checkbox - if (onSelect != null) { - CheckboxComponent checkbox = Components.checkbox(Text.empty()) - .checked(isSelected) - .onChanged(checked -> onSelect.accept(node, checked)); - row.child(checkbox); - } - - // Icon - String icon = getNodeIcon(node); - row.child(Components.label(Text.literal(icon)) - .color(color(ACCENT_SECONDARY))); - - // Label - row.child(Components.label(Text.literal(node.getName())) - .color(color(isSelected ? ACCENT_SECONDARY : TEXT_PRIMARY)) - .horizontalSizing(Sizing.expand())); - - // File count for directories - if (node.isDirectory() && !node.getChildren().isEmpty()) { - int fileCount = countFiles(node); - if (fileCount > 0) { - row.child(Components.label(Text.literal("[" + fileCount + "]")) - .color(color(TEXT_SECONDARY))); - } - } - - // Hover effects - if (onHover != null) { - row.mouseEnter().subscribe(() -> { - row.surface(Surface.flat(ENTRY_HOVER)); - onHover.accept(node); - }); - row.mouseLeave().subscribe(() -> - row.surface(Surface.BLANK)); - } - - return row; - } - - /** - * Create a simple tree node row without selection (for display only) - */ - public static FlowLayout createTreeNodeRowReadOnly( - FileTreeNode node, - int depth, - Runnable onExpand, - Consumer onHover) { - - return createTreeNodeRow(node, depth, false, onExpand, null, onHover); - } - - /** - * Get appropriate icon for a tree node - */ - private static String getNodeIcon(FileTreeNode node) { - if (node.isDirectory()) { - return node.isExpanded() ? "📂" : "📁"; - } - - // Get icon from registry based on path - return FileDescriptionRegistry.getIcon(node.getPath().toString()); - } - - /** - * Count total files in a directory tree - */ - public static int countFiles(FileTreeNode node) { - if (!node.isDirectory()) { - return 1; - } - - int count = 0; - for (FileTreeNode child : node.getChildren()) { - if (!child.isHidden()) { - count += countFiles(child); - } - } - return count; - } - - /** - * Calculate total size of files in a tree - */ - public static long calculateSize(FileTreeNode node) { - if (!node.isDirectory()) { - try { - return java.nio.file.Files.size(node.getPath()); - } catch (Exception e) { - return 0; - } - } - - long total = 0; - for (FileTreeNode child : node.getChildren()) { - if (!child.isHidden()) { - total += calculateSize(child); - } - } - return total; - } - - /** - * Apply selection to all children of a node - */ - public static void selectNodeAndChildren(FileTreeNode node, boolean selected, - java.util.Set selectedPaths) { - String path = node.getPath().toString(); - - if (selected) { - selectedPaths.add(path); - } else { - selectedPaths.remove(path); - } - - if (node.isDirectory()) { - for (FileTreeNode child : node.getChildren()) { - selectNodeAndChildren(child, selected, selectedPaths); - } - } - } - - /** - * Check if node or any children are selected - */ - public static boolean hasSelectedChildren(FileTreeNode node, java.util.Set selectedPaths) { - if (selectedPaths.contains(node.getPath().toString())) { - return true; - } - - if (node.isDirectory()) { - for (FileTreeNode child : node.getChildren()) { - if (hasSelectedChildren(child, selectedPaths)) { - return true; - } - } - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/help/guide/GuideDetailScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/help/guide/GuideDetailScreen.java deleted file mode 100644 index 1161abd..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/help/guide/GuideDetailScreen.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.help.guide; - -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import com.github.kd_gaming1.packcore.util.help.guide.GuideInfo; -import com.github.kd_gaming1.packcore.ui.theme.UITheme; -import io.wispforest.owo.ui.base.BaseOwoScreen; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import org.jetbrains.annotations.NotNull; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Screen that displays the detailed content of a selected guide. - */ -public class GuideDetailScreen extends BaseOwoScreen { - - private final GuideInfo guide; - private final Screen parentScreen; - private final Identifier backgroundTexture = Identifier.of(MOD_ID, "textures/gui/wizard/welcome_bg.png"); - - public GuideDetailScreen(GuideInfo guide, Screen parentScreen) { - this.guide = guide; - this.parentScreen = parentScreen; - } - - @Override - protected @NotNull OwoUIAdapter createAdapter() { - return OwoUIAdapter.create(this, Containers::verticalFlow); - } - - @Override - protected void build(FlowLayout rootUIComponent) { - rootUIComponent - .surface(TextureSurfaces.stretched(backgroundTexture, 1920, 1082)) - .padding(Insets.of(8)); - - rootUIComponent.child(createHeader()); - rootUIComponent.child(createMainContent()); - } - - private FlowLayout createHeader() { - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.fixed(42)) - .padding(Insets.of(2)) - .verticalAlignment(VerticalAlignment.CENTER); - - // Logo - header.child(Components.texture( - Identifier.of(MOD_ID, "textures/gui/assets/sbe_logo.png"), - 0, 0, 40, 40, 40, 40)); - - // Title - strip markdown formatting - String cleanTitle = stripMarkdownFormatting(guide.getTitle()); - header.child(Components.label( - Text.literal(cleanTitle) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(Color.ofRgb(UITheme.ACCENT_SECONDARY)) - .shadow(true) - .margins(Insets.of(0, 0, 4, 4))); - - // Spacer - header.child(Containers.horizontalFlow(Sizing.expand(), Sizing.expand())); - - // Back button - header.child(Components.button( - Text.literal("Back"), - btn -> MinecraftClient.getInstance().setScreen(parentScreen) - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/previous.png"), 0, 0, 90, 57)) - .sizing(Sizing.fixed(90), Sizing.fixed(19))); - - return header; - } - - private ScrollContainer createMainContent() { - String content = guide.getFullContent(); - if (content == null || content.isEmpty()) { - content = "# Error\n\nFailed to load guide content."; - } - - // Use the reusable markdown scroll component - return WizardUIComponents.createMarkdownScroll(content); - } - - /** - * Strip markdown formatting from text - */ - private String stripMarkdownFormatting(String text) { - if (text == null) return ""; - - // Remove {gold} and other color codes - text = text.replaceAll("\\{[^}]*\\}", ""); - - // Remove ** for bold - text = text.replaceAll("\\*\\*", ""); - - // Remove other common markdown - text = text.replaceAll("^#+\\s*", ""); // Headers - text = text.replaceAll("\\[([^\\]]+)\\]\\([^)]+\\)", "$1"); // Links - text = text.replaceAll("_", ""); // Italics - text = text.replaceAll("`", ""); // Code - - return text.trim(); - } - - @Override - public boolean shouldCloseOnEsc() { - return true; - } - - @Override - public void close() { - assert this.client != null; - this.client.setScreen(parentScreen); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/help/guide/GuideListScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/help/guide/GuideListScreen.java deleted file mode 100644 index fbeb9c4..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/help/guide/GuideListScreen.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.help.guide; - -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import com.github.kd_gaming1.packcore.ui.theme.UITheme; -import com.github.kd_gaming1.packcore.util.help.guide.GuideInfo; -import com.github.kd_gaming1.packcore.util.help.guide.GuideService; -import io.wispforest.owo.ops.TextOps; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import java.util.List; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Screen that displays a list of available guides for the user to browse and open. - * Refactored to use BasePackCoreScreen and ScreenUIComponents for cleaner code. - */ -public class GuideListScreen extends BasePackCoreScreen { - - private FlowLayout guideListContainer; - - public GuideListScreen() { - this(null); - } - - public GuideListScreen(Screen parentScreen) { - super(parentScreen); - } - - @Override - protected Component createTitleLabel() { - return Components.label( - Text.literal("Guides & Help") - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(Color.ofRgb(UITheme.ACCENT_SECONDARY)) - .shadow(true) - .margins(Insets.of(0, 0, 4, 4)); - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = (FlowLayout) Containers.verticalFlow(Sizing.fill(98), Sizing.expand()) - .gap(6) - .padding(Insets.of(8)); - - // Description - mainContent.child(Components.label( - TextOps.withColor( - "Welcome to the PackCore Guides & Help! Browse the list of guides, or join the Discord for help.", - UITheme.TEXT_PRIMARY - )) - .horizontalSizing(Sizing.fill(100)) - .margins(Insets.of(0, 0, 2, 0))); - - // Guide list container - guideListContainer = Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(4); - - // Wrap in scroll container - mainContent.child(ScreenUIComponents.createScrollContainer(guideListContainer)); - - // Load guides - loadGuides(); - - return mainContent; - } - - /** - * Load and display guides - */ - private void loadGuides() { - guideListContainer.clearChildren(); - - List guides = GuideService.loadAvailableGuides(); - - if (guides.isEmpty()) { - guideListContainer.child(Components.label( - TextOps.withColor( - "No guides found. Place .md files in the packcore/guides folder.", - UITheme.TEXT_SECONDARY - ) - )); - } else { - for (GuideInfo guide : guides) { - guideListContainer.child(createGuideEntry(guide)); - } - } - } - - /** - * Create a single guide entry - */ - private FlowLayout createGuideEntry(GuideInfo guide) { - FlowLayout entry = ScreenUIComponents.createListEntry(); - - // Title - strip markdown formatting - String cleanTitle = ScreenUIComponents.stripMarkdown(guide.getTitle()); - LabelComponent titleLabel = Components.label(Text.literal(cleanTitle)) - .color(Color.ofRgb(UITheme.ACCENT_SECONDARY)) - .shadow(false); - - // Preview text - String cleanPreview = ScreenUIComponents.stripMarkdown(guide.getPreview()); - LabelComponent previewLabel = (LabelComponent) Components.label(Text.literal(cleanPreview)) - .color(Color.ofRgb(UITheme.TEXT_SECONDARY)) - .sizing(Sizing.fill(100), Sizing.content()); - - entry.child(titleLabel); - entry.child(previewLabel); - - // Apply hover effects and click handling - ScreenUIComponents.applyHoverEffects(entry, () -> openGuide(guide)); - - return entry; - } - - /** - * Open a guide in the detail screen - */ - private void openGuide(GuideInfo guide) { - // Load guide content if not already loaded - if (!guide.isContentLoaded()) { - GuideService.loadGuideContent(guide); - } - - // Open the guide viewer screen - assert this.client != null; - this.client.setScreen(new GuideDetailScreen(guide, this)); - } - - /** - * Refresh the guide list - */ - public void refresh() { - loadGuides(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/base/BasePackCoreScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/base/BasePackCoreScreen.java deleted file mode 100644 index c8bc509..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/base/BasePackCoreScreen.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.base; - -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import io.wispforest.owo.ui.base.BaseOwoScreen; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Base class for all PackCore screens providing common layout patterns and utilities. - * Simplifies screen creation with standardized header, content areas, and navigation. - */ -public abstract class BasePackCoreScreen extends BaseOwoScreen { - - protected static final Identifier DEFAULT_BACKGROUND = - Identifier.of(MOD_ID, "textures/gui/wizard/welcome_bg.png"); - - protected final Screen parentScreen; - protected final Identifier backgroundTexture; - protected FlowLayout rootComponent; - - /** - * Create a screen with default background - */ - protected BasePackCoreScreen(@Nullable Screen parentScreen) { - this(parentScreen, DEFAULT_BACKGROUND); - } - - /** - * Create a screen with custom background - */ - protected BasePackCoreScreen(@Nullable Screen parentScreen, Identifier backgroundTexture) { - this.parentScreen = parentScreen; - this.backgroundTexture = backgroundTexture; - } - - @Override - protected @NotNull OwoUIAdapter createAdapter() { - return OwoUIAdapter.create(this, Containers::verticalFlow); - } - - @Override - protected final void build(FlowLayout rootComponent) { - this.rootComponent = rootComponent; - - // Apply background - rootComponent.surface(TextureSurfaces.stretched(backgroundTexture, 1920, 1082)); - rootComponent.padding(Insets.of(8)); - - // Build header - rootComponent.child(createHeader()); - - // Build main content - rootComponent.child(createMainContent()); - } - - /** - * Create the screen header with title and navigation - */ - protected FlowLayout createHeader() { - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.fixed(50)) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - // Logo - header.child(createLogo()); - - // Title - header.child(createTitleLabel()); - - // Spacer - header.child(Containers.horizontalFlow(Sizing.expand(), Sizing.content())); - - // Navigation buttons - header.child(createHeaderActions()); - - return header; - } - - /** - * Create the logo component - */ - protected Component createLogo() { - return Components.texture( - Identifier.of(MOD_ID, "textures/gui/assets/sbe_logo.png"), - 0, 0, 40, 40, 40, 40 - ); - } - - /** - * Create the title label - override to customize - */ - protected abstract Component createTitleLabel(); - - /** - * Create header action buttons (e.g., Back, Close) - */ - protected FlowLayout createHeaderActions() { - FlowLayout actions = Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(8); - - // Add back/close button - String buttonText = parentScreen != null ? "Back" : "Close"; - actions.child(Components.button( - Text.literal(buttonText), - btn -> navigateBack() - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/previous.png"), 0, 0, 90, 57 - )).sizing(Sizing.fixed(90), Sizing.fixed(19))); - - return actions; - } - - /** - * Create the main content area - implement in subclasses - */ - protected abstract FlowLayout createMainContent(); - - /** - * Navigate back to parent screen or close - */ - protected void navigateBack() { - MinecraftClient.getInstance().setScreen(parentScreen); - } - - /** - * Show an overlay dialog - */ - protected void showOverlay(FlowLayout content, boolean closeOnClick) { - var overlay = Containers.overlay(content); - overlay.closeOnClick(closeOnClick); - overlay.surface(Surface.flat(0x80_000000)); - rootComponent.child(overlay); - } - - /** - * Close the topmost overlay - */ - protected void closeTopOverlay() { - if (!rootComponent.children().isEmpty()) { - Component lastChild = rootComponent.children().getLast(); - if (lastChild instanceof io.wispforest.owo.ui.container.OverlayContainer) { - rootComponent.removeChild(lastChild); - } - } - } - - @Override - public void close() { - navigateBack(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/components/ScreenUIComponents.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/components/ScreenUIComponents.java deleted file mode 100644 index 5ab34eb..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/components/ScreenUIComponents.java +++ /dev/null @@ -1,385 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.components; - -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Reusable UI components for PackCore screens. - * Provides factory methods for common UI patterns used across different screens. - */ -public class ScreenUIComponents { - - // ===== Panel Creation ===== - - /** - * Create a standard sidebar panel - */ - public static FlowLayout createSidebar(int widthPercent) { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(widthPercent), Sizing.expand()) - .gap(8) - .surface(TextureSurfaces.stretched( - Identifier.of(MOD_ID, "textures/gui/menu/notif_box.png"), 607, 755)) - .padding(Insets.of(12)); - } - - /** - * Create a standard info panel - */ - public static FlowLayout createInfoPanel(int widthPercent) { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(widthPercent), Sizing.expand()) - .gap(8) - .surface(TextureSurfaces.stretched( - Identifier.of(MOD_ID, "textures/gui/menu/info_box.png"), 1142, 934)) - .padding(Insets.of(14)); - } - - /** - * Create a section with title and content - */ - public static FlowLayout createSection(String title, int expandPercent) { - Sizing verticalSizing = expandPercent > 0 ? Sizing.expand(expandPercent) : Sizing.content(); - - FlowLayout section = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), verticalSizing) - .gap(4) - .surface(Surface.flat(PANEL_BACKGROUND).and(Surface.outline(ACCENT_SECONDARY))) - .padding(Insets.of(8)); - - if (title != null) { - section.child(Components.label(Text.literal(title) - .setStyle(Style.EMPTY.withBold(true))) - .color(color(TEXT_PRIMARY))); - } - - return section; - } - - // ===== Info Display ===== - - /** - * Create an info row with label and value - */ - public static FlowLayout createInfoRow(String label, String value) { - FlowLayout row = Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8); - - row.child(Components.label(Text.literal(label)) - .color(color(TEXT_SECONDARY)) - .sizing(Sizing.fixed(80), Sizing.content())); - - row.child(Components.label(Text.literal(value)) - .color(color(TEXT_PRIMARY)) - .horizontalSizing(Sizing.expand())); - - return row; - } - - /** - * Create an info box with multiple rows - */ - public static FlowLayout createInfoBox() { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .surface(Surface.flat(PANEL_BACKGROUND).and(Surface.outline(ENTRY_BORDER))) - .padding(Insets.of(8)); - } - - // ===== Status Cards ===== - - /** - * Create a status card with icon and message - */ - public static FlowLayout createStatusCard(String icon, String title, String message, - int bgColor, int borderColor) { - FlowLayout card = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .surface(Surface.flat(bgColor).and(Surface.outline(borderColor))) - .padding(Insets.of(12)); - - // Header with icon - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .verticalAlignment(VerticalAlignment.CENTER); - - if (icon != null) { - header.child(Components.label(Text.literal(icon)) - .color(color(borderColor))); - } - - header.child(Components.label(Text.literal(title).setStyle(Style.EMPTY.withBold(true))) - .color(color(TEXT_PRIMARY))); - - card.child(header); - - // Message - if (message != null) { - LabelComponent msg = (LabelComponent) Components.label(Text.literal(message)) - .color(color(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(95)); - card.child(msg); - } - - return card; - } - - /** - * Create a success status card - */ - public static FlowLayout createSuccessCard(String title, String message) { - return createStatusCard("✓", title, message, SUCCESS_BG, SUCCESS_BORDER); - } - - /** - * Create a warning status card - */ - public static FlowLayout createWarningCard(String title, String message) { - return createStatusCard("⚠", title, message, WARNING_BG, WARNING_BORDER); - } - - /** - * Create an error status card - */ - public static FlowLayout createErrorCard(String title, String message) { - return createStatusCard("✗", title, message, ERROR_BG, ERROR_BORDER); - } - - // ===== Buttons ===== - - /** - * Create a standard action button - */ - public static ButtonComponent createButton(String text, ButtonComponent.PressAction action) { - return (ButtonComponent) Components.button(Text.literal(text), action::onPress) - .renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 100, 60)) - .sizing(Sizing.fixed(100), Sizing.fixed(20)); - } - - /** - * Create a button with custom size - */ - public static ButtonComponent createButton(String text, ButtonComponent.PressAction action, - int width, int height) { - return (ButtonComponent) Components.button(Text.literal(text), action::onPress) - .renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, width, height * 3)) - .sizing(Sizing.fixed(width), Sizing.fixed(height)); - } - - /** - * Create a horizontal button row - */ - public static FlowLayout createButtonRow(ButtonComponent... buttons) { - FlowLayout row = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER); - - for (ButtonComponent button : buttons) { - row.child(button); - } - - return row; - } - - // ===== Lists & Entries ===== - - /** - * Create a selectable list entry - */ - public static FlowLayout createListEntry() { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(95), Sizing.content()) - .gap(2) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ENTRY_BORDER))) - .padding(Insets.of(6)); - } - - /** - * Apply hover effects to a list entry - */ - public static void applyHoverEffects(FlowLayout entry, Runnable onSelect) { - entry.mouseEnter().subscribe(() -> - entry.surface(Surface.flat(ENTRY_HOVER).and(Surface.outline(ACCENT_SECONDARY)))); - - entry.mouseLeave().subscribe(() -> - entry.surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ENTRY_BORDER)))); - - if (onSelect != null) { - entry.mouseDown().subscribe((click, doubled) -> { - if (click.button() == 0) { - onSelect.run(); - return true; - } - return false; - }); - entry.cursorStyle(CursorStyle.HAND); - } - } - - /** - * Apply selected state to entry - */ - public static void applySelectedState(FlowLayout entry, boolean selected) { - if (selected) { - entry.surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ACCENT_SECONDARY))); - } else { - entry.surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ENTRY_BORDER))); - } - } - - // ===== Dialogs ===== - - /** - * Create a confirmation dialog - */ - public static FlowLayout createDialog(String title, String message, int width) { - FlowLayout dialog = (FlowLayout) Containers.verticalFlow(Sizing.fixed(width), Sizing.content()) - .gap(12) - .surface(Surface.flat(PANEL_BACKGROUND).and(Surface.outline(ACCENT_SECONDARY))) - .padding(Insets.of(20)) - .positioning(Positioning.relative(50, 40)); - - // Title - dialog.child(Components.label(Text.literal(title).setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - // Message - if (message != null) { - LabelComponent msg = (LabelComponent) Components.label(Text.literal(message)) - .color(color(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(100)); - dialog.child(msg); - } - - return dialog; - } - - /** - * Create a warning dialog - */ - public static FlowLayout createWarningDialog(String title, String message, int width) { - FlowLayout dialog = (FlowLayout) Containers.verticalFlow(Sizing.fixed(width), Sizing.content()) - .gap(12) - .surface(Surface.flat(PANEL_BACKGROUND).and(Surface.outline(WARNING_BORDER))) - .padding(Insets.of(20)) - .positioning(Positioning.relative(50, 40)); - - // Header with icon - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - header.child(Components.label(Text.literal("⚠")) - .color(color(WARNING_BORDER)) - .sizing(Sizing.fixed(24), Sizing.content())); - - header.child(Components.label(Text.literal(title).setStyle(Style.EMPTY.withBold(true))) - .color(color(WARNING_BORDER))); - - dialog.child(header); - - // Message - if (message != null) { - LabelComponent msg = (LabelComponent) Components.label(Text.literal(message)) - .color(color(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(100)); - dialog.child(msg); - } - - return dialog; - } - - // ===== Scrollable Content ===== - - /** - * Create a scrollable content container - */ - public static ScrollContainer createScrollContainer(FlowLayout content) { - ScrollContainer scroll = Containers.verticalScroll( - Sizing.fill(100), - Sizing.expand(), - content - ); - scroll.scrollbar(ScrollContainer.Scrollbar.vanilla()); - scroll.scrollbarThiccness(6); - return scroll; - } - - /** - * Create a scrollable content container horizontal - */ - public static ScrollContainer createScrollBoxHorizontal(FlowLayout content) { - ScrollContainer scroll = Containers.horizontalScroll( - Sizing.fill(100), - Sizing.expand(), - content - ); - scroll.scrollbar(ScrollContainer.Scrollbar.vanilla()); - scroll.scrollbarThiccness(6); - return scroll; - } - - // ===== Empty States ===== - - /** - * Create an empty state message - */ - public static FlowLayout createEmptyState(String message) { - FlowLayout empty = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand()) - .horizontalAlignment(HorizontalAlignment.CENTER) - .verticalAlignment(VerticalAlignment.CENTER); - - empty.child(Components.label(Text.literal(message)) - .color(color(TEXT_SECONDARY))); - - return empty; - } - - // ===== Utility Functions ===== - - /** - * Format file size in human-readable format - */ - public static String formatSize(long bytes) { - if (bytes < 1024) return bytes + " B"; - if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); - return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); - } - - /** - * Format timestamp for display - */ - public static String formatTimestamp(String isoTimestamp) { - try { - return isoTimestamp.replace('T', ' ').substring(0, Math.min(isoTimestamp.length(), 19)); - } catch (Exception e) { - return isoTimestamp; - } - } - - /** - * Strip markdown formatting from text - */ - public static String stripMarkdown(String text) { - if (text == null) return ""; - - text = text.replaceAll("\\{[^}]*}", ""); // Color codes - text = text.replaceAll("\\*\\*", ""); // Bold - text = text.replaceAll("^#+\\s*", ""); // Headers - text = text.replaceAll("\\[([^]]+)]\\([^)]+\\)", "$1"); // Links - text = text.replaceAll("_", ""); // Italics - text = text.replaceAll("`", ""); // Code - - return text.trim(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/components/WizardUIComponents.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/components/WizardUIComponents.java deleted file mode 100644 index 85312e5..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/components/WizardUIComponents.java +++ /dev/null @@ -1,305 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.components; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.lavendermd.CustomLavenderCompiler; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; -import io.wispforest.lavendermd.MarkdownProcessor; -import io.wispforest.lavendermd.feature.*; -import io.wispforest.owo.ops.TextOps; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import net.minecraft.util.Identifier; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Reusable UI components for wizard pages. - * Provides factory methods for common UI patterns. - */ -public class WizardUIComponents { - - // Shared markdown processor - private static final MarkdownProcessor MARKDOWN_PROCESSOR = - new MarkdownProcessor<>( - CustomLavenderCompiler::new, - new BasicFormattingFeature(), - new ColorFeature(), - new LinkFeature(), - new ListFeature(), - new BlockQuoteFeature(), - new ImageFeature() - ); - - private static final Map MARKDOWN_CACHE = new ConcurrentHashMap<>(); - private static final ModpackInfo MODPACK_INFO = PackCore.getModpackInfo(); - - /** - * Create a standard wizard page header with title and subtitle - */ - public static FlowLayout createHeader(String titlePrefix, String subtitle) { - return createHeader(titlePrefix, subtitle, true); - } - - /** - * Create a wizard page header with optional modpack name highlight - */ - public static FlowLayout createHeader(String titlePrefix, String subtitle, boolean includeModpackName) { - FlowLayout header = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(6); - - Text titleText; - if (includeModpackName) { - titleText = TextOps.concat( - TextOps.withColor(titlePrefix + " ", TEXT_PRIMARY), - Text.literal(MODPACK_INFO.getName()) - .setStyle(Style.EMPTY.withColor(ACCENT_SECONDARY).withBold(true)) - ); - } else { - titleText = TextOps.withColor(titlePrefix, TEXT_PRIMARY); - } - - header.child(Components.label(titleText).shadow(true)); - - if (subtitle != null && !subtitle.isEmpty()) { - LabelComponent subtitleLabel = (LabelComponent) Components.label( - Text.literal(subtitle) - .setStyle(Style.EMPTY.withColor(Formatting.GRAY).withItalic(true)) - ).color(Color.ofRgb(TEXT_SECONDARY)) - .margins(Insets.of(2, 0, 2, 0)) - .sizing(Sizing.expand(), Sizing.content()); - - header.child(subtitleLabel); - } - - return header; - } - - /** - * Create a selection card for options (used in multiple wizard pages) - */ - public static FlowLayout createSelectionCard(boolean isSelected, Consumer onClick) { - int borderColor = isSelected ? SUCCESS_BORDER : 0x40_FFFFFF; - - FlowLayout card = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .surface(Surface.outline(borderColor)) - .padding(Insets.of(12)) - .cursorStyle(CursorStyle.HAND); - - if (onClick != null) { - card.mouseDown().subscribe((click, doubled) -> { - onClick.accept(card); - return true; - }); - } - - return card; - } - - /** - * Create a scrollable markdown section - */ - public static ScrollContainer createMarkdownScroll(String content) { - FlowLayout wrapper = Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(4); - - var markdownUIComponent = MARKDOWN_CACHE.computeIfAbsent( - content, - MARKDOWN_PROCESSOR::process - ).horizontalSizing(Sizing.fill(98)); - - wrapper.child(markdownUIComponent); - - ScrollContainer scrollContainer = Containers.verticalScroll( - Sizing.fill(100), - Sizing.expand(), - wrapper - ); - - scrollContainer.scrollbar(ScrollContainer.Scrollbar.vanilla()); - scrollContainer.scrollbarThiccness(6); - scrollContainer.surface(Surface.flat(0x40_000000).and(Surface.outline(0x30_FFFFFF))); - scrollContainer.padding(Insets.of(8)); - - return scrollContainer; - } - - /** - * Create a status label with icon and color - */ - public static LabelComponent createStatusLabel(String text, String icon, int color) { - String displayText = icon != null ? icon + " " + text : text; - return (LabelComponent) Components.label( - Text.literal(displayText).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(color)) - .horizontalSizing(Sizing.fill(100)); - } - - /** - * Create an info card with title and description - */ - public static FlowLayout createInfoCard(String title, String description, int bgColor, int borderColor) { - FlowLayout card = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .surface(Surface.flat(bgColor).and(Surface.outline(borderColor))) - .padding(Insets.of(8)) - .margins(Insets.of(6,0,6,6)); - - if (title != null) { - card.child(Components.label( - Text.literal(title).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(borderColor))); - } - - if (description != null) { - LabelComponent desc = (LabelComponent) Components.label(Text.literal(description)) - .color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(95)); - card.child(desc); - } - - return card; - } - - /** - * Create an option box with icon, title, and description - */ - public static FlowLayout createOptionBox(String icon, String title, String description, - boolean isSelected, Consumer onClick) { - FlowLayout card = createSelectionCard(isSelected, onClick); - - // Header with icon and title - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - if (icon != null) { - header.child(Components.label(Text.literal(icon)) - .color(Color.ofRgb(ACCENT_SECONDARY))); - } - - header.child(Components.label( - Text.literal(title).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY)) - .horizontalSizing(Sizing.expand())); - - card.child(header); - - if (description != null) { - LabelComponent desc = (LabelComponent) Components.label(Text.literal(description)) - .color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(100)); - card.child(desc); - } - - return card; - } - - /** - * Create a progress step label with status icon - */ - public static LabelComponent createProgressStepLabel(String stepName, ProgressStatus status) { - String icon = switch (status) { - case SUCCESS -> "✅"; - case ERROR -> "❌"; - case RUNNING -> "⏳"; - case PENDING -> "⏸"; - }; - - Formatting color = switch (status) { - case SUCCESS -> Formatting.GREEN; - case ERROR -> Formatting.RED; - case RUNNING -> Formatting.YELLOW; - case PENDING -> Formatting.GRAY; - }; - - return (LabelComponent) Components.label( - Text.literal(icon + " " + stepName) - .setStyle(Style.EMPTY.withColor(color)) - ).margins(Insets.left(8)); - } - - /** - * Create a summary item for configuration display - */ - public static FlowLayout createSummaryItem(String label, String value) { - FlowLayout item = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .margins(Insets.vertical(2)); - - item.child(Components.label(Text.literal(label)) - .color(Color.ofRgb(TEXT_PRIMARY))); - - if (value != null) { - LabelComponent valueLabel = (LabelComponent) Components.label(Text.literal(value)) - .color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.expand()); - - item.child(valueLabel); - } - - return item; - } - - /** - * Create an image selection box with click handling - */ - public static FlowLayout createImageOption(String title, String texturePath, int textureWidth, - int textureHeight, boolean isSelected, Runnable onClick) { - FlowLayout wrapper = (FlowLayout) Containers.verticalFlow(Sizing.fill(32), Sizing.expand()) - .verticalAlignment(VerticalAlignment.CENTER) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.of(8)) - .cursorStyle(CursorStyle.HAND); - - // Title - wrapper.child(Components.label( - TextOps.withColor(title, TEXT_PRIMARY).setStyle(Style.EMPTY.withBold(true)) - ).margins(Insets.of(4, 4, 2, 4))); - - // Image container - Identifier textureId = Identifier.of(MOD_ID, texturePath); - Surface imageSurface = isSelected - ? Surface.outline(SUCCESS_BORDER).and(Surface.tiled(textureId, textureWidth, textureHeight)) - : Surface.tiled(textureId, textureWidth, textureHeight); - - FlowLayout imageContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand()) - .verticalAlignment(VerticalAlignment.CENTER) - .horizontalAlignment(HorizontalAlignment.CENTER) - .surface(imageSurface) - .margins(Insets.of(4)) - .cursorStyle(CursorStyle.HAND); - - if (onClick != null) { - imageContainer.mouseDown().subscribe((click, doubled) -> { - onClick.run(); - return true; - }); - } - - wrapper.child(imageContainer); - return wrapper; - } - - /** - * Progress status enum for step indicators - */ - public enum ProgressStatus { - SUCCESS, - ERROR, - RUNNING, - PENDING - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/BackupManagementScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/BackupManagementScreen.java deleted file mode 100644 index 27fd44a..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/BackupManagementScreen.java +++ /dev/null @@ -1,665 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.configmanager; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.apply.ConfigApplyService; -import com.github.kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.notification.BackupNotifications; -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.component.TextBoxComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; - -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.PackCore.getModpackInfo; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Backup management screen - refactored for improved code clarity. - * Handles backup creation, restoration, and deletion with async operations. - */ -public class BackupManagementScreen extends BasePackCoreScreen { - - private BackupManager.BackupInfo selectedBackup = null; - private FlowLayout infoPanel; - private FlowLayout sidebarContent; - private final Map entryComponents = new HashMap<>(); - - // Progress tracking - private FlowLayout progressDialog = null; - private LabelComponent progressLabel = null; - private volatile boolean operationInBackground = false; - private volatile String currentOperationName = ""; - private volatile boolean isRestoreOperation = false; - - public BackupManagementScreen() { - super(new ConfigManagerScreen()); - } - - @Override - protected Component createTitleLabel() { - return Components.label( - Text.literal("Backup Manager - " + getModpackInfo().getName()) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(color(TEXT_PRIMARY)); - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(8); - - mainContent.child(createSidebar()); - mainContent.child(createInfoPanel()); - - return mainContent; - } - - // ===== Sidebar ===== - - private FlowLayout createSidebar() { - FlowLayout sidebar = ScreenUIComponents.createSidebar(35); - - // Info text - sidebar.child(createInfoText()); - - // Backup sections container - sidebarContent = Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(8); - - sidebar.child(ScreenUIComponents.createScrollContainer(sidebarContent)); - - // Action buttons - sidebar.child(createSidebarButtons()); - - // Load backups - rebuildSidebarContent(); - - return sidebar; - } - - private Component createInfoText() { - int guiScale = MinecraftClient.getInstance().options.getGuiScale().getValue(); - int padding = guiScale <= 2 ? 16 : 8; - - FlowLayout container = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .padding(Insets.of(padding, 0, padding, 0)); - - LabelComponent infoLabel = (LabelComponent) Components.label( - Text.literal("Manage your configuration backups. Auto backups are created before applying new configs.") - ).color(color(TEXT_PRIMARY)) - .sizing(Sizing.fill(95), Sizing.content()); - - container.child(infoLabel); - return container; - } - - private void rebuildSidebarContent() { - sidebarContent.clearChildren(); - entryComponents.clear(); - - // Show loading - sidebarContent.child(ScreenUIComponents.createEmptyState("Loading backups...")); - - // Load backups asynchronously - BackupManager.getBackupsAsync().thenAccept(allBackups -> - MinecraftClient.getInstance().execute(() -> { - sidebarContent.clearChildren(); - - List manualBackups = allBackups.stream() - .filter(b -> b.type() == BackupManager.BackupType.MANUAL) - .toList(); - - List autoBackups = allBackups.stream() - .filter(b -> b.type() == BackupManager.BackupType.AUTO) - .toList(); - - sidebarContent.child(createBackupSection("Manual Backups", manualBackups, true)); - sidebarContent.child(createBackupSection("Auto Backups", autoBackups, false)); - }) - ); - } - - private FlowLayout createBackupSection(String title, List backups, - boolean isManual) { - FlowLayout section = ScreenUIComponents.createSection(title, isManual ? 45 : 50); - section.horizontalSizing(Sizing.fill(98)); - - FlowLayout listContent = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(2); - - if (backups.isEmpty()) { - listContent.child(Components.label(Text.literal("No backups found")) - .color(color(TEXT_SECONDARY))); - } else { - for (BackupManager.BackupInfo backup : backups) { - listContent.child(createBackupEntry(backup)); - } - } - - section.child(ScreenUIComponents.createScrollContainer(listContent)); - return section; - } - - private FlowLayout createBackupEntry(BackupManager.BackupInfo backup) { - FlowLayout entry = ScreenUIComponents.createListEntry(); - - // Display title - String displayTitle = backup.title() != null && !backup.title().isEmpty() - ? backup.title() - : backup.configName(); - - entry.child(Components.label(Text.literal(displayTitle)) - .color(color(TEXT_PRIMARY))); - - // Badges - FlowLayout badges = Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - badges.child(Components.label(Text.literal(backup.type().getDisplayName())) - .color(color(backup.type() == BackupManager.BackupType.MANUAL - ? SUCCESS_BORDER - : WARNING_BORDER))); - - badges.child(Components.label(Text.literal("v" + backup.configVersion())) - .color(color(TEXT_SECONDARY))); - - entry.child(badges); - - // Store reference - entryComponents.put(backup, entry); - - // Apply hover and selection - ScreenUIComponents.applyHoverEffects(entry, () -> selectBackup(backup)); - - return entry; - } - - private FlowLayout createSidebarButtons() { - FlowLayout buttonRow = (FlowLayout) Containers.ltrTextFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .horizontalAlignment(HorizontalAlignment.CENTER); - - buttonRow.child(ScreenUIComponents.createButton("Create Backup", - btn -> showCreateBackupDialog(), 90, 19) - .margins(Insets.bottom(4))); - - buttonRow.child(ScreenUIComponents.createButton("Open Folder", - btn -> BackupManager.openBackupsFolder(), 90, 19) - .margins(Insets.bottom(4))); - - buttonRow.child(ScreenUIComponents.createButton("Refresh", - btn -> refreshBackupsList(), 90, 19) - .margins(Insets.bottom(4))); - - return buttonRow; - } - - // ===== Info Panel ===== - - private FlowLayout createInfoPanel() { - infoPanel = ScreenUIComponents.createInfoPanel(65); - showEmptyState(); - return infoPanel; - } - - private void showEmptyState() { - infoPanel.clearChildren(); - infoPanel.child(ScreenUIComponents.createEmptyState( - "Select a backup to view details")); - } - - private void selectBackup(BackupManager.BackupInfo backup) { - // Reset previous selection - if (selectedBackup != null && entryComponents.containsKey(selectedBackup)) { - ScreenUIComponents.applySelectedState(entryComponents.get(selectedBackup), false); - } - - // Set new selection - selectedBackup = backup; - if (entryComponents.containsKey(backup)) { - ScreenUIComponents.applySelectedState(entryComponents.get(backup), true); - } - - showBackupDetails(); - } - - private void showBackupDetails() { - if (selectedBackup == null) return; - - infoPanel.clearChildren(); - infoPanel.horizontalAlignment(HorizontalAlignment.LEFT); - infoPanel.verticalAlignment(VerticalAlignment.TOP); - - int padding = MinecraftClient.getInstance().options.getGuiScale().getValue() <= 2 ? 6 : 0; - - // Header - String headerText = selectedBackup.title() != null && !selectedBackup.title().isEmpty() - ? selectedBackup.title() - : selectedBackup.configName(); - - infoPanel.child(Components.label(Text.literal(headerText) - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY)) - .margins(Insets.of(padding, 0, 0, 0))); - - // Info box - FlowLayout infoBox = ScreenUIComponents.createInfoBox(); - infoBox.child(ScreenUIComponents.createInfoRow("Type:", selectedBackup.type().getDisplayName())); - infoBox.child(ScreenUIComponents.createInfoRow("Config:", selectedBackup.configName())); - infoBox.child(ScreenUIComponents.createInfoRow("Version:", selectedBackup.configVersion())); - infoBox.child(ScreenUIComponents.createInfoRow("Created:", - ScreenUIComponents.formatTimestamp(selectedBackup.timestamp()))); - infoBox.child(ScreenUIComponents.createInfoRow("Size:", - ScreenUIComponents.formatSize(selectedBackup.sizeBytes()))); - infoBox.child(ScreenUIComponents.createInfoRow("Backup ID:", selectedBackup.backupId())); - - infoPanel.child(infoBox); - - // Description - if (selectedBackup.description() != null && !selectedBackup.description().trim().isEmpty()) { - infoPanel.child(Components.label(Text.literal("Description:") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - infoPanel.child(Components.label(Text.literal(selectedBackup.description())) - .color(color(TEXT_PRIMARY)) - .sizing(Sizing.fill(95), Sizing.content())); - } - - // Warning box - infoPanel.child(ScreenUIComponents.createWarningCard( - "Restore Information", - "Restoring will replace current files. An auto-backup will be created first." - )); - - // Action buttons - infoPanel.child(createActionButtons()); - } - - private FlowLayout createActionButtons() { - FlowLayout buttonPanel = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER); - - // Full restore - buttonPanel.child(ScreenUIComponents.createButton("Restore Full Backup", - btn -> showRestoreConfirmation(), 120, 20)); - - // Selective restore (NEW) - buttonPanel.child(ScreenUIComponents.createButton("Restore Specific Files", - btn -> MinecraftClient.getInstance().setScreen( - new SelectiveFileApplicationScreen(selectedBackup, this)), 120, 20)); - - // Delete (if manual) - if (selectedBackup.type() == BackupManager.BackupType.MANUAL) { - buttonPanel.child(ScreenUIComponents.createButton("Delete", - btn -> showDeleteConfirmation(), 90, 20)); - } - - return buttonPanel; - } - - // ===== Backup Operations ===== - - private void showCreateBackupDialog() { - FlowLayout dialog = ScreenUIComponents.createDialog( - "Create Manual Backup", - null, - 450 - ); - - dialog.child(Components.label(Text.literal("Title:*")) - .color(color(TEXT_PRIMARY))); - - TextBoxComponent titleField = Components.textBox(Sizing.fill(95), ""); - titleField.setPlaceholder(Text.literal("Enter backup title")); - dialog.child(titleField); - - dialog.child(Components.label(Text.literal("Description (optional):")) - .color(color(TEXT_PRIMARY))); - - TextBoxComponent descriptionField = Components.textBox(Sizing.fill(95), ""); - descriptionField.setPlaceholder(Text.literal("Additional details about this backup")); - dialog.child(descriptionField); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Create", btn -> { - String title = titleField.getText().trim(); - String description = descriptionField.getText().trim(); - closeTopOverlay(); - performCreateBackup( - title.isEmpty() ? null : title, - description.isEmpty() ? null : description - ); - }), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay()) - )); - - showOverlay(dialog, false); - } - - private void performCreateBackup(String title, String description) { - String finalTitle = title != null ? title : "Manual backup - " + - java.time.LocalDateTime.now().format( - java.time.format.DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm")); - - currentOperationName = finalTitle; - operationInBackground = false; - isRestoreOperation = false; - - showBackupWarningDialog(finalTitle, description); - } - - private void showBackupWarningDialog(String title, String description) { - FlowLayout dialog = ScreenUIComponents.createWarningDialog( - "Backup Notice", - null, - 400 - ); - - FlowLayout warningText = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - warningText.child(Components.label(Text.literal("⚠ Important Notice:")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(4))); - - warningText.child(Components.label(Text.literal("• The backup will run in the background")) - .color(color(TEXT_PRIMARY))); - - warningText.child(Components.label(Text.literal("• A progress indicator will show the status")) - .color(color(TEXT_PRIMARY))); - - warningText.child(Components.label(Text.literal("• You can continue using the interface")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(8))); - - dialog.child(warningText); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 80, 20), - ScreenUIComponents.createButton("Continue", btn -> { - closeTopOverlay(); - executeBackupCreation(title, description); - }, 120, 20) - )); - - showOverlay(dialog, false); - } - - private void executeBackupCreation(String title, String description) { - operationInBackground = false; - showProgressDialog("Creating Backup", "Preparing backup..."); - - BackupManager.createManualBackupAsync(title, description, this::updateProgress) - .thenAccept(backupPath -> MinecraftClient.getInstance().execute(() -> { - closeProgressDialog(); - refreshBackupsList(); - - BackupNotifications.notifyBackupComplete( - currentOperationName, backupPath, false); - - // Auto-open folder if still on screen - if (MinecraftClient.getInstance().currentScreen == this) { - try { - Util.getOperatingSystem().open(backupPath.getParent().toFile()); - } catch (Exception e) { - PackCore.LOGGER.warn("Failed to auto-open backup folder", e); - } - } - })) - .exceptionally(throwable -> { - MinecraftClient.getInstance().execute(() -> { - closeProgressDialog(); - PackCore.LOGGER.error("Failed to create backup", throwable); - showErrorDialog("Backup failed: " + throwable.getMessage()); - }); - return null; - }); - } - - private void showRestoreConfirmation() { - if (selectedBackup == null) return; - - FlowLayout dialog = ScreenUIComponents.createWarningDialog( - "Restore Backup?", - null, - 500 - ); - - dialog.child(Components.label(Text.literal("Backup: " + selectedBackup.getDisplayName()) - .setStyle(Style.EMPTY.withBold(true))) - .color(color(TEXT_PRIMARY))); - - FlowLayout warningBox = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(WARNING_BORDER))) - .padding(Insets.of(12)); - - warningBox.child(Components.label(Text.literal("This will:")) - .color(color(TEXT_PRIMARY))); - - warningBox.child(Components.label(Text.literal("• Replace your current configuration files")) - .color(color(TEXT_SECONDARY))); - - warningBox.child(Components.label(Text.literal("• Create an auto-backup of your current state")) - .color(color(TEXT_SECONDARY))); - - warningBox.child(Components.label(Text.literal("• Overwrite mod configs and settings")) - .color(color(TEXT_SECONDARY))); - - dialog.child(warningBox); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Restore", btn -> { - closeTopOverlay(); - showRestoreWarningDialog(); - }), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay()) - )); - - showOverlay(dialog, false); - } - - private void showRestoreWarningDialog() { - FlowLayout dialog = ScreenUIComponents.createWarningDialog( - "Restore Notice", - null, - 400 - ); - - FlowLayout warningText = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - warningText.child(Components.label(Text.literal("⚠ Game Will Close")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(4))); - - warningText.child(Components.label(Text.literal("• The game will close and restart")) - .color(color(TEXT_PRIMARY))); - - warningText.child(Components.label(Text.literal("• The backup will be applied on startup")) - .color(color(TEXT_PRIMARY))); - - warningText.child(Components.label(Text.literal("• An auto-backup will be created first")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(8))); - - dialog.child(warningText); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 80, 20), - ScreenUIComponents. createButton("Restore & Close Game", btn -> { - closeTopOverlay(); - performRestore(); - }, 150, 20) - )); - - showOverlay(dialog, false); - } - - private void performRestore() { - if (selectedBackup == null) return; - - closeTopOverlay(); - - try { - // Get the backup file path - Path gameDir = MinecraftClient.getInstance().runDirectory. toPath(); - Path backupsDir = gameDir.resolve("packcore/backups"); - Path backupZipPath = backupsDir.resolve(selectedBackup.backupId() + ".zip"); - - // Create a temporary ConfigFile wrapper for the backup - ConfigMetadata metadata = ConfigMetadata.builder() - .name(selectedBackup.configName() != null ? selectedBackup.configName() : "Restored Backup") - .version(selectedBackup.configVersion() != null ? selectedBackup.configVersion() : "1.0.0") - .description(selectedBackup.description() != null ? selectedBackup.description() : "Backup restoration") - .build(); - - ConfigFileRepository.ConfigFile backupAsConfig = new ConfigFileRepository.ConfigFile( - backupZipPath.getFileName().toString(), - backupZipPath, - false, - metadata - ); - - // Use ConfigApplyService to schedule restoration - ConfigApplyService.scheduleConfigApplication(backupAsConfig); - - if (MinecraftClient.getInstance().player != null) { - MinecraftClient.getInstance().player.sendMessage( - Text.literal("Restoring: " + selectedBackup.getDisplayName() + " - Restarting..."), - false - ); - } - - } catch (Exception e) { - PackCore.LOGGER.error("Failed to schedule backup restoration", e); - showErrorDialog("Failed to schedule restore: " + e.getMessage()); - } - } - - private void showDeleteConfirmation() { - if (selectedBackup == null) return; - - FlowLayout dialog = ScreenUIComponents.createDialog( - "Delete Backup?", - selectedBackup.getDisplayName() + "\n\nThis action cannot be undone.", - 400 - ); - - dialog.surface(Surface.flat(PANEL_BACKGROUND).and(Surface.outline(ERROR_BORDER))); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Delete", btn -> { - closeTopOverlay(); - performDelete(); - }), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay()) - )); - - showOverlay(dialog, false); - } - - private void performDelete() { - if (selectedBackup == null) return; - - if (BackupManager.deleteBackup(selectedBackup)) { - PackCore.LOGGER.info("Deleted backup: {}", selectedBackup.getDisplayName()); - selectedBackup = null; - refreshBackupsList(); - } else { - showErrorDialog("Failed to delete backup"); - } - } - - // ===== Progress & Dialogs ===== - - private void showProgressDialog(String title, String message) { - progressDialog = ScreenUIComponents.createDialog(title, null, 350); - progressDialog.positioning(Positioning.absolute( - (this.width - 350) / 2, - (this.height - 150) / 2 - )); - progressLabel = (LabelComponent) Components.label(Text.literal(message)) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(12)); - progressDialog.child(progressLabel); - - FlowLayout buttonRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER); - - ButtonComponent backgroundButton = (ButtonComponent) Components.button( - Text.literal("Continue in Background"), - btn -> { - operationInBackground = true; - closeProgressDialog(); - } - ).horizontalSizing(Sizing.content()); - - buttonRow.child(backgroundButton); - progressDialog.child(buttonRow); - - rootComponent.child(progressDialog); - } - - private void updateProgress(String message) { - MinecraftClient.getInstance().execute(() -> { - if (progressLabel != null && !operationInBackground) { - progressLabel.text(Text.literal(message)); - } - }); - } - - private void closeProgressDialog() { - if (progressDialog != null) { - rootComponent.removeChild(progressDialog); - progressDialog = null; - progressLabel = null; - } - } - - private void showErrorDialog(String message) { - FlowLayout dialog = ScreenUIComponents.createDialog("Error", message, 350); - dialog.surface(Surface.flat(DARK_PANEL_BACKGROUND).and(Surface.outline(ERROR_BORDER))); - dialog.positioning(Positioning.absolute( - (this.width - 350) / 2, - (this.height - 120) / 2 - )); - dialog.child(ScreenUIComponents.createButton("OK", - btn -> rootComponent.removeChild(dialog), 80, 20) - .horizontalSizing(Sizing.content())); - - rootComponent.child(dialog); - } - - private void refreshBackupsList() { - selectedBackup = null; - showEmptyState(); - rebuildSidebarContent(); - } - - @Override - public void close() { - super.close(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ConfigManagerScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ConfigManagerScreen.java deleted file mode 100644 index 96eaf26..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ConfigManagerScreen.java +++ /dev/null @@ -1,395 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.configmanager; - -import com.github.kd_gaming1.packcore.config.apply.ConfigApplyService; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Main configuration menu screen - refactored for improved maintainability. - * Uses ScreenUIComponents for common patterns and cleaner code structure. - */ -public class ConfigManagerScreen extends BasePackCoreScreen { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - private ConfigFileRepository.ConfigFile selectedConfig = null; - private FlowLayout infoPanel; - private final Map entryComponents = new HashMap<>(); - - public ConfigManagerScreen() { - super(null); // No parent - closes to main menu - } - - @Override - protected Component createTitleLabel() { - return Components.label( - Text.literal("Configuration Manager") - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(color(TEXT_PRIMARY)); - } - - @Override - protected FlowLayout createHeaderActions() { - FlowLayout actions = Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(8); - - // Current config display - ConfigMetadata currentMeta = ConfigFileRepository.getCurrentConfig(); - FlowLayout currentConfigInfo = (FlowLayout) Containers.verticalFlow(Sizing.content(), Sizing.content()) - .gap(2) - .horizontalAlignment(HorizontalAlignment.RIGHT) - .margins(Insets.right(8)); - - currentConfigInfo.child(Components.label( - Text.literal("Active: " + currentMeta.getName()).setStyle(Style.EMPTY.withBold(true)) - ).color(color(ACCENT_SECONDARY))); - - currentConfigInfo.child(Components.label( - Text.literal("v" + currentMeta.getVersion() + " | " + currentMeta.getTargetResolution()) - ).color(color(TEXT_SECONDARY))); - - actions.child(currentConfigInfo); - - // Close button - actions.child(ScreenUIComponents.createButton("Close", - btn -> navigateBack(), 90, 19)); - - return actions; - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(8); - - mainContent.child(createSidebar()); - mainContent.child(createInfoPanel()); - - return mainContent; - } - - // ===== Sidebar ===== - - private FlowLayout createSidebar() { - FlowLayout sidebar = ScreenUIComponents.createSidebar(35); - - // Info text - sidebar.child(createInfoText()); - - // Config sections - sidebar.child(createConfigSection("Official Configs", - ConfigFileRepository.getOfficialConfigs(), true)); - sidebar.child(createConfigSection("Custom Configs", - ConfigFileRepository.getCustomConfigs(), false)); - - // Action buttons - sidebar.child(createSidebarButtons()); - - return sidebar; - } - - private FlowLayout createInfoText() { - int guiScale = MinecraftClient.getInstance().options.getGuiScale().getValue(); - int padding = guiScale <= 2 ? 16 : 8; - - FlowLayout infoContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .padding(Insets.of(padding, 0, padding, 0)); - - LabelComponent infoLabel = (LabelComponent) Components.label( - Text.literal("Manage your modpack configurations. Select a config to view details or apply it.") - ).color(color(TEXT_PRIMARY)) - .sizing(Sizing.fill(95), Sizing.content()); - - infoContainer.child(infoLabel); - return infoContainer; - } - - private FlowLayout createConfigSection(String title, List configs, - boolean isOfficial) { - FlowLayout section = ScreenUIComponents.createSection(title, isOfficial ? 45 : 50); - - // List content - FlowLayout listContent = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(2); - - if (configs.isEmpty()) { - listContent.child(Components.label(Text.literal("No configs found")) - .color(color(TEXT_SECONDARY))); - } else { - for (ConfigFileRepository.ConfigFile config : configs) { - listContent.child(createConfigEntry(config)); - } - } - - // Wrap in scroll container - section.child(ScreenUIComponents.createScrollContainer(listContent)); - - return section; - } - - private FlowLayout createConfigEntry(ConfigFileRepository.ConfigFile config) { - FlowLayout entry = ScreenUIComponents.createListEntry(); - - // Extract display name - String version = "v" + config.metadata().getVersion(); - String configName = config.getDisplayName().endsWith(version) - ? config.getDisplayName().replaceAll(version, "") - : config.getDisplayName(); - - // Name - entry.child(Components.label(Text.literal(configName)) - .color(color(TEXT_PRIMARY))); - - // Badges - FlowLayout badges = Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - badges.child(Components.label(Text.literal(config.official() ? "Official" : "Custom")) - .color(color(config.official() ? SUCCESS_BORDER : WARNING_BORDER))); - - badges.child(Components.label(Text.literal(version)) - .color(color(TEXT_SECONDARY))); - - entry.child(badges); - - // Store reference - entryComponents.put(config, entry); - - // Apply hover and selection - ScreenUIComponents.applyHoverEffects(entry, () -> selectConfig(config)); - - return entry; - } - - private FlowLayout createSidebarButtons() { - FlowLayout buttonRow = (FlowLayout) Containers.ltrTextFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .horizontalAlignment(HorizontalAlignment.CENTER); - - buttonRow.child(ScreenUIComponents.createButton("Import", - btn -> MinecraftClient.getInstance().setScreen(new ImportConfigScreen()), 90, 19) - .margins(Insets.bottom(4))); - - buttonRow.child(ScreenUIComponents.createButton("Export", - btn -> MinecraftClient.getInstance().setScreen(new ExportConfigScreen()), 90, 19) - .margins(Insets.bottom(4))); - - buttonRow.child(ScreenUIComponents.createButton("Backup", - btn -> MinecraftClient.getInstance().setScreen(new BackupManagementScreen()), 90, 19) - .margins(Insets.bottom(4))); - - return buttonRow; - } - - // ===== Info Panel ===== - - private FlowLayout createInfoPanel() { - infoPanel = ScreenUIComponents.createInfoPanel(65); - showEmptyState(); - return infoPanel; - } - - private void showEmptyState() { - infoPanel.clearChildren(); - infoPanel.child(ScreenUIComponents.createEmptyState( - "Select a configuration to view details")); - } - - private void selectConfig(ConfigFileRepository.ConfigFile config) { - // Reset previous selection - if (selectedConfig != null && entryComponents.containsKey(selectedConfig)) { - ScreenUIComponents.applySelectedState(entryComponents.get(selectedConfig), false); - } - - // Set new selection - selectedConfig = config; - if (entryComponents.containsKey(config)) { - ScreenUIComponents.applySelectedState(entryComponents.get(config), true); - } - - showConfigDetails(); - } - - private void showConfigDetails() { - if (selectedConfig == null) return; - - infoPanel.clearChildren(); - infoPanel.horizontalAlignment(HorizontalAlignment.LEFT); - infoPanel.verticalAlignment(VerticalAlignment.TOP); - - ConfigMetadata meta = selectedConfig.metadata(); - int padding = MinecraftClient.getInstance().options.getGuiScale().getValue() <= 2 ? 6 : 0; - - // Create scrollable content container - FlowLayout scrollableContent = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8); - - // Header - scrollableContent.child(Components.label(Text.literal(meta.getName()) - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY)) - .margins(Insets.of(padding, 0, 0, 0))); - - // Info box - FlowLayout infoBox = ScreenUIComponents.createInfoBox(); - infoBox.child(ScreenUIComponents.createInfoRow("Version:", meta.getVersion())); - infoBox.child(ScreenUIComponents.createInfoRow("Author:", meta.getAuthor())); - infoBox.child(ScreenUIComponents.createInfoRow("Resolution:", meta.getTargetResolution())); - infoBox.child(ScreenUIComponents.createInfoRow("Source:", meta.getSource())); - - if (meta.getCreatedDate() != null && !meta.getCreatedDate().isEmpty()) { - infoBox.child(ScreenUIComponents.createInfoRow("Created:", - ScreenUIComponents.formatTimestamp(meta.getCreatedDate()))); - } - - scrollableContent.child(infoBox); - - // Description - if (meta.getDescription() != null && !meta.getDescription().isEmpty()) { - scrollableContent.child(Components.label(Text.literal("Description:") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - scrollableContent.child(Components.label(Text.literal(meta.getDescription())) - .color(color(TEXT_PRIMARY)) - .sizing(Sizing.fill(94), Sizing.content())); - } - - // Mods list - scrollableContent.child(createModsList(meta)); - - // Add scrollable content to panel - infoPanel.child(ScreenUIComponents.createScrollContainer(scrollableContent) - .sizing(Sizing.fill(96), Sizing.expand())); - - // Action buttons (outside scroll, always visible at bottom) - infoPanel.child(createActionButtons()); - } - - private Component createModsList(ConfigMetadata meta) { - if (meta.getMods() == null || meta.getMods().isEmpty()) { - return Containers.verticalFlow(Sizing.content(), Sizing.content()); - } - - FlowLayout container = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - container.child(Components.label(Text.literal("Included Mods:") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - FlowLayout modsContainer = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(2); - - int displayCount = Math.min(15, meta.getMods().size()); - for (int i = 0; i < displayCount; i++) { - modsContainer.child(Components.label(Text.literal("• " + meta.getMods().get(i))) - .color(color(TEXT_PRIMARY))); - } - - if (meta.getMods().size() > displayCount) { - modsContainer.child(Components.label( - Text.literal("... and " + (meta.getMods().size() - displayCount) + " more") - ).color(color(TEXT_SECONDARY))); - } - - container.child(ScreenUIComponents.createScrollContainer(modsContainer) - .sizing(Sizing.fill(100), Sizing.fixed(150))); - - return container; - } - - private FlowLayout createActionButtons() { - FlowLayout buttonPanel = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.top(12)); - - // Full config - buttonPanel.child(ScreenUIComponents.createButton("Apply Full Config", - btn -> showConfirmationDialog(), 120, 20)); - - // Selective config (NEW) - buttonPanel.child(ScreenUIComponents.createButton("Apply Specific Files", - btn -> MinecraftClient.getInstance().setScreen( - new SelectiveFileApplicationScreen(selectedConfig, this)), 120, 20)); - - // Delete (if custom) - if (!selectedConfig.official()) { - buttonPanel.child(ScreenUIComponents.createButton("Delete", - btn -> deleteConfig(), 90, 20)); - } - - return buttonPanel; - } - - // ===== Actions ===== - - private void showConfirmationDialog() { - if (selectedConfig == null) return; - - FlowLayout dialog = ScreenUIComponents.createDialog( - "Apply Configuration?", - "This will close the game, and when the game is open again, apply:\"\n" + selectedConfig.getDisplayName() + - "\n\n⚠ Current configurations will be backed up automatically.", - 400 - ); - - FlowLayout buttons = ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Apply", btn -> applyConfig(), 90, 20), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 90, 20) - ); - - dialog.child(buttons); - showOverlay(dialog, false); - } - - private void applyConfig() { - if (selectedConfig == null) return; - - closeTopOverlay(); - - try { - ConfigApplyService.scheduleConfigApplication(selectedConfig); - - if (MinecraftClient.getInstance().player != null) { - MinecraftClient.getInstance().player.sendMessage( - Text.literal("Applying: " + selectedConfig.getDisplayName() + " - Restarting..."), - false - ); - } - } catch (Exception e) { - LOGGER.error("Failed to apply config", e); - } - } - - private void deleteConfig() { - if (selectedConfig == null || selectedConfig.official()) return; - - if (ConfigFileRepository.deleteConfig(selectedConfig)) { - selectedConfig = null; - // Rebuild the screen - build(this.uiAdapter.rootComponent); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ExportConfigScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ExportConfigScreen.java deleted file mode 100644 index 6eb8072..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ExportConfigScreen.java +++ /dev/null @@ -1,890 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.configmanager; - -import com.github.kd_gaming1.packcore.config.export.ConfigExportService; -import com.github.kd_gaming1.packcore.config.export.ConfigExportService.ExportRequest; -import com.github.kd_gaming1.packcore.config.export.ConfigExportService.PresetType; -import com.github.kd_gaming1.packcore.notification.ExportNotifications; -import com.github.kd_gaming1.packcore.ui.component.PlaceholderTextArea; -import com.github.kd_gaming1.packcore.ui.component.tree.FileTreeNode; -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import io.wispforest.owo.ui.component.*; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.stream.Collectors; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.PackCore.getModpackInfo; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Export configuration screen - refactored for improved code clarity. - * Handles file selection, metadata input, and async export operations. - */ -public class ExportConfigScreen extends BasePackCoreScreen { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - private static final String DEFAULT_VERSION = "1.0.0"; - - private final ScheduledExecutorService asyncExecutor; - private final ConfigExportService exportManager; - - // State - private final Set selectedPaths = ConcurrentHashMap.newKeySet(); - private final Map modsToInclude = new LinkedHashMap<>(); - private FileTreeNode rootNode; - - // UI Components - private FlowLayout treeContainer; - private FlowLayout contentPanel; - private LabelComponent selectionInfoLabel; - private ButtonComponent nextButton; - - // Metadata form - private TextBoxComponent nameField; - private TextAreaComponent descriptionArea; - private TextBoxComponent versionField; - private TextBoxComponent authorField; - private ButtonComponent resolutionButton; - private FlowLayout modsListContainer; - - private boolean showingMetadata = false; - private String selectedResolution; - private String currentResolution; - - // Tree caching - private final Map nodeRowCache = new ConcurrentHashMap<>(); - private final Map nodeCheckboxCache = new ConcurrentHashMap<>(); - private volatile boolean isLoading = false; - - // Progress tracking - private FlowLayout exportProgressDialog; - private LabelComponent exportProgressLabel; - private volatile boolean exportInBackground = false; - private volatile String currentExportName = ""; - - public ExportConfigScreen() { - super(new ConfigManagerScreen()); - - asyncExecutor = Executors.newScheduledThreadPool(2); - exportManager = new ConfigExportService(); - detectCurrentResolution(); - - // Load initial tree asynchronously - CompletableFuture.runAsync(() -> { - rootNode = exportManager.buildFileTree(); - scanMods(); - }, asyncExecutor).thenRun(() -> MinecraftClient.getInstance().execute(() -> { - if (treeContainer != null) { - displayInitialTree(); - } - })); - } - - @Override - protected Component createTitleLabel() { - return Components.label( - Text.literal("Export Configuration - " + getModpackInfo().getName()) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(color(TEXT_PRIMARY)); - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(8); - - mainContent.child(createSidebar()); - mainContent.child(createContentArea()); - - return mainContent; - } - - // ===== Sidebar ===== - - private FlowLayout createSidebar() { - FlowLayout sidebar = ScreenUIComponents.createSidebar(35); - - FlowLayout scrollContent = Containers.verticalFlow(Sizing.fill(96), Sizing.content()) - .gap(8); - - scrollContent.child(createInfoSection()); - scrollContent.child(createPresetSection()); - scrollContent.child(createSelectionInfo()); - - sidebar.child(ScreenUIComponents.createScrollContainer(scrollContent)); - - // Next button - nextButton = ScreenUIComponents.createButton("Next: Add Details", - btn -> showMetadataView(), 120, 20); - nextButton.active(false); - - sidebar.child(nextButton).horizontalAlignment(HorizontalAlignment.CENTER); - - return sidebar; - } - - private Component createInfoSection() { - FlowLayout section = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .padding(Insets.of(8)); - - section.child(Components.label( - Text.literal("Select files and folders to include in your configuration export.") - ).color(color(TEXT_PRIMARY)).horizontalSizing(Sizing.fill(100))); - - return section; - } - - private FlowLayout createPresetSection() { - FlowLayout section = ScreenUIComponents.createSection("Quick Presets", 0); - section.horizontalAlignment(HorizontalAlignment.CENTER); - - for (PresetType preset : PresetType.values()) { - section.child(ScreenUIComponents.createButton(preset.getDisplayName(), - btn -> applyPreset(preset), 90, 19)); - } - - return section; - } - - private FlowLayout createSelectionInfo() { - FlowLayout section = ScreenUIComponents.createSection("Selection Info", 0); - section.horizontalAlignment(HorizontalAlignment.CENTER); - - selectionInfoLabel = Components.label(Text.literal("0 items selected\nSize: 0 KB")) - .color(color(TEXT_SECONDARY)); - section.child(selectionInfoLabel); - - return section; - } - - // ===== Content Area ===== - - private FlowLayout createContentArea() { - contentPanel = ScreenUIComponents.createInfoPanel(65); - showFileTreeView(); - return contentPanel; - } - - private void showFileTreeView() { - contentPanel.clearChildren(); - showingMetadata = false; - updateNextButton(); - - contentPanel.child(Components.label(Text.literal("Select Files to Export") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - treeContainer = Containers.verticalFlow(Sizing.fill(98), Sizing.content()); - - ScrollContainer treeScrollContainer = ScreenUIComponents.createScrollContainer(treeContainer); - contentPanel.child(treeScrollContainer); - - if (rootNode == null) { - treeContainer.child(ScreenUIComponents.createEmptyState("Loading files...")); - } else { - displayInitialTree(); - } - } - - private void displayInitialTree() { - if (treeContainer == null || rootNode == null) return; - - treeContainer.clearChildren(); - nodeRowCache.clear(); - nodeCheckboxCache.clear(); - - for (FileTreeNode child : rootNode.getChildren()) { - addTreeNodeOptimized(child, 0); - } - } - - private void addTreeNodeOptimized(FileTreeNode node, int depth) { - if (node.isHidden() || depth > 10) return; - - FlowLayout nodeRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .padding(Insets.left(depth * 16)) - .verticalAlignment(VerticalAlignment.CENTER); - - // Expand/collapse button for directories - if (node.isDirectory() && (!node.getChildren().isEmpty() || node.hasUnloadedChildren())) { - nodeRow.child(Components.button( - Text.literal(node.isExpanded() ? "▼" : "▶"), - btn -> toggleNodeExpansion(node) - ).renderer(ButtonComponent.Renderer.flat(ENTRY_BACKGROUND, ACCENT_SECONDARY, ENTRY_BORDER)) - .sizing(Sizing.fixed(16), Sizing.fixed(16))); - } else { - BoxComponent placeholder = Components.box(Sizing.fixed(16), Sizing.fixed(16)); - placeholder.fill(true); - placeholder.color(Color.ofArgb(0x00000000)); - nodeRow.child(placeholder); - } - - // Checkbox - boolean isSelected = selectedPaths.contains(node.getPath()); - CheckboxComponent checkbox = Components.checkbox(Text.empty()) - .checked(isSelected) - .onChanged(checked -> toggleSelectionAsync(node, checked)); - nodeRow.child(checkbox); - nodeCheckboxCache.put(node, checkbox); - - // Label - String icon = node.isDirectory() ? "📁" : "📄"; - nodeRow.child(Components.label(Text.literal(icon + " " + node.getName())) - .color(color(isSelected ? ACCENT_SECONDARY : TEXT_PRIMARY))); - - nodeRowCache.put(node, nodeRow); - treeContainer.child(nodeRow); - - // Add expanded children - if (node.isDirectory() && node.isExpanded() && node.isChildrenLoaded()) { - for (FileTreeNode child : node.getChildren()) { - addTreeNodeOptimized(child, depth + 1); - } - } - } - - private void toggleNodeExpansion(FileTreeNode node) { - if (isLoading) return; - - if (!node.isExpanded() && !node.isChildrenLoaded()) { - isLoading = true; - showLoadingForNode(node); - - CompletableFuture.runAsync(() -> exportManager.loadNodeChildren(node), asyncExecutor) - .thenRun(() -> MinecraftClient.getInstance().execute(() -> { - node.setExpanded(true); - updateNodeExpansion(node); - isLoading = false; - })); - } else { - node.setExpanded(!node.isExpanded()); - updateNodeExpansion(node); - } - } - - private void showLoadingForNode(FileTreeNode node) { - FlowLayout nodeRow = nodeRowCache.get(node); - if (nodeRow != null && !nodeRow.children().isEmpty()) { - Component firstChild = nodeRow.children().getFirst(); - if (firstChild instanceof ButtonComponent btn) { - btn.setMessage(Text.literal("⏳")); - } - } - } - - private void updateNodeExpansion(FileTreeNode node) { - int nodeIndex = -1; - for (int i = 0; i < treeContainer.children().size(); i++) { - if (treeContainer.children().get(i) == nodeRowCache.get(node)) { - nodeIndex = i; - break; - } - } - - if (nodeIndex == -1) return; - - FlowLayout nodeRow = nodeRowCache.get(node); - if (nodeRow != null && !nodeRow.children().isEmpty()) { - Component firstChild = nodeRow.children().getFirst(); - if (firstChild instanceof ButtonComponent btn) { - btn.setMessage(Text.literal(node.isExpanded() ? "▼" : "▶")); - } - } - - if (node.isExpanded()) { - boolean parentSelected = selectedPaths.contains(node.getPath()); - - int depth = calculateDepth(node); - int insertIndex = nodeIndex + 1; - for (FileTreeNode child : node.getChildren()) { - if (parentSelected) { - selectedPaths.add(child.getPath()); - if (child.isDirectory()) { - addDescendantsAsync(child); - } - } - - if (!nodeRowCache.containsKey(child)) { - createNodeRow(child, depth + 1, insertIndex++); - } - } - - if (parentSelected) { - updateSelectionInfo(); - } - } else { - removeChildrenFromTree(node); - } - } - - private void createNodeRow(FileTreeNode node, int depth, int insertIndex) { - if (node.isHidden()) return; - - FlowLayout nodeRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .padding(Insets.left(depth * 16)) - .verticalAlignment(VerticalAlignment.CENTER); - - // Expand/collapse button - if (node.isDirectory() && (!node.getChildren().isEmpty() || node.hasUnloadedChildren())) { - nodeRow.child(Components.button( - Text.literal(node.isExpanded() ? "▼" : "▶"), - btn -> toggleNodeExpansion(node) - ).renderer(ButtonComponent.Renderer.flat(ENTRY_BACKGROUND, ACCENT_SECONDARY, ENTRY_BORDER)) - .sizing(Sizing.fixed(16), Sizing.fixed(16))); - } else { - BoxComponent placeholder = Components.box(Sizing.fixed(16), Sizing.fixed(16)); - placeholder.fill(true); - placeholder.color(Color.ofArgb(0x00000000)); - nodeRow.child(placeholder); - } - - // Checkbox - boolean isSelected = selectedPaths.contains(node.getPath()); - CheckboxComponent checkbox = Components.checkbox(Text.empty()) - .checked(isSelected) - .onChanged(checked -> toggleSelectionAsync(node, checked)); - nodeRow.child(checkbox); - nodeCheckboxCache.put(node, checkbox); - - // Label - String icon = node.isDirectory() ? "📁" : "📄"; - nodeRow.child(Components.label(Text.literal(icon + " " + node.getName())) - .color(color(isSelected ? ACCENT_SECONDARY : TEXT_PRIMARY))); - - nodeRowCache.put(node, nodeRow); - - // Insert at specific index - List children = new ArrayList<>(treeContainer.children()); - children.add(Math.min(insertIndex, children.size()), nodeRow); - treeContainer.clearChildren(); - children.forEach(treeContainer::child); - - // Recursively add expanded children - if (node.isDirectory() && node.isExpanded() && node.isChildrenLoaded()) { - int childIndex = insertIndex + 1; - for (FileTreeNode child : node.getChildren()) { - createNodeRow(child, depth + 1, childIndex++); - } - } - } - - private void removeChildrenFromTree(FileTreeNode node) { - for (FileTreeNode child : node.getChildren()) { - FlowLayout childRow = nodeRowCache.remove(child); - nodeCheckboxCache.remove(child); - if (childRow != null) { - treeContainer.removeChild(childRow); - } - if (child.isDirectory()) { - removeChildrenFromTree(child); - } - } - } - - private int calculateDepth(FileTreeNode node) { - FlowLayout nodeRow = nodeRowCache.get(node); - if (nodeRow == null) return 0; - Insets padding = nodeRow.padding().get(); - return padding.left() / 16; - } - - private void toggleSelectionAsync(FileTreeNode node, boolean selected) { - CompletableFuture.runAsync(() -> { - if (selected) { - selectedPaths.add(node.getPath()); - if (node.isDirectory()) { - if (!node.isChildrenLoaded()) { - exportManager.loadNodeChildren(node); - } - addDescendantsAsync(node); - } - } else { - selectedPaths.remove(node.getPath()); - if (node.isDirectory()) { - if (!node.isChildrenLoaded()) { - exportManager.loadNodeChildren(node); - } - removeDescendantsAsync(node); - } - } - }, asyncExecutor).thenRun(() -> MinecraftClient.getInstance().execute(() -> { - updateNodeVisualsRecursive(node); - updateSelectionInfo(); - })); - } - - private void addDescendantsAsync(FileTreeNode node) { - for (FileTreeNode child : node.getChildren()) { - selectedPaths.add(child.getPath()); - if (child.isDirectory()) { - addDescendantsAsync(child); - } - } - } - - private void removeDescendantsAsync(FileTreeNode node) { - for (FileTreeNode child : node.getChildren()) { - selectedPaths.remove(child.getPath()); - if (child.isDirectory()) { - removeDescendantsAsync(child); - } - } - } - - private void updateNodeVisualsRecursive(FileTreeNode node) { - CheckboxComponent checkbox = nodeCheckboxCache.get(node); - if (checkbox != null) { - checkbox.checked(selectedPaths.contains(node.getPath())); - } - - if (node.isDirectory() && node.isExpanded()) { - for (FileTreeNode child : node.getChildren()) { - updateNodeVisualsRecursive(child); - } - } - } - - private void updateSelectionInfo() { - CompletableFuture.supplyAsync(() -> { - int count = selectedPaths.size(); - long size = exportManager.calculateSelectionSize(selectedPaths); - return new SelectionInfo(count, size); - }, asyncExecutor).thenAccept(info -> MinecraftClient.getInstance().execute(() -> { - String sizeText = ScreenUIComponents.formatSize(info.size); - selectionInfoLabel.text(Text.literal( - info.count + " item" + (info.count != 1 ? "s" : "") + " selected\nSize: " + sizeText)); - updateNextButton(); - })); - } - - private record SelectionInfo(int count, long size) {} - - private void updateNextButton() { - nextButton.active(!selectedPaths.isEmpty() && !showingMetadata); - if (showingMetadata) { - nextButton.setMessage(Text.literal("Currently editing...")); - } else { - nextButton.setMessage(Text.literal("Next: Add Details")); - } - } - - private void applyPreset(PresetType preset) { - CompletableFuture.supplyAsync(() -> { - Set presetPaths = exportManager.getPresetPaths(preset); - selectedPaths.clear(); - - for (Path path : presetPaths) { - FileTreeNode node = findNodeByPath(rootNode, path); - if (node != null) { - selectedPaths.add(node.getPath()); - if (node.isDirectory()) { - addDescendantsAsync(node); - } - } - } - return presetPaths; - }, asyncExecutor).thenAccept(paths -> MinecraftClient.getInstance().execute(() -> { - nodeCheckboxCache.forEach((node, checkbox) -> - checkbox.checked(selectedPaths.contains(node.getPath()))); - updateSelectionInfo(); - })); - } - - private FileTreeNode findNodeByPath(FileTreeNode currentNode, Path targetPath) { - if (currentNode.getPath().equals(targetPath)) { - return currentNode; - } - - for (FileTreeNode child : currentNode.getChildren()) { - FileTreeNode result = findNodeByPath(child, targetPath); - if (result != null) { - return result; - } - } - - return null; - } - - private void scanMods() { - List mods = exportManager.scanInstalledMods(); - modsToInclude.clear(); - for (String mod : mods) { - modsToInclude.put(mod, true); - } - } - - // ===== Metadata View ===== - - private void showMetadataView() { - contentPanel.clearChildren(); - showingMetadata = true; - updateNextButton(); - - contentPanel.child(Components.label(Text.literal("Configuration Details") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - FlowLayout formContainer = Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(8); - - // Name field - nameField = Components.textBox(Sizing.fill(70), ""); - nameField.setPlaceholder(Text.literal("Enter configuration name")); - formContainer.child(createFormRow("Name*:", nameField)); - - // Description field - descriptionArea = PlaceholderTextArea.create( - Sizing.fill(70), - Sizing.fixed(80), - Text.literal("Describe what this configuration does...") - ); - formContainer.child(createFormRow("Description:", descriptionArea)); - - // Version field - versionField = Components.textBox(Sizing.fixed(120), DEFAULT_VERSION); - formContainer.child(createFormRow("Version:", versionField)); - - // Author field - authorField = Components.textBox(Sizing.fill(70), - MinecraftClient.getInstance().getSession().getUsername()); - formContainer.child(createFormRow("Author:", authorField)); - - // Resolution dropdown - populateResolutionDropdown(); - formContainer.child(createFormRow("Target Resolution:", resolutionButton)); - - // Mods list - formContainer.child(Components.label(Text.literal("Installed mods when the configs was exported:")) - .color(color(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(100))); - - FlowLayout modsListWrapper = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.fixed(125)) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ENTRY_BORDER))) - .padding(Insets.of(8)); - - modsListContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .padding(Insets.bottom(8)); - - modsListWrapper.child(ScreenUIComponents.createScrollContainer(modsListContainer) - .sizing(Sizing.fill(100), Sizing.fixed(120))); - - formContainer.child(modsListWrapper); - populateModsList(); - - contentPanel.child(ScreenUIComponents.createScrollContainer(formContainer)); - - // Action buttons - FlowLayout buttonRow = ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Back", btn -> showFileTreeView(), 90, 20), - ScreenUIComponents.createButton("Export", btn -> showExportWarningDialog()) - ); - buttonRow.margins(Insets.top(6)); - contentPanel.child(buttonRow); - } - - private FlowLayout createFormRow(String label, Component field) { - FlowLayout row = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - row.child(Components.label(Text.literal(label)) - .color(color(TEXT_PRIMARY)) - .sizing(Sizing.fixed(60), Sizing.content())); - - row.child(field); - - return row; - } - - private void populateResolutionDropdown() { - resolutionButton = ScreenUIComponents.createButton(currentResolution, - btn -> openResolutionDropdown(), 120, 20); - selectedResolution = currentResolution; - } - - private void openResolutionDropdown() { - List commonResolutions = List.of( - "1280×720", "1920×1080", "1920×1200", - "2560×1440", "2560×1080", "3440×1440", "3840×2160", - currentResolution - ); - - List uniqueResolutions = commonResolutions.stream() - .distinct() - .toList(); - - DropdownComponent.openContextMenu( - this, - this.uiAdapter.rootComponent, - FlowLayout::child, - resolutionButton.x(), - resolutionButton.y() + resolutionButton.height(), - dropdown -> { - for (String resolution : uniqueResolutions) { - dropdown.button(Text.literal(resolution), selectedDropdown -> { - selectedResolution = resolution; - currentResolution = resolution; - resolutionButton.setMessage(Text.literal(resolution)); - assert selectedDropdown.parent() != null; - selectedDropdown.parent().removeChild(selectedDropdown); - }); - } - - dropdown.divider(); - dropdown.button(Text.literal("Custom..."), selectedDropdown -> { - openCustomResolutionDialog(); - assert selectedDropdown.parent() != null; - selectedDropdown.parent().removeChild(selectedDropdown); - }); - } - ); - } - private void openCustomResolutionDialog() { - FlowLayout dialog = ScreenUIComponents.createDialog( - "Enter Custom Resolution", - null, - 300 - ); - - TextBoxComponent customResolutionField = Components.textBox(Sizing.fixed(200), "1920x1080"); - customResolutionField.setPlaceholder(Text.literal("Width x Height (e.g. 1920x1080)")); - dialog.child(customResolutionField); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 60, 20), - ScreenUIComponents.createButton("OK", btn -> { - String customRes = customResolutionField.getText().trim(); - if (customRes.matches("\\d+x\\d+")) { - selectedResolution = customRes; - currentResolution = customRes; - resolutionButton.setMessage(Text.literal(customRes)); - } - closeTopOverlay(); - }, 60, 20) - )); - - showOverlay(dialog, false); - } - - private void populateModsList() { - modsListContainer.clearChildren(); - - if (modsToInclude.isEmpty()) { - modsListContainer.child(Components.label(Text.literal("No mods found")) - .color(color(TEXT_SECONDARY))); - return; - } - - for (Map.Entry entry : modsToInclude.entrySet()) { - CheckboxComponent checkbox = Components.checkbox(Text.literal(entry.getKey())) - .checked(entry.getValue()) - .onChanged(checked -> modsToInclude.put(entry.getKey(), checked)); - modsListContainer.child(checkbox); - } - } - - private void detectCurrentResolution() { - MinecraftClient mc = MinecraftClient.getInstance(); - currentResolution = mc.getWindow().getWidth() + "x" + mc.getWindow().getHeight(); - } - - // ===== Export Operations ===== - - private void showExportWarningDialog() { - FlowLayout dialog = ScreenUIComponents.createWarningDialog( - "Export Warning", - null, - 400 - ); - - FlowLayout warningText = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - warningText.child(Components.label(Text.literal("⚠ Important Notice:")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(4))); - - warningText.child(Components.label(Text.literal("• The export will run in the background")) - .color(color(TEXT_PRIMARY))); - - warningText.child(Components.label(Text.literal("• A progress indicator will show the status")) - .color(color(TEXT_PRIMARY))); - - warningText.child(Components.label(Text.literal("• You can continue using the interface")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(8))); - - dialog.child(warningText); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 80, 20), - ScreenUIComponents.createButton("Continue Export", btn -> { - closeTopOverlay(); - performAsyncExport(); - }, 120, 20) - )); - - showOverlay(dialog, false); - } - - private void performAsyncExport() { - String name = nameField.getText().trim(); - if (name.isEmpty()) { - showErrorDialog("Configuration name is required!"); - return; - } - - currentExportName = name; - exportInBackground = false; - showExportProgressDialog(); - - List includedMods = modsToInclude.entrySet().stream() - .filter(Map.Entry::getValue) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - ExportRequest request = new ExportRequest( - new HashSet<>(selectedPaths), - name, - descriptionArea.getText().trim(), - versionField.getText().trim(), - authorField.getText().trim(), - selectedResolution, - includedMods - ); - - CompletableFuture.runAsync(() -> { - try { - updateExportProgress("Copying files..."); - Path exportedPath = exportManager.exportConfigAsync(request, this::updateExportProgress); - - MinecraftClient.getInstance().execute(() -> { - closeExportProgressDialog(); - - ExportNotifications.notifyExportComplete(currentExportName, exportedPath); - - if (MinecraftClient.getInstance().currentScreen == this) { - try { - Util.getOperatingSystem().open(exportedPath.getParent().toFile()); - } catch (Exception e) { - LOGGER.warn("Failed to auto-open export folder", e); - } - } - - if (!exportInBackground) { - shutdownExecutor(); - MinecraftClient.getInstance().setScreen(new ConfigManagerScreen()); - } - }); - } catch (Exception e) { - LOGGER.error("Failed to export configuration", e); - MinecraftClient.getInstance().execute(() -> { - closeExportProgressDialog(); - showErrorDialog("Export failed: " + e.getMessage()); - }); - } - }, asyncExecutor); - } - - private void showExportProgressDialog() { - exportProgressDialog = ScreenUIComponents.createDialog("Exporting Configuration", null, 350); - exportProgressDialog.positioning(Positioning.absolute( - (this.width - 350) / 2, - (this.height - 150) / 2 - )); - - exportProgressLabel = (LabelComponent) Components.label(Text.literal("Preparing export...")) - .color(color(TEXT_PRIMARY)) - .margins(Insets.bottom(12)); - exportProgressDialog.child(exportProgressLabel); - - FlowLayout buttonRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER); - - ButtonComponent backgroundButton = (ButtonComponent) Components.button( - Text.literal("Continue in Background"), - btn -> { - exportInBackground = true; - closeExportProgressDialog(); - } - ).horizontalSizing(Sizing.content()); - - buttonRow.child(backgroundButton); - exportProgressDialog.child(buttonRow); - - rootComponent.child(exportProgressDialog); - } - - private void updateExportProgress(String message) { - MinecraftClient.getInstance().execute(() -> { - if (exportProgressLabel != null && !exportInBackground) { - exportProgressLabel.text(Text.literal(message)); - } - }); - } - - private void closeExportProgressDialog() { - MinecraftClient.getInstance().execute(() -> { - if (exportProgressDialog != null) { - rootComponent.removeChild(exportProgressDialog); - exportProgressDialog = null; - exportProgressLabel = null; - } - }); - } - - private void showErrorDialog(String message) { - FlowLayout dialog = ScreenUIComponents.createDialog("Error", message, 350); - dialog.surface(Surface.flat(DARK_PANEL_BACKGROUND).and(Surface.outline(ERROR_BORDER))); - dialog.positioning(Positioning.absolute( - (this.width - 350) / 2, - (this.height - 120) / 2 - )); - - dialog.child(ScreenUIComponents.createButton("OK", - btn -> rootComponent.removeChild(dialog), 80, 20) - .horizontalSizing(Sizing.content())); - - rootComponent.child(dialog); - } - - private void shutdownExecutor() { - if (asyncExecutor != null && !asyncExecutor.isShutdown()) { - asyncExecutor.shutdown(); - try { - if (!asyncExecutor.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS)) { - asyncExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - asyncExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } - - @Override - public void close() { - shutdownExecutor(); - super.close(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ImportConfigScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ImportConfigScreen.java deleted file mode 100644 index 3a1bd79..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/ImportConfigScreen.java +++ /dev/null @@ -1,600 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.configmanager; - -import com.github.kd_gaming1.packcore.config.imports.ConfigImportService; -import com.github.kd_gaming1.packcore.config.imports.ConfigImportService.ImportableFile; -import com.github.kd_gaming1.packcore.config.model.ConfigMetadata; -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import com.github.kd_gaming1.packcore.util.task.ProgressListener; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.CheckboxComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.PackCore.getModpackInfo; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Configuration import screen - folder-based approach. - * Users place files in the imports folder, then select them from this UI. - */ -public class ImportConfigScreen extends BasePackCoreScreen { - - private ImportableFile selectedFile = null; - private List availableFiles = List.of(); - private final Map entryComponents = new HashMap<>(); - - // UI Components - private LabelComponent statusLabel; - private FlowLayout fileListContainer; - private FlowLayout previewContainer; - private CheckboxComponent applyImmediatelyCheckbox; - private CheckboxComponent deleteAfterImportCheckbox; - private ButtonComponent importButton; - private FlowLayout progressPanel; - - public ImportConfigScreen() { - super(new ConfigManagerScreen()); - } - - @Override - protected Component createTitleLabel() { - return Components.label( - Text.literal("Import Configuration - " + getModpackInfo().getName()) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(color(TEXT_PRIMARY)); - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(8); - - mainContent.child(createSidebar()); - mainContent.child(createPreviewPanel()); - - // Load files on screen open - refreshFileList(); - - return mainContent; - } - - // ===== Sidebar ===== - - private FlowLayout createSidebar() { - // Create the base sidebar with styling - FlowLayout sidebar = (FlowLayout) ScreenUIComponents.createSidebar(40) - .padding(Insets.of(22, 18, 14, 14)); - - // Create scrollable content container - FlowLayout sidebarContent = Containers.verticalFlow(Sizing.fill(96), Sizing.content()) - .gap(8); - - // Add all sections to the content - sidebarContent.child(createInstructionsSection()); - sidebarContent.child(createFileListSection()); - sidebarContent.child(createImportOptionsSection()); - - // Wrap content in scroll container and add to sidebar - sidebar.child(ScreenUIComponents.createScrollContainer(sidebarContent)); - - return sidebar; - } - - - - - private FlowLayout createInstructionsSection() { - FlowLayout section = ScreenUIComponents.createSection("How to Import", 0); - - // Instructions - LabelComponent instructions = (LabelComponent) Components.label( - Text.literal(""" - 1. Click 'Open Imports Folder' - 2. Place your config .zip file there - 3. Click 'Refresh' to see it - 4. Select and import""") - ).color(color(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(95)); - - section.child(instructions); - - // Button row - FlowLayout buttonRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .horizontalAlignment(HorizontalAlignment.CENTER); - - buttonRow.child(ScreenUIComponents.createButton("Open Folder", - btn -> openImportsFolder(), 90, 20)); - - buttonRow.child(ScreenUIComponents.createButton("Refresh", - btn -> refreshFileList(), 90, 20)); - - section.child(buttonRow); - - // Folder path display - LabelComponent pathLabel = (LabelComponent) Components.label( - Text.literal("Path: " + ConfigImportService.getImportsFolderPath()) - ).color(color(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(95)); - - section.child(pathLabel); - - return section; - } - - private FlowLayout createFileListSection() { - FlowLayout section = ScreenUIComponents.createSection("Available Files", 60); - - fileListContainer = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(2); - - section.child(ScreenUIComponents.createScrollContainer(fileListContainer)); - - return section; - } - - private FlowLayout createImportOptionsSection() { - FlowLayout section = ScreenUIComponents.createSection("Import Options", 0); - - // Status - statusLabel = Components.label(Text.literal("Select a file to import")) - .color(color(TEXT_SECONDARY)); - section.child(statusLabel); - - // Progress - progressPanel = Containers.verticalFlow(Sizing.fill(100), Sizing.content()); - section.child(progressPanel); - - // Options - applyImmediatelyCheckbox = Components.checkbox(Text.literal("Apply Immediately")); - applyImmediatelyCheckbox.checked(false); - applyImmediatelyCheckbox.tooltip(Text.literal( - "If checked, the game will restart and apply this configuration")); - section.child(applyImmediatelyCheckbox); - - deleteAfterImportCheckbox = Components.checkbox(Text.literal("Delete After Import")); - deleteAfterImportCheckbox.checked(true); - deleteAfterImportCheckbox.tooltip(Text.literal( - "Remove the file from imports folder after successful import")); - section.child(deleteAfterImportCheckbox); - - // Import button - importButton = ScreenUIComponents.createButton("Import", - btn -> handleImportClick()); - importButton.active(false); - - section.child(importButton); - - return section; - } - - // ===== Preview Panel ===== - - private FlowLayout createPreviewPanel() { - previewContainer = ScreenUIComponents.createInfoPanel(60); - showEmptyPreview(); - return previewContainer; - } - - private void showEmptyPreview() { - previewContainer.clearChildren(); - previewContainer.horizontalAlignment(HorizontalAlignment.CENTER); - previewContainer.verticalAlignment(VerticalAlignment.CENTER); - - previewContainer.child(Components.label( - Text.literal("Configuration Preview").setStyle(Style.EMPTY.withBold(true)) - ).color(color(ACCENT_SECONDARY))); - - previewContainer.child(Components.label( - Text.literal("Select a file to preview its contents") - ).color(color(TEXT_SECONDARY))); - } - - // ===== File Operations ===== - - private void openImportsFolder() { - updateStatus("Opening folder...", TEXT_SECONDARY); - - ConfigImportService.openImportsFolder().thenAccept(success -> { - MinecraftClient.getInstance().execute(() -> { - if (success) { - updateStatus("Folder opened - place files there and click Refresh", SUCCESS_BORDER); - } else { - updateStatus("Could not open folder - check game directory manually", WARNING_BORDER); - } - }); - }).exceptionally(throwable -> { - MinecraftClient.getInstance().execute(() -> - updateStatus("Error: " + throwable.getMessage(), ERROR_BORDER)); - return null; - }); - } - - private void refreshFileList() { - updateStatus("Scanning for files...", TEXT_SECONDARY); - fileListContainer.clearChildren(); - entryComponents.clear(); - - // Show loading indicator - fileListContainer.child(Components.label(Text.literal("Scanning imports folder...")) - .color(color(TEXT_SECONDARY))); - - ConfigImportService.scanImportsFolder().thenAccept(files -> { - MinecraftClient.getInstance().execute(() -> { - availableFiles = files; - fileListContainer.clearChildren(); - - if (files.isEmpty()) { - fileListContainer.child(Components.label( - Text.literal("No config files found\n\nPlace .zip files in the imports folder") - ).color(color(TEXT_SECONDARY))); - updateStatus("No files found - add files and refresh", TEXT_SECONDARY); - } else { - for (ImportableFile file : files) { - fileListContainer.child(createFileEntry(file)); - } - updateStatus("Found " + files.size() + " file(s)", SUCCESS_BORDER); - } - }); - }).exceptionally(throwable -> { - MinecraftClient.getInstance().execute(() -> { - fileListContainer.clearChildren(); - fileListContainer.child(Components.label( - Text.literal("Error scanning folder: " + throwable.getMessage()) - ).color(color(ERROR_BORDER))); - updateStatus("Error scanning folder", ERROR_BORDER); - }); - return null; - }); - } - - private FlowLayout createFileEntry(ImportableFile file) { - FlowLayout entry = ScreenUIComponents.createListEntry(); - - // Main file name - String displayName = file.getDisplayName(); - if (displayName.length() > 35) { - displayName = displayName.substring(0, 32) + "..."; - } - - entry.child(Components.label(Text.literal(displayName)) - .color(color(TEXT_PRIMARY))); - - // Info row - FlowLayout infoRow = Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(6); - - // Validation status - if (file.isValid()) { - infoRow.child(Components.label(Text.literal("✓ Valid")) - .color(color(SUCCESS_BORDER))); - } else { - infoRow.child(Components.label(Text.literal("✗ Invalid")) - .color(color(ERROR_BORDER))); - } - - // File size - infoRow.child(Components.label(Text.literal(file.getFileSizeFormatted())) - .color(color(TEXT_SECONDARY))); - - entry.child(infoRow); - - // Store reference - entryComponents.put(file, entry); - - // Apply hover and selection (only for valid files) - if (file.isValid()) { - ScreenUIComponents.applyHoverEffects(entry, () -> selectFile(file)); - } else { - // Show tooltip for invalid files - entry.tooltip(Text.literal(file.validation().errorMessage())); - entry.surface(Surface.flat(ERROR_BG).and(Surface.outline(ERROR_BORDER))); - } - - return entry; - } - - private void selectFile(ImportableFile file) { - // Reset previous selection - if (selectedFile != null && entryComponents.containsKey(selectedFile)) { - ScreenUIComponents.applySelectedState(entryComponents.get(selectedFile), false); - } - - // Set new selection - selectedFile = file; - if (entryComponents.containsKey(file)) { - ScreenUIComponents.applySelectedState(entryComponents.get(file), true); - } - - showPreview(); - importButton.active(file.isValid()); - updateStatus("Ready to import: " + file.getDisplayName(), SUCCESS_BORDER); - } - - private void showPreview() { - if (selectedFile == null) { - showEmptyPreview(); - return; - } - - previewContainer.clearChildren(); - previewContainer.horizontalAlignment(HorizontalAlignment.LEFT); - previewContainer.verticalAlignment(VerticalAlignment.TOP); - - // Validation status - if (!selectedFile.isValid()) { - previewContainer.child(ScreenUIComponents.createErrorCard( - "Invalid Configuration", - selectedFile.validation().errorMessage() - )); - return; - } - - ConfigMetadata meta = selectedFile.metadata(); - if (meta == null) { - previewContainer.child(Components.label( - Text.literal("Could not read metadata") - ).color(color(ERROR_BORDER))); - return; - } - - // Header - previewContainer.child(Components.label( - Text.literal(meta.getName()).setStyle(Style.EMPTY.withBold(true)) - ).color(color(ACCENT_SECONDARY))); - - // Info box - FlowLayout infoBox = ScreenUIComponents.createInfoBox(); - infoBox.child(ScreenUIComponents.createInfoRow("File:", selectedFile.fileName())); - infoBox.child(ScreenUIComponents.createInfoRow("Size:", selectedFile.getFileSizeFormatted())); - infoBox.child(ScreenUIComponents.createInfoRow("Version:", meta.getVersion())); - infoBox.child(ScreenUIComponents.createInfoRow("Author:", meta.getAuthor())); - infoBox.child(ScreenUIComponents.createInfoRow("Resolution:", meta.getTargetResolution())); - - if (meta.getCreatedDate() != null) { - infoBox.child(ScreenUIComponents.createInfoRow("Created:", - ScreenUIComponents.formatTimestamp(meta.getCreatedDate()))); - } - - previewContainer.child(infoBox); - - // Description - if (meta.getDescription() != null && !meta.getDescription().isEmpty()) { - previewContainer.child(Components.label( - Text.literal("Description:").setStyle(Style.EMPTY.withBold(true)) - ).color(color(ACCENT_SECONDARY))); - - previewContainer.child(Components.label(Text.literal(meta.getDescription())) - .color(color(TEXT_PRIMARY)) - .sizing(Sizing.fill(95), Sizing.content())); - } - - // Mods list (if present) - if (meta.getMods() != null && !meta.getMods().isEmpty()) { - previewContainer.child(createModsList(meta.getMods())); - } - - // Delete button for this file - FlowLayout actionButtons = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.top(12)); - - actionButtons.child(ScreenUIComponents.createButton("Delete File", - btn -> confirmDeleteFile(), 100, 20)); - - previewContainer.child(actionButtons); - } - - private Component createModsList(java.util.List mods) { - FlowLayout container = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - container.child(Components.label( - Text.literal("Included Mods:").setStyle(Style.EMPTY.withBold(true)) - ).color(color(ACCENT_SECONDARY))); - - FlowLayout modsContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(2) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ENTRY_BORDER))) - .padding(Insets.of(8)); - - int displayCount = Math.min(10, mods.size()); - for (int i = 0; i < displayCount; i++) { - modsContainer.child(Components.label(Text.literal("• " + mods.get(i))) - .color(color(TEXT_PRIMARY))); - } - - if (mods.size() > displayCount) { - modsContainer.child(Components.label( - Text.literal("... and " + (mods.size() - displayCount) + " more") - ).color(color(TEXT_SECONDARY))); - } - - container.child(ScreenUIComponents.createScrollContainer(modsContainer) - .sizing(Sizing.fill(100), Sizing.fixed(150))); - - return container; - } - - // ===== Import Operations ===== - - private void handleImportClick() { - if (selectedFile == null || !selectedFile.isValid()) return; - - // Show confirmation if applying immediately - if (applyImmediatelyCheckbox.isChecked()) { - showRestartWarningDialog(); - } else { - performImport(); - } - } - - private void showRestartWarningDialog() { - FlowLayout dialog = ScreenUIComponents.createWarningDialog( - "Restart Required", - null, - 500 - ); - - // Configuration info - dialog.child(Components.label( - Text.literal("Configuration: " + selectedFile.getDisplayName()) - .setStyle(Style.EMPTY.withBold(true)) - ).color(color(TEXT_PRIMARY))); - - // Warning box - FlowLayout warningBox = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(WARNING_BORDER))) - .padding(Insets.of(12)); - - warningBox.child(Components.label(Text.literal("This will:")) - .color(color(TEXT_PRIMARY))); - warningBox.child(Components.label(Text.literal("• Import and apply the configuration")) - .color(color(TEXT_SECONDARY))); - warningBox.child(Components.label(Text.literal("• Restart Minecraft automatically")) - .color(color(TEXT_SECONDARY))); - warningBox.child(Components.label(Text.literal("• Replace your current configuration")) - .color(color(TEXT_SECONDARY))); - - dialog.child(warningBox); - - // Backup notice - dialog.child(ScreenUIComponents.createErrorCard( - "BACKUP RECOMMENDATION", - "Please export your current configuration before proceeding. " + - "If you do not export a backup, you will lose your current configuration and cannot revert!" - )); - - // Buttons - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Export First", btn -> { - closeTopOverlay(); - MinecraftClient.getInstance().setScreen(new ExportConfigScreen()); - }), - ScreenUIComponents.createButton("Import & Restart", btn -> { - closeTopOverlay(); - performImport(); - }), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay()) - )); - - showOverlay(dialog, false); - } - - private void performImport() { - if (selectedFile == null || !selectedFile.isValid()) return; - - importButton.active(false); - progressPanel.clearChildren(); - - boolean applyImmediately = applyImmediatelyCheckbox.isChecked(); - boolean deleteAfterImport = deleteAfterImportCheckbox.isChecked(); - - ConfigImportService.importConfigAndCleanup( - selectedFile.path(), - applyImmediately, - deleteAfterImport, - new ProgressListener() { - @Override - public void onProgress(String message, int percentage) { - MinecraftClient.getInstance().execute(() -> { - progressPanel.clearChildren(); - progressPanel.child(Components.label( - Text.literal(message + " (" + percentage + "%)") - ).color(color(ACCENT_SECONDARY))); - }); - } - - @Override - public void onComplete(boolean success, String message) { - MinecraftClient.getInstance().execute(() -> { - progressPanel.clearChildren(); - - if (success) { - updateStatus("Success!", SUCCESS_BORDER); - if (applyImmediately) { - progressPanel.child(Components.label( - Text.literal("Game will restart to apply configuration...") - ).color(color(SUCCESS_BORDER))); - } else { - progressPanel.child(Components.label(Text.literal(message)) - .color(color(SUCCESS_BORDER))); - - // Refresh the file list after successful import - refreshFileList(); - selectedFile = null; - showEmptyPreview(); - importButton.active(false); - } - } else { - updateStatus("Import failed", ERROR_BORDER); - progressPanel.child(Components.label(Text.literal(message)) - .color(color(ERROR_BORDER))); - importButton.active(true); - } - }); - } - } - ); - } - - private void confirmDeleteFile() { - if (selectedFile == null) return; - - FlowLayout dialog = ScreenUIComponents.createDialog( - "Delete File?", - "Are you sure you want to delete:\n" + selectedFile.fileName() + - "\n\nThis action cannot be undone.", - 400 - ); - - FlowLayout buttons = ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton("Delete", btn -> { - closeTopOverlay(); - deleteSelectedFile(); - }, 90, 20), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 90, 20) - ); - - dialog.child(buttons); - showOverlay(dialog, false); - } - - private void deleteSelectedFile() { - if (selectedFile == null) return; - - boolean deleted = ConfigImportService.deleteImportFile(selectedFile.path()); - - if (deleted) { - updateStatus("File deleted", SUCCESS_BORDER); - selectedFile = null; - showEmptyPreview(); - refreshFileList(); - } else { - updateStatus("Failed to delete file", ERROR_BORDER); - } - } - - // ===== Utility ===== - - private void updateStatus(String message, int color) { - statusLabel.text(Text.literal(message)); - statusLabel.color(color(color)); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/SelectiveFileApplicationScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/SelectiveFileApplicationScreen.java deleted file mode 100644 index 3989616..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/configmanager/SelectiveFileApplicationScreen.java +++ /dev/null @@ -1,765 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.configmanager; - -import com.github.kd_gaming1.packcore.config.apply.FileDescriptionRegistry; -import com.github.kd_gaming1.packcore.config.apply.SelectiveConfigApplyService; -import com.github.kd_gaming1.packcore.config.apply.SelectiveConfigApplyService.SelectableFile; -import com.github.kd_gaming1.packcore.config.backup.BackupManager; -import com.github.kd_gaming1.packcore.config.backup.SelectiveBackupRestoreService; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.ui.component.tree.FileTreeNode; -import com.github.kd_gaming1.packcore.ui.component.tree.FileTreeUIHelper; -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.CheckboxComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Unified screen for selectively applying/restoring files. - * Two-column layout consistent with ExportConfigScreen. - */ -public class SelectiveFileApplicationScreen extends BasePackCoreScreen { - private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - // Mode enum - public enum Mode { - CONFIG_APPLY("Apply Configuration Files"), - BACKUP_RESTORE("Restore Backup Files"); - - private final String title; - Mode(String title) { this.title = title; } - public String getTitle() { return title; } - } - - private final Mode mode; - private final ConfigFileRepository.ConfigFile sourceConfig; - private final BackupManager.BackupInfo sourceBackup; - private final Set selectedPaths = ConcurrentHashMap.newKeySet(); - - // UI Components - private FlowLayout sidebar; - private FlowLayout mainContent; - private FlowLayout fileTreeContainer; - private FlowLayout detailsPanel; - - private ButtonComponent applyButton; - private LabelComponent selectionInfoLabel; - private ScrollContainer fileTreeScrollContainer; - - // File data - private List allFiles = new ArrayList<>(); - private FileTreeNode rootNode; - private final Map treeNodeRows = new HashMap<>(); - private Path virtualRoot; // For building tree structure - - /** - * Constructor for config application mode - */ - public SelectiveFileApplicationScreen(ConfigFileRepository.ConfigFile sourceConfig, Screen parent) { - super(parent); - this.mode = Mode.CONFIG_APPLY; - this.sourceConfig = sourceConfig; - this.sourceBackup = null; - } - - /** - * Constructor for backup restoration mode - */ - public SelectiveFileApplicationScreen(BackupManager.BackupInfo sourceBackup, Screen parent) { - super(parent); - this.mode = Mode.BACKUP_RESTORE; - this.sourceConfig = null; - this.sourceBackup = sourceBackup; - } - - @Override - protected Component createTitleLabel() { - String sourceName = mode == Mode.CONFIG_APPLY - ? sourceConfig.getDisplayName() - : sourceBackup.getDisplayName(); - - return Components.label( - Text.literal(mode.getTitle() + " - " + sourceName) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(color(TEXT_PRIMARY)); - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(8); - - mainContent.child(createSidebar().padding(Insets.of(20,16,14,14))); - mainContent.child(createContentArea()); - - // Load files - loadFiles(); - - return mainContent; - } - - // ===== Sidebar (35%) ===== - - private FlowLayout createSidebar() { - sidebar = ScreenUIComponents.createSidebar(35); - - FlowLayout scrollContent = Containers.verticalFlow(Sizing.fill(96), Sizing.content()) - .gap(8); - - scrollContent.child(createInstructions()); - scrollContent.child(createQuickSelectButtons()); - scrollContent.child(createSelectionInfo()); - - sidebar.child(ScreenUIComponents.createScrollContainer(scrollContent)); - - // Apply button at bottom - applyButton = ScreenUIComponents.createButton( - mode == Mode.CONFIG_APPLY ? "Apply Selected" : "Restore Selected", - btn -> showConfirmationDialog(), - 120, 20 - ); - applyButton.active(false); - sidebar.child(applyButton).horizontalAlignment(HorizontalAlignment.CENTER); - - return sidebar; - } - - private FlowLayout createInstructions() { - FlowLayout section = ScreenUIComponents.createSection("Instructions", 0); - - String instructions = mode == Mode.CONFIG_APPLY - ? "Select specific files from the configuration to apply. " + - "Expand folders to see contents. Check items to select them." - : "Choose which files to restore from this backup. " + - "A safety backup will be created before restoration."; - - LabelComponent label = (LabelComponent) Components.label(Text.literal(instructions)) - .color(color(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(95)); - - section.child(label); - return section; - } - - private FlowLayout createQuickSelectButtons() { - FlowLayout section = ScreenUIComponents.createSection("Quick Select", 0); - section.horizontalAlignment(HorizontalAlignment.CENTER); - - FlowLayout row1 = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .horizontalAlignment(HorizontalAlignment.CENTER); - - row1.child(ScreenUIComponents.createButton("Select All", - btn -> selectAll(), 90, 19)); - row1.child(ScreenUIComponents.createButton("Clear All", - btn -> deselectAll(), 90, 19)); - - section.child(row1); - - FlowLayout row2 = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .horizontalAlignment(HorizontalAlignment.CENTER); - - row2.child(ScreenUIComponents.createButton("Configs Only", - btn -> selectByType(SelectableFile.FileType.MOD_CONFIG), 90, 19)); - row2.child(ScreenUIComponents.createButton("Game Options", - btn -> selectByType(SelectableFile.FileType.GAME_OPTIONS), 90, 19)); - - section.child(row2); - - return section; - } - - private FlowLayout createSelectionInfo() { - FlowLayout section = ScreenUIComponents.createSection("Selection", 0); - section.horizontalAlignment(HorizontalAlignment.CENTER); - - selectionInfoLabel = Components.label(Text.literal("0 files selected\n0 B")) - .color(color(TEXT_SECONDARY)); - section.child(selectionInfoLabel); - - return section; - } - - // ===== Main Content Area (65%) ===== - - private FlowLayout createContentArea() { - mainContent = ScreenUIComponents.createInfoPanel(65); - mainContent.verticalAlignment(VerticalAlignment.TOP); - - // File tree section - mainContent.child(Components.label(Text.literal("File Structure") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY)) - .margins(Insets.of(6, 4,8,4))); - - fileTreeContainer = Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(2); - - mainContent.child(ScreenUIComponents.createScrollContainer(fileTreeContainer) - .sizing(Sizing.fill(100), Sizing.expand(60))); - - // Details panel section - mainContent.child(createDetailsSection()); - - return mainContent; - } - - private FlowLayout createDetailsSection() { - FlowLayout section = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand(35)) - .gap(4) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ACCENT_SECONDARY))) - .padding(Insets.of(8)) - .margins(Insets.top(8)); - - section.child(Components.label(Text.literal("File Details") - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - detailsPanel = Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(4); - - showEmptyDetails(); - - section.child(ScreenUIComponents.createScrollContainer(detailsPanel) - .sizing(Sizing.fill(100), Sizing.expand())); - - return section; - } - - private void showEmptyDetails() { - detailsPanel.clearChildren(); - detailsPanel.child(Components.label(Text.literal("Hover over a file to see details")) - .color(color(TEXT_SECONDARY))); - } - - private void showFileDetails(SelectableFile file) { - detailsPanel.clearChildren(); - - // Get enhanced description from registry - var description = FileDescriptionRegistry.getDescription(file.path()); - String icon = description - .map(FileDescriptionRegistry.FileDescription::icon) - .orElse(FileDescriptionRegistry.getIcon(file.path())); - - // Header with icon - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - header.child(Components.label(Text.literal(icon)) - .color(color(ACCENT_SECONDARY))); - - header.child(Components.label(Text.literal(file.displayName()) - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY)) - .horizontalSizing(Sizing.expand())); - - detailsPanel.child(header); - - // Info - detailsPanel.child(ScreenUIComponents.createInfoRow("Type:", file.type().getDisplayName())); - detailsPanel.child(ScreenUIComponents.createInfoRow("Size:", - ScreenUIComponents.formatSize(file.size()))); - - // Path (truncated if too long) - String displayPath = file.path(); - if (displayPath.length() > 50) { - displayPath = "..." + displayPath.substring(displayPath.length() - 47); - } - detailsPanel.child(ScreenUIComponents.createInfoRow("Path:", displayPath)); - - // Enhanced description - if (description.isPresent()) { - var desc = description.get(); - detailsPanel.child(ScreenUIComponents.createInfoRow("Description:", desc.description())); - - if (desc.isImportant()) { - detailsPanel.child(ScreenUIComponents.createWarningCard( - "Important File", - "This file contains critical settings that will significantly affect your experience." - )); - } - } else if (file.description() != null && !file.description().isEmpty()) { - detailsPanel.child(ScreenUIComponents.createInfoRow("Description:", file.description())); - } - } - - // ===== File Loading & Tree Building ===== - - private void loadFiles() { - fileTreeContainer.clearChildren(); - fileTreeContainer.child(ScreenUIComponents.createEmptyState("Loading files...")); - - CompletableFuture> loadFuture = mode == Mode.CONFIG_APPLY - ? SelectiveConfigApplyService.scanConfigFiles(sourceConfig) - : SelectiveBackupRestoreService.scanBackupFiles(sourceBackup); - - loadFuture.thenAccept(files -> MinecraftClient.getInstance().execute(() -> { - this.allFiles = files; - buildFileTree(); - })).exceptionally(throwable -> { - MinecraftClient.getInstance().execute(() -> { - LOGGER.error("Failed to load files", throwable); - fileTreeContainer.clearChildren(); - fileTreeContainer.child(ScreenUIComponents.createErrorCard( - "Load Error", - "Failed to scan files: " + throwable.getMessage() - )); - }); - return null; - }); - } - - private void buildFileTree() { - fileTreeContainer.clearChildren(); - treeNodeRows.clear(); - - if (allFiles.isEmpty()) { - fileTreeContainer.child(ScreenUIComponents.createEmptyState("No files found")); - return; - } - - // Build tree structure from flat file list - virtualRoot = java.nio.file.Paths.get(""); - rootNode = new FileTreeNode(virtualRoot, "root", true); - rootNode.setExpanded(true); - rootNode.setChildrenLoaded(true); - - // Create a set of all directory paths for quick lookup - Set directoryPaths = new HashSet<>(); - for (SelectableFile file : allFiles) { - String path = file.path(); - String[] parts = path.split("[/\\\\]"); - - // Add all parent directories - StringBuilder currentPath = new StringBuilder(); - for (int i = 0; i < parts.length - 1; i++) { - if (currentPath.length() > 0) { - currentPath.append("/"); - } - currentPath.append(parts[i]); - directoryPaths.add(currentPath.toString()); - } - } - - // Build tree from files - for (SelectableFile file : allFiles) { - addFileToTree(file, directoryPaths); - } - - // Render root's children - for (FileTreeNode child : rootNode.getChildren()) { - addTreeNodeToUI(child, 0); - } - - updateSelectionInfo(); - } - - private void addFileToTree(SelectableFile file, Set directoryPaths) { - String[] pathParts = file.path().split("[/\\\\]"); - FileTreeNode currentNode = rootNode; - - for (int i = 0; i < pathParts.length; i++) { - String part = pathParts[i]; - - // Build the full path up to this point - StringBuilder pathBuilder = new StringBuilder(); - for (int j = 0; j <= i; j++) { - if (pathBuilder.length() > 0) { - pathBuilder.append("/"); - } - pathBuilder.append(pathParts[j]); - } - String fullPath = pathBuilder.toString(); - - // Determine if this is a directory - boolean isDirectory = directoryPaths.contains(fullPath); - - FileTreeNode childNode = findChild(currentNode, part); - if (childNode == null) { - Path nodePath = currentNode.getPath().resolve(part); - childNode = new FileTreeNode(nodePath, part, isDirectory); - childNode.setChildrenLoaded(true); - currentNode.addChild(childNode); - } - - currentNode = childNode; - } - } - - private FileTreeNode findChild(FileTreeNode parent, String name) { - for (FileTreeNode child : parent.getChildren()) { - if (child.getName().equals(name)) { - return child; - } - } - return null; - } - - /** - * Adds a single tree node to the UI and recursively adds its children if expanded. - * Used primarily during initial tree build. - */ - private void addTreeNodeToUI(FileTreeNode node, int depth) { - if (node.isHidden()) return; - - boolean isSelected = selectedPaths.contains(node.getPath().toString()); - - FlowLayout nodeRow = FileTreeUIHelper.createTreeNodeRow( - node, - depth, - isSelected, - () -> toggleNodeExpansion(node), - (n, checked) -> toggleSelection(n, checked), - this::showNodeDetails - ); - - treeNodeRows.put(node, nodeRow); - fileTreeContainer.child(nodeRow); - - if (node.isDirectory() && node.isExpanded()) { - for (FileTreeNode child : node.getChildren()) { - addTreeNodeToUI(child, depth + 1); - } - } - } - - private void showNodeDetails(FileTreeNode node) { - if (node.isDirectory()) { - int fileCount = FileTreeUIHelper.countFiles(node); - long size = FileTreeUIHelper.calculateSize(node); - - detailsPanel.clearChildren(); - detailsPanel.child(Components.label(Text.literal("📁 " + node.getName()) - .setStyle(Style.EMPTY.withBold(true))) - .color(color(ACCENT_SECONDARY))); - - detailsPanel.child(ScreenUIComponents.createInfoRow("Type:", "Directory")); - detailsPanel.child(ScreenUIComponents.createInfoRow("Files:", String.valueOf(fileCount))); - detailsPanel.child(ScreenUIComponents.createInfoRow("Total Size:", - ScreenUIComponents.formatSize(size))); - } else { - String path = node.getPath().toString(); - allFiles.stream() - .filter(f -> f.path().equals(path)) - .findFirst() - .ifPresent(this::showFileDetails); - } - } - - // ===== Tree Updates & Expansion Handling ===== - - private void toggleNodeExpansion(FileTreeNode node) { - node.setExpanded(!node.isExpanded()); - updateNodeExpansion(node); - } - - private void updateNodeExpansion(FileTreeNode node) { - FlowLayout nodeRow = treeNodeRows.get(node); - if (nodeRow == null) return; - - // Find index of this node row in the container - int nodeIndex = -1; - var children = fileTreeContainer.children(); - for (int i = 0; i < children.size(); i++) { - if (children.get(i) == nodeRow) { - nodeIndex = i; - break; - } - } - - if (nodeIndex == -1) return; - - updateExpandButton(nodeRow, node.isExpanded()); - - if (node.isExpanded()) { - int depth = calculateDepth(nodeRow); - int insertIndex = nodeIndex + 1; - for (FileTreeNode child : node.getChildren()) { - // Use createNodeRow which returns number of added rows to correctly position siblings - int rowsAdded = createNodeRow(child, depth + 1, insertIndex); - insertIndex += rowsAdded; - } - } else { - removeChildrenFromTree(node); - } - } - - private void updateExpandButton(FlowLayout row, boolean expanded) { - if (!row.children().isEmpty() && row.children().get(0) instanceof ButtonComponent btn) { - btn.setMessage(Text.literal(expanded ? "▼" : "▶")); - } - } - - private int calculateDepth(FlowLayout row) { - Insets padding = row.padding().get(); - return padding.left() / 16; - } - - private int createNodeRow(FileTreeNode node, int depth, int insertIndex) { - if (node.isHidden()) return 0; - - boolean isSelected = selectedPaths.contains(node.getPath().toString()); - - FlowLayout nodeRow = FileTreeUIHelper.createTreeNodeRow( - node, - depth, - isSelected, - () -> toggleNodeExpansion(node), - this::toggleSelection, - this::showNodeDetails - ); - - treeNodeRows.put(node, nodeRow); - - // Insert row at specific index - List children = new ArrayList<>(fileTreeContainer.children()); - if (insertIndex <= children.size()) { - children.add(insertIndex, nodeRow); - } else { - children.add(nodeRow); - } - - fileTreeContainer.clearChildren(); - children.forEach(fileTreeContainer::child); - - int rowsAdded = 1; - - // Recursively add expanded children - if (node.isDirectory() && node.isExpanded()) { - int childIndex = insertIndex + 1; - for (FileTreeNode child : node.getChildren()) { - int childRows = createNodeRow(child, depth + 1, childIndex); - childIndex += childRows; - rowsAdded += childRows; - } - } - - return rowsAdded; - } - - private void removeChildrenFromTree(FileTreeNode node) { - for (FileTreeNode child : node.getChildren()) { - FlowLayout childRow = treeNodeRows.remove(child); - if (childRow != null) { - fileTreeContainer.removeChild(childRow); - } - if (child.isDirectory()) { - removeChildrenFromTree(child); - } - } - } - - // ===== Selection Logic ===== - - private void toggleSelection(FileTreeNode node, boolean selected) { - FileTreeUIHelper.selectNodeAndChildren(node, selected, selectedPaths); - updateSelectionInfo(); - updateNodeVisualsRecursive(node); - } - - private void updateNodeVisualsRecursive(FileTreeNode node) { - FlowLayout row = treeNodeRows.get(node); - if (row != null) { - boolean isSelected = selectedPaths.contains(node.getPath().toString()); - for (Component child : row.children()) { - if (child instanceof CheckboxComponent checkbox) { - checkbox.checked(isSelected); - } else if (child instanceof LabelComponent label) { - // Heuristic to update only the text label, not the icon - if (label.text().getString().equals(node.getName())) { - label.color(color(isSelected ? ACCENT_SECONDARY : TEXT_PRIMARY)); - } - } - } - } - - if (node.isDirectory()) { - for (FileTreeNode child : node.getChildren()) { - updateNodeVisualsRecursive(child); - } - } - } - - private void updateTreeVisuals() { - for (Map.Entry entry : treeNodeRows.entrySet()) { - FileTreeNode node = entry.getKey(); - FlowLayout row = entry.getValue(); - boolean isSelected = selectedPaths.contains(node.getPath().toString()); - - for (Component child : row.children()) { - if (child instanceof CheckboxComponent checkbox) { - checkbox.checked(isSelected); - } else if (child instanceof LabelComponent label) { - if (label.text().getString().equals(node.getName())) { - label.color(color(isSelected ? ACCENT_SECONDARY : TEXT_PRIMARY)); - } - } - } - } - updateSelectionInfo(); - } - - private void selectAll() { - for (SelectableFile file : allFiles) { - selectedPaths.add(file.path()); - } - updateTreeVisuals(); - } - - private void deselectAll() { - selectedPaths.clear(); - updateTreeVisuals(); - } - - private void selectByType(SelectableFile.FileType type) { - selectedPaths.clear(); // Clear logic first - for (SelectableFile file : allFiles) { - if (file.type() == type) { - selectedPaths.add(file.path()); - } - } - updateTreeVisuals(); - } - - private void updateSelectionInfo() { - int count = selectedPaths.size(); - long totalSize = allFiles.stream() - .filter(f -> selectedPaths.contains(f.path())) - .mapToLong(SelectableFile::size) - .sum(); - - String sizeText = ScreenUIComponents.formatSize(totalSize); - selectionInfoLabel.text(Text.literal( - count + " file" + (count != 1 ? "s" : "") + " selected\n" + sizeText)); - - applyButton.active(count > 0); - } - - // ===== Application/Restoration ===== - - private void showConfirmationDialog() { - String action = mode == Mode.CONFIG_APPLY ? "Apply" : "Restore"; - String sourceName; - if (mode == Mode.CONFIG_APPLY) { - assert sourceConfig != null; - sourceName = sourceConfig.getDisplayName(); - } else { - assert sourceBackup != null; - sourceName = sourceBackup.getDisplayName(); - } - - FlowLayout dialog = ScreenUIComponents.createWarningDialog( - action + " Selected Files?", - null, - 500 - ); - - dialog.child(Components.label( - Text.literal("You have selected " + selectedPaths.size() + " file" + - (selectedPaths.size() != 1 ? "s" : "") + " to " + action.toLowerCase() + " from:") - ).color(color(TEXT_PRIMARY))); - - dialog.child(Components.label( - Text.literal(sourceName).setStyle(Style.EMPTY.withBold(true)) - ).color(color(ACCENT_SECONDARY)).margins(Insets.bottom(8))); - - dialog.child(ScreenUIComponents.createWarningCard( - "⚠ Game Will Close", - "The game will close and apply the selected files on next startup. " + - "Configuration files cannot be hot-reloaded while the game is running." - )); - - dialog.child(ScreenUIComponents.createWarningCard( - "Auto-Backup", - "An automatic backup will be created before applying changes. " + - "You can restore it later if needed." - )); - - dialog.child(ScreenUIComponents.createButtonRow( - ScreenUIComponents.createButton(action + " & Close Game", btn -> { - closeTopOverlay(); - performApplication(); - }, 140, 20), - ScreenUIComponents.createButton("Cancel", btn -> closeTopOverlay(), 90, 20) - )); - - showOverlay(dialog, false); - } - - private void performApplication() { - FlowLayout progressDialog = ScreenUIComponents.createDialog( - "Scheduling Files", - "Preparing to " + (mode == Mode.CONFIG_APPLY ? "apply" : "restore") + " files on next startup...", - 350 - ); - - showOverlay(progressDialog, false); - - CompletableFuture applyFuture = mode == Mode.CONFIG_APPLY - ? SelectiveConfigApplyService.scheduleSelectiveConfigApplication(sourceConfig, selectedPaths) - : SelectiveBackupRestoreService.scheduleSelectiveRestore(sourceBackup, selectedPaths); - - applyFuture.thenAccept(success -> MinecraftClient.getInstance().execute(() -> { - closeTopOverlay(); - if (success) { - if (MinecraftClient.getInstance().player != null) { - String action = mode == Mode.CONFIG_APPLY ? "Applying" : "Restoring"; - MinecraftClient.getInstance().player.sendMessage( - Text.literal(action + " " + selectedPaths.size() + " selected files - Restarting..."), - false - ); - } - } else { - showErrorDialog(); - } - })).exceptionally(throwable -> { - MinecraftClient.getInstance().execute(() -> { - closeTopOverlay(); - LOGGER.error("Failed to schedule files", throwable); - showErrorDialog(); - }); - return null; - }); - } - - private void showErrorDialog() { - String action = mode == Mode.CONFIG_APPLY ? "apply" : "restore"; - - FlowLayout dialog = ScreenUIComponents.createDialog( - "Error", - "Failed to " + action + " selected files. Check logs for details.", - 350 - ); - dialog.surface(Surface.flat(DARK_PANEL_BACKGROUND).and(Surface.outline(ERROR_BORDER))); - - dialog.child(ScreenUIComponents.createButton("OK", - btn -> closeTopOverlay(), - 80, 20) - .horizontalSizing(Sizing.content())); - - showOverlay(dialog, false); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/scamshield/ScamEducationScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/scamshield/ScamEducationScreen.java deleted file mode 100644 index 5ef85e5..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/scamshield/ScamEducationScreen.java +++ /dev/null @@ -1,320 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.scamshield; - -import com.github.kd_gaming1.packcore.ui.screen.base.BasePackCoreScreen; -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.util.markdown.MarkdownService; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Educational screen about Hypixel SkyBlock scams. - * Displays comprehensive information to help players identify and avoid scams. - */ -public class ScamEducationScreen extends BasePackCoreScreen { - - private String markdownContent; - - /** - * Create education screen with full content - */ - public ScamEducationScreen(Screen parentScreen, String markdownContent) { - this(parentScreen); - this.markdownContent = markdownContent; - } - - /** - * Create education screen with specific content - * @param parentScreen The parent screen to return to - */ - public ScamEducationScreen(Screen parentScreen) { - super(parentScreen); - MarkdownService markdownService = new MarkdownService(); - this.markdownContent = markdownService.getOrDefault("scam_education.md", FALLBACK_CONTENT); - } - - @Override - protected Component createTitleLabel() { - return Components.label( - Text.literal("Scam Protection Education") - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(Color.ofRgb(ACCENT_SECONDARY)) - .shadow(true) - .margins(Insets.of(0, 0, 4, 4)); - } - - @Override - protected FlowLayout createMainContent() { - FlowLayout mainContent = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand()) - .gap(8) - .padding(Insets.of(8)); - - // Warning banner at the top - mainContent.child(createWarningBanner()); - - // Quick tips section - mainContent.child(createQuickTips()); - - // Main markdown content - mainContent.child(createMarkdownContent()); - - return mainContent; - } - - /** - * Create prominent warning banner - */ - private FlowLayout createWarningBanner() { - FlowLayout banner = ScreenUIComponents.createWarningCard( - "Stay Alert!", - "Scammers are constantly finding new ways to steal your items. " + - "If something seems too good to be true, it probably is. Always verify and be carefull when interacting with others." - ); - - // Make it more prominent - banner.margins(Insets.of(4, 0, 8, 0)); - - return banner; - } - - /** - * Create quick tips cards - */ - private FlowLayout createQuickTips() { - FlowLayout tipsSection = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4); - - tipsSection.child(Components.label( - Text.literal("⚡ Quick Tips").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(ACCENT_SECONDARY))); - - FlowLayout tipsContainer = Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(8); - - // Use FlowLayout tip cards (no per-card scroll) - tipsContainer.child(createTipCard("🔒", "Protect Your Account", - "Never share your password or session ID with anyone — even if they claim to be staff.")); - tipsContainer.child(createTipCard("🌐", "Avoid Suspicious Links", - "Never click on unknown links or login pages sent by other players.")); - tipsContainer.child(createTipCard("💰", "If It’s Too Good to Be True...", - "Free coins, items, or giveaways are almost always scams.")); - tipsContainer.child(createTipCard("🤝", "Trade Safely", - "Double-check items and coins in every trade — scammers may switch items.")); - tipsContainer.child(createTipCard("🧠", "Stay Informed", - "Learn about common scams by doing '/scamshield education' to protect yourself.")); - - // Wrap the whole horizontal container in a single scroll box - var tipsScroll = ScreenUIComponents.createScrollBoxHorizontal(tipsContainer); - tipsScroll.sizing(Sizing.fill(100), Sizing.fixed(96)); - tipsSection.child(tipsScroll); - - return tipsSection; - } - - /** - * Create a single tip card - */ - private FlowLayout createTipCard(String icon, String title, String description) { - FlowLayout card = (FlowLayout) Containers.verticalFlow(Sizing.fixed(260), Sizing.content()) - .gap(4) - .surface(Surface.flat(ENTRY_BACKGROUND).and(Surface.outline(ACCENT_PRIMARY))) - .padding(Insets.of(8)) - .horizontalAlignment(HorizontalAlignment.CENTER); - - card.child(Components.label(Text.literal(icon)) - .color(Color.ofRgb(ACCENT_SECONDARY))); - - card.child(Components.label(Text.literal(title).setStyle(Style.EMPTY.withBold(true))) - .color(Color.ofRgb(TEXT_PRIMARY))); - - card.child(Components.label(Text.literal(description)) - .color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalTextAlignment(HorizontalAlignment.CENTER) - .horizontalSizing(Sizing.fill(90))); - - return card; - } - - /** - * Create the main markdown content area - */ - private ScrollContainer createMarkdownContent() { - // Add section header - FlowLayout wrapper = Containers.verticalFlow(Sizing.fill(100), Sizing.expand()) - .gap(4); - - wrapper.child(Components.label( - Text.literal("📚 Complete Scam Guide").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(ACCENT_SECONDARY))); - - // Create markdown scroll with the content - return WizardUIComponents.createMarkdownScroll(markdownContent); - } - - /** - * Get default markdown content - * You'll replace this with your actual markdown content - */ - private static final String FALLBACK_CONTENT = """ - # Hypixel SkyBlock Scam Prevention Guide - - ## Introduction - - Scamming is the act of stealing money, items, or accounts from another player through deception or trickery. - This guide will help you identify and avoid the most common scams in Hypixel SkyBlock. - - {gold}**Important:** Victims of scams will not have their items returned under any circumstances.{gold} - - --- - - ## Golden Rules - - 1. **If it seems too good to be true, it probably is** - 2. **Never give items to someone you don't trust** - 3. **Always double-check trades before accepting** - 4. **Enable API and check player stats** - 5. **Never click suspicious links** - - --- - - ## Common Scam Types - - ### 🎯 Price Manipulation - Scammers artificially inflate or deflate item prices to trick you into bad trades. - - **How to avoid:** - - Check multiple sources for prices (Bazaar, Auction House) - - Use price checking tools - - Don't trust "limited time offers" - - ### 💰 Unbalanced Trades - Offering less valuable items disguised as expensive ones. - - **How to avoid:** - - Hover over items to verify their exact name - - Check item stats and abilities - - Use a resource pack to distinguish similar items - - ### 🎁 False Rewards - Promising rare items or coins for auction bids. - - **How to avoid:** - - Never bid on items expecting "bonus rewards" - - Check if player is actually quitting (SkyCrypt) - - Remember: legitimate players just give items away - - ### 🔨 Crafting/Reforging Scams - Asking for materials to craft items but stealing them. - - **How to avoid:** - - Only trade with trusted friends - - Check if they have the required recipes (API enabled) - - Meet the crafting requirements yourself, or ask a close friend you personally know to help - - ### 🤝 Borrowing/Loaning Scams - Borrowing items and never returning them. - - **How to avoid:** - - Don't lend items to strangers - - If you must loan items, be aware that scammers may provide worthless collateral or never return items. It's recommends simply not to loan to people you don't know well. - - Better yet: meet requirements yourself - - ### 🔄 Item Switching - Showing expensive item then swapping for cheap lookalike. - - **How to avoid:** - - Always verify the exact item before accepting - - If they cancel and retry, check EXTRA carefully - - Take your time, don't rush trades - - ### 👑 Rank Selling - Claiming they can give you ranks for in-game items. - - **How to avoid:** - - Ranks can ONLY be purchased on store.hypixel.net - - Players cannot give other players ranks - - This is always 100% a scam - - ### 🏰 Dungeon Carry Scams - Not paying after carry or taking payment without carrying. - - **How to avoid:** - - Use trusted carry services from official Discord - - Check carrier's Catacombs level and gear - - Agree on payment terms before starting - - ### 🏝️ Co-op Island Stealing - Joining co-op to steal items or kick you out. - - **How to avoid:** - - NEVER add strangers to co-op - - Only co-op with people you completely trust - - Remember: /coopadd invites them to YOUR island - - ### 🔗 Phishing Links - Fake websites designed to steal your account. - - **How to avoid:** - - Never click links in Minecraft chat - - Only log in at minecraft.net and hypixel.net - - Use URL scanners if you must check a link - - Hypixel staff will NEVER DM you in-game - - --- - - ## Red Flags 🚩 - - Watch out for these warning signs: - - - Player asks you to disable API - - Deal requires multiple trades - - Player is rushing you - - "Limited time offer" - - Asking for collateral when they have recipes - - New account with expensive items - - Multiple trade cancellations - - Pressure tactics ("last chance!", "hurry!") - - --- - - ## Reporting Scammers - - If you encounter a scammer: - - 1. **Screenshot evidence** (F2 in Minecraft) - 2. **Report via** `/report [player] [reason]` - 3. **Report on forums** with evidence - 4. **Warn others** in your guild/party - - {red}**Remember:** You won't get items back, but reporting helps protect others!{red} - - --- - - ## Protection Tools - - - **SkyBlockAddons** - Has scam protection features - - **NEU (NotEnoughUpdates)** - Shows item prices - - **SkyCrypt** - Check player profiles - - **Hypixel API** - Verify player stats - - --- - - ## Stay Safe! - - The best protection is knowledge and caution. Take your time with trades, verify everything, - and trust your instincts. If something feels wrong, it probably is. - - {gold}**Remember: Your items are YOUR responsibility. Stay alert and trade smart!**{gold} - """; -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/scamshield/ScamWarningScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/scamshield/ScamWarningScreen.java deleted file mode 100644 index 39766b2..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/scamshield/ScamWarningScreen.java +++ /dev/null @@ -1,473 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.scamshield; - -import com.github.kd_gaming1.packcore.ui.screen.components.ScreenUIComponents; -import io.wispforest.owo.ui.base.BaseOwoScreen; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import org.jetbrains.annotations.NotNull; - -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Enhanced warning overlay screen for detected scam attempts. - * - * DESIGN PHILOSOPHY: - * - Minecraft-integrated aesthetic (not a harsh system alert) - * - Clear visual hierarchy guiding user attention - * - Contextual, helpful messaging instead of alarm - * - Confidence-driven UI that adapts to threat level - * - * DISPLAY LOGIC: - * - Only shown for HIGH confidence detections (95%+) - * - Lower confidence uses chat warnings only - * - Blocks interaction until user acknowledges - * - * @author KD_Gaming1 - * @version 2.0 - Refactored for better UX - */ -public class ScamWarningScreen extends BaseOwoScreen { - - private final ScamWarning warning; - private final Runnable onDismiss; - - // Animation state for visual interest - private int tickCount = 0; - - /** - * Immutable data object containing scam detection information. - * - * @param playerName The username of the suspected scammer - * @param scamType The category of scam detected - * @param detectedMessage The original suspicious message - * @param confidenceLevel Confidence score (0-100, typically 90-100 for this screen) - */ - public record ScamWarning( - String playerName, - ScamType scamType, - String detectedMessage, - int confidenceLevel - ) { - /** - * Get human-readable confidence description. - */ - public String getConfidenceText() { - if (confidenceLevel >= 95) return "Extremely High"; - if (confidenceLevel >= 90) return "Very High"; - if (confidenceLevel >= 80) return "High"; - if (confidenceLevel >= 70) return "Medium"; - if (confidenceLevel >= 50) return "Moderate"; - return "Low"; - } - - /** - * Get appropriate color for confidence level display. - */ - public int getConfidenceColor() { - if (confidenceLevel >= 90) return 0xFF3333; // Bright red for critical - if (confidenceLevel >= 70) return 0xFF9933; // Orange for high - if (confidenceLevel >= 50) return 0xFFCC33; // Yellow for medium - return TEXT_SECONDARY; - } - } - - /** - * Scam type categorization with contextual information. - * Each type has specific messaging and guidance. - */ - public enum ScamType { - PHISHING_LINK( - "Phishing Link Detected", - "🔗", - "This player is trying to steal your account credentials.", - "Never click suspicious links or enter login info on unfamiliar sites." - ), - PRICE_MANIPULATION( - "Price Manipulation", - "💰", - "This player may be manipulating item values for unfair trades.", - "Always verify market prices before making large trades." - ), - RANK_SELLING( - "Rank Selling Scam", - "👑", - "This player is likely offering fake ranks or perks.", - "Official ranks can only be purchased through legitimate channels." - ), - COOP_SCAM( - "Co-op Access Theft", - "🏝️", - "This player may steal items or delete your island if given co-op access.", - "Only add trusted friends to your island co-op." - ), - BORROWING( - "Borrowing/Loaning Scam", - "🤝", - "This player may not return borrowed items.", - "Don't loan valuable items to players you don't know well." - ), - FALSE_REWARDS( - "Fake Giveaway/Reward", - "🎁", - "This player is offering rewards that don't exist.", - "Real giveaways don't require you to send items or login elsewhere." - ), - CRAFTING( - "Crafting/Reforging Scam", - "🔨", - "This player may keep your items without completing the service.", - "Only use crafters you personally know and trust. Check their API to verify they have the required recipes unlocked." - ), - GENERAL( - "Suspicious Activity", - "⚠️", - "Multiple suspicious patterns detected in this player's messages.", - "Exercise caution when interacting with this player." - ); - - private final String displayName; - private final String icon; - private final String threatDescription; - private final String guidanceText; - - ScamType(String displayName, String icon, String threatDescription, String guidanceText) { - this.displayName = displayName; - this.icon = icon; - this.threatDescription = threatDescription; - this.guidanceText = guidanceText; - } - - public String getDisplayName() { return displayName; } - public String getIcon() { return icon; } - public String getThreatDescription() { return threatDescription; } - public String getGuidanceText() { return guidanceText; } - } - - /** - * Create a scam warning overlay screen. - * - * @param warning Immutable detection data - * @param onDismiss Callback invoked when user dismisses the warning - */ - public ScamWarningScreen(ScamWarning warning, Runnable onDismiss) { - this.warning = warning; - this.onDismiss = onDismiss; - } - - @Override - protected @NotNull OwoUIAdapter createAdapter() { - return OwoUIAdapter.create(this, Containers::verticalFlow); - } - - @Override - protected void build(FlowLayout rootUIComponent) { - rootUIComponent.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER).surface(Surface.VANILLA_TRANSLUCENT); - - FlowLayout content = createScrollableContent(); - rootUIComponent.child(ScreenUIComponents.createScrollContainer(content)); - } - - /** - * Creates scrollable content with all warning information. - * Designed to fit within a ScrollContainer for long content. - */ - private FlowLayout createScrollableContent() { - FlowLayout content = (FlowLayout) Containers.verticalFlow(Sizing.expand(), Sizing.content()) - .gap(16) - .padding(Insets.of(24)); - - // Build card sections in priority order - content.child(createWarningHeader()); - content.child(createThreatInfoPanel()); - content.child(createPlayerInfoSection()); - content.child(createDetectedMessageSection()); - content.child(createActionButtonRow()); - content.child(createProtectionTipsFooter()); - - return content; - } - - /** - * Creates an attractive card surface with proper borders. - * Uses darker Minecraft-style panel with accent border. - */ - private Surface createCardSurface() { - // Dark panel background with a warning-colored border - int borderColor = warning.confidenceLevel() >= 90 ? 0xFF4444 : WARNING_BORDER; - return Surface.flat(0xFF_1A1A1A).and(Surface.outline(borderColor)); - } - - /** - * Creates the prominent warning header section. - * Displays confidence level and general alert status. - */ - private FlowLayout createWarningHeader() { - FlowLayout header = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .padding(Insets.bottom(8)); - - // Large attention-grabbing icon with subtle pulse effect - header.child(Components.label(Text.literal("⚠")) - .color(Color.ofRgb(warning.getConfidenceColor())) - .shadow(true)); - - // Main title - direct and clear - header.child(Components.label( - Text.literal("Scam Attempt Detected").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0xFFFFFF)).shadow(true)); - - // Confidence indicator - shows detection reliability - FlowLayout confidenceBadge = createConfidenceBadge(); - header.child(confidenceBadge); - - return header; - } - - /** - * Creates a visual badge showing confidence level. - * Helps users understand detection reliability. - */ - private FlowLayout createConfidenceBadge() { - FlowLayout badge = (FlowLayout) Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(6) - .surface(Surface.flat(0xFF_2A2A2A).and(Surface.outline(warning.getConfidenceColor()))) - .padding(Insets.of(8)); - - badge.child(Components.label(Text.literal("Confidence:")) - .color(Color.ofRgb(TEXT_SECONDARY))); - - badge.child(Components.label( - Text.literal(warning.getConfidenceText()).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(warning.getConfidenceColor()))); - - badge.child(Components.label( - Text.literal("(" + warning.confidenceLevel() + "%)") - ).color(Color.ofRgb(TEXT_SECONDARY))); - - return badge; - } - - /** - * Creates the threat information panel. - * Explains WHAT this scam is and WHY it's dangerous. - */ - private FlowLayout createThreatInfoPanel() { - ScamType type = warning.scamType(); - - FlowLayout panel = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(10) - .surface(Surface.flat(0xFF_2A1A1A).and(Surface.outline(0xFF4444))) - .padding(Insets.of(16)); - - // Scam type header with icon - FlowLayout typeHeader = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - typeHeader.child(Components.label(Text.literal(type.getIcon())) - .color(Color.ofRgb(0xFF6666))); - - typeHeader.child(Components.label( - Text.literal(type.getDisplayName()).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0xFFAAAA))); - - panel.child(typeHeader); - - // Threat description - explains the danger - panel.child(Components.label(Text.literal(type.getThreatDescription())) - .color(Color.ofRgb(0xFFCCCC)) - .horizontalSizing(Sizing.fill(96))); - - // Guidance text - tells user what to do - FlowLayout guidanceBox = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .surface(Surface.flat(0xFF_1A2A1A).and(Surface.outline(0x44AA44))) - .padding(Insets.of(10)) - .margins(Insets.top(6)); - - guidanceBox.child(Components.label(Text.literal("💡")) - .color(Color.ofRgb(0x77DD77))); - - guidanceBox.child(Components.label(Text.literal(type.getGuidanceText())) - .color(Color.ofRgb(0xAAFFAA)) - .horizontalSizing(Sizing.fill(90))); - - panel.child(guidanceBox); - - return panel; - } - - /** - * Creates the player information section. - * Shows WHO triggered the detection. - */ - private FlowLayout createPlayerInfoSection() { - FlowLayout section = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .surface(Surface.flat(0xFF_1A1A2A).and(Surface.outline(0x4466AA))) - .padding(Insets.of(14)); - - // Section title - section.child(Components.label( - Text.literal("Suspected Player").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0xAABBFF))); - - // Player name in prominent display - FlowLayout playerDisplay = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(10) - .verticalAlignment(VerticalAlignment.CENTER); - - playerDisplay.child(Components.label(Text.literal("👤")) - .color(Color.ofRgb(0x6699FF))); - - playerDisplay.child(Components.label( - Text.literal(warning.playerName()).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0xFFFFFF))); - - section.child(playerDisplay); - - // Warning text - section.child(Components.label( - Text.literal("⚠ Avoid all transactions with this player until you verify their legitimacy.") - ).color(Color.ofRgb(0xFFAA66)).horizontalSizing(Sizing.fill(96))); - - return section; - } - - /** - * Creates the detected message display section. - * Shows WHAT was said that triggered detection. - */ - private FlowLayout createDetectedMessageSection() { - FlowLayout section = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .surface(Surface.flat(0xFF_2A2A1A).and(Surface.outline(0xAA9944))) - .padding(Insets.of(14)); - - // Section title - section.child(Components.label( - Text.literal("Suspicious Message").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0xFFCC77))); - - // Message content in a scrollable container if needed - String message = warning.detectedMessage(); - - // Truncate very long messages - String displayMessage = message.length() > 200 - ? message.substring(0, 197) + "..." - : message; - - section.child(Components.label( - Text.literal("\"" + displayMessage + "\"").setStyle(Style.EMPTY.withItalic(true)) - ).color(Color.ofRgb(0xEEEEEE)).horizontalSizing(Sizing.fill(96))); - - return section; - } - - /** - * Creates the action button row. - * Provides clear next steps for the user. - */ - private FlowLayout createActionButtonRow() { - FlowLayout buttonRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(10) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.vertical(8)); - - // Primary action: Learn More (education) - buttonRow.child(ScreenUIComponents.createButton("🛡 Learn More", btn -> { - MinecraftClient.getInstance().setScreen( - new ScamEducationScreen(null) - ); - }, 140, 22)); - - // Secondary action: Report Player - buttonRow.child(ScreenUIComponents.createButton("📢 Report", btn -> { - if (MinecraftClient.getInstance().player != null) { - MinecraftClient.getInstance().player.networkHandler.sendChatCommand( - "report " + warning.playerName() + " Scamming" - ); - } - dismiss(); - }, 110, 22)); - - // Tertiary action: Dismiss - buttonRow.child(ScreenUIComponents.createButton("✓ Got It", btn -> dismiss(), 110, 22)); - - return buttonRow; - } - - /** - * Creates the protection tips footer. - * Provides quick safety reminders. - */ - private FlowLayout createProtectionTipsFooter() { - FlowLayout footer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .surface(Surface.flat(0xFF_1A2A1A).and(Surface.outline(0x44AA44))) - .padding(Insets.of(12)) - .margins(Insets.top(4)); - - // Footer title - footer.child(Components.label( - Text.literal("🛡 Stay Safe").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0x77DD77))); - - // Quick tips - concise and actionable - String[] tips = { - "• Never share your login credentials with anyone", - "• Check Auction House and Bazaar prices before trading to verify fair value", - "• Only use /coopadd with people you know personally", - "• Report suspicious behavior to server staff" - }; - - for (String tip : tips) { - footer.child(Components.label(Text.literal(tip)) - .color(Color.ofRgb(0xAAFFAA))); - } - - return footer; - } - - /** - * Dismiss the warning and return control to the player. - * Invokes the provided callback if present. - */ - private void dismiss() { - if (onDismiss != null) { - onDismiss.run(); - } - this.close(); - } - - @Override - public boolean shouldPause() { - // Don't pause the game - this is an overlay, not a full pause screen - return false; - } - - @Override - public boolean shouldCloseOnEsc() { - // Allow ESC to dismiss - user has read the warning - return true; - } - - @Override - public void close() { - if (onDismiss != null) { - onDismiss.run(); - } - super.close(); - } - - @Override - public void tick() { - super.tick(); - tickCount++; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/title/SBEStyledTitleScreen.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/title/SBEStyledTitleScreen.java deleted file mode 100644 index df45ca1..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/title/SBEStyledTitleScreen.java +++ /dev/null @@ -1,546 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.title; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import com.github.kd_gaming1.packcore.ui.screen.configmanager.ConfigManagerScreen; -import com.github.kd_gaming1.packcore.ui.help.guide.GuideListScreen; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; -import com.github.kd_gaming1.packcore.util.update.modrinth.UpdateCache; -import com.github.kd_gaming1.packcore.util.update.UpdateResult; -import com.github.kd_gaming1.packcore.notification.UpdateNotifier; -import com.github.kd_gaming1.packcore.ui.toast.PackCoreToast; -import com.terraformersmc.modmenu.api.ModMenuApi; -import io.wispforest.lavendermd.MarkdownProcessor; -import io.wispforest.lavendermd.compiler.OwoUICompiler; -import io.wispforest.lavendermd.feature.*; -import io.wispforest.owo.ui.base.BaseOwoScreen; -import io.wispforest.owo.ui.component.*; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.multiplayer.ConnectScreen; -import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; -import net.minecraft.client.gui.screen.option.OptionsScreen; -import net.minecraft.client.gui.screen.world.SelectWorldScreen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.network.ServerAddress; -import net.minecraft.client.network.ServerInfo; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Improved styled title screen with better component organization - */ -public class SBEStyledTitleScreen extends BaseOwoScreen { - - private final Identifier backgroundTexture = Identifier.of(MOD_ID, "textures/gui/title/main_menu_background.png"); - private static final ModpackInfo info = PackCore.getModpackInfo(); - - private static long lastToastTime = 0; - private static final long TOAST_COOLDOWN_MS = 5 * 60 * 1000; - - private static long lastRamWarningTime = 0; - private static final long RAM_WARNING_COOLDOWN_MS = 5 * 60 * 1000; - - // Update state - private final boolean updateNotificationEnabled = - info != null && - PackCoreConfig.showUpdateNotificationsOnTitleScreen && - PackCoreConfig.enableUpdateNotifications; - private boolean updateAvailable; - private String currentVersion; - private String newVersion; - private String changelog; - private String modrinthName; - - // UI state - private boolean showChangelog = false; - private FlowLayout mainButtonLayout; - private FlowLayout changelogLayout; - - // Cached Markdown processor - private static final MarkdownProcessor MARKDOWN_PROCESSOR = - new MarkdownProcessor<>( - OwoUICompiler::new, - new BasicFormattingFeature(), - new ColorFeature(), - new LinkFeature(), - new ListFeature(), - new BlockQuoteFeature(), - new ImageFeature() - ); - - private static final Map COMPONENT_CACHE = new ConcurrentHashMap<>(); - - @Override - protected @NotNull OwoUIAdapter createAdapter() { - return OwoUIAdapter.create(this, Containers::verticalFlow); - } - - @Override - protected void build(FlowLayout rootUIComponent) { - rootUIComponent.surface(TextureSurfaces.stretched(backgroundTexture, 1920, 1082)); - - // Main components - rootUIComponent.child(createMainButtonAndTitle()).horizontalAlignment(HorizontalAlignment.CENTER); - rootUIComponent.child(createSocialButtons().positioning(Positioning.relative(0, 100))); - rootUIComponent.child(createSeeWhatIsNewButtons().positioning(Positioning.relative(100, 0))); - rootUIComponent.child(createModpackButtons().positioning(Positioning.relative(100, 100))); - - // Create changelog layout but don't add it initially - changelogLayout = createChangelogPanel(); - } - - @Override - public void init() { - UpdateResult result = checkForUpdates(); - - if (result.isSuccess() && result.isUpdateAvailable() && updateNotificationEnabled) { - long currentTime = System.currentTimeMillis(); - if (currentTime - lastToastTime > TOAST_COOLDOWN_MS && UpdateNotifier.shouldShowMainMenuToast(result.getVersionNumber())) { - PackCoreToast.showUpdateAvailable(currentVersion, newVersion, modrinthName); - lastToastTime = currentTime; - } - } - - if (!result.isSuccess()) { - PackCore.LOGGER.warn("Update check failed: {}", result.getErrorMessage()); - } - - if (PackCoreConfig.showLowMemoryWarning) { - long currentTime = System.currentTimeMillis(); - if (currentTime - lastRamWarningTime > RAM_WARNING_COOLDOWN_MS) { - checkAndWarnLowMemory(); - } - } - - super.init(); - } - - /** - * Check allocated RAM and show warning if less than 3GB - */ - private void checkAndWarnLowMemory() { - long maxMemory = Runtime.getRuntime().maxMemory(); - double maxMemoryGB = maxMemory / (1024.0 * 1024.0 * 1024.0); - - if (maxMemoryGB < PackCoreConfig.minimumRamGB) { - PackCoreToast.showWarning( - "Low Memory Allocation", - String.format("Only %.1f GB allocated! Recommend %.0fGB+ for optimal performance.", maxMemoryGB, (double) PackCoreConfig.minimumRamGB) - ); - lastRamWarningTime = System.currentTimeMillis(); - } - } - - /** - * Create main button area and title - */ - private FlowLayout createMainButtonAndTitle() { - FlowLayout buttonAndTitle = (FlowLayout) Containers.verticalFlow(Sizing.fixed(320), Sizing.fill(100)) - .gap(4) - .padding(Insets.of(4)) - .margins(Insets.of(4, 4, 4, 4)); - - // Title texture - TextureComponent title = (TextureComponent) Components.texture( - Identifier.of(MOD_ID, "textures/gui/title/title.png"), - 0, 0, 1476, 157, 1476, 157 - ) - .margins(Insets.top(8)) - .horizontalSizing(Sizing.fixed(312)) - .verticalSizing(Sizing.fixed(34)); - - // Button layout - mainButtonLayout = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .padding(Insets.of(8)) - .margins(Insets.top(12)); - - // Add all buttons - ButtonComponent joinHypixel = createButton("Join Hypixel", this::joinHypixel); - ButtonComponent openSingleplayer = createButton("SINGLEPLAYER", this::openSingleplayer); - ButtonComponent openMultiplayer = createButton("Multiplayer", this::openMultiplayer); - ButtonComponent openMods = createButton("MODS", this::openMods); - ButtonComponent openOptions = createButton("OPTIONS", this::openOptions); - - mainButtonLayout - .child(joinHypixel) - .child(openSingleplayer) - .child(openMultiplayer) - .child(openMods) - .child(openOptions) - .child(createButton("QUIT", button -> MinecraftClient.getInstance().scheduleStop())); - - - buttonAndTitle.child(title); - buttonAndTitle.child(mainButtonLayout); - - return buttonAndTitle; - } - - /** - * Create a standard button - */ - private ButtonComponent createButton(String text, ButtonComponent.PressAction action) { - return (ButtonComponent) Components.button( - Text.literal(text) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))), - action::onPress - ) - .renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/menu/blank_button.png"), 0, 0, 200, 66)) - .horizontalSizing(Sizing.fixed(200)) - .verticalSizing(Sizing.fixed(22)); - } - - - /** - * Create an icon button - */ - private ButtonComponent createIconButton(String texture, String tooltip, Runnable action) { - return (ButtonComponent) Components.button( - Text.empty(), - button -> action.run() - ) - .renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, texture), 0, 0, 22, 22)) - .horizontalSizing(Sizing.fixed(22)) - .verticalSizing(Sizing.fixed(22)) - .tooltip(Text.literal(tooltip)); - } - - /** - * Create social buttons panel - */ - private FlowLayout createSocialButtons() { - FlowLayout buttonLayout = (FlowLayout) Containers.verticalFlow(Sizing.content(), Sizing.content()) - .gap(6) - .horizontalAlignment(HorizontalAlignment.LEFT) - .padding(Insets.of(4)); - - buttonLayout - .child(createIconButton("textures/gui/menu/discord_icon.png", - "Join our Discord server", - () -> Util.getOperatingSystem().open(info.getDiscord()))) - .child(createIconButton("textures/gui/menu/modrinth_icon.png", - "Visit the modrinth page", - () -> Util.getOperatingSystem().open(info.getWebsite()))) - .child(createIconButton("textures/gui/menu/github_icon.png", - "Report an issue", - () -> Util.getOperatingSystem().open(info.getIssueTracker()))) - .child(createVersionInfo()); - - return buttonLayout; - } - - /** - * Create version info display - */ - private FlowLayout createVersionInfo() { - FlowLayout mainLayout = (FlowLayout) Containers.verticalFlow(Sizing.content(), Sizing.content()) - .gap(4) - .horizontalAlignment(HorizontalAlignment.LEFT); - - LabelComponent versionLabel = Components.label( - Text.literal("Pack Version: " + currentVersion) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(Color.ofArgb(TEXT_DARK)); - - mainLayout.child(versionLabel); - - if (updateAvailable) { - LabelComponent updateAvailableLabel = Components.label( - Text.literal("Update Available: " + newVersion) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).color(Color.ofArgb(TEXT_DARK)); - mainLayout.child(updateAvailableLabel); - } - - return mainLayout; - } - - /** - * Create see what's new button - */ - private FlowLayout createSeeWhatIsNewButtons() { - FlowLayout buttonLayout = (FlowLayout) Containers.verticalFlow(Sizing.content(), Sizing.content()) - .gap(6) - .horizontalAlignment(HorizontalAlignment.RIGHT) - .padding(Insets.of(4)); - - buttonLayout.child(createIconButton("textures/gui/menu/update_icon.png", - "See what's new", - this::toggleChangelog)); - - return buttonLayout; - } - - /** - * Create modpack buttons - */ - private FlowLayout createModpackButtons() { - FlowLayout buttonLayout = (FlowLayout) Containers.verticalFlow(Sizing.content(), Sizing.content()) - .gap(6) - .horizontalAlignment(HorizontalAlignment.RIGHT) - .padding(Insets.of(4)); - - buttonLayout - .child(createIconButton("textures/gui/menu/settings_icon.png", - "Modpack Settings import/export your config", - () -> { - assert this.client != null; - this.client.setScreen(new ConfigManagerScreen()); - })) - .child(createIconButton("textures/gui/menu/guide_icon.png", - "See Guides on how to use the modpack", - () -> { - assert this.client != null; - this.client.setScreen(new GuideListScreen()); - })); - - return buttonLayout; - } - - /** - * Create changelog panel with help button - */ - private FlowLayout createChangelogPanel() { - FlowLayout mainLayout = (FlowLayout) Containers.verticalFlow(Sizing.fill(65), Sizing.fill(75)) - .gap(4) - .horizontalAlignment(HorizontalAlignment.CENTER) - .padding(Insets.of(4)) - .surface(TextureSurfaces.stretched( - Identifier.of(MOD_ID, "textures/gui/menu/info_box.png"), 1142, 934)) - .margins(Insets.of(4, 4, 4, 4)) - .positioning(Positioning.relative(50, 75)); - - // Header section - FlowLayout changelogInfo = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(2) - .padding(Insets.of(6, 0, 8, 8)) - .horizontalAlignment(HorizontalAlignment.CENTER); - - // Determine status text - String changeLogInfoText; - if (Objects.equals(currentVersion, newVersion)) { - changeLogInfoText = "You are up to date! See change log for current version below:"; - } else if (compareVersions(currentVersion, newVersion) < 0) { - changeLogInfoText = "A new version is available! See what's new below:"; - } else { - changeLogInfoText = "You are using a newer or unknown version."; - } - - LabelComponent changelogLabel = Components.label( - Text.literal(changeLogInfoText) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte")))) - ).shadow(false); - - // Divider - FlowLayout divider = (FlowLayout) Containers.horizontalFlow(Sizing.fill(98), Sizing.fill(8)) - .surface(TextureSurfaces.scaledContain( - Identifier.of(MOD_ID, "textures/gui/menu/divider.png"), 2401, 96)); - - changelogInfo.child(changelogLabel); - changelogInfo.child(divider); - mainLayout.child(changelogInfo); - - // Changelog content with markdown - String changelogContent = changelog != null ? changelog : "No changelog available."; - - // Process markdown - var markdownUIComponent = COMPONENT_CACHE.computeIfAbsent( - changelogContent, - MARKDOWN_PROCESSOR::process - ); - markdownUIComponent.horizontalSizing(Sizing.fill(98)); - markdownUIComponent.padding(Insets.of(0, 4, 4, 4)); - - // Scrollable content - ScrollContainer scrollContainer = Containers.verticalScroll( - Sizing.fill(98), - Sizing.expand(), - (FlowLayout) markdownUIComponent - ); - scrollContainer.scrollbar(ScrollContainer.Scrollbar.vanilla()); - scrollContainer.margins(Insets.bottom(10)); - - mainLayout.child(scrollContainer); - - // Help button section - FlowLayout buttonSection = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .padding(Insets.of(8)); - - ButtonComponent helpButton = (ButtonComponent) Components.button( - Text.literal("📚 Open Update Guide"), - button -> { - // Open the guide screen and close the changelog - assert this.client != null; - this.client.setScreen(new GuideListScreen(this)); - toggleChangelog(); // Hide changelog when opening guides - } - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 180, 60)) - .horizontalSizing(Sizing.fixed(180)) - .verticalSizing(Sizing.fixed(20)); - - buttonSection.child(helpButton); - mainLayout.child(buttonSection); - - return mainLayout; - } - - /** - * Toggle changelog visibility - */ - private void toggleChangelog() { - showChangelog = !showChangelog; - - if (showChangelog) { - // Hide main buttons and show changelog - mainButtonLayout.remove(); - this.uiAdapter.rootComponent.child(changelogLayout); - } else { - // Hide changelog and show main buttons - changelogLayout.remove(); - FlowLayout buttonAndTitle = (FlowLayout) this.uiAdapter.rootComponent.children().getFirst(); - buttonAndTitle.child(mainButtonLayout); - } - } - - // ===== Button Actions ===== - - private void joinHypixel(ButtonWidget button) { - MinecraftClient client = MinecraftClient.getInstance(); - ServerInfo serverInfo = new ServerInfo("Hypixel", - PackCoreConfig.serverAddressForQuickJoinButton, - ServerInfo.ServerType.OTHER); - ConnectScreen.connect(this, client, - ServerAddress.parse(PackCoreConfig.serverAddressForQuickJoinButton), - serverInfo, false, null); - } - - private void openSingleplayer(ButtonWidget button) { - MinecraftClient.getInstance().setScreen(new SelectWorldScreen(this)); - } - - private void openMultiplayer(ButtonWidget button) { - MinecraftClient.getInstance().setScreen(new MultiplayerScreen(this)); - } - - private void openMods(ButtonWidget button) { - MinecraftClient client = MinecraftClient.getInstance(); - Screen current = client.currentScreen; - - try { - Screen modsScreen = ModMenuApi.createModsScreen(current); - client.setScreen(modsScreen); - } catch (Throwable t) { - PackCore.LOGGER.error("Failed to open Mod Menu screen", t); - PackCoreToast.showError("Mod Menu Error", "Could not open Mod Menu"); - } - } - - private void openOptions(ButtonWidget button) { - MinecraftClient client = MinecraftClient.getInstance(); - client.setScreen(new OptionsScreen(this, client.options)); - } - - // ===== Update Check Logic ===== - - public UpdateResult checkForUpdates() { - UpdateCache updateManager = PackCore.getUpdateManager(); - ModpackInfo info = PackCore.getModpackInfo(); - - if (updateManager == null || info == null) { - PackCore.LOGGER.error("Update system not initialized properly"); - // Initialize defaults to prevent NPEs in UI - this.currentVersion = "Unknown"; - this.newVersion = "Unknown"; - this.modrinthName = "Modpack"; - this.changelog = "Update system error"; - return UpdateResult.error("Update system not initialized properly"); - } - - // Always initialize local state from info first to ensure variables are not null - this.currentVersion = info.getVersion() != null ? info.getVersion() : "Unknown"; - this.modrinthName = info.getName(); - // Default newVersion to currentVersion so equality checks in build() don't fail - this.newVersion = this.currentVersion; - - // Check if the configuration is valid - if (info.isConfigurationValid()) { - this.updateAvailable = false; - this.changelog = "Configuration invalid: " + info.getValidationError(); - PackCore.LOGGER.warn("Skipping update check - configuration not properly set up: {}", - info.getValidationError()); - return UpdateResult.error("Configuration not properly set up: " + info.getValidationError()); - } - - UpdateResult result = updateManager.checkForUpdates(info); - - if (!result.isSuccess()) { - PackCore.LOGGER.error("Update check failed: {}", result.getErrorMessage()); - // Set error info for the UI - this.updateAvailable = false; - this.changelog = "Failed to check for updates.\n\nError: " + result.getErrorMessage(); - return result; - } - - // Update instance variables with remote data - this.updateAvailable = result.isUpdateAvailable(); - this.newVersion = result.getVersionNumber(); - this.changelog = result.getChangelog(); - - return result; - } - - /** - * Compare version strings - */ - public static int compareVersions(String v1, String v2) { - // Treat nulls explicitly - if (v1 == null && v2 == null) return 0; - if (v1 == null) return -1; - if (v2 == null) return 1; - - String[] parts1 = v1.replaceAll("[^0-9.]", "").split("\\."); - String[] parts2 = v2.replaceAll("[^0-9.]", "").split("\\."); - - int maxLength = Math.max(parts1.length, parts2.length); - - for (int i = 0; i < maxLength; i++) { - int p1 = 0; - int p2 = 0; - - if (i < parts1.length && !parts1[i].isEmpty()) { - try { p1 = Integer.parseInt(parts1[i]); } catch (NumberFormatException ignored) { p1 = 0; } - } - - if (i < parts2.length && !parts2[i].isEmpty()) { - try { p2 = Integer.parseInt(parts2[i]); } catch (NumberFormatException ignored) { p2 = 0; } - } - - if (p1 != p2) return p1 - p2; - } - - return 0; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/BaseWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/BaseWizardPage.java deleted file mode 100644 index b57edc1..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/BaseWizardPage.java +++ /dev/null @@ -1,368 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; -import io.wispforest.owo.ui.base.BaseOwoScreen; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.StackLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.StyleSpriteSource; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -public abstract class BaseWizardPage extends BaseOwoScreen { - - protected static final int HEADER_HEIGHT = 35; - protected static final int CONTENT_PADDING = getScaledPadding(); - protected static final int PROGRESS_BAR_WIDTH = 125; - - protected ButtonComponent primaryButton; - private boolean primaryButtonInitialActive = true; - - private StackLayout rootComponent; - - private final Identifier backgroundTexture; - private final WizardPageInfo pageInfo; - - private static final ModpackInfo info = PackCore.getModpackInfo(); - - protected BaseWizardPage(@NotNull WizardPageInfo pageInfo, @Nullable Identifier backgroundTexture) { - super(pageInfo.title()); - this.pageInfo = pageInfo; - this.backgroundTexture = backgroundTexture != null ? backgroundTexture : - Identifier.of(MOD_ID, "textures/gui/wizard/welcome_bg.png"); - } - - @Override - protected @NotNull OwoUIAdapter createAdapter() { - return OwoUIAdapter.create(this, Containers::stack); - } - - @Override - protected final void build(StackLayout rootComponent) { - this.rootComponent = rootComponent; - - rootComponent.surface(TextureSurfaces.stretched(backgroundTexture, 1920, 1082)); - - FlowLayout mainLayout = createMainLayout(); - - mainLayout.child(createHeader()); - - if (shouldShowStatusInfo()) { - mainLayout.child(createStatusPanel()); - } - - FlowLayout contentContainer = createContentContainer(); - buildContent(contentContainer); - mainLayout.child(contentContainer); - - if (shouldShowRightPanel()) { - FlowLayout contentContainerRight = createContentContainerRight(); - buildContentRight(contentContainerRight); - mainLayout.child(contentContainerRight); - } - - mainLayout.child(createFooter()); - - rootComponent.child( - Containers.stack(Sizing.fill(100), Sizing.fill(100)) - .child(Components.box(Sizing.fill(100), Sizing.fill(100)).color(Color.ofArgb(0x80_000000))) - .child(mainLayout) - ); - } - - protected StackLayout getRootComponent() { - return rootComponent; - } - - private FlowLayout createMainLayout() { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.fill(100)) - .padding(Insets.of(CONTENT_PADDING)); - } - - private FlowLayout createHeader() { - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(getContentColumnWidthPercent()), Sizing.fixed(HEADER_HEIGHT)) - .padding(Insets.of(CONTENT_PADDING - 6)) - .verticalAlignment(VerticalAlignment.CENTER); - - LabelComponent titleLabel = (LabelComponent) Components.label(pageInfo.title.copy() - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte"))))) - .color(Color.ofRgb(ACCENT_SECONDARY)) - .shadow(true) - .margins(Insets.of(0, 0, 4, 4)); - - header.child(titleLabel); - - FlowLayout progressIndicator = (FlowLayout) Containers.horizontalFlow(Sizing.content(), Sizing.fill(100)) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - progressIndicator.child(Components.label(Text.literal("| Step " + pageInfo.currentStep() + " of " + pageInfo.totalSteps()) - .styled(s -> s.withFont(new StyleSpriteSource.Font(Identifier.of(MOD_ID, "gallaeciaforte"))))) - .color(Color.ofRgb(ACCENT_SECONDARY)) - .shadow(true) - ); - - progressIndicator.child(createProgressBar()); - - header.child(progressIndicator); - - return header; - } - - private Component createProgressBar() { - int barHeight = 6; - - FlowLayout progressContainer = (FlowLayout) Containers.horizontalFlow( - Sizing.fixed(PROGRESS_BAR_WIDTH), - Sizing.fixed(barHeight) - ) - .surface(Surface.flat(0x60_000000)); - - int progressWidth = (int) (PROGRESS_BAR_WIDTH * ((double) pageInfo.currentStep() / pageInfo.totalSteps())); - - Component progressFill = Components.box(Sizing.fixed(progressWidth), Sizing.fill(100)) - .color(Color.ofRgb(ACCENT_PRIMARY)); - - Component progressHighlight = Components.box(Sizing.fixed(2), Sizing.fill(100)) - .color(Color.ofRgb(ACCENT_SECONDARY)) - .positioning(Positioning.absolute(progressWidth - 2, 0)); - - return Containers.stack(Sizing.fixed(PROGRESS_BAR_WIDTH), Sizing.fixed(barHeight)) - .child(progressContainer) - .child(progressFill) - .child(progressHighlight); - } - - private FlowLayout createStatusPanel() { - FlowLayout statusPanel = (FlowLayout) Containers.verticalFlow(Sizing.fill(38), Sizing.content()) - .gap(8) - .surface(TextureSurfaces.stretched(Identifier.of(MOD_ID, "textures/gui/wizard/small_info_box.png"), 722, 338)) - .padding(Insets.of(10)) - .verticalAlignment(VerticalAlignment.CENTER) - .positioning(Positioning.relative(100, 0)); - - FlowLayout messageSection = Containers.verticalFlow(Sizing.expand(), Sizing.content()) - .gap(4); - - FlowLayout headerRow = (FlowLayout) Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(6) - .verticalAlignment(VerticalAlignment.CENTER); - - headerRow.child(Components.label( - Text.literal("✓ Configuration Successful") - .setStyle(Style.EMPTY.withBold(Boolean.TRUE)) - ).color(Color.ofRgb(TEXT_PRIMARY))); - - messageSection.child(headerRow); - - LabelComponent detailLabel = Components.label( - Text.literal("PackCore has automatically applied the config below based on your screen resolution.").setStyle(Style.EMPTY) - ).color(Color.ofRgb(TEXT_SECONDARY)); - detailLabel.horizontalSizing(Sizing.fill(100)); - messageSection.child(detailLabel); - - LabelComponent infoLabel = (LabelComponent) Components.label( - Text.literal("Applied configuration: " + ConfigFileRepository.getCurrentConfig().getDisplayName()) - .setStyle(Style.EMPTY.withItalic(Boolean.TRUE)) - ).color(Color.ofRgb(TEXT_SECONDARY)) - .margins(Insets.of(2, 2, 2, 2)); - infoLabel.horizontalSizing(Sizing.fill(100)); - messageSection.child(infoLabel.margins(Insets.top(2))); - - statusPanel.child(messageSection); - - return statusPanel; - } - - private FlowLayout createContentContainer() { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(getContentColumnWidthPercent()), Sizing.expand()) - .gap(12) - .surface(TextureSurfaces.stretched(Identifier.of(MOD_ID, "textures/gui/wizard/info_box.png"), 1142, 934)) - .padding(Insets.of(CONTENT_PADDING + 10, CONTENT_PADDING + 10, CONTENT_PADDING + 4, CONTENT_PADDING + 4)) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.of(0, 28, 0, 0)); - } - - private FlowLayout createContentContainerRight() { - return (FlowLayout) Containers.verticalFlow(Sizing.fill(getContentColumnWidthRightPercent()), Sizing.fill(100)) - .gap(12) - .surface(TextureSurfaces.stretched(Identifier.of(MOD_ID, "textures/gui/wizard/box.png"), 607, 755)) - .padding(Insets.of(CONTENT_PADDING + 10, CONTENT_PADDING + 10, CONTENT_PADDING + 4, CONTENT_PADDING + 4)) - .horizontalAlignment(HorizontalAlignment.CENTER) - .positioning(Positioning.relative(100, 0)) - .margins(Insets.of(0, 28, 0, 0)); - } - - private FlowLayout createFooter() { - FlowLayout footer = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .padding(Insets.of(CONTENT_PADDING - 8, CONTENT_PADDING - 8, CONTENT_PADDING, CONTENT_PADDING)) - .verticalAlignment(VerticalAlignment.CENTER) - .positioning(Positioning.relative(0, 100)); - - FlowLayout buttonContainer = (FlowLayout) Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(12) - .positioning(Positioning.relative(100, 50)); - - if (hasPreviousPage()) { - ButtonComponent backButton = (ButtonComponent) Components.button( - Text.literal("Back"), - button -> onBackPressed() - ).renderer(ButtonComponent.Renderer.texture(Identifier.of(MOD_ID, "textures/gui/wizard/previous.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20)); - buttonContainer.child(backButton); - } - - if (isSkippable()) { - ButtonComponent skipButton = (ButtonComponent) Components.button( - Text.literal("Skip"), - button -> onSkipPressed() - ).renderer(ButtonComponent.Renderer.texture(Identifier.of(MOD_ID, "textures/gui/wizard/continue.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20)); - buttonContainer.child(skipButton); - } - - ButtonComponent primaryButton = createPrimaryButton(); - buttonContainer.child(primaryButton); - - FlowLayout linkButtonContainer = (FlowLayout) Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .gap(8) - .margins(Insets.of(0, 0, 4, 0)); - - ButtonComponent discord = (ButtonComponent) Components.button( - Text.empty(), - button -> Util.getOperatingSystem().open(info.getDiscord()) - ) - .renderer(ButtonComponent.Renderer.texture(Identifier.of(MOD_ID, "textures/gui/menu/discord_icon.png"), 0, 0, 22, 22)) - .horizontalSizing(Sizing.fixed(22)) - .verticalSizing(Sizing.fixed(22)); - - ButtonComponent modrinth = (ButtonComponent) Components.button( - Text.empty(), - button -> Util.getOperatingSystem().open(info.getWebsite()) - ) - .renderer(ButtonComponent.Renderer.texture(Identifier.of(MOD_ID, "textures/gui/menu/modrinth_icon.png"), 0, 0, 22, 22)) - .horizontalSizing(Sizing.fixed(22)) - .verticalSizing(Sizing.fixed(22)); - - ButtonComponent github = (ButtonComponent) Components.button( - Text.empty(), - button -> Util.getOperatingSystem().open(info.getIssueTracker()) - ) - .renderer(ButtonComponent.Renderer.texture(Identifier.of(MOD_ID, "textures/gui/menu/github_icon.png"), 0, 0, 22, 22)) - .horizontalSizing(Sizing.fixed(22)) - .verticalSizing(Sizing.fixed(22)); - - linkButtonContainer.child(discord); - linkButtonContainer.child(modrinth); - linkButtonContainer.child(github); - footer.child(linkButtonContainer); - footer.child(buttonContainer); - - return footer; - } - - protected void updatePrimaryButtonState(boolean enabled) { - if (this.primaryButton != null) { - this.primaryButton.active = enabled; - } else { - this.primaryButtonInitialActive = enabled; - } - } - - private ButtonComponent createPrimaryButton() { - String buttonText = isLastPage() ? "Finish" : "Continue"; - this.primaryButton = (ButtonComponent) Components.button( - Text.literal(buttonText), - button -> onContinuePressed() - ).renderer(ButtonComponent.Renderer.texture(Identifier.of(MOD_ID, "textures/gui/wizard/continue.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20)); - - this.primaryButton.active = this.primaryButtonInitialActive; - return this.primaryButton; - } - - private static int getScaledPadding() { - int screenWidth = MinecraftClient.getInstance().getWindow().getScaledWidth(); - return screenWidth < 900 ? 8 : (screenWidth > 1400 ? 20 : 15); - } - - protected FlowLayout createSelectionCard(boolean selected) { - int borderColor = selected ? SUCCESS_BORDER : 0x40_FFFFFF; - - return (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .surface((Surface.outline(borderColor))) - .padding(Insets.of(12)) - .cursorStyle(CursorStyle.HAND); - } - - protected abstract void buildContent(FlowLayout contentContainer); - - protected abstract void buildContentRight(FlowLayout contentContainerRight); - - protected abstract void onContinuePressed(); - - protected void onBackPressed() { - if (hasPreviousPage()) { - MinecraftClient.getInstance().setScreen( - WizardNavigator.createWizardPage(pageInfo.currentStep() - 1) - ); - } - } - - protected void onSkipPressed() { - onContinuePressed(); - } - - protected boolean hasPreviousPage() { - return pageInfo.currentStep() >= 1; - } - - protected boolean isLastPage() { - return pageInfo.currentStep() >= pageInfo.totalSteps(); - } - - protected boolean isSkippable() { - return false; - } - - protected boolean shouldShowStatusInfo() { - return true; - } - - protected boolean shouldShowRightPanel() { - return false; - } - - protected int getContentColumnWidthPercent() { - return 60; - } - - protected int getContentColumnWidthRightPercent() { - return 40; - } - - public record WizardPageInfo( - Text title, - int currentStep, - int totalSteps - ) {} -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/WizardNavigator.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/WizardNavigator.java deleted file mode 100644 index 158f051..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/WizardNavigator.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard; - -import com.github.kd_gaming1.packcore.ui.screen.wizard.pages.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.Text; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Central registry and navigation system for wizard pages. - * Manages page creation, ordering, and metadata. - */ -public class WizardNavigator { - - /** - * Metadata for a wizard page including its factory and display information - */ - public record PageInfo( - String id, - Text displayName, - PageFactory factory, - boolean optional - ) {} - - /** - * Functional interface for creating wizard page instances - */ - @FunctionalInterface - public interface PageFactory { - Screen create(); - } - - // Registry of all wizard pages in order - private static final Map PAGES = new LinkedHashMap<>(); - - static { - // Register all wizard pages with metadata - registerPage(0, "welcome", Text.literal("Welcome"), - WelcomeWizardPage::new, false); - - registerPage(1, "optimization", Text.literal("Performance Settings"), - OptimizationWizardPage::new, false); - - registerPage(2, "tab_design", Text.literal("Tab Design"), - TabDesignWizardPage::new, true); - - registerPage(3, "item_background", Text.literal("Item Background"), - ItemBackgroundWizardPage::new, true); - - registerPage(4, "resource_packs", Text.literal("Resource Packs"), - ResourcePacksWizardPage::new, true); - - registerPage(5, "useful_info", Text.literal("Useful Information"), - UsefulInfoWizardPage::new, false); - - registerPage(6, "apply", Text.literal("Apply Configuration"), - ApplyConfigurationWizard::new, false); - } - - /** - * Register a wizard page with the navigator - */ - private static void registerPage(int index, String id, Text displayName, - PageFactory factory, boolean optional) { - PAGES.put(index, new PageInfo(id, displayName, factory, optional)); - } - - /** - * Create a wizard page by its index - * @param pageNumber The page number (0-based) - * @return The created screen, or WelcomeWizardPage if index is invalid - */ - public static Screen createWizardPage(int pageNumber) { - PageInfo info = PAGES.get(pageNumber); - return info != null ? info.factory().create() : new WelcomeWizardPage(); - } - - /** - * Get metadata for a specific page - */ - public static PageInfo getPageInfo(int pageNumber) { - return PAGES.get(pageNumber); - } - - /** - * Get the total number of wizard pages - */ - public static int getTotalPages() { - return PAGES.size(); - } - - /** - * Check if a page is optional (can be skipped) - */ - public static boolean isPageOptional(int pageNumber) { - PageInfo info = PAGES.get(pageNumber); - return info != null && info.optional(); - } - - /** - * Get the next non-optional page index after the given page - * @return Next required page index, or -1 if at the end - */ - public static int getNextRequiredPage(int currentPage) { - for (int i = currentPage + 1; i < PAGES.size(); i++) { - PageInfo info = PAGES.get(i); - if (info != null && !info.optional()) { - return i; - } - } - return -1; - } - - /** - * Get all registered pages for iteration - */ - public static Map getAllPages() { - return new LinkedHashMap<>(PAGES); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ApplyConfigurationWizard.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ApplyConfigurationWizard.java deleted file mode 100644 index 78cdd38..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ApplyConfigurationWizard.java +++ /dev/null @@ -1,662 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.config.apply.WizardOptionApplyService; -import com.github.kd_gaming1.packcore.ui.screen.title.SBEStyledTitleScreen; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com .github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.OverlayContainer; -import io.wispforest.owo.ui.container.StackLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import net.minecraft.util.Identifier; - -import java.util.*; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; -import static com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents.ProgressStatus; - -/** - * Apply Configuration page - final page that applies all selected settings - */ -public class ApplyConfigurationWizard extends BaseWizardPage { - - private boolean configurationApplied = false; - private final WizardDataStore dataStore; - private LabelComponent statusLabel; - private ButtonComponent applyButton; - private FlowLayout progressContainer; - private FlowLayout warningBanner; - private final Map stepLabels = new LinkedHashMap<>(); - - public ApplyConfigurationWizard() { - super( - new WizardPageInfo( - Text.literal("Apply Configuration"), - 6, - 6 - ), - Identifier.of(MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - this.dataStore = WizardDataStore.getInstance(); - } - - @Override - protected void buildContent(FlowLayout contentContainer) { - contentContainer.gap(4); - - // Header - contentContainer.child(createHeader()); - - // Configuration summary - contentContainer.child(createConfigurationSummary()); - - // Warning banner (initially hidden) - warningBanner = createWarningBanner(); - contentContainer.child(warningBanner); - - // Progress section - contentContainer.child(createProgressSection()); - - // Status section - contentContainer.child(createStatusSection()); - - // Apply buttons - contentContainer.child(createActionButtons()); - - // Initialize UI state - initializeUIState(); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - contentContainerRight.child(createHelpSection()); - } - - /** - * Create the header section - */ - private FlowLayout createHeader() { - return Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .child(Components.label( - Text.literal("✨ Ready to Apply Your Settings!") - .setStyle(Style.EMPTY.withColor(ACCENT_SECONDARY).withBold(true)) - ).horizontalSizing(Sizing.fill(98)).margins(Insets.of(2))) - .child(Components.label( - Text.literal("Review your choices below. When you're ready, click 'Apply Settings' to activate everything.") - .setStyle(Style.EMPTY.withColor(Formatting.GRAY).withItalic(true)) - ).color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(98)) - .margins(Insets.of(2))); - } - - /** - * Create configuration summary - */ - private FlowLayout createConfigurationSummary() { - FlowLayout summary = WizardUIComponents.createInfoCard( - "📋 What You've Selected:", - null, - 0x20_FFD700, - ACCENT_SECONDARY - ); - - // Optimization Profile - String optimization = dataStore.getOptimizationProfile(); - summary.child(WizardUIComponents.createSummaryItem( - "🚀 Performance Level:", - optimization.isEmpty() ? "Default (no changes)" : optimization - )); - - // Tab design - String tabDesign = dataStore.getTabDesign(); - summary.child(WizardUIComponents.createSummaryItem( - "🖼 Tab Menu Style:", - tabDesign.isEmpty() ? "Default (no changes)" : tabDesign - )); - - // Item background - String itemBackground = dataStore.getItemBackground(); - summary.child(WizardUIComponents.createSummaryItem( - "🎨 Item Background Style:", - itemBackground.isEmpty() ? "Default (no changes)" : itemBackground - )); - - // Resource packs - List resourcePacks = dataStore.getResourcePacksOrdered(); - if (!resourcePacks.isEmpty()) { - summary.child(WizardUIComponents.createSummaryItem("📦 Resource Packs (loading order):", "")); - for (int i = 0; i < resourcePacks.size(); i++) { - summary.child(Components.label( - Text.literal(" " + (i + 1) + ". " + resourcePacks.get(i)) - ).color(Color.ofRgb(TEXT_SECONDARY)).margins(Insets.left(16))); - } - } else { - summary.child(WizardUIComponents.createSummaryItem("📦 Resource Packs:", "None selected")); - } - - return summary; - } - - /** - * Create warning banner (initially hidden) - */ - private FlowLayout createWarningBanner() { - FlowLayout banner = (FlowLayout) Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(4) - .surface(Surface.flat(0x30_FF8C00).and(Surface.outline(0xFF_FFA500))) - .padding(Insets.of(8)) - .margins(Insets.vertical(4)); - - // Initially hidden - banner.positioning(Positioning.absolute(0, -1000)); - return banner; - } - - /** - * Show warning banner with message - */ - private void showWarningBanner() { - MinecraftClient.getInstance().execute(() -> { - warningBanner.clearChildren(); - warningBanner.child(Components.label( - Text.literal("⚠ " + "HypixelPlus Requires Special Setup").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(0xFFA500))); - - warningBanner.child(Components.label(Text.literal("HypixelPlus needs the JVM argument -Xss4M to work properly. Please add -Xss4M to your launcher's JVM arguments and restart the game.")) - .color(Color.ofRgb(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(95))); - - warningBanner.positioning(Positioning.layout()); - }); - } - - /** - * Hide warning banner - */ - private void hideWarningBanner() { - MinecraftClient.getInstance().execute(() -> - warningBanner.positioning(Positioning.absolute(0, -1000))); - } - - /** - * Create progress section - */ - private FlowLayout createProgressSection() { - progressContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(98), Sizing.content()) - .gap(3) - .surface(Surface.flat(0x20_000000).and(Surface.outline(0x40_FFFFFF))) - .padding(Insets.of(6)); - - progressContainer.child(Components.label( - Text.literal("⚙ Applying Your Settings:") - .setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(ACCENT_SECONDARY))); - - initializeProgressSteps(); - - // Hide initially unless applying - if (!dataStore.isConfigurationApplying()) { - progressContainer.positioning(Positioning.absolute(0, -1000)); - } - - return progressContainer; - } - - /** - * Initialize progress steps based on configuration - */ - private void initializeProgressSteps() { - stepLabels.clear(); - - if (!dataStore.getOptimizationProfile().isEmpty()) { - addProgressStep("performance", "Performance Settings"); - } - - if (!dataStore.getResourcePacksOrdered().isEmpty()) { - addProgressStep("resourcepacks", "Resource Packs"); - } - - if (!dataStore.getTabDesign().isEmpty()) { - addProgressStep("tabdesign", "Tab Menu Style"); - } - - if (!dataStore.getItemBackground().isEmpty()) { - addProgressStep("itembackground", "Item Background Style"); - } - - if (!dataStore.getAdditionalSettings().isEmpty()) { - addProgressStep("additional", "Extra Settings"); - } - } - - /** - * Add a progress step - */ - private void addProgressStep(String key, String name) { - LabelComponent stepLabel = WizardUIComponents.createProgressStepLabel(name, ProgressStatus.PENDING); - stepLabels.put(key, stepLabel); - progressContainer.child(stepLabel); - } - - /** - * Update progress step status - */ - private void updateProgressStep(String stepKey, String status, String errorMessage) { - MinecraftClient.getInstance().execute(() -> { - LabelComponent stepLabel = stepLabels.get(stepKey); - if (stepLabel != null) { - String stepName = getStepName(stepKey); - - ProgressStatus progressStatus = switch (status) { - case "success" -> ProgressStatus.SUCCESS; - case "error" -> ProgressStatus.ERROR; - case "running" -> ProgressStatus.RUNNING; - default -> ProgressStatus.PENDING; - }; - - String displayText = getStatusIcon(progressStatus) + " " + stepName; - if (progressStatus == ProgressStatus.ERROR && errorMessage != null) { - displayText += " - " + errorMessage; - } - - Formatting color = getStatusColor(progressStatus); - stepLabel.text(Text.literal(displayText).setStyle(Style.EMPTY.withColor(color))); - } - }); - } - - /** - * Get step name from key - */ - private String getStepName(String key) { - return switch (key) { - case "performance" -> "Performance Settings"; - case "resourcepacks" -> "Resource Packs"; - case "tabdesign" -> "Tab Menu Style"; - case "itembackground" -> "Item Background Style"; - case "additional" -> "Extra Settings"; - default -> "Unknown Step"; - }; - } - - /** - * Get status icon - */ - private String getStatusIcon(ProgressStatus status) { - return switch (status) { - case SUCCESS -> "✅"; - case ERROR -> "❌"; - case RUNNING -> "⏳"; - case PENDING -> "⏸"; - }; - } - - /** - * Get status color - */ - private Formatting getStatusColor(ProgressStatus status) { - return switch (status) { - case SUCCESS -> Formatting.GREEN; - case ERROR -> Formatting.RED; - case RUNNING -> Formatting.YELLOW; - case PENDING -> Formatting.GRAY; - }; - } - - /** - * Create status section - */ - private FlowLayout createStatusSection() { - FlowLayout statusContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(4) - .margins(Insets.vertical(8)); - - statusLabel = (LabelComponent) Components.label( - Text.literal("👉 Ready to begin! Click 'Apply Settings' when you're ready.") - .setStyle(Style.EMPTY.withItalic(true)) - ).color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(98)) - .margins(Insets.of(2)); - - statusContainer.child(statusLabel); - return statusContainer; - } - - /** - * Create action buttons - */ - private FlowLayout createActionButtons() { - FlowLayout buttonContainer = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.top(16)) - .positioning(Positioning.relative(50, 100)); - - applyButton = (ButtonComponent) Components.button( - Text.literal("Apply Settings"), - this::onApplyPressed - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 130, 66)) - .horizontalSizing(Sizing.fixed(130)) - .verticalSizing(Sizing.fixed(22)); - - buttonContainer.child(applyButton); - return buttonContainer; - } - - /** - * Create help section for right panel - */ - private FlowLayout createHelpSection() { - FlowLayout help = WizardUIComponents.createInfoCard( - "ℹ What Happens Next?", - null, - 0x20_000000, - ACCENT_SECONDARY - ); - - String[] steps = { - "1. Performance settings will be adjusted", - "2. Tab menu style will be configured", - "3. Item background style will be applied", - "4. Resource packs will be enabled in order" - }; - - for (String step : steps) { - help.child(Components.label(Text.literal(step)) - .color(Color.ofRgb(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(95))); - } - - help.child(Components.label( - Text.literal("💡 Tip: Each step shows a progress indicator. If something fails, you'll see exactly what went wrong!") - .setStyle(Style.EMPTY.withItalic(true)) - ).color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(95)) - .margins(Insets.of(6, 2, 2, 2))); - - return help; - } - - /** - * Initialize UI state based on stored data - */ - private void initializeUIState() { - if (dataStore.isConfigurationApplying()) { - progressContainer.positioning(Positioning.layout()); - updateApplyButtonState(true, "Applying Settings..."); - updateStatusLabel("⏳ Please wait while we apply your settings...", Formatting.YELLOW); - updatePrimaryButtonState(false); - } else if (dataStore.isConfigurationApplied()) { - String result = dataStore.getConfigurationResult(); - if ("success".equals(result)) { - onConfigurationApplied(); - } else if ("failed".equals(result)) { - onConfigurationFailed(null, new RuntimeException(dataStore.getConfigurationErrorMessage())); - } - updatePrimaryButtonState(dataStore.isConfigurationApplied()); - } else { - updatePrimaryButtonState(false); - } - } - - /** - * Handle apply button press - */ - private void onApplyPressed(ButtonComponent button) { - if (dataStore.isConfigurationApplying() || dataStore.isConfigurationApplied()) { - return; - } - - PackCore.LOGGER.info("Starting configuration application process"); - - hideWarningBanner(); - - // Update state - dataStore.setConfigurationApplying(true); - dataStore.setConfigurationApplied(false); - dataStore.setConfigurationResult("", ""); - - // Show progress - progressContainer.positioning(Positioning.layout()); - updateApplyButtonState(true, "Applying..."); - updateStatusLabel("⏳ Please wait while we apply your settings. This may take a moment...", Formatting.YELLOW); - - // Apply configurations - WizardOptionApplyService.applyAllConfigurationsWithProgress(this::updateProgressStep) - .whenComplete((result, throwable) -> - MinecraftClient.getInstance().execute(() -> { - dataStore.setConfigurationApplying(false); - - if (result.overallSuccess() && throwable == null) { - dataStore.setConfigurationApplied(true); - dataStore.setConfigurationResult("success", ""); - onConfigurationApplied(); - } else { - dataStore.setConfigurationApplied(false); - - StringBuilder failureMessage = new StringBuilder(); - if (!result.failedSteps().isEmpty()) { - for (Map.Entry failure : result.failedSteps().entrySet()) { - failureMessage.append("❌ ").append(failure.getKey()) - .append(":\n ").append(failure.getValue()).append("\n\n"); - } - } else if (throwable != null) { - failureMessage.append(throwable.getMessage() != null ? - throwable.getMessage() : "An unexpected error occurred"); - } - - dataStore.setConfigurationResult("failed", failureMessage.toString()); - onConfigurationFailed(result, throwable); - } - })); - } - - /** - * Update apply button state - */ - private void updateApplyButtonState(boolean isApplying, String message) { - if (applyButton != null) { - applyButton.setMessage(Text.literal(message)); - applyButton.active = !isApplying; - } - } - - /** - * Update status label - */ - private void updateStatusLabel(String message, Formatting color) { - if (statusLabel != null) { - statusLabel.text(Text.literal(message).setStyle(Style.EMPTY.withColor(color))) - .horizontalSizing(Sizing.fill(100)); - } - } - - /** - * Handle successful configuration - */ - private void onConfigurationApplied() { - PackCore.LOGGER.info("Wizard configuration applied successfully!"); - - updateApplyButtonState(true, "✅ All Done!"); - updateStatusLabel("✅ Success! All your settings have been applied. Click 'Continue' to start playing!", - Formatting.GREEN); - - updatePrimaryButtonState(true); - - configurationApplied = true; - - PackCoreConfig.haveShownWelcomeWizard = true; - PackCoreConfig.write(MOD_ID); - } - - /** - * Handle configuration failure - */ - private void onConfigurationFailed(WizardOptionApplyService.ConfigurationResult result, Throwable throwable) { - PackCore.LOGGER.error("Failed to apply wizard configuration", throwable); - - // Check for specific failures - if (result != null && !result.failedSteps().isEmpty()) { - boolean hasResourcePackFailure = result.failedSteps().keySet().stream() - .anyMatch(key -> key.contains("Resource Pack")); - - if (hasResourcePackFailure && dataStore.getResourcePacksOrdered().stream() - .anyMatch(pack -> pack.equalsIgnoreCase("HypixelPlus"))) { - showWarningBanner( - ); - } - } - - updateApplyButtonState(false, "🔄 Retry Settings"); - updateStatusLabel("⚠ Some settings couldn't be applied. See details above. " + - "Click 'Retry Settings' or 'Finish' to continue.", Formatting.RED); - - updatePrimaryButtonState(true); - - dataStore.setConfigurationApplied(false); - dataStore.setConfigurationApplying(false); - } - - /** - * Show skip confirmation dialog - */ - private void showSkipConfirmation() { - FlowLayout dialog = WizardUIComponents.createInfoCard( - "⚠ Skip Configuration?", - "You haven't applied your configuration yet. If you skip now, none of your selected settings will be saved.", - PANEL_BACKGROUND, - WARNING_BORDER - ); - - dialog.positioning(Positioning.relative(50, 50)); - - // Add buttons - FlowLayout buttons = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(10) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.top(8)); - - buttons.child(Components.button( - Text.literal("Go Back"), - btn -> getRootComponent().removeChild(getRootComponent().children().getLast()) - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20))); - - buttons.child(Components.button( - Text.literal("Skip Anyway"), - btn -> proceedWithSkip() - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 120, 60)) - .horizontalSizing(Sizing.fixed(120)) - .verticalSizing(Sizing.fixed(20))); - - dialog.child(buttons); - - OverlayContainer overlay = Containers.overlay(dialog); - overlay.closeOnClick(true); - overlay.surface(Surface.flat(0x80_000000)); - //? if <=1.21.8 { - /*overlay.zIndex(10); - *///?} - - getRootComponent().child(overlay); - } - - /** - * Proceed with skipping configuration - */ - private void proceedWithSkip() { - PackCoreConfig.haveShownWelcomeWizard = true; - PackCoreConfig.write(MOD_ID); - assert this.client != null; - this.client.setScreen(new SBEStyledTitleScreen()); - } - - private void rebuildMenu() { - MinecraftClient.getInstance().execute(() -> { - StackLayout root = getRootComponent(); - root.clearChildren(); - build(root); - }); - } - - @Override - protected void onContinuePressed() { - if (dataStore.isConfigurationApplying()) { - updateStatusLabel("⏳ Please wait - we're still applying your settings...", Formatting.YELLOW); - return; - } - - if (!dataStore.isConfigurationApplied() && !"failed".equals(dataStore.getConfigurationResult())) { - updateStatusLabel("⚠ Please apply your settings first, or click 'Skip' to configure manually later.", - Formatting.GOLD); - return; - } - - assert this.client != null; - this.client.setScreen(new SBEStyledTitleScreen()); - } - - @Override - protected void onSkipPressed() { - if (dataStore.isConfigurationApplying()) { - updateStatusLabel("⏳ Please wait - we're still applying your settings. You can skip once it finishes.", - Formatting.YELLOW); - return; - } - - if (!dataStore.isConfigurationApplied() && !"failed".equals(dataStore.getConfigurationResult())) { - showSkipConfirmation(); - return; - } - - proceedWithSkip(); - } - - @Override - protected boolean isLastPage() { - return true; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } - - @Override - protected boolean shouldShowRightPanel() { - return true; - } - - @Override - protected int getContentColumnWidthPercent() { - return super.getContentColumnWidthPercent(); - } - - @Override - protected int getContentColumnWidthRightPercent() { - return 38; - } - - @Override - protected boolean isSkippable() { - return !configurationApplied; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ItemBackgroundWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ItemBackgroundWizardPage.java deleted file mode 100644 index adb2c62..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ItemBackgroundWizardPage.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.WizardNavigator; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import java.util.List; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Item Background selection page - allows choosing item background styles - */ -public class ItemBackgroundWizardPage extends BaseWizardPage { - - /** - * Item background style options - */ - private record BackgroundOption( - String key, - String displayName, - String texturePath - ) {} - - private static final List OPTIONS = List.of( - new BackgroundOption("No Background", "No Background", "item_bg_none.png"), - new BackgroundOption("Circular", "Circular Background", "item_bg_circular.png"), - new BackgroundOption("Square", "Square Background", "item_bg_square.png") - ); - - private final WizardDataStore dataStore; - private String selectedBackground; - private LabelComponent selectionLabel; - private FlowLayout contentContainer; - - public ItemBackgroundWizardPage() { - super( - new WizardPageInfo( - Text.literal("Item Background"), - 3, - 6 - ), - Identifier.of(PackCore.MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - this.dataStore = WizardDataStore.getInstance(); - this.selectedBackground = dataStore.getItemBackground(); - - // Default to "None" if nothing selected - if (selectedBackground.isEmpty()) { - selectedBackground = "None"; - } - } - - - @Override - protected void buildContent(FlowLayout contentContainer) { - this.contentContainer = contentContainer; - - // Apply frame texture - contentContainer.surface(TextureSurfaces.stretched( - Identifier.of(MOD_ID, "textures/gui/wizard/frame.png"), 1920, 1080 - )); - contentContainer.padding(Insets.of(22, 22, 18, 18)); - - // Header - contentContainer.child( - WizardUIComponents.createHeader( - "Choose your preferred item background style for", - "Select how items should appear in your inventory and menus. " + - "You can choose no background, a circular design, or a full square background. " + - "(Tip: Click the image)" - ).margins(Insets.horizontal(34)) - ); - - // Selection label - contentContainer.child(createSelectionLabel()); - - // Image choices - contentContainer.child(createImageChoices()); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - // No right panel for this page - } - - /** - * Create the selection status label - */ - private LabelComponent createSelectionLabel() { - String displayText; - int color; - - if ("None".equals(selectedBackground)) { - displayText = "👇 Click an image below to choose your item background style"; - color = ACCENT_SECONDARY; - } else { - displayText = "✓ Selected: " + selectedBackground; - color = SUCCESS_BORDER; - } - - this.selectionLabel = WizardUIComponents.createStatusLabel(displayText, null, color); - this.selectionLabel.margins(Insets.left(16)); - - return this.selectionLabel; - } - - /** - * Create the image choice section - */ - private FlowLayout createImageChoices() { - FlowLayout choicesRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(12) - .horizontalAlignment(HorizontalAlignment.CENTER) - .verticalAlignment(VerticalAlignment.CENTER); - - // Create each option - for (BackgroundOption option : OPTIONS) { - choicesRow.child(createBackgroundOption(option)); - } - - return choicesRow; - } - - /** - * Create a single background option - */ - private FlowLayout createBackgroundOption(BackgroundOption option) { - boolean isSelected = option.key.equals(selectedBackground); - - FlowLayout wrapper = (FlowLayout) Containers.verticalFlow(Sizing.fill(32), Sizing.expand()) - .verticalAlignment(VerticalAlignment.CENTER) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.of(8)) - .cursorStyle(CursorStyle.HAND); - - // Title - wrapper.child(Components.label( - Text.literal(option.displayName).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY)) - .margins(Insets.of(4, 4, 2, 4))); - - // Image container - Identifier textureId = Identifier.of(MOD_ID, "textures/gui/wizard/" + option.texturePath); - - Surface imageSurface = isSelected - ? TextureSurfaces.scaledContain(textureId, 555, 666).and(Surface.outline(SUCCESS_BORDER)) - : TextureSurfaces.scaledContain(textureId, 555, 666); - - FlowLayout imageContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand()) - .verticalAlignment(VerticalAlignment.CENTER) - .horizontalAlignment(HorizontalAlignment.CENTER) - .surface(imageSurface) - .margins(Insets.of(4)) - .cursorStyle(CursorStyle.HAND); - - // Click handler - imageContainer.mouseDown().subscribe((click, doubled) -> { - selectBackground(option.key); - return true; - }); - - wrapper.child(imageContainer); - return wrapper; - } - - /** - * Handle background selection - */ - private void selectBackground(String background) { - this.selectedBackground = background; - dataStore.setItemBackground(background); - - // Update label - if (this.selectionLabel != null) { - this.selectionLabel.text( - Text.literal("✓ Selected: " + selectedBackground) - .setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(SUCCESS_BORDER)); - } - - // Rebuild to update borders (simpler than tracking all containers) - rebuildContent(); - } - - /** - * Rebuild the content to update selection states - */ - private void rebuildContent() { - if (this.contentContainer != null) { - this.contentContainer.clearChildren(); - buildContent(this.contentContainer); - } - } - - @Override - protected void onContinuePressed() { - assert this.client != null; - this.client.setScreen(WizardNavigator.createWizardPage(4)); - } - - @Override - protected int getContentColumnWidthPercent() { - return 100; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/OptimizationWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/OptimizationWizardPage.java deleted file mode 100644 index 68f0128..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/OptimizationWizardPage.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.WizardNavigator; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.util.markdown.MarkdownService; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import java.util.List; - -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Example of a simplified wizard page using the new component system. - * This shows the optimization profile selection page. - */ -public class OptimizationWizardPage extends BaseWizardPage { - - private static final String FALLBACK_CONTENT = """ - # Optimization Profile - - Choose your preferred performance settings. - Find and edit this content in `rundir/packcore/wizard_markdown_content/optimisation.md` - """; - - /** - * Configuration for optimization profiles - */ - private record ProfileOption( - String key, - String icon, - String title, - String description - ) {} - - private static final List PROFILES = List.of( - new ProfileOption("Max FPS", "🚀", "Maximum FPS", - "Highest frame rates. Reduces visual quality for smooth gameplay."), - new ProfileOption("Balanced", "⚖", "Balanced", - "Best of both. Good FPS while keeping important visuals."), - new ProfileOption("Quality", "💎", "Visual Quality", - "Beautiful graphics. Requires good hardware."), - new ProfileOption("Shaders", "✨", "Shaders Enabled", - "Ultimate visuals. Needs high-end system.") - ); - - private final String markdownContent; - private final WizardDataStore dataStore; - - private String selectedProfile; - private LabelComponent headerTitle; - private FlowLayout rightPanel; - - public OptimizationWizardPage() { - super( - new WizardPageInfo( - Text.literal("FPS OR QUALITY???"), - 1, - 6 - ), - Identifier.of(PackCore.MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - // Load markdown content - MarkdownService markdownService = new MarkdownService(); - this.markdownContent = markdownService.getOrDefault("optimisation.md", FALLBACK_CONTENT); - - // Initialize data store - this.dataStore = WizardDataStore.getInstance(); - this.selectedProfile = dataStore.getOptimizationProfile(); - } - - @Override - protected void buildContent(FlowLayout contentContainer) { - // Create header using component factory - contentContainer.child( - WizardUIComponents.createHeader( - "Choose your preferred optimisation profile for", - "Please read the information below carefully before continuing." - ) - ); - - // Create markdown section using component factory - contentContainer.child( - WizardUIComponents.createMarkdownScroll(markdownContent) - ); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - this.rightPanel = contentContainerRight; - - // Create selection header - rightPanel.child(createSelectionHeader()); - - // Create profile options - rightPanel.child(createProfileOptions()); - } - - /** - * Create the selection status header - */ - private FlowLayout createSelectionHeader() { - FlowLayout header = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .margins(Insets.of(8, 0, 8, 8)); - - // Create status label based on selection - if (selectedProfile.isEmpty()) { - headerTitle = WizardUIComponents.createStatusLabel( - "Select your performance profile below", - "👇", - ACCENT_SECONDARY - ); - } else { - ProfileOption selected = findProfile(selectedProfile); - String icon = selected != null ? selected.icon() + " " : ""; - headerTitle = WizardUIComponents.createStatusLabel( - "Selected: " + icon + selectedProfile, - "✓", - SUCCESS_BORDER - ); - } - - header.child(headerTitle); - return header; - } - - /** - * Create scrollable profile options - */ - private ScrollContainer createProfileOptions() { - FlowLayout profilesLayout = Containers.verticalFlow(Sizing.fill(96), Sizing.content()) - .gap(8); - - // Create option boxes using component factory - for (ProfileOption profile : PROFILES) { - boolean isSelected = profile.key().equals(selectedProfile); - - FlowLayout optionBox = WizardUIComponents.createOptionBox( - profile.icon(), - profile.title(), - profile.description(), - isSelected, - (box) -> selectProfile(profile.key()) - ); - - profilesLayout.child(optionBox); - } - - // Create scroll container - ScrollContainer scrollContainer = Containers.verticalScroll( - Sizing.fill(100), - Sizing.expand(), - profilesLayout - ); - - scrollContainer.scrollbar(ScrollContainer.Scrollbar.vanilla()); - scrollContainer.scrollbarThiccness(6); - scrollContainer.surface(Surface.flat(0x40_000000).and(Surface.outline(0x30_FFFFFF))); - scrollContainer.padding(Insets.of(6)); - scrollContainer.margins(Insets.bottom(4)); - - return scrollContainer; - } - - /** - * Handle profile selection - */ - private void selectProfile(String profileKey) { - selectedProfile = profileKey; - dataStore.setOptimizationProfile(profileKey); - - // Update header with selection - if (headerTitle != null) { - ProfileOption selected = findProfile(profileKey); - String icon = selected != null ? selected.icon() + " " : ""; - - headerTitle.text( - Text.literal("✓ Selected: " + icon + profileKey) - .setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(SUCCESS_BORDER)); - } - - // Refresh profile boxes to update selection state - rebuildRightPanel(); - } - - /** - * Rebuild the right panel to update selection states - */ - private void rebuildRightPanel() { - rightPanel.clearChildren(); - rightPanel.child(createSelectionHeader()); - rightPanel.child(createProfileOptions()); - } - - /** - * Find a profile by key - */ - private ProfileOption findProfile(String key) { - return PROFILES.stream() - .filter(p -> p.key().equals(key)) - .findFirst() - .orElse(null); - } - - @Override - protected void onContinuePressed() { - assert this.client != null; - this.client.setScreen(WizardNavigator.createWizardPage(2)); - } - - @Override - protected int getContentColumnWidthPercent() { - return 55; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } - - @Override - protected boolean shouldShowRightPanel() { - return true; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ResourcePacksWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ResourcePacksWizardPage.java deleted file mode 100644 index 2ea0813..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/ResourcePacksWizardPage.java +++ /dev/null @@ -1,359 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.WizardNavigator; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.util.markdown.MarkdownService; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import java.util.*; - -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Resource Packs selection page - allows choosing multiple resource packs - */ -public class ResourcePacksWizardPage extends BaseWizardPage { - - private static final String FALLBACK_CONTENT = """ - # Resource Packs - - Choose your Resource Pack! This is the default Resource Pack content. - - Find and edit this content in `rundir/packcore/wizard_markdown_content/resource_packs.md` - """; - - /** - * Resource pack configuration - */ - private record PackOption( - String key, - String icon, - String title, - String description, - boolean requiresJVM - ) {} - - private static final List AVAILABLE_PACKS = List.of( - new PackOption( - "HypixelPlus", - "⭐", - "Hypixel Plus", - "Clean vanilla-style pack for Hypixel. Updates items and icons for clarity.", - true - ), - new PackOption( - "FurfSkyOverlay", - "🎭", - "FurfSky Overlay", - "Popular choice. Retextures items only, keeping vanilla GUI.", - false - ), - new PackOption( - "FurfSkyFull", - "🌟", - "FurfSky Full", - "Complete makeover. Items + GUI + menus in unique style.", - false - ), - new PackOption( - "SkyBlockDarkUI", - "🌙", - "SkyBlock Dark UI", - "Sleek dark theme for menus and mod interfaces. Modern aesthetic.", - false - ), - new PackOption( - "SkyBlockDarkMode", - "🌑", - "SkyBlock Dark mode", - "Sleek dark theme for The End island and The Mist", - false - ), - new PackOption( - "SophieHypixelEnchants", - "📚", - "Sophie's Hypixel Enchants", - "Custom textures for all Enchantment Books throughout the whole of Skyblock.", - false - ), - new PackOption( - "Defrosted", - "❄", - "Defrosted", - "Frosty blue 16x pack. Minimalist look with cool aesthetic.", - false - ), - new PackOption( - "Looshy", - "✨", - "Looshy", - "Smooth vanilla-like 16x. Refined textures, fresh and polished.", - false - ) - ); - - private final String markdownContent; - private final WizardDataStore dataStore; - private final Set selectedResourcePacks = new LinkedHashSet<>(); - - private FlowLayout rightPanel; - - public ResourcePacksWizardPage() { - super( - new WizardPageInfo( - Text.literal("Resource Packs"), - 4, - 6 - ), - Identifier.of(PackCore.MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - MarkdownService markdownService = new MarkdownService(); - this.markdownContent = markdownService.getOrDefault("resource_packs.md", FALLBACK_CONTENT); - - this.dataStore = WizardDataStore.getInstance(); - this.selectedResourcePacks.addAll(dataStore.getResourcePacksOrdered()); - } - - @Override - protected void buildContent(FlowLayout contentContainer) { - // Header - contentContainer.child( - WizardUIComponents.createHeader( - "Choose resource packs for", - null // No subtitle for this page - ) - ); - - // Markdown content - contentContainer.child(WizardUIComponents.createMarkdownScroll(markdownContent)); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - this.rightPanel = contentContainerRight; - rightPanel.child(createSelectionHeader()); - rightPanel.child(createPackOptions()); - } - - /** - * Create the selection header with count and loading order - */ - private FlowLayout createSelectionHeader() { - FlowLayout header = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .margins(Insets.of(8, 0, 8, 8)); - - // Title - header.child(Components.label( - Text.literal("🎨 Resource Packs").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY))); - - // Selection status - LabelComponent headerTitle; - if (selectedResourcePacks.isEmpty()) { - headerTitle = (LabelComponent) Components.label( - Text.literal("Select packs below (optional)") - ).color(Color.ofRgb(ACCENT_SECONDARY)) - .horizontalSizing(Sizing.fill(100)); - } else { - int count = selectedResourcePacks.size(); - headerTitle = (LabelComponent) Components.label( - Text.literal("✓ " + count + " pack" + (count == 1 ? "" : "s") + " selected") - .setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(SUCCESS_BORDER)) - .horizontalSizing(Sizing.fill(100)); - } - - header.child(headerTitle); - - // Show loading order if any selected - if (!selectedResourcePacks.isEmpty()) { - header.child(createLoadingOrderPreview()); - } - - return header; - } - - /** - * Create loading order preview - */ - private FlowLayout createLoadingOrderPreview() { - FlowLayout orderSection = WizardUIComponents.createInfoCard( - "📋 Loading Order (Top = Priority)", - null, - 0x20_4A90E2, - ACCENT_PRIMARY - ); - - int index = 1; - for (String packKey : selectedResourcePacks) { - FlowLayout orderItem = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .verticalAlignment(VerticalAlignment.CENTER); - - // Number badge - FlowLayout badge = (FlowLayout) Containers.horizontalFlow(Sizing.fixed(20), Sizing.fixed(20)) - .surface(Surface.flat(ACCENT_PRIMARY)) - .horizontalAlignment(HorizontalAlignment.CENTER) - .verticalAlignment(VerticalAlignment.CENTER); - - badge.child(Components.label(Text.literal(String.valueOf(index))) - .color(Color.ofRgb(TEXT_PRIMARY))); - - orderItem.child(badge); - - // Pack name - orderItem.child(Components.label(Text.literal(packKey)) - .color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.expand())); - - orderSection.child(orderItem); - index++; - } - - return orderSection; - } - - /** - * Create scrollable pack options - */ - private ScrollContainer createPackOptions() { - FlowLayout packsLayout = Containers.verticalFlow(Sizing.fill(96), Sizing.content()).gap(6); - - for (PackOption pack : AVAILABLE_PACKS) { - packsLayout.child(createPackOption(pack)); - } - - ScrollContainer scrollContainer = Containers.verticalScroll( - Sizing.fill(100), - Sizing.expand(), - packsLayout - ); - - scrollContainer.scrollbar(ScrollContainer.Scrollbar.vanilla()); - scrollContainer.scrollbarThiccness(6); - scrollContainer.surface(Surface.flat(0x40_000000).and(Surface.outline(0x30_FFFFFF))); - scrollContainer.padding(Insets.of(6)); - scrollContainer.margins(Insets.bottom(4)); - - return scrollContainer; - } - - /** - * Create a single pack option - */ - private FlowLayout createPackOption(PackOption pack) { - boolean isSelected = selectedResourcePacks.contains(pack.key); - - FlowLayout card = WizardUIComponents.createSelectionCard(isSelected, (c) -> togglePack(pack.key)); - card.cursorStyle(CursorStyle.HAND); - - // Header with icon, title, and JVM badge if needed - FlowLayout header = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .verticalAlignment(VerticalAlignment.CENTER); - - // Icon - header.child(Components.label(Text.literal(pack.icon)) - .color(Color.ofRgb(ACCENT_SECONDARY))); - - // Title - header.child(Components.label( - Text.literal(pack.title).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY)) - .horizontalSizing(Sizing.expand())); - - // JVM warning badge - if (pack.requiresJVM) { - FlowLayout warningBadge = (FlowLayout) Containers.horizontalFlow(Sizing.content(), Sizing.content()) - .surface(Surface.flat(WARNING_BG).and(Surface.outline(WARNING_BORDER))) - .padding(Insets.of(2)); - - warningBadge.child(Components.label(Text.literal("⚠ JVM")) - .color(Color.ofRgb(WARNING_BORDER))); - - header.child(warningBadge); - } - - card.child(header); - - // Description - LabelComponent desc = (LabelComponent) Components.label(Text.literal(pack.description)) - .color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(100)); - card.child(desc); - - // JVM warning details if selected and requires JVM - if (pack.requiresJVM && isSelected) { - FlowLayout jvmWarning = WizardUIComponents.createInfoCard( - "⚠ Requires JVM Argument", - "Add -Xss4M to launcher settings. Exit wizard, add argument, restart game.", - 0x20_F59E0B, - WARNING_BORDER - ); - jvmWarning.margins(Insets.top(6)); - card.child(jvmWarning); - } - - return card; - } - - /** - * Toggle pack selection - */ - private void togglePack(String packKey) { - if (selectedResourcePacks.contains(packKey)) { - selectedResourcePacks.remove(packKey); - } else { - selectedResourcePacks.add(packKey); - } - - // Update data store - dataStore.setResourcePacksOrdered(new ArrayList<>(selectedResourcePacks)); - - // Rebuild right panel to update UI - rebuildRightPanel(); - } - - /** - * Rebuild the right panel - */ - private void rebuildRightPanel() { - rightPanel.clearChildren(); - rightPanel.child(createSelectionHeader()); - rightPanel.child(createPackOptions()); - } - - @Override - protected void onContinuePressed() { - assert this.client != null; - this.client.setScreen(WizardNavigator.createWizardPage(5)); - } - - @Override - protected int getContentColumnWidthPercent() { - return 55; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } - - @Override - protected boolean shouldShowRightPanel() { - return true; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/TabDesignWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/TabDesignWizardPage.java deleted file mode 100644 index ba1922a..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/TabDesignWizardPage.java +++ /dev/null @@ -1,266 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.util.wizard.WizardDataStore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.WizardNavigator; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Tab Design selection page - allows choosing between SkyHanni and Skyblocker tab styles - */ -public class TabDesignWizardPage extends BaseWizardPage { - - /** - * Tab design options configuration - */ - private enum TabDesignOption { - SKYHANNI("SkyHanni", "SkyHanni Compact Tab", "skyhanni_tab.png"), - SKYBLOCKER("Skyblocker", "Skyblocker Fancy TabList", "skyblocker_tab.png"); - - private final String key; - private final String displayName; - private final String texturePath; - - TabDesignOption(String key, String displayName, String texturePath) { - this.key = key; - this.displayName = displayName; - this.texturePath = texturePath; - } - } - - private final WizardDataStore dataStore; - private String selectedDesign; - private LabelComponent selectionLabel; - private FlowLayout classicImageContainer; - private FlowLayout modernImageContainer; - - public TabDesignWizardPage() { - super( - new WizardPageInfo( - Text.literal("Tab design"), - 2, - 6 - ), - Identifier.of(PackCore.MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - this.dataStore = WizardDataStore.getInstance(); - this.selectedDesign = dataStore.getTabDesign(); - - // Default to "None" if nothing selected - if (selectedDesign.isEmpty()) { - selectedDesign = "None"; - } - } - - @Override - protected void buildContent(FlowLayout contentContainer) { - // Apply frame texture - contentContainer.surface(TextureSurfaces.stretched( - Identifier.of(MOD_ID, "textures/gui/wizard/frame.png"), 1920, 1080 - )); - contentContainer.padding(Insets.of(22, 22, 18, 18)); - - // Header - contentContainer.child( - WizardUIComponents.createHeader( - "Choose your preferred tab design to use in-game when playing with", - "The pack has two mods that change the tab list: SkyHanni and Skyblocker. " + - "You can not use both at the same time, so decide which one you like best—and select it. " + - "(Tip: Click the image)" - ).margins(Insets.horizontal(34)) - ); - - // Selection label - contentContainer.child(createSelectionLabel()); - - // Image choices - contentContainer.child(createImageChoices()); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - // No right panel for this page - } - - /** - * Create the selection status label - */ - private LabelComponent createSelectionLabel() { - String displayText; - int color; - - if ("None".equals(selectedDesign)) { - displayText = "👇 Click an image below to choose your tab design"; - color = ACCENT_SECONDARY; - } else { - displayText = "✓ Selected: " + selectedDesign; - color = SUCCESS_BORDER; - } - - this.selectionLabel = WizardUIComponents.createStatusLabel(displayText, null, color); - this.selectionLabel.margins(Insets.left(16)); - - return this.selectionLabel; - } - - /** - * Create the image choice section - */ - private FlowLayout createImageChoices() { - FlowLayout choicesRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.expand()) - .gap(12) - .horizontalAlignment(HorizontalAlignment.CENTER) - .verticalAlignment(VerticalAlignment.CENTER); - - // SkyHanni option - choicesRow.child(createTabDesignOption(TabDesignOption.SKYHANNI)); - - // Skyblocker option - choicesRow.child(createTabDesignOption(TabDesignOption.SKYBLOCKER)); - - return choicesRow; - } - - /** - * Create a single tab design option - */ - private FlowLayout createTabDesignOption(TabDesignOption option) { - FlowLayout wrapper = (FlowLayout) Containers.verticalFlow(Sizing.fill(48), Sizing.expand()) - .verticalAlignment(VerticalAlignment.CENTER) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.of(0,10,8,8)) - .cursorStyle(CursorStyle.HAND); - - // Title - wrapper.child(Components.label( - Text.literal(option.displayName).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY)) - .margins(Insets.of(4, 4, 2, 4))); - - // Image container - boolean isSelected = option.key.equals(selectedDesign); - Identifier textureId = Identifier.of(MOD_ID, "textures/gui/wizard/" + option.texturePath); - - FlowLayout imageContainer = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand()) - .verticalAlignment(VerticalAlignment.CENTER) - .horizontalAlignment(HorizontalAlignment.CENTER) - .surface(TextureSurfaces.scaledContain(textureId, 2560, 1441)) - .margins(Insets.of(4)) - .cursorStyle(CursorStyle.HAND); - - // Apply selection border if selected - if (isSelected) { - imageContainer.surface( - Surface.outline(SUCCESS_BORDER).and( - TextureSurfaces.scaledContain(textureId, 2560, 1441) - ) - ); - } - - // Store references for updating - if (option == TabDesignOption.SKYHANNI) { - this.classicImageContainer = imageContainer; - } else { - this.modernImageContainer = imageContainer; - } - - // Click handler - imageContainer.mouseDown().subscribe((click, doubled) -> { - selectDesign(option.key); - return true; - }); - - wrapper.child(imageContainer); - return wrapper; - } - - /** - * Handle design selection - */ - private void selectDesign(String design) { - this.selectedDesign = design; - dataStore.setTabDesign(design); - - // Update label - if (this.selectionLabel != null) { - this.selectionLabel.text( - Text.literal("✓ Selected: " + selectedDesign) - .setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(SUCCESS_BORDER)); - } - - // Update borders - updateBorders(); - } - - /** - * Update image borders based on selection - */ - private void updateBorders() { - if (classicImageContainer != null && modernImageContainer != null) { - // Remove all borders first - classicImageContainer.surface( - TextureSurfaces.scaledContain( - Identifier.of(MOD_ID, "textures/gui/wizard/skyhanni_tab.png"), - 2560, 1441 - ) - ); - - modernImageContainer.surface( - TextureSurfaces.scaledContain( - Identifier.of(MOD_ID, "textures/gui/wizard/skyblocker_tab.png"), - 2560, 1441 - ) - ); - - // Add border to selected - if ("SkyHanni".equals(selectedDesign)) { - classicImageContainer.surface( - TextureSurfaces.scaledContain( - Identifier.of(MOD_ID, "textures/gui/wizard/skyhanni_tab.png"), - 2560, 1441 - ).and(Surface.outline(SUCCESS_BORDER) - ) - ); - } else if ("Skyblocker".equals(selectedDesign)) { - modernImageContainer.surface( - TextureSurfaces.scaledContain( - Identifier.of(MOD_ID, "textures/gui/wizard/skyblocker_tab.png"), - 2560, 1441 - ).and(Surface.outline(SUCCESS_BORDER) - ) - ); - } - } - } - - @Override - protected void onContinuePressed() { - assert this.client != null; - this.client.setScreen(WizardNavigator.createWizardPage(3)); - } - - @Override - protected int getContentColumnWidthPercent() { - return 100; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/UsefulInfoWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/UsefulInfoWizardPage.java deleted file mode 100644 index 3168531..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/UsefulInfoWizardPage.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.ui.screen.wizard.WizardNavigator; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.ui.surface.effects.TextureSurfaces; -import com.github.kd_gaming1.packcore.util.markdown.MarkdownService; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.Insets; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; - -/** - * Useful Information page - displays helpful tips and information - */ -public class UsefulInfoWizardPage extends BaseWizardPage { - - private static final String FALLBACK_CONTENT = """ - # Useful Information - - Useful Information! This is the default Useful Information content. - - Find and edit this content in `rundir/packcore/wizard_markdown_content/useful_information.md` - """; - - private final String markdownContent; - - public UsefulInfoWizardPage() { - super( - new WizardPageInfo( - Text.literal("Useful information"), - 5, - 6 - ), - Identifier.of(PackCore.MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - MarkdownService markdownService = new MarkdownService(); - this.markdownContent = markdownService.getOrDefault("useful_information.md", FALLBACK_CONTENT); - } - - @Override - protected void buildContent(FlowLayout contentContainer) { - // Apply frame texture - contentContainer.surface(TextureSurfaces.stretched( - Identifier.of(MOD_ID, "textures/gui/wizard/frame.png"), 1920, 1080 - )); - contentContainer.padding(Insets.of(24, 36, 24, 24)); - - // Header - contentContainer.child( - WizardUIComponents.createHeader( - "Useful information when playing on", - "The pack have many mods and some features, here is a few tips to get you started." - ).margins(Insets.horizontal(34)) - ); - - // Markdown content in scrollable area - var scrollContainer = WizardUIComponents.createMarkdownScroll(markdownContent); - scrollContainer.margins(Insets.bottom(10)); - contentContainer.child(scrollContainer); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - // No right panel for this page - } - - @Override - protected void onContinuePressed() { - assert this.client != null; - this.client.setScreen(WizardNavigator.createWizardPage(6)); - } - - @Override - protected int getContentColumnWidthPercent() { - return 100; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/WelcomeWizardPage.java b/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/WelcomeWizardPage.java deleted file mode 100644 index 8a4dd0d..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/screen/wizard/pages/WelcomeWizardPage.java +++ /dev/null @@ -1,382 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.screen.wizard.pages; - -import com.github.kd_gaming1.packcore.config.PackCoreConfig; -import com.github.kd_gaming1.packcore.config.storage.ConfigFileRepository; -import com.github.kd_gaming1.packcore.ui.screen.wizard.BaseWizardPage; -import com.github.kd_gaming1.packcore.ui.screen.wizard.WizardNavigator; -import com.github.kd_gaming1.packcore.ui.screen.components.WizardUIComponents; -import com.github.kd_gaming1.packcore.util.markdown.MarkdownService; -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.container.OverlayContainer; -import io.wispforest.owo.ui.container.ScrollContainer; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Style; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; -import static com.github.kd_gaming1.packcore.ui.theme.UITheme.*; - -/** - * Welcome page - first page of the wizard - */ -public class WelcomeWizardPage extends BaseWizardPage { - - private static final String FALLBACK_CONTENT = """ - # Welcome - - Welcome to PackCore! This is the default welcome content. - - Find and edit this content in `rundir/packcore/wizard_markdown_content/welcome.md` - """; - - private final String markdownContent; - private final boolean configApplied; - - private boolean detailsExpanded = false; - private FlowLayout detailsSection; - private ButtonComponent toggleButton; - - public WelcomeWizardPage() { - super( - new WizardPageInfo(Text.literal("Welcome"), 0, 6), - Identifier.of(MOD_ID, "textures/gui/wizard/welcome_bg.png") - ); - - MarkdownService markdownService = new MarkdownService(); - this.markdownContent = markdownService.getOrDefault("welcome.md", FALLBACK_CONTENT); - this.configApplied = PackCoreConfig.defaultConfigSuccessfullyApplied; - } - - @Override - protected void buildContent(FlowLayout contentContainer) { - contentContainer.gap(8); - - // Header - contentContainer.child( - WizardUIComponents.createHeader( - "Welcome to", - "Let's get you set up for the best experience! Take 30 seconds and read through the welcome please." - ) - ); - - // Status card - contentContainer.child(createStatusCard()); - - // Expandable details - contentContainer.child(createExpandableDetails()); - - // Bottom tip - contentContainer.child(createBottomTip()); - } - - @Override - protected void buildContentRight(FlowLayout contentContainerRight) { - contentContainerRight.gap(8); - contentContainerRight.child(createWizardOverview()); - } - - /** - * Create status card showing config application status - */ - private FlowLayout createStatusCard() { - if (configApplied) { - return createSuccessCard(); - } else { - return createFailureCard(); - } - } - - /** - * Create success status card - */ - private FlowLayout createSuccessCard() { - FlowLayout card = WizardUIComponents.createInfoCard( - "✓ Default Configs Successfully Applied", - null, - 0x20_10B981, - SUCCESS_BORDER - ); - - // Add resolution details - MinecraftClient mc = MinecraftClient.getInstance(); - int width = mc.getWindow().getWidth(); - int height = mc.getWindow().getHeight(); - String configName = ConfigFileRepository.getCurrentConfig().getDisplayName(); - - LabelComponent details = (LabelComponent) Components.label( - Text.literal("We have detected your screen resolution to be " + width + "x" + height + - " and have enabled configs: " + configName) - ).color(Color.ofRgb(TEXT_SECONDARY)) - .horizontalSizing(Sizing.fill(100)); - - card.child(details); - return card; - } - - /** - * Create failure status card with reset option - */ - private FlowLayout createFailureCard() { - FlowLayout card = WizardUIComponents.createInfoCard( - "⚠ Automatic Config Application Failed", - "Automatic config setup didn't work. You can reset and try again, " + - "or continue and configure manually from the main menu later.", - 0x20_EF4444, - ERROR_BORDER - ); - - ButtonComponent resetButton = (ButtonComponent) Components.button( - Text.literal("Reset & Try Again"), - btn -> showResetConfirmation() - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20)); - - card.child(resetButton); - return card; - } - - /** - * Create expandable details section - */ - private FlowLayout createExpandableDetails() { - FlowLayout container = Containers.verticalFlow(Sizing.fill(100), Sizing.content()).gap(8); - - toggleButton = (ButtonComponent) Components.button( - Text.literal(getToggleButtonText()), - btn -> toggleDetails() - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/menu/blank_button.png"), 0, 0, 200, 66)) - .horizontalSizing(Sizing.fixed(200)) - .verticalSizing(Sizing.fixed(22)); - - container.child(toggleButton); - - // Details section (initially hidden) - detailsSection = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.expand(64)) - .gap(8) - .surface(Surface.flat(0x20_000000).and(Surface.outline(0x30_FFFFFF))) - .padding(Insets.of(8)); - - detailsSection.child(WizardUIComponents.createMarkdownScroll(markdownContent) - .sizing(Sizing.fill(100), Sizing.fill())); - - if (!detailsExpanded) { - detailsSection.positioning(Positioning.absolute(0, -10000)); - } - - container.child(detailsSection); - return container; - } - - /** - * Toggle details visibility - */ - private void toggleDetails() { - detailsExpanded = !detailsExpanded; - toggleButton.setMessage(Text.literal(getToggleButtonText())); - - if (detailsExpanded) { - detailsSection.positioning(Positioning.layout()); - } else { - detailsSection.positioning(Positioning.absolute(0, -10000)); - } - } - - private String getToggleButtonText() { - return detailsExpanded ? "▼ Hide Details" : "▶ Show More Details"; - } - - /** - * Create bottom tip panel - */ - private FlowLayout createBottomTip() { - FlowLayout tip = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .surface(Surface.flat(0x20_FFB84D)) - .padding(Insets.of(12)); - - tip.child(Components.label(Text.literal("💡")).color(Color.ofRgb(ACCENT_SECONDARY))); - - LabelComponent tipText = (LabelComponent) Components.label( - Text.literal("Tip: You can reconfigure everything later using ") - .append(Text.literal("/packcore").setStyle(Style.EMPTY.withBold(true))) - .append(Text.literal(" in-game")) - ).color(Color.ofRgb(TEXT_PRIMARY)) - .horizontalSizing(Sizing.expand()); - - tip.child(tipText); - return tip; - } - - /** - * Create wizard overview panel for right side - */ - private FlowLayout createWizardOverview() { - FlowLayout overview = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .margins(Insets.of(4)); - - overview.child(Components.label( - Text.literal("📋 Setup Steps").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY))); - - // Create step list - FlowLayout stepsContainer = Containers.verticalFlow(Sizing.fill(98), Sizing.content()).gap(6); - - String[][] steps = { - {"1", "Performance", "Optimize FPS & quality"}, - {"2", "Tab Design", "Choose Tab style"}, - {"3", "Background", "Pick item style"}, - {"4", "Resources", "Select texture packs"}, - {"5", "Info", "Useful tips"}, - {"6", "Apply", "Activate settings"} - }; - - for (String[] step : steps) { - stepsContainer.child(createStepIndicator(step[0], step[1], step[2])); - } - - // Wrap the steps container in an scroll container - ScrollContainer stepsScroll = Containers.verticalScroll( - Sizing.fill(100), - Sizing.expand(), - stepsContainer - ); - - stepsScroll.scrollbar(ScrollContainer.Scrollbar.vanilla()); - stepsScroll.scrollbarThiccness(6); - stepsScroll.surface(Surface.flat(0x40_000000).and(Surface.outline(0x30_FFFFFF))); - stepsScroll.padding(Insets.of(8)); - - overview.child(stepsScroll); - return overview; - } - - /** - * Create a step indicator for the overview - */ - private FlowLayout createStepIndicator(String number, String title, String desc) { - FlowLayout indicator = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .padding(Insets.of(8)) - .surface(Surface.flat(0x20_4A90E2).and(Surface.outline(0x30_FFFFFF))) - .verticalAlignment(VerticalAlignment.CENTER); - - // Badge - FlowLayout badge = (FlowLayout) Containers.horizontalFlow(Sizing.fixed(28), Sizing.fixed(28)) - .surface(Surface.flat(ACCENT_PRIMARY).and(Surface.outline(0xFF_FFFFFF))) - .horizontalAlignment(HorizontalAlignment.CENTER) - .verticalAlignment(VerticalAlignment.CENTER); - - badge.child(Components.label( - Text.literal(number).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY)).shadow(true)); - - indicator.child(badge); - - // Text content - FlowLayout textContent = Containers.verticalFlow(Sizing.expand(), Sizing.content()).gap(2); - - textContent.child(Components.label( - Text.literal(title).setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(TEXT_PRIMARY))); - - textContent.child(Components.label(Text.literal(desc)) - .color(Color.ofRgb(TEXT_SECONDARY))); - - indicator.child(textContent); - return indicator; - } - - /** - * Show reset confirmation dialog - */ - private void showResetConfirmation() { - FlowLayout dialog = (FlowLayout) Containers.verticalFlow(Sizing.fixed(350), Sizing.content()) - .gap(12) - .surface(Surface.flat(PANEL_BACKGROUND).and(Surface.outline(WARNING_BORDER))) - .padding(Insets.of(20)) - .positioning(Positioning.relative(50, 50)); - - dialog.child(Components.label( - Text.literal("⚠ Reset Setup?").setStyle(Style.EMPTY.withBold(true)) - ).color(Color.ofRgb(WARNING_BORDER))); - - LabelComponent msg = (LabelComponent) Components.label( - Text.literal("This will reset PackCore and close the game. " + - "The wizard will restart when you reopen Minecraft.") - ).color(Color.ofRgb(TEXT_PRIMARY)) - .horizontalSizing(Sizing.fill(100)); - - dialog.child(msg); - - // Buttons - FlowLayout buttons = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(8) - .horizontalAlignment(HorizontalAlignment.CENTER) - .margins(Insets.top(8)); - - buttons.child(Components.button( - Text.literal("Cancel"), - btn -> getRootComponent().removeChild(getRootComponent().children().getLast()) - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20))); - - buttons.child(Components.button( - Text.literal("Reset & Exit"), - btn -> performReset() - ).renderer(ButtonComponent.Renderer.texture( - Identifier.of(MOD_ID, "textures/gui/wizard/button.png"), 0, 0, 100, 60)) - .horizontalSizing(Sizing.fixed(100)) - .verticalSizing(Sizing.fixed(20))); - - dialog.child(buttons); - - // Show as overlay - OverlayContainer overlay = Containers.overlay(dialog); - overlay.closeOnClick(true); - overlay.surface(Surface.flat(0x80_000000)); - - getRootComponent().child(overlay); - } - - /** - * Perform reset action - */ - private void performReset() { - PackCoreConfig.defaultConfigSuccessfullyApplied = false; - PackCoreConfig.write(MOD_ID); - MinecraftClient.getInstance().scheduleStop(); - } - - @Override - protected void onContinuePressed() { - assert this.client != null; - this.client.setScreen(WizardNavigator.createWizardPage(1)); - } - - @Override - protected int getContentColumnWidthPercent() { - return 58; - } - - @Override - protected boolean shouldShowRightPanel() { - return true; - } - - @Override - protected boolean shouldShowStatusInfo() { - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/surface/effects/TextureSurfaces.java b/src/main/java/com/github/kd_gaming1/packcore/ui/surface/effects/TextureSurfaces.java deleted file mode 100644 index a9b7e2f..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/surface/effects/TextureSurfaces.java +++ /dev/null @@ -1,220 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.surface.effects; - -import io.wispforest.owo.ui.core.Surface; -import net.minecraft.util.Identifier; -import net.minecraft.client.gl.RenderPipelines; - -/** - * Enhanced Surface implementations for texture rendering with different scaling modes. - * - *

Provides CSS-like background scaling options: - *

    - *
  • {@link #scaledCover} - Scale to cover component while preserving aspect ratio (may crop)
  • - *
  • {@link #scaledContain} - Scale to fit within component while preserving aspect ratio (may letterbox)
  • - *
  • {@link #stretched} - Stretch texture to fill component exactly (may distort)
  • - *
  • {@link #tiled} - Tile texture to fill component (software-based, no GL_REPEAT dependency)
  • - *
- */ -public final class TextureSurfaces { - private TextureSurfaces() {} - - /** - * Scale texture to cover the entire component while preserving aspect ratio. - * The texture may be cropped if aspect ratios don't match. - * Equivalent to CSS {@code background-size: cover}. - */ - public static Surface scaledCover(Identifier texture, int textureWidth, int textureHeight) { - validateTextureDimensions(textureWidth, textureHeight); - return (context, component) -> { - if (component.width() <= 0 || component.height() <= 0) return; - - int cx = component.x(); - int cy = component.y(); - int cw = component.width(); - int ch = component.height(); - - // Calculate scale to cover (larger scale factor) - float scaleX = (float) cw / textureWidth; - float scaleY = (float) ch / textureHeight; - float scale = Math.max(scaleX, scaleY); - - int scaledWidth = Math.round(textureWidth * scale); - int scaledHeight = Math.round(textureHeight * scale); - - // Center the scaled texture - int drawX = cx + (cw - scaledWidth) / 2; - int drawY = cy + (ch - scaledHeight) / 2; - - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - texture, - drawX, drawY, - 0f, 0f, - scaledWidth, scaledHeight, - textureWidth, textureHeight, - textureWidth, textureHeight, - -1 - ); - }; - } - - /** - * Scale texture to fit within the component while preserving aspect ratio. - * The component may have empty space if aspect ratios don't match. - * Equivalent to CSS {@code background-size: contain}. - */ - public static Surface scaledContain(Identifier texture, int textureWidth, int textureHeight) { - validateTextureDimensions(textureWidth, textureHeight); - return (context, component) -> { - if (component.width() <= 0 || component.height() <= 0) return; - - int cx = component.x(); - int cy = component.y(); - int cw = component.width(); - int ch = component.height(); - - // Calculate scale to contain (smaller scale factor) - float scaleX = (float) cw / textureWidth; - float scaleY = (float) ch / textureHeight; - float scale = Math.min(scaleX, scaleY); - - int scaledWidth = Math.round(textureWidth * scale); - int scaledHeight = Math.round(textureHeight * scale); - - // Center the scaled texture - int drawX = cx + (cw - scaledWidth) / 2; - int drawY = cy + (ch - scaledHeight) / 2; - - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - texture, - drawX, drawY, - 0f, 0f, - scaledWidth, scaledHeight, - textureWidth, textureHeight, - textureWidth, textureHeight, - -1 - ); - }; - } - - /** - * Stretch texture to fill the component exactly, ignoring aspect ratio. - * This may distort the texture if component and texture aspect ratios differ. - * Equivalent to CSS {@code background-size: 100% 100%}. - */ - public static Surface stretched(Identifier texture, int textureWidth, int textureHeight) { - validateTextureDimensions(textureWidth, textureHeight); - return (context, component) -> { - if (component.width() <= 0 || component.height() <= 0) return; - - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - texture, - component.x(), component.y(), - 0f, 0f, - component.width(), component.height(), - textureWidth, textureHeight, - textureWidth, textureHeight, - -1 - ); - }; - } - - /** - * Tile texture to fill the component by repeating it as needed. - * This is implemented in software and doesn't depend on GL_REPEAT or atlas settings. - * Partial tiles at edges are clipped appropriately. - */ - public static Surface tiled(Identifier texture, int textureWidth, int textureHeight) { - validateTextureDimensions(textureWidth, textureHeight); - return (context, component) -> { - if (component.width() <= 0 || component.height() <= 0) return; - - int startX = component.x(); - int startY = component.y(); - int endX = startX + component.width(); - int endY = startY + component.height(); - - // Tile the texture across the component - for (int y = startY; y < endY; y += textureHeight) { - int tileHeight = Math.min(textureHeight, endY - y); - - for (int x = startX; x < endX; x += textureWidth) { - int tileWidth = Math.min(textureWidth, endX - x); - - // Draw partial texture region for edge tiles - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - texture, - x, y, - 0f, 0f, - tileWidth, tileHeight, - textureWidth, textureHeight - ); - } - } - }; - } - - /** - * Create a tiled surface with custom tile dimensions. - * Useful when you want to tile at a different scale than the original texture. - */ - public static Surface tiledCustom(Identifier texture, int textureWidth, int textureHeight, int tileWidth, int tileHeight) { - validateTextureDimensions(textureWidth, textureHeight); - if (tileWidth <= 0 || tileHeight <= 0) { - throw new IllegalArgumentException("Tile dimensions must be positive: " + tileWidth + "x" + tileHeight); - } - - return (context, component) -> { - if (component.width() <= 0 || component.height() <= 0) return; - - int startX = component.x(); - int startY = component.y(); - int endX = startX + component.width(); - int endY = startY + component.height(); - - for (int y = startY; y < endY; y += tileHeight) { - int drawHeight = Math.min(tileHeight, endY - y); - - for (int x = startX; x < endX; x += tileWidth) { - int drawWidth = Math.min(tileWidth, endX - x); - - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - texture, - x, y, - 0f, 0f, - drawWidth, drawHeight, - textureWidth, textureHeight, - tileWidth, tileHeight, - -1 - ); - } - } - }; - } - - /** - * Create a surface that combines multiple surfaces, drawing them in order. - * Later surfaces are drawn on top of earlier ones. - */ - public static Surface layered(Surface... surfaces) { - if (surfaces.length == 0) { - return Surface.BLANK; - } - - return (context, component) -> { - for (Surface surface : surfaces) { - surface.draw(context, component); - } - }; - } - - private static void validateTextureDimensions(int width, int height) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("Texture dimensions must be positive: " + width + "x" + height); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/theme/UITheme.java b/src/main/java/com/github/kd_gaming1/packcore/ui/theme/UITheme.java deleted file mode 100644 index bafe4ae..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/theme/UITheme.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.theme; - -import io.wispforest.owo.ui.component.ButtonComponent; -import io.wispforest.owo.ui.core.Color; - -public final class UITheme { - - private UITheme() {} - - // Panel colors - public static final int PANEL_BACKGROUND = 0xC0_1A1A1A; - public static final int DARK_PANEL_BACKGROUND = 0xFF_2A2A2A; - - // NEW: Primary accent colors - public static final int ACCENT_PRIMARY = 0xFF_4A90E2; // Blue - public static final int ACCENT_SECONDARY = 0xFF_FFB84D; // Warm yellow - - // OLD: Kept for backwards compatibility - @Deprecated public static final int ACCENT_GOLD = ACCENT_SECONDARY; - - // NEW: Text colors - public static final int TEXT_PRIMARY = 0xFF_FFFFFF; // White - public static final int TEXT_SECONDARY = 0xFF_B0B8C8; // Light gray - public static final int TEXT_DARK = 0xFF_000000; // Black - public static final int TEXT_HINT = 0xFF_6B7280; // Darker gray - - // OLD: Kept for backwards compatibility - @Deprecated public static final int TEXT_WHITE = TEXT_PRIMARY; - - // NEW: Status colors with proper naming - public static final int SUCCESS_BG = 0xE0_1A4D2E; - public static final int SUCCESS_BORDER = 0xFF_10B981; // Green - public static final int WARNING_BG = 0xE0_78350F; - public static final int WARNING_BORDER = 0xFF_F59E0B; // Orange - public static final int ERROR_BG = 0xE0_7F1D1D; - public static final int ERROR_BORDER = 0xFF_EF4444; // Red - - // OLD: Kept for backwards compatibility - @Deprecated public static final int STATUS_SUCCESS_BG = SUCCESS_BG; - @Deprecated public static final int STATUS_SUCCESS_BORDER = SUCCESS_BORDER; - @Deprecated public static final int STATUS_WARNING_BG = WARNING_BG; - @Deprecated public static final int STATUS_WARNING_BORDER = WARNING_BORDER; - @Deprecated public static final int STATUS_ERROR_BG = ERROR_BG; - @Deprecated public static final int STATUS_ERROR_BORDER = ERROR_BORDER; - - // Entry/List item colors - public static final int ENTRY_BACKGROUND = 0xC0_2A2A2A; - public static final int ENTRY_HOVER = 0xC0_3A3A3A; - public static final int ENTRY_SELECTED = 0xC0_4A4A4A; - public static final int ENTRY_BORDER = 0xFF_555555; - - // Convenience helpers - public static Color color(int rgb) { return Color.ofRgb(rgb); } - - public static ButtonComponent.Renderer defaultEntryRenderer() { - return ButtonComponent.Renderer.flat(ENTRY_BACKGROUND, ENTRY_BORDER, ENTRY_BORDER); - } - - public static ButtonComponent.Renderer successRenderer() { - return ButtonComponent.Renderer.flat(SUCCESS_BG, SUCCESS_BORDER, ENTRY_BORDER); - } - - public static ButtonComponent.Renderer warningRenderer() { - return ButtonComponent.Renderer.flat(WARNING_BG, WARNING_BORDER, ENTRY_BORDER); - } - - public static ButtonComponent.Renderer errorRenderer() { - return ButtonComponent.Renderer.flat(ERROR_BG, ERROR_BORDER, ENTRY_BORDER); - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/ui/toast/PackCoreToast.java b/src/main/java/com/github/kd_gaming1/packcore/ui/toast/PackCoreToast.java deleted file mode 100644 index 01b660d..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/ui/toast/PackCoreToast.java +++ /dev/null @@ -1,407 +0,0 @@ -package com.github.kd_gaming1.packcore.ui.toast; - -import io.wispforest.owo.ui.base.BaseOwoToast; -import io.wispforest.owo.ui.component.Components; -import io.wispforest.owo.ui.component.LabelComponent; -import io.wispforest.owo.ui.container.Containers; -import io.wispforest.owo.ui.container.FlowLayout; -import io.wispforest.owo.ui.core.*; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.toast.Toast; -import net.minecraft.client.toast.ToastManager; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import net.minecraft.util.Identifier; - -import java.util.ArrayList; -import java.util.List; -/** - * Improved toast system using builder pattern for easy toast creation - */ -public class PackCoreToast extends BaseOwoToast { - - // Constants - private static final int DEFAULT_WIDTH = 280; - private static final int DEFAULT_PADDING = 12; - private static final long DEFAULT_DURATION_MS = 10000; - - /** - * Toast type enum for common toast styles - */ - public enum ToastType { - SUCCESS(0xFF_55FF55, "✓"), - WARNING(0xFF_FFD700, "⚠"), - ERROR(0xFF_FF5555, "✗"), - INFO(0xFF_5555FF, "ℹ"), - UPDATE(0xFF_FFD700, "↑"); - - private final int borderColor; - private final String icon; - - ToastType(int borderColor, String icon) { - this.borderColor = borderColor; - this.icon = icon; - } - } - - private PackCoreToast(Builder builder) { - super( - () -> createContent(builder), - createTimeoutPredicate(System.currentTimeMillis(), builder.duration) - ); - } - - /** - * Create the timeout predicate - */ - private static VisibilityPredicate createTimeoutPredicate(long startTime, long duration) { - return (toast, time) -> { - long elapsed = System.currentTimeMillis() - startTime; - return elapsed < duration ? Toast.Visibility.SHOW : Toast.Visibility.HIDE; - }; - } - - /** - * Create the toast content from builder - */ - private static FlowLayout createContent(Builder builder) { - FlowLayout container = Containers.verticalFlow( - Sizing.fixed(builder.width), - Sizing.content() - ); - - // Add icon if present - if (builder.icon != null) { - FlowLayout iconRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content()) - .gap(6) - .verticalAlignment(VerticalAlignment.CENTER); - - // Icon texture or text - if (builder.iconTexture != null) { - iconRow.child(Components.texture( - builder.iconTexture, - 0, 0, builder.iconSize, builder.iconSize, - builder.iconSize, builder.iconSize - )); - } else if (builder.icon != null) { - iconRow.child(Components.label( - Text.literal(builder.icon).formatted(Formatting.BOLD) - ).color(Color.ofRgb(builder.borderColor))); - } - - // Title next to icon - if (builder.title != null) { - iconRow.child(builder.title); - } - - container.child(iconRow); - } else if (builder.title != null) { - // Title without icon - container.child(builder.title); - } - - // Add all lines - for (LabelComponent line : builder.lines) { - container.child(line); - } - - // Apply styling - container - .gap(builder.gap) - .padding(Insets.of(builder.padding)) - .surface(Surface.flat(builder.backgroundColor)) - .horizontalAlignment(builder.horizontalAlignment) - .verticalAlignment(VerticalAlignment.TOP); - - // Add border if specified - if (builder.borderColor != 0) { - FlowLayout borderContainer = Containers.verticalFlow( - Sizing.content(), - Sizing.content() - ); - borderContainer - .child(container) - .surface(Surface.outline(builder.borderColor)) - .padding(Insets.of(1)); - return borderContainer; - } - - return container; - } - - /** - * Show this toast - */ - public void show() { - ToastManager toastManager = MinecraftClient.getInstance().getToastManager(); - toastManager.add(this); - } - - /** - * Builder class for creating toasts - */ - public static class Builder { - // Required - private LabelComponent title; - - // Optional with defaults - private final List lines = new ArrayList<>(); - private ToastType type = ToastType.INFO; - private int borderColor = ToastType.INFO.borderColor; - private String icon = null; - private Identifier iconTexture = null; - private int iconSize = 16; - private int width = DEFAULT_WIDTH; - private int padding = DEFAULT_PADDING; - private int gap = 2; - private long duration = DEFAULT_DURATION_MS; - private int backgroundColor = 0xC0_000000; - private HorizontalAlignment horizontalAlignment = HorizontalAlignment.LEFT; - - /** - * Set the toast type (applies default styling) - */ - public Builder type(ToastType type) { - this.type = type; - this.borderColor = type.borderColor; - this.icon = type.icon; - return this; - } - - /** - * Set the title text - */ - public Builder title(String text) { - this.title = Components.label(Text.literal(text).formatted(Formatting.WHITE, Formatting.BOLD)); - this.title.horizontalTextAlignment(HorizontalAlignment.LEFT); - return this; - } - - /** - * Set the title text with formatting - */ - public Builder title(Text text) { - this.title = Components.label(text); - this.title.horizontalTextAlignment(HorizontalAlignment.LEFT); - return this; - } - - /** - * Add a line of text - */ - public Builder line(String text) { - return line(text, Formatting.GRAY); - } - - /** - * Add a line of text with formatting - */ - public Builder line(String text, Formatting... formatting) { - LabelComponent line = (LabelComponent) Components.label(Text.literal(text).formatted(formatting)).horizontalSizing(Sizing.fill(100)); - line.horizontalTextAlignment(HorizontalAlignment.LEFT); - this.lines.add(line); - return this; - } - - /** - * Add a Text component as a line - */ - public Builder line(Text text) { - LabelComponent line = Components.label(text); - line.horizontalTextAlignment(HorizontalAlignment.LEFT); - this.lines.add(line); - return this; - } - - /** - * Set custom icon - */ - public Builder icon(String icon) { - this.icon = icon; - this.iconTexture = null; - return this; - } - - /** - * Set icon texture - */ - public Builder iconTexture(Identifier texture, int size) { - this.iconTexture = texture; - this.iconSize = size; - this.icon = null; - return this; - } - - /** - * Set border color - */ - public Builder borderColor(int color) { - this.borderColor = color; - return this; - } - - /** - * Set background color - */ - public Builder backgroundColor(int color) { - this.backgroundColor = color; - return this; - } - - /** - * Set duration in milliseconds - */ - public Builder duration(long durationMs) { - this.duration = durationMs; - return this; - } - - /** - * Set width - */ - public Builder width(int width) { - this.width = width; - return this; - } - - /** - * Set padding - */ - public Builder padding(int padding) { - this.padding = padding; - return this; - } - - /** - * Set gap between elements - */ - public Builder gap(int gap) { - this.gap = gap; - return this; - } - - /** - * Set horizontal alignment - */ - public Builder alignment(HorizontalAlignment alignment) { - this.horizontalAlignment = alignment; - return this; - } - - /** - * Build the toast - */ - public PackCoreToast build() { - if (title == null) { - throw new IllegalStateException("Toast must have a title"); - } - return new PackCoreToast(this); - } - - /** - * Build and show the toast immediately - */ - public void show() { - build().show(); - } - } - - // ===== Static factory methods for common toasts ===== - - /** - * Create an update available toast - */ - public static void showUpdateAvailable(String currentVersion, String newVersion, String modpackName) { - new Builder() - .type(ToastType.UPDATE) - .title(Text.literal("Update Available for ") - .append(Text.literal(modpackName).formatted(Formatting.GOLD, Formatting.BOLD)) - .append(Text.literal("!"))) - .line(Text.literal(currentVersion).formatted(Formatting.GRAY) - .append(Text.literal(" → ").formatted(Formatting.DARK_AQUA)) - .append(Text.literal(newVersion).formatted(Formatting.GOLD))) - .line("Update the pack in your launcher", Formatting.GRAY, Formatting.ITALIC) - .duration(12000) - .show(); - } - - /** - * Create an export completion toast - */ - public static void showExportComplete(String configName, String fileName) { - new Builder() - .type(ToastType.SUCCESS) - .title("Export Complete!") - .line(Text.literal("Config: ").formatted(Formatting.GRAY) - .append(Text.literal(configName).formatted(Formatting.GREEN, Formatting.BOLD))) - .line(Text.literal("Saved as: ").formatted(Formatting.GRAY) - .append(Text.literal(fileName).formatted(Formatting.YELLOW))) - .line("You can now share or import this config", Formatting.GRAY, Formatting.ITALIC) - .show(); - } - - /** - * Create a backup completion toast - */ - public static void showBackupComplete(String backupName, String fileName, boolean isRestore) { - String title = isRestore ? "Restore Complete!" : "Backup Complete!"; - String message = isRestore ? "Configuration restored successfully." : "Backup saved successfully."; - - new Builder() - .type(ToastType.SUCCESS) - .borderColor(isRestore ? 0xFF_5555FF : 0xFF_55FF55) - .title(title) - .line(Text.literal("Name: ").formatted(Formatting.GRAY) - .append(Text.literal(backupName).formatted(Formatting.GREEN, Formatting.BOLD))) - .line(Text.literal("File: ").formatted(Formatting.GRAY) - .append(Text.literal(fileName).formatted(Formatting.YELLOW))) - .line(message, Formatting.GRAY, Formatting.ITALIC) - .show(); - } - - /** - * Create an error toast - */ - public static void showError(String title, String message) { - new Builder() - .type(ToastType.ERROR) - .title(title) - .line(message, Formatting.RED) - .duration(8000) - .show(); - } - - /** - * Create a warning toast - */ - public static void showWarning(String title, String message) { - new Builder() - .type(ToastType.WARNING) - .title(title) - .line(message, Formatting.YELLOW) - .show(); - } - - /** - * Create an info toast - */ - public static void showInfo(String title, String message) { - new Builder() - .type(ToastType.INFO) - .title(title) - .line(message, Formatting.AQUA) - .show(); - } - - /** - * Create a simple success toast - */ - public static void showSuccess(String message) { - new Builder() - .type(ToastType.SUCCESS) - .title("Success!") - .line(message, Formatting.GREEN) - .duration(5000) - .show(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/update/ModrinthClient.java b/src/main/java/com/github/kd_gaming1/packcore/update/ModrinthClient.java new file mode 100644 index 0000000..10df87e --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/update/ModrinthClient.java @@ -0,0 +1,80 @@ +package com.github.kd_gaming1.packcore.update; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +public final class ModrinthClient { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ModrinthClient"); + private static final String API_BASE = "https://api.modrinth.com/v2"; + private static final String USER_AGENT = "PackCore/1.0"; + private static final int TIMEOUT_MS = 5000; + + private ModrinthClient() {} + + public record VersionInfo(String versionNumber, String changelog) {} + + public static Optional fetchLatestVersion(String projectId) { + return get(API_BASE + "/project/" + projectId + "/version") + .flatMap(element -> { + JsonArray versions = element.getAsJsonArray(); + if (versions.isEmpty()) { + LOGGER.warn("Modrinth project '{}' has no versions listed.", projectId); + return Optional.empty(); + } + + JsonObject latest = versions.get(0).getAsJsonObject(); + + String versionNumber = latest.get("version_number").getAsString(); + String changelog = latest.has("changelog") && !latest.get("changelog").isJsonNull() + ? latest.get("changelog").getAsString() + : null; + + return Optional.of(new VersionInfo(versionNumber, changelog)); + }); + } + + private static Optional get(String url) { + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) URI.create(url).toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setConnectTimeout(TIMEOUT_MS); + connection.setReadTimeout(TIMEOUT_MS); + + int status = connection.getResponseCode(); + if (status == 404) { + LOGGER.warn("Modrinth returned 404 for: {}", url); + return Optional.empty(); + } + if (status != 200) { + LOGGER.warn("Modrinth API returned status {} for: {}", status, url); + return Optional.empty(); + } + + try (InputStreamReader reader = new InputStreamReader( + connection.getInputStream(), StandardCharsets.UTF_8)) { + return Optional.of(JsonParser.parseReader(reader)); + } + + } catch (Exception e) { + LOGGER.warn("Failed to reach Modrinth API: {}", e.getMessage()); + return Optional.empty(); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/update/UpdateCache.java b/src/main/java/com/github/kd_gaming1/packcore/update/UpdateCache.java new file mode 100644 index 0000000..eea32b3 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/update/UpdateCache.java @@ -0,0 +1,26 @@ +package com.github.kd_gaming1.packcore.update; + +import com.github.kd_gaming1.packcore.update.ModrinthClient.VersionInfo; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** In-memory cache — resets on every restart. */ +public final class UpdateCache { + + private static final AtomicReference cachedVersionInfo = new AtomicReference<>(); + + private UpdateCache() {} + + public static Optional get() { + return Optional.ofNullable(cachedVersionInfo.get()); + } + + public static void set(VersionInfo versionInfo) { + cachedVersionInfo.set(versionInfo); + } + + public static void invalidate() { + cachedVersionInfo.set(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/update/UpdateChecker.java b/src/main/java/com/github/kd_gaming1/packcore/update/UpdateChecker.java new file mode 100644 index 0000000..967b669 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/update/UpdateChecker.java @@ -0,0 +1,117 @@ +package com.github.kd_gaming1.packcore.update; + +import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; +import com.github.kd_gaming1.packcore.update.ModrinthClient.VersionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +public final class UpdateChecker { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/UpdateChecker"); + private static final Executor NETWORK_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + + private static final AtomicReference CACHED_STATUS = + new AtomicReference<>(UpdateStatus.unknown()); + private static final AtomicReference> IN_FLIGHT = + new AtomicReference<>(); + + private UpdateChecker() {} + + public static UpdateStatus getCachedStatus() { + return CACHED_STATUS.get(); + } + + public static CompletableFuture checkAsync() { + CompletableFuture existing = IN_FLIGHT.get(); + if (existing != null) { + return existing; + } + + CompletableFuture created = CompletableFuture + .supplyAsync(UpdateChecker::check, NETWORK_EXECUTOR) + .thenApply(status -> { + CACHED_STATUS.set(status); + return status; + }) + .whenComplete((result, throwable) -> IN_FLIGHT.set(null)); + + if (IN_FLIGHT.compareAndSet(null, created)) { + return created; + } + + CompletableFuture winner = IN_FLIGHT.get(); + return winner != null ? winner : created; + } + + private static UpdateStatus check() { + ModpackMetadata metadata = ModpackMetadata.getInstance(); + String projectId = metadata.getModrinthProjectId(); + String installedVersion = metadata.getModpackVersion(); + + if (projectId == null || projectId.isBlank()) { + LOGGER.warn("No Modrinth project ID set in modpack.json, skipping update check."); + return UpdateStatus.unknown(); + } + + Optional cached = UpdateCache.get(); + VersionInfo versionInfo; + + if (cached.isPresent()) { + versionInfo = cached.get(); + LOGGER.info("Using cached latest version: {}", versionInfo.versionNumber()); + } else { + Optional fetched = ModrinthClient.fetchLatestVersion(projectId); + if (fetched.isEmpty()) { + return UpdateStatus.unknown(); + } + + versionInfo = fetched.get(); + UpdateCache.set(versionInfo); + LOGGER.info("Fetched latest version from Modrinth: {}", versionInfo.versionNumber()); + } + + UpdateStatus status = isNewerVersion(versionInfo.versionNumber(), installedVersion) + ? UpdateStatus.updateAvailable(installedVersion, versionInfo.versionNumber(), versionInfo.changelog()) + : UpdateStatus.upToDate(installedVersion); + + LOGGER.info( + "Update check result: {} (installed: {}, latest: {})", + status.state(), + installedVersion, + versionInfo.versionNumber() + ); + return status; + } + + public static boolean isNewerVersion(String available, String installed) { + String[] availableParts = available.split("\\."); + String[] installedParts = installed.split("\\."); + + int segmentCount = Math.max(availableParts.length, installedParts.length); + + for (int i = 0; i < segmentCount; i++) { + int availableSegment = i < availableParts.length ? parseSegment(availableParts[i]) : 0; + int installedSegment = i < installedParts.length ? parseSegment(installedParts[i]) : 0; + + if (availableSegment != installedSegment) { + return availableSegment > installedSegment; + } + } + + return false; + } + + private static int parseSegment(String segment) { + try { + return Integer.parseInt(segment.trim()); + } catch (NumberFormatException e) { + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/update/UpdateStatus.java b/src/main/java/com/github/kd_gaming1/packcore/update/UpdateStatus.java new file mode 100644 index 0000000..c9d1e4a --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/update/UpdateStatus.java @@ -0,0 +1,40 @@ +package com.github.kd_gaming1.packcore.update; + +public final class UpdateStatus { + + public enum State { + UP_TO_DATE, + UPDATE_AVAILABLE, + UNKNOWN + } + + private final State state; + private final String installedVersion; + private final String latestVersion; + private final String changelog; + + private UpdateStatus(State state, String installedVersion, String latestVersion, String changelog) { + this.state = state; + this.installedVersion = installedVersion; + this.latestVersion = latestVersion; + this.changelog = changelog; + } + + public static UpdateStatus upToDate(String version) { + return new UpdateStatus(State.UP_TO_DATE, version, version, null); + } + + public static UpdateStatus updateAvailable(String installed, String latest, String changelog) { + return new UpdateStatus(State.UPDATE_AVAILABLE, installed, latest, changelog); + } + + public static UpdateStatus unknown() { + return new UpdateStatus(State.UNKNOWN, null, null, null); + } + + public State state() { return state; } + public String installedVersion() { return installedVersion; } + public String latestVersion() { return latestVersion; } + public String changelog() { return changelog; } + public boolean isUpdateAvailable() { return state == State.UPDATE_AVAILABLE; } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/GsonUtils.java b/src/main/java/com/github/kd_gaming1/packcore/util/GsonUtils.java deleted file mode 100644 index 7b1b4e7..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/GsonUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.kd_gaming1.packcore.util; - -import com.google.gson.ExclusionStrategy; -import com.google.gson.FieldAttributes; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -public final class GsonUtils { - private GsonUtils() {} - - public static final ExclusionStrategy EXCLUSION_STRATEGY = new ExclusionStrategy() { - @Override - public boolean shouldSkipField(FieldAttributes attributes) { - return attributes.getAnnotation(Exclude.class) != null; - } - - @Override - public boolean shouldSkipClass(Class clazz) { - return false; - } - }; - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.FIELD) - public @interface Exclude {} - - public static final Gson GSON = new GsonBuilder() - .setPrettyPrinting() - .addSerializationExclusionStrategy(EXCLUSION_STRATEGY) - .create(); -} - - diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/HypixelEventUtil.java b/src/main/java/com/github/kd_gaming1/packcore/util/HypixelEventUtil.java deleted file mode 100644 index 328f5f7..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/HypixelEventUtil.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.github.kd_gaming1.packcore.util; - -import com.github.kd_gaming1.packcore.PackCore; -import net.azureaaron.hmapi.events.HypixelPacketEvents; -import net.azureaaron.hmapi.network.packet.s2c.HypixelS2CPacket; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; - -import java.util.concurrent.atomic.AtomicBoolean; - -public final class HypixelEventUtil { - private static final AtomicBoolean helloPacketReceived = new AtomicBoolean(false); - - // Call once on client init - public static void init() { - // Register HM-api HELLO event listener - HypixelPacketEvents.HELLO.register((HypixelS2CPacket packet) -> { - helloPacketReceived.set(true); - PackCore.LOGGER.info("HELLO packet received — connected to Hypixel!"); - }); - - // Reset on disconnect so state reflects current connection - ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> reset()); - } - - public static boolean isHelloPacketReceived() { - return helloPacketReceived.get(); - } - - public static void reset() { - helloPacketReceived.set(false); - PackCore.LOGGER.info("Hello packet state reset. Not connected to Hypixel."); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/JvmArgs.java b/src/main/java/com/github/kd_gaming1/packcore/util/JvmArgs.java new file mode 100644 index 0000000..3844894 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/util/JvmArgs.java @@ -0,0 +1,152 @@ +package com.github.kd_gaming1.packcore.util; + +import java.lang.management.ManagementFactory; +import java.util.Locale; + +public final class JvmArgs { + + private JvmArgs() {} + + // --------------------------------------------------------------- + // Stack-size check + // --------------------------------------------------------------- + + public static boolean hasXssAtLeast(long thresholdBytes) { + return ManagementFactory.getRuntimeMXBean().getInputArguments() + .stream() + .anyMatch(arg -> xssBytes(arg) >= thresholdBytes); + } + + private static long xssBytes(String arg) { + if (arg == null || !arg.startsWith("-Xss")) return -1L; + + String val = arg.substring(4).trim().toLowerCase(Locale.ROOT); + if (val.isEmpty()) return -1L; + + try { + char suffix = val.charAt(val.length() - 1); + long multiplier = 1L; + String number = val; + + if (suffix == 'k' || suffix == 'm' || suffix == 'g') { + number = val.substring(0, val.length() - 1); + multiplier = switch (suffix) { + case 'k' -> 1024L; + case 'm' -> 1024L * 1024L; + case 'g' -> 1024L * 1024L * 1024L; + default -> 1L; + }; + } + + return Long.parseLong(number) * multiplier; + } catch (NumberFormatException ignored) { + return -1L; + } + } + + // --------------------------------------------------------------- + // Launcher detection — best-effort, used for hint messages only + // --------------------------------------------------------------- + + public enum Launcher { + PRISM_POLYMC, + CURSEFORGE, + ATLAUNCHER, + MODRINTH, + OFFICIAL, + UNKNOWN; + + /** Human-readable display name. */ + public String displayName() { + return switch (this) { + case PRISM_POLYMC -> "Prism / PolyMC"; + case CURSEFORGE -> "CurseForge"; + case ATLAUNCHER -> "ATLauncher"; + case MODRINTH -> "Modrinth App"; + case OFFICIAL -> "Official Launcher"; + case UNKNOWN -> "your launcher"; + }; + } + } + + /** + * Attempts to identify the launcher by inspecting system properties and + * JVM arguments set by known launchers. + */ + public static Launcher detectLauncher() { + // Prism / PolyMC set a dedicated system property + String prism = System.getProperty("org.prismlauncher.instance.name"); + if (prism != null) return Launcher.PRISM_POLYMC; + + // ATLauncher sets this property + String atl = System.getProperty("atlauncher.instance.name"); + if (atl != null) return Launcher.ATLAUNCHER; + + // CurseForge passes a recognisable classpath or agent path + String classPath = System.getProperty("java.class.path", ""); + if (classPath.toLowerCase(Locale.ROOT).contains("curseforge")) return Launcher.CURSEFORGE; + + // Modrinth App (theseus) sets launcher metadata in the classpath too + if (classPath.toLowerCase(Locale.ROOT).contains("modrinth") + || classPath.toLowerCase(Locale.ROOT).contains("theseus")) { + return Launcher.MODRINTH; + } + + // Official launcher leaves a distinctive version-type argument + boolean hasOfficialArg = ManagementFactory.getRuntimeMXBean().getInputArguments() + .stream() + .anyMatch(a -> a.contains("minecraft-launcher") || a.contains("launcher_name")); + if (hasOfficialArg) return Launcher.OFFICIAL; + + return Launcher.UNKNOWN; + } + + /** + * Returns step-by-step instructions for adding {@code -Xss4M} in the + * detected (or provided) launcher. + */ + public static String xss4MInstructions(Launcher launcher) { + return switch (launcher) { + case PRISM_POLYMC -> + "In Prism / PolyMC:\n" + + " 1. Right-click your instance → Edit\n" + + " 2. Go to the \"Settings\" tab → \"Java\" section\n" + + " 3. Add -Xss4M to the \"JVM Arguments\" field\n" + + " 4. Click OK and relaunch"; + case CURSEFORGE -> + "In CurseForge:\n" + + " 1. Open the profile → click the three-dot menu → Profile Options\n" + + " 2. Enable \"Additional Java Arguments\"\n" + + " 3. Add -Xss4M to the field\n" + + " 4. Save and relaunch"; + case ATLAUNCHER -> + "In ATLauncher:\n" + + " 1. Open Settings → Java/Minecraft tab\n" + + " 2. Add -Xss4M to \"Extra Java Parameters\"\n" + + " 3. Save and relaunch"; + case MODRINTH -> + "In the Modrinth App:\n" + + " 1. Click the instance → setting icon in the top right corner\n" + + " 2. Under \"Java Settings\", find \"Java arguments\"\n" + + " 3. Add -Xss4M\n" + + " 4. Relaunch"; + case OFFICIAL -> + "In the Official Launcher:\n" + + " 1. Open Installations → hover your profile → click the pencil icon\n" + + " 2. Click \"More Options\" at the bottom\n" + + " 3. In \"JVM Arguments\", add -Xss4M before the existing flags\n" + + " 4. Save and relaunch"; + default -> + "To add the JVM argument:\n" + + " 1. Open your launcher and find the instance/profile settings\n" + + " 2. Look for a \"JVM Arguments\" or \"Java Arguments\" field\n" + + " 3. Add -Xss4M to the field\n" + + " 4. Save and relaunch"; + }; + } + + /** Convenience overload — auto-detects the launcher. */ + public static String xss4MInstructions() { + return xss4MInstructions(detectLauncher()); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/RamWarningHelper.java b/src/main/java/com/github/kd_gaming1/packcore/util/RamWarningHelper.java new file mode 100644 index 0000000..0e6a999 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/util/RamWarningHelper.java @@ -0,0 +1,49 @@ +package com.github.kd_gaming1.packcore.util; + +import com.github.kd_gaming1.packcore.config.PackCoreConfig; +import com.github.kd_gaming1.packcore.gui.util.ToastHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows a low-RAM toast once on the main menu and once on the first world join, + * if the JVM max heap is under {@value RAM_THRESHOLD_GB}GB. + */ +public final class RamWarningHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/RamWarning"); + + private static final long RAM_THRESHOLD_BYTES = 3L * 1024 * 1024 * 1024; + private static final int RAM_THRESHOLD_GB = 3; + + private static boolean lowRam = false; + private static boolean shownMainMenu = false; + private static boolean shownInGame = false; + + private RamWarningHelper() {} + + /** Call once in onInitializeClient to evaluate RAM. */ + public static void init() { + long maxRam = Runtime.getRuntime().maxMemory(); + if (maxRam < RAM_THRESHOLD_BYTES) { + lowRam = true; + LOGGER.warn("Low RAM detected: {}MB allocated ({}GB+ recommended)", maxRam / (1024 * 1024), RAM_THRESHOLD_GB); + } + } + + /** Call when the main menu is first shown. */ + public static void onMainMenu() { + if (lowRam && !shownMainMenu && PackCoreConfig.showRamWarningToast) { + shownMainMenu = true; + ToastHelper.showLowRam(); + } + } + + /** Call when the player first joins a world or server. */ + public static void onWorldJoin() { + if (lowRam && !shownInGame && PackCoreConfig.showRamWarningToast) { + shownInGame = true; + ToastHelper.showLowRam(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/ScreenResolution.java b/src/main/java/com/github/kd_gaming1/packcore/util/ScreenResolution.java new file mode 100644 index 0000000..3960480 --- /dev/null +++ b/src/main/java/com/github/kd_gaming1/packcore/util/ScreenResolution.java @@ -0,0 +1,45 @@ +package com.github.kd_gaming1.packcore.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; + +/** + * Detects the primary screen resolution using AWT. + */ +public class ScreenResolution { + + private static final Logger LOGGER = LoggerFactory.getLogger("PackCore/ScreenResolution"); + + public static final int FALLBACK_WIDTH = 1920; + public static final int FALLBACK_HEIGHT = 1080; + + private ScreenResolution() {} + + public record ScreenSize(int width, int height) {} + + public static ScreenSize detect() { + try { + Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); + + if (screen.width <= 0 || screen.height <= 0) { + LOGGER.warn("AWT returned invalid screen size ({}x{}), using fallback", screen.width, screen.height); + return fallback(); + } + + LOGGER.info("Detected screen resolution: {}x{}", screen.width, screen.height); + return new ScreenSize(screen.width, screen.height); + + } catch (HeadlessException e) { + // Should not happen on a client — headless environment detected + LOGGER.warn("Headless environment detected, using fallback resolution"); + return fallback(); + } + } + + private static ScreenSize fallback() { + LOGGER.info("Using fallback resolution: {}x{}", FALLBACK_WIDTH, FALLBACK_HEIGHT); + return new ScreenSize(FALLBACK_WIDTH, FALLBACK_HEIGHT); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/help/guide/GuideInfo.java b/src/main/java/com/github/kd_gaming1/packcore/util/help/guide/GuideInfo.java deleted file mode 100644 index 5d69b77..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/help/guide/GuideInfo.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.kd_gaming1.packcore.util.help.guide; - -import java.nio.file.Path; - -public class GuideInfo { - private final String title; - private final String preview; - private final Path filePath; - private String fullContent; - - public GuideInfo(String title, String preview, Path filePath) { - this.title = title; - this.preview = preview; - this.filePath = filePath; - this.fullContent = null; // Lazy loaded - } - - public String getTitle() { - return title; - } - - public String getPreview() { - return preview; - } - - public Path getFilePath() { - return filePath; - } - - public String getFullContent() { - return fullContent; - } - - public void setFullContent(String fullContent) { - this.fullContent = fullContent; - } - - public boolean isContentLoaded() { - return fullContent != null; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/help/guide/GuideService.java b/src/main/java/com/github/kd_gaming1/packcore/util/help/guide/GuideService.java deleted file mode 100644 index 74af309..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/help/guide/GuideService.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.github.kd_gaming1.packcore.util.help.guide; - -import com.github.kd_gaming1.packcore.PackCore; -import net.fabricmc.loader.api.FabricLoader; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; - -public class GuideService { - private static final String GUIDES_FOLDER = "packcore/guides"; - private static final ConcurrentHashMap GUIDE_CACHE = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap CONTENT_CACHE = new ConcurrentHashMap<>(); - - /** - * Gets the guides directory path - */ - public static Path getGuidesDirectory() { - return FabricLoader.getInstance().getGameDir().resolve(GUIDES_FOLDER); - } - - /** - * Loads all available guides from the guides folder - */ - public static List loadAvailableGuides() { - List guides = new ArrayList<>(); - Path guidesDir = getGuidesDirectory(); - - if (!Files.exists(guidesDir)) { - PackCore.LOGGER.warn("Guides directory doesn't exist: {}", guidesDir); - return guides; - } - - try (Stream files = Files.list(guidesDir)) { - files.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".md")) - .forEach(path -> { - try { - GuideInfo guide = loadGuideInfo(path); - if (guide != null) { - guides.add(guide); - GUIDE_CACHE.put(path.getFileName().toString(), guide); - } - } catch (IOException e) { - PackCore.LOGGER.error("Failed to load guide: {}", path, e); - } - }); - } catch (IOException e) { - PackCore.LOGGER.error("Failed to read guides directory: {}", guidesDir, e); - } - - return guides; - } - - /** - * Loads guide information (title and preview) from a markdown file - */ - private static GuideInfo loadGuideInfo(Path filePath) throws IOException { - List lines = Files.readAllLines(filePath); - - if (lines.isEmpty()) { - return null; - } - - // Extract title (first line, remove markdown heading syntax) - String title = lines.getFirst().replaceFirst("^#+\\s*", "").trim(); - - if (title.isEmpty()) { - title = filePath.getFileName().toString().replace(".md", ""); - } - - // Extract preview - build up to 3 lines of visible text - StringBuilder previewBuilder = new StringBuilder(); - int currentLine = 0; - int maxLines = 3; - - for (int i = 1; i < lines.size() && currentLine < maxLines; i++) { - String line = lines.get(i).trim(); - - // Skip empty lines and headers - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - - // Remove markdown formatting for cleaner preview - String cleanLine = line.replaceAll("^[>\\-*+]\\s*", "") // Remove blockquote and list markers - .replaceAll("\\*\\*(.*?)\\*\\*", "$1") // Remove bold - .replaceAll("\\*(.*?)\\*", "$1") // Remove italic - .replaceAll("\\[([^]]+)]\\([^)]+\\)", "$1") // Remove links, keep text - .replaceAll("`([^`]+)`", "$1"); // Remove inline code - - if (!cleanLine.isEmpty()) { - if (currentLine > 0) { - previewBuilder.append(" "); - } - previewBuilder.append(cleanLine); - currentLine++; - } - } - - String preview = getString(previewBuilder, currentLine, maxLines); - - return new GuideInfo(title, preview, filePath); - } - - private static @NotNull String getString(StringBuilder previewBuilder, int currentLine, int maxLines) { - String preview = previewBuilder.toString(); - - // Truncate if too long and add ellipsis - if (preview.length() > 200) { - int lastSpace = preview.lastIndexOf(' ', 197); - if (lastSpace > 150) { - preview = preview.substring(0, lastSpace) + "..."; - } else { - preview = preview.substring(0, 197) + "..."; - } - } else if (currentLine == maxLines && !preview.endsWith("...")) { - // If we hit the line limit, add ellipsis even if under character limit - preview += "..."; - } - return preview; - } - - /** - * Loads the full content of a guide (with caching) - */ - public static void loadGuideContent(GuideInfo guide) { - String fileName = guide.getFilePath().getFileName().toString(); - - // Check cache first - String cachedContent = CONTENT_CACHE.get(fileName); - if (cachedContent != null) { - guide.setFullContent(cachedContent); - return; - } - - // Load from file - try { - String content = Files.readString(guide.getFilePath()); - CONTENT_CACHE.put(fileName, content); - guide.setFullContent(content); - } catch (IOException e) { - PackCore.LOGGER.error("Failed to load guide content: {}", guide.getFilePath(), e); - } - } - - /** - * Clears the guide cache (useful for development or manual refresh) - */ - public static void clearCache() { - GUIDE_CACHE.clear(); - CONTENT_CACHE.clear(); - } - - /** - * Ensures the guides directory exists - */ - public static void ensureGuidesDirectory() { - Path guidesDir = getGuidesDirectory(); - try { - Files.createDirectories(guidesDir); - } catch (IOException e) { - PackCore.LOGGER.error("Failed to create guides directory: {}", guidesDir, e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/io/file/ExclusionPatterns.java b/src/main/java/com/github/kd_gaming1/packcore/util/io/file/ExclusionPatterns.java deleted file mode 100644 index 854a63c..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/io/file/ExclusionPatterns.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.github.kd_gaming1.packcore.util.io.file; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Locale; -import java.util.Set; - -/** - * Centralized exclusion patterns for file operations across the mod. - * Prevents large mod cache/storage folders from slowing down backups and exports. - */ -public final class ExclusionPatterns { - - private ExclusionPatterns() {} // Utility class - - /** Mod storage/cache folders that should be excluded from backups and exports */ - public static final Set EXCLUDED_CONFIG_SUBFOLDERS = Set.of( - "firmament/profiles", - "firmament/storage", - "skyhanni/backup", - "skyhanni/repo", - "skyhanni/logs", - "skyblocker/item-repo", - "skyocean/data", - "skyblocktweaks/repo", - "skyblocker/reward-trackers", - "skyblocker/garden_plots", - "skyblocker/config_backups", - "skyblocker/backpack-preview" - ); - - /** - * Generic heavy folder names often used for caches/logs/repos/backups. - * Applied ONLY under config/. - */ - private static final Set GENERIC_HEAVY_FOLDER_NAMES = Set.of( - "cache", "caches", - "log", "logs", - "backup", "backups", - "repo", "repos", "repository", "repositories", - "storage", "profiles", - "downloads" - ); - - /** - * If a file under config/ is huge, it's very likely a cache/database rather than a setting. - * This protects users on slow disks from multi-minute backups. - */ - private static final long MAX_CONFIG_FILE_SIZE_BYTES = 50L * 1024L * 1024L; // 50 MB - - /** Folders to hide from the file tree UI */ - public static final Set HIDDEN_FOLDERS = Set.of( - "packcore", "logs", "crash-reports", "screenshots", - ".git", ".minecraft", "saves", "assets", "mods", ".firmament" - ); - - /** - * Check if a path should be excluded during backup/export operations. - * @param relativePath Path relative to game directory (use forward slashes) - */ - public static boolean shouldExclude(String relativePath) { - String normalized = relativePath.replace("\\", "/"); - String lower = normalized.toLowerCase(Locale.ROOT); - - // Only apply these exclusions inside config/ - if (lower.startsWith("config/")) { - // 1) Specific known-heavy subfolders - for (String excluded : EXCLUDED_CONFIG_SUBFOLDERS) { - String configPath = "config/" + excluded; - if (lower.equals(configPath) || lower.startsWith(configPath + "/")) { - return true; - } - } - - // 2) Generic heavy folder names anywhere under config/ - // Example: config/somemod/cache/** or config/somemod/profiles/** - String afterConfig = lower.substring("config/".length()); - String[] parts = afterConfig.split("/"); - for (String part : parts) { - if (GENERIC_HEAVY_FOLDER_NAMES.contains(part)) { - return true; - } - } - } - - return false; - } - - /** - * Check if a path should be excluded (Path variant). - * @param basePath The base directory (e.g., gameDir) - * @param fullPath The full path to check - */ - public static boolean shouldExclude(Path basePath, Path fullPath) { - String rel = basePath.relativize(fullPath).toString(); - if (shouldExclude(rel)) { - return true; - } - - // Extra guard: skip huge files under config/ - try { - String lower = rel.replace("\\", "/").toLowerCase(Locale.ROOT); - if (lower.startsWith("config/") && Files.isRegularFile(fullPath)) { - long size = Files.size(fullPath); - return size > MAX_CONFIG_FILE_SIZE_BYTES; - } - } catch (Exception ignored) { - // If we can't stat it, don't exclude based on size. - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/io/file/FileLayoutInitializer.java b/src/main/java/com/github/kd_gaming1/packcore/util/io/file/FileLayoutInitializer.java deleted file mode 100644 index 965fb33..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/io/file/FileLayoutInitializer.java +++ /dev/null @@ -1,397 +0,0 @@ -package com.github.kd_gaming1.packcore.util.io.file; - -import net.fabricmc.loader.api.FabricLoader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Map; - -public class FileLayoutInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger(FileLayoutInitializer.class); - private static final Path RUN_DIR = FabricLoader.getInstance().getGameDir(); - - private static boolean hasInitialized = false; - - /** - * Initialize all required directories and default files. - * Should be called during pre-launch on first startup. - */ - public static void initializeFileStructure() { - if (hasInitialized) { - LOGGER.info("File structure already initialized, skipping..."); - return; - } - - LOGGER.info("Initializing PackCore file structure..."); - - try { - // Create all required directories - createDirectories(); - - // Create default markdown files for in-game menus - createDefaultMarkdownFiles(); - - hasInitialized = true; - LOGGER.info("PackCore file structure initialization complete"); - - } catch (Exception e) { - LOGGER.error("Failed to initialize PackCore file structure", e); - } - } - - /** - * Create all required directories - */ - private static void createDirectories() throws IOException { - Map directories = Map.of( - "packcore/modpack_config/official_configs", "Official modpack configurations", - "packcore/modpack_config/custom_configs", "Custom modpack configurations", - "packcore/imports", "Import staging folder for config files", - "packcore/wizard_markdown_content", "Information and help markdown files", - "packcore/guides", "User guides and documentation" - ); - - for (Map.Entry entry : directories.entrySet()) { - Path dirPath = RUN_DIR.resolve(entry.getKey()); - Files.createDirectories(dirPath); - LOGGER.info("Created directory: {} - {}", dirPath, entry.getValue()); - } - - // Create a README in the imports folder - createImportsReadme(); - } - - /** - * Create default markdown files with instructions - */ - private static void createDefaultMarkdownFiles() { - // Guide files - createMarkdownFile("packcore/guides", "Getting Started.md", getGettingStartedContent()); - createMarkdownFile("packcore/guides", "FAQ.md", getFAQContent()); - createMarkdownFile("packcore/guides", "Troubleshooting.md", getTroubleshootingContent()); - - // Info help files - createMarkdownFile("packcore/wizard_markdown_content", "Welcome.md", getWelcomeContent()); - createMarkdownFile("packcore/wizard_markdown_content", "Optimisation.md", getOptimisationContent()); - createMarkdownFile("packcore/wizard_markdown_content", "ResourcePacks.md", getResourcePacksContent()); - createMarkdownFile("packcore/wizard_markdown_content", "UsefulInformation.md", getUsefulInformationContent()); - } - - /** - * Create markdown file with user instructions - */ - private static void createMarkdownFile(String directory, String fileName, String content) { - Path filePath = RUN_DIR.resolve(directory).resolve(fileName); - - if (Files.exists(filePath)) { - LOGGER.debug("Markdown file already exists, skipping: {}", filePath); - return; - } - - try { - String fullContent = content + "\n\n---\n\n" + - "> **📝 Edit this file:** Navigate to `" + directory + "/" + fileName + "` in your game directory to customize this content.\n" + - "> **🔄 Refresh:** Restart the game or reopen the menu to see your changes."; - - Files.writeString(filePath, fullContent, StandardOpenOption.CREATE_NEW); - LOGGER.info("Created markdown file with instructions: {}", filePath); - } catch (IOException e) { - LOGGER.error("Failed to create markdown file: {}", filePath, e); - } - } - - /** - * Check if a markdown file exists and provide fallback content - */ - public static String getMarkdownContentSafe(String directory, String fileName, String fallbackContent) { - Path filePath = RUN_DIR.resolve(directory).resolve(fileName); - - if (!Files.exists(filePath)) { - LOGGER.warn("Markdown file not found: {}, using fallback content", filePath); - return fallbackContent + "\n\n> **File not found:** Expected at `" + directory + "/" + fileName + "`"; - } - - try { - return Files.readString(filePath); - } catch (IOException e) { - LOGGER.error("Failed to read markdown file: {}", filePath, e); - return fallbackContent + "\n\n> **Error reading file:** " + e.getMessage(); - } - } - - // Default content methods for in-game help system - private static String getGettingStartedContent() { - return """ - # Getting Started with PackCore - - Welcome to PackCore! This guide will help you get familiar with the modpack. - - ## First Steps - - 1. **Apply a Configuration** - Use the config manager to apply optimized settings - 2. **Check Your Keybinds** - Press `ESC > Options > Controls` to see all mod keybinds - 3. **Explore the Interface** - Many mods add new UI elements and features - - ## Key Features - - - **Optimized Performance** - Pre-configured settings for smooth gameplay - - **Enhanced UI** - Improved interfaces and helpful overlays - - **Quality of Life** - Many small improvements to make the game more enjoyable - - ## Need Help? - - - Press `F1` in-game for contextual help - - Check the other guides in this menu - - Join our Discord community for live support - """; - } - - private static String getFAQContent() { - return """ - # Frequently Asked Questions - - ## General Questions - - **Q: How do I reset my settings?** - A: Delete the `packcore` folder in your game directory and restart. - - **Q: Can I add my own mods?** - A: Yes, but be careful about compatibility. Check mod requirements first. - - **Q: Why is my performance poor?** - A: Try applying a lower resolution configuration profile. - - ## Technical Issues - - **Q: The game crashes on startup** - A: Check your Java version and allocated memory. See the Troubleshooting guide. - - **Q: Mods aren't working properly** - A: Try pressing F3+T to reload resources, or restart the game. - - ## Getting More Help - - If your question isn't answered here, check the Troubleshooting guide or join our Discord. - """; - } - - private static String getTroubleshootingContent() { - return """ - # Troubleshooting Guide - - ## Common Issues and Solutions - - ### Game Won't Start - - 1. **Check Java Version** - Ensure you're using Java 21 or newer - 2. **Memory Allocation** - Allocate at least 4GB RAM to Minecraft - 3. **Mod Conflicts** - Remove recently added mods one by one - - ### Performance Issues - - 1. **Lower Settings** - Reduce render distance and graphics quality - 2. **Update Drivers** - Ensure your graphics drivers are current - 3. **Close Other Programs** - Free up system resources - - ### Visual Glitches - - 1. **Reload Resources** - Press F3+T in-game - 2. **Check Resource Packs** - Disable resource packs temporarily - 3. **Update Graphics Drivers** - Especially important for shader support - - ## Still Need Help? - - 1. **Check Logs** - Look in `.minecraft/logs/latest.log` for error messages - 2. **Discord Support** - Join our community for live help - 3. **GitHub Issues** - Report bugs on our GitHub repository - - ## System Requirements - - - **Java:** 21 or newer - - **RAM:** 6GB minimum, 8GB recommended - - **Graphics:** OpenGL 3.2 support required - """; - } - - private static String getWelcomeContent() { - return """ - # 🎮 Welcome to PackCore! - - Thank you for choosing **PackCore**! This modpack provides an optimized experience for your gameplay. - - ## 🚀 Key Features - - - **🔍 Automatic Configuration** - Smart config detection and application on first launch - - **💡 Optimized Performance** - Pre-configured settings for smooth gameplay - - **⚙ Config Manager** - Import, export, and apply configurations in-game - - **🎯 Resolution Profiles** - Optimized settings for different screen resolutions - - ## 📋 Getting Started - - 1. **First Launch** - The mod automatically detects your screen resolution and applies the best config - 2. **In-Game Config Manager** - Access it from the main menu or ESC menu to manage configurations - 3. **Import/Export** - Share configurations with friends or create your own - - --- - - ## 💡 About Configurations - - Each configuration package contains: - - **Optimized game settings** for your resolution - - **Mod interface layouts** positioned for best visibility - - **Performance tweaks** to ensure smooth gameplay - - **Resource pack selections** that complement your setup - - > **Need help?** Check the other guides in this menu or join our Discord community! - """; - } - - private static String getOptimisationContent() { - return """ - # ⚡ Optimisation Tips - - Get the most out of your modpack with these optimization tips! - - ## Video Settings - - ## Performance Mods - - The modpack includes several performance-enhancing mods. Make sure they're properly configured in their respective settings. - - ## Memory Allocation - - - **Recommended:** 6-8GB for optimal performance - - **Minimum:** 4GB for basic gameplay - - Configure in your launcher settings - - """; - } - - private static String getResourcePacksContent() { - return """ - # 🎨 Resource Pack Selection - - Choose the visual style that best fits your preferences! - - ## Available Packs: - - ### **Hypixel Plus** - A clean, mostly vanilla pack designed for Hypixel modes like SkyBlock. Updates items and icons for better clarity without changing the overall Minecraft feel. - - ### **FurfSky Overlay** - A comprehensive resource pack for Hypixel SkyBlock, offering textures for nearly every item in the game with special styled retextures for items only. - - ### **FurfSky Full** - A comprehensive resource pack for Hypixel SkyBlock with full retextures for both items and menus in a special artistic style. - - ### **SkyBlock Dark UI** - A sleek, dark-themed resource pack for Hypixel SkyBlock, enhancing all GUI elements including mod interfaces with a modern aesthetic. - - ### **Defrosted** - Icy-themed 16x pack for Minecraft 1.21.5 with a frosty blue aesthetic across items and menus, maintaining minimalist clarity. - - ### **Looshy** - A smooth, vanilla-like 16x resource pack with clean updates and subtle charm that keeps Minecraft's original style while offering refined textures. - - ## 💡 Tips: - - - You can select multiple packs - they'll be applied in order - - Resource packs can be changed later in the game settings - - Some packs work better together than others - """; - } - - private static String getUsefulInformationContent() { - return """ - # ℹ Useful Information - - ## Config Management - - - **Import Config:** Import configurations from zip files - - **Export Config:** Create and share your own configurations - - **Apply Config:** Switch between different configuration profiles - - ## Backup & Restore - - Backups are automatically created before applying new configurations. - Find them in: `packcore/backups/` - - ## Community - - Join our Discord for support, updates, and to share your configurations! - """; - } - - /** - * Create a helpful README file in the imports folder - */ - private static void createImportsReadme() { - Path readmePath = RUN_DIR.resolve("packcore/imports/README.txt"); - - if (Files.exists(readmePath)) { - return; - } - - try { - String content = """ - ═══════════════════════════════════════════════════════════ - PackCore Configuration Imports Folder - ═══════════════════════════════════════════════════════════ - - 📂 How to Use This Folder: - - 1. Place your configuration .zip files here - 2. Open the game and go to: Config Manager > Import - 3. Click "Refresh" to see your files - 4. Select a file to preview and import it - - ✅ Valid Config Files Must: - - Be .zip archives - - Contain packcore_metadata.json - - NOT contain any .jar files (configs only!) - - ⚠️ Important Notes: - - Files are automatically validated before import - - You can preview files before importing - - Invalid files will be marked with an error - - Successfully imported files can be auto-deleted - - 📋 What Gets Imported: - - Game settings (options.txt) - - Mod configurations - - Resource pack selections - - Keybindings - - UI layouts - - 💡 Tip: Always export your current config before importing - a new one, so you can revert if needed! - - ═══════════════════════════════════════════════════════════ - """; - - Files.writeString(readmePath, content); - LOGGER.info("Created imports README: {}", readmePath); - } catch (IOException e) { - LOGGER.error("Failed to create imports README", e); - } - } - - /** - * Force re-initialization (useful for development) - */ - public static void forceReinitialize() { - hasInitialized = false; - initializeFileStructure(); - } - - /** - * Check if initialization has been completed - */ - public static boolean isInitialized() { - return hasInitialized; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/io/file/FileUtils.java b/src/main/java/com/github/kd_gaming1/packcore/util/io/file/FileUtils.java deleted file mode 100644 index 68b3949..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/io/file/FileUtils.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.github.kd_gaming1.packcore.util.io.file; - -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Comparator; -import java.util.stream.Stream; - -/** - * Low-level file operations used by higher-level backup logic. - */ -public class FileUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(FileUtils.class); - - /** - * Recursively copies a directory and its contents to a target location. - * - * @param source The source directory to copy. - * @param target The target directory. - * @throws IOException If an I/O error occurs during copying. - */ - public static void copyDirectory(Path source, Path target) throws IOException { - Files.walkFileTree(source, new SimpleFileVisitor() { - @Override - public @NotNull FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) - throws IOException { - Path targetDir = target.resolve(source.relativize(dir)); - Files.createDirectories(targetDir); - return FileVisitResult.CONTINUE; - } - - @Override - public @NotNull FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Path targetFile = target.resolve(source.relativize(file)); - Files.copy(file, targetFile, - StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.COPY_ATTRIBUTES); - return FileVisitResult.CONTINUE; - } - - @Override - public @NotNull FileVisitResult visitFileFailed(Path file, IOException exc) { - LOGGER.warn("Failed to copy file: {} - {}", file, exc.getMessage()); - return FileVisitResult.CONTINUE; - } - }); - } - - /** - * Recursively copies a directory while excluding specified subfolders. - * Uses the centralized exclusion patterns. - * - * @param source The source directory to copy. - * @param target The target directory. - * @param gameDir The game directory (for calculating relative paths). - * @throws IOException If an I/O error occurs during copying. - */ - public static void copyDirectoryWithExclusions(Path source, Path target, Path gameDir) throws IOException { - Files.walkFileTree(source, new SimpleFileVisitor<>() { - @Override - public @NotNull FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) - throws IOException { - // Check if this directory should be excluded - if (ExclusionPatterns.shouldExclude(gameDir, dir)) { - LOGGER.debug("Skipping excluded folder: {}", gameDir.relativize(dir)); - return FileVisitResult.SKIP_SUBTREE; - } - - Path targetDir = target.resolve(source.relativize(dir)); - Files.createDirectories(targetDir); - return FileVisitResult.CONTINUE; - } - - @Override - public @NotNull FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Path targetFile = target.resolve(source.relativize(file)); - Files.copy(file, targetFile, - StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.COPY_ATTRIBUTES); - return FileVisitResult.CONTINUE; - } - - @Override - public @NotNull FileVisitResult visitFileFailed(Path file, IOException exc) { - LOGGER.warn("Failed to copy file: {} - {}", file, exc.getMessage()); - return FileVisitResult.CONTINUE; - } - }); - } - - /** - * Recursively deletes a directory and all its contents. Ignored when directory does not exist. - * - * @param directory The directory to delete. - */ - public static void deleteDirectory(Path directory) { - if (!Files.exists(directory)) { - return; - } - - try (Stream paths = Files.walk(directory)) { - paths.sorted(Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - LOGGER.debug("Could not delete: {}", path); - } - }); - } catch (IOException e) { - LOGGER.warn("Could not fully delete directory: {}", directory); - } - } - - /** - * Calculates the total size (in bytes) of a file or directory. - * - * @param path The file or directory to measure. - * @return The total size in bytes, or 0 if the size could not be determined. - */ - public static long calculateSize(Path path) { - try { - if (Files.isRegularFile(path)) { - return Files.size(path); - } else if (Files.isDirectory(path)) { - try (Stream paths = Files.walk(path)) { - return paths - .filter(Files::isRegularFile) - .mapToLong(p -> { - try { - return Files.size(p); - } catch (IOException e) { - return 0L; - } - }) - .sum(); - } - } - } catch (IOException e) { - LOGGER.debug("Could not calculate size for: {}", path); - } - return 0L; - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/UnzipAsyncTask.java b/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/UnzipAsyncTask.java deleted file mode 100644 index b98c8b5..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/UnzipAsyncTask.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.github.kd_gaming1.packcore.util.io.zip; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class UnzipAsyncTask { - - private static final Logger LOGGER = LoggerFactory.getLogger(UnzipAsyncTask.class); - private static final int BUFFER_SIZE = 32768; // 32KB buffer - private static final ExecutorService UNZIP_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread thread = new Thread(r); - thread.setName("AsyncUnzip-" + thread.threadId()); - thread.setDaemon(true); - return thread; - }); - - public interface ProgressCallback { - void onProgress(long bytesProcessed, long totalBytes, int percentage); - } - - public CompletableFuture unzipAsync(String zipFilePath, String destDir, - ProgressCallback progressCallback) { - return CompletableFuture.runAsync(() -> { - try { - unzip(zipFilePath, destDir, progressCallback); - } catch (IOException e) { - throw new RuntimeException("Failed to unzip file", e); - } - }, UNZIP_EXECUTOR); - } - - public void unzip(String zipFilePath, String destDir, - ProgressCallback progressCallback) throws IOException { - - Path destPath = Path.of(destDir); - Files.createDirectories(destPath); // fast - - // Optimization: Open ZipFile ONCE for both calculation and extraction - try (ZipFile zipFile = new ZipFile(zipFilePath)) { - - // 1. Fast Size Calculation - long totalSize = 0; - var sizeEnum = zipFile.entries(); - while (sizeEnum.hasMoreElements()) { - ZipEntry e = sizeEnum.nextElement(); - if (!e.isDirectory()) totalSize += e.getSize(); - } - - // 2. Extraction - AtomicLong processedBytes = new AtomicLong(0); - int lastReportedProgress = -1; - byte[] buffer = new byte[BUFFER_SIZE]; - - var entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - Path entryPath = destPath.resolve(entry.getName()).normalize(); - - if (!entryPath.normalize().toAbsolutePath().startsWith(destPath.toAbsolutePath())) { - LOGGER.warn("Zip entry outside destination: {}", entry.getName()); - continue; - } - - if (entry.isDirectory()) { - Files.createDirectories(entryPath); - } else { - Files.createDirectories(entryPath.getParent()); - - try (InputStream is = zipFile.getInputStream(entry); - BufferedInputStream bis = new BufferedInputStream(is, BUFFER_SIZE); - OutputStream os = Files.newOutputStream(entryPath); - BufferedOutputStream bos = new BufferedOutputStream(os, BUFFER_SIZE)) { - - int bytesRead; - while ((bytesRead = bis.read(buffer)) > 0) { - bos.write(buffer, 0, bytesRead); - long processed = processedBytes.addAndGet(bytesRead); - - if (progressCallback != null && totalSize > 0) { - int currentProgress = (int) ((processed * 100) / totalSize); - - // Optimization: Only callback if percent changes OR every 5MB - // Prevents spamming on small files, fixes "stuck" feeling - if (currentProgress != lastReportedProgress) { - lastReportedProgress = currentProgress; - progressCallback.onProgress(processed, totalSize, currentProgress); - } - } - } - } - - // Handle file times... - if (entry.getTime() != -1) { - try { - Files.setLastModifiedTime(entryPath, - java.nio.file.attribute.FileTime.fromMillis(entry.getTime())); - } catch (IOException e) { - LOGGER.debug("Failed to set file time for {}: {}", entryPath, e.getMessage()); - } - } - } - } - - if (progressCallback != null) { - progressCallback.onProgress(processedBytes.get(), totalSize, 100); - } - } - } - - public static void shutdown() { - UNZIP_EXECUTOR.shutdown(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/UnzipService.java b/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/UnzipService.java deleted file mode 100644 index 5d209d0..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/UnzipService.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.kd_gaming1.packcore.util.io.zip; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class UnzipService { - - private static final Logger LOGGER = LoggerFactory.getLogger(UnzipService.class); - private static final int BUFFER_SIZE = 16384; // Larger buffer for better I/O performance - - public interface ProgressCallback { - void onProgress(long bytesProcessed, long totalBytes, int percentage); - } - - public void unzip(String zipFilePath, String destDir, ProgressCallback progressCallback) throws IOException { - File dir = new File(destDir); - if (!dir.exists()) dir.mkdirs(); - - // Use ZipFile instead of ZipInputStream - allows random access and pre-calculated sizes - try (ZipFile zipFile = new ZipFile(zipFilePath)) { - // Calculate total size from ZIP entries (no second pass needed) - long totalSize = zipFile.stream() - .filter(e -> !e.isDirectory()) - .mapToLong(ZipEntry::getSize) - .filter(size -> size > 0) - .sum(); - - long processedBytes = 0; - byte[] buffer = new byte[BUFFER_SIZE]; - - var entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String fileName = entry.getName(); - File newFile = new File(destDir + File.separator + fileName); - - if (entry.isDirectory()) { - newFile.mkdirs(); - } else { - LOGGER.debug("Unzipping: {}", fileName); - - File parent = newFile.getParentFile(); - if (parent != null && !parent.exists()) parent.mkdirs(); - - try (InputStream is = new BufferedInputStream(zipFile.getInputStream(entry), BUFFER_SIZE); - FileOutputStream fos = new FileOutputStream(newFile); - BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER_SIZE)) { - - int len; - while ((len = is.read(buffer)) > 0) { - bos.write(buffer, 0, len); - processedBytes += len; - - if (progressCallback != null && totalSize > 0) { - int percentage = (int) ((processedBytes * 100) / totalSize); - progressCallback.onProgress(processedBytes, totalSize, percentage); - } - } - } - } - } - } catch (IOException e) { - LOGGER.error("Failed to unzip files", e); - throw e; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/ZipAsyncTask.java b/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/ZipAsyncTask.java deleted file mode 100644 index 64f47d0..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/io/zip/ZipAsyncTask.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.github.kd_gaming1.packcore.util.io.zip; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public class ZipAsyncTask { - - private static final Logger LOGGER = LoggerFactory.getLogger(ZipAsyncTask.class); - private static final int BUFFER_SIZE = 32768; // Increased buffer (32KB) - private static final ExecutorService ZIP_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread thread = new Thread(r); - thread.setName("AsyncZip-" + thread.threadId()); - thread.setDaemon(true); - return thread; - }); - - public interface ProgressCallback { - void onProgress(long bytesProcessed, long totalBytes, int percentage); - } - - public CompletableFuture zipDirectoryAsync(File dir, String zipFilePath, - ProgressCallback progressCallback) { - return CompletableFuture.runAsync(() -> { - try { - zipDirectory(dir, zipFilePath, progressCallback); - } catch (IOException e) { - throw new RuntimeException("Failed to zip directory", e); - } - }, ZIP_EXECUTOR); - } - - public void zipDirectory(File dir, String zipFilePath, - ProgressCallback progressCallback) throws IOException { - Path basePath = dir.toPath(); - - // OPTIMIZATION: Removed the Files.walk pre-scan. - // It causes massive delays on large modpacks just to get a total size. - // We will report bytes processed, but total might be estimated or specific logic needed. - long estimatedTotal = -1; - - AtomicLong processedBytes = new AtomicLong(0); - // Use an array to hold state regarding last progress report - int[] lastReportedProgress = {-1}; - - try (FileOutputStream fos = new FileOutputStream(zipFilePath); - BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER_SIZE); - ZipOutputStream zos = new ZipOutputStream(bos)) { - - zos.setLevel(3); - byte[] buffer = new byte[BUFFER_SIZE]; - - Files.walkFileTree(basePath, new SimpleFileVisitor<>() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) - throws IOException { - Path relativePath = basePath.relativize(dir); - if (!relativePath.toString().isEmpty()) { - String entryName = relativePath.toString().replace(File.separatorChar, '/') + '/'; - ZipEntry dirEntry = new ZipEntry(entryName); - dirEntry.setTime(attrs.lastModifiedTime().toMillis()); - zos.putNextEntry(dirEntry); - zos.closeEntry(); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - String entryName = basePath.relativize(file).toString().replace(File.separatorChar, '/'); - ZipEntry fileEntry = new ZipEntry(entryName); - fileEntry.setTime(attrs.lastModifiedTime().toMillis()); - // Storing size is optional but good for unzip progress later - fileEntry.setSize(attrs.size()); - - zos.putNextEntry(fileEntry); - - try (InputStream is = Files.newInputStream(file, StandardOpenOption.READ)) { - int bytesRead; - while ((bytesRead = is.read(buffer)) > 0) { - zos.write(buffer, 0, bytesRead); - long processed = processedBytes.addAndGet(bytesRead); - - // Progress logic: If we don't know total, just tick every 1MB or similar - // Or relies on the callback handling unknown (-1) totals - if (progressCallback != null) { - // Rate limit updates to avoid spamming the UI thread (e.g. every 1MB) - if (processed / (1024 * 1024) != (processed - bytesRead) / (1024 * 1024)) { - progressCallback.onProgress(processed, estimatedTotal, -1); - } - } - } - } - zos.closeEntry(); - return FileVisitResult.CONTINUE; - } - }); - - // Final callback - if (progressCallback != null) { - progressCallback.onProgress(processedBytes.get(), processedBytes.get(), 100); - } - LOGGER.info("Successfully zipped {} to {}", dir.getAbsolutePath(), zipFilePath); - } - } - - public static void shutdown() { - ZIP_EXECUTOR.shutdown(); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/markdown/MarkdownLoadResult.java b/src/main/java/com/github/kd_gaming1/packcore/util/markdown/MarkdownLoadResult.java deleted file mode 100644 index 69cb32d..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/markdown/MarkdownLoadResult.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.kd_gaming1.packcore.util.markdown; - -public sealed interface MarkdownLoadResult permits MarkdownLoadResult.Success, MarkdownLoadResult.NotFound, MarkdownLoadResult.Error { - record Success(String content) implements MarkdownLoadResult {} - record NotFound(String fileName) implements MarkdownLoadResult {} - record Error(String fileName, String message, Throwable cause) implements MarkdownLoadResult {} -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/markdown/MarkdownService.java b/src/main/java/com/github/kd_gaming1/packcore/util/markdown/MarkdownService.java deleted file mode 100644 index 0e32361..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/markdown/MarkdownService.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.github.kd_gaming1.packcore.util.markdown; - -import net.fabricmc.loader.api.FabricLoader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -public final class MarkdownService { - private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownService.class); - - private final Path infoHelpDir; - private final String assetsNamespace; // e.g. "packcore" - - public MarkdownService() { - this(FabricLoader.getInstance().getGameDir().resolve("packcore/wizard_markdown_content"), "packcore"); - } - - public MarkdownService(Path infoHelpDir, String assetsNamespace) { - this.infoHelpDir = infoHelpDir; - this.assetsNamespace = assetsNamespace; - } - - public MarkdownLoadResult load(String fileName) { - final String sanitized = sanitize(fileName); - if (sanitized == null) { - return new MarkdownLoadResult.Error(fileName, "Invalid file name", null); - } - - // 1) Try filesystem (user-editable) - Path file = infoHelpDir.resolve(sanitized); - if (Files.isRegularFile(file)) { - try { - return new MarkdownLoadResult.Success(Files.readString(file)); - } catch (IOException e) { - LOGGER.error("Failed to read markdown file: {}", file, e); - return new MarkdownLoadResult.Error(sanitized, "Failed to read file", e); - } - } - - // 2) Try classpath defaults under assets//markdown/ - String resourcePath = "/assets/" + assetsNamespace + "/markdown/" + sanitized; - try (InputStream in = MarkdownService.class.getResourceAsStream(resourcePath)) { - if (in != null) { - byte[] bytes = in.readAllBytes(); - return new MarkdownLoadResult.Success(new String(bytes, StandardCharsets.UTF_8)); - } - } catch (IOException e) { - LOGGER.error("Failed to read classpath markdown: {}", resourcePath, e); - return new MarkdownLoadResult.Error(sanitized, "Failed to read classpath resource", e); - } - - // 3) Not found anywhere - LOGGER.warn("Markdown not found: {} (fs: {}, classpath: {})", sanitized, file, resourcePath); - return new MarkdownLoadResult.NotFound(sanitized); - } - - // Convenience for callers that want a fallback string - public String getOrDefault(String fileName, String fallback) { - var result = load(fileName); - return switch (result) { - case MarkdownLoadResult.Success s -> s.content(); - case MarkdownLoadResult.NotFound __ -> fallback; - case MarkdownLoadResult.Error e -> fallback; - }; - } - - // Only allow simple ".md" files; no separators or traversal - private static String sanitize(String name) { - if (name == null) return null; - String n = name.trim(); - if (n.isEmpty() || n.contains("..") || n.contains("/") || n.contains("\\")) - return null; - if (!n.endsWith(".md")) n = n + ".md"; - return n; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/task/ProgressListener.java b/src/main/java/com/github/kd_gaming1/packcore/util/task/ProgressListener.java deleted file mode 100644 index 22cf2ae..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/task/ProgressListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.kd_gaming1.packcore.util.task; - -/** - * Generic progress listener for long-running tasks (import/export/backup/zip). - */ -public interface ProgressListener { - /** - * Report progress. Percentage should be 0..100 when applicable, or -1 for indeterminate. - */ - void onProgress(String message, int percentage); - - /** - * Called when the task completes. If success is false, message should describe the failure. - */ - void onComplete(boolean success, String message); -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/update/UpdateResult.java b/src/main/java/com/github/kd_gaming1/packcore/util/update/UpdateResult.java deleted file mode 100644 index 4050304..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/update/UpdateResult.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.kd_gaming1.packcore.util.update; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; - -import static com.mojang.text2speech.Narrator.LOGGER; - -public class UpdateResult { - private final boolean success; - private final boolean updateAvailable; - private final String versionNumber; - private final String versionType; - private final String changelog; - private final String versionId; - private final String errorMessage; - - public UpdateResult(boolean updateAvailable, String versionNumber, - String versionType, String changelog, String versionId) { - this.success = true; - this.updateAvailable = updateAvailable; - this.versionNumber = versionNumber; - this.versionType = versionType; - this.changelog = changelog; - this.versionId = versionId; - this.errorMessage = null; - } - - private UpdateResult(String errorMessage) { - this.success = false; - this.updateAvailable = false; - this.versionNumber = null; - this.versionType = null; - this.changelog = null; - this.versionId = null; - this.errorMessage = errorMessage; - } - - public static UpdateResult error(String errorMessage) { - return new UpdateResult(errorMessage); - } - - public boolean isSuccess() { return success; } - public boolean isUpdateAvailable() { return updateAvailable; } - public String getVersionNumber() { return versionNumber; } - public String getVersionType() { return versionType; } - public String getChangelog() { return changelog; } - public String getVersionId() { return versionId; } - public String getErrorMessage() { return errorMessage; } - - public String getModrinthUrl() { - ModpackInfo info = PackCore.getModpackInfo(); - if (info == null) { - LOGGER.error("Update system not initialized properly. Cannot create URL"); - return null; - } - - if (info.isConfigurationValid()) { - LOGGER.warn("Configuration is invalid, cannot create URL for changelog: {}", - info.getValidationError()); - return null; - } - - return versionId != null - ? "https://modrinth.com/project/" + info.getModrinthProjectId() + "/version/" + versionId - : null; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/ModrinthClient.java b/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/ModrinthClient.java deleted file mode 100644 index 71ac608..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/ModrinthClient.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.github.kd_gaming1.packcore.util.update.modrinth; - -import com.github.kd_gaming1.packcore.PackCore; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -public class ModrinthClient { - private static final String API_BASE_URL = "https://api.modrinth.com/v2"; - private static final String USER_AGENT = "kdgaming0/packcore/2.0.0"; - private final HttpClient httpClient; - private final Gson gson; - - public ModrinthClient() { - this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .build(); - this.gson = new Gson(); - } - - // Get all versions for a project and find the latest suitable one - public ModrinthVersion getLatestVersion(String projectId, String updateChannel, String minecraftVersion) throws IOException, InterruptedException { - PackCore.LOGGER.info("Checking for updates - Project: {}, Channel: {}, MC Version: {}", - projectId, updateChannel, minecraftVersion); - - String url = API_BASE_URL + "/project/" + projectId + "/version"; - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("User-Agent", USER_AGENT) - .timeout(Duration.ofSeconds(30)) - .GET() - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - String error = "Modrinth API returned status: " + response.statusCode() + " for project: " + projectId; - PackCore.LOGGER.error(error); - throw new IOException(error); - } - - PackCore.LOGGER.debug("API Response: {}", response.body()); - return parseLatestVersion(response.body(), updateChannel, minecraftVersion); - } - - private ModrinthVersion parseLatestVersion(String jsonResponse, String updateChannel, String minecraftVersion) { - JsonArray versionsArray = gson.fromJson(jsonResponse, JsonArray.class); - List suitableVersions = new ArrayList<>(); - - for (int i = 0; i < versionsArray.size(); i++) { - JsonObject versionObj = versionsArray.get(i).getAsJsonObject(); - String versionType = versionObj.get("version_type").getAsString(); - JsonArray gameVersions = versionObj.getAsJsonArray("game_versions"); - - if (!isVersionTypeAllowed(versionType, updateChannel) || !supportsMinecraftVersion(gameVersions, minecraftVersion)) { - continue; - } - - ModrinthVersion version = new ModrinthVersion( - versionObj.get("version_number").getAsString(), - versionType, - versionObj.has("changelog") && !versionObj.get("changelog").isJsonNull() - ? versionObj.get("changelog").getAsString() : "No changelog available", - versionObj.get("id").getAsString(), - versionObj.get("date_published").getAsString() - ); - - suitableVersions.add(version); - } - - return suitableVersions.isEmpty() ? null : suitableVersions.getFirst(); - } - - private boolean isVersionTypeAllowed(String versionType, String updateChannel) { - return switch (updateChannel.toLowerCase()) { - case "alpha" -> true; - case "beta" -> versionType.equals("beta") || versionType.equals("release"); - case "release" -> versionType.equals("release"); - default -> false; - }; - } - - private boolean supportsMinecraftVersion(JsonArray gameVersions, String targetVersion) { - String baseVersion = targetVersion.endsWith("+") ? targetVersion.substring(0, targetVersion.length() - 1) : null; - - for (int i = 0; i < gameVersions.size(); i++) { - String version = gameVersions.get(i).getAsString(); - - if (version.equals(targetVersion) || (baseVersion != null && version.startsWith(baseVersion))) { - return true; - } - } - - return false; - } -} diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/ModrinthVersion.java b/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/ModrinthVersion.java deleted file mode 100644 index 43311b2..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/ModrinthVersion.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.kd_gaming1.packcore.util.update.modrinth; - -public record ModrinthVersion(String versionNumber, String versionType, String changelog, String versionId, - String datePublished) { -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/UpdateCache.java b/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/UpdateCache.java deleted file mode 100644 index 969c241..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/update/modrinth/UpdateCache.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.github.kd_gaming1.packcore.util.update.modrinth; - -import com.github.kd_gaming1.packcore.PackCore; -import com.github.kd_gaming1.packcore.ui.screen.title.SBEStyledTitleScreen; -import com.github.kd_gaming1.packcore.util.update.UpdateResult; -import com.github.kd_gaming1.packcore.modpack.ModpackInfo; - -import java.time.Instant; - -public class UpdateCache { - private String cachedVersionNumber; - private String cachedVersionType; - private String cachedChangelog; - private String cachedVersionId; - private boolean updateAvailable; - - private String cachedModrinthProjectId; - private String cachedUpdateChannel; - private String cachedCurrentVersion; - private String cachedMinecraftVersion; - - private Instant lastUpdateCheck; - private static final long CACHE_DURATION_MINUTES = 15; - - private final ModrinthClient apiClient; - - public UpdateCache() { - this.apiClient = new ModrinthClient(); - } - - // Main method - this is what other classes call - public UpdateResult checkForUpdates(ModpackInfo modpackInfo) { - // Validate configuration first - if (modpackInfo.isConfigurationValid()) { - String error = modpackInfo.getValidationError(); - PackCore.LOGGER.error("Invalid modpack configuration: {}", error); - return UpdateResult.error("Configuration error: " + error); - } - - if (isCacheValid(modpackInfo)) { - return createResultFromCache(); - } - - // Cache is invalid, fetch fresh data - try { - return fetchAndCacheUpdates(modpackInfo); - } catch (Exception e) { - return UpdateResult.error("Failed to check for updates: " + e.getMessage()); - } - } - - private boolean isCacheValid(ModpackInfo modpackInfo) { - return lastUpdateCheck != null && - java.time.Duration.between(lastUpdateCheck, Instant.now()).toMinutes() < CACHE_DURATION_MINUTES && - configMatches(modpackInfo); - } - - private boolean configMatches(ModpackInfo modpackInfo) { - return modpackInfo.getModrinthProjectId().equals(cachedModrinthProjectId) && - modpackInfo.getUpdateChannel().equals(cachedUpdateChannel) && - modpackInfo.getVersion().equals(cachedCurrentVersion) && - modpackInfo.getMinecraftVersion().equals(cachedMinecraftVersion); - } - - private UpdateResult createResultFromCache() { - return new UpdateResult(updateAvailable, cachedVersionNumber, - cachedVersionType, cachedChangelog, cachedVersionId); - } - - private UpdateResult fetchAndCacheUpdates(ModpackInfo modpackInfo) { - try { - ModrinthVersion latestVersion = apiClient.getLatestVersion( - modpackInfo.getModrinthProjectId(), - modpackInfo.getUpdateChannel(), - modpackInfo.getMinecraftVersion() - ); - - updateCacheConfig(modpackInfo); - lastUpdateCheck = Instant.now(); - - if (latestVersion == null) { - updateAvailable = false; - cachedVersionNumber = null; - cachedVersionType = null; - cachedChangelog = "No versions found matching your criteria"; - cachedVersionId = null; - - return new UpdateResult(false, null, null, - "No versions found matching your criteria", null); - } - - boolean isNewer = compareVersions(latestVersion.versionNumber(), modpackInfo.getVersion()) > 0; - - updateAvailable = isNewer; - cachedVersionNumber = latestVersion.versionNumber(); - cachedVersionType = latestVersion.versionType(); - cachedChangelog = latestVersion.changelog(); - cachedVersionId = latestVersion.versionId(); - - return new UpdateResult(isNewer, cachedVersionNumber, - cachedVersionType, cachedChangelog, cachedVersionId); - - } catch (Exception e) { - return UpdateResult.error("Failed to check for updates: " + e.getMessage()); - } - } - - private void updateCacheConfig(ModpackInfo modpackInfo) { - cachedModrinthProjectId = modpackInfo.getModrinthProjectId(); - cachedUpdateChannel = modpackInfo.getUpdateChannel(); - cachedCurrentVersion = modpackInfo.getVersion(); - cachedMinecraftVersion = modpackInfo.getMinecraftVersion(); - } - - private int compareVersions(String v1, String v2) { - return SBEStyledTitleScreen.compareVersions(v1, v2); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/kd_gaming1/packcore/util/wizard/WizardDataStore.java b/src/main/java/com/github/kd_gaming1/packcore/util/wizard/WizardDataStore.java deleted file mode 100644 index 876f5c6..0000000 --- a/src/main/java/com/github/kd_gaming1/packcore/util/wizard/WizardDataStore.java +++ /dev/null @@ -1,235 +0,0 @@ -package com.github.kd_gaming1.packcore.util.wizard; - -import com.github.kd_gaming1.packcore.PackCore; -import java.util.*; - -/** - * Manages wizard configuration data and application state. - * Uses a simple singleton pattern for data persistence across wizard pages. - */ -public class WizardDataStore { - - private static final WizardDataStore INSTANCE = new WizardDataStore(); - - // Configuration selections - private final WizardData data = new WizardData(); - - // Application state - private ApplicationState appState = new ApplicationState(); - - private WizardDataStore() {} - - /** - * Get the singleton instance - */ - public static WizardDataStore getInstance() { - return INSTANCE; - } - - // ===== Configuration Getters/Setters ===== - - public void setOptimizationProfile(String profile) { - data.optimizationProfile = profile; - logUpdate("optimization profile", profile); - } - - public String getOptimizationProfile() { - return data.optimizationProfile; - } - - public void setTabDesign(String design) { - data.tabDesign = design; - logUpdate("tab design", design); - } - - public String getTabDesign() { - return data.tabDesign; - } - - public void setItemBackground(String background) { - data.itemBackground = background; - logUpdate("item background", background); - } - - public String getItemBackground() { - return data.itemBackground; - } - - public void setResourcePacksOrdered(List packs) { - data.resourcePacksOrdered.clear(); - data.resourcePacksOrdered.addAll(packs); - logUpdate("resource packs", packs.toString()); - } - - public List getResourcePacksOrdered() { - return new ArrayList<>(data.resourcePacksOrdered); - } - - public Set getAdditionalSettings() { - return new HashSet<>(); // Placeholder for compatibility - } - - // ===== Application State ===== - - public void setApplicationState(boolean applying, boolean applied, String error) { - appState = new ApplicationState(applying, applied, error); - } - - public boolean isConfigurationApplying() { - return appState.applying; - } - - public void setConfigurationApplying(boolean applying) { - appState = appState.withApplying(applying); - } - - public boolean isConfigurationApplied() { - return appState.applied; - } - - public void setConfigurationApplied(boolean applied) { - appState = appState.withApplied(applied); - } - - public void setConfigurationResult(String result, String errorMessage) { - appState = appState.withError(errorMessage != null ? errorMessage : ""); - logUpdate("configuration result", result + " - " + errorMessage); - } - - public String getConfigurationResult() { - return appState.applied ? "success" : (!appState.error.isEmpty() ? "failed" : ""); - } - - public String getConfigurationErrorMessage() { - return appState.error; - } - - // ===== Summary Methods ===== - - /** - * Get the complete configuration - */ - public WizardConfiguration getConfiguration() { - return new WizardConfiguration( - data.optimizationProfile, - new ArrayList<>(data.resourcePacksOrdered), - data.tabDesign, - data.itemBackground - ); - } - - /** - * Check if minimum configuration is complete - */ - public boolean isConfigurationComplete() { - return !data.optimizationProfile.isEmpty(); - } - - /** - * Reset all data to defaults - */ - public void reset() { - data.clear(); - appState = new ApplicationState(); - PackCore.LOGGER.info("Wizard data reset"); - } - - /** - * Get a human-readable summary of the configuration - */ - public String getSummary() { - StringBuilder sb = new StringBuilder(); - - if (!data.optimizationProfile.isEmpty()) { - sb.append("Performance: ").append(data.optimizationProfile); - } - - if (!data.tabDesign.isEmpty()) { - if (!sb.isEmpty()) sb.append(", "); - sb.append("Tab: ").append(data.tabDesign); - } - - if (!data.itemBackground.isEmpty()) { - if (!sb.isEmpty()) sb.append(", "); - sb.append("Items: ").append(data.itemBackground); - } - - if (!data.resourcePacksOrdered.isEmpty()) { - if (!sb.isEmpty()) sb.append(", "); - sb.append("Packs: ").append(String.join(", ", data.resourcePacksOrdered)); - } - - return sb.toString(); - } - - private void logUpdate(String field, String value) { - PackCore.LOGGER.debug("Set {}: {}", field, value); - } - - /** - * Internal data storage class - */ - private static class WizardData { - String optimizationProfile = ""; - String tabDesign = ""; - String itemBackground = ""; - final List resourcePacksOrdered = new ArrayList<>(); - - void clear() { - optimizationProfile = ""; - tabDesign = ""; - itemBackground = ""; - resourcePacksOrdered.clear(); - } - } - - /** - * Immutable application state - */ - private record ApplicationState( - boolean applying, - boolean applied, - String error - ) { - ApplicationState() { - this(false, false, ""); - } - - ApplicationState withApplying(boolean applying) { - return new ApplicationState(applying, this.applied, this.error); - } - - ApplicationState withApplied(boolean applied) { - return new ApplicationState(this.applying, applied, this.error); - } - - ApplicationState withError(String error) { - return new ApplicationState(this.applying, this.applied, error); - } - } - - /** - * Configuration record for external use - */ - public record WizardConfiguration( - String optimizationProfile, - List resourcePacksOrdered, - String tabDesign, - String itemBackground - ) { - - @Override - public List resourcePacksOrdered() { - return new ArrayList<>(resourcePacksOrdered); - } - - // Compatibility methods - public Set getAdditionalSettings() { - return new HashSet<>(); - } - - public Map getCustomSettings() { - return new HashMap<>(); - } - } -} \ No newline at end of file diff --git a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_0.png b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_0.png index 455716f..213c483 100644 Binary files a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_0.png and b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_0.png differ diff --git a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_1.png b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_1.png index 00295d7..83e2bcf 100644 Binary files a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_1.png and b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_1.png differ diff --git a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_2.png b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_2.png index 1c966f5..3366106 100644 Binary files a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_2.png and b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_2.png differ diff --git a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_3.png b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_3.png index 13165cf..a569219 100644 Binary files a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_3.png and b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_3.png differ diff --git a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_4.png b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_4.png index bcea7e6..6736fb1 100644 Binary files a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_4.png and b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_4.png differ diff --git a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_5.png b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_5.png index af450a6..d47ead3 100644 Binary files a/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_5.png and b/src/main/resources/assets/minecraft/textures/gui/title/background/panorama_5.png differ diff --git a/src/main/resources/assets/packcore/file-descriptions.json b/src/main/resources/assets/packcore/file-descriptions.json deleted file mode 100644 index 0d3aa78..0000000 --- a/src/main/resources/assets/packcore/file-descriptions.json +++ /dev/null @@ -1,353 +0,0 @@ -{ - "version": "1.0", - "descriptions": { - "options.txt": { - "displayName": "Minecraft Game Settings", - "description": "Video settings, controls, sound, language, and all vanilla Minecraft options", - "icon": "\uD83D\uDEE0", - "isImportant": false - }, - "servers.dat": { - "displayName": "Multiplayer Server List", - "description": "Contains the list of multiplayer servers in your server list, including their connection details.", - "icon": "🌐", - "isImportant": false - }, - "config/skyblocker.json": { - "displayName": "SkyBlocker Configuration", - "description": "Contains the configuration values for the Skyblocker mod.", - "icon": "🛡️", - "isImportant": false - }, - "config/skyblocktweaks-config.json": { - "displayName": "SkyBlock Tweaks Configuration", - "description": "Contains the configuration values for the SkyBlock Tweaks mod.", - "icon": "\uD83D\uDEE0", - "isImportant": false - }, - "config/sodium-options.json": { - "displayName": "Sodium Configuration", - "description": "Performance optimization settings for rendering, chunks, and graphics quality", - "icon": "⚡", - "isImportant": false - }, - "config/sodium-extra-options.json": { - "displayName": "Sodium Extra Configuration", - "description": "Additional Sodium performance and visual options", - "icon": "⚡", - "isImportant": false - }, - "config/soundcontroller.json": { - "displayName": "Sound Controller Configuration", - "description": "Contains the configuration values for the Sound Controller mod. This includes the custom individual sound settings you have configured with the mod; the master values are saved in the options.txt file.", - "icon": "🔊", - "isImportant": false - }, - "config/modmenu.json": { - "displayName": "Mod Menu Configuration", - "description": "Mod list display settings and menu customization options", - "icon": "📋", - "isImportant": false - }, - "config/firmament/profiles": { - "displayName": "Firmament Profiles", - "description": "Contains data for each of your SkyBlock profiles, such as your storage contents.", - "icon": "👤", - "isImportant": false - }, - "config/firmament/storage": { - "displayName": "Firmament Storage", - "description": "Contains data for storage contents.", - "icon": "💾", - "isImportant": false - }, - "config/firmament/storage/configconfig.json": { - "displayName": "Firmament Config", - "description": "Firmament configuration file.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/copy-chat.json": { - "displayName": "Firmament Config", - "description": "Firmament copy chat settings.", - "icon": "📋", - "isImportant": false - }, - "config/firmament/storage/custom-skyblock-textures.json": { - "displayName": "Firmament Config", - "description": "Firmament custom textures settings.", - "icon": "🎨", - "isImportant": false - }, - "config/firmament/storage/developer.json": { - "displayName": "Firmament Config", - "description": "Firmament developer settings.", - "icon": "🔧", - "isImportant": false - }, - "config/firmament/storage/developer-capes.json": { - "displayName": "Firmament Config", - "description": "Firmament developer capes settings.", - "icon": "🦸", - "isImportant": false - }, - "config/firmament/storage/diana.json": { - "displayName": "Firmament Config", - "description": "Firmament diana settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/etherwarp-overlay.json": { - "displayName": "Firmament Config", - "description": "Firmament etherwarp overlay settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/fairy-souls.json": { - "displayName": "Firmament Config", - "description": "Firmament fairy souls settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/fixes.json": { - "displayName": "Firmament Config", - "description": "Firmament fixes settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/hud.json": { - "displayName": "Firmament Config", - "description": "Firmament hud settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/inventory-buttons-config.json": { - "displayName": "Firmament Config", - "description": "Firmament inventory buttons settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/item-hotkeys.json": { - "displayName": "Firmament Config", - "description": "Firmament item hotkeys settings.", - "icon": "⌨⚙", - "isImportant": false - }, - "config/firmament/storage/item-rarity-cosmetics.json": { - "displayName": "Firmament Config", - "description": "Firmament item rarity cosmetics settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/jade-integration.json": { - "displayName": "Firmament Config", - "description": "Firmament Jade integration settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/junk-highlighter.json": { - "displayName": "Firmament Config", - "description": "Firmament junk highlighter settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/lore-timers.json": { - "displayName": "Firmament Config", - "description": "Firmament lore timers settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/party-commands.json": { - "displayName": "Firmament Config", - "description": "Firmament party commands settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/pets.json": { - "displayName": "Firmament Config", - "description": "Firmament pets settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/pickaxe-info.json": { - "displayName": "Firmament Config", - "description": "Firmament pickaxe info settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/power-user.json": { - "displayName": "Firmament Config", - "description": "Firmament power user settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/price-data.json": { - "displayName": "Firmament Config", - "description": "Firmament price data settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/pristine-profit.json": { - "displayName": "Firmament Config", - "description": "Firmament pristine profit settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/quick-commands.json": { - "displayName": "Firmament Config", - "description": "Firmament quick commands settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/repo.json": { - "displayName": "Firmament Config", - "description": "Firmament repo settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/save-cursor-position.json": { - "displayName": "Firmament Config", - "description": "Firmament save cursor position settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/slot-locking.json": { - "displayName": "Firmament Config", - "description": "Firmament slot locking settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/storage-overlay.json": { - "displayName": "Firmament Config", - "description": "Firmament storage overlay settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/wardrobe-keybinds.json": { - "displayName": "Firmament Config", - "description": "Firmament wardrobe keybinds settings.", - "icon": "⚙", - "isImportant": false - }, - "config/firmament/storage/waypoints.json": { - "displayName": "Firmament Config", - "description": "Firmament waypoints settings.", - "icon": "⚙", - "isImportant": false - }, - "config/fzzy_config/keybinds.toml": { - "displayName": "Fzzy Config Keybinds", - "description": "Fzzy Config mod keybindings settings, used by soundcontroller mod", - "icon": "⚙", - "isImportant": false - }, - "config/roughlyenoughitems/config.json5": { - "displayName": "Roughly Enough Items Configuration", - "description": "Recipe viewer and item list customization settings for the Roughly Enough Items mod.", - "icon": "⚙", - "isImportant": false - }, - "config/skyblocker/hud_widgets.json": { - "displayName": "SkyBlocker Config", - "description": "Stores SkyBlocker HUD widget settings here.", - "icon": "⚙", - "isImportant": false - }, - "config/skyblocker/status_bars.json": { - "displayName": "SkyBlocker Config", - "description": "Stores SkyBlocker status bar settings here.", - "icon": "⚙", - "isImportant": false - }, - "config/skyblocker/update_notifications.json": { - "displayName": "SkyBlocker Config", - "description": "Stores SkyBlocker update notification settings here.", - "icon": "⚙", - "isImportant": false - }, - "config/skyblockpv/config.jsonc": { - "displayName": "SkyBlockPV Configuration", - "description": "Contains the configuration values for the SkyBlockPV mod.", - "icon": "⚙", - "isImportant": false - }, - "config/skyhanni/config.json": { - "displayName": "SkyHanni Configuration", - "description": "Contains the configuration values for the SkyHanni mod.", - "icon": "⚙", - "isImportant": false - }, - "config/skyocean/config.jsonc": { - "displayName": "SkyOcean Configuration", - "description": "Contains the configuration values for the SkyOcean mod.", - "icon": "⚙", - "isImportant": false - }, - "config/Zen-1.21/zen-data.json": { - "displayName": "Zen Configuration", - "description": "Stores Zen values that tell the mod if it is the first startup after installation.", - "icon": "⚙", - "isImportant": false - }, - "config/zen/config/Config.json": { - "displayName": "Zen Config", - "description": "Contains the configuration values for the Zen mod.", - "icon": "⚙", - "isImportant": false - }, - "config/zen/hud_positions.json": { - "displayName": "Zen Config", - "description": "Contains the HUD positions for the Zen mod.", - "icon": "⚙", - "isImportant": false - }, - "config/sbo/config.jsonc": { - "displayName": "Skyblock Overhaul", - "description": "Contains the configuration values for the Skyblock Overhaul mod.", - "icon": "⚙", - "isImportant": false - }, - "config/bazaarutils.json": { - "displayName": "Bazaar Utils Configuration", - "description": "Bazaar Utils configuration file.", - "icon": "⚙", - "isImportant": false - }, - "config/bobby.conf": { - "displayName": "Bobby Configuration", - "description": "Bobby mod settings and preferences", - "icon": "⚙", - "isImportant": false - }, - "config/chatpatches.json": { - "displayName": "Chat Patches Configuration", - "description": "Chat patches configuration file. Controls config values for chat patches mod.", - "icon": "💬", - "isImportant": false - }, - "config/gammautils.json": { - "displayName": "Gamma Utils Configuration", - "description": "Gamma Utils configuration file. Controls config values for gamma utils mod. Your gamma setting.", - "icon": "🔆", - "isImportant": false - }, - "config/moreculling.toml": { - "displayName": "More Culling Configuration", - "description": "Advanced culling options for better performance", - "icon": "⚙", - "isImportant": false - }, - "config/scaleme.json": { - "displayName": "Scale Me Configuration", - "description": "Scale Me mod settings player scaling and item soling and rotation", - "icon": "⚙", - "isImportant": false - }, - "config/tooltipscroll.json": { - "displayName": "Tooltip Scroll Configuration", - "description": "Tooltip Scroll mod settings for scrolling long tooltips", - "icon": "⚙", - "isImportant": false - } - } -} \ No newline at end of file diff --git a/src/main/resources/assets/packcore/font/gallaeciaforte.json b/src/main/resources/assets/packcore/font/gallaeciaforte.json deleted file mode 100644 index 975e244..0000000 --- a/src/main/resources/assets/packcore/font/gallaeciaforte.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "providers": [ - { - "type": "ttf", - "file": "packcore:gallaeciaforte.ttf", - "shift": [0, -1], - "size": 11, - "oversample": 4 - } - ] -} \ No newline at end of file diff --git a/src/main/resources/assets/packcore/font/gallaeciaforte.ttf b/src/main/resources/assets/packcore/font/gallaeciaforte.ttf deleted file mode 100644 index b07e851..0000000 Binary files a/src/main/resources/assets/packcore/font/gallaeciaforte.ttf and /dev/null differ diff --git a/src/main/resources/assets/packcore/font/gallaeciaforte2.json b/src/main/resources/assets/packcore/font/gallaeciaforte2.json deleted file mode 100644 index ccc061c..0000000 --- a/src/main/resources/assets/packcore/font/gallaeciaforte2.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "providers": [ - { - "type": "ttf", - "file": "packcore:gallaeciaforte.ttf", - "shift": [0, -1], - "size": 14, - "oversample": 8 - } - ] -} \ No newline at end of file diff --git a/src/main/resources/assets/packcore/lang/en_us.json b/src/main/resources/assets/packcore/lang/en_us.json index a22dd6a..000e1ef 100644 --- a/src/main/resources/assets/packcore/lang/en_us.json +++ b/src/main/resources/assets/packcore/lang/en_us.json @@ -1,48 +1,243 @@ { - "packcore.midnightconfig.title": "PackCore Configuration", - - "packcore.midnightconfig.category.interface": "User Interface", - "packcore.midnightconfig.category.backups": "Backups", - "packcore.midnightconfig.category.customization": "Customization & Updates", - "packcore.midnightconfig.category.scam_protection": "Scam protection", - "packcore.midnightconfig.category.advanced": "Advanced", - - "packcore.midnightconfig.interface_info": "Interface settings for the PackCore mod.", - "packcore.midnightconfig.enable_custom_menu": "Enable custom main menu", - "packcore.midnightconfig.enable_custom_menu.tooltip": "Turns the PackCore custom main menu on or off.", - - "packcore.midnightconfig.backup_info": "Auto backups run when you apply configuration changes. Scheduled backups run at the interval below.", - "packcore.midnightconfig.enable_auto_backups": "Enable auto backups", - "packcore.midnightconfig.enable_auto_backups.tooltip": "Creates a backup each time you apply new settings.", - "packcore.midnightconfig.enable_scheduled_backups": "Enable scheduled backups", - "packcore.midnightconfig.enable_scheduled_backups.tooltip": "Creates backups at regular times.", - "packcore.midnightconfig.max_backups": "Maximum number of backups to keep", - "packcore.midnightconfig.max_backups.tooltip": "Limits how many backups PackCore stores. It deletes the oldest when full.", - "packcore.midnightconfig.backup_interval_days": "Backup interval (days)", - "packcore.midnightconfig.backup_interval_days.tooltip": "Sets how long PackCore waits before creating the next scheduled backup.", - "packcore.midnightconfig.enable_backup_debug_logging": "Enable Backup Debug Logging", - "packcore.midnightconfig.enable_backup_debug_logging.tooltip": "Logs detailed timing information for each backup phase. Enable this if backups are slow to help diagnose the issue.", - "packcore.midnightconfig.customization_spacer_1": "", - - "packcore.midnightconfig.server_address": "Server address for quick-join button", - "packcore.midnightconfig.server_address.tooltip": "Sets the server used by the Quick Join button.", - "packcore.midnightconfig.enable_update_notifications": "Enable update notifications", - "packcore.midnightconfig.enable_update_notifications.tooltip": "Shows a message in chat when the modpack has updates.", - "packcore.midnightconfig.show_update_notifications_title": "Show update notifications on title screen", - "packcore.midnightconfig.show_update_notifications_title.tooltip": "Shows update notices on the title screen.", - "packcore.midnightconfig.show_low_memory_warning": "Show Low Memory Warning", - "packcore.midnightconfig.show_low_memory_warning.tooltip": "Show a warning on the main menu if allocated RAM is below the minimum", - "packcore.midnightconfig.minimum_ram_gb": "Minimum RAM (GB)", - "packcore.midnightconfig.minimum_ram_gb.tooltip": "Minimum amount of RAM in GB before showing a warning", - - "packcore.midnightconfig.first_startup": "Treat next launch as first startup", - "packcore.midnightconfig.first_startup.tooltip": "Controls whether PackCore runs its first-time setup. Turn this on if you want the modpack to run the setup steps again.", - "packcore.midnightconfig.welcome_wizard_shown": "Mark welcome wizard as shown", - "packcore.midnightconfig.welcome_wizard_shown.tooltip": "Controls if the welcome wizard shows. Turn it off to show it again.", - - "packcore.midnightconfig.have_set_bobby_config": "Mark Bobby config as applied", - "packcore.midnightconfig.have_set_bobby_config.tooltip": "Tracks if PackCore has applied Bobby’s settings. Change it to force the setup again.", - - "packcore.midnightconfig.setup_wizard_completed": "Mark setup wizard as completed", - "packcore.midnightconfig.setup_wizard_completed.tooltip": "Tracks if the setup wizard has been completed." + "packcore.midnightconfig.title": "PackCore Settings", + + "packcore.midnightconfig.category.toast": "Notifications", + "packcore.midnightconfig.category.menu": "Main Menu", + "packcore.midnightconfig.category.backup": "Automatic Backups", + "packcore.midnightconfig.category.meta": "Internal", + + "packcore.midnightconfig.showRamWarningToast": "Low RAM Warning", + "packcore.midnightconfig.showRamWarningToast.tooltip": "Show a toast when less than 3GB of RAM is allocated.", + + "packcore.midnightconfig.showUpdateToast": "Update Notification", + "packcore.midnightconfig.showUpdateToast.tooltip": "Show a toast when a modpack update is available.", + + "packcore.midnightconfig.showBackupToast": "Backup Notification", + "packcore.midnightconfig.showBackupToast.tooltip": "Show a toast when an automatic backup is created.", + + "packcore.midnightconfig.menuStyle": "Menu Style (requires restart)", + "packcore.midnightconfig.menuStyle.tooltip": "Controls which title screen style is used.\nModern: fully custom branded screen with a custom background.\nModern Minimal: fully custom branded screen with the vanilla panorama.\nMinimal: vanilla-style screen with quick-join and social buttons added.", + + "packcore.midnightconfig.enum.MenuStyle.MODERN": "Modern", + "packcore.midnightconfig.enum.MenuStyle.MODERN_MINIMAL": "Modern Minimal", + "packcore.midnightconfig.enum.MenuStyle.MINIMAL": "Minimal", + + "packcore.midnightconfig.serverAddressForQuickJoinButton": "Quick Join Address", + "packcore.midnightconfig.serverAddressForQuickJoinButton.tooltip": "The server address used by the Join button on the main menu.", + + "packcore.midnightconfig.autoBackupEnabled": "Automatic Backups", + "packcore.midnightconfig.autoBackupEnabled.tooltip": "Automatically create a backup when you log in after not playing for a while.", + + "packcore.midnightconfig.autoBackupIntervalDays": "Backup Interval (days)", + "packcore.midnightconfig.autoBackupIntervalDays.tooltip": "How many days must pass before a new automatic backup is triggered (1–90).", + + "gui.packcore.button.join_hypixel": "Join Hypixel", + "gui.packcore.button.modmenu": "Mod Menu", + + "gui.packcore.tooltip.discord": "Join our Discord server", + "gui.packcore.tooltip.modrinth": "View on Modrinth", + "gui.packcore.tooltip.github": "View source on GitHub", + "gui.packcore.tooltip.modpack_config": "Open Configuration menu for the modpack", + "gui.packcore.tooltip.modpack_update": "See what's new in the modpack.", + "gui.packcore.tooltip.update_available": "Update available: v%s", + "gui.packcore.tooltip.changelog": "View Changelog", + + "gui.packcore.overlay.changelog.title": "Changelog — v%s", + "gui.packcore.overlay.changelog.close": "Close", + "gui.packcore.overlay.changelog.empty": "No changelog available.", + "gui.packcore.overlay.changelog.empty.hint": "PackCore could not load changelog text for this version.", + + "gui.packcore.wizard.title": "Welcome to v%s", + + "gui.packcore.wizard.button.back": "Back", + "gui.packcore.wizard.button.continue": "Continue", + "gui.packcore.wizard.button.skip": "Skip", + "gui.packcore.wizard.button.finish": "Finish ✓", + + "gui.packcore.wizard.page.welcome.title": "Welcome", + + "gui.packcore.wizard.card.config.applied": "✓ Applied: %s", + "gui.packcore.wizard.card.config.error": "No Config Applied", + "gui.packcore.wizard.card.config.error.hint": "Restart the game to automatically try again, or manually apply a config from the list below.", + + "gui.packcore.wizard.card.configs.heading": "Available Configurations", + "gui.packcore.wizard.card.configs.hint": "Want to switch from the auto-selected one?", + + "gui.packcore.wizard.page.main_menu_design.title": "Main Menu Design", + "gui.packcore.wizard.page.main_menu_design.explanation": "Select how your main menu should look. Modern gives the game a custom SkyBlock-branded look. Modern Minimal uses the same layout but with the vanilla panorama background. Minimal adds quick-join and social buttons to the standard Minecraft menu.", + + "gui.packcore.wizard.menu_design.modern.name": "Modern", + "gui.packcore.wizard.menu_design.modern.desc": "A fully custom main menu designed to give the game the look and feel of the official Hypixel SkyBlock game rather than Minecraft.", + + "gui.packcore.wizard.menu_design.modern_minimal.name": "Modern Minimal", + "gui.packcore.wizard.menu_design.modern_minimal.desc": "The same custom layout as Modern, but uses the vanilla Minecraft panorama instead of a custom background image.", + + "gui.packcore.wizard.menu_design.minimal.name": "Minimal", + "gui.packcore.wizard.menu_design.minimal.desc": "A custom main menu based on the vanilla style, with quick-join, social, and update buttons added.", + + "gui.packcore.wizard.page.performance.title": "Performance", + + "gui.packcore.wizard.performance.max_fps.name": "Max FPS", + "gui.packcore.wizard.performance.max_fps.desc": "Reduces visual quality to maximize frame rate.", + + "gui.packcore.wizard.performance.balanced.name": "Balanced", + "gui.packcore.wizard.performance.balanced.desc": "A balance between performance and visuals. Recommended for most systems.", + + "gui.packcore.wizard.performance.quality.name": "Quality", + "gui.packcore.wizard.performance.quality.desc": "Prioritizes visual quality over frame rate. Recommended for mid-range hardware.", + + "gui.packcore.wizard.performance.quality_performance_shaders.name": "Quality + Performance Shaders", + "gui.packcore.wizard.performance.quality_performance_shaders.desc": "Quality settings with lightweight shaders that add visual polish with minimal FPS impact.", + + "gui.packcore.wizard.performance.quality_quality_shaders.name": "Quality + Quality Shaders", + "gui.packcore.wizard.performance.quality_quality_shaders.desc": "High quality settings with demanding shaders. Requires a powerful GPU.", + + "gui.packcore.wizard.page.tab_design.title": "Tab List Design", + "gui.packcore.wizard.page.tab_design.explanation": "Choose a tab list style. Compact (from SkyHanni) shows more information in a condensed layout with a vanilla look. Fancy (from Skyblocker) uses a more decorative and styled display.", + + "gui.packcore.wizard.tab_design.compact.name": "Compact", + "gui.packcore.wizard.tab_design.compact.desc": "A condensed tab list from SkyHanni.", + "gui.packcore.wizard.tab_design.fancy.name": "Fancy", + "gui.packcore.wizard.tab_design.fancy.desc": "A decorative styled tab list from Skyblocker.", + + "gui.packcore.wizard.page.item_background.title": "Item Background", + "gui.packcore.wizard.page.item_background.explanation": "Choose how items appear in your inventory. All options come from Skyblocker. None keeps the default Minecraft look, while Circle and Square add a styled background behind each item slot that matches the item rarity color.", + + "gui.packcore.wizard.item_background.none.name": "None", + "gui.packcore.wizard.item_background.none.desc": "Default Minecraft item appearance with no background.", + + "gui.packcore.wizard.item_background.circle.name": "Circle", + "gui.packcore.wizard.item_background.circle.desc": "Adds a circular background behind each item.", + + "gui.packcore.wizard.item_background.square.name": "Square", + "gui.packcore.wizard.item_background.square.desc": "Adds a square background behind each item.", + + "gui.packcore.wizard.page.storage_design.title": "Storage Design", + "gui.packcore.wizard.page.storage_design.explanation": "Choose how your SkyBlock storage is displayed. Overlay shows a full storage interface with all pages visible at once. Vanilla keeps the default chest look but adds tooltips that show what each page contains when you hover over it.", + + "gui.packcore.wizard.storage_design.overlay.name": "Overlay", + "gui.packcore.wizard.storage_design.overlay.desc": "A full storage interface with all pages and their contents visible at once.", + + "gui.packcore.wizard.storage_design.vanilla.name": "Vanilla", + "gui.packcore.wizard.storage_design.vanilla.desc": "Default chest appearance with tooltips that show each page's contents on hover.", + + "gui.packcore.wizard.page.scamscreener.title": "ScamScreener", + "gui.packcore.wizard.page.scamscreener.explanation": "Configure how aggressively ScamScreener should warn and whether warning messages should ping you in chat.", + "gui.packcore.wizard.scamscreener.alerts.heading": "Minimum Alert Level", + "gui.packcore.wizard.scamscreener.pings.heading": "Warning Pings", + "gui.packcore.wizard.scamscreener.pings.hint": "Choose which ScamScreener warning types should play a ping in chat.", + + "gui.packcore.wizard.scamscreener.minimum_risk.LOW.name": "Low", + "gui.packcore.wizard.scamscreener.minimum_risk.LOW.desc": "Show all ScamScreener warnings, including low-confidence alerts. ⚠ Higher chance of false positives!", + "gui.packcore.wizard.scamscreener.minimum_risk.MEDIUM.name": "Medium (Recommended)", + "gui.packcore.wizard.scamscreener.minimum_risk.MEDIUM.desc": "Hide low-confidence alerts and only warn when risk is medium or higher. Best default for most players.", + "gui.packcore.wizard.scamscreener.minimum_risk.HIGH.name": "High", + "gui.packcore.wizard.scamscreener.minimum_risk.HIGH.desc": "Only surface stronger warnings with a lower chance of false positives.", + "gui.packcore.wizard.scamscreener.minimum_risk.CRITICAL.name": "Critical", + "gui.packcore.wizard.scamscreener.minimum_risk.CRITICAL.desc": "Only show the most severe warnings.", + + "gui.packcore.wizard.scamscreener.pings.risk.name": "Ping On Risk Warning", + "gui.packcore.wizard.scamscreener.pings.risk.desc": "Enable the chat ping when ScamScreener raises a normal risk warning.", + "gui.packcore.wizard.scamscreener.pings.blacklist.name": "Ping On Blacklist Warning", + "gui.packcore.wizard.scamscreener.pings.blacklist.desc": "Enable the chat ping when ScamScreener detects a blacklist warning.", + + "gui.packcore.wizard.page.resource_pack.title": "Resource Packs", + "gui.packcore.wizard.resource_pack.none_found": "No resource packs were found in your resource packs folder. Add resource packs there and reopen the wizard.", + "gui.packcore.wizard.resource_pack.no_description": "No description provided.", + + "gui.packcore.wizard.resource_pack.xss_warning": "⚠ Disabled — add -Xss4M JVM argument to enable.", + + "gui.packcore.wizard.resource_pack.xss_banner_header": "⚠ Hypixel+ requires the JVM argument -Xss4M, which is not currently set.", + "gui.packcore.wizard.resource_pack.xss_banner_launcher": "Detected launcher: %s", + + "gui.packcore.wizard.resource_pack.xss_fix.prism": "Fix: right-click instance → Edit → Settings → Java → add -Xss4M to JVM Arguments.", + "gui.packcore.wizard.resource_pack.xss_fix.curseforge": "Fix: profile ⋯ → Profile Options → Additional Java Arguments → add -Xss4M.", + "gui.packcore.wizard.resource_pack.xss_fix.atlauncher": "Fix: Settings → Java/Minecraft → Extra Java Parameters → add -Xss4M.", + "gui.packcore.wizard.resource_pack.xss_fix.modrinth": "Fix: instance ⋯ → Edit → Java Settings → Java arguments → add -Xss4M.", + "gui.packcore.wizard.resource_pack.xss_fix.official": "Fix: Installations → edit profile → More Options → JVM Arguments → add -Xss4M.", + "gui.packcore.wizard.resource_pack.xss_fix.unknown": "Fix: open your launcher's instance/profile settings, find the JVM Arguments field, and add -Xss4M.", + "gui.packcore.wizard.resource_pack.xss_fix.restart": "Restart the game after saving to enable Hypixel+.", + + "gui.packcore.wizard.page.confirm.title": "Confirm & Apply", + "gui.packcore.wizard.page.confirm.subtitle": "Review your selections.", + "gui.packcore.wizard.confirm.title": "Review your selections before applying.", + "gui.packcore.wizard.confirm.world_join_required": "⚠ Tab and storage settings will finish applying after you join a world/server. Please do so immediately, and do NOT close the game first.", + "gui.packcore.wizard.confirm.apply_all_configs": "Apply Settings", + "gui.packcore.wizard.button.finish.locked_tooltip": "Click \"Apply Settings\" to apply your selections before finishing.", + + "gui.packcore.wizard.confirm.resource_pack.xss_partial_apply": "Hypixel+ was skipped — -Xss4M is missing. Other selected packs were applied.", + "gui.packcore.wizard.confirm.resource_pack.xss_blocked": "Hypixel+ was not applied — -Xss4M JVM argument is missing.", + + "gui.packcore.wizard.page.sword_block.title": "Sword Block", + "gui.packcore.wizard.page.sword_block.explanation": "Choose whether to enable the sword blocking animation from ScaleMe. When enabled, holding right click with a sword will display the classic blocking pose from 1.8.9.", + + "gui.packcore.wizard.sword_block.enabled.name": "Enabled", + "gui.packcore.wizard.sword_block.enabled.desc": "Holding right click with a sword will show the blocking animation.", + + "gui.packcore.wizard.sword_block.disabled.name": "Disabled", + "gui.packcore.wizard.sword_block.disabled.desc": "No blocking animation will be shown when holding right click with a sword.", + + "gui.packcore.config.title": "Modpack Configuration", + + "gui.packcore.config.tab.configuration": "Configuration", + "gui.packcore.config.tab.export": "Export", + "gui.packcore.config.tab.backups": "Backups", + "gui.packcore.config.tab.import": "Import", + + "gui.packcore.config.files.heading": "Files in Config Preset", + "gui.packcore.config.files.hint": "Select a config preset on the right to browse its files.", + "gui.packcore.config.presets.heading": "Available Presets", + + "gui.packcore.config.button.apply_selected": "Apply Selected", + "gui.packcore.config.button.apply_all": "Apply All", + + "gui.packcore.export.metadata.heading": "Export Metadata", + "gui.packcore.export.files.heading": "Select Files to Export", + "gui.packcore.export.field.name": "Config Name", + "gui.packcore.export.field.version": "Version", + "gui.packcore.export.field.author": "Author", + "gui.packcore.export.field.description": "Description", + "gui.packcore.export.field.resolution": "Resolution (e.g. 1920x1080)", + "gui.packcore.export.field.gui_scale": "GUI Scale", + "gui.packcore.export.button.export": "Export Config Pack", + "gui.packcore.export.button.open_folder": "Open Exports Folder", + + "gui.packcore.export.tooltip.missing_name": "Enter a config name.", + "gui.packcore.export.tooltip.missing_version": "Fix the version format (e.g. 1.0.0).", + "gui.packcore.export.tooltip.missing_resolution": "Fix the resolution format (e.g. 1920x1080).", + "gui.packcore.export.tooltip.missing_gui_scale": "GUI scale must be a whole number (0 = Auto)", + "gui.packcore.export.tooltip.missing_files": "Select at least one file to export.", + + "gui.packcore.config.source.official": "Official", + "gui.packcore.config.source.my_exports": "My Exports", + + "gui.packcore.config.tooltip.apply_selected": "Apply only the files you have checked in the file tree.", + "gui.packcore.config.tooltip.apply_all": "Apply every file in this preset, overwriting existing ones.", + + "gui.packcore.backups.heading": "Backups", + "gui.packcore.backups.empty": "No backups found. Create one with the button above.", + "gui.packcore.backups.button.create": "Create Backup Now", + "gui.packcore.backups.button.restore": "Restore", + + "gui.packcore.import.files.heading": "Files in Import", + "gui.packcore.import.files.hint": "Select an import on the right to browse its files.", + "gui.packcore.import.list.heading": "Imported Configs", + "gui.packcore.import.list.empty": "No imports found", + "gui.packcore.import.list.empty.hint": "Place a config pack .zip file in the imports folder", + "gui.packcore.import.button.open_folder": "Open Imports Folder", + + "gui.packcore.overlay.restore.title": "Restore Backup?", + "gui.packcore.overlay.restore.button.cancel": "Cancel", + "gui.packcore.overlay.restore.button.confirm": "Restore on Restart", + "gui.packcore.overlay.restore.warning1": "⚠ This will overwrite your current config.", + "gui.packcore.overlay.restore.warning2": "Any unsaved changes since this backup will be lost.", + "gui.packcore.overlay.restore.note": "The game will close to apply the restore.", + + "gui.packcore.toast.update.title": "Update Available", + "gui.packcore.toast.update.message": "PackCore %s is available!", + "gui.packcore.toast.backup.title": "Backup Created", + "gui.packcore.toast.backup.message": "Auto-backup saved: %s", + "gui.packcore.toast.ram.title": "Low Memory Warning", + "gui.packcore.toast.ram.message": "Allocate 3GB+ RAM for best performance.", + "gui.packcore.wizard.card.config.applied.hint": "Your config changes were preserved during the update." } \ No newline at end of file diff --git a/src/main/resources/assets/packcore/markdown/optimisation.md b/src/main/resources/assets/packcore/markdown/optimisation.md deleted file mode 100644 index 7f1c054..0000000 --- a/src/main/resources/assets/packcore/markdown/optimisation.md +++ /dev/null @@ -1,65 +0,0 @@ -{gold}**Optimisation**{} - -Skyblock Enhanced comes optimized out of the box and includes many performance mods to help you hit high FPS. However, some users with older hardware may struggle to run the pack. To accommodate different hardware, several profiles are available—you can click the box on the left that best matches your needs. More info about the profiles is below: - - -{gold}**Performance Profiles**{} - -> Choose the profile that fits your system for the best balance of performance and visuals. - - -{yellow}***Max FPS Profile****{} - -Perfect for older laptops. This profile reduces visual effects while maintaining gameplay quality. - -{#275EF5}**Changes Applied:**{} -- Graphics Mode: Fast -- View Distance: 10 chunks -- Simulation Distance: 10 chunks -- Entity Shadows: Disabled -- Particles: Minimal -- Cloud Rendering: Fast -- Biome Blending: Reduced -- Sodium optimizations for maximum performance - - -{yellow}***Balanced Profile****{} - -Recommended for most users, providing a balance between visual quality and performance. - -{#275EF5}**Changes Applied:**{} -- Graphics Mode: Fancy -- View Distance: 14 chunks -- Simulation Distance: 12 chunks -- Entity Shadows: Enabled -- Particles: All -- Cloud Rendering: Fancy -- Full visual effects with optimized settings - - -{yellow}***Quality Profile****{} - -Designed for mid-end systems that want the best visual experience without shaders. - -{#275EF5}**Changes Applied:**{} -- Graphics Mode: Fabulous -- View Distance: 16 chunks on 1.21.5 or 20 on 1.21.8 -- Simulation Distance: 12 chunks -- Entity Distance Scaling: 125% -- Enhanced lighting and shadows -- Maximum visual fidelity settings - - -{yellow}***Shaders Profile****{} - -> {#b2651b}Warning! Using shaders may break some mod features. If waypoints or in-world overlays do not display correctly, disable shaders using Keybind K.{} - -The ultimate visual experience for high-end systems. This profile enables shaders with optimized settings. - -{#275EF5}**Shader Pack Included:**{} Complementary Unbound (customized) -- Tweaked to match Hypixel's official trailer aesthetics -- Optimized for performance while maintaining stunning visuals - -{#275EF5}**Changes Applied:**{} -- All Quality profile settings -- Shader Pack: Complementary Unbound diff --git a/src/main/resources/assets/packcore/markdown/resource_packs.md b/src/main/resources/assets/packcore/markdown/resource_packs.md deleted file mode 100644 index ac4a2ed..0000000 --- a/src/main/resources/assets/packcore/markdown/resource_packs.md +++ /dev/null @@ -1,86 +0,0 @@ -{gold}**Resource & Texture Packs**{} - -There are many texture packs for Hypixel SkyBlock. Use the ones you like. Packs can stack. The pack on top wins when two packs change the same texture. Packs below only fill missing textures. - -To put a pack at the top, click it first. You can change order later in **Options → Resource Packs**. - ---- - -{yellow}**Available Packs**{} - -- **Hypixel Plus** -> {#b2651b}Warning! Hypixel Plus needs the JVM flag `-Xss4M`. Do not enable it until you finish setup and restart Minecraft. If you already clicked it, close the game, add the flag, and reopen. Need help? {#275EF5}[Join the Discord](https://discord.gg/pdwxyjTta7){} - -- **FurfSky Overlay** -- **FurfSky Full** *do not use with SkyBlock Dark UI* -- **SkyBlock Dark UI** -- **Defrosted** -- **Looshy** -- **Dark Mode SkyBlock** -- **Sophie's Enchants** - ---- - -{yellow}**Popular Combinations**{} - -- Hypixel Plus + SkyBlock Dark UI -- FurfSky Overlay + SkyBlock Dark UI -- SkyBlock Dark UI + Defrosted + Looshy -- Defrosted + Looshy -- Hypixel Plus (solo) -- FurfSky Full (solo) -- Dark Mode SkyBlock + any main pack -- Sophie's Enchants + any pack (overlays cleanly) - -> Note: FurfSky Full + SkyBlock Dark UI can break some menus. - ---- - -{yellow}**Descriptions**{} - -**Hypixel Plus** -A clean, mostly-vanilla UI and item pack made for Hypixel. Clear icons, polished menus, and great clarity without heavy style changes. - -{#275EF5}Go to: https://modrinth.com/resourcepack/hypixel-plus/gallery for showcase{} - ---- - -**FurfSky (Full & Overlay)** -A very popular SkyBlock pack. **Full** replaces almost everything, including GUI. **Overlay** only changes SkyBlock item textures. - -{#275EF5}Go to: https://modrinth.com/resourcepack/furfsky-reborn/gallery for showcase{} - ---- - -**SkyBlock Dark UI** -A dark GUI theme for SkyBlock menus and mod interfaces. Inspired by PacksHQ. Smooth modern menus. - -{#275EF5}Go to: https://modrinth.com/resourcepack/skyblock-dark-ui/gallery for showcase{} - ---- - -**Defrosted** -Frost-blue themed 16× pack with clean minimalist style. Also available in pink. - -{#275EF5}Go to: https://modrinth.com/resourcepack/defrosted_pack/gallery for showcase{} - ---- - -**Looshy** -Smooth vanilla-style 16× pack. Minimalist, clean, clear visuals. Adds polish without changing the Minecraft feel. - -{#275EF5}Go to: https://modrinth.com/resourcepack/looshy/gallery for showcase{} - ---- - -**Dark Mode SkyBlock** -A dark-theme SkyBlock world pack for 1.21.5-1.21.8. Makes zones like The End, The Mist, Rift, and Glacite Tunnels easier on your eyes during long sessions. - -{#275EF5}Go to: https://modrinth.com/resourcepack/dark-mode-skyblock/gallery for showcase{} - ---- - -**Sophie's Enchants** -Gives every enchanted book a unique texture. Helps you sort enchants fast in the inventory or chests. Great quality-of-life pack. - -{#275EF5}Go to: https://modrinth.com/resourcepack/sophies-enchants/gallery for showcase{} \ No newline at end of file diff --git a/src/main/resources/assets/packcore/markdown/scam_education.md b/src/main/resources/assets/packcore/markdown/scam_education.md deleted file mode 100644 index b5ea162..0000000 --- a/src/main/resources/assets/packcore/markdown/scam_education.md +++ /dev/null @@ -1,181 +0,0 @@ -{gold}**Hypixel SkyBlock Scam Prevention Guide**{} - -> Scamming means stealing coins, items, or accounts through lies or tricks. -> This guide helps you spot scams and avoid them. - -{gold}**Important:** Victims of scams will not get items back in any case.{} - - - - -{gold}**Golden Rules**{} - -1. **If it looks too good to be true, it is** -2. **Never hand items to someone you do not trust** -3. **Double-check trades before you click accept** -4. **Enable API and check player stats** -5. **Do not click weird or unknown links** - - - - -{gold}**Common Scam Types**{} - - -{yellow}**Price Manipulation**{} - -Scammers push prices up or down to trick you. - -**Avoid it** -- Check Bazaar and Auction House -- Use price check mods -- Ignore “limited offer” deals - - -{yellow}**Unbalanced Trades**{} - -Cheap items disguised as rare items. - - -**Avoid it** -- Hover items to read names -- Check stats before trading -- Use packs that show item differences - - -{yellow}**False Rewards**{} - -“Bid on this and get free items!” - - -**Avoid it** -- Do not bid expecting rewards -- Check player SkyCrypt -- Real players just give items away - - -{yellow}**Crafting or Reforge Scams**{} - -They ask for mats, then steal them. - - -**Avoid it** -- Only trade with trusted friends -- Check they have recipes (API on) -- Best choice: unlock recipes yourself - - -{yellow}**Borrowing and Loan Scams**{} - -They ask to “borrow,” then vanish. - - -**Avoid it** -- Never loan items to strangers -- Collateral does not make it safe -- Best choice: do not loan gear at all - - -{yellow}**Item Switching**{} - -Show good item, switch to a fake copy. - - -**Avoid it** -- Check item before you accept -- If they cancel and redo, inspect again -- Take your time - - -{yellow}**Rank Selling**{} - -“Give me coins and I give rank.” - - -**Avoid it** -- Ranks only from store.hypixel.net -- Players cannot give ranks -- Always a scam - - -{yellow}**Dungeon Carry Scams**{} - -No carry after payment or reverse. - - -**Avoid it** -- Use trusted Discord carry services -- Check Catacombs level -- Agree payment first - - -{yellow}**Co-op Island Theft**{yellow} - -They join, steal, or kick you. - - -**Avoid it** -- Never co-op strangers -- Only co-op trusted real friends -- `/coopadd` gives them your island access - - -{yellow}**Phishing Links**{} - -Fake login sites steal your account. - - -**Avoid it** -- Never click chat links -- Login only at hypixel.net and minecraft.net -- Hypixel staff do not DM you in-game - - - - -{gold}**Red Flags** 🚩{} - -Watch out for: - -- They ask you to turn off API -- Trade needs many steps -- They rush you -- “Only now” or “last chance” -- They ask for collateral but have recipes unlocked -- New account with rich gear -- Trade canceling over and over -- Pressure to hurry - - - - -{gold}**Reporting Scammers**{} - -Do this if scammed or see one: - -1. Take screenshots (F2) -2. Use `/report [name] [reason]` -3. Report on forums with proof -4. Warn others in guild or party - -{red}You will not get items back. Reporting protects others.{} - - - - -{gold}**Tools To Protect You**{} - -- **Skyblocker** – price info -- **SkyCrypt** – check players - - - -{gold}**Stay Safe**{} - -Stay calm when trading. - -Check items, check players, trust your gut. - -If something feels wrong, stop. - -{gold}**Your items are your responsibility. Stay sharp. Trade safe.**{} \ No newline at end of file diff --git a/src/main/resources/assets/packcore/markdown/useful_information.md b/src/main/resources/assets/packcore/markdown/useful_information.md deleted file mode 100644 index 5bd532a..0000000 --- a/src/main/resources/assets/packcore/markdown/useful_information.md +++ /dev/null @@ -1,150 +0,0 @@ -{gold}**Skyblock Enhanced: Configuration & Tips**{} - -{yellow}**Skyblock Enhanced**{} is a large modpack with many mods. Even if it’s preconfigured and you’ve just made some tweaks, a preconfigured pack will never reach its full potential. Here are some tips on how to edit and configure the pack, plus advice on playing with mods. The pack includes default keybinds, which are listed below. - -{#275EF5}**Topics covered:**{} -- Useful keybinds -- In-game help guides -- GUI elements: how to move and disable them -- How to open a mod config -- How to search for features -- How to get more help - ---- - -{yellow}**Keybinds**{} - -Below are the main default keybinds included with the pack. -> You can always rebind these to your liking. - - -{gold}**CHAT PATCHES**{} - -{#275EF5}**GUI & Interface**{} -- **Right CTRL** – Move a chat Hud make it larger/move it -- **X** - Chat Peek - - - -{gold}**SKYBLOCKER**{} - -{#275EF5}**Wiki & Information**{} -- **F1** – Fandom Wiki Lookup (Hover over an item and press F1 to open its Fandom Wiki page) -- **F4** – Hypixel Wiki Lookup (Hover over an item and press F4 to open its Hypixel Wiki page) -- **F6** – Item Price Lookup (Hover over an item and press F6 to check its price) - -{#275EF5}**Item Management**{} -- **P** – Protect Item (Hover or hold an item to protect it. Prevents accidental drops but allows moving it around) -- **H** – Slot Lock (Hotbar) (Lock a hotbar slot to prevent moving/dropping items from that slot. Only affects the hotbar) - -{#275EF5}**Bazaar**{} -- **Z** – Bazaar Refresh - - - -{gold}**SKYHANNI**{} - -{#275EF5}**GUI & Interface**{} -- **F7** – Move a GUI element on-screen - -{#275EF5}**Garden Features (Garden only)**{} -- **Z** – Home Hotkey (Teleport to Garden home) -- **H** – Sethome Hotkey (Set your Garden home) -- **B** – Barn Hotkey (Teleport to the Garden barn) -- **V** – Teleport Hotkey (Warp to nearest plot with pests) - -{#275EF5}**Mining & Glacite Mineshaft**{} -- **Y** – Share Waypoint Location (Corpse – Glacite Mineshaft) -- **Z** – Next Spot Hotkey (Select next spot – Glacite Mineshaft) - -{#275EF5}**Fishing**{} -- **R** – Reset Timer Hotkey (Reset the fishing timer manually) - -{#275EF5}**Crimson Isles & Reputation**{} -- **H** – Show Reputation Helper (Crimson Isles) -- **Y** – Share Key (Share your Inquisitor waypoint) - -{#275EF5}**Crystal Hollows**{} -- **V** – Warp Key (Warp to nearest burrow waypoint) - -{#275EF5}**Farming & Visitors**{} -- **K** – Accept Hotkey (Accept visitor in Visitor GUI) -- **Left Control** – Bypass Key (Hold to bypass “Prevent Refusing” feature) - -{#275EF5}**Trapper Quest**{} -- **V** – Trapper Hotkey (Warp to Trapper’s Den or accept quest) -- **W** – Campfire Hotkey (Warp to campfire or show path) - -{#275EF5}**Utility**{} -- **X** – Reduce Mouse Sensitivity (Temporarily lowers sensitivity while held) -- **Left Shift** – Breakdown Hotkey (Show breakdown of all fortune sources on a tool) - - - -{gold}**SKYOCEAN**{} - -{#275EF5}**Item Search & Value**{} -- **O** – Open Item Search (Search your inventory, island chest, and storage) -- **J** – Open Item Value -- **C** – Set as Active Craft Helper Item - - - -{gold}**MODERN WARP MENU**{} -- **M** – Open the Modern Warp Menu - ---- - -{yellow}**In-game Help Guides**{} - -The pack includes built-in guides for updating and using mods. -Open them with `/packcore guides` or from the main menu button. - -> {#275EF5}Tip:{} There’s a guide showcasing many cool features from different mods in the pack, along with how to use them. If you’re interested in learning more about what’s included, definitely check it out! - ---- - -{yellow}**GUI Elements**{} - -Many mods display information on-screen via overlays. These overlays are useful but can feel overwhelming. - -Press **`G`** to open the GUI editor: - -- Most overlays come from **SkyHanni**, which has a full GUI editor. Open it with `/sh gui` or **`G`**. Move any visible overlays after opening the editor. -- To disable an overlay, right-click it in the GUI editor to access its config. -- Overlays from **SkyBlocker** can be managed with `/widgets`. To fully disable a widget, use `/skyblocker config` and search for its name. - -> {#275EF5}Tip:{} New overlays may appear when performing different actions. Keep in mind that what you see can change, and an area that looks empty might later display something new. - -If you need help, {#275EF5}[Join the Discord](https://discord.gg/pdwxyjTta7){} for support. - ---- - -{yellow}**How to Open a Mod Config**{} - -Each mod has its own config screen. You can access them in a few ways: -- Many mods have a chat command, e.g. `/modname`. -- Press **ESC → Mods** to see all installed mods, then scroll to the one you want to configure. - ---- - -{yellow}**How to Search for Features**{} - -Finding a specific feature can be tricky. Try these tips: -- Search by what the feature *does* (like “waypoints” or “dungeons”). -- If you don’t know which mod it’s in, try different ones. You’ll learn over time which mods cover which features. -- Start with **SkyHanni** and **SkyBlocker**, as they include most QoL and overlay features. - ---- - -{yellow}**How to Get More Help**{} - -Click the Discord icon button to join the support server. -Read the **Start Here** channel, then post your issue in the correct place. -Include a clear description and screenshots to get faster help. - ---- - -{#275EF5}**Extra Tip:**{} - -Keep a backup of your `config` folder. If something breaks, restore your old settings instead of starting over. The pack includes a built-in config manager accessible from the **main menu** or via `/packcore configmanager`. diff --git a/src/main/resources/assets/packcore/markdown/welcome.md b/src/main/resources/assets/packcore/markdown/welcome.md deleted file mode 100644 index 2191d71..0000000 --- a/src/main/resources/assets/packcore/markdown/welcome.md +++ /dev/null @@ -1,35 +0,0 @@ -{gold}**Welcome!**{} - -You’ve just installed {yellow}**SkyBlock Enhanced**{}! - -Let’s walk through a quick setup to make your Hypixel SkyBlock experience smooth and personal. - ---- - -{gold}**What to expect**{} - -- {yellow}SkyBlock Enhanced{} comes packed with quality-of-life mods to improve every part of the game. - But with so many mods, things can overlap or clash. - {gold}Good news:{} I’ve already done the fine-tuning so everything works well together. -- The pack is {yellow}pre-configured{} for your resolution. - Each resolution needs unique configs since overlays shift with screen size. - I’ve included ready-made configs for **1080p, 1440p, and 4K.** The modpack checks your resolution on first launch and applies the closest one automatically! - Look for a small box on the right showing which config loaded and if it worked. -- Didn’t get the right one? Continue the setup. Once you’re at the main menu, open the config manager. - You can switch configs or import community-made ones from Discord. - -{gold}Before:{} - ![Before preconfiguring](packcore:textures/lavender/images/before_default_configs.png,fit) - -{gold}After:{} - ![After preconfiguring](packcore:textures/lavender/images/after_default_configs.png,fit) - ---- - -{gold}**Next Steps**{} - -1. Choose your video preset: FPS, Balanced, Quality, or Shaders. -2. Pick your {yellow}tab design{}: SkyHanni (compact) or SkyBlocker (fancy). -3. Select the resource packs you want. -4. Check the {yellow}hotkeys{} and open the {gold}in-game guide{} to learn how to use and customize mods. -5. Apply your chosen settings! \ No newline at end of file diff --git a/src/main/resources/assets/packcore/textures/gui/SkyBlock-Enhanced-v6.png b/src/main/resources/assets/packcore/textures/gui/SkyBlock-Enhanced-v6.png deleted file mode 100644 index f6e7146..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/SkyBlock-Enhanced-v6.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/blank_button.png b/src/main/resources/assets/packcore/textures/gui/menu/blank_button.png deleted file mode 100644 index a37aba3..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/menu/blank_button.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/arrow_down.png b/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/arrow_down.png deleted file mode 100644 index 0fe1d7e..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/arrow_down.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/arrow_up.png b/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/arrow_up.png deleted file mode 100644 index 37adfcc..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/arrow_up.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/scrollbar.png b/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/scrollbar.png deleted file mode 100644 index 063b281..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/scrollbar.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/slider.png b/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/slider.png deleted file mode 100644 index ad79afc..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/slider.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/track.png b/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/track.png deleted file mode 100644 index 2330e94..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/menu/scrollbar/track.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/performance/balanced.png b/src/main/resources/assets/packcore/textures/gui/performance/balanced.png new file mode 100644 index 0000000..805fb81 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/performance/balanced.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/performance/balanced_shaders.png b/src/main/resources/assets/packcore/textures/gui/performance/balanced_shaders.png new file mode 100644 index 0000000..0f4d2e2 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/performance/balanced_shaders.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/performance/max_fps.png b/src/main/resources/assets/packcore/textures/gui/performance/max_fps.png new file mode 100644 index 0000000..4fbff5a Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/performance/max_fps.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/performance/quality.png b/src/main/resources/assets/packcore/textures/gui/performance/quality.png new file mode 100644 index 0000000..05d0b01 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/performance/quality.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/performance/quality_shaders.png b/src/main/resources/assets/packcore/textures/gui/performance/quality_shaders.png new file mode 100644 index 0000000..9060164 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/performance/quality_shaders.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/assets/sbe_logo.png b/src/main/resources/assets/packcore/textures/gui/sprites/assets/sbe_logo.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/assets/sbe_logo.png rename to src/main/resources/assets/packcore/textures/gui/sprites/assets/sbe_logo.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/blank_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/blank_gray_button.png new file mode 100644 index 0000000..d5d6c48 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/blank_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/blank_idle_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/blank_red_button.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/blank_idle_button.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/blank_red_button.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/continue_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/continue_gray_button.png new file mode 100644 index 0000000..fc5ebd9 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/continue_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/disabled_blank_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/disabled_blank_gray_button.png new file mode 100644 index 0000000..c1afc85 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/disabled_blank_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/disabled_red_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/disabled_red_button.png new file mode 100644 index 0000000..ff1ac58 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/disabled_red_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_blank_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_blank_gray_button.png new file mode 100644 index 0000000..194dc8b Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_blank_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_continue_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_continue_gray_button.png new file mode 100644 index 0000000..e3b8037 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_continue_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_previous_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_previous_gray_button.png new file mode 100644 index 0000000..b16590d Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_previous_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/hover_idle_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_red_button.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/hover_idle_button.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/hover_red_button.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/previous_gray_button.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/previous_gray_button.png new file mode 100644 index 0000000..b07eed6 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/previous_gray_button.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/x.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/x.png new file mode 100644 index 0000000..b180145 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/x.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/xhover.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/xhover.png new file mode 100644 index 0000000..653b0dc Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/buttons/xhover.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/menu/discord_icon.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/discord_icon.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/discord_icon.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/discord_icon.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/divider.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/divider.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/divider.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/divider.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/frame.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/frame.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/frame.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/frame.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/github_icon.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/github_icon.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/github_icon.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/github_icon.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/guide_icon.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/guide_icon.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/guide_icon.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/guide_icon.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/info_box.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/info_box.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/info_box.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/info_box.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/modrinth_icon.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/modrinth_icon.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/modrinth_icon.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/modrinth_icon.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/notif_box.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/notif_box.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/notif_box.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/notif_box.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/settings_icon.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/settings_icon.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/settings_icon.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/settings_icon.png diff --git a/src/main/resources/assets/packcore/textures/gui/menu/update_icon.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/update_icon.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/menu/update_icon.png rename to src/main/resources/assets/packcore/textures/gui/sprites/menu/update_icon.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/menu/update_icon_available.png b/src/main/resources/assets/packcore/textures/gui/sprites/menu/update_icon_available.png new file mode 100644 index 0000000..3d53a91 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/menu/update_icon_available.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/title/SkyBlockEnhanced_title1.png b/src/main/resources/assets/packcore/textures/gui/sprites/title/SkyBlockEnhanced_title1.png new file mode 100644 index 0000000..402ae85 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/title/SkyBlockEnhanced_title1.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/title/title.png b/src/main/resources/assets/packcore/textures/gui/sprites/title/title.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/title/title.png rename to src/main/resources/assets/packcore/textures/gui/sprites/title/title.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/box.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/box.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/box.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/box.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/frame.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/frame.png new file mode 100644 index 0000000..75f54db Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/frame.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/info_box.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/info_box.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/info_box.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/info_box.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/item_bg_circular.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_background_preview/circle.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/item_bg_circular.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_background_preview/circle.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/item_bg_none.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_background_preview/none.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/item_bg_none.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_background_preview/none.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/item_bg_square.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_background_preview/square.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/item_bg_square.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_background_preview/square.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_circular.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_circular.png new file mode 100644 index 0000000..13259e7 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_circular.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_none.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_none.png new file mode 100644 index 0000000..e0c2522 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_none.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_square.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_square.png new file mode 100644 index 0000000..26f7cec Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/item_bg_square.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/minimal.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/minimal.png new file mode 100644 index 0000000..770b0c7 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/minimal.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/modern.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/modern.png new file mode 100644 index 0000000..f092127 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/modern.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/modern_minimal.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/modern_minimal.png new file mode 100644 index 0000000..ac1baa4 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/menu_preview/modern_minimal.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_active.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_active.png new file mode 100644 index 0000000..b089f66 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_active.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_locked.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_locked.png new file mode 100644 index 0000000..e8afe3e Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_locked.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_visited.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_visited.png new file mode 100644 index 0000000..53f7554 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/node_visited.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/skyblocker_tab.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/skyblocker_tab.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/skyblocker_tab.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/skyblocker_tab.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/skyhanni_tab.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/skyhanni_tab.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/skyhanni_tab.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/skyhanni_tab.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/small_info_box.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/small_info_box.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/small_info_box.png rename to src/main/resources/assets/packcore/textures/gui/sprites/wizard/small_info_box.png diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/storage_preview/overlay.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/storage_preview/overlay.png new file mode 100644 index 0000000..423774c Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/storage_preview/overlay.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/storage_preview/vanilla.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/storage_preview/vanilla.png new file mode 100644 index 0000000..441fa78 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/storage_preview/vanilla.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/sword_block_preview/disabled.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/sword_block_preview/disabled.png new file mode 100644 index 0000000..f51bd62 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/sword_block_preview/disabled.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/sword_block_preview/enabled.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/sword_block_preview/enabled.png new file mode 100644 index 0000000..f9b67c6 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/sword_block_preview/enabled.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/tab_preview/compact.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/tab_preview/compact.png new file mode 100644 index 0000000..d7e276c Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/tab_preview/compact.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/sprites/wizard/tab_preview/fancy.png b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/tab_preview/fancy.png new file mode 100644 index 0000000..f234f46 Binary files /dev/null and b/src/main/resources/assets/packcore/textures/gui/sprites/wizard/tab_preview/fancy.png differ diff --git a/src/main/resources/assets/packcore/textures/gui/title/main_menu_background.png b/src/main/resources/assets/packcore/textures/gui/title_menu_background.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/title/main_menu_background.png rename to src/main/resources/assets/packcore/textures/gui/title_menu_background.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/welcome_bg.png b/src/main/resources/assets/packcore/textures/gui/welcome_bg.png similarity index 100% rename from src/main/resources/assets/packcore/textures/gui/wizard/welcome_bg.png rename to src/main/resources/assets/packcore/textures/gui/welcome_bg.png diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/button.png b/src/main/resources/assets/packcore/textures/gui/wizard/button.png deleted file mode 100644 index ad6c878..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/wizard/button.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/continue.png b/src/main/resources/assets/packcore/textures/gui/wizard/continue.png deleted file mode 100644 index 92c83eb..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/wizard/continue.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/gui/wizard/previous.png b/src/main/resources/assets/packcore/textures/gui/wizard/previous.png deleted file mode 100644 index 0011cc7..0000000 Binary files a/src/main/resources/assets/packcore/textures/gui/wizard/previous.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/after_default_configs.png b/src/main/resources/assets/packcore/textures/lavender/images/after_default_configs.png deleted file mode 100644 index 0018097..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/after_default_configs.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/before_default_configs.png b/src/main/resources/assets/packcore/textures/lavender/images/before_default_configs.png deleted file mode 100644 index bba7084..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/before_default_configs.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/f3_memory_usage.png b/src/main/resources/assets/packcore/textures/lavender/images/f3_memory_usage.png deleted file mode 100644 index d7116f8..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/f3_memory_usage.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_custom_arguments.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_custom_arguments.png deleted file mode 100644 index c9b36ab..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_custom_arguments.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_default_memory.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_default_memory.png deleted file mode 100644 index 10d2c74..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_default_memory.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_folder.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_folder.png deleted file mode 100644 index 629e6da..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_folder.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_memory_slider.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_memory_slider.png deleted file mode 100644 index 936a04f..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_memory_slider.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_settings.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_settings.png deleted file mode 100644 index 0f88796..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_instance_settings.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_logs_tab.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_logs_tab.png deleted file mode 100644 index e050212..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_logs_tab.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_settings_sidebar.png b/src/main/resources/assets/packcore/textures/lavender/images/modrinth_settings_sidebar.png deleted file mode 100644 index de03438..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/modrinth_settings_sidebar.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/prism_default_memory.png b/src/main/resources/assets/packcore/textures/lavender/images/prism_default_memory.png deleted file mode 100644 index 08969a3..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/prism_default_memory.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/prism_edit_instance.png b/src/main/resources/assets/packcore/textures/lavender/images/prism_edit_instance.png deleted file mode 100644 index 48a3125..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/prism_edit_instance.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/prism_instance_folder.png b/src/main/resources/assets/packcore/textures/lavender/images/prism_instance_folder.png deleted file mode 100644 index 49500b9..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/prism_instance_folder.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/prism_instance_memory.png b/src/main/resources/assets/packcore/textures/lavender/images/prism_instance_memory.png deleted file mode 100644 index c0eb96e..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/prism_instance_memory.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/prism_java_arguments.png b/src/main/resources/assets/packcore/textures/lavender/images/prism_java_arguments.png deleted file mode 100644 index 528f7ee..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/prism_java_arguments.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/prism_settings_button.png b/src/main/resources/assets/packcore/textures/lavender/images/prism_settings_button.png deleted file mode 100644 index df56fe4..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/prism_settings_button.png and /dev/null differ diff --git a/src/main/resources/assets/packcore/textures/lavender/images/task_manager_memory.png b/src/main/resources/assets/packcore/textures/lavender/images/task_manager_memory.png deleted file mode 100644 index 9494c33..0000000 Binary files a/src/main/resources/assets/packcore/textures/lavender/images/task_manager_memory.png and /dev/null differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c1e98ef..4c212d1 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -14,7 +14,7 @@ "environment": "client", "entrypoints": { "preLaunch": [ - "com.github.kd_gaming1.packcore.prelaunch.PreLaunchWizardInitializer" + "com.github.kd_gaming1.packcore.PackCorePreLaunch" ], "client": [ "com.github.kd_gaming1.packcore.PackCore" @@ -22,14 +22,12 @@ }, "mixins": [ "packcore.mixins.json" ], "depends": { - "fabricloader": ">=${fabric_loader}", + "fabricloader": ">=${fabricloader}", "minecraft": "${minecraft}", - "midnightlib": ">=${midnightlib_version}", - "owo": ">=${owo_version}" + "uilib": ">=${uilib_version}" }, "suggests": { - "sodium": ">=${sodium_version}", - "iris": ">=${iris_version}", - "modmenu": ">=${modmenu_version}" + "scamscreener": "*", + "scaleme": "*" } } diff --git a/src/main/resources/packcore.mixins.json b/src/main/resources/packcore.mixins.json index fa3e327..845cff4 100644 --- a/src/main/resources/packcore.mixins.json +++ b/src/main/resources/packcore.mixins.json @@ -2,12 +2,10 @@ "required": true, "package": "com.github.kd_gaming1.packcore.mixin", "compatibilityLevel": "JAVA_21", - "mixins": [ - "CrashReportMixin" - ], + "mixins": [], "injectors": { "defaultRequire": 1 }, "client": [ ] -} \ No newline at end of file +} diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 57ac966..777ba2b 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -10,7 +10,7 @@ stonecutter tasks { order("publishCurseforge") } -stonecutter active "1.21.10" +stonecutter active "1.21.11" stonecutter parameters { swaps["mod_version"] = "\"" + property("mod.version") + "\";" diff --git a/versions/1.21.10/gradle.properties b/versions/1.21.10/gradle.properties deleted file mode 100644 index f1ac439..0000000 --- a/versions/1.21.10/gradle.properties +++ /dev/null @@ -1,14 +0,0 @@ -deps.yarn_mappings=1.21.10+build.2 -deps.fabric_api=0.138.4+1.21.10 - -deps.midnightlib_version=1.9.2+1.21.10-fabric -deps.lavender_md_version=0.1.2+1.21.8 -deps.modmenu_version=16.0.0 -deps.owo_version=0.12.24+1.21.9 - -deps.sodium_version=mc1.21.10-0.7.3-fabric -deps.iris_version=1.9.7+1.21.10-fabric - -mod.mc_dep=>=1.21.9 <=1.21.10 -mod.mc_title=1.21.10 -mod.mc_targets=1.21.9 1.21.10 \ No newline at end of file diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties index 453eb26..5b21535 100644 --- a/versions/1.21.11/gradle.properties +++ b/versions/1.21.11/gradle.properties @@ -1,14 +1,14 @@ -deps.yarn_mappings=1.21.11+build.4 -deps.fabric_api=0.141.2+1.21.11 +deps.fabric_api=0.141.3+1.21.11 deps.midnightlib_version=1.9.2+1.21.11-fabric +deps.hm_api_version=1.0.1+1.21.2 deps.modmenu_version=17.0.0-beta.2 -deps.owo_version=0.13.0+1.21.11 -deps.lavender_md_version=0.1.2+1.21.11 +deps.uilib_version=19.1.0 +deps.scaleme_version=3.0.0 deps.sodium_version=mc1.21.11-0.8.4-fabric deps.iris_version=1.10.5+1.21.11-fabric -mod.mc_dep=1.21.11 +mod.mc_dep==1.21.11 mod.mc_title=1.21.11 -mod.mc_targets=1.21.11 \ No newline at end of file +mod.mc_targets=1.21.11