Compare commits
6 Commits
58919ced40
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21453d194 | ||
|
|
64edfa4d73 | ||
|
|
99412b2876 | ||
|
|
aaa562f114 | ||
|
|
1a79c54cfb | ||
|
|
c0fd2a2787 |
99
.cursor/rules/changelog.mdc
Normal file
99
.cursor/rules/changelog.mdc
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
globs: CHANGELOG.md
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Changelog Management
|
||||
|
||||
## Overview
|
||||
The changelog is located at `docs/CHANGELOG.md` and follows a structured format that integrates with the automated release workflow.
|
||||
|
||||
## Structure
|
||||
- Newest releases at the **TOP** of the file
|
||||
- Each release has a `## [version] - date` header
|
||||
- Follow semantic versioning: `[MAJOR.MINOR.PATCH]`
|
||||
- Use ISO date format: `YYYY-MM-DD`
|
||||
|
||||
## Adding New Releases
|
||||
|
||||
### Before Creating a Release
|
||||
1. **Update `docs/CHANGELOG.md`** - Add new version section at the TOP
|
||||
2. **Use this format:**
|
||||
```markdown
|
||||
## [X.Y.Z] - YYYY-MM-DD
|
||||
|
||||
### 🎉 Added
|
||||
- New features
|
||||
|
||||
### 🔧 Changed
|
||||
- Modifications to existing features
|
||||
|
||||
### 🐛 Fixed
|
||||
- Bug fixes
|
||||
|
||||
### 🗑️ Removed
|
||||
- Deprecated features removed
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
- Changes that break backward compatibility
|
||||
```
|
||||
|
||||
### Section Headers (pick what's relevant)
|
||||
- `### 🎉 Added` - New features
|
||||
- `### 🔧 Changed` - Changes to existing functionality
|
||||
- `### 🐛 Fixed` - Bug fixes
|
||||
- `### 🗑️ Removed` - Removed features
|
||||
- `### ⚠️ Breaking Changes` - Breaking changes
|
||||
- `### 🔒 Security` - Security fixes
|
||||
- `### 📚 Documentation` - Documentation changes
|
||||
- `### ⚡ Performance` - Performance improvements
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
The release workflow (`.gitea/workflows/release.yml`) automatically:
|
||||
1. Extracts the **latest/topmost** changelog section (between first `##` and second `##`)
|
||||
2. Uses it as the release notes on Gitea
|
||||
3. Attaches the built JAR file
|
||||
|
||||
**Only the most recent section is extracted** - previous releases stay in the file for history.
|
||||
|
||||
## Example
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [1.0.2] - 2025-10-24
|
||||
|
||||
### 🐛 Fixed
|
||||
- Fixed permission check timeout issue
|
||||
- Resolved MOTD formatting bug
|
||||
|
||||
### ⚡ Performance
|
||||
- Optimized schedule calculation
|
||||
|
||||
## [1.0.1] - 2025-10-23
|
||||
|
||||
### 🎉 Added
|
||||
- Added French language support
|
||||
- New command `/hours status`
|
||||
|
||||
### 🔧 Changed
|
||||
- Improved error messages
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
1. **Update changelog:** Add new `## [X.Y.Z]` section at top of `docs/CHANGELOG.md`
|
||||
2. **Commit changes:** `git add -A && git commit -m "docs(changelog): update for vX.Y.Z"`
|
||||
3. **Run release script:** `./release.sh patch` (or `minor`/`major`)
|
||||
4. **Workflow handles the rest:** Extracts changelog, builds, creates release
|
||||
|
||||
## Important Rules
|
||||
|
||||
- ✅ **DO:** Add new releases at the TOP
|
||||
- ✅ **DO:** Use consistent section headers
|
||||
- ✅ **DO:** Include date and version number
|
||||
- ✅ **DO:** Keep old releases for history
|
||||
- ❌ **DON'T:** Edit old release sections
|
||||
- ❌ **DON'T:** Delete previous releases
|
||||
- ❌ **DON'T:** Add entries at the bottom
|
||||
247
.cursor/rules/commits.mdc
Normal file
247
.cursor/rules/commits.mdc
Normal file
@@ -0,0 +1,247 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
# Commit Message Format for PlayHours Mod
|
||||
|
||||
## Format Structure
|
||||
|
||||
```
|
||||
type(scope): emoji description
|
||||
|
||||
[Optional body with bullet points]
|
||||
- Detail 1
|
||||
- Detail 2
|
||||
|
||||
[Optional footer]
|
||||
```
|
||||
|
||||
## Commit Types & Emojis
|
||||
|
||||
### Core Types
|
||||
- **feat** ✨ - New feature
|
||||
- **fix** 🐛 - Bug fix
|
||||
- **docs** 📚 - Documentation changes
|
||||
- **refactor** ♻️ - Code refactoring (no functional change)
|
||||
- **test** 🧪 - Adding or updating tests
|
||||
- **chore** 🔧 - Maintenance tasks (dependencies, build, etc.)
|
||||
|
||||
### Additional Types
|
||||
- **perf** ⚡ - Performance improvements
|
||||
- **style** 💎 - Code style/formatting (no logic change)
|
||||
- **build** 📦 - Build system or dependencies
|
||||
- **ci** 🤖 - CI/CD workflow changes
|
||||
- **revert** ⏪ - Revert previous commit
|
||||
|
||||
## Scopes (Mod-Specific)
|
||||
|
||||
### Feature Areas
|
||||
- **command** - Command system (`/hours` and subcommands)
|
||||
- **config** - Configuration files and handlers
|
||||
- **schedule** - Schedule calculation and enforcement
|
||||
- **login** - Login guard and blocking
|
||||
- **motd** - MOTD system and server list display
|
||||
- **warnings** - Warning and auto-kick system
|
||||
- **permissions** - Permission system and LuckPerms integration
|
||||
- **i18n** - Localization and translations
|
||||
- **exceptions** - Date exceptions handling
|
||||
- **lists** - Whitelist/blacklist management
|
||||
- **force** - Force mode functionality
|
||||
|
||||
### Technical Areas
|
||||
- **core** - Core logic and services
|
||||
- **events** - Event handlers (LoginGuard, TickScheduler, etc.)
|
||||
- **text** - Message formatting and locale loading
|
||||
- **gradle** - Build configuration
|
||||
- **workflow** - CI/CD and release automation
|
||||
- **mod** - Main mod class and initialization
|
||||
|
||||
### Documentation & Meta
|
||||
- **docs** - Documentation files
|
||||
- **changelog** - Changelog updates
|
||||
- **release** - Release preparation
|
||||
- **readme** - README updates
|
||||
|
||||
## Examples by Feature Area
|
||||
|
||||
### Commands
|
||||
```
|
||||
feat(command): ✨ add new /hours lists command for whitelist/blacklist management
|
||||
fix(command): 🐛 fix permission check in DayCommandBuilder
|
||||
refactor(command): ♻️ simplify command builder hierarchy
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```
|
||||
feat(config): ✨ add support for timezone configuration
|
||||
fix(config): 🐛 fix TOML parsing error with midnight-spanning ranges
|
||||
docs(config): 📚 add inline comments to default config
|
||||
```
|
||||
|
||||
### Schedule System
|
||||
```
|
||||
feat(schedule): ✨ implement date exceptions for holidays
|
||||
fix(schedule): 🐛 fix midnight-spanning time range calculation
|
||||
perf(schedule): ⚡ optimize schedule caching mechanism
|
||||
```
|
||||
|
||||
### Login & Access Control
|
||||
```
|
||||
feat(login): ✨ add custom kick messages per denial reason
|
||||
fix(login): 🐛 fix race condition in login guard event handler
|
||||
refactor(login): ♻️ extract access checker to separate service
|
||||
```
|
||||
|
||||
### MOTD System
|
||||
```
|
||||
feat(motd): ✨ add countdown timer when server closing soon
|
||||
fix(motd): 🐛 fix color code parsing for custom MOTD format
|
||||
style(motd): 💎 improve MOTD formatter code readability
|
||||
```
|
||||
|
||||
### Warnings & Auto-Kick
|
||||
```
|
||||
feat(warnings): ✨ add configurable warning intervals
|
||||
fix(warnings): 🐛 fix warning broadcaster not respecting exempt permission
|
||||
perf(warnings): ⚡ reduce tick scheduler overhead
|
||||
```
|
||||
|
||||
### Permissions
|
||||
```
|
||||
feat(permissions): ✨ add LuckPerms soft dependency detection
|
||||
fix(permissions): 🐛 fix timeout protection in permission checker
|
||||
docs(permissions): 📚 document all permission nodes in PERMISSIONS.md
|
||||
```
|
||||
|
||||
### Localization
|
||||
```
|
||||
feat(i18n): ✨ add Spanish language support (es_es.json)
|
||||
fix(i18n): 🐛 fix time format for French locale
|
||||
refactor(text): ♻️ improve message formatter placeholder handling
|
||||
```
|
||||
|
||||
### Build & Release
|
||||
```
|
||||
build(gradle): 📦 update Forge to 47.4.10
|
||||
ci(workflow): 🤖 add automated release workflow for Gitea
|
||||
chore(release): 🔧 prepare v1.0.1 release
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```
|
||||
docs(installation): 📚 add troubleshooting section
|
||||
docs(examples): 📚 add real-world configuration examples
|
||||
docs(changelog): 📚 update changelog for v1.0.2
|
||||
```
|
||||
|
||||
## Commit Message Rules
|
||||
|
||||
### Title (Required)
|
||||
- **Format:** `type(scope): emoji description`
|
||||
- **Length:** Max 72 characters
|
||||
- **Language:** English only
|
||||
- **Case:** Lowercase for type and scope
|
||||
- **Description:** Start with verb in past tense (added, fixed, updated)
|
||||
- **Emoji:** Include relevant emoji after scope
|
||||
|
||||
### Body (Optional but Recommended)
|
||||
- Add blank line after title
|
||||
- Use bullet points for multiple changes
|
||||
- Explain **what** and **why**, not how
|
||||
- Reference issues/PRs if applicable
|
||||
- Provide as much detail as possible
|
||||
|
||||
### Footer (Optional)
|
||||
- Breaking changes: `BREAKING CHANGE: description`
|
||||
- Close issues: `Closes #123`
|
||||
- Co-authors: `Co-authored-by: name <email>`
|
||||
|
||||
## Multi-Feature Commits
|
||||
|
||||
When committing multiple changes:
|
||||
|
||||
```
|
||||
feat(command,config): ✨ add support for per-player schedule overrides
|
||||
|
||||
- Added new command `/hours player <name> set schedule`
|
||||
- Updated config to support player-specific sections
|
||||
- Added permission node playhours.admin.player
|
||||
- Updated documentation with new feature
|
||||
```
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
```
|
||||
feat(config)!: ✨ redesign configuration structure for better clarity
|
||||
|
||||
BREAKING CHANGE: Configuration file structure has changed.
|
||||
Old format will not work. Please regenerate config or migrate manually.
|
||||
|
||||
- Separated general settings from day-specific settings
|
||||
- Renamed `default_periods` to `defaults.periods`
|
||||
- Moved timezone from general to defaults section
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Bug Fixes
|
||||
```
|
||||
fix(login): 🐛 fix player kicked immediately after login
|
||||
|
||||
- Fixed race condition in LoginGuard event handler
|
||||
- Added delay before checking schedule on login
|
||||
- Resolves issue where players couldn't join during open hours
|
||||
```
|
||||
|
||||
### New Features
|
||||
```
|
||||
feat(exceptions): ✨ add recurring date exceptions support
|
||||
|
||||
- Support weekly/monthly recurring exceptions
|
||||
- Add new configuration syntax for recurrence
|
||||
- Update exception handler to process recurring dates
|
||||
- Add validation for recurring patterns
|
||||
```
|
||||
|
||||
### Refactoring
|
||||
```
|
||||
refactor(core): ♻️ extract schedule logic into dedicated service
|
||||
|
||||
- Created ScheduleService to centralize schedule operations
|
||||
- Moved calculation logic from LoginGuard to ScheduleCalculator
|
||||
- Improved testability and separation of concerns
|
||||
```
|
||||
|
||||
### Performance
|
||||
```
|
||||
perf(schedule): ⚡ cache timezone conversions to reduce CPU usage
|
||||
|
||||
- Implement timezone conversion cache
|
||||
- Reduce ZonedDateTime allocations
|
||||
- Benchmarked 40% improvement in schedule checks
|
||||
```
|
||||
|
||||
## Anti-Patterns (DON'T)
|
||||
|
||||
❌ `updated stuff`
|
||||
❌ `fix bug`
|
||||
❌ `feat: added new feature` (missing scope)
|
||||
❌ `FIX(LOGIN): Fixed login` (wrong case)
|
||||
❌ `feat(command): Add feature` (missing emoji)
|
||||
❌ `wip` (not descriptive)
|
||||
|
||||
## Good Examples (DO)
|
||||
|
||||
✅ `feat(command): ✨ add /hours status command with detailed server info`
|
||||
✅ `fix(schedule): 🐛 fix midnight-spanning time range calculation`
|
||||
✅ `docs(readme): 📚 update quick start guide with v1.0.1 changes`
|
||||
✅ `perf(events): ⚡ optimize tick scheduler to run every 20 ticks instead of 1`
|
||||
✅ `chore(release): 🔧 prepare v1.0.2`
|
||||
|
||||
## Forge Mod Specific Tips
|
||||
|
||||
- Always test changes on a running server before committing
|
||||
- Update relevant documentation when changing features
|
||||
- Update language files when adding new messages
|
||||
- Bump version in build.gradle for releases
|
||||
- Update CHANGELOG.md before release commits
|
||||
- Run `./gradlew build` to ensure no build errors
|
||||
49
.gitea/workflows/release.yml
Normal file
49
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: sh gradlew build --no-daemon
|
||||
|
||||
- name: Extract Changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# Extract the latest version section from CHANGELOG.md
|
||||
if [ -f "docs/CHANGELOG.md" ]; then
|
||||
# Get content between first ## and second ## (or end of file)
|
||||
CHANGELOG=$(sed -n '/^## \[/,/^## \[/p' docs/CHANGELOG.md | sed '$d' | tail -n +2)
|
||||
if [ -z "$CHANGELOG" ]; then
|
||||
CHANGELOG=$(sed -n '/^## \[/,$p' docs/CHANGELOG.md | tail -n +2)
|
||||
fi
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CHANGELOG" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "RELEASE_NOTES=Release ${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.TOKEN }}
|
||||
files: build/libs/*.jar
|
||||
draft: false
|
||||
prerelease: false
|
||||
body: ${{ steps.changelog.outputs.RELEASE_NOTES }}
|
||||
|
||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# PlayHours - Server Operation Hours Enforcement
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Minecraft:** 1.20.1
|
||||
**Forge:** 47.4.10
|
||||
**Java:** 17+
|
||||
**Author:** Mr-KayJayDee
|
||||
|
||||
A production-ready server-side Forge mod that enforces configurable server open hours with per-day schedules, automatic warnings, login blocking, and comprehensive admin commands.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Download** `playhours-1.0.0.jar` from the `build/libs/` directory
|
||||
2. Place in your server's `mods/` folder
|
||||
3. Start the server to generate the default config
|
||||
4. Edit `config/playhours.toml` to your needs
|
||||
5. Use `/hours reload` or restart the server
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
- **🕒 Schedule Enforcement** - Per-day schedules with midnight-spanning support
|
||||
- **🚫 Login Control** - Deny logins outside open hours with clear messages
|
||||
- **⚠️ Warnings & Auto-Kick** - Configurable warnings and automatic player removal
|
||||
- **🔧 Force Modes** - Override schedule (NORMAL/FORCE_OPEN/FORCE_CLOSED)
|
||||
- **👥 Whitelist/Blacklist** - Player access control independent of schedule
|
||||
- **📅 Date Exceptions** - Special open/closed dates for holidays and events
|
||||
- **🌍 Multi-Language** - English and French support with smart time formatting
|
||||
- **🔑 Permissions** - LuckPerms integration with ops fallback
|
||||
- **📢 MOTD Display** - Dynamic server list information with schedule status
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### Core Documentation
|
||||
- **[Installation Guide](docs/INSTALLATION.md)** - Setup and installation instructions
|
||||
- **[Configuration Guide](docs/CONFIGURATION.md)** - Complete configuration reference
|
||||
- **[Commands Reference](docs/COMMANDS.md)** - All available commands and usage
|
||||
- **[Features Overview](docs/FEATURES.md)** - Detailed feature explanations
|
||||
|
||||
### Advanced Documentation
|
||||
- **[MOTD System](docs/MOTD.md)** - Message of the Day configuration and customization
|
||||
- **[Permissions System](docs/PERMISSIONS.md)** - Permission nodes and LuckPerms integration
|
||||
- **[Technical Details](docs/TECHNICAL.md)** - Architecture, limitations, and implementation details
|
||||
- **[Usage Examples](docs/EXAMPLES.md)** - Real-world scenarios and configurations
|
||||
|
||||
### Additional Resources
|
||||
- **[Changelog](docs/CHANGELOG.md)** - Version history and updates
|
||||
- **[Building from Source](docs/TECHNICAL.md#building-from-source)** - Compilation instructions
|
||||
|
||||
## 🎯 Common Use Cases
|
||||
|
||||
### Basic Server Hours
|
||||
```toml
|
||||
[defaults]
|
||||
periods = ["09:00 AM-06:00 PM"]
|
||||
|
||||
[days]
|
||||
saturday = ["02:00 PM-11:59 PM"]
|
||||
sunday = ["02:00 PM-10:00 PM"]
|
||||
```
|
||||
|
||||
### Weekend Events
|
||||
```toml
|
||||
[exceptions]
|
||||
open_dates = ["2025-12-31 08:00 PM-11:59 PM"] # New Year's Eve
|
||||
closed_dates = ["2025-12-25"] # Christmas Day
|
||||
```
|
||||
|
||||
### Maintenance Mode
|
||||
```
|
||||
/hours force close
|
||||
```
|
||||
|
||||
## 🔧 Quick Commands
|
||||
|
||||
| Command | Description | Permission |
|
||||
|---------|-------------|------------|
|
||||
| `/hours status` | Show current server status | `playhours.view` |
|
||||
| `/hours force open` | Override to always open | `playhours.admin` |
|
||||
| `/hours force close` | Override to always closed | `playhours.admin` |
|
||||
| `/hours reload` | Reload configuration | `playhours.admin` |
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Minecraft:** 1.20.1
|
||||
- **Forge:** 47.4.10+
|
||||
- **Java:** 17 or 23
|
||||
- **Server-side only** - No client mod required
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For issues, feature requests, or questions, please refer to the detailed documentation or contact the author.
|
||||
|
||||
## 📄 License
|
||||
|
||||
All Rights Reserved © 2025 Mr-KayJayDee
|
||||
|
||||
---
|
||||
|
||||
*For detailed information about any aspect of the mod, please refer to the specific documentation files listed above.*
|
||||
46
README.txt
46
README.txt
@@ -1,46 +0,0 @@
|
||||
|
||||
Source installation information for modders
|
||||
-------------------------------------------
|
||||
This code follows the Minecraft Forge installation methodology. It will apply
|
||||
some small patches to the vanilla MCP source code, giving you and it access
|
||||
to some of the data and functions you need to build a successful mod.
|
||||
|
||||
Note also that the patches are built against "un-renamed" MCP source code (aka
|
||||
SRG Names) - this means that you will not be able to read them directly against
|
||||
normal code.
|
||||
|
||||
Setup Process:
|
||||
==============================
|
||||
|
||||
Step 1: Open your command-line and browse to the folder where you extracted the zip file.
|
||||
|
||||
Step 2: You're left with a choice.
|
||||
If you prefer to use Eclipse:
|
||||
1. Run the following command: `./gradlew genEclipseRuns`
|
||||
2. Open Eclipse, Import > Existing Gradle Project > Select Folder
|
||||
or run `gradlew eclipse` to generate the project.
|
||||
|
||||
If you prefer to use IntelliJ:
|
||||
1. Open IDEA, and import project.
|
||||
2. Select your build.gradle file and have it import.
|
||||
3. Run the following command: `./gradlew genIntellijRuns`
|
||||
4. Refresh the Gradle Project in IDEA if required.
|
||||
|
||||
If at any point you are missing libraries in your IDE, or you've run into problems you can
|
||||
run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything
|
||||
(this does not affect your code) and then start the process again.
|
||||
|
||||
Mapping Names:
|
||||
=============================
|
||||
By default, the MDK is configured to use the official mapping names from Mojang for methods and fields
|
||||
in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this
|
||||
license, if you do not agree with it you can change your mapping names to other crowdsourced names in your
|
||||
build.gradle. For the latest license text, refer to the mapping file itself, or the reference copy here:
|
||||
https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
|
||||
|
||||
Additional Resources:
|
||||
=========================
|
||||
Community Documentation: https://docs.minecraftforge.net/en/1.20.1/gettingstarted/
|
||||
LexManos' Install Video: https://youtu.be/8VEdtQLuLO0
|
||||
Forge Forums: https://forums.minecraftforge.net/
|
||||
Forge Discord: https://discord.minecraftforge.net/
|
||||
14
build.gradle
14
build.gradle
@@ -114,12 +114,7 @@ sourceSets.main.resources { srcDir 'src/generated/resources' }
|
||||
repositories {
|
||||
// Put repositories for dependencies here
|
||||
// ForgeGradle automatically adds the Forge maven and Maven Central for you
|
||||
|
||||
// If you have mod jar dependencies in ./libs, you can declare them as a repository like so.
|
||||
// See https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:flat_dir_resolver
|
||||
// flatDir {
|
||||
// dir 'libs'
|
||||
// }
|
||||
maven { url = 'https://repo.lucko.me/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -130,11 +125,8 @@ dependencies {
|
||||
// then special handling is done to allow a setup of a vanilla dependency without the use of an external repository.
|
||||
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
|
||||
|
||||
// Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings
|
||||
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
|
||||
// compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}")
|
||||
// compileOnly fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}")
|
||||
// runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}-forge:${jei_version}")
|
||||
// LuckPerms API (soft dependency)
|
||||
compileOnly 'net.luckperms:api:5.4'
|
||||
|
||||
// Example mod dependency using a mod jar from ./libs with a flat dir repository
|
||||
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
|
||||
|
||||
195
docs/CHANGELOG.md
Normal file
195
docs/CHANGELOG.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Changelog
|
||||
|
||||
This document tracks all changes and updates to the PlayHours mod.
|
||||
|
||||
## [1.0.0] - 2025-01-23
|
||||
|
||||
### 🎉 Initial Release
|
||||
|
||||
This is the first public release of PlayHours, a comprehensive server operation hours enforcement mod for Minecraft Forge.
|
||||
|
||||
### ✨ Features Added
|
||||
|
||||
#### Core Functionality
|
||||
- **Schedule Enforcement** - Per-day schedules with midnight-spanning support
|
||||
- **Login Control** - Deny logins outside open hours with clear messages
|
||||
- **Warnings & Auto-Kick** - Configurable warnings and automatic player removal
|
||||
- **Force Modes** - Override schedule (NORMAL/FORCE_OPEN/FORCE_CLOSED)
|
||||
- **Whitelist/Blacklist** - Player access control independent of schedule
|
||||
- **Date Exceptions** - Special open/closed dates for holidays and events
|
||||
|
||||
#### Multi-Language Support
|
||||
- **English (en_us)** - 12-hour AM/PM format with full localization
|
||||
- **French (fr_fr)** - 24-hour format with full localization
|
||||
- **Smart Time Formatting** - Automatically adapts to locale
|
||||
- **Humanized Messages** - User-friendly status messages instead of raw booleans
|
||||
|
||||
#### Permission System
|
||||
- **LuckPerms Integration** - Soft dependency with automatic detection
|
||||
- **Vanilla Ops Fallback** - Works with or without LuckPerms
|
||||
- **Permission Nodes** - `playhours.admin`, `playhours.view`, `playhours.exempt`
|
||||
- **Timeout Protection** - Prevents blocking on permission checks
|
||||
|
||||
#### MOTD System
|
||||
- **Dynamic Server List** - Real-time schedule information in server list
|
||||
- **Status Display** - Open/Closed status with color coding
|
||||
- **Next Open/Close Times** - Shows when server will open/close next
|
||||
- **Countdown Timer** - Minutes until close when closing soon
|
||||
- **Force Mode Indicators** - FORCE_OPEN/FORCE_CLOSED status display
|
||||
- **Custom Formatting** - Flexible format strings with placeholders
|
||||
- **Performance Optimized** - Updates every 60 seconds (configurable)
|
||||
|
||||
#### Command System
|
||||
- **Comprehensive Commands** - Full `/hours` command tree
|
||||
- **Permission Integration** - Automatic permission checking
|
||||
- **Hot Reload** - `/hours reload` for configuration changes
|
||||
- **Status Commands** - `/hours status` for current information
|
||||
- **Force Mode Commands** - `/hours force normal|open|close`
|
||||
- **Configuration Commands** - `/hours set` for various settings
|
||||
- **Schedule Commands** - `/hours set day` for per-day configuration
|
||||
- **Exception Commands** - `/hours exceptions` for date exceptions
|
||||
- **List Commands** - `/hours lists` for whitelist/blacklist management
|
||||
- **MOTD Commands** - `/hours motd` for MOTD control
|
||||
|
||||
#### Configuration System
|
||||
- **TOML Configuration** - Human-readable configuration format
|
||||
- **Hot Reload** - Apply changes without server restart
|
||||
- **Validation** - Comprehensive configuration validation
|
||||
- **Default Values** - Sensible defaults for all settings
|
||||
- **Documentation** - Extensive inline documentation
|
||||
|
||||
### 🔧 Technical Features
|
||||
|
||||
#### Architecture
|
||||
- **Modular Design** - Clean separation of concerns
|
||||
- **Event-Driven** - Efficient event handling system
|
||||
- **Performance Optimized** - Minimal server resource usage
|
||||
- **Error Handling** - Graceful degradation on errors
|
||||
- **Logging** - Comprehensive logging for debugging
|
||||
|
||||
#### Time Handling
|
||||
- **Timezone Support** - Full IANA timezone support
|
||||
- **Midnight Spanning** - Proper handling of time ranges across midnight
|
||||
- **DST Support** - Automatic daylight saving time handling
|
||||
- **Precise Calculations** - Accurate time-based schedule calculations
|
||||
|
||||
#### Schedule Logic
|
||||
- **Priority System** - Force mode → Blacklist → Whitelist → Exceptions → Schedule
|
||||
- **Caching** - Efficient schedule data caching
|
||||
- **Validation** - Time range validation and error handling
|
||||
- **Flexibility** - Support for complex schedule patterns
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
#### Comprehensive Documentation
|
||||
- **Installation Guide** - Step-by-step setup instructions
|
||||
- **Configuration Guide** - Complete configuration reference
|
||||
- **Commands Reference** - All available commands and usage
|
||||
- **Features Overview** - Detailed feature explanations
|
||||
- **MOTD System** - MOTD configuration and customization
|
||||
- **Permissions System** - Permission details and LuckPerms integration
|
||||
- **Technical Details** - Architecture and implementation details
|
||||
- **Usage Examples** - Real-world scenarios and configurations
|
||||
- **Changelog** - Version history and updates
|
||||
|
||||
#### Code Documentation
|
||||
- **JavaDoc** - Comprehensive code documentation
|
||||
- **Inline Comments** - Detailed implementation comments
|
||||
- **Architecture Notes** - Design decisions and rationale
|
||||
- **Performance Notes** - Optimization strategies and considerations
|
||||
|
||||
### 🛠️ Development
|
||||
|
||||
#### Build System
|
||||
- **Gradle Build** - Modern build system with wrapper
|
||||
- **Forge Integration** - Full Forge mod development support
|
||||
- **Dependency Management** - Proper dependency handling
|
||||
- **Version Management** - Semantic versioning
|
||||
|
||||
#### Code Quality
|
||||
- **Clean Code** - Well-structured and readable code
|
||||
- **Error Handling** - Comprehensive error handling
|
||||
- **Logging** - Appropriate logging levels
|
||||
- **Testing** - Manual testing and validation
|
||||
|
||||
### 🚨 Known Limitations
|
||||
|
||||
#### Technical Limitations
|
||||
- **Early Login Denial** - Forge 1.20.1 lacks pre-join network event
|
||||
- **Deprecation Warning** - `FMLJavaModLoadingContext.get()` deprecated
|
||||
- **MOTD Caching** - Some server list clients may cache MOTD content
|
||||
|
||||
#### Workarounds
|
||||
- **Login Guard** - Uses `PlayerLoggedInEvent` with immediate disconnection
|
||||
- **Compatibility** - Required for 1.20.1 compatibility
|
||||
- **Update Frequency** - MOTD updates every 60 seconds by default
|
||||
|
||||
### 📋 Requirements
|
||||
|
||||
#### System Requirements
|
||||
- **Java:** 17 or 23 (recommended: 17 LTS)
|
||||
- **Minecraft:** 1.20.1
|
||||
- **Forge:** 47.4.10 or higher
|
||||
- **Server:** Dedicated server (not single-player)
|
||||
|
||||
#### Mod Requirements
|
||||
- **Server-side only** - No client mod required
|
||||
- **Forge mod loader** - Not compatible with Fabric or other loaders
|
||||
- **Dedicated server** - Single-player mode not supported
|
||||
|
||||
### 🔄 Installation
|
||||
|
||||
#### Quick Start
|
||||
1. Download `playhours-1.0.0.jar` from `build/libs/`
|
||||
2. Place in server's `mods/` folder
|
||||
3. Start server to generate default config
|
||||
4. Edit `config/playhours.toml` to your needs
|
||||
5. Use `/hours reload` or restart server
|
||||
|
||||
#### Configuration
|
||||
- **Auto-generated** - Configuration file created on first startup
|
||||
- **Sensible defaults** - Works out of the box with minimal configuration
|
||||
- **Hot reload** - Apply changes without server restart
|
||||
- **Validation** - Configuration validation with helpful error messages
|
||||
|
||||
### 🔮 Future Plans
|
||||
|
||||
#### Potential Enhancements
|
||||
- **Additional Languages** - Support for more locales
|
||||
- **Advanced Scheduling** - More complex schedule patterns
|
||||
- **Integration APIs** - External system integration
|
||||
- **Performance Improvements** - Further optimization
|
||||
- **Feature Extensions** - Additional functionality
|
||||
|
||||
#### Version Roadmap
|
||||
- **1.1.0** - Additional language support
|
||||
- **1.2.0** - Advanced scheduling features
|
||||
- **2.0.0** - Major feature additions
|
||||
|
||||
### Release Notes
|
||||
|
||||
#### v1.0.0 Release Notes
|
||||
- **Initial release** - First public release
|
||||
- **Full feature set** - All planned features implemented
|
||||
- **Comprehensive documentation** - Complete documentation suite
|
||||
- **Production ready** - Tested and validated for production use
|
||||
|
||||
#### Breaking Changes
|
||||
- **None** - This is the initial release
|
||||
|
||||
#### Deprecations
|
||||
- **None** - This is the initial release
|
||||
|
||||
#### Security
|
||||
- **Permission system** - Secure permission checking
|
||||
- **Input validation** - Comprehensive input validation
|
||||
- **Error handling** - Secure error handling
|
||||
|
||||
#### Performance
|
||||
- **Optimized** - Performance optimized for production use
|
||||
- **Caching** - Efficient caching strategies
|
||||
- **Resource management** - Minimal resource usage
|
||||
|
||||
---
|
||||
|
||||
*For the latest updates and changes, check the repository or contact the author.*
|
||||
467
docs/COMMANDS.md
Normal file
467
docs/COMMANDS.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# Commands Reference
|
||||
|
||||
This guide covers all available commands in PlayHours, their usage, permissions, and examples.
|
||||
|
||||
## 📋 Command Overview
|
||||
|
||||
All PlayHours commands use the `/hours` root command. Permission checks are applied automatically.
|
||||
|
||||
### Permission Requirements
|
||||
|
||||
| Permission | Level | Description |
|
||||
|------------|-------|-------------|
|
||||
| `playhours.view` | Ops ≥1 | Read-only access to status |
|
||||
| `playhours.admin` | Ops ≥2 | Full administrative access |
|
||||
| `playhours.exempt` | Ops ≥2 | Bypass all schedule restrictions |
|
||||
|
||||
## 🔍 Status Commands
|
||||
|
||||
### `/hours status`
|
||||
|
||||
Shows current server status and schedule information.
|
||||
|
||||
**Permission:** `playhours.view` (ops ≥1)
|
||||
|
||||
**Output Example:**
|
||||
```
|
||||
Mode: NORMAL. Server open. Next close: 10:00 PM. Next open: Monday at 9:00 AM.
|
||||
Today's periods: 09:00 AM-06:00 PM, 11:00 PM-01:00 AM
|
||||
```
|
||||
|
||||
**Information Displayed:**
|
||||
- Current force mode
|
||||
- Server open/closed status
|
||||
- Next closing time (if open)
|
||||
- Next opening time (if closed)
|
||||
- Today's schedule periods
|
||||
|
||||
## 🔧 Force Mode Commands
|
||||
|
||||
### `/hours force normal`
|
||||
### `/hours force open`
|
||||
### `/hours force close`
|
||||
|
||||
Override the normal schedule enforcement.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Modes:**
|
||||
- `normal` - Follow configured schedule
|
||||
- `open` - Always allow access (24/7)
|
||||
- `close` - Always deny access (maintenance mode)
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/hours force open # Enable 24/7 access
|
||||
/hours force close # Maintenance mode
|
||||
/hours force normal # Return to normal schedule
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Force modes override schedule but not blacklist
|
||||
- Exempt players can still join in FORCE_CLOSED mode
|
||||
- Changes take effect immediately
|
||||
|
||||
## 🔄 Configuration Commands
|
||||
|
||||
### `/hours reload`
|
||||
|
||||
Reloads configuration from `config/playhours.toml` without restarting the server.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours reload
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Reloads all configuration settings
|
||||
- Rebuilds schedule cache
|
||||
- Reloads message translations
|
||||
- Applies changes immediately
|
||||
|
||||
**When to use:**
|
||||
- After editing `config/playhours.toml`
|
||||
- After changing timezone settings
|
||||
- After modifying schedule configuration
|
||||
|
||||
## ⚙️ Settings Commands
|
||||
|
||||
### `/hours set timezone <timezone>`
|
||||
|
||||
Change the server timezone.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set timezone America/New_York
|
||||
/hours set timezone Europe/London
|
||||
/hours set timezone Asia/Tokyo
|
||||
```
|
||||
|
||||
**Valid Timezones:**
|
||||
- `America/New_York` (Eastern Time)
|
||||
- `America/Chicago` (Central Time)
|
||||
- `America/Denver` (Mountain Time)
|
||||
- `America/Los_Angeles` (Pacific Time)
|
||||
- `Europe/London` (GMT/BST)
|
||||
- `Europe/Paris` (CET/CEST)
|
||||
- `Asia/Tokyo` (JST)
|
||||
- `Australia/Sydney` (AEST/AEDT)
|
||||
|
||||
### `/hours set threshold <minutes>`
|
||||
|
||||
Set the closing threshold in minutes.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set threshold 20
|
||||
```
|
||||
|
||||
**Effect:**
|
||||
- Denies new logins X minutes before close
|
||||
- Existing players can stay connected
|
||||
- Exempt players can bypass if configured
|
||||
|
||||
### `/hours set warnings <minutes...>`
|
||||
|
||||
Configure warning broadcast times.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set warnings 30 15 10 5 1
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `15 10 5 1` - Warn at 15, 10, 5, and 1 minutes before close
|
||||
- `30 15 5` - Warn at 30, 15, and 5 minutes before close
|
||||
- `60 30 15 10 5 1` - Extended warning sequence
|
||||
|
||||
### `/hours set countdown <seconds>`
|
||||
|
||||
Set countdown duration before closing.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set countdown 10
|
||||
```
|
||||
|
||||
**Range:** 0-60 seconds (0 = disabled)
|
||||
|
||||
**Effect:**
|
||||
- Sends "Closing in Xs" messages every second
|
||||
- Only active during the last X seconds before close
|
||||
- Provides final warning to players
|
||||
|
||||
### `/hours set exempt_bypass_schedule <true|false>`
|
||||
### `/hours set exempt_bypass_threshold <true|false>`
|
||||
|
||||
Control exempt player behavior.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set exempt_bypass_schedule true
|
||||
/hours set exempt_bypass_threshold false
|
||||
```
|
||||
|
||||
**Settings:**
|
||||
- `exempt_bypass_schedule` - Allow exempt players to join when closed
|
||||
- `exempt_bypass_threshold` - Allow exempt players during threshold
|
||||
|
||||
## 📅 Schedule Commands
|
||||
|
||||
### `/hours set default periods add "<time_range>"`
|
||||
### `/hours set default periods clear`
|
||||
|
||||
Manage default schedule periods.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set default periods add "09:00 AM-06:00 PM"
|
||||
/hours set default periods add "11:00 PM-01:00 AM"
|
||||
/hours set default periods clear
|
||||
```
|
||||
|
||||
**Time Format:** 12-hour AM/PM format with quotes
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/hours set default periods add "09:00 AM-06:00 PM"
|
||||
/hours set default periods add "10:00 PM-02:00 AM" # Midnight span
|
||||
/hours set default periods clear # Remove all periods
|
||||
```
|
||||
|
||||
### `/hours set day <day> periods add "<time_range>"`
|
||||
### `/hours set day <day> periods clear`
|
||||
|
||||
Manage per-day schedule periods.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours set day mon periods add "09:00 AM-06:00 PM"
|
||||
/hours set day fri periods add "11:00 PM-01:00 AM"
|
||||
/hours set day tue periods clear
|
||||
```
|
||||
|
||||
**Day Names:** `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/hours set day mon periods add "09:00 AM-06:00 PM"
|
||||
/hours set day fri periods add "11:00 PM-01:00 AM"
|
||||
/hours set day sat periods add "02:00 PM-11:59 PM"
|
||||
/hours set day sun periods clear
|
||||
```
|
||||
|
||||
## 📅 Exception Commands
|
||||
|
||||
### `/hours exceptions add-open "<date_time_range>"`
|
||||
### `/hours exceptions add-closed "<date>"`
|
||||
### `/hours exceptions clear`
|
||||
|
||||
Manage date exceptions.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours exceptions add-open "2025-12-31 08:00 PM-11:59 PM"
|
||||
/hours exceptions add-closed "2025-12-25"
|
||||
/hours exceptions clear
|
||||
```
|
||||
|
||||
**Date Formats:**
|
||||
- **Open dates:** `YYYY-MM-DD hh:mm AM-hh:mm PM`
|
||||
- **Closed dates:** `YYYY-MM-DD`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/hours exceptions add-open "2025-12-31 08:00 PM-11:59 PM" # New Year's Eve
|
||||
/hours exceptions add-closed "2025-12-25" # Christmas Day
|
||||
/hours exceptions add-open "2025-07-04 12:00 PM-11:59 PM" # Independence Day
|
||||
/hours exceptions clear # Remove all exceptions
|
||||
```
|
||||
|
||||
## 👥 List Commands
|
||||
|
||||
### `/hours lists whitelist toggle <player>`
|
||||
### `/hours lists blacklist toggle <player>`
|
||||
|
||||
Toggle players in/out of lists.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours lists whitelist toggle PlayerName
|
||||
/hours lists blacklist toggle PlayerName
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Automatically enables the list if not already enabled
|
||||
- Case-insensitive player name matching
|
||||
- Toggle: Add if not present, remove if present
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/hours lists whitelist toggle Steve
|
||||
/hours lists blacklist toggle Griefer123
|
||||
```
|
||||
|
||||
### `/hours lists whitelist enable`
|
||||
### `/hours lists whitelist disable`
|
||||
### `/hours lists blacklist enable`
|
||||
### `/hours lists blacklist disable`
|
||||
|
||||
Enable or disable lists.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours lists whitelist enable
|
||||
/hours lists blacklist disable
|
||||
```
|
||||
|
||||
## 📢 MOTD Commands
|
||||
|
||||
### `/hours motd toggle`
|
||||
|
||||
Enable or disable MOTD schedule display.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours motd toggle
|
||||
```
|
||||
|
||||
**Effect:**
|
||||
- Toggles MOTD feature on/off
|
||||
- Changes take effect immediately
|
||||
- Server list will show/hide schedule information
|
||||
|
||||
### `/hours motd status`
|
||||
|
||||
View current MOTD settings.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours motd status
|
||||
```
|
||||
|
||||
**Output:**
|
||||
- MOTD enabled/disabled status
|
||||
- Current MOTD configuration
|
||||
- Update frequency settings
|
||||
|
||||
## 💬 Message Commands
|
||||
|
||||
### `/hours messages reload`
|
||||
|
||||
Reload message translations.
|
||||
|
||||
**Permission:** `playhours.admin` (ops ≥2)
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/hours messages reload
|
||||
```
|
||||
|
||||
**Effect:**
|
||||
- Reloads message translations from language files
|
||||
- Applies any config overrides
|
||||
- Updates all player-facing messages
|
||||
|
||||
## 🔧 Advanced Commands
|
||||
|
||||
### Command Chaining
|
||||
|
||||
Some commands support chaining for efficiency:
|
||||
|
||||
```
|
||||
/hours set day mon periods add "09:00 AM-06:00 PM"
|
||||
/hours set day tue periods add "09:00 AM-06:00 PM"
|
||||
/hours set day wed periods add "09:00 AM-06:00 PM"
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
Use multiple commands for complex setups:
|
||||
|
||||
```
|
||||
/hours set default periods clear
|
||||
/hours set day mon periods add "09:00 AM-06:00 PM"
|
||||
/hours set day fri periods add "11:00 PM-01:00 AM"
|
||||
/hours exceptions add-closed "2025-12-25"
|
||||
```
|
||||
|
||||
## 🚨 Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
**Permission Denied:**
|
||||
```
|
||||
You do not have permission to use this command
|
||||
```
|
||||
**Solution:** Check your permission level or LuckPerms configuration
|
||||
|
||||
**Invalid Time Format:**
|
||||
```
|
||||
Invalid time range. Use: hh:mm AM-hh:mm PM
|
||||
```
|
||||
**Solution:** Use proper 12-hour AM/PM format with quotes
|
||||
|
||||
**Config Not Ready:**
|
||||
```
|
||||
PlayHours config not ready yet. Try again in a moment.
|
||||
```
|
||||
**Solution:** Wait for server startup to complete
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
1. **Check permissions:** Ensure you have the required permission level
|
||||
2. **Verify format:** Use correct time format and quotes
|
||||
3. **Wait for startup:** Allow server to fully initialize
|
||||
4. **Check logs:** Look for error messages in server logs
|
||||
|
||||
## 📚 Command Examples
|
||||
|
||||
### Basic Server Setup
|
||||
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone America/New_York
|
||||
|
||||
# Configure default hours
|
||||
/hours set default periods add "09:00 AM-06:00 PM"
|
||||
|
||||
# Set weekend hours
|
||||
/hours set day sat periods add "02:00 PM-11:59 PM"
|
||||
/hours set day sun periods add "02:00 PM-10:00 PM"
|
||||
|
||||
# Configure warnings
|
||||
/hours set warnings 15 10 5 1
|
||||
/hours set countdown 5
|
||||
```
|
||||
|
||||
### Holiday Configuration
|
||||
|
||||
```bash
|
||||
# Add holiday closures
|
||||
/hours exceptions add-closed "2025-12-25"
|
||||
/hours exceptions add-closed "2026-01-01"
|
||||
|
||||
# Add special event hours
|
||||
/hours exceptions add-open "2025-12-31 08:00 PM-11:59 PM"
|
||||
```
|
||||
|
||||
### Maintenance Mode
|
||||
|
||||
```bash
|
||||
# Enter maintenance mode
|
||||
/hours force close
|
||||
|
||||
# Exit maintenance mode
|
||||
/hours force normal
|
||||
```
|
||||
|
||||
### Player Management
|
||||
|
||||
```bash
|
||||
# Add players to whitelist
|
||||
/hours lists whitelist toggle PlayerName
|
||||
|
||||
# Add players to blacklist
|
||||
/hours lists blacklist toggle Griefer123
|
||||
|
||||
# Enable whitelist
|
||||
/hours lists whitelist enable
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Configuration Guide](CONFIGURATION.md)** - File-based configuration
|
||||
- **[Features Overview](FEATURES.md)** - How features work
|
||||
- **[Permissions System](PERMISSIONS.md)** - Permission details
|
||||
- **[Usage Examples](EXAMPLES.md)** - Real-world scenarios
|
||||
|
||||
---
|
||||
|
||||
*For configuration file options, see the [Configuration Guide](CONFIGURATION.md).*
|
||||
0
docs/CONFIGURATION.md
Normal file
0
docs/CONFIGURATION.md
Normal file
612
docs/EXAMPLES.md
Normal file
612
docs/EXAMPLES.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# Usage Examples
|
||||
|
||||
This guide provides real-world examples and scenarios for configuring PlayHours in different server environments.
|
||||
|
||||
## 🏢 Business Server
|
||||
|
||||
### Standard Business Hours
|
||||
|
||||
**Scenario:** A business server that operates Monday-Friday, 9 AM to 6 PM.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/New_York"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 15
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [15, 10, 5, 1]
|
||||
countdown_seconds = 5
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["09:00 AM-06:00 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["09:00 AM-06:00 PM"]
|
||||
tuesday = ["09:00 AM-06:00 PM"]
|
||||
wednesday = ["09:00 AM-06:00 PM"]
|
||||
thursday = ["09:00 AM-06:00 PM"]
|
||||
friday = ["09:00 AM-06:00 PM"]
|
||||
saturday = []
|
||||
sunday = []
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone America/New_York
|
||||
|
||||
# Configure default hours
|
||||
/hours set default periods add "09:00 AM-06:00 PM"
|
||||
|
||||
# Set weekend to closed
|
||||
/hours set day sat periods clear
|
||||
/hours set day sun periods clear
|
||||
|
||||
# Configure warnings
|
||||
/hours set warnings 15 10 5 1
|
||||
/hours set countdown 5
|
||||
```
|
||||
|
||||
## 🎮 Gaming Server
|
||||
|
||||
### Extended Gaming Hours
|
||||
|
||||
**Scenario:** A gaming server with extended hours, including late-night sessions.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "Europe/London"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 30
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [30, 15, 10, 5, 1]
|
||||
countdown_seconds = 10
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["06:00 PM-11:59 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["06:00 PM-11:59 PM"]
|
||||
tuesday = ["06:00 PM-11:59 PM"]
|
||||
wednesday = ["06:00 PM-11:59 PM"]
|
||||
thursday = ["06:00 PM-11:59 PM"]
|
||||
friday = ["06:00 PM-02:00 AM"] # Extended Friday night
|
||||
saturday = ["02:00 PM-02:00 AM"] # All day Saturday
|
||||
sunday = ["02:00 PM-11:59 PM"]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone Europe/London
|
||||
|
||||
# Configure default hours
|
||||
/hours set default periods add "06:00 PM-11:59 PM"
|
||||
|
||||
# Set extended Friday night
|
||||
/hours set day fri periods add "06:00 PM-02:00 AM"
|
||||
|
||||
# Set all-day Saturday
|
||||
/hours set day sat periods add "02:00 PM-02:00 AM"
|
||||
|
||||
# Configure extended warnings
|
||||
/hours set warnings 30 15 10 5 1
|
||||
/hours set countdown 10
|
||||
```
|
||||
|
||||
## 🎓 Educational Server
|
||||
|
||||
### School Hours with Breaks
|
||||
|
||||
**Scenario:** An educational server that operates during school hours with breaks.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/Chicago"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 10
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [10, 5, 1]
|
||||
countdown_seconds = 3
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["08:00 AM-03:00 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["08:00 AM-03:00 PM"]
|
||||
tuesday = ["08:00 AM-03:00 PM"]
|
||||
wednesday = ["08:00 AM-03:00 PM"]
|
||||
thursday = ["08:00 AM-03:00 PM"]
|
||||
friday = ["08:00 AM-03:00 PM"]
|
||||
saturday = []
|
||||
sunday = []
|
||||
|
||||
[exceptions]
|
||||
closed_dates = [
|
||||
"2025-12-23", # Winter break
|
||||
"2025-12-24",
|
||||
"2025-12-25",
|
||||
"2025-12-26",
|
||||
"2025-12-27",
|
||||
"2025-12-28",
|
||||
"2025-12-29",
|
||||
"2025-12-30",
|
||||
"2025-12-31",
|
||||
"2026-01-01",
|
||||
"2026-01-02",
|
||||
"2026-01-03"
|
||||
]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone America/Chicago
|
||||
|
||||
# Configure school hours
|
||||
/hours set default periods add "08:00 AM-03:00 PM"
|
||||
|
||||
# Add winter break closures
|
||||
/hours exceptions add-closed "2025-12-23"
|
||||
/hours exceptions add-closed "2025-12-24"
|
||||
/hours exceptions add-closed "2025-12-25"
|
||||
# ... (continue for all break days)
|
||||
|
||||
# Configure warnings
|
||||
/hours set warnings 10 5 1
|
||||
/hours set countdown 3
|
||||
```
|
||||
|
||||
## 🏥 Healthcare Server
|
||||
|
||||
### 24/7 with Maintenance Windows
|
||||
|
||||
**Scenario:** A healthcare server that operates 24/7 with scheduled maintenance windows.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/Los_Angeles"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 0
|
||||
deny_login_during_threshold = false
|
||||
kick_exempt = false
|
||||
warning_minutes = [60, 30, 15, 10, 5, 1]
|
||||
countdown_seconds = 10
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["12:00 AM-11:59 PM"] # 24/7
|
||||
|
||||
[days]
|
||||
monday = ["12:00 AM-11:59 PM"]
|
||||
tuesday = ["12:00 AM-11:59 PM"]
|
||||
wednesday = ["12:00 AM-11:59 PM"]
|
||||
thursday = ["12:00 AM-11:59 PM"]
|
||||
friday = ["12:00 AM-11:59 PM"]
|
||||
saturday = ["12:00 AM-11:59 PM"]
|
||||
sunday = ["12:00 AM-11:59 PM"]
|
||||
|
||||
[exceptions]
|
||||
closed_dates = [
|
||||
"2025-01-01", # New Year's Day
|
||||
"2025-07-04", # Independence Day
|
||||
"2025-12-25" # Christmas Day
|
||||
]
|
||||
|
||||
# Maintenance windows (closed for maintenance)
|
||||
open_dates = [
|
||||
"2025-01-15 02:00 AM-04:00 AM", # Monthly maintenance
|
||||
"2025-02-15 02:00 AM-04:00 AM",
|
||||
"2025-03-15 02:00 AM-04:00 AM"
|
||||
]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone America/Los_Angeles
|
||||
|
||||
# Configure 24/7 operation
|
||||
/hours set default periods add "12:00 AM-11:59 PM"
|
||||
|
||||
# Add holiday closures
|
||||
/hours exceptions add-closed "2025-01-01"
|
||||
/hours exceptions add-closed "2025-07-04"
|
||||
/hours exceptions add-closed "2025-12-25"
|
||||
|
||||
# Add maintenance windows
|
||||
/hours exceptions add-open "2025-01-15 02:00 AM-04:00 AM"
|
||||
|
||||
# Configure extended warnings
|
||||
/hours set warnings 60 30 15 10 5 1
|
||||
/hours set countdown 10
|
||||
```
|
||||
|
||||
## 🎉 Event Server
|
||||
|
||||
### Special Event Hours
|
||||
|
||||
**Scenario:** A server that hosts special events with extended hours.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "Europe/Paris"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 20
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [20, 15, 10, 5, 1]
|
||||
countdown_seconds = 5
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "fr_fr"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["07:00 PM-11:00 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["07:00 PM-11:00 PM"]
|
||||
tuesday = ["07:00 PM-11:00 PM"]
|
||||
wednesday = ["07:00 PM-11:00 PM"]
|
||||
thursday = ["07:00 PM-11:00 PM"]
|
||||
friday = ["07:00 PM-11:00 PM"]
|
||||
saturday = ["07:00 PM-11:00 PM"]
|
||||
sunday = ["07:00 PM-11:00 PM"]
|
||||
|
||||
[exceptions]
|
||||
open_dates = [
|
||||
"2025-12-31 08:00 PM-11:59 PM", # New Year's Eve
|
||||
"2025-07-14 08:00 PM-11:59 PM", # Bastille Day
|
||||
"2025-12-25 08:00 PM-11:59 PM" # Christmas Eve
|
||||
]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone Europe/Paris
|
||||
|
||||
# Configure regular hours
|
||||
/hours set default periods add "07:00 PM-11:00 PM"
|
||||
|
||||
# Add special event hours
|
||||
/hours exceptions add-open "2025-12-31 08:00 PM-11:59 PM"
|
||||
/hours exceptions add-open "2025-07-14 08:00 PM-11:59 PM"
|
||||
/hours exceptions add-open "2025-12-25 08:00 PM-11:59 PM"
|
||||
|
||||
# Configure warnings
|
||||
/hours set warnings 20 15 10 5 1
|
||||
/hours set countdown 5
|
||||
```
|
||||
|
||||
## 🛡️ Whitelist Server
|
||||
|
||||
### Invitation-Only Server
|
||||
|
||||
**Scenario:** A private server with whitelist-only access.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/New_York"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 0
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [15, 10, 5, 1]
|
||||
countdown_seconds = 5
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["06:00 PM-10:00 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["06:00 PM-10:00 PM"]
|
||||
tuesday = ["06:00 PM-10:00 PM"]
|
||||
wednesday = ["06:00 PM-10:00 PM"]
|
||||
thursday = ["06:00 PM-10:00 PM"]
|
||||
friday = ["06:00 PM-10:00 PM"]
|
||||
saturday = ["02:00 PM-10:00 PM"]
|
||||
sunday = ["02:00 PM-10:00 PM"]
|
||||
|
||||
[lists]
|
||||
whitelist_enabled = true
|
||||
whitelist = ["Player1", "Player2", "Player3"]
|
||||
blacklist_enabled = false
|
||||
blacklist = []
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone America/New_York
|
||||
|
||||
# Configure regular hours
|
||||
/hours set default periods add "06:00 PM-10:00 PM"
|
||||
|
||||
# Set weekend hours
|
||||
/hours set day sat periods add "02:00 PM-10:00 PM"
|
||||
/hours set day sun periods add "02:00 PM-10:00 PM"
|
||||
|
||||
# Add players to whitelist
|
||||
/hours lists whitelist toggle Player1
|
||||
/hours lists whitelist toggle Player2
|
||||
/hours lists whitelist toggle Player3
|
||||
|
||||
# Enable whitelist
|
||||
/hours lists whitelist enable
|
||||
```
|
||||
|
||||
## 🚫 Blacklist Server
|
||||
|
||||
### Problem Player Management
|
||||
|
||||
**Scenario:** A server that needs to block specific players.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/Chicago"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 0
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [15, 10, 5, 1]
|
||||
countdown_seconds = 5
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["12:00 PM-11:59 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["12:00 PM-11:59 PM"]
|
||||
tuesday = ["12:00 PM-11:59 PM"]
|
||||
wednesday = ["12:00 PM-11:59 PM"]
|
||||
thursday = ["12:00 PM-11:59 PM"]
|
||||
friday = ["12:00 PM-11:59 PM"]
|
||||
saturday = ["12:00 PM-11:59 PM"]
|
||||
sunday = ["12:00 PM-11:59 PM"]
|
||||
|
||||
[lists]
|
||||
whitelist_enabled = false
|
||||
whitelist = []
|
||||
blacklist_enabled = true
|
||||
blacklist = ["Griefer123", "Spammer456", "Troll789"]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set timezone
|
||||
/hours set timezone America/Chicago
|
||||
|
||||
# Configure hours
|
||||
/hours set default periods add "12:00 PM-11:59 PM"
|
||||
|
||||
# Add players to blacklist
|
||||
/hours lists blacklist toggle Griefer123
|
||||
/hours lists blacklist toggle Spammer456
|
||||
/hours lists blacklist toggle Troll789
|
||||
|
||||
# Enable blacklist
|
||||
/hours lists blacklist enable
|
||||
```
|
||||
|
||||
## 🔧 Maintenance Mode
|
||||
|
||||
### Server Maintenance
|
||||
|
||||
**Scenario:** A server that needs to enter maintenance mode.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Enter maintenance mode
|
||||
/hours force close
|
||||
|
||||
# Check status
|
||||
/hours status
|
||||
|
||||
# Exit maintenance mode
|
||||
/hours force normal
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
force_mode = "FORCE_CLOSED" # Maintenance mode
|
||||
```
|
||||
|
||||
## 🌍 Multi-Language Server
|
||||
|
||||
### International Server
|
||||
|
||||
**Scenario:** A server with international players using different languages.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[general]
|
||||
timezone = "UTC"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 15
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [15, 10, 5, 1]
|
||||
countdown_seconds = 5
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us" # or "fr_fr"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["12:00 PM-11:59 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["12:00 PM-11:59 PM"]
|
||||
tuesday = ["12:00 PM-11:59 PM"]
|
||||
wednesday = ["12:00 PM-11:59 PM"]
|
||||
thursday = ["12:00 PM-11:59 PM"]
|
||||
friday = ["12:00 PM-11:59 PM"]
|
||||
saturday = ["12:00 PM-11:59 PM"]
|
||||
sunday = ["12:00 PM-11:59 PM"]
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Set UTC timezone
|
||||
/hours set timezone UTC
|
||||
|
||||
# Configure 24/7 operation
|
||||
/hours set default periods add "12:00 PM-11:59 PM"
|
||||
|
||||
# Set language
|
||||
/hours set message_locale en_us # or fr_fr
|
||||
```
|
||||
|
||||
## 📢 MOTD Examples
|
||||
|
||||
### Basic MOTD
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
show_status = true
|
||||
show_next_open = true
|
||||
show_next_close = true
|
||||
use_colors = true
|
||||
open_color = "green"
|
||||
closed_color = "red"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
🟢 Open | Closes at 10:00 PM
|
||||
```
|
||||
|
||||
### Custom MOTD
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
custom_format = "Status: %status% - Next: %openday% at %opentime%"
|
||||
use_colors = true
|
||||
open_color = "green"
|
||||
closed_color = "red"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
Status: Closed - Next: Monday at 9:00 AM
|
||||
```
|
||||
|
||||
### Branded MOTD
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
custom_lines = [
|
||||
"🎮 My Game Server",
|
||||
"Status: %status%",
|
||||
"Next open: %openday% at %opentime%"
|
||||
]
|
||||
use_colors = true
|
||||
open_color = "green"
|
||||
closed_color = "red"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
🎮 My Game Server
|
||||
Status: Open
|
||||
Next open: Monday at 9:00 AM
|
||||
```
|
||||
|
||||
## 🔄 Configuration Updates
|
||||
|
||||
### Hot Reload Examples
|
||||
|
||||
**After editing config file:**
|
||||
```bash
|
||||
/hours reload
|
||||
```
|
||||
|
||||
**After changing timezone:**
|
||||
```bash
|
||||
/hours set timezone Europe/London
|
||||
/hours reload
|
||||
```
|
||||
|
||||
**After modifying schedule:**
|
||||
```bash
|
||||
/hours set day mon periods add "09:00 AM-06:00 PM"
|
||||
/hours reload
|
||||
```
|
||||
|
||||
## 🚨 Emergency Scenarios
|
||||
|
||||
### Server Emergency
|
||||
|
||||
**Immediate closure:**
|
||||
```bash
|
||||
/hours force close
|
||||
```
|
||||
|
||||
**Emergency maintenance:**
|
||||
```bash
|
||||
/hours force close
|
||||
# Perform maintenance
|
||||
/hours force normal
|
||||
```
|
||||
|
||||
### Player Issues
|
||||
|
||||
**Block problematic player:**
|
||||
```bash
|
||||
/hours lists blacklist toggle ProblemPlayer
|
||||
```
|
||||
|
||||
**Temporary maintenance:**
|
||||
```bash
|
||||
/hours force close
|
||||
# Fix issues
|
||||
/hours force normal
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Configuration Guide](CONFIGURATION.md)** - Detailed configuration options
|
||||
- **[Commands Reference](COMMANDS.md)** - All available commands
|
||||
- **[Features Overview](FEATURES.md)** - How features work
|
||||
- **[MOTD System](MOTD.md)** - MOTD configuration
|
||||
|
||||
---
|
||||
|
||||
*For more configuration options, see the [Configuration Guide](CONFIGURATION.md).*
|
||||
453
docs/FEATURES.md
Normal file
453
docs/FEATURES.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# Features Overview
|
||||
|
||||
This guide provides detailed explanations of all PlayHours features, how they work, and their interactions.
|
||||
|
||||
## 🕒 Schedule Enforcement
|
||||
|
||||
### Core Functionality
|
||||
|
||||
PlayHours enforces server operating hours based on configurable schedules with support for:
|
||||
|
||||
- **Per-day schedules** - Different hours for each day of the week
|
||||
- **Multiple periods per day** - Support for split schedules (e.g., morning and evening)
|
||||
- **Midnight-spanning periods** - Ranges that cross midnight (e.g., 10:00 PM-02:00 AM)
|
||||
- **Default schedule** - Fallback schedule for days without specific configuration
|
||||
- **Timezone awareness** - All times are calculated in the configured timezone
|
||||
|
||||
### Schedule Logic
|
||||
|
||||
The schedule enforcement follows this priority order:
|
||||
|
||||
1. **Force Mode** - Override all schedule logic
|
||||
2. **Blacklist** - Always deny listed players
|
||||
3. **Whitelist** - Always allow listed players (if enabled)
|
||||
4. **Date Exceptions** - Special open/closed dates
|
||||
5. **Daily Schedule** - Per-day time periods
|
||||
|
||||
### Time Format
|
||||
|
||||
- **Input Format:** 12-hour AM/PM format (`"09:00 AM-06:00 PM"`)
|
||||
- **Display Format:** Automatically adapts to locale (English: 12-hour, French: 24-hour)
|
||||
- **Midnight Spanning:** Detected when end time is before start time
|
||||
- **Validation:** All time ranges are validated for correctness
|
||||
|
||||
### Examples
|
||||
|
||||
```toml
|
||||
# Standard business hours
|
||||
[defaults]
|
||||
periods = ["09:00 AM-06:00 PM"]
|
||||
|
||||
# Weekend with midnight span
|
||||
[days]
|
||||
friday = ["06:00 PM-02:00 AM"] # Friday 6 PM to Saturday 2 AM
|
||||
saturday = ["02:00 PM-02:00 AM"] # Saturday 2 PM to Sunday 2 AM
|
||||
```
|
||||
|
||||
## 🚫 Login Control
|
||||
|
||||
### Access Denial
|
||||
|
||||
PlayHours prevents players from joining the server when:
|
||||
|
||||
- **Server is closed** according to schedule
|
||||
- **Within closing threshold** (configurable minutes before close)
|
||||
- **Player is blacklisted** (regardless of schedule)
|
||||
- **Whitelist is enabled** and player is not whitelisted
|
||||
|
||||
### Login Process
|
||||
|
||||
1. **Player attempts to join**
|
||||
2. **Permission check** - Exempt players may bypass restrictions
|
||||
3. **Schedule check** - Is server currently open?
|
||||
4. **Threshold check** - Is within closing threshold?
|
||||
5. **List check** - Is player whitelisted/blacklisted?
|
||||
6. **Decision** - Allow or deny with appropriate message
|
||||
|
||||
### Exempt Players
|
||||
|
||||
Players with exempt permission can:
|
||||
|
||||
- **Bypass schedule** - Join even when server is closed
|
||||
- **Bypass threshold** - Join during closing threshold
|
||||
- **Override blacklist** - Join even if blacklisted
|
||||
- **Stay connected** - Not kicked at closing time (configurable)
|
||||
|
||||
### Messages
|
||||
|
||||
Players receive clear messages when denied access:
|
||||
|
||||
- **Server closed:** "Server closed. Next open: Monday at 9:00 AM."
|
||||
- **Closing threshold:** "Server closing soon. Next open: Monday at 9:00 AM."
|
||||
- **Blacklisted:** "Access denied." (no schedule information)
|
||||
|
||||
## ⚠️ Warnings & Auto-Kick
|
||||
|
||||
### Warning System
|
||||
|
||||
PlayHours broadcasts warnings before server closure:
|
||||
|
||||
- **Configurable timing** - Default: 15, 10, 5, 1 minutes before close
|
||||
- **Broadcast to all players** - Server-wide announcements
|
||||
- **Include next open time** - Inform players when server reopens
|
||||
- **Localized messages** - Support for multiple languages
|
||||
|
||||
### Countdown System
|
||||
|
||||
Second-by-second countdown before closing:
|
||||
|
||||
- **Configurable duration** - 0-60 seconds (0 = disabled)
|
||||
- **Final warning** - "Closing in 5s" messages
|
||||
- **Precise timing** - Exact second when server closes
|
||||
|
||||
### Auto-Kick System
|
||||
|
||||
Automatic player removal at closing time:
|
||||
|
||||
- **Kick non-exempt players** - Remove players without exempt permission
|
||||
- **Optional exempt kick** - Configurable whether to kick exempt players
|
||||
- **Clear messages** - Inform players why they were kicked
|
||||
- **Next open time** - Tell players when they can return
|
||||
|
||||
### Warning Examples
|
||||
|
||||
```
|
||||
[Server] Server closing in 15 minutes at 10:00 PM.
|
||||
[Server] Server closing in 10 minutes at 10:00 PM.
|
||||
[Server] Server closing in 5 minutes at 10:00 PM.
|
||||
[Server] Server closing in 1 minute at 10:00 PM.
|
||||
[Server] Closing in 5s
|
||||
[Server] Closing in 4s
|
||||
[Server] Closing in 3s
|
||||
[Server] Closing in 2s
|
||||
[Server] Closing in 1s
|
||||
```
|
||||
|
||||
## 🔧 Force Modes
|
||||
|
||||
### NORMAL Mode
|
||||
|
||||
Default operational mode:
|
||||
|
||||
- **Follows schedule** - Enforces configured hours
|
||||
- **Respects exceptions** - Honors date exceptions
|
||||
- **Applies lists** - Whitelist/blacklist in effect
|
||||
- **Standard behavior** - All features work as configured
|
||||
|
||||
### FORCE_OPEN Mode
|
||||
|
||||
Always allow access:
|
||||
|
||||
- **24/7 access** - Server always open
|
||||
- **Override schedule** - Ignores all schedule restrictions
|
||||
- **Respect blacklist** - Blacklisted players still denied
|
||||
- **Exempt players** - Can still join (redundant but allowed)
|
||||
- **Use case** - Special events, maintenance overrides
|
||||
|
||||
### FORCE_CLOSED Mode
|
||||
|
||||
Always deny access:
|
||||
|
||||
- **Maintenance mode** - Server always closed
|
||||
- **Override schedule** - Ignores all schedule restrictions
|
||||
- **Exempt players** - Can still join (unless kick_exempt=true)
|
||||
- **Use case** - Server maintenance, emergency closures
|
||||
|
||||
### Force Mode Examples
|
||||
|
||||
```bash
|
||||
/hours force open # Enable 24/7 access
|
||||
/hours force close # Enter maintenance mode
|
||||
/hours force normal # Return to normal schedule
|
||||
```
|
||||
|
||||
## 👥 Whitelist/Blacklist
|
||||
|
||||
### Whitelist System
|
||||
|
||||
When enabled, only listed players can join:
|
||||
|
||||
- **Always allowed** - Listed players can join anytime
|
||||
- **Always denied** - Non-listed players denied even during open hours
|
||||
- **Independent of schedule** - Works regardless of server hours
|
||||
- **Case insensitive** - Player names matched regardless of case
|
||||
|
||||
### Blacklist System
|
||||
|
||||
When enabled, listed players are always denied:
|
||||
|
||||
- **Always denied** - Listed players cannot join anytime
|
||||
- **Override schedule** - Denied even during open hours
|
||||
- **Independent of schedule** - Works regardless of server hours
|
||||
- **Exempt override** - Exempt players can still join
|
||||
|
||||
### List Management
|
||||
|
||||
- **Toggle players** - Add/remove players from lists
|
||||
- **Auto-enable** - Lists automatically enabled when players added
|
||||
- **Independent operation** - Whitelist and blacklist work separately
|
||||
- **Permission override** - Exempt players can bypass blacklist
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Add to whitelist
|
||||
/hours lists whitelist toggle PlayerName
|
||||
|
||||
# Add to blacklist
|
||||
/hours lists blacklist toggle Griefer123
|
||||
|
||||
# Enable whitelist
|
||||
/hours lists whitelist enable
|
||||
```
|
||||
|
||||
## 📅 Date Exceptions
|
||||
|
||||
### Closed Dates
|
||||
|
||||
Full day closures:
|
||||
|
||||
- **Format:** `YYYY-MM-DD` (e.g., `2025-12-25`)
|
||||
- **Effect:** Server closed all day regardless of schedule
|
||||
- **Use cases:** Holidays, maintenance days, special events
|
||||
|
||||
### Open Dates
|
||||
|
||||
Special open windows:
|
||||
|
||||
- **Format:** `YYYY-MM-DD hh:mm AM-hh:mm PM` (e.g., `2025-12-31 08:00 PM-11:59 PM`)
|
||||
- **Effect:** Server open during specified time regardless of schedule
|
||||
- **Use cases:** Special events, extended hours, one-off openings
|
||||
|
||||
### Exception Priority
|
||||
|
||||
Exceptions take precedence over normal schedule:
|
||||
|
||||
1. **Closed exceptions** - Always deny access
|
||||
2. **Open exceptions** - Always allow access
|
||||
3. **Normal schedule** - Follow configured hours
|
||||
|
||||
### Examples
|
||||
|
||||
```toml
|
||||
[exceptions]
|
||||
# Holiday closures
|
||||
closed_dates = ["2025-12-25", "2026-01-01"]
|
||||
|
||||
# Special event windows
|
||||
open_dates = [
|
||||
"2025-12-31 08:00 PM-11:59 PM", # New Year's Eve
|
||||
"2025-07-04 12:00 PM-11:59 PM" # Independence Day
|
||||
]
|
||||
```
|
||||
|
||||
## 🌍 Multi-Language Support
|
||||
|
||||
### Supported Languages
|
||||
|
||||
- **English (en_us)** - 12-hour AM/PM format
|
||||
- **French (fr_fr)** - 24-hour format
|
||||
|
||||
### Smart Time Formatting
|
||||
|
||||
Time display automatically adapts to locale:
|
||||
|
||||
- **English:** "06:00 PM" (12-hour format)
|
||||
- **French:** "18:00" (24-hour format)
|
||||
|
||||
### Message Localization
|
||||
|
||||
All player-facing messages are localized:
|
||||
|
||||
- **Access denied messages**
|
||||
- **Warning broadcasts**
|
||||
- **Kick messages**
|
||||
- **Status displays**
|
||||
- **MOTD content**
|
||||
|
||||
### Locale Configuration
|
||||
|
||||
```toml
|
||||
[general]
|
||||
message_locale = "en_us" # or "fr_fr"
|
||||
```
|
||||
|
||||
### Custom Messages
|
||||
|
||||
Override default messages with custom text:
|
||||
|
||||
```toml
|
||||
[messages]
|
||||
kick = "Le serveur est fermé ! Revenez %openday% à %opentime%."
|
||||
warn = "⚠️ ATTENTION: Server will close in %minutes% minute%s% at %closetime%."
|
||||
```
|
||||
|
||||
## 🔑 Permission System
|
||||
|
||||
### Permission Nodes
|
||||
|
||||
- **`playhours.admin`** - Full administrative access
|
||||
- **`playhours.view`** - Read-only status access
|
||||
- **`playhours.exempt`** - Bypass all schedule restrictions
|
||||
|
||||
### LuckPerms Integration
|
||||
|
||||
Soft dependency on LuckPerms:
|
||||
|
||||
- **Automatic detection** - Works with or without LuckPerms
|
||||
- **Permission checking** - Uses LuckPerms when available
|
||||
- **Fallback system** - Falls back to vanilla ops when LuckPerms unavailable
|
||||
- **Timeout protection** - Prevents blocking on permission checks
|
||||
|
||||
### Vanilla Ops Fallback
|
||||
|
||||
When LuckPerms is not available:
|
||||
|
||||
- **Level 1+** - Can use `/hours status`
|
||||
- **Level 2+** - Full admin access and exemption
|
||||
|
||||
### Permission Examples
|
||||
|
||||
```bash
|
||||
# LuckPerms commands
|
||||
/lp group default permission set playhours.view true
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
|
||||
# Vanilla ops
|
||||
/op PlayerName # Level 2 for admin access
|
||||
```
|
||||
|
||||
## 📢 MOTD System
|
||||
|
||||
### Dynamic Server List
|
||||
|
||||
The MOTD (Message of the Day) displays real-time schedule information:
|
||||
|
||||
- **Server status** - Open/Closed with colors
|
||||
- **Next open time** - When server will open next
|
||||
- **Next close time** - When server will close next
|
||||
- **Countdown timer** - Minutes until close
|
||||
- **Force mode indicators** - FORCE_OPEN/FORCE_CLOSED status
|
||||
|
||||
### MOTD Features
|
||||
|
||||
- **Automatic updates** - Refreshes every 60 seconds (configurable)
|
||||
- **Color support** - Minecraft color codes for status indication
|
||||
- **Custom formatting** - Flexible format strings with placeholders
|
||||
- **Multi-language** - Localized content based on server locale
|
||||
- **Performance optimized** - Efficient updates to minimize server load
|
||||
|
||||
### MOTD Configuration
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
show_status = true # Display Open/Closed status
|
||||
show_next_open = true # Show next opening time
|
||||
show_next_close = true # Show next closing time
|
||||
use_colors = true # Enable colored MOTD
|
||||
open_color = "green" # Color for "open" status
|
||||
closed_color = "red" # Color for "closed" status
|
||||
update_delay_seconds = 60 # Update frequency
|
||||
```
|
||||
|
||||
### MOTD Examples
|
||||
|
||||
**When Open:**
|
||||
```
|
||||
🟢 Open | Closes at 10:00 PM
|
||||
```
|
||||
|
||||
**When Closed:**
|
||||
```
|
||||
🔴 Closed | Opens Monday at 9:00 AM
|
||||
```
|
||||
|
||||
**With Countdown:**
|
||||
```
|
||||
🟢 Open | Closing in 15 min | Closes at 10:00 PM
|
||||
```
|
||||
|
||||
## 🔄 Hot Reload
|
||||
|
||||
### Configuration Reload
|
||||
|
||||
Apply changes without server restart:
|
||||
|
||||
- **Command:** `/hours reload`
|
||||
- **Effect:** Reloads all configuration settings
|
||||
- **Immediate:** Changes take effect instantly
|
||||
- **Safe:** No data loss or server interruption
|
||||
|
||||
### What Gets Reloaded
|
||||
|
||||
- **Schedule configuration** - All time periods and exceptions
|
||||
- **Message translations** - Language files and custom messages
|
||||
- **MOTD settings** - Display configuration
|
||||
- **Permission settings** - Exemption and bypass settings
|
||||
|
||||
### When to Use
|
||||
|
||||
- **After editing config file** - Apply configuration changes
|
||||
- **After changing timezone** - Update time calculations
|
||||
- **After modifying schedule** - Apply new hours
|
||||
- **After updating messages** - Apply translation changes
|
||||
|
||||
## 🚨 Error Handling
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
PlayHours handles errors gracefully:
|
||||
|
||||
- **Config not ready** - Defers checks until configuration is loaded
|
||||
- **Permission timeouts** - Falls back to vanilla ops on timeout
|
||||
- **Invalid configuration** - Uses safe defaults until fixed
|
||||
- **Network issues** - Continues operation with cached data
|
||||
|
||||
### Error Recovery
|
||||
|
||||
- **Automatic retry** - Retries failed operations
|
||||
- **Fallback systems** - Uses alternative methods when primary fails
|
||||
- **Logging** - Comprehensive error logging for debugging
|
||||
- **User feedback** - Clear error messages for administrators
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Midnight Spanning
|
||||
|
||||
Support for time ranges that cross midnight:
|
||||
|
||||
- **Automatic detection** - When end time is before start time
|
||||
- **Proper calculation** - Handles day boundaries correctly
|
||||
- **Schedule logic** - Includes spanning ranges in both days
|
||||
- **Next open/close** - Calculates correctly across day boundaries
|
||||
|
||||
### Timezone Handling
|
||||
|
||||
Robust timezone support:
|
||||
|
||||
- **IANA identifiers** - Standard timezone names
|
||||
- **Automatic DST** - Handles daylight saving time changes
|
||||
- **Local time display** - Times shown in server timezone
|
||||
- **Consistent calculations** - All time operations use same timezone
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
Efficient operation:
|
||||
|
||||
- **Cached calculations** - Schedule data cached for performance
|
||||
- **Minimal updates** - MOTD updates only when necessary
|
||||
- **Efficient checks** - Optimized permission and schedule checks
|
||||
- **Resource management** - Minimal server resource usage
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Configuration Guide](CONFIGURATION.md)** - Detailed configuration options
|
||||
- **[Commands Reference](COMMANDS.md)** - All available commands
|
||||
- **[MOTD System](MOTD.md)** - MOTD configuration and customization
|
||||
- **[Permissions System](PERMISSIONS.md)** - Permission details
|
||||
- **[Usage Examples](EXAMPLES.md)** - Real-world scenarios
|
||||
|
||||
---
|
||||
|
||||
*For specific configuration options, see the [Configuration Guide](CONFIGURATION.md).*
|
||||
202
docs/INSTALLATION.md
Normal file
202
docs/INSTALLATION.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Installation Guide
|
||||
|
||||
This guide covers the installation and initial setup of the PlayHours mod for Minecraft Forge servers.
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
### System Requirements
|
||||
- **Java:** 17 or 23 (recommended: 17 LTS)
|
||||
- **Minecraft:** 1.20.1
|
||||
- **Forge:** 47.4.10 or higher
|
||||
- **Server:** Dedicated server (not single-player)
|
||||
|
||||
### Mod Requirements
|
||||
- **Server-side only** - No client mod required
|
||||
- **Forge mod loader** - Not compatible with Fabric or other loaders
|
||||
- **Dedicated server** - Single-player mode not supported
|
||||
|
||||
## 🚀 Installation Steps
|
||||
|
||||
### 1. Download the Mod
|
||||
|
||||
1. Navigate to the `build/libs/` directory in this repository
|
||||
2. Download `playhours-1.0.0.jar`
|
||||
3. Verify the file size and integrity
|
||||
|
||||
### 2. Install on Server
|
||||
|
||||
1. **Stop your Minecraft server** if it's running
|
||||
2. Navigate to your server's root directory
|
||||
3. Locate the `mods/` folder (create it if it doesn't exist)
|
||||
4. Copy `playhours-1.0.0.jar` into the `mods/` folder
|
||||
5. Ensure no other version of PlayHours is present
|
||||
|
||||
### 3. First Startup
|
||||
|
||||
1. **Start your server** with the Forge mod loader
|
||||
2. **Wait for complete startup** - the mod will generate default configuration
|
||||
3. **Check the logs** for any error messages
|
||||
4. **Verify installation** by looking for:
|
||||
```
|
||||
[INFO] PlayHours loading... (modId=playhours)
|
||||
[INFO] PlayHours common setup initialized
|
||||
```
|
||||
|
||||
### 4. Configuration Setup
|
||||
|
||||
1. **Locate the config file:** `config/playhours.toml`
|
||||
2. **Edit the configuration** to match your server's needs
|
||||
3. **Use `/hours reload`** to apply changes without restart
|
||||
4. **Or restart the server** to load new configuration
|
||||
|
||||
## ⚙️ Initial Configuration
|
||||
|
||||
### Basic Setup Example
|
||||
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/New_York"
|
||||
force_mode = "NORMAL"
|
||||
closing_threshold_minutes = 0
|
||||
deny_login_during_threshold = true
|
||||
kick_exempt = false
|
||||
warning_minutes = [15, 10, 5, 1]
|
||||
countdown_seconds = 5
|
||||
exempt_bypass_schedule = true
|
||||
exempt_bypass_threshold = true
|
||||
message_locale = "en_us"
|
||||
motd_enabled = true
|
||||
|
||||
[defaults]
|
||||
periods = ["09:00 AM-06:00 PM"]
|
||||
|
||||
[days]
|
||||
monday = ["09:00 AM-06:00 PM"]
|
||||
tuesday = ["09:00 AM-06:00 PM"]
|
||||
wednesday = ["09:00 AM-06:00 PM"]
|
||||
thursday = ["09:00 AM-06:00 PM"]
|
||||
friday = ["09:00 AM-06:00 PM"]
|
||||
saturday = []
|
||||
sunday = []
|
||||
```
|
||||
|
||||
### Timezone Configuration
|
||||
|
||||
Set your server's timezone using IANA timezone identifiers:
|
||||
|
||||
```toml
|
||||
[general]
|
||||
timezone = "America/New_York" # Eastern Time
|
||||
timezone = "Europe/London" # GMT/BST
|
||||
timezone = "Asia/Tokyo" # JST
|
||||
timezone = "Australia/Sydney" # AEST/AEDT
|
||||
```
|
||||
|
||||
## 🔧 Post-Installation
|
||||
|
||||
### 1. Test Basic Functionality
|
||||
|
||||
1. **Check status:** `/hours status`
|
||||
2. **Test force modes:** `/hours force open` then `/hours force normal`
|
||||
3. **Verify permissions:** Ensure you have `playhours.admin` permission
|
||||
|
||||
### 2. Configure Permissions
|
||||
|
||||
#### With LuckPerms (Recommended)
|
||||
```bash
|
||||
/lp group default permission set playhours.view true
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
```
|
||||
|
||||
#### With Vanilla Ops
|
||||
- **Level 1+:** Can use `/hours status`
|
||||
- **Level 2+:** Full admin access and exemption
|
||||
|
||||
### 3. Set Up Schedule
|
||||
|
||||
1. **Configure default hours** in the `[defaults]` section
|
||||
2. **Set per-day overrides** in the `[days]` section
|
||||
3. **Add exceptions** for holidays and special events
|
||||
4. **Test the schedule** by checking status at different times
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Mod Not Loading
|
||||
- **Check Forge version:** Must be 47.4.10 or higher
|
||||
- **Check Java version:** Must be Java 17 or 23
|
||||
- **Check server logs** for error messages
|
||||
- **Verify file integrity:** Re-download if corrupted
|
||||
|
||||
#### Configuration Errors
|
||||
- **Check TOML syntax:** Use a TOML validator
|
||||
- **Check timezone format:** Must be valid IANA identifier
|
||||
- **Check time format:** Must use 12-hour AM/PM format
|
||||
- **Use `/hours reload`** to test configuration
|
||||
|
||||
#### Permission Issues
|
||||
- **Check LuckPerms integration:** Ensure LuckPerms is loaded first
|
||||
- **Check ops levels:** Verify operator permissions
|
||||
- **Check permission nodes:** Use correct permission names
|
||||
|
||||
### Log Analysis
|
||||
|
||||
Look for these log messages:
|
||||
|
||||
```
|
||||
[INFO] PlayHours loading... (modId=playhours) # Successful loading
|
||||
[ERROR] PlayHours config error: ... # Configuration issue
|
||||
[WARN] PlayHours: LuckPerms not found, using ops fallback # Permission fallback
|
||||
[DEBUG] PlayHours: Config not ready yet # Normal during startup
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. **Check the logs** for specific error messages
|
||||
2. **Verify configuration** using `/hours status`
|
||||
3. **Test with minimal config** to isolate issues
|
||||
4. **Check Forge compatibility** and Java version
|
||||
|
||||
## 🔄 Updates
|
||||
|
||||
### Updating the Mod
|
||||
|
||||
1. **Stop the server**
|
||||
2. **Backup your configuration** (`config/playhours.toml`)
|
||||
3. **Remove the old mod file** from `mods/` folder
|
||||
4. **Install the new version**
|
||||
5. **Start the server**
|
||||
6. **Verify configuration** is still valid
|
||||
7. **Use `/hours reload`** if needed
|
||||
|
||||
### Configuration Migration
|
||||
|
||||
- **Automatic:** Most configuration changes are backward compatible
|
||||
- **Manual:** Check changelog for breaking changes
|
||||
- **Backup:** Always backup your config before updates
|
||||
|
||||
## ✅ Verification
|
||||
|
||||
After installation, verify everything works:
|
||||
|
||||
1. **Server starts without errors**
|
||||
2. **`/hours status` command works**
|
||||
3. **Configuration file is generated**
|
||||
4. **Schedule enforcement works** (test with force modes)
|
||||
5. **Permissions work correctly**
|
||||
6. **MOTD updates** (if enabled)
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
After successful installation:
|
||||
|
||||
1. **Read the [Configuration Guide](CONFIGURATION.md)** for detailed setup
|
||||
2. **Check the [Commands Reference](COMMANDS.md)** for available commands
|
||||
3. **Review [Features Overview](FEATURES.md)** for advanced functionality
|
||||
4. **See [Usage Examples](EXAMPLES.md)** for real-world scenarios
|
||||
|
||||
---
|
||||
|
||||
*For detailed configuration options, see the [Configuration Guide](CONFIGURATION.md).*
|
||||
434
docs/MOTD.md
Normal file
434
docs/MOTD.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# MOTD System
|
||||
|
||||
The MOTD (Message of the Day) system displays real-time schedule information in the Minecraft server list, providing players with up-to-date information about server availability.
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
The MOTD system automatically updates the server list description to show:
|
||||
|
||||
- **Server status** (Open/Closed) with color coding
|
||||
- **Next opening time** when the server is closed
|
||||
- **Next closing time** when the server is open
|
||||
- **Countdown timer** when closing soon
|
||||
- **Force mode indicators** for maintenance or special modes
|
||||
- **Custom information** with flexible formatting
|
||||
|
||||
## ⚙️ Basic Configuration
|
||||
|
||||
### Enabling MOTD
|
||||
|
||||
```toml
|
||||
[general]
|
||||
motd_enabled = true # Enable MOTD feature
|
||||
```
|
||||
|
||||
### Basic Display Settings
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
show_status = true # Display Open/Closed status
|
||||
show_next_open = true # Show next opening time when closed
|
||||
show_next_close = true # Show next closing time when open
|
||||
show_schedule_times = false # Show detailed schedule summary
|
||||
show_on_second_line = true # Place info on second MOTD line
|
||||
separator = " | " # Separator between elements
|
||||
```
|
||||
|
||||
## 🎨 Visual Configuration
|
||||
|
||||
### Colors
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
use_colors = true # Enable colored MOTD
|
||||
open_color = "green" # Color for "open" status
|
||||
closed_color = "red" # Color for "closed" status
|
||||
info_color = "gray" # Color for informational text
|
||||
```
|
||||
|
||||
### Supported Colors
|
||||
|
||||
| Color | Code | Description |
|
||||
|-------|------|-------------|
|
||||
| `black` | `§0` | Black text |
|
||||
| `dark_blue` | `§1` | Dark blue text |
|
||||
| `dark_green` | `§2` | Dark green text |
|
||||
| `dark_aqua` | `§3` | Dark aqua text |
|
||||
| `dark_red` | `§4` | Dark red text |
|
||||
| `dark_purple` | `§5` | Dark purple text |
|
||||
| `gold` | `§6` | Gold text |
|
||||
| `gray` | `§7` | Gray text |
|
||||
| `dark_gray` | `§8` | Dark gray text |
|
||||
| `blue` | `§9` | Blue text |
|
||||
| `green` | `§a` | Green text |
|
||||
| `aqua` | `§b` | Aqua text |
|
||||
| `red` | `§c` | Red text |
|
||||
| `light_purple` | `§d` | Light purple text |
|
||||
| `yellow` | `§e` | Yellow text |
|
||||
| `white` | `§f` | White text |
|
||||
|
||||
### Color Examples
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
use_colors = true
|
||||
open_color = "green" # Green for open status
|
||||
closed_color = "red" # Red for closed status
|
||||
info_color = "yellow" # Yellow for informational text
|
||||
```
|
||||
|
||||
## 📝 Custom Formatting
|
||||
|
||||
### Custom Format String
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
custom_format = "Status: %status% - Next: %openday% at %opentime%"
|
||||
```
|
||||
|
||||
### Available Placeholders
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
|-------------|-------------|---------|
|
||||
| `%status%` | Current server status | "Open" or "Closed" |
|
||||
| `%openday%` | Day when server next opens | "Monday" |
|
||||
| `%opentime%` | Time when server next opens | "9:00 AM" |
|
||||
| `%closetime%` | Time when server closes | "10:00 PM" |
|
||||
| `%nextopen%` | Combined next open info | "Monday at 9:00 AM" |
|
||||
| `%nextclose%` | Next close time | "10:00 PM" |
|
||||
| `%minutes%` | Minutes until close (countdown) | "15" |
|
||||
| `%mode%` | Current force mode | "NORMAL", "FORCE_OPEN", or "FORCE_CLOSED" |
|
||||
| `%isopen%` | Whether server is currently open | "yes" or "no" |
|
||||
|
||||
### Format Examples
|
||||
|
||||
```toml
|
||||
# Simple status
|
||||
custom_format = "Status: %status%"
|
||||
|
||||
# Detailed information
|
||||
custom_format = "Status: %status% - Next: %openday% at %opentime%"
|
||||
|
||||
# With countdown
|
||||
custom_format = "Status: %status% - Closing in %minutes% min"
|
||||
```
|
||||
|
||||
## 📋 Custom Lines
|
||||
|
||||
### Static Text Lines
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
custom_lines = [
|
||||
"🎮 My Game Server",
|
||||
"Status: %status%",
|
||||
"Next open: %openday% at %opentime%"
|
||||
]
|
||||
```
|
||||
|
||||
### Multi-Line Examples
|
||||
|
||||
```toml
|
||||
# Server branding with status
|
||||
custom_lines = [
|
||||
"🌟 Welcome to My Server!",
|
||||
"Status: %status% | Next: %openday%"
|
||||
]
|
||||
|
||||
# Detailed information
|
||||
custom_lines = [
|
||||
"🎮 My Game Server",
|
||||
"Status: %status%",
|
||||
"Next open: %openday% at %opentime%",
|
||||
"Visit: example.com"
|
||||
]
|
||||
```
|
||||
|
||||
## 🔄 Advanced Features
|
||||
|
||||
### Force Mode Display
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
show_force_mode = true # Show FORCE_OPEN/FORCE_CLOSED indicator
|
||||
```
|
||||
|
||||
**Display Examples:**
|
||||
- `⚠️ Always Open` (FORCE_OPEN mode)
|
||||
- `⚠️ Maintenance` (FORCE_CLOSED mode)
|
||||
|
||||
### Countdown Display
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
show_countdown = true # Show countdown when closing soon
|
||||
countdown_threshold_minutes = 30 # Minutes before close to show countdown
|
||||
```
|
||||
|
||||
**Display Examples:**
|
||||
- `Closing in 15 min` (when within threshold)
|
||||
- `Closing in 5 min` (when very close)
|
||||
|
||||
### Update Frequency
|
||||
|
||||
```toml
|
||||
[motd]
|
||||
update_delay_seconds = 60 # Delay between MOTD updates (1-600)
|
||||
```
|
||||
|
||||
**Update Intervals:**
|
||||
- **Fast (10-30 seconds)** - Near real-time updates
|
||||
- **Default (60 seconds)** - Good balance of accuracy and performance
|
||||
- **Slow (120+ seconds)** - Reduced server load
|
||||
|
||||
## 🎭 MOTD Examples
|
||||
|
||||
### Example 1: Default MOTD
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
show_status = true
|
||||
show_next_open = true
|
||||
show_next_close = true
|
||||
use_colors = true
|
||||
open_color = "green"
|
||||
closed_color = "red"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
[Your Server Name]
|
||||
🟢 Open | Closes at 10:00 PM
|
||||
```
|
||||
|
||||
### Example 2: Custom Format
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
custom_format = "Status: %status% - Next: %openday% at %opentime%"
|
||||
use_colors = true
|
||||
open_color = "green"
|
||||
closed_color = "red"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
Status: Closed - Next: Monday at 9:00 AM
|
||||
```
|
||||
|
||||
### Example 3: Custom Lines
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
custom_lines = [
|
||||
"🎮 My Game Server",
|
||||
"Status: %status%",
|
||||
"Next open: %openday% at %opentime%"
|
||||
]
|
||||
use_colors = true
|
||||
open_color = "green"
|
||||
closed_color = "red"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
🎮 My Game Server
|
||||
Status: Open
|
||||
Next open: Monday at 9:00 AM
|
||||
```
|
||||
|
||||
### Example 4: Force Mode Display
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
show_force_mode = true
|
||||
show_status = true
|
||||
use_colors = true
|
||||
```
|
||||
|
||||
**Display (FORCE_OPEN):**
|
||||
```
|
||||
⚠️ Always Open | Open
|
||||
```
|
||||
|
||||
**Display (FORCE_CLOSED):**
|
||||
```
|
||||
⚠️ Maintenance | Closed
|
||||
```
|
||||
|
||||
### Example 5: Countdown Display
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[motd]
|
||||
show_countdown = true
|
||||
countdown_threshold_minutes = 30
|
||||
show_status = true
|
||||
show_next_close = true
|
||||
```
|
||||
|
||||
**Display:**
|
||||
```
|
||||
🟢 Open | Closing in 15 min | Closes at 10:00 PM
|
||||
```
|
||||
|
||||
## 🔧 Performance Optimization
|
||||
|
||||
### Update Frequency Guidelines
|
||||
|
||||
| Server Size | Recommended Interval | Reason |
|
||||
|-------------|---------------------|---------|
|
||||
| Small (10-50 players) | 10-30 seconds | Near real-time updates |
|
||||
| Medium (50-150 players) | 30-60 seconds | Good balance |
|
||||
| Large (150+ players) | 60-120 seconds | Reduce server load |
|
||||
| Very Large (300+ players) | 120-300 seconds | Minimal impact |
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
**High-Traffic Server:**
|
||||
```toml
|
||||
[motd]
|
||||
update_delay_seconds = 120 # Update every 2 minutes
|
||||
```
|
||||
|
||||
**Real-Time Updates:**
|
||||
```toml
|
||||
[motd]
|
||||
update_delay_seconds = 10 # Update every 10 seconds
|
||||
```
|
||||
|
||||
**Minimal Impact:**
|
||||
```toml
|
||||
[motd]
|
||||
update_delay_seconds = 300 # Update every 5 minutes
|
||||
```
|
||||
|
||||
## 🚫 Minecraft Limitations
|
||||
|
||||
### Hard Limits
|
||||
|
||||
The MOTD display in Minecraft has strict limitations:
|
||||
|
||||
- **Maximum 2 lines** - Only 2 lines are displayed in the server list
|
||||
- **Character limit** - ~59 characters per line
|
||||
- **Total limit** - ~118 characters across both lines
|
||||
- **Color codes count** - Color codes count toward character limit
|
||||
|
||||
### What Gets Truncated
|
||||
|
||||
- **Lines beyond 2nd** - Automatically removed
|
||||
- **Lines exceeding 59 characters** - Automatically truncated
|
||||
- **Long placeholder values** - May cause truncation
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Keep it concise** - Use short, informative text
|
||||
2. **Test in-game** - Verify MOTD displays correctly
|
||||
3. **Use abbreviations** - "Mon" instead of "Monday"
|
||||
4. **Avoid redundancy** - Don't repeat information
|
||||
5. **Consider timezone** - Times can vary in length
|
||||
|
||||
### Good MOTD Examples
|
||||
|
||||
**Concise and Informative:**
|
||||
```
|
||||
Open | Next: Monday at 09:00 AM
|
||||
```
|
||||
(~41 characters - well under limit)
|
||||
|
||||
**With Status:**
|
||||
```
|
||||
Status: Open | Closing at 11:59 PM
|
||||
```
|
||||
(~39 characters per line)
|
||||
|
||||
**With Countdown:**
|
||||
```
|
||||
Open | Closing in 15 min | Next: Mon
|
||||
```
|
||||
(~35 characters per line)
|
||||
|
||||
## 🌍 Multi-Language Support
|
||||
|
||||
### Language-Specific Formatting
|
||||
|
||||
The MOTD system automatically adapts to the configured locale:
|
||||
|
||||
**English (en_us):**
|
||||
- Time format: 12-hour AM/PM (`09:00 AM`)
|
||||
- Day names: Full names (`Monday`)
|
||||
|
||||
**French (fr_fr):**
|
||||
- Time format: 24-hour (`18:00`)
|
||||
- Day names: French names (`Lundi`)
|
||||
|
||||
### Locale Configuration
|
||||
|
||||
```toml
|
||||
[general]
|
||||
message_locale = "en_us" # or "fr_fr"
|
||||
```
|
||||
|
||||
### Localized Examples
|
||||
|
||||
**English MOTD:**
|
||||
```
|
||||
🟢 Open | Closes at 10:00 PM
|
||||
```
|
||||
|
||||
**French MOTD:**
|
||||
```
|
||||
🟢 Ouvert | Ferme à 22:00
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**MOTD Not Updating:**
|
||||
- Check if `motd_enabled = true`
|
||||
- Verify update frequency settings
|
||||
- Check server logs for errors
|
||||
|
||||
**Truncated Display:**
|
||||
- Reduce text length
|
||||
- Use abbreviations
|
||||
- Test in-game to verify
|
||||
|
||||
**Color Issues:**
|
||||
- Verify color names are correct
|
||||
- Check for invalid color codes
|
||||
- Test with `use_colors = false`
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
/hours motd status # View current MOTD settings
|
||||
/hours motd toggle # Toggle MOTD on/off
|
||||
/hours reload # Reload configuration
|
||||
```
|
||||
|
||||
### Log Messages
|
||||
|
||||
Look for these log messages:
|
||||
|
||||
```
|
||||
[INFO] MOTD updated: Status: Open | Closes at 10:00 PM
|
||||
[WARN] MOTD truncated: Line exceeds 59 characters
|
||||
[ERROR] MOTD error: Invalid color code
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Configuration Guide](CONFIGURATION.md)** - MOTD configuration options
|
||||
- **[Commands Reference](COMMANDS.md)** - MOTD commands
|
||||
- **[Features Overview](FEATURES.md)** - MOTD feature details
|
||||
- **[Usage Examples](EXAMPLES.md)** - Real-world MOTD scenarios
|
||||
|
||||
---
|
||||
|
||||
*For configuration file options, see the [Configuration Guide](CONFIGURATION.md).*
|
||||
428
docs/PERMISSIONS.md
Normal file
428
docs/PERMISSIONS.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# Permissions System
|
||||
|
||||
PlayHours includes a comprehensive permission system with LuckPerms integration and vanilla ops fallback support.
|
||||
|
||||
## 🔑 Permission Nodes
|
||||
|
||||
### Core Permissions
|
||||
|
||||
| Permission | Description | Default Level |
|
||||
|------------|-------------|---------------|
|
||||
| `playhours.admin` | Full administrative access | Ops Level 2+ |
|
||||
| `playhours.view` | Read-only status access | Ops Level 1+ |
|
||||
| `playhours.exempt` | Bypass all schedule restrictions | Ops Level 2+ |
|
||||
|
||||
### Permission Details
|
||||
|
||||
#### `playhours.admin`
|
||||
**Full administrative access including:**
|
||||
- All `/hours` commands
|
||||
- Force mode changes
|
||||
- Configuration modifications
|
||||
- Schedule management
|
||||
- Exception handling
|
||||
- List management
|
||||
- MOTD control
|
||||
|
||||
#### `playhours.view`
|
||||
**Read-only access including:**
|
||||
- `/hours status` command
|
||||
- View current server status
|
||||
- Check schedule information
|
||||
- View force mode status
|
||||
|
||||
#### `playhours.exempt`
|
||||
**Bypass all schedule restrictions:**
|
||||
- Join server when closed
|
||||
- Join during closing threshold
|
||||
- Stay connected during kicks
|
||||
- Override blacklist restrictions
|
||||
|
||||
## 🔌 LuckPerms Integration
|
||||
|
||||
### Automatic Detection
|
||||
|
||||
PlayHours automatically detects and integrates with LuckPerms:
|
||||
|
||||
- **Soft dependency** - Works with or without LuckPerms
|
||||
- **Automatic detection** - Detected at server startup
|
||||
- **Graceful fallback** - Falls back to vanilla ops if LuckPerms unavailable
|
||||
- **No configuration required** - Works out of the box
|
||||
|
||||
### LuckPerms Setup
|
||||
|
||||
#### Basic Permission Setup
|
||||
|
||||
```bash
|
||||
# Grant view permission to all players
|
||||
/lp group default permission set playhours.view true
|
||||
|
||||
# Grant admin permission to admin group
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
|
||||
# Grant exempt permission to specific players
|
||||
/lp user PlayerName permission set playhours.exempt true
|
||||
```
|
||||
|
||||
#### Advanced Permission Setup
|
||||
|
||||
```bash
|
||||
# Create custom groups
|
||||
/lp creategroup moderator
|
||||
/lp creategroup admin
|
||||
|
||||
# Grant permissions to groups
|
||||
/lp group moderator permission set playhours.view true
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
|
||||
# Add players to groups
|
||||
/lp user PlayerName parent add moderator
|
||||
/lp user AdminName parent add admin
|
||||
```
|
||||
|
||||
#### Permission Inheritance
|
||||
|
||||
```bash
|
||||
# Set up permission inheritance
|
||||
/lp group moderator permission set playhours.view true
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
|
||||
# Admin inherits from moderator
|
||||
/lp group admin parent add moderator
|
||||
```
|
||||
|
||||
### LuckPerms Commands
|
||||
|
||||
#### User Management
|
||||
|
||||
```bash
|
||||
# Grant permissions to specific users
|
||||
/lp user PlayerName permission set playhours.view true
|
||||
/lp user PlayerName permission set playhours.admin true
|
||||
/lp user PlayerName permission set playhours.exempt true
|
||||
|
||||
# Remove permissions
|
||||
/lp user PlayerName permission unset playhours.view
|
||||
/lp user PlayerName permission unset playhours.admin
|
||||
/lp user PlayerName permission unset playhours.exempt
|
||||
```
|
||||
|
||||
#### Group Management
|
||||
|
||||
```bash
|
||||
# Grant permissions to groups
|
||||
/lp group default permission set playhours.view true
|
||||
/lp group moderator permission set playhours.view true
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
|
||||
# Remove permissions from groups
|
||||
/lp group default permission unset playhours.view
|
||||
/lp group admin permission unset playhours.admin
|
||||
```
|
||||
|
||||
#### Permission Checking
|
||||
|
||||
```bash
|
||||
# Check user permissions
|
||||
/lp user PlayerName permission check playhours.admin
|
||||
/lp user PlayerName permission check playhours.view
|
||||
/lp user PlayerName permission check playhours.exempt
|
||||
|
||||
# Check group permissions
|
||||
/lp group admin permission check playhours.admin
|
||||
/lp group admin permission check playhours.exempt
|
||||
```
|
||||
|
||||
## 🔄 Vanilla Ops Fallback
|
||||
|
||||
### When LuckPerms is Not Available
|
||||
|
||||
PlayHours automatically falls back to vanilla operator levels:
|
||||
|
||||
- **Level 0** - No access
|
||||
- **Level 1+** - `playhours.view` permission
|
||||
- **Level 2+** - `playhours.admin` and `playhours.exempt` permissions
|
||||
|
||||
### Ops Level Configuration
|
||||
|
||||
```bash
|
||||
# Grant ops level 1 (view access)
|
||||
/op PlayerName 1
|
||||
|
||||
# Grant ops level 2 (admin access)
|
||||
/op PlayerName 2
|
||||
|
||||
# Grant ops level 3 (admin access)
|
||||
/op PlayerName 3
|
||||
|
||||
# Remove ops
|
||||
/deop PlayerName
|
||||
```
|
||||
|
||||
### Ops Level Examples
|
||||
|
||||
```bash
|
||||
# Give view access to a player
|
||||
/op PlayerName 1
|
||||
|
||||
# Give admin access to a player
|
||||
/op PlayerName 2
|
||||
|
||||
# Give admin access to multiple players
|
||||
/op Player1 2
|
||||
/op Player2 2
|
||||
/op Player3 2
|
||||
```
|
||||
|
||||
## ⚡ Performance Considerations
|
||||
|
||||
### Permission Checking
|
||||
|
||||
PlayHours optimizes permission checking for performance:
|
||||
|
||||
- **Cached results** - Permission results are cached
|
||||
- **Timeout protection** - Prevents blocking on permission checks
|
||||
- **Efficient queries** - Minimal database queries
|
||||
- **Fallback handling** - Graceful degradation on errors
|
||||
|
||||
### Timeout Configuration
|
||||
|
||||
```java
|
||||
// Default timeout: 2 seconds
|
||||
public static final int LUCKPERMS_TIMEOUT_SECONDS = 2;
|
||||
```
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Use groups** - Grant permissions to groups rather than individual users
|
||||
2. **Limit exempt players** - Only grant exempt permission to necessary players
|
||||
3. **Cache permissions** - LuckPerms handles caching automatically
|
||||
4. **Monitor performance** - Check server logs for permission-related delays
|
||||
|
||||
## 🔧 Permission Scenarios
|
||||
|
||||
### Basic Server Setup
|
||||
|
||||
**All players can view status:**
|
||||
```bash
|
||||
/lp group default permission set playhours.view true
|
||||
```
|
||||
|
||||
**Admins have full access:**
|
||||
```bash
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
```
|
||||
|
||||
### Moderator Setup
|
||||
|
||||
**Moderators can view but not modify:**
|
||||
```bash
|
||||
/lp group moderator permission set playhours.view true
|
||||
# No admin or exempt permissions
|
||||
```
|
||||
|
||||
**Admins have full control:**
|
||||
```bash
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
```
|
||||
|
||||
### Custom Roles
|
||||
|
||||
**Create custom permission groups:**
|
||||
```bash
|
||||
# Create custom groups
|
||||
/lp creategroup helper
|
||||
/lp creategroup moderator
|
||||
/lp creategroup admin
|
||||
|
||||
# Grant appropriate permissions
|
||||
/lp group helper permission set playhours.view true
|
||||
/lp group moderator permission set playhours.view true
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
```
|
||||
|
||||
### Player-Specific Permissions
|
||||
|
||||
**Grant permissions to specific players:**
|
||||
```bash
|
||||
# Grant view access to specific player
|
||||
/lp user PlayerName permission set playhours.view true
|
||||
|
||||
# Grant admin access to specific player
|
||||
/lp user PlayerName permission set playhours.admin true
|
||||
/lp user PlayerName permission set playhours.exempt true
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Permission Denied Errors
|
||||
|
||||
**Error:** `You do not have permission to use this command`
|
||||
|
||||
**Solutions:**
|
||||
1. Check if player has required permission
|
||||
2. Verify LuckPerms is working correctly
|
||||
3. Check if player is in correct group
|
||||
4. Verify permission syntax is correct
|
||||
|
||||
#### LuckPerms Not Working
|
||||
|
||||
**Symptoms:**
|
||||
- Permissions not being recognized
|
||||
- Commands not working
|
||||
- Fallback to vanilla ops
|
||||
|
||||
**Solutions:**
|
||||
1. Check if LuckPerms is loaded
|
||||
2. Verify LuckPerms configuration
|
||||
3. Check server logs for errors
|
||||
4. Restart server if necessary
|
||||
|
||||
#### Permission Inheritance Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Players not inheriting group permissions
|
||||
- Inconsistent permission behavior
|
||||
|
||||
**Solutions:**
|
||||
1. Check group inheritance setup
|
||||
2. Verify parent-child relationships
|
||||
3. Check for conflicting permissions
|
||||
4. Use `/lp user PlayerName permission check` to debug
|
||||
|
||||
### Debug Commands
|
||||
|
||||
#### Check User Permissions
|
||||
|
||||
```bash
|
||||
# Check specific permission
|
||||
/lp user PlayerName permission check playhours.admin
|
||||
|
||||
# Check all permissions
|
||||
/lp user PlayerName permission check
|
||||
|
||||
# Check group permissions
|
||||
/lp group admin permission check playhours.admin
|
||||
```
|
||||
|
||||
#### Debug Permission Issues
|
||||
|
||||
```bash
|
||||
# Check user's groups
|
||||
/lp user PlayerName info
|
||||
|
||||
# Check group inheritance
|
||||
/lp group admin info
|
||||
|
||||
# Check permission inheritance
|
||||
/lp user PlayerName permission check playhours.admin
|
||||
```
|
||||
|
||||
### Log Analysis
|
||||
|
||||
Look for these log messages:
|
||||
|
||||
```
|
||||
[INFO] PlayHours: LuckPerms integration enabled
|
||||
[WARN] PlayHours: LuckPerms not found, using ops fallback
|
||||
[ERROR] PlayHours: Permission check timeout for user PlayerName
|
||||
[DEBUG] PlayHours: Permission check result: true for playhours.admin
|
||||
```
|
||||
|
||||
## 🔄 Permission Updates
|
||||
|
||||
### Hot Reload
|
||||
|
||||
Permissions are checked in real-time:
|
||||
|
||||
- **No restart required** - Changes take effect immediately
|
||||
- **Automatic updates** - LuckPerms changes are detected
|
||||
- **Cached results** - Permission results are cached for performance
|
||||
- **Fallback handling** - Graceful degradation on errors
|
||||
|
||||
### Permission Changes
|
||||
|
||||
```bash
|
||||
# Grant permission (takes effect immediately)
|
||||
/lp user PlayerName permission set playhours.admin true
|
||||
|
||||
# Remove permission (takes effect immediately)
|
||||
/lp user PlayerName permission unset playhours.admin
|
||||
|
||||
# Change group membership (takes effect immediately)
|
||||
/lp user PlayerName parent add admin
|
||||
```
|
||||
|
||||
## 📚 Permission Examples
|
||||
|
||||
### Server Owner Setup
|
||||
|
||||
```bash
|
||||
# Grant full access to server owner
|
||||
/lp user ServerOwner permission set playhours.admin true
|
||||
/lp user ServerOwner permission set playhours.exempt true
|
||||
```
|
||||
|
||||
### Admin Team Setup
|
||||
|
||||
```bash
|
||||
# Create admin group
|
||||
/lp creategroup admin
|
||||
|
||||
# Grant admin permissions
|
||||
/lp group admin permission set playhours.admin true
|
||||
/lp group admin permission set playhours.exempt true
|
||||
|
||||
# Add admins to group
|
||||
/lp user Admin1 parent add admin
|
||||
/lp user Admin2 parent add admin
|
||||
/lp user Admin3 parent add admin
|
||||
```
|
||||
|
||||
### Moderator Setup
|
||||
|
||||
```bash
|
||||
# Create moderator group
|
||||
/lp creategroup moderator
|
||||
|
||||
# Grant view permission
|
||||
/lp group moderator permission set playhours.view true
|
||||
|
||||
# Add moderators to group
|
||||
/lp user Mod1 parent add moderator
|
||||
/lp user Mod2 parent add moderator
|
||||
```
|
||||
|
||||
### Helper Setup
|
||||
|
||||
```bash
|
||||
# Create helper group
|
||||
/lp creategroup helper
|
||||
|
||||
# Grant view permission
|
||||
/lp group helper permission set playhours.view true
|
||||
|
||||
# Add helpers to group
|
||||
/lp user Helper1 parent add helper
|
||||
/lp user Helper2 parent add helper
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Commands Reference](COMMANDS.md)** - Permission requirements for commands
|
||||
- **[Features Overview](FEATURES.md)** - How permissions affect features
|
||||
- **[Configuration Guide](CONFIGURATION.md)** - Permission-related configuration
|
||||
- **[Usage Examples](EXAMPLES.md)** - Real-world permission scenarios
|
||||
|
||||
---
|
||||
|
||||
*For command-specific permission requirements, see the [Commands Reference](COMMANDS.md).*
|
||||
485
docs/TECHNICAL.md
Normal file
485
docs/TECHNICAL.md
Normal file
@@ -0,0 +1,485 @@
|
||||
# Technical Details
|
||||
|
||||
This document covers the technical architecture, implementation details, and limitations of the PlayHours mod.
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
### Core Components
|
||||
|
||||
PlayHours is built with a modular architecture consisting of several key components:
|
||||
|
||||
- **Core Logic** - Schedule calculation and enforcement
|
||||
- **Event Handlers** - Server tick and login event processing
|
||||
- **Configuration** - TOML-based configuration management
|
||||
- **Commands** - Brigadier-based command system
|
||||
- **Permissions** - LuckPerms integration with ops fallback
|
||||
- **Text System** - Localization and message formatting
|
||||
- **MOTD System** - Dynamic server list updates
|
||||
|
||||
### Class Structure
|
||||
|
||||
```
|
||||
com.mrkayjaydee.playhours/
|
||||
├── PlayHoursMod.java # Main mod class
|
||||
├── command/ # Command system
|
||||
│ ├── HoursCommand.java # Main command registration
|
||||
│ ├── CommandBuilder.java # Command utilities
|
||||
│ └── [Command Builders] # Specialized command builders
|
||||
├── config/ # Configuration system
|
||||
│ ├── ServerConfig.java # Main config class
|
||||
│ ├── GeneralConfig.java # General settings
|
||||
│ ├── DaysConfig.java # Day-specific schedules
|
||||
│ ├── ExceptionsConfig.java # Date exceptions
|
||||
│ ├── ListsConfig.java # Whitelist/blacklist
|
||||
│ ├── MessagesConfig.java # Message customization
|
||||
│ └── MOTDConfig.java # MOTD settings
|
||||
├── core/ # Core logic
|
||||
│ ├── ScheduleService.java # Main schedule service
|
||||
│ ├── ScheduleCalculator.java # Schedule calculations
|
||||
│ ├── ExceptionHandler.java # Exception handling
|
||||
│ ├── PlayerAccessChecker.java # Access control
|
||||
│ ├── TimeRange.java # Time range representation
|
||||
│ └── ForceMode.java # Force mode enum
|
||||
├── events/ # Event handlers
|
||||
│ ├── LoginGuard.java # Login event handling
|
||||
│ ├── TickScheduler.java # Server tick processing
|
||||
│ ├── MOTDHandler.java # MOTD updates
|
||||
│ └── [Event Handlers] # Other event handlers
|
||||
├── permissions/ # Permission system
|
||||
│ ├── PermissionChecker.java # Main permission checker
|
||||
│ ├── LuckPermsIntegration.java # LuckPerms integration
|
||||
│ └── PermissionConstants.java # Permission constants
|
||||
└── text/ # Text system
|
||||
├── Messages.java # Message management
|
||||
├── LocaleLoader.java # Locale loading
|
||||
└── [Text Handlers] # Text processing
|
||||
```
|
||||
|
||||
## ⚙️ Core Logic
|
||||
|
||||
### Schedule Service
|
||||
|
||||
The `ScheduleService` class is the central coordinator for all schedule-related operations:
|
||||
|
||||
```java
|
||||
public final class ScheduleService {
|
||||
private final PlayerAccessChecker accessChecker;
|
||||
private final ScheduleCalculator scheduleCalculator;
|
||||
private final ExceptionHandler exceptionHandler;
|
||||
|
||||
// Main schedule check
|
||||
public boolean isOpen(ZonedDateTime now) {
|
||||
// 1. Check force mode
|
||||
// 2. Check exceptions
|
||||
// 3. Check daily schedule
|
||||
}
|
||||
|
||||
// Player-specific access check
|
||||
public boolean isOpenForName(String name, boolean isExempt) {
|
||||
// 1. Check player access (lists, force mode)
|
||||
// 2. Check schedule if not exempt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Schedule Calculation
|
||||
|
||||
The `ScheduleCalculator` handles time-based schedule calculations:
|
||||
|
||||
```java
|
||||
public final class ScheduleCalculator {
|
||||
// Check if schedule is open at specific time
|
||||
public boolean isScheduleOpen(ZonedDateTime now) {
|
||||
// 1. Get current day of week
|
||||
// 2. Get periods for that day
|
||||
// 3. Check if current time falls within any period
|
||||
// 4. Handle midnight-spanning periods
|
||||
}
|
||||
|
||||
// Calculate next open/close times
|
||||
public Optional<ZonedDateTime> nextClose(ZonedDateTime now) {
|
||||
// Search through upcoming periods
|
||||
// Handle midnight-spanning periods
|
||||
// Return next closing time
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Time Range Handling
|
||||
|
||||
The `TimeRange` class represents time periods with support for midnight spanning:
|
||||
|
||||
```java
|
||||
public final class TimeRange {
|
||||
private final LocalTime start;
|
||||
private final LocalTime end;
|
||||
private final boolean spansMidnight;
|
||||
|
||||
// Check if time falls within range
|
||||
public boolean contains(LocalTime time) {
|
||||
if (spansMidnight) {
|
||||
return time.isAfter(start) || time.isBefore(end);
|
||||
} else {
|
||||
return time.isAfter(start) && time.isBefore(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Event System
|
||||
|
||||
### Login Guard
|
||||
|
||||
The `LoginGuard` class handles player login events:
|
||||
|
||||
```java
|
||||
@Mod.EventBusSubscriber
|
||||
public class LoginGuard {
|
||||
@SubscribeEvent
|
||||
public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
|
||||
// 1. Check if player is allowed by schedule/lists
|
||||
// 2. Check if within closing threshold
|
||||
// 3. Disconnect player if denied
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tick Scheduler
|
||||
|
||||
The `TickScheduler` class handles server tick events for warnings and kicks:
|
||||
|
||||
```java
|
||||
@Mod.EventBusSubscriber
|
||||
public class TickScheduler {
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
// 1. Check if server is open
|
||||
// 2. Broadcast warnings if closing soon
|
||||
// 3. Handle countdown messages
|
||||
// 4. Kick players at closing time
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MOTD Handler
|
||||
|
||||
The `MOTDHandler` class manages dynamic MOTD updates:
|
||||
|
||||
```java
|
||||
@Mod.EventBusSubscriber
|
||||
public class MOTDHandler {
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
// 1. Check if MOTD is enabled
|
||||
// 2. Update MOTD periodically
|
||||
// 3. Build MOTD content
|
||||
// 4. Apply to server
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Configuration System
|
||||
|
||||
### Forge Config Integration
|
||||
|
||||
PlayHours uses Forge's configuration system with TOML format:
|
||||
|
||||
```java
|
||||
public final class ServerConfig {
|
||||
public static final ForgeConfigSpec SPEC;
|
||||
static {
|
||||
ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
|
||||
|
||||
// Initialize all config sections
|
||||
GeneralConfig.init(BUILDER);
|
||||
DefaultsConfig.init(BUILDER);
|
||||
DaysConfig.init(BUILDER);
|
||||
ExceptionsConfig.init(BUILDER);
|
||||
ListsConfig.init(BUILDER);
|
||||
MessagesConfig.init(BUILDER);
|
||||
MOTDConfig.init(BUILDER);
|
||||
|
||||
SPEC = BUILDER.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Reload
|
||||
|
||||
The `ConfigEventHandler` handles configuration changes:
|
||||
|
||||
```java
|
||||
@Mod.EventBusSubscriber
|
||||
public class ConfigEventHandler {
|
||||
@SubscribeEvent
|
||||
public static void onConfigLoad(ModConfigEvent event) {
|
||||
// 1. Validate configuration
|
||||
// 2. Update schedule service
|
||||
// 3. Reload messages
|
||||
// 4. Update MOTD settings
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔑 Permission System
|
||||
|
||||
### Permission Checker
|
||||
|
||||
The `PermissionChecker` class handles permission validation:
|
||||
|
||||
```java
|
||||
public final class PermissionChecker {
|
||||
public static boolean hasAdmin(ServerPlayer player) {
|
||||
return hasPermission(player, PermissionConstants.ADMIN, PermissionConstants.ADMIN_FALLBACK_LEVEL);
|
||||
}
|
||||
|
||||
private static boolean hasPermission(ServerPlayer player, String permission, int fallbackLevel) {
|
||||
// 1. Try LuckPerms if available
|
||||
// 2. Fall back to vanilla ops
|
||||
// 3. Handle timeouts gracefully
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### LuckPerms Integration
|
||||
|
||||
The `LuckPermsIntegration` class handles LuckPerms-specific operations:
|
||||
|
||||
```java
|
||||
public final class LuckPermsIntegration {
|
||||
private static LuckPerms luckPerms;
|
||||
|
||||
static {
|
||||
try {
|
||||
luckPerms = LuckPermsProvider.get();
|
||||
} catch (Throwable ignored) {
|
||||
luckPerms = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasPermission(ServerPlayer player, String permission) {
|
||||
if (!isAvailable()) return false;
|
||||
|
||||
var user = luckPerms.getUserManager().getUser(player.getUUID());
|
||||
if (user == null) return false;
|
||||
|
||||
var data = user.getCachedData().getPermissionData();
|
||||
return data.checkPermission(permission).asBoolean();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 💬 Text System
|
||||
|
||||
### Message Management
|
||||
|
||||
The `Messages` class handles localized message loading and formatting:
|
||||
|
||||
```java
|
||||
public final class Messages {
|
||||
private static volatile String currentLocale = "en_us";
|
||||
private static volatile Map<String, String> bundle = new HashMap<>();
|
||||
private static volatile Map<String, String> overrides = new HashMap<>();
|
||||
|
||||
public static Component accessDenied(String openDay, String openTime) {
|
||||
// 1. Get message template
|
||||
// 2. Replace placeholders
|
||||
// 3. Return formatted message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Locale Loading
|
||||
|
||||
The `LocaleLoader` class handles language file loading:
|
||||
|
||||
```java
|
||||
public final class LocaleLoader {
|
||||
public static Map<String, String> loadLocale(String locale) {
|
||||
// 1. Load language file from resources
|
||||
// 2. Parse JSON content
|
||||
// 3. Return message mappings
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📢 MOTD System
|
||||
|
||||
### MOTD Builder
|
||||
|
||||
The `MOTDBuilder` class constructs MOTD content:
|
||||
|
||||
```java
|
||||
public final class MOTDBuilder {
|
||||
public static Component build(ScheduleService scheduleService, ZonedDateTime now) {
|
||||
// 1. Check current status
|
||||
// 2. Build status line
|
||||
// 3. Add next open/close times
|
||||
// 4. Add countdown if applicable
|
||||
// 5. Apply colors and formatting
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MOTD Validator
|
||||
|
||||
The `MOTDValidator` class ensures MOTD content fits Minecraft limits:
|
||||
|
||||
```java
|
||||
public final class MOTDValidator {
|
||||
public static Component validateAndTruncate(Component motd) {
|
||||
// 1. Check line count (max 2 lines)
|
||||
// 2. Check character count (max 59 per line)
|
||||
// 3. Truncate if necessary
|
||||
// 4. Return validated MOTD
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 Known Limitations
|
||||
|
||||
### Early Login Denial
|
||||
|
||||
**Issue:** Forge 1.20.1 lacks a true pre-join network event.
|
||||
|
||||
**Impact:** Players briefly "join" before being disconnected.
|
||||
|
||||
**Workaround:** Uses `PlayerLoggedInEvent` with immediate disconnection.
|
||||
|
||||
**Future:** Could be improved with newer Forge versions.
|
||||
|
||||
### Deprecation Warning
|
||||
|
||||
**Issue:** `FMLJavaModLoadingContext.get()` is deprecated.
|
||||
|
||||
**Impact:** Warning in server logs.
|
||||
|
||||
**Reason:** Required for 1.20.1 compatibility.
|
||||
|
||||
**Future:** Will be updated for newer Forge versions.
|
||||
|
||||
### MOTD Caching
|
||||
|
||||
**Issue:** Some server list clients may cache MOTD content.
|
||||
|
||||
**Impact:** MOTD updates may not be immediately visible.
|
||||
|
||||
**Workaround:** MOTD updates every 60 seconds by default.
|
||||
|
||||
**Future:** Could be improved with more frequent updates.
|
||||
|
||||
## 🔧 Performance Considerations
|
||||
|
||||
### Schedule Caching
|
||||
|
||||
- **Cached calculations** - Schedule data is cached for performance
|
||||
- **Efficient lookups** - Time-based lookups are optimized
|
||||
- **Minimal memory usage** - Only essential data is cached
|
||||
|
||||
### Event Processing
|
||||
|
||||
- **Tick optimization** - Events are processed efficiently
|
||||
- **Minimal overhead** - Only necessary checks are performed
|
||||
- **Resource management** - Proper cleanup of resources
|
||||
|
||||
### Permission Checking
|
||||
|
||||
- **Cached results** - Permission results are cached
|
||||
- **Timeout protection** - Prevents blocking on permission checks
|
||||
- **Fallback handling** - Graceful degradation on errors
|
||||
|
||||
## 🛠️ Building from Source
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Java:** 17 or 23
|
||||
- **Gradle:** 8.8+ (included via wrapper)
|
||||
- **Forge:** 47.4.10+
|
||||
|
||||
### Build Process
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone <repository-url>
|
||||
cd PlayHours
|
||||
|
||||
# Build the mod
|
||||
./gradlew build
|
||||
|
||||
# Output: build/libs/playhours-1.0.0.jar
|
||||
```
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Setup development environment
|
||||
./gradlew genEclipseRuns # For Eclipse
|
||||
./gradlew genIntellijRuns # For IntelliJ IDEA
|
||||
|
||||
# Run development server
|
||||
./gradlew runServer
|
||||
```
|
||||
|
||||
### Build Configuration
|
||||
|
||||
The `build.gradle` file contains the build configuration:
|
||||
|
||||
```gradle
|
||||
plugins {
|
||||
id 'net.minecraftforge.gradle' version '5.1.+'
|
||||
id 'org.parchmentmc.librarian.forgegradle' version '1.+'
|
||||
}
|
||||
|
||||
minecraft {
|
||||
version = '1.20.1'
|
||||
mappings channel: 'official', version: '1.20.1'
|
||||
runs {
|
||||
server {
|
||||
workingDirectory project.file('run')
|
||||
property 'forge.logging.markers', 'REGISTRIES'
|
||||
property 'forge.logging.console.level', 'debug'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Debugging
|
||||
|
||||
### Log Levels
|
||||
|
||||
PlayHours uses different log levels for different types of information:
|
||||
|
||||
- **INFO** - General information and status
|
||||
- **WARN** - Warnings and fallback behavior
|
||||
- **ERROR** - Errors and exceptions
|
||||
- **DEBUG** - Detailed debugging information
|
||||
|
||||
### Debug Configuration
|
||||
|
||||
Enable debug logging by setting the log level:
|
||||
|
||||
```properties
|
||||
# In server.properties or log4j2.xml
|
||||
logger.playhours.level=DEBUG
|
||||
```
|
||||
|
||||
### Common Debug Messages
|
||||
|
||||
```
|
||||
[INFO] PlayHours loading... (modId=playhours)
|
||||
[INFO] PlayHours common setup initialized
|
||||
[WARN] PlayHours: LuckPerms not found, using ops fallback
|
||||
[DEBUG] PlayHours: Config not ready yet
|
||||
[ERROR] PlayHours: Permission check timeout for user PlayerName
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Features Overview](FEATURES.md)** - How features work
|
||||
- **[Configuration Guide](CONFIGURATION.md)** - Configuration details
|
||||
- **[Commands Reference](COMMANDS.md)** - Command implementation
|
||||
- **[Usage Examples](EXAMPLES.md)** - Real-world usage
|
||||
|
||||
---
|
||||
|
||||
*For feature-specific details, see the [Features Overview](FEATURES.md).*
|
||||
@@ -42,9 +42,9 @@ mapping_version=1.20.1
|
||||
|
||||
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
|
||||
# Must match the String constant located in the main mod class annotated with @Mod.
|
||||
mod_id=examplemod
|
||||
mod_id=playhours
|
||||
# The human-readable display name for the mod.
|
||||
mod_name=Example Mod
|
||||
mod_name=PlayHours
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=All Rights Reserved
|
||||
# The mod version. See https://semver.org/
|
||||
@@ -52,8 +52,8 @@ mod_version=1.0.0
|
||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||
# This should match the base package used for the mod sources.
|
||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||
mod_group_id=com.example.examplemod
|
||||
mod_group_id=com.mrkayjaydee.playhours
|
||||
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
|
||||
mod_authors=YourNameHere, OtherNameHere
|
||||
mod_authors=Mr-KayJayDee
|
||||
# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
|
||||
mod_description=Example mod description.\nNewline characters can be used and will be replaced properly.
|
||||
mod_description=Enforces server open hours with schedules, warnings, login control, and admin commands.\nServer-only. LuckPerms compatible (soft).
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.example.examplemod;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.event.config.ModConfigEvent;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
// An example config class. This is not required, but it's a good idea to have one to keep your config organized.
|
||||
// Demonstrates how to use Forge's config APIs
|
||||
@Mod.EventBusSubscriber(modid = ExampleMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
|
||||
public class Config
|
||||
{
|
||||
private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
|
||||
|
||||
private static final ForgeConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER
|
||||
.comment("Whether to log the dirt block on common setup")
|
||||
.define("logDirtBlock", true);
|
||||
|
||||
private static final ForgeConfigSpec.IntValue MAGIC_NUMBER = BUILDER
|
||||
.comment("A magic number")
|
||||
.defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE);
|
||||
|
||||
public static final ForgeConfigSpec.ConfigValue<String> MAGIC_NUMBER_INTRODUCTION = BUILDER
|
||||
.comment("What you want the introduction message to be for the magic number")
|
||||
.define("magicNumberIntroduction", "The magic number is... ");
|
||||
|
||||
// a list of strings that are treated as resource locations for items
|
||||
private static final ForgeConfigSpec.ConfigValue<List<? extends String>> ITEM_STRINGS = BUILDER
|
||||
.comment("A list of items to log on common setup.")
|
||||
.defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), Config::validateItemName);
|
||||
|
||||
static final ForgeConfigSpec SPEC = BUILDER.build();
|
||||
|
||||
public static boolean logDirtBlock;
|
||||
public static int magicNumber;
|
||||
public static String magicNumberIntroduction;
|
||||
public static Set<Item> items;
|
||||
|
||||
private static boolean validateItemName(final Object obj)
|
||||
{
|
||||
return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(new ResourceLocation(itemName));
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
static void onLoad(final ModConfigEvent event)
|
||||
{
|
||||
logDirtBlock = LOG_DIRT_BLOCK.get();
|
||||
magicNumber = MAGIC_NUMBER.get();
|
||||
magicNumberIntroduction = MAGIC_NUMBER_INTRODUCTION.get();
|
||||
|
||||
// convert the list of strings into a set of items
|
||||
items = ITEM_STRINGS.get().stream()
|
||||
.map(itemName -> ForgeRegistries.ITEMS.getValue(new ResourceLocation(itemName)))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.example.examplemod;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.food.FoodProperties;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.CreativeModeTabs;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.BuildCreativeModeTabContentsEvent;
|
||||
import net.minecraftforge.event.server.ServerStartingEvent;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
|
||||
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
// The value here should match an entry in the META-INF/mods.toml file
|
||||
@Mod(ExampleMod.MODID)
|
||||
public class ExampleMod
|
||||
{
|
||||
// Define mod id in a common place for everything to reference
|
||||
public static final String MODID = "examplemod";
|
||||
// Directly reference a slf4j logger
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
// Create a Deferred Register to hold Blocks which will all be registered under the "examplemod" namespace
|
||||
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
|
||||
// Create a Deferred Register to hold Items which will all be registered under the "examplemod" namespace
|
||||
public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID);
|
||||
// Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "examplemod" namespace
|
||||
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);
|
||||
|
||||
// Creates a new Block with the id "examplemod:example_block", combining the namespace and path
|
||||
public static final RegistryObject<Block> EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE)));
|
||||
// Creates a new BlockItem with the id "examplemod:example_block", combining the namespace and path
|
||||
public static final RegistryObject<Item> EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties()));
|
||||
|
||||
// Creates a new food item with the id "examplemod:example_id", nutrition 1 and saturation 2
|
||||
public static final RegistryObject<Item> EXAMPLE_ITEM = ITEMS.register("example_item", () -> new Item(new Item.Properties().food(new FoodProperties.Builder()
|
||||
.alwaysEat().nutrition(1).saturationMod(2f).build())));
|
||||
|
||||
// Creates a creative tab with the id "examplemod:example_tab" for the example item, that is placed after the combat tab
|
||||
public static final RegistryObject<CreativeModeTab> EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder()
|
||||
.withTabsBefore(CreativeModeTabs.COMBAT)
|
||||
.icon(() -> EXAMPLE_ITEM.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(EXAMPLE_ITEM.get()); // Add the example item to the tab. For your own tabs, this method is preferred over the event
|
||||
}).build());
|
||||
|
||||
public ExampleMod(FMLJavaModLoadingContext context)
|
||||
{
|
||||
IEventBus modEventBus = context.getModEventBus();
|
||||
|
||||
// Register the commonSetup method for modloading
|
||||
modEventBus.addListener(this::commonSetup);
|
||||
|
||||
// Register the Deferred Register to the mod event bus so blocks get registered
|
||||
BLOCKS.register(modEventBus);
|
||||
// Register the Deferred Register to the mod event bus so items get registered
|
||||
ITEMS.register(modEventBus);
|
||||
// Register the Deferred Register to the mod event bus so tabs get registered
|
||||
CREATIVE_MODE_TABS.register(modEventBus);
|
||||
|
||||
// Register ourselves for server and other game events we are interested in
|
||||
MinecraftForge.EVENT_BUS.register(this);
|
||||
|
||||
// Register the item to a creative tab
|
||||
modEventBus.addListener(this::addCreative);
|
||||
|
||||
// Register our mod's ForgeConfigSpec so that Forge can create and load the config file for us
|
||||
context.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
|
||||
}
|
||||
|
||||
private void commonSetup(final FMLCommonSetupEvent event)
|
||||
{
|
||||
// Some common setup code
|
||||
LOGGER.info("HELLO FROM COMMON SETUP");
|
||||
|
||||
if (Config.logDirtBlock)
|
||||
LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT));
|
||||
|
||||
LOGGER.info(Config.magicNumberIntroduction + Config.magicNumber);
|
||||
|
||||
Config.items.forEach((item) -> LOGGER.info("ITEM >> {}", item.toString()));
|
||||
}
|
||||
|
||||
// Add the example block item to the building blocks tab
|
||||
private void addCreative(BuildCreativeModeTabContentsEvent event)
|
||||
{
|
||||
if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS)
|
||||
event.accept(EXAMPLE_BLOCK_ITEM);
|
||||
}
|
||||
|
||||
// You can use SubscribeEvent and let the Event Bus discover methods to call
|
||||
@SubscribeEvent
|
||||
public void onServerStarting(ServerStartingEvent event)
|
||||
{
|
||||
// Do something when the server starts
|
||||
LOGGER.info("HELLO from server starting");
|
||||
}
|
||||
|
||||
// You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent
|
||||
@Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
|
||||
public static class ClientModEvents
|
||||
{
|
||||
@SubscribeEvent
|
||||
public static void onClientSetup(FMLClientSetupEvent event)
|
||||
{
|
||||
// Some client setup code
|
||||
LOGGER.info("HELLO FROM CLIENT SETUP");
|
||||
LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/main/java/com/mrkayjaydee/playhours/PlayHoursMod.java
Normal file
57
src/main/java/com/mrkayjaydee/playhours/PlayHoursMod.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.mrkayjaydee.playhours;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.mrkayjaydee.playhours.command.HoursCommand;
|
||||
import com.mrkayjaydee.playhours.config.ConfigEventHandler;
|
||||
import com.mrkayjaydee.playhours.config.ServerConfig;
|
||||
import com.mrkayjaydee.playhours.events.LoginGuard;
|
||||
import com.mrkayjaydee.playhours.events.TickScheduler;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.ModLoadingContext;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* PlayHours - Server-side mod for enforcing operating hours.
|
||||
* Provides schedule-based access control with warnings, auto-kick, and admin commands.
|
||||
* Compatible with LuckPerms for permissions (soft dependency).
|
||||
*/
|
||||
@Mod(PlayHoursMod.MODID)
|
||||
public class PlayHoursMod {
|
||||
public static final String MODID = "playhours";
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
/**
|
||||
* Mod constructor. Registers config, events, and command handlers.
|
||||
*/
|
||||
public PlayHoursMod() {
|
||||
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
|
||||
modEventBus.register(ConfigEventHandler.class);
|
||||
// Register as COMMON so the file is created under <server>/config (root), not per-world
|
||||
ModLoadingContext.get().registerConfig(net.minecraftforge.fml.config.ModConfig.Type.COMMON, ServerConfig.SPEC, MODID + ".toml");
|
||||
modEventBus.addListener(this::onCommonSetup);
|
||||
|
||||
// Register gameplay/event listeners
|
||||
MinecraftForge.EVENT_BUS.register(new LoginGuard());
|
||||
MinecraftForge.EVENT_BUS.register(new TickScheduler());
|
||||
MinecraftForge.EVENT_BUS.register(this);
|
||||
|
||||
LOGGER.info("PlayHours loading... (modId={})", MODID);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onRegisterCommands(RegisterCommandsEvent event) {
|
||||
LOGGER.info("Registering /hours command tree");
|
||||
HoursCommand.register(event.getDispatcher());
|
||||
}
|
||||
|
||||
private void onCommonSetup(final FMLCommonSetupEvent event) {
|
||||
LOGGER.info("PlayHours common setup initialized");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.permissions.PermissionChecker;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
/**
|
||||
* Utility class for building common command patterns.
|
||||
* Reduces code duplication in command registration.
|
||||
*/
|
||||
public final class CommandBuilder {
|
||||
private CommandBuilder() {}
|
||||
|
||||
/**
|
||||
* Creates a literal command with permission requirements.
|
||||
*
|
||||
* @param literal the command literal
|
||||
* @param requiresAdmin whether admin permission is required
|
||||
* @return a literal argument builder
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> literal(String literal, boolean requiresAdmin) {
|
||||
return Commands.literal(literal)
|
||||
.requires(src -> src.getPlayer() == null ||
|
||||
(requiresAdmin ? PermissionChecker.hasAdmin(src.getPlayer()) : PermissionChecker.hasView(src.getPlayer())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a command that requires admin permission.
|
||||
*
|
||||
* @param literal the command literal
|
||||
* @return a literal argument builder
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> adminLiteral(String literal) {
|
||||
return literal(literal, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a command that requires view permission.
|
||||
*
|
||||
* @param literal the command literal
|
||||
* @return a literal argument builder
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> viewLiteral(String literal) {
|
||||
return literal(literal, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a save and rebuild helper for commands that modify config.
|
||||
*
|
||||
* @param src the command source
|
||||
*/
|
||||
public static void saveAndRebuild(CommandSourceStack src) {
|
||||
com.mrkayjaydee.playhours.config.ConfigEventHandler.save();
|
||||
com.mrkayjaydee.playhours.core.ScheduleService.get().rebuildFromConfig();
|
||||
src.sendSuccess(() -> com.mrkayjaydee.playhours.text.Messages.settingsUpdated(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.config.*;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builder for day-specific command nodes.
|
||||
* Reduces duplication in day command registration.
|
||||
*/
|
||||
public final class DayCommandBuilder {
|
||||
private DayCommandBuilder() {}
|
||||
|
||||
/**
|
||||
* Creates a day command node with add/clear operations for periods.
|
||||
*
|
||||
* @param dayName the day name (e.g., "mon", "tue")
|
||||
* @param configValue the config value to modify
|
||||
* @return a command node
|
||||
*/
|
||||
public static ArgumentBuilder<CommandSourceStack, ?> createDayNode(String dayName,
|
||||
net.minecraftforge.common.ForgeConfigSpec.ConfigValue<List<? extends String>> configValue) {
|
||||
return Commands.literal(dayName)
|
||||
.then(Commands.literal("periods")
|
||||
.then(Commands.literal("add")
|
||||
.then(Commands.argument("range", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String range = StringArgumentType.getString(ctx, "range");
|
||||
TimeRangeValidator.validateRange(range);
|
||||
List<String> list = new ArrayList<>(configValue.get());
|
||||
list.add(range);
|
||||
configValue.set(list);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("clear")
|
||||
.executes(ctx -> {
|
||||
configValue.set(new ArrayList<>());
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all day command nodes.
|
||||
*
|
||||
* @return a command node containing all day commands
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> createAllDayNodes() {
|
||||
return Commands.literal("day")
|
||||
.then(createDayNode("mon", DaysConfig.MON))
|
||||
.then(createDayNode("tue", DaysConfig.TUE))
|
||||
.then(createDayNode("wed", DaysConfig.WED))
|
||||
.then(createDayNode("thu", DaysConfig.THU))
|
||||
.then(createDayNode("fri", DaysConfig.FRI))
|
||||
.then(createDayNode("sat", DaysConfig.SAT))
|
||||
.then(createDayNode("sun", DaysConfig.SUN));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.core.TimeRange;
|
||||
import com.mrkayjaydee.playhours.core.ForceModeFormatter;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import com.mrkayjaydee.playhours.config.*;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.TextStyle;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Main entry point for /hours command registration.
|
||||
* Delegates to specialized builders for different command groups.
|
||||
*/
|
||||
public final class HoursCommand {
|
||||
private HoursCommand() {}
|
||||
|
||||
/**
|
||||
* Registers the /hours command tree with the Minecraft command dispatcher.
|
||||
* @param d the command dispatcher
|
||||
*/
|
||||
public static void register(CommandDispatcher<CommandSourceStack> d) {
|
||||
d.register(Commands.literal("hours")
|
||||
.requires(src -> src.hasPermission(0))
|
||||
.then(registerStatusCommand())
|
||||
.then(registerForceCommand())
|
||||
.then(registerReloadCommand())
|
||||
.then(SetCommandBuilder.build())
|
||||
.then(registerExceptionsCommand())
|
||||
.then(ListsCommandBuilder.build())
|
||||
.then(MOTDCommandBuilder.build())
|
||||
.then(MessagesCommandBuilder.build())
|
||||
);
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> registerStatusCommand() {
|
||||
return CommandBuilder.viewLiteral("status")
|
||||
.executes(ctx -> {
|
||||
if (!ConfigEventHandler.isReady()) {
|
||||
ctx.getSource().sendFailure(Messages.configNotReady());
|
||||
return 0;
|
||||
}
|
||||
ScheduleService s = ScheduleService.get();
|
||||
ZonedDateTime now = ZonedDateTime.now(s.getZoneId());
|
||||
boolean open = s.isOpen(now);
|
||||
String nextClose = s.nextClose(now).map(z -> TimeRange.formatTime(z.toLocalTime(), Messages.getJavaLocale())).orElse("-");
|
||||
var no = s.nextOpen(now);
|
||||
String day = no.map(z -> z.getDayOfWeek().getDisplayName(TextStyle.FULL, Messages.getJavaLocale())).orElse("-");
|
||||
String time = no.map(z -> TimeRange.formatTime(z.toLocalTime(), Messages.getJavaLocale())).orElse("-");
|
||||
String modeDisplay = ForceModeFormatter.format(s.getForceMode());
|
||||
ctx.getSource().sendSuccess(() -> Messages.statusLine(modeDisplay, open, nextClose, day, time), false);
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> registerForceCommand() {
|
||||
return CommandBuilder.adminLiteral("force")
|
||||
.then(Commands.literal("normal").executes(ctx -> setForce("NORMAL", ctx.getSource())))
|
||||
.then(Commands.literal("open").executes(ctx -> setForce("FORCE_OPEN", ctx.getSource())))
|
||||
.then(Commands.literal("close").executes(ctx -> setForce("FORCE_CLOSED", ctx.getSource())));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> registerReloadCommand() {
|
||||
return CommandBuilder.adminLiteral("reload")
|
||||
.executes(ctx -> {
|
||||
com.mrkayjaydee.playhours.PlayHoursMod.LOGGER.info("/hours reload invoked by {}", ctx.getSource().getTextName());
|
||||
ConfigEventHandler.reloadFromDisk();
|
||||
ctx.getSource().sendSuccess(() -> Messages.configReloaded(), true);
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> registerExceptionsCommand() {
|
||||
return CommandBuilder.adminLiteral("exceptions")
|
||||
.then(Commands.literal("add-open").then(Commands.argument("spec", StringArgumentType.greedyString()).executes(ctx -> {
|
||||
String spec = StringArgumentType.getString(ctx, "spec");
|
||||
List<String> list = new ArrayList<>(ExceptionsConfig.OPEN_DATES.get());
|
||||
list.add(spec);
|
||||
ExceptionsConfig.OPEN_DATES.set(list);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("add-closed").then(Commands.argument("spec", StringArgumentType.greedyString()).executes(ctx -> {
|
||||
String spec = StringArgumentType.getString(ctx, "spec");
|
||||
List<String> list = new ArrayList<>(ExceptionsConfig.CLOSED_DATES.get());
|
||||
list.add(spec);
|
||||
ExceptionsConfig.CLOSED_DATES.set(list);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("clear").executes(ctx -> {
|
||||
ExceptionsConfig.OPEN_DATES.set(new ArrayList<>());
|
||||
ExceptionsConfig.CLOSED_DATES.set(new ArrayList<>());
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
}));
|
||||
}
|
||||
|
||||
private static int setForce(String mode, CommandSourceStack src) {
|
||||
GeneralConfig.FORCE_MODE.set(mode);
|
||||
CommandBuilder.saveAndRebuild(src);
|
||||
if ("FORCE_OPEN".equals(mode)) src.sendSuccess(() -> Messages.forceOpen(), true);
|
||||
if ("FORCE_CLOSED".equals(mode)) src.sendSuccess(() -> Messages.forceClosed(), true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.config.ListsConfig;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds /hours lists command variants for whitelist and blacklist management.
|
||||
* Provides add, remove, toggle, enable/disable, and clear operations.
|
||||
*/
|
||||
public final class ListsCommandBuilder {
|
||||
private ListsCommandBuilder() {}
|
||||
|
||||
/**
|
||||
* Builds the complete /hours lists command node.
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build() {
|
||||
return CommandBuilder.adminLiteral("lists")
|
||||
.then(Commands.literal("whitelist")
|
||||
.then(Commands.literal("toggle").then(Commands.argument("player", StringArgumentType.word()).executes(ctx -> {
|
||||
String p = StringArgumentType.getString(ctx, "player");
|
||||
List<String> set = new ArrayList<>(ListsConfig.WHITELIST.get());
|
||||
if (set.contains(p)) set.remove(p); else set.add(p);
|
||||
ListsConfig.WHITELIST.set(set);
|
||||
ListsConfig.WHITELIST_ENABLED.set(true);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("enabled").then(Commands.argument("enabled", BoolArgumentType.bool()).executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
ListsConfig.WHITELIST_ENABLED.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("add").then(Commands.argument("player", StringArgumentType.word()).executes(ctx -> {
|
||||
String p = StringArgumentType.getString(ctx, "player");
|
||||
List<String> set = new ArrayList<>(ListsConfig.WHITELIST.get());
|
||||
if (!set.contains(p)) set.add(p);
|
||||
ListsConfig.WHITELIST.set(set);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("remove").then(Commands.argument("player", StringArgumentType.word()).executes(ctx -> {
|
||||
String p = StringArgumentType.getString(ctx, "player");
|
||||
List<String> set = new ArrayList<>(ListsConfig.WHITELIST.get());
|
||||
set.remove(p);
|
||||
ListsConfig.WHITELIST.set(set);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("clear").executes(ctx -> {
|
||||
ListsConfig.WHITELIST.set(new ArrayList<>());
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("blacklist")
|
||||
.then(Commands.literal("toggle").then(Commands.argument("player", StringArgumentType.word()).executes(ctx -> {
|
||||
String p = StringArgumentType.getString(ctx, "player");
|
||||
List<String> set = new ArrayList<>(ListsConfig.BLACKLIST.get());
|
||||
if (set.contains(p)) set.remove(p); else set.add(p);
|
||||
ListsConfig.BLACKLIST.set(set);
|
||||
ListsConfig.BLACKLIST_ENABLED.set(true);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("enabled").then(Commands.argument("enabled", BoolArgumentType.bool()).executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
ListsConfig.BLACKLIST_ENABLED.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("add").then(Commands.argument("player", StringArgumentType.word()).executes(ctx -> {
|
||||
String p = StringArgumentType.getString(ctx, "player");
|
||||
List<String> set = new ArrayList<>(ListsConfig.BLACKLIST.get());
|
||||
if (!set.contains(p)) set.add(p);
|
||||
ListsConfig.BLACKLIST.set(set);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("remove").then(Commands.argument("player", StringArgumentType.word()).executes(ctx -> {
|
||||
String p = StringArgumentType.getString(ctx, "player");
|
||||
List<String> set = new ArrayList<>(ListsConfig.BLACKLIST.get());
|
||||
set.remove(p);
|
||||
ListsConfig.BLACKLIST.set(set);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("clear").executes(ctx -> {
|
||||
ListsConfig.BLACKLIST.set(new ArrayList<>());
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.config.MOTDConfig;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
/**
|
||||
* Builds /hours motd command variants for MOTD (Message of the Day) configuration.
|
||||
* Handles display options, colors, updates, and rotation settings.
|
||||
*/
|
||||
public final class MOTDCommandBuilder {
|
||||
private MOTDCommandBuilder() {}
|
||||
|
||||
/**
|
||||
* Builds the complete /hours motd command node.
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build() {
|
||||
return CommandBuilder.adminLiteral("motd")
|
||||
.then(Commands.literal("show_status").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_STATUS.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("show_next_open").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_NEXT_OPEN.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("show_next_close").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_NEXT_CLOSE.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("show_schedule_times").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_SCHEDULE_TIMES.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("show_on_second_line").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_ON_SECOND_LINE.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("separator").then(Commands.argument("separator", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String sep = StringArgumentType.getString(ctx, "separator");
|
||||
MOTDConfig.SEPARATOR.set(sep);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("custom_format").then(Commands.argument("format", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String format = StringArgumentType.getString(ctx, "format");
|
||||
MOTDConfig.CUSTOM_FORMAT.set(format);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("use_colors").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.USE_COLORS.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("open_color").then(Commands.argument("color", StringArgumentType.word())
|
||||
.executes(ctx -> {
|
||||
String color = StringArgumentType.getString(ctx, "color");
|
||||
MOTDConfig.OPEN_COLOR.set(color);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("closed_color").then(Commands.argument("color", StringArgumentType.word())
|
||||
.executes(ctx -> {
|
||||
String color = StringArgumentType.getString(ctx, "color");
|
||||
MOTDConfig.CLOSED_COLOR.set(color);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("info_color").then(Commands.argument("color", StringArgumentType.word())
|
||||
.executes(ctx -> {
|
||||
String color = StringArgumentType.getString(ctx, "color");
|
||||
MOTDConfig.INFO_COLOR.set(color);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("show_force_mode").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_FORCE_MODE.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("show_countdown").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.SHOW_COUNTDOWN.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("countdown_threshold").then(Commands.argument("minutes", IntegerArgumentType.integer(0, 1440))
|
||||
.executes(ctx -> {
|
||||
int m = IntegerArgumentType.getInteger(ctx, "minutes");
|
||||
MOTDConfig.COUNTDOWN_THRESHOLD_MINUTES.set(m);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("update_delay").then(Commands.argument("seconds", IntegerArgumentType.integer(1, 600))
|
||||
.executes(ctx -> {
|
||||
int s = IntegerArgumentType.getInteger(ctx, "seconds");
|
||||
MOTDConfig.UPDATE_DELAY_SECONDS.set(s);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("rotation_enabled").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
MOTDConfig.ROTATION_ENABLED.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("rotation_interval").then(Commands.argument("seconds", IntegerArgumentType.integer(1, 3600))
|
||||
.executes(ctx -> {
|
||||
int s = IntegerArgumentType.getInteger(ctx, "seconds");
|
||||
MOTDConfig.ROTATION_INTERVAL_SECONDS.set(s);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.config.MessagesConfig;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
/**
|
||||
* Builds /hours messages command variants for custom message configuration.
|
||||
* Allows customization of all player-facing messages and notifications.
|
||||
*/
|
||||
public final class MessagesCommandBuilder {
|
||||
private MessagesCommandBuilder() {}
|
||||
|
||||
/**
|
||||
* Builds the complete /hours messages command node.
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build() {
|
||||
return CommandBuilder.adminLiteral("messages")
|
||||
.then(Commands.literal("access_denied").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.ACCESS_DENIED.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("threshold_denied").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.THRESHOLD_DENIED.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("warn").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.WARN.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("kick").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.KICK.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("force_open").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.FORCE_OPEN.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("force_closed").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.FORCE_CLOSED.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("status_line").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.STATUS_LINE.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("status_open").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.STATUS_OPEN.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("status_closed").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.STATUS_CLOSED.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("countdown").then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String msg = StringArgumentType.getString(ctx, "message");
|
||||
MessagesConfig.COUNTDOWN.set(msg);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mrkayjaydee.playhours.config.*;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds /hours set command variants for general configuration.
|
||||
* Handles timezone, thresholds, warnings, countdown, exemptions, locale, and default periods.
|
||||
*/
|
||||
public final class SetCommandBuilder {
|
||||
private SetCommandBuilder() {}
|
||||
|
||||
/**
|
||||
* Builds the complete /hours set command node.
|
||||
*/
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build() {
|
||||
return CommandBuilder.adminLiteral("set")
|
||||
.then(Commands.literal("timezone").then(Commands.argument("zoneId", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String zone = StringArgumentType.getString(ctx, "zoneId");
|
||||
GeneralConfig.TIMEZONE.set(zone);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("threshold").then(Commands.argument("minutes", IntegerArgumentType.integer(0, 1440))
|
||||
.executes(ctx -> {
|
||||
int m = IntegerArgumentType.getInteger(ctx, "minutes");
|
||||
GeneralConfig.CLOSING_THRESHOLD_MINUTES.set(m);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("deny_threshold_login").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
GeneralConfig.DENY_LOGIN_DURING_THRESHOLD.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("warnings").then(Commands.argument("minutes", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String arg = StringArgumentType.getString(ctx, "minutes");
|
||||
List<Integer> list = TimeRangeValidator.parseMinutesList(arg);
|
||||
GeneralConfig.WARNING_MINUTES.set(list);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("countdown").then(Commands.argument("seconds", IntegerArgumentType.integer(0, 60))
|
||||
.executes(ctx -> {
|
||||
int s = IntegerArgumentType.getInteger(ctx, "seconds");
|
||||
GeneralConfig.COUNTDOWN_SECONDS.set(s);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("exempt_bypass_schedule").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
GeneralConfig.EXEMPT_BYPASS_SCHEDULE.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("exempt_bypass_threshold").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
GeneralConfig.EXEMPT_BYPASS_THRESHOLD.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("kick_exempt").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
GeneralConfig.KICK_EXEMPT.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("message_locale").then(Commands.argument("locale", StringArgumentType.word())
|
||||
.executes(ctx -> {
|
||||
String locale = StringArgumentType.getString(ctx, "locale");
|
||||
GeneralConfig.MESSAGE_LOCALE.set(locale);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("motd_enabled").then(Commands.argument("enabled", BoolArgumentType.bool())
|
||||
.executes(ctx -> {
|
||||
boolean enabled = BoolArgumentType.getBool(ctx, "enabled");
|
||||
GeneralConfig.MOTD_ENABLED.set(enabled);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("default").then(Commands.literal("periods")
|
||||
.then(Commands.literal("add").then(Commands.argument("range", StringArgumentType.greedyString())
|
||||
.executes(ctx -> {
|
||||
String r = StringArgumentType.getString(ctx, "range");
|
||||
TimeRangeValidator.validateRange(r);
|
||||
List<String> list = new ArrayList<>(DefaultsConfig.PERIODS.get());
|
||||
list.add(r);
|
||||
DefaultsConfig.PERIODS.set(list);
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("clear").executes(ctx -> {
|
||||
DefaultsConfig.PERIODS.set(new ArrayList<>());
|
||||
CommandBuilder.saveAndRebuild(ctx.getSource());
|
||||
return 1;
|
||||
}))))
|
||||
.then(DayCommandBuilder.createAllDayNodes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mrkayjaydee.playhours.command;
|
||||
|
||||
/**
|
||||
* Validates time range strings for commands.
|
||||
* Separates validation logic from command execution.
|
||||
*/
|
||||
public final class TimeRangeValidator {
|
||||
private TimeRangeValidator() {}
|
||||
|
||||
/**
|
||||
* Validates a time range string.
|
||||
*
|
||||
* @param range the time range string to validate
|
||||
* @throws IllegalArgumentException if the range is invalid
|
||||
*/
|
||||
public static void validateRange(String range) {
|
||||
com.mrkayjaydee.playhours.core.TimeRangeValidator.validateRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of minute values from a space-separated string.
|
||||
*
|
||||
* @param input the input string
|
||||
* @return a list of integers
|
||||
* @throws NumberFormatException if any value is not a valid integer
|
||||
*/
|
||||
public static java.util.List<Integer> parseMinutesList(String input) {
|
||||
java.util.List<Integer> list = new java.util.ArrayList<>();
|
||||
for (String s : input.split(" ")) {
|
||||
if (!s.isBlank()) {
|
||||
list.add(Integer.parseInt(s.trim()));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import com.mrkayjaydee.playhours.PlayHoursMod;
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.event.config.ModConfigEvent;
|
||||
|
||||
/**
|
||||
* Handles configuration loading and reloading events.
|
||||
* Separates config event handling from config structure definition.
|
||||
*/
|
||||
public final class ConfigEventHandler {
|
||||
private ConfigEventHandler() {}
|
||||
|
||||
private static ModConfig serverConfig;
|
||||
private static volatile boolean ready = false;
|
||||
|
||||
public static boolean isReady() {
|
||||
return ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles config loading and reloading events.
|
||||
* Rebuilds schedule cache and reloads messages when config changes.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onConfigEvent(final ModConfigEvent event) {
|
||||
ModConfig cfg = event.getConfig();
|
||||
if (cfg.getType() == ModConfig.Type.COMMON && cfg.getModId().equals(PlayHoursMod.MODID)) {
|
||||
serverConfig = cfg;
|
||||
ready = true;
|
||||
PlayHoursMod.LOGGER.info("PlayHours SERVER config bound: {}", cfg.getFileName());
|
||||
}
|
||||
PlayHoursMod.LOGGER.info("PlayHours config (re)loaded: {}", cfg.getFileName());
|
||||
|
||||
// Rebuild caches and reload messages
|
||||
try {
|
||||
ScheduleService.get().rebuildFromConfig();
|
||||
PlayHoursMod.LOGGER.info("PlayHours schedule rebuilt from config");
|
||||
} catch (Throwable t) {
|
||||
PlayHoursMod.LOGGER.error("Failed to rebuild schedule from config", t);
|
||||
}
|
||||
|
||||
try {
|
||||
Messages.reloadFromConfig();
|
||||
PlayHoursMod.LOGGER.info("PlayHours messages reloaded (locale={})", Messages.getJavaLocale());
|
||||
} catch (Throwable t) {
|
||||
PlayHoursMod.LOGGER.error("Failed to reload messages from config", t);
|
||||
}
|
||||
|
||||
// Persist generated/default config to disk on first load and every reload
|
||||
try {
|
||||
save();
|
||||
PlayHoursMod.LOGGER.info("PlayHours config saved to disk");
|
||||
} catch (Throwable t) {
|
||||
PlayHoursMod.LOGGER.error("Failed to save PlayHours config after load/reload", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void save() {
|
||||
try {
|
||||
if (serverConfig != null) {
|
||||
serverConfig.save();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
PlayHoursMod.LOGGER.warn("Failed to save PlayHours config: {}", t.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the config from disk and rebuilds all caches.
|
||||
* This is used by the /hours reload command to pick up manual config file edits.
|
||||
*/
|
||||
public static void reloadFromDisk() {
|
||||
try {
|
||||
if (serverConfig != null) {
|
||||
PlayHoursMod.LOGGER.info("Reloading config from disk: {}", serverConfig.getFileName());
|
||||
|
||||
// Get the underlying file config and reload it from disk
|
||||
com.electronwill.nightconfig.core.file.FileConfig fileConfig =
|
||||
(com.electronwill.nightconfig.core.file.FileConfig) serverConfig.getConfigData();
|
||||
|
||||
// Reload the file from disk
|
||||
fileConfig.load();
|
||||
PlayHoursMod.LOGGER.info("Config file reloaded from disk");
|
||||
|
||||
// Force ForgeConfigSpec to re-read the values from the file config
|
||||
// This is necessary because ForgeConfigSpec caches values
|
||||
ServerConfig.SPEC.afterReload();
|
||||
PlayHoursMod.LOGGER.info("Config spec refreshed");
|
||||
|
||||
// Rebuild schedule from the reloaded config
|
||||
ScheduleService.get().rebuildFromConfig();
|
||||
PlayHoursMod.LOGGER.info("Schedule rebuilt from reloaded config");
|
||||
|
||||
// Reload messages with new locale/overrides
|
||||
Messages.reloadFromConfig();
|
||||
PlayHoursMod.LOGGER.info("Messages reloaded (locale={})", Messages.getJavaLocale());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
PlayHoursMod.LOGGER.error("Failed to reload config from disk", t);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Day-specific schedule configuration for PlayHours.
|
||||
* Contains opening periods for each day of the week.
|
||||
*/
|
||||
public final class DaysConfig {
|
||||
private DaysConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> MON;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> TUE;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> WED;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> THU;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> FRI;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> SAT;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> SUN;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("days");
|
||||
MON = builder.comment("Monday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("monday", new ArrayList<>(), o -> o instanceof String);
|
||||
TUE = builder.comment("Tuesday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("tuesday", new ArrayList<>(), o -> o instanceof String);
|
||||
WED = builder.comment("Wednesday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("wednesday", new ArrayList<>(), o -> o instanceof String);
|
||||
THU = builder.comment("Thursday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("thursday", new ArrayList<>(), o -> o instanceof String);
|
||||
FRI = builder.comment("Friday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("friday", new ArrayList<>(), o -> o instanceof String);
|
||||
SAT = builder.comment("Saturday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("saturday", new ArrayList<>(), o -> o instanceof String);
|
||||
SUN = builder.comment("Sunday-specific opening periods. Format: 'hh:mm AM-hh:mm PM'. Empty => use defaults.")
|
||||
.defineListAllowEmpty("sunday", new ArrayList<>(), o -> o instanceof String);
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Default schedule configuration for PlayHours.
|
||||
* Contains default daily opening periods used for any day without a specific override.
|
||||
*/
|
||||
public final class DefaultsConfig {
|
||||
private DefaultsConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> PERIODS;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("defaults");
|
||||
PERIODS = builder.comment(
|
||||
"Default daily opening periods used for any day without a specific override.",
|
||||
"Format: 12-hour time ranges 'hh:mm AM-hh:mm PM'.",
|
||||
"Examples: '09:00 AM-12:00 PM', '10:00 PM-02:00 AM' (spans midnight).",
|
||||
"Provide zero or more ranges.")
|
||||
.defineListAllowEmpty("periods", List.of("09:00 AM-06:00 PM"), o -> o instanceof String);
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Exception configuration for PlayHours.
|
||||
* Contains date-specific overrides for open and closed dates.
|
||||
*/
|
||||
public final class ExceptionsConfig {
|
||||
private ExceptionsConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> OPEN_DATES;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> CLOSED_DATES;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("exceptions");
|
||||
OPEN_DATES = builder.comment(
|
||||
"Force OPEN overrides.",
|
||||
"Two accepted formats:",
|
||||
"- Full day: 'YYYY-MM-DD' (opens for the entire day)",
|
||||
"- Window: 'YYYY-MM-DD hh:mm AM-hh:mm PM' (opens only for that range).",
|
||||
"Examples: '2025-12-25', '2025-12-31 07:00 PM-11:30 PM'.")
|
||||
.defineListAllowEmpty("open_dates", new ArrayList<>(), o -> o instanceof String);
|
||||
CLOSED_DATES = builder.comment(
|
||||
"Force CLOSED overrides (take priority over normal schedule).",
|
||||
"Two accepted formats:",
|
||||
"- Full day: 'YYYY-MM-DD'",
|
||||
"- Window: 'YYYY-MM-DD hh:mm AM-hh:mm PM'.")
|
||||
.defineListAllowEmpty("closed_dates", new ArrayList<>(), o -> o instanceof String);
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* General configuration settings for PlayHours.
|
||||
* Contains timezone, force mode, thresholds, warnings, and exemption settings.
|
||||
*/
|
||||
public final class GeneralConfig {
|
||||
private GeneralConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.ConfigValue<String> TIMEZONE;
|
||||
public static ForgeConfigSpec.ConfigValue<String> FORCE_MODE;
|
||||
public static ForgeConfigSpec.IntValue CLOSING_THRESHOLD_MINUTES;
|
||||
public static ForgeConfigSpec.BooleanValue DENY_LOGIN_DURING_THRESHOLD;
|
||||
public static ForgeConfigSpec.BooleanValue KICK_EXEMPT;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends Integer>> WARNING_MINUTES;
|
||||
public static ForgeConfigSpec.IntValue COUNTDOWN_SECONDS;
|
||||
public static ForgeConfigSpec.BooleanValue EXEMPT_BYPASS_SCHEDULE;
|
||||
public static ForgeConfigSpec.BooleanValue EXEMPT_BYPASS_THRESHOLD;
|
||||
public static ForgeConfigSpec.ConfigValue<String> MESSAGE_LOCALE;
|
||||
public static ForgeConfigSpec.BooleanValue MOTD_ENABLED;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("general");
|
||||
TIMEZONE = builder.comment(
|
||||
"Server timezone used to evaluate schedules.",
|
||||
"Must be a valid IANA ZoneId (examples: America/New_York, Europe/Paris, Asia/Tokyo).",
|
||||
"All time ranges and exceptions are interpreted in this timezone.")
|
||||
.define("timezone", "Europe/Paris");
|
||||
FORCE_MODE = builder.comment(
|
||||
"Operating mode override:",
|
||||
"- NORMAL: obey schedule and exceptions",
|
||||
"- FORCE_OPEN: always allow (blacklist still denies; exempt always allows)",
|
||||
"- FORCE_CLOSED: always deny (exempt still allows unless you kick them at close)")
|
||||
.define("force_mode", "NORMAL");
|
||||
CLOSING_THRESHOLD_MINUTES = builder.comment(
|
||||
"Number of minutes before the next scheduled close considered the 'closing threshold'.",
|
||||
"Used together with deny_login_during_threshold to optionally deny NEW logins during the threshold.",
|
||||
"Range: 0..1440 minutes.")
|
||||
.defineInRange("closing_threshold_minutes", 0, 0, 24 * 60);
|
||||
DENY_LOGIN_DURING_THRESHOLD = builder.comment(
|
||||
"If true, NEW logins during the closing threshold are denied with the threshold message.",
|
||||
"Existing players remain until the actual close time.")
|
||||
.define("deny_login_during_threshold", true);
|
||||
KICK_EXEMPT = builder.comment(
|
||||
"If true, even exempt players are kicked at close time.",
|
||||
"If false, exempt players are never kicked by the mod.")
|
||||
.define("kick_exempt", false);
|
||||
WARNING_MINUTES = builder.comment(
|
||||
"Minutes-before-close at which a broadcast warning is sent.",
|
||||
"Provide integers in minutes; example: [15, 10, 5, 1].",
|
||||
"The list is processed every second; duplicates are ignored for a given minute.")
|
||||
.defineList("warning_minutes", Arrays.asList(15, 10, 5, 1), o -> o instanceof Integer i && i >= 0 && i < 24 * 60);
|
||||
COUNTDOWN_SECONDS = builder.comment(
|
||||
"Number of seconds before closing to start countdown messages.",
|
||||
"Set to 0 to disable countdown. Range: 0-60 seconds.",
|
||||
"Example: 5 = sends 'Closing in 5s', 'Closing in 4s', etc.")
|
||||
.defineInRange("countdown_seconds", 5, 0, 60);
|
||||
EXEMPT_BYPASS_SCHEDULE = builder.comment(
|
||||
"If true, exempt players can join even when server is closed.",
|
||||
"Exempt players: ops level 2+, playhours.exempt, playhours.admin permissions.")
|
||||
.define("exempt_bypass_schedule", true);
|
||||
EXEMPT_BYPASS_THRESHOLD = builder.comment(
|
||||
"If true, exempt players can join during closing threshold.",
|
||||
"Only applies when deny_login_during_threshold is true.")
|
||||
.define("exempt_bypass_threshold", true);
|
||||
MESSAGE_LOCALE = builder.comment(
|
||||
"Locale (language) for messages (resource bundle name).",
|
||||
"Examples: en_us, fr_fr. If blank or missing, defaults to en_us.")
|
||||
.define("message_locale", "en_us");
|
||||
MOTD_ENABLED = builder.comment(
|
||||
"Enable MOTD (Message of the Day) modification to show server schedule.",
|
||||
"When enabled, server list will display opening/closing times dynamically.")
|
||||
.define("motd_enabled", true);
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Whitelist and blacklist configuration for PlayHours.
|
||||
* Contains player name lists for access control.
|
||||
*/
|
||||
public final class ListsConfig {
|
||||
private ListsConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.BooleanValue WHITELIST_ENABLED;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> WHITELIST;
|
||||
public static ForgeConfigSpec.BooleanValue BLACKLIST_ENABLED;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> BLACKLIST;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("lists");
|
||||
WHITELIST_ENABLED = builder.comment(
|
||||
"If true, names in 'whitelist' are always allowed (unless FORCE_CLOSED).",
|
||||
"Names are matched case-insensitively.")
|
||||
.define("whitelist_enabled", false);
|
||||
WHITELIST = builder.comment(
|
||||
"Lowercase player names to always allow when whitelist_enabled=true.")
|
||||
.defineListAllowEmpty("whitelist", new ArrayList<>(), o -> o instanceof String);
|
||||
BLACKLIST_ENABLED = builder.comment(
|
||||
"If true, names in 'blacklist' are always denied (even if schedule is open).",
|
||||
"Exempt permission still allows.")
|
||||
.define("blacklist_enabled", false);
|
||||
BLACKLIST = builder.comment(
|
||||
"Lowercase player names to always deny when blacklist_enabled=true.")
|
||||
.defineListAllowEmpty("blacklist", new ArrayList<>(), o -> o instanceof String);
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
158
src/main/java/com/mrkayjaydee/playhours/config/MOTDConfig.java
Normal file
158
src/main/java/com/mrkayjaydee/playhours/config/MOTDConfig.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MOTD (Message of the Day) configuration for PlayHours.
|
||||
* Controls how server schedule information is displayed in the server list.
|
||||
*/
|
||||
public final class MOTDConfig {
|
||||
private MOTDConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_STATUS;
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_NEXT_OPEN;
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_NEXT_CLOSE;
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_SCHEDULE_TIMES;
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_ON_SECOND_LINE;
|
||||
public static ForgeConfigSpec.ConfigValue<String> SEPARATOR;
|
||||
public static ForgeConfigSpec.ConfigValue<String> CUSTOM_FORMAT;
|
||||
public static ForgeConfigSpec.BooleanValue USE_COLORS;
|
||||
public static ForgeConfigSpec.ConfigValue<String> OPEN_COLOR;
|
||||
public static ForgeConfigSpec.ConfigValue<String> CLOSED_COLOR;
|
||||
public static ForgeConfigSpec.ConfigValue<String> INFO_COLOR;
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_FORCE_MODE;
|
||||
public static ForgeConfigSpec.BooleanValue SHOW_COUNTDOWN;
|
||||
public static ForgeConfigSpec.IntValue COUNTDOWN_THRESHOLD_MINUTES;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> CUSTOM_LINES;
|
||||
public static ForgeConfigSpec.IntValue UPDATE_DELAY_SECONDS;
|
||||
public static ForgeConfigSpec.BooleanValue ROTATION_ENABLED;
|
||||
public static ForgeConfigSpec.IntValue ROTATION_INTERVAL_SECONDS;
|
||||
public static ForgeConfigSpec.ConfigValue<List<? extends String>> ROTATION_TEMPLATES;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("motd");
|
||||
builder.comment(
|
||||
"MOTD (Message of the Day) settings for server list display.",
|
||||
"Configure what schedule information appears when players view your server in the multiplayer list."
|
||||
);
|
||||
|
||||
SHOW_STATUS = builder.comment(
|
||||
"Show server open/closed status in MOTD.",
|
||||
"Displays whether the server is currently open or closed.")
|
||||
.define("show_status", true);
|
||||
|
||||
SHOW_NEXT_OPEN = builder.comment(
|
||||
"Show next opening time when server is closed.",
|
||||
"Displays the day and time when the server will next open.")
|
||||
.define("show_next_open", true);
|
||||
|
||||
SHOW_NEXT_CLOSE = builder.comment(
|
||||
"Show next closing time when server is open.",
|
||||
"Displays the time when the server will close.")
|
||||
.define("show_next_close", true);
|
||||
|
||||
SHOW_SCHEDULE_TIMES = builder.comment(
|
||||
"Show detailed schedule times.",
|
||||
"Displays a summary of server operating hours.")
|
||||
.define("show_schedule_times", false);
|
||||
|
||||
SHOW_ON_SECOND_LINE = builder.comment(
|
||||
"Place schedule information on the second MOTD line.",
|
||||
"If false, information is appended to the first line.",
|
||||
"Note: The second line may be limited in character count.")
|
||||
.define("show_on_second_line", true);
|
||||
|
||||
SEPARATOR = builder.comment(
|
||||
"Separator between MOTD elements.",
|
||||
"Used to separate different pieces of information.")
|
||||
.define("separator", " | ");
|
||||
|
||||
CUSTOM_FORMAT = builder.comment(
|
||||
"Custom MOTD format string (leave blank to use default).",
|
||||
"Placeholders: %status%, %nextopen%, %nextclose%, %openday%, %opentime%, %closetime%",
|
||||
"Example: 'Status: %status% - Next open: %openday% at %opentime%'",
|
||||
"If blank, the mod will auto-generate MOTD based on other settings.",
|
||||
"NOTE: Minecraft MOTD limit is 2 lines × ~59 chars per line. Longer text will be truncated.")
|
||||
.define("custom_format", "");
|
||||
|
||||
USE_COLORS = builder.comment(
|
||||
"Enable colored MOTD messages.",
|
||||
"Uses Minecraft color codes for status messages.")
|
||||
.define("use_colors", true);
|
||||
|
||||
OPEN_COLOR = builder.comment(
|
||||
"Color code for 'open' status (Minecraft format codes).",
|
||||
"Examples: green, dark_green, aqua. Or use § codes like §a.",
|
||||
"See: https://minecraft.fandom.com/wiki/Formatting_codes")
|
||||
.define("open_color", "green");
|
||||
|
||||
CLOSED_COLOR = builder.comment(
|
||||
"Color code for 'closed' status.",
|
||||
"Examples: red, dark_red, gold.")
|
||||
.define("closed_color", "red");
|
||||
|
||||
INFO_COLOR = builder.comment(
|
||||
"Color code for informational text.",
|
||||
"Examples: gray, yellow, white.")
|
||||
.define("info_color", "gray");
|
||||
|
||||
SHOW_FORCE_MODE = builder.comment(
|
||||
"Show force mode status (FORCE_OPEN/FORCE_CLOSED) in MOTD.",
|
||||
"Useful for admins to see at a glance if schedule is overridden.")
|
||||
.define("show_force_mode", true);
|
||||
|
||||
SHOW_COUNTDOWN = builder.comment(
|
||||
"Show countdown timer when server is closing soon.",
|
||||
"Displays time remaining until close.")
|
||||
.define("show_countdown", true);
|
||||
|
||||
COUNTDOWN_THRESHOLD_MINUTES = builder.comment(
|
||||
"Minutes before close to start showing countdown in MOTD.",
|
||||
"Range: 0-1440 minutes. Set to 0 to disable.",
|
||||
"Example: 30 = show 'Closing in 30 minutes' when close is within 30 min.")
|
||||
.defineInRange("countdown_threshold_minutes", 30, 0, 24 * 60);
|
||||
|
||||
CUSTOM_LINES = builder.comment(
|
||||
"Custom static text lines to add to MOTD (advanced).",
|
||||
"Each entry is a separate line. Leave empty for automatic formatting.",
|
||||
"Supports placeholders: %status%, %nextopen%, %nextclose%, %openday%, %opentime%, %closetime%",
|
||||
"Example: ['Welcome!', 'Status: %status%', 'Next open: %openday%']",
|
||||
"MINECRAFT LIMITS: Maximum 2 lines displayed. Each line ~59 characters max.",
|
||||
"Longer lines will be truncated by Minecraft.")
|
||||
.defineList("custom_lines", Arrays.asList(), o -> o instanceof String);
|
||||
|
||||
UPDATE_DELAY_SECONDS = builder.comment(
|
||||
"Delay in seconds between MOTD updates.",
|
||||
"Lower values update more frequently but use more server resources.",
|
||||
"Higher values update less frequently but may show stale information.",
|
||||
"Range: 1-600 seconds. Recommended: 30-60 seconds.")
|
||||
.defineInRange("update_delay_seconds", 60, 1, 600);
|
||||
|
||||
ROTATION_ENABLED = builder.comment(
|
||||
"Enable MOTD rotation to cycle through multiple MOTDs.",
|
||||
"When enabled, the MOTD will switch between different templates at regular intervals.",
|
||||
"Each template should contain 1-2 lines (max ~59 chars per line).")
|
||||
.define("rotation_enabled", false);
|
||||
|
||||
ROTATION_INTERVAL_SECONDS = builder.comment(
|
||||
"Seconds between MOTD rotations.",
|
||||
"Each rotation_interval_seconds, the MOTD will switch to the next template.",
|
||||
"Range: 1-3600 seconds. Recommended: 10-30 seconds for variety.")
|
||||
.defineInRange("rotation_interval_seconds", 15, 1, 3600);
|
||||
|
||||
ROTATION_TEMPLATES = builder.comment(
|
||||
"List of MOTD templates to rotate through.",
|
||||
"Each entry is a complete MOTD (1-2 lines, separate with \\n).",
|
||||
"Supports placeholders: %status%, %nextopen%, %nextclose%, %openday%, %opentime%, %closetime%",
|
||||
"Example: ['Status: %status%', 'Closes: %closetime%\\nNext: %nextopen%']",
|
||||
"Leave empty or set rotation_enabled=false to disable rotation.",
|
||||
"MINECRAFT LIMITS: Maximum 2 lines per template, ~59 characters per line.")
|
||||
.defineList("rotation_templates", Arrays.asList(), o -> o instanceof String);
|
||||
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
/**
|
||||
* Message configuration for PlayHours.
|
||||
* Contains custom message overrides for various events.
|
||||
*/
|
||||
public final class MessagesConfig {
|
||||
private MessagesConfig() {}
|
||||
|
||||
public static ForgeConfigSpec.ConfigValue<String> ACCESS_DENIED;
|
||||
public static ForgeConfigSpec.ConfigValue<String> THRESHOLD_DENIED;
|
||||
public static ForgeConfigSpec.ConfigValue<String> WARN;
|
||||
public static ForgeConfigSpec.ConfigValue<String> KICK;
|
||||
public static ForgeConfigSpec.ConfigValue<String> FORCE_OPEN;
|
||||
public static ForgeConfigSpec.ConfigValue<String> FORCE_CLOSED;
|
||||
public static ForgeConfigSpec.ConfigValue<String> STATUS_LINE;
|
||||
public static ForgeConfigSpec.ConfigValue<String> STATUS_OPEN;
|
||||
public static ForgeConfigSpec.ConfigValue<String> STATUS_CLOSED;
|
||||
public static ForgeConfigSpec.ConfigValue<String> COUNTDOWN;
|
||||
public static ForgeConfigSpec.ConfigValue<String> CONFIG_NOT_READY;
|
||||
public static ForgeConfigSpec.ConfigValue<String> UNEXPECTED_ERROR;
|
||||
public static ForgeConfigSpec.ConfigValue<String> CONFIG_RELOADED;
|
||||
public static ForgeConfigSpec.ConfigValue<String> INVALID_TIME_RANGE;
|
||||
public static ForgeConfigSpec.ConfigValue<String> FAILED_CLEAR_DEFAULT_PERIODS;
|
||||
public static ForgeConfigSpec.ConfigValue<String> SETTINGS_UPDATED;
|
||||
|
||||
static void init(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("messages");
|
||||
ACCESS_DENIED = builder.comment(
|
||||
"Custom text for access denied on login (server closed).",
|
||||
"Placeholders: %openday%, %opentime%.")
|
||||
.define("access_denied", "");
|
||||
THRESHOLD_DENIED = builder.comment(
|
||||
"Custom text when deny_login_during_threshold denies new logins.",
|
||||
"Placeholders: %openday%, %opentime%.")
|
||||
.define("threshold_denied", "");
|
||||
WARN = builder.comment(
|
||||
"Broadcast warning format when approaching close.",
|
||||
"Placeholders: %minutes%, %s% (plural suffix), %closetime%.")
|
||||
.define("warn", "");
|
||||
KICK = builder.comment(
|
||||
"Kick message at close time.",
|
||||
"Placeholders: %openday%, %opentime%.")
|
||||
.define("kick", "");
|
||||
FORCE_OPEN = builder.comment("Notification when force mode is set to FORCE_OPEN.")
|
||||
.define("force_open", "");
|
||||
FORCE_CLOSED = builder.comment("Notification when force mode is set to FORCE_CLOSED.")
|
||||
.define("force_closed", "");
|
||||
STATUS_LINE = builder.comment(
|
||||
"Text used by /hours status.",
|
||||
"Placeholders: %mode%, %isopen%, %closetime%, %openday%, %opentime%.")
|
||||
.define("status_line", "");
|
||||
STATUS_OPEN = builder.comment(
|
||||
"Text displayed when server is open.",
|
||||
"Used in status messages. No placeholders.")
|
||||
.define("status_open", "");
|
||||
STATUS_CLOSED = builder.comment(
|
||||
"Text displayed when server is closed.",
|
||||
"Used in status messages. No placeholders.")
|
||||
.define("status_closed", "");
|
||||
COUNTDOWN = builder.comment(
|
||||
"Countdown message sent every second before closing.",
|
||||
"Placeholders: %seconds%")
|
||||
.define("countdown", "");
|
||||
CONFIG_NOT_READY = builder.comment(
|
||||
"Error message when config is not ready.",
|
||||
"No placeholders.")
|
||||
.define("config_not_ready", "");
|
||||
UNEXPECTED_ERROR = builder.comment(
|
||||
"Generic error message for unexpected errors.",
|
||||
"No placeholders.")
|
||||
.define("unexpected_error", "");
|
||||
CONFIG_RELOADED = builder.comment(
|
||||
"Success message when config is reloaded.",
|
||||
"No placeholders.")
|
||||
.define("config_reloaded", "");
|
||||
INVALID_TIME_RANGE = builder.comment(
|
||||
"Error message for invalid time range format.",
|
||||
"No placeholders.")
|
||||
.define("invalid_time_range", "");
|
||||
FAILED_CLEAR_DEFAULT_PERIODS = builder.comment(
|
||||
"Error message when clearing default periods fails.",
|
||||
"No placeholders.")
|
||||
.define("failed_clear_default_periods", "");
|
||||
SETTINGS_UPDATED = builder.comment(
|
||||
"Success message when settings are updated.",
|
||||
"No placeholders.")
|
||||
.define("settings_updated", "");
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.mrkayjaydee.playhours.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
/**
|
||||
* Main Forge SERVER configuration for PlayHours.
|
||||
* Delegates to individual config classes for better organization.
|
||||
*/
|
||||
public final class ServerConfig {
|
||||
private ServerConfig() {}
|
||||
|
||||
private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
|
||||
|
||||
public static final ForgeConfigSpec SPEC;
|
||||
static {
|
||||
// Initialize all config sections in order
|
||||
GeneralConfig.init(BUILDER);
|
||||
DefaultsConfig.init(BUILDER);
|
||||
DaysConfig.init(BUILDER);
|
||||
ExceptionsConfig.init(BUILDER);
|
||||
ListsConfig.init(BUILDER);
|
||||
MessagesConfig.init(BUILDER);
|
||||
MOTDConfig.init(BUILDER);
|
||||
|
||||
SPEC = BUILDER.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
import com.mrkayjaydee.playhours.PlayHoursMod;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Handles open and closed date exceptions.
|
||||
* Separates exception logic from main schedule logic.
|
||||
*/
|
||||
public final class ExceptionHandler {
|
||||
private ExceptionHandler() {}
|
||||
|
||||
public static ExceptionHandler create() {
|
||||
return new ExceptionHandler();
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private volatile List<String> openExceptions = List.of();
|
||||
private volatile List<String> closedExceptions = List.of();
|
||||
|
||||
/**
|
||||
* Updates the exception lists.
|
||||
*/
|
||||
public void updateExceptions(List<String> openExceptions, List<String> closedExceptions) {
|
||||
this.openExceptions = List.copyOf(Objects.requireNonNull(openExceptions, "openExceptions"));
|
||||
this.closedExceptions = List.copyOf(Objects.requireNonNull(closedExceptions, "closedExceptions"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given time falls within a closed exception.
|
||||
*
|
||||
* @param dateTime the time to check
|
||||
* @return true if the time is in a closed exception
|
||||
*/
|
||||
public boolean isClosedException(ZonedDateTime dateTime) {
|
||||
String date = dateTime.format(DATE_FORMAT);
|
||||
|
||||
// Full-day closed if present
|
||||
if (closedExceptions.stream().anyMatch(s -> s.trim().equals(date))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Timed closed entries
|
||||
for (String exception : closedExceptions) {
|
||||
if (exception.contains(" ")) {
|
||||
String[] parts = exception.split(" ", 2);
|
||||
if (!parts[0].equals(date)) continue;
|
||||
|
||||
try {
|
||||
TimeRange range = TimeRange.parse(parts[1]);
|
||||
if (range.contains(dateTime.toLocalTime())) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
PlayHoursMod.LOGGER.debug("Failed to parse closed exception '{}': {}", exception, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given time falls within an open exception.
|
||||
*
|
||||
* @param dateTime the time to check
|
||||
* @return true if the time is in an open exception
|
||||
*/
|
||||
public boolean isOpenException(ZonedDateTime dateTime) {
|
||||
String date = dateTime.format(DATE_FORMAT);
|
||||
|
||||
for (String exception : openExceptions) {
|
||||
if (exception.equals(date)) return true; // whole day open
|
||||
|
||||
if (exception.contains(" ")) {
|
||||
String[] parts = exception.split(" ", 2);
|
||||
if (!parts[0].equals(date)) continue;
|
||||
|
||||
try {
|
||||
TimeRange range = TimeRange.parse(parts[1]);
|
||||
if (range.contains(dateTime.toLocalTime())) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
PlayHoursMod.LOGGER.debug("Failed to parse open exception '{}': {}", exception, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/mrkayjaydee/playhours/core/ForceMode.java
Normal file
22
src/main/java/com/mrkayjaydee/playhours/core/ForceMode.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
/**
|
||||
* Defines the operational mode of the server.
|
||||
* Controls whether the server schedule is enforced or overridden.
|
||||
*/
|
||||
public enum ForceMode {
|
||||
/**
|
||||
* Normal operation - obey schedule and exceptions.
|
||||
*/
|
||||
NORMAL,
|
||||
|
||||
/**
|
||||
* Force open - always allow access (blacklist still denies; exempt always allows).
|
||||
*/
|
||||
FORCE_OPEN,
|
||||
|
||||
/**
|
||||
* Force closed - always deny access (exempt still allows unless kick_exempt is true).
|
||||
*/
|
||||
FORCE_CLOSED
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import com.mrkayjaydee.playhours.text.MessageKeys;
|
||||
|
||||
/**
|
||||
* Utility for formatting ForceMode values into user-friendly display strings.
|
||||
* Converts enum values to readable text suitable for player-facing messages.
|
||||
* Strings are loaded from language files via the Messages system.
|
||||
*/
|
||||
public final class ForceModeFormatter {
|
||||
private ForceModeFormatter() {}
|
||||
|
||||
/**
|
||||
* Formats a ForceMode enum into a user-friendly display string.
|
||||
* Uses localized strings from language files.
|
||||
*
|
||||
* @param mode the force mode to format
|
||||
* @return a user-friendly localized string representation
|
||||
*/
|
||||
public static String format(ForceMode mode) {
|
||||
return switch (mode) {
|
||||
case NORMAL -> Messages.get(MessageKeys.MODE_NORMAL);
|
||||
case FORCE_OPEN -> Messages.get(MessageKeys.MODE_FORCE_OPEN);
|
||||
case FORCE_CLOSED -> Messages.get(MessageKeys.MODE_FORCE_CLOSED);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a ForceMode enum into a shortened, compact display string.
|
||||
* Uses localized strings from language files.
|
||||
*
|
||||
* @param mode the force mode to format
|
||||
* @return a compact localized string representation
|
||||
*/
|
||||
public static String formatShort(ForceMode mode) {
|
||||
return switch (mode) {
|
||||
case NORMAL -> Messages.get(MessageKeys.MODE_NORMAL);
|
||||
case FORCE_OPEN -> Messages.get(MessageKeys.MOTD_FORCE_OPEN);
|
||||
case FORCE_CLOSED -> Messages.get(MessageKeys.MOTD_FORCE_CLOSED);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Handles player access checking based on whitelist, blacklist, and force mode.
|
||||
* Separates player-specific access logic from schedule logic.
|
||||
*/
|
||||
public final class PlayerAccessChecker {
|
||||
private PlayerAccessChecker() {}
|
||||
|
||||
public static PlayerAccessChecker create() {
|
||||
return new PlayerAccessChecker();
|
||||
}
|
||||
|
||||
private volatile ForceMode forceMode = ForceMode.NORMAL;
|
||||
private volatile boolean whitelistEnabled = false;
|
||||
private volatile boolean blacklistEnabled = false;
|
||||
private volatile Set<String> whitelist = Set.of();
|
||||
private volatile Set<String> blacklist = Set.of();
|
||||
|
||||
/**
|
||||
* Updates the access checker configuration.
|
||||
*/
|
||||
public void updateConfig(ForceMode forceMode, boolean whitelistEnabled, boolean blacklistEnabled,
|
||||
Set<String> whitelist, Set<String> blacklist) {
|
||||
this.forceMode = Objects.requireNonNull(forceMode, "forceMode");
|
||||
this.whitelistEnabled = whitelistEnabled;
|
||||
this.blacklistEnabled = blacklistEnabled;
|
||||
this.whitelist = Set.copyOf(Objects.requireNonNull(whitelist, "whitelist"));
|
||||
this.blacklist = Set.copyOf(Objects.requireNonNull(blacklist, "blacklist"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player is allowed access based on force mode and lists.
|
||||
* Does not consider schedule - that should be checked separately.
|
||||
*
|
||||
* @param playerName the player name (case-insensitive)
|
||||
* @param isExempt whether the player has exempt permission
|
||||
* @return true if the player is allowed by force mode and lists
|
||||
*/
|
||||
public boolean isPlayerAllowed(String playerName, boolean isExempt) {
|
||||
if (isExempt) return true;
|
||||
if (forceMode == ForceMode.FORCE_OPEN) return true;
|
||||
if (forceMode == ForceMode.FORCE_CLOSED) return false;
|
||||
|
||||
String key = playerName == null ? "" : playerName.toLowerCase(Locale.ROOT);
|
||||
if (blacklistEnabled && blacklist.contains(key)) return false;
|
||||
if (whitelistEnabled && whitelist.contains(key)) return true;
|
||||
|
||||
return true; // Default allow if not in any list
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current force mode.
|
||||
*/
|
||||
public ForceMode getForceMode() {
|
||||
return forceMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handles schedule calculations for next open/close times.
|
||||
* Separates schedule calculation logic from other concerns.
|
||||
*/
|
||||
public final class ScheduleCalculator {
|
||||
private ScheduleCalculator() {}
|
||||
|
||||
public static ScheduleCalculator create() {
|
||||
return new ScheduleCalculator();
|
||||
}
|
||||
|
||||
private static final int DEFAULT_SEARCH_DAYS = 14;
|
||||
|
||||
private volatile Map<DayOfWeek, List<TimeRange>> dayToRanges = new EnumMap<>(DayOfWeek.class);
|
||||
private volatile ZoneId zoneId = ZoneId.of("Europe/Paris");
|
||||
|
||||
/**
|
||||
* Updates the schedule configuration.
|
||||
*/
|
||||
public void updateSchedule(Map<DayOfWeek, List<TimeRange>> dayToRanges, ZoneId zoneId) {
|
||||
this.dayToRanges = new EnumMap<>(Objects.requireNonNull(dayToRanges, "dayToRanges"));
|
||||
this.zoneId = Objects.requireNonNull(zoneId, "zoneId");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server schedule is open at the specified time.
|
||||
* Does not consider exceptions or force modes - only the base schedule.
|
||||
*
|
||||
* @param now the time to check
|
||||
* @return true if the schedule is open at this time
|
||||
*/
|
||||
public boolean isScheduleOpen(ZonedDateTime now) {
|
||||
DayOfWeek dow = now.getDayOfWeek();
|
||||
LocalTime time = now.toLocalTime();
|
||||
|
||||
// Check today's ranges
|
||||
List<TimeRange> today = dayToRanges.getOrDefault(dow, List.of());
|
||||
for (TimeRange range : today) {
|
||||
if (range.contains(time)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check yesterday's midnight-spanning ranges
|
||||
List<TimeRange> yesterday = dayToRanges.getOrDefault(dow.minus(1), List.of());
|
||||
for (TimeRange range : yesterday) {
|
||||
if (range.spansMidnight() && time.isBefore(range.getEnd().plusSeconds(1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next closing time from the current moment.
|
||||
* Only returns a value if the server is currently open.
|
||||
*
|
||||
* @param now the current time
|
||||
* @return the next closing time, or empty if server is not currently open
|
||||
*/
|
||||
public Optional<ZonedDateTime> nextClose(ZonedDateTime now) {
|
||||
if (!isScheduleOpen(now)) return Optional.empty();
|
||||
|
||||
LocalTime time = now.toLocalTime();
|
||||
DayOfWeek dow = now.getDayOfWeek();
|
||||
|
||||
// Collect all relevant ranges
|
||||
List<TimeRange> candidates = List.copyOf(dayToRanges.getOrDefault(dow, List.of()));
|
||||
List<TimeRange> yesterday = dayToRanges.getOrDefault(dow.minus(1), List.of());
|
||||
for (TimeRange range : yesterday) {
|
||||
if (range.spansMidnight()) {
|
||||
candidates.add(range);
|
||||
}
|
||||
}
|
||||
|
||||
for (TimeRange range : candidates) {
|
||||
if (range.contains(time)) {
|
||||
LocalDateTime endDt = LocalDateTime.of(now.toLocalDate(), range.getEnd());
|
||||
|
||||
if (range.spansMidnight() && time.isBefore(range.getEnd().plusSeconds(1))) {
|
||||
// We are in the after-midnight part
|
||||
} else if (range.spansMidnight() && !time.isAfter(range.getEnd())) {
|
||||
endDt = LocalDateTime.of(now.toLocalDate().plusDays(1), range.getEnd());
|
||||
}
|
||||
|
||||
if (!range.spansMidnight() && time.isAfter(range.getEnd())) continue;
|
||||
|
||||
ZonedDateTime z = endDt.atZone(zoneId);
|
||||
if (!z.isBefore(now)) return Optional.of(z);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next opening time from the current moment.
|
||||
* Searches up to two weeks ahead for the next scheduled open period.
|
||||
*
|
||||
* @param now the current time
|
||||
* @return the next opening time, or empty if none found in the next 14 days
|
||||
*/
|
||||
public Optional<ZonedDateTime> nextOpen(ZonedDateTime now) {
|
||||
if (isScheduleOpen(now)) return Optional.of(now);
|
||||
|
||||
for (int d = 0; d < DEFAULT_SEARCH_DAYS; d++) {
|
||||
ZonedDateTime day = now.plusDays(d);
|
||||
DayOfWeek dow = day.getDayOfWeek();
|
||||
LocalDate date = day.toLocalDate();
|
||||
|
||||
for (TimeRange range : dayToRanges.getOrDefault(dow, List.of())) {
|
||||
LocalDateTime startDt = LocalDateTime.of(date, range.getStart());
|
||||
|
||||
if (d == 0 && startDt.isBefore(now.toLocalDateTime())) {
|
||||
// Already passed today start
|
||||
if (range.spansMidnight()) {
|
||||
// If we are before end and past start, we would be open already; handled earlier
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
return Optional.of(startDt.atZone(zoneId));
|
||||
}
|
||||
|
||||
if (range.spansMidnight()) {
|
||||
// Open at start, even if that is today and time passed
|
||||
if (d > 0) return Optional.of(startDt.atZone(zoneId));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
import com.mrkayjaydee.playhours.PlayHoursMod;
|
||||
import com.mrkayjaydee.playhours.config.*;
|
||||
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Central service for managing server operation schedules.
|
||||
* Coordinates between PlayerAccessChecker, ScheduleCalculator, and ExceptionHandler.
|
||||
* All schedule checks are timezone-aware and support midnight-spanning periods.
|
||||
*/
|
||||
public final class ScheduleService {
|
||||
|
||||
private static final ScheduleService INSTANCE = new ScheduleService();
|
||||
|
||||
public static ScheduleService get() { return INSTANCE; }
|
||||
|
||||
private final PlayerAccessChecker accessChecker = PlayerAccessChecker.create();
|
||||
private final ScheduleCalculator scheduleCalculator = ScheduleCalculator.create();
|
||||
private final ExceptionHandler exceptionHandler = ExceptionHandler.create();
|
||||
|
||||
private volatile ZoneId zoneId = ZoneId.of("Europe/Paris");
|
||||
private volatile List<Integer> warningMinutes = List.of(15, 10, 5, 1);
|
||||
private volatile int closingThresholdMinutes = 0;
|
||||
private volatile boolean denyLoginDuringThreshold = true;
|
||||
private volatile boolean kickExempt = false;
|
||||
private volatile boolean exemptBypassSchedule = true;
|
||||
private volatile boolean exemptBypassThreshold = true;
|
||||
|
||||
private ScheduleService() {
|
||||
// Use safe defaults until config is loaded; rebuilt via ModConfigEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the schedule cache from the current ServerConfig values.
|
||||
* Should be called after any configuration changes.
|
||||
*/
|
||||
public void rebuildFromConfig() {
|
||||
this.zoneId = ZoneId.of(GeneralConfig.TIMEZONE.get());
|
||||
this.warningMinutes = GeneralConfig.WARNING_MINUTES.get().stream()
|
||||
.map(Object::toString)
|
||||
.map(Integer::parseInt)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.collect(Collectors.toList());
|
||||
this.closingThresholdMinutes = GeneralConfig.CLOSING_THRESHOLD_MINUTES.get();
|
||||
this.denyLoginDuringThreshold = GeneralConfig.DENY_LOGIN_DURING_THRESHOLD.get();
|
||||
this.kickExempt = GeneralConfig.KICK_EXEMPT.get();
|
||||
this.exemptBypassSchedule = GeneralConfig.EXEMPT_BYPASS_SCHEDULE.get();
|
||||
this.exemptBypassThreshold = GeneralConfig.EXEMPT_BYPASS_THRESHOLD.get();
|
||||
|
||||
// Update access checker
|
||||
ForceMode forceMode = ForceMode.valueOf(GeneralConfig.FORCE_MODE.get().toUpperCase(Locale.ROOT));
|
||||
boolean whitelistEnabled = ListsConfig.WHITELIST_ENABLED.get();
|
||||
boolean blacklistEnabled = ListsConfig.BLACKLIST_ENABLED.get();
|
||||
Set<String> whitelist = new HashSet<>(ListsConfig.WHITELIST.get());
|
||||
Set<String> blacklist = new HashSet<>(ListsConfig.BLACKLIST.get());
|
||||
accessChecker.updateConfig(forceMode, whitelistEnabled, blacklistEnabled, whitelist, blacklist);
|
||||
|
||||
// Update exception handler
|
||||
List<String> openExceptions = new ArrayList<>(ExceptionsConfig.OPEN_DATES.get());
|
||||
List<String> closedExceptions = new ArrayList<>(ExceptionsConfig.CLOSED_DATES.get());
|
||||
exceptionHandler.updateExceptions(openExceptions, closedExceptions);
|
||||
|
||||
// Update schedule calculator
|
||||
Map<DayOfWeek, List<TimeRange>> dayToRanges = new EnumMap<>(DayOfWeek.class);
|
||||
List<TimeRange> defaults = parseRanges(DefaultsConfig.PERIODS.get());
|
||||
dayToRanges.put(DayOfWeek.MONDAY, parseOrDefault(DaysConfig.MON.get(), defaults));
|
||||
dayToRanges.put(DayOfWeek.TUESDAY, parseOrDefault(DaysConfig.TUE.get(), defaults));
|
||||
dayToRanges.put(DayOfWeek.WEDNESDAY, parseOrDefault(DaysConfig.WED.get(), defaults));
|
||||
dayToRanges.put(DayOfWeek.THURSDAY, parseOrDefault(DaysConfig.THU.get(), defaults));
|
||||
dayToRanges.put(DayOfWeek.FRIDAY, parseOrDefault(DaysConfig.FRI.get(), defaults));
|
||||
dayToRanges.put(DayOfWeek.SATURDAY, parseOrDefault(DaysConfig.SAT.get(), defaults));
|
||||
dayToRanges.put(DayOfWeek.SUNDAY, parseOrDefault(DaysConfig.SUN.get(), defaults));
|
||||
scheduleCalculator.updateSchedule(dayToRanges, zoneId);
|
||||
}
|
||||
|
||||
private static List<TimeRange> parseOrDefault(List<? extends String> raw, List<TimeRange> dflt) {
|
||||
if (raw == null || raw.isEmpty()) return dflt;
|
||||
List<TimeRange> list = parseRanges(raw);
|
||||
return list.isEmpty() ? dflt : list;
|
||||
}
|
||||
|
||||
private static List<TimeRange> parseRanges(List<? extends String> raw) {
|
||||
List<TimeRange> list = new ArrayList<>();
|
||||
for (String s : raw) {
|
||||
try {
|
||||
list.add(TimeRange.parse(s));
|
||||
} catch (Exception e) {
|
||||
// Invalid time range format - skip and continue
|
||||
PlayHoursMod.LOGGER.debug("Failed to parse time range '{}': {}", s, e.getMessage());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public ZoneId getZoneId() { return zoneId; }
|
||||
public List<Integer> getWarningMinutes() { return warningMinutes; }
|
||||
public int getClosingThresholdMinutes() { return closingThresholdMinutes; }
|
||||
public boolean isDenyLoginDuringThreshold() { return denyLoginDuringThreshold; }
|
||||
public boolean isKickExempt() { return kickExempt; }
|
||||
public boolean isExemptBypassSchedule() { return exemptBypassSchedule; }
|
||||
public boolean isExemptBypassThreshold() { return exemptBypassThreshold; }
|
||||
|
||||
public ForceMode getForceMode() { return accessChecker.getForceMode(); }
|
||||
|
||||
/**
|
||||
* Checks if the server is open for a specific player by name.
|
||||
* Applies force mode, blacklist/whitelist, and schedule checks.
|
||||
*
|
||||
* @param name the player name (case-insensitive)
|
||||
* @param isExempt whether the player has exempt permission and bypass is enabled
|
||||
* @return true if the player is allowed to connect
|
||||
*/
|
||||
public boolean isOpenForName(String name, boolean isExempt) {
|
||||
if (!accessChecker.isPlayerAllowed(name, isExempt)) {
|
||||
return false;
|
||||
}
|
||||
// Exempt players bypass schedule check if exemptBypassSchedule is enabled
|
||||
if (isExempt) {
|
||||
return true;
|
||||
}
|
||||
return isOpen(ZonedDateTime.now(zoneId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server is open at the specified time based on schedule and exceptions.
|
||||
* Does not consider player-specific whitelists/blacklists.
|
||||
*
|
||||
* @param now the time to check
|
||||
* @return true if the server schedule is open at this time
|
||||
*/
|
||||
public boolean isOpen(ZonedDateTime now) {
|
||||
ForceMode forceMode = accessChecker.getForceMode();
|
||||
if (forceMode == ForceMode.FORCE_OPEN) return true;
|
||||
if (forceMode == ForceMode.FORCE_CLOSED) return false;
|
||||
|
||||
if (exceptionHandler.isClosedException(now)) return false;
|
||||
if (exceptionHandler.isOpenException(now)) return true;
|
||||
|
||||
return scheduleCalculator.isScheduleOpen(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next closing time from the current moment.
|
||||
* Only returns a value if the server is currently open.
|
||||
*
|
||||
* @param now the current time
|
||||
* @return the next closing time, or empty if server is not currently open
|
||||
*/
|
||||
public Optional<ZonedDateTime> nextClose(ZonedDateTime now) {
|
||||
if (!isOpen(now)) return Optional.empty();
|
||||
return scheduleCalculator.nextClose(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next opening time from the current moment.
|
||||
* Searches up to two weeks ahead for the next scheduled open period.
|
||||
*
|
||||
* @param now the current time
|
||||
* @return the next opening time, or empty if none found in the next 14 days
|
||||
*/
|
||||
public Optional<ZonedDateTime> nextOpen(ZonedDateTime now) {
|
||||
if (isOpen(now)) return Optional.of(now);
|
||||
|
||||
// Check exceptions first
|
||||
for (int d = 0; d < 14; d++) {
|
||||
ZonedDateTime day = now.plusDays(d);
|
||||
if (exceptionHandler.isClosedException(day)) continue;
|
||||
if (exceptionHandler.isOpenException(day)) return Optional.of(day);
|
||||
}
|
||||
|
||||
return scheduleCalculator.nextOpen(now);
|
||||
}
|
||||
}
|
||||
|
||||
200
src/main/java/com/mrkayjaydee/playhours/core/TimeRange.java
Normal file
200
src/main/java/com/mrkayjaydee/playhours/core/TimeRange.java
Normal file
@@ -0,0 +1,200 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a time range with start and end times (12-hour AM/PM format).
|
||||
* Supports midnight-spanning ranges where end < start.
|
||||
* Examples: "09:00 AM-05:00 PM", "10:00 PM-02:00 AM"
|
||||
*/
|
||||
public final class TimeRange {
|
||||
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("hh:mm a", java.util.Locale.ENGLISH);
|
||||
|
||||
private final LocalTime start;
|
||||
private final LocalTime end;
|
||||
|
||||
public TimeRange(LocalTime start, LocalTime end) {
|
||||
this.start = Objects.requireNonNull(start, "start");
|
||||
this.end = Objects.requireNonNull(end, "end");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TimeRange builder.
|
||||
*
|
||||
* @return a new TimeRangeBuilder
|
||||
*/
|
||||
public static TimeRangeBuilder builder() {
|
||||
return new TimeRangeBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for TimeRange objects.
|
||||
*/
|
||||
public static final class TimeRangeBuilder {
|
||||
private LocalTime start;
|
||||
private LocalTime end;
|
||||
|
||||
private TimeRangeBuilder() {}
|
||||
|
||||
/**
|
||||
* Sets the start time.
|
||||
*
|
||||
* @param start the start time
|
||||
* @return this builder
|
||||
*/
|
||||
public TimeRangeBuilder start(LocalTime start) {
|
||||
this.start = start;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the end time.
|
||||
*
|
||||
* @param end the end time
|
||||
* @return this builder
|
||||
*/
|
||||
public TimeRangeBuilder end(LocalTime end) {
|
||||
this.end = end;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the TimeRange.
|
||||
*
|
||||
* @return a new TimeRange
|
||||
*/
|
||||
public TimeRange build() {
|
||||
return new TimeRange(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalTime getStart() { return start; }
|
||||
public LocalTime getEnd() { return end; }
|
||||
|
||||
/**
|
||||
* Checks if this time range spans midnight (e.g., 22:00-02:00).
|
||||
* @return true if end time is before start time
|
||||
*/
|
||||
public boolean spansMidnight() {
|
||||
return end.isBefore(start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given time falls within this range.
|
||||
* Correctly handles midnight-spanning ranges.
|
||||
*
|
||||
* @param time the time to check
|
||||
* @return true if time is within this range
|
||||
*/
|
||||
public boolean contains(LocalTime time) {
|
||||
if (!spansMidnight()) {
|
||||
return !time.isBefore(start) && !time.isAfter(end);
|
||||
}
|
||||
// Midnight span: covers [start..24:00] U [00:00..end]
|
||||
return !time.isBefore(start) || !time.isAfter(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a time range from a string in 12-hour AM/PM format "hh:mm AM-hh:mm PM".
|
||||
*
|
||||
* @param value the string to parse (e.g., "09:00 AM-05:00 PM" or "10:00 PM-02:00 AM")
|
||||
* @return the parsed TimeRange
|
||||
* @throws IllegalArgumentException if the format is invalid
|
||||
*/
|
||||
public static TimeRange parse(String value) {
|
||||
String[] parts = value.trim().split("-");
|
||||
if (parts.length != 2) throw new IllegalArgumentException("Invalid time range: " + value);
|
||||
try {
|
||||
LocalTime s = LocalTime.parse(parts[0].trim(), FORMAT);
|
||||
LocalTime e = LocalTime.parse(parts[1].trim(), FORMAT);
|
||||
return new TimeRange(s, e);
|
||||
} catch (DateTimeParseException ex) {
|
||||
throw new IllegalArgumentException("Invalid time format in range: " + value + " (expected format: hh:mm AM-hh:mm PM)", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return start.format(FORMAT) + "-" + end.format(FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a LocalTime to 12-hour AM/PM format string.
|
||||
* @param time the time to format
|
||||
* @return formatted time string (e.g., "09:00 AM")
|
||||
*/
|
||||
public static String formatTime(LocalTime time) {
|
||||
return time.format(FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a LocalTime using the provided locale.
|
||||
* For French locale, uses 24-hour format (HH:mm).
|
||||
* For other locales, uses 12-hour AM/PM format (hh:mm a).
|
||||
* Falls back to English if locale is null.
|
||||
*/
|
||||
public static String formatTime(LocalTime time, java.util.Locale locale) {
|
||||
if (locale == null) locale = java.util.Locale.ENGLISH;
|
||||
|
||||
// French locale prefers 24-hour format (18:00 instead of 06:00 PM)
|
||||
String pattern = locale.getLanguage().equals("fr") ? "HH:mm" : "hh:mm a";
|
||||
java.time.format.DateTimeFormatter fmt = java.time.format.DateTimeFormatter.ofPattern(pattern, locale);
|
||||
return time.format(fmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time range is valid (start and end are not null).
|
||||
*
|
||||
* @return true if the range is valid
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return start != null && end != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration of this time range.
|
||||
* For midnight-spanning ranges, returns the duration until midnight plus the duration after midnight.
|
||||
*
|
||||
* @return the duration of this range
|
||||
*/
|
||||
public java.time.Duration getDuration() {
|
||||
if (!spansMidnight()) {
|
||||
return java.time.Duration.between(start, end);
|
||||
} else {
|
||||
// Midnight-spanning range: duration from start to midnight + duration from midnight to end
|
||||
java.time.Duration toMidnight = java.time.Duration.between(start, java.time.LocalTime.MIDNIGHT);
|
||||
java.time.Duration fromMidnight = java.time.Duration.between(java.time.LocalTime.MIDNIGHT, end);
|
||||
return toMidnight.plus(fromMidnight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time range overlaps with another time range.
|
||||
*
|
||||
* @param other the other time range
|
||||
* @return true if the ranges overlap
|
||||
*/
|
||||
public boolean overlaps(TimeRange other) {
|
||||
if (other == null) return false;
|
||||
|
||||
if (!spansMidnight() && !other.spansMidnight()) {
|
||||
// Neither spans midnight - simple case
|
||||
return !start.isAfter(other.end) && !end.isBefore(other.start);
|
||||
} else if (spansMidnight() && !other.spansMidnight()) {
|
||||
// This spans midnight, other doesn't
|
||||
return other.contains(start) || other.contains(end) ||
|
||||
(other.start.isAfter(start) && other.end.isBefore(end));
|
||||
} else if (!spansMidnight() && other.spansMidnight()) {
|
||||
// Other spans midnight, this doesn't
|
||||
return contains(other.start) || contains(other.end) ||
|
||||
(start.isAfter(other.start) && end.isBefore(other.end));
|
||||
} else {
|
||||
// Both span midnight
|
||||
return true; // Two midnight-spanning ranges always overlap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.mrkayjaydee.playhours.core;
|
||||
|
||||
/**
|
||||
* Validates time range strings and provides validation utilities.
|
||||
* Separates validation logic from parsing logic.
|
||||
*/
|
||||
public final class TimeRangeValidator {
|
||||
private TimeRangeValidator() {}
|
||||
|
||||
/**
|
||||
* Validates a time range string format.
|
||||
*
|
||||
* @param range the time range string to validate
|
||||
* @return true if the format is valid
|
||||
*/
|
||||
public static boolean isValidFormat(String range) {
|
||||
if (range == null || range.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] parts = range.trim().split("-");
|
||||
if (parts.length != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
TimeRange.parse(range);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a time range string and throws an exception if invalid.
|
||||
*
|
||||
* @param range the time range string to validate
|
||||
* @throws IllegalArgumentException if the range is invalid
|
||||
*/
|
||||
public static void validateRange(String range) {
|
||||
if (!isValidFormat(range)) {
|
||||
throw new IllegalArgumentException("Invalid time range format: " + range);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.permissions.PermissionChecker;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import com.mrkayjaydee.playhours.config.ConfigEventHandler;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handles login enforcement based on server operating hours and thresholds.
|
||||
* Disconnects players immediately after login if they are not permitted during current schedule.
|
||||
*/
|
||||
@Mod.EventBusSubscriber
|
||||
public class LoginGuard {
|
||||
|
||||
// Note: Using PlayerLoggedInEvent as per PLAN.md fallback strategy.
|
||||
// Early denial could be implemented via ServerLoginNetworkEvent.CheckLogin if needed.
|
||||
|
||||
/**
|
||||
* Checks if player login is permitted based on current schedule, threshold, and exemptions.
|
||||
* Disconnects the player immediately if access is denied.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer player)) return;
|
||||
if (!ConfigEventHandler.isReady()) {
|
||||
// Defer checks until config ready to avoid early access
|
||||
com.mrkayjaydee.playhours.PlayHoursMod.LOGGER.debug("LoginGuard deferred: config not ready for {}", player.getGameProfile().getName());
|
||||
return;
|
||||
}
|
||||
ScheduleService sched = ScheduleService.get();
|
||||
boolean exempt = PermissionChecker.isExempt(player);
|
||||
ZonedDateTime now = ZonedDateTime.now(sched.getZoneId());
|
||||
|
||||
// First check: is the player allowed by schedule/lists/force mode?
|
||||
boolean bypassSchedule = exempt && sched.isExemptBypassSchedule();
|
||||
if (!sched.isOpenForName(player.getGameProfile().getName(), bypassSchedule)) {
|
||||
ScheduleFormatter.FormattedSchedule nextOpen = ScheduleFormatter.formatNextOpen(sched.nextOpen(now));
|
||||
player.connection.disconnect(Messages.accessDenied(nextOpen.day, nextOpen.time));
|
||||
return;
|
||||
}
|
||||
|
||||
// Second check: threshold - deny login if within closing threshold
|
||||
boolean bypassThreshold = exempt && sched.isExemptBypassThreshold();
|
||||
if (!bypassThreshold && sched.isOpen(now) && sched.isDenyLoginDuringThreshold()) {
|
||||
Optional<ZonedDateTime> nextClose = sched.nextClose(now);
|
||||
if (nextClose.isPresent()) {
|
||||
long minutesUntilClose = Duration.between(now, nextClose.get()).toMinutes();
|
||||
if (minutesUntilClose >= 0 && minutesUntilClose <= sched.getClosingThresholdMinutes()) {
|
||||
ScheduleFormatter.FormattedSchedule nextOpen = ScheduleFormatter.formatNextOpen(sched.nextOpen(nextClose.get().plusMinutes(1)));
|
||||
player.connection.disconnect(Messages.thresholdDenied(nextOpen.day, nextOpen.time));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
218
src/main/java/com/mrkayjaydee/playhours/events/MOTDBuilder.java
Normal file
218
src/main/java/com/mrkayjaydee/playhours/events/MOTDBuilder.java
Normal file
@@ -0,0 +1,218 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.config.MOTDConfig;
|
||||
import com.mrkayjaydee.playhours.core.ForceMode;
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.events.ScheduleFormatter.FormattedSchedule;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Builds MOTD components based on configuration.
|
||||
* Supports multiple MOTD strategies: rotating, custom lines, custom format, and automatic.
|
||||
*/
|
||||
public final class MOTDBuilder {
|
||||
private MOTDBuilder() {}
|
||||
|
||||
private static int currentRotationIndex = 0;
|
||||
private static long lastRotationTime = 0;
|
||||
private static List<? extends String> lastRotationTemplates = null;
|
||||
|
||||
/**
|
||||
* Builds the MOTD component based on configuration and current schedule state.
|
||||
* Priority: rotation → custom lines → custom format → automatic
|
||||
*
|
||||
* @param scheduleService the schedule service
|
||||
* @param now the current time
|
||||
* @return the MOTD component
|
||||
*/
|
||||
public static Component build(ScheduleService scheduleService, ZonedDateTime now) {
|
||||
// Check for rotation first
|
||||
if (MOTDConfig.ROTATION_ENABLED.get()) {
|
||||
List<? extends String> templates = MOTDConfig.ROTATION_TEMPLATES.get();
|
||||
if (templates != null && !templates.isEmpty()) {
|
||||
return buildRotating(scheduleService, now, templates);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for custom lines
|
||||
List<? extends String> customLines = MOTDConfig.CUSTOM_LINES.get();
|
||||
if (customLines != null && !customLines.isEmpty()) {
|
||||
return buildCustomLines(scheduleService, now, customLines);
|
||||
}
|
||||
|
||||
// Check for custom format
|
||||
String customFormat = MOTDConfig.CUSTOM_FORMAT.get();
|
||||
if (customFormat != null && !customFormat.trim().isEmpty()) {
|
||||
return buildCustomFormat(scheduleService, now, customFormat);
|
||||
}
|
||||
|
||||
// Build automatic MOTD
|
||||
return buildAutomatic(scheduleService, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a rotating MOTD that cycles through configured templates.
|
||||
*/
|
||||
private static Component buildRotating(ScheduleService scheduleService, ZonedDateTime now, List<? extends String> templates) {
|
||||
if (templates.isEmpty()) {
|
||||
return buildCustomLines(scheduleService, now, MOTDConfig.CUSTOM_LINES.get());
|
||||
}
|
||||
|
||||
// Check if template list changed
|
||||
if (lastRotationTemplates != templates) {
|
||||
lastRotationTemplates = templates;
|
||||
currentRotationIndex = 0;
|
||||
lastRotationTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// Calculate time elapsed since last rotation
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long rotationIntervalMillis = MOTDConfig.ROTATION_INTERVAL_SECONDS.get() * 1000L;
|
||||
long timeSinceLastRotation = currentTime - lastRotationTime;
|
||||
|
||||
// Check if we should advance to next template
|
||||
if (timeSinceLastRotation >= rotationIntervalMillis) {
|
||||
// Move to next template
|
||||
currentRotationIndex = (currentRotationIndex + 1) % templates.size();
|
||||
lastRotationTime = currentTime;
|
||||
}
|
||||
|
||||
String template = templates.get(currentRotationIndex);
|
||||
return MOTDFormatter.formatLine(scheduleService, now, template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds MOTD from custom line configuration.
|
||||
*/
|
||||
private static Component buildCustomLines(ScheduleService scheduleService, ZonedDateTime now, List<? extends String> customLines) {
|
||||
MutableComponent result = Component.empty();
|
||||
|
||||
for (int i = 0; i < customLines.size(); i++) {
|
||||
String line = customLines.get(i);
|
||||
Component formatted = MOTDFormatter.formatLine(scheduleService, now, line);
|
||||
result.append(formatted);
|
||||
|
||||
if (i < customLines.size() - 1) {
|
||||
result.append(Component.literal("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds MOTD from custom format string.
|
||||
*/
|
||||
private static Component buildCustomFormat(ScheduleService scheduleService, ZonedDateTime now, String format) {
|
||||
return MOTDFormatter.formatLine(scheduleService, now, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds automatic MOTD based on configuration flags.
|
||||
*/
|
||||
private static Component buildAutomatic(ScheduleService scheduleService, ZonedDateTime now) {
|
||||
List<Component> parts = new ArrayList<>();
|
||||
|
||||
// Get schedule information
|
||||
boolean isOpen = scheduleService.isOpen(now);
|
||||
ForceMode forceMode = scheduleService.getForceMode();
|
||||
Optional<ZonedDateTime> nextClose = scheduleService.nextClose(now);
|
||||
FormattedSchedule nextOpen = ScheduleFormatter.formatNextOpen(scheduleService.nextOpen(now));
|
||||
|
||||
// Show force mode if enabled
|
||||
if (MOTDConfig.SHOW_FORCE_MODE.get() && forceMode != ForceMode.NORMAL) {
|
||||
String forceModeText = forceMode == ForceMode.FORCE_OPEN
|
||||
? Messages.get("msg.motd_force_open")
|
||||
: Messages.get("msg.motd_force_closed");
|
||||
parts.add(MOTDFormatter.colorize(forceModeText, ChatFormatting.GOLD));
|
||||
}
|
||||
|
||||
// Show status if enabled
|
||||
if (MOTDConfig.SHOW_STATUS.get()) {
|
||||
String statusKey = isOpen ? "msg.motd_status_open" : "msg.motd_status_closed";
|
||||
String statusText = Messages.get(statusKey);
|
||||
ChatFormatting statusColor = isOpen
|
||||
? MOTDColorParser.parseColor(MOTDConfig.OPEN_COLOR.get())
|
||||
: MOTDColorParser.parseColor(MOTDConfig.CLOSED_COLOR.get());
|
||||
parts.add(MOTDFormatter.colorize(statusText, statusColor));
|
||||
}
|
||||
|
||||
// Show countdown if enabled and applicable
|
||||
if (MOTDConfig.SHOW_COUNTDOWN.get() && isOpen && nextClose.isPresent()) {
|
||||
addCountdownIfApplicable(parts, nextClose.get(), now);
|
||||
}
|
||||
|
||||
// Show next close if enabled and open
|
||||
if (MOTDConfig.SHOW_NEXT_CLOSE.get() && isOpen && nextClose.isPresent()) {
|
||||
addNextClose(parts, nextClose.get());
|
||||
}
|
||||
|
||||
// Show next open if enabled and closed
|
||||
if (MOTDConfig.SHOW_NEXT_OPEN.get() && !isOpen) {
|
||||
addNextOpen(parts, nextOpen);
|
||||
}
|
||||
|
||||
// Combine parts
|
||||
if (parts.isEmpty()) {
|
||||
return Component.literal("");
|
||||
}
|
||||
|
||||
return combineParts(parts);
|
||||
}
|
||||
|
||||
private static void addCountdownIfApplicable(List<Component> parts, ZonedDateTime nextClose, ZonedDateTime now) {
|
||||
long minutesUntilClose = java.time.temporal.ChronoUnit.MINUTES.between(now, nextClose);
|
||||
int countdownThreshold = MOTDConfig.COUNTDOWN_THRESHOLD_MINUTES.get();
|
||||
|
||||
if (countdownThreshold > 0 && minutesUntilClose <= countdownThreshold && minutesUntilClose > 0) {
|
||||
String countdownText = Messages.get("msg.motd_countdown")
|
||||
.replace("%minutes%", String.valueOf(minutesUntilClose));
|
||||
parts.add(MOTDFormatter.colorize(countdownText, ChatFormatting.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addNextClose(List<Component> parts, ZonedDateTime nextClose) {
|
||||
String closeTime = com.mrkayjaydee.playhours.core.TimeRange.formatTime(nextClose.toLocalTime(), Messages.getJavaLocale());
|
||||
String closeText = Messages.get("msg.motd_next_close")
|
||||
.replace("%closetime%", closeTime);
|
||||
ChatFormatting infoColor = MOTDColorParser.parseColor(MOTDConfig.INFO_COLOR.get());
|
||||
parts.add(MOTDFormatter.colorize(closeText, infoColor));
|
||||
}
|
||||
|
||||
private static void addNextOpen(List<Component> parts, FormattedSchedule nextOpen) {
|
||||
String openText = Messages.get("msg.motd_next_open")
|
||||
.replace("%openday%", nextOpen.day)
|
||||
.replace("%opentime%", nextOpen.time);
|
||||
ChatFormatting infoColor = MOTDColorParser.parseColor(MOTDConfig.INFO_COLOR.get());
|
||||
parts.add(MOTDFormatter.colorize(openText, infoColor));
|
||||
}
|
||||
|
||||
private static Component combineParts(List<Component> parts) {
|
||||
String separator = MOTDConfig.SEPARATOR.get();
|
||||
boolean useSecondLine = MOTDConfig.SHOW_ON_SECOND_LINE.get();
|
||||
|
||||
MutableComponent result = Component.empty();
|
||||
|
||||
if (useSecondLine) {
|
||||
// Put on second line
|
||||
result.append(Component.literal("\n"));
|
||||
}
|
||||
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
result.append(parts.get(i));
|
||||
if (i < parts.size() - 1) {
|
||||
result.append(Component.literal(separator));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.PlayHoursMod;
|
||||
import net.minecraft.ChatFormatting;
|
||||
|
||||
/**
|
||||
* Utility for parsing color names and codes to Minecraft ChatFormatting.
|
||||
* Handles both named colors (e.g., "green") and section sign codes (e.g., "§a").
|
||||
*/
|
||||
public final class MOTDColorParser {
|
||||
private MOTDColorParser() {}
|
||||
|
||||
/**
|
||||
* Parses a color name or code to ChatFormatting.
|
||||
*
|
||||
* @param colorStr color name (e.g., "green", "red") or § code (e.g., "§a")
|
||||
* @return the ChatFormatting color, or GRAY as default
|
||||
*/
|
||||
public static ChatFormatting parseColor(String colorStr) {
|
||||
if (colorStr == null || colorStr.trim().isEmpty()) {
|
||||
return ChatFormatting.GRAY;
|
||||
}
|
||||
|
||||
String normalized = colorStr.trim().toUpperCase().replace(" ", "_");
|
||||
|
||||
try {
|
||||
return ChatFormatting.valueOf(normalized);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Try parsing § codes
|
||||
if (colorStr.startsWith("§") && colorStr.length() == 2) {
|
||||
ChatFormatting byCode = ChatFormatting.getByCode(colorStr.charAt(1));
|
||||
if (byCode != null) {
|
||||
return byCode;
|
||||
}
|
||||
}
|
||||
|
||||
PlayHoursMod.LOGGER.warn("Invalid color code '{}', using GRAY", colorStr);
|
||||
return ChatFormatting.GRAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.config.MOTDConfig;
|
||||
import com.mrkayjaydee.playhours.core.ForceModeFormatter;
|
||||
import com.mrkayjaydee.playhours.core.ForceMode;
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.core.TimeRange;
|
||||
import com.mrkayjaydee.playhours.events.ScheduleFormatter.FormattedSchedule;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import com.mrkayjaydee.playhours.text.MessageKeys;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Formats MOTD lines with placeholder replacement and color application.
|
||||
* Replaces dynamic placeholders like %status%, %mode%, %isopen%, etc. with actual values.
|
||||
*/
|
||||
public final class MOTDFormatter {
|
||||
private MOTDFormatter() {}
|
||||
|
||||
/**
|
||||
* Formats a line with placeholder replacement and coloring.
|
||||
*/
|
||||
public static Component formatLine(ScheduleService scheduleService, ZonedDateTime now, String format) {
|
||||
boolean isOpen = scheduleService.isOpen(now);
|
||||
Optional<ZonedDateTime> nextClose = scheduleService.nextClose(now);
|
||||
FormattedSchedule nextOpen = ScheduleFormatter.formatNextOpen(scheduleService.nextOpen(now));
|
||||
ForceMode forceMode = scheduleService.getForceMode();
|
||||
long minutesUntilClose = nextClose.isPresent() ? ChronoUnit.MINUTES.between(now, nextClose.get()) : 0;
|
||||
|
||||
// Replace placeholders
|
||||
String formatted = format
|
||||
.replace("%status%", isOpen
|
||||
? Messages.get("msg.motd_status_open")
|
||||
: Messages.get("msg.motd_status_closed"))
|
||||
.replace("%mode%", ForceModeFormatter.format(forceMode))
|
||||
.replace("%isopen%", isOpen ? Messages.get(MessageKeys.YES) : Messages.get(MessageKeys.NO))
|
||||
.replace("%nextopen%", nextOpen.day + Messages.get(MessageKeys.DAY_TIME_SEPARATOR) + nextOpen.time)
|
||||
.replace("%openday%", nextOpen.day)
|
||||
.replace("%opentime%", nextOpen.time)
|
||||
.replace("%closetime%", nextClose.map(z -> TimeRange.formatTime(z.toLocalTime(), Messages.getJavaLocale())).orElse("-"))
|
||||
.replace("%nextclose%", nextClose.map(z -> TimeRange.formatTime(z.toLocalTime(), Messages.getJavaLocale())).orElse("-"))
|
||||
.replace("%minutes%", String.valueOf(minutesUntilClose));
|
||||
|
||||
// Apply coloring if enabled
|
||||
if (MOTDConfig.USE_COLORS.get()) {
|
||||
return parseFormattedText(formatted);
|
||||
}
|
||||
|
||||
return Component.literal(formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses text with color codes and returns a Component.
|
||||
*/
|
||||
private static Component parseFormattedText(String text) {
|
||||
// Simple implementation - can be enhanced to support inline color codes
|
||||
return Component.literal(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorizes a component with the specified color.
|
||||
*/
|
||||
public static Component colorize(String text, ChatFormatting color) {
|
||||
if (MOTDConfig.USE_COLORS.get()) {
|
||||
return Component.literal(text).withStyle(color);
|
||||
}
|
||||
return Component.literal(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.PlayHoursMod;
|
||||
import com.mrkayjaydee.playhours.config.GeneralConfig;
|
||||
import com.mrkayjaydee.playhours.config.MOTDConfig;
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/**
|
||||
* Handles server MOTD (Message of the Day) customization.
|
||||
* Orchestrates periodic MOTD updates based on current schedule state.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = PlayHoursMod.MODID)
|
||||
public final class MOTDHandler {
|
||||
|
||||
private static long lastMOTDUpdate = 0;
|
||||
|
||||
private MOTDHandler() {}
|
||||
|
||||
/**
|
||||
* Handles server tick to periodically update MOTD with schedule information.
|
||||
*
|
||||
* @param event the server tick event
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (event.getServer() == null) return;
|
||||
|
||||
try {
|
||||
// Check if MOTD feature is enabled
|
||||
if (!GeneralConfig.MOTD_ENABLED.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update periodically based on configured delay
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long updateIntervalMillis = MOTDConfig.UPDATE_DELAY_SECONDS.get() * 1000L;
|
||||
if (currentTime - lastMOTDUpdate < updateIntervalMillis) {
|
||||
return;
|
||||
}
|
||||
lastMOTDUpdate = currentTime;
|
||||
|
||||
MinecraftServer server = event.getServer();
|
||||
|
||||
// Get current schedule information
|
||||
ScheduleService scheduleService = ScheduleService.get();
|
||||
ZonedDateTime now = ZonedDateTime.now(scheduleService.getZoneId());
|
||||
|
||||
// Build MOTD component
|
||||
Component motd = MOTDBuilder.build(scheduleService, now);
|
||||
|
||||
// Validate and truncate to Minecraft limits (2 lines, 59 chars per line)
|
||||
Component validatedMotd = MOTDValidator.validateAndTruncate(motd);
|
||||
|
||||
// Apply MOTD to server
|
||||
server.setMotd(validatedMotd.getString());
|
||||
|
||||
} catch (Exception e) {
|
||||
PlayHoursMod.LOGGER.error("Error updating MOTD", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.PlayHoursMod;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/**
|
||||
* Validates and truncates MOTD to comply with Minecraft protocol limits.
|
||||
* Enforces: 2 lines maximum, ~59 characters per line.
|
||||
*/
|
||||
public final class MOTDValidator {
|
||||
private MOTDValidator() {}
|
||||
|
||||
private static final int MINECRAFT_LINE_LIMIT = 59;
|
||||
private static final int MINECRAFT_MAX_LINES = 2;
|
||||
|
||||
/**
|
||||
* Validates and truncates MOTD to comply with Minecraft protocol limits.
|
||||
* Minecraft MOTD: 2 lines max, ~59 characters per line
|
||||
*
|
||||
* @param motd the MOTD component to validate
|
||||
* @return the validated/truncated MOTD component
|
||||
*/
|
||||
public static Component validateAndTruncate(Component motd) {
|
||||
String text = motd.getString();
|
||||
String[] lines = text.split("\n", -1);
|
||||
|
||||
// Limit to 2 lines maximum (Minecraft displays 2 lines in server list)
|
||||
if (lines.length > MINECRAFT_MAX_LINES) {
|
||||
PlayHoursMod.LOGGER.warn("MOTD has {} lines but Minecraft only displays {}. Truncating.", lines.length, MINECRAFT_MAX_LINES);
|
||||
lines = new String[]{lines[0], lines[1]};
|
||||
}
|
||||
|
||||
// Truncate each line to ~59 characters (Minecraft MOTD line width limit)
|
||||
StringBuilder validatedMotd = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
if (line.length() > MINECRAFT_LINE_LIMIT) {
|
||||
PlayHoursMod.LOGGER.warn("MOTD line {} is {} chars but Minecraft limit is ~{}. Truncating.", i + 1, line.length(), MINECRAFT_LINE_LIMIT);
|
||||
line = line.substring(0, Math.min(line.length(), MINECRAFT_LINE_LIMIT));
|
||||
}
|
||||
validatedMotd.append(line);
|
||||
if (i < lines.length - 1) {
|
||||
validatedMotd.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return Component.literal(validatedMotd.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.permissions.PermissionChecker;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles player kicking logic for schedule enforcement.
|
||||
* Separates kick logic from tick scheduling.
|
||||
*/
|
||||
public final class PlayerKickHandler {
|
||||
private PlayerKickHandler() {}
|
||||
|
||||
/**
|
||||
* Kicks players at closing time based on schedule and exemptions.
|
||||
*
|
||||
* @param players the list of players to check
|
||||
* @param scheduleService the schedule service
|
||||
* @param nextClose the next closing time
|
||||
*/
|
||||
public static void kickPlayersAtClose(List<ServerPlayer> players, ScheduleService scheduleService, ZonedDateTime nextClose) {
|
||||
ScheduleFormatter.FormattedSchedule nextOpen = ScheduleFormatter.formatNextOpen(scheduleService.nextOpen(nextClose.plusMinutes(1)));
|
||||
|
||||
for (ServerPlayer player : players) {
|
||||
boolean exempt = PermissionChecker.isExempt(player);
|
||||
if (!exempt || scheduleService.isKickExempt()) {
|
||||
player.connection.disconnect(Messages.kick(nextOpen.day, nextOpen.time));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.core.TimeRange;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.TextStyle;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handles formatting of schedule information for display.
|
||||
* Separates formatting logic from event handling.
|
||||
*/
|
||||
public final class ScheduleFormatter {
|
||||
private ScheduleFormatter() {}
|
||||
|
||||
/**
|
||||
* Formats the next opening time for display.
|
||||
*
|
||||
* @param nextOpen the next opening time
|
||||
* @return formatted day and time strings
|
||||
*/
|
||||
public static FormattedSchedule formatNextOpen(Optional<ZonedDateTime> nextOpen) {
|
||||
String day = nextOpen.map(dt -> dt.getDayOfWeek().getDisplayName(TextStyle.FULL, Messages.getJavaLocale())).orElse("?");
|
||||
String time = nextOpen.map(dt -> TimeRange.formatTime(dt.toLocalTime(), Messages.getJavaLocale())).orElse("?");
|
||||
return new FormattedSchedule(day, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the next closing time for display.
|
||||
*
|
||||
* @param nextClose the next closing time
|
||||
* @return formatted time string
|
||||
*/
|
||||
public static String formatNextClose(Optional<ZonedDateTime> nextClose) {
|
||||
return nextClose.map(z -> TimeRange.formatTime(z.toLocalTime(), Messages.getJavaLocale())).orElse("-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current schedule status and next times.
|
||||
*
|
||||
* @param scheduleService the schedule service
|
||||
* @param now the current time
|
||||
* @return formatted schedule information
|
||||
*/
|
||||
public static FormattedSchedule getScheduleInfo(ScheduleService scheduleService, ZonedDateTime now) {
|
||||
boolean open = scheduleService.isOpen(now);
|
||||
String nextClose = formatNextClose(scheduleService.nextClose(now));
|
||||
FormattedSchedule nextOpen = formatNextOpen(scheduleService.nextOpen(now));
|
||||
|
||||
return new FormattedSchedule(open, nextClose, nextOpen.day, nextOpen.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for formatted schedule information.
|
||||
*/
|
||||
public static final class FormattedSchedule {
|
||||
public final String day;
|
||||
public final String time;
|
||||
public final boolean isOpen;
|
||||
public final String nextClose;
|
||||
|
||||
public FormattedSchedule(String day, String time) {
|
||||
this.day = day;
|
||||
this.time = time;
|
||||
this.isOpen = false;
|
||||
this.nextClose = null;
|
||||
}
|
||||
|
||||
public FormattedSchedule(boolean isOpen, String nextClose, String day, String time) {
|
||||
this.isOpen = isOpen;
|
||||
this.nextClose = nextClose;
|
||||
this.day = day;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.config.*;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Handles periodic server tick processing for warnings and auto-kick at closing time.
|
||||
* Runs checks every second (20 ticks) to broadcast warnings and enforce closing times.
|
||||
*/
|
||||
@Mod.EventBusSubscriber
|
||||
public class TickScheduler {
|
||||
private static final int TICKS_PER_SECOND = 20;
|
||||
private static int tickCounter = 0;
|
||||
private static final Set<Integer> sentCountdowns = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Server tick event handler that checks for closing warnings and kicks players at closing time.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
tickCounter++;
|
||||
if (tickCounter % TICKS_PER_SECOND != 0) return; // once per second
|
||||
|
||||
var server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer();
|
||||
if (server == null) return;
|
||||
|
||||
ScheduleService sched = ScheduleService.get();
|
||||
if (!ConfigEventHandler.isReady()) {
|
||||
// Config not ready yet; avoid accessing values
|
||||
com.mrkayjaydee.playhours.PlayHoursMod.LOGGER.debug("Tick skipped: config not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
ZonedDateTime now = ZonedDateTime.now(sched.getZoneId());
|
||||
boolean open = sched.isOpen(now);
|
||||
|
||||
if (open) {
|
||||
Optional<ZonedDateTime> nextClose = sched.nextClose(now);
|
||||
if (nextClose.isPresent()) {
|
||||
// Broadcast warnings
|
||||
WarningBroadcaster.broadcastWarnings(server, sched, now, nextClose.get());
|
||||
|
||||
// Handle countdown messages
|
||||
handleCountdown(server, now, nextClose.get());
|
||||
|
||||
// Kick at close - use reliable time comparison
|
||||
if (!now.isBefore(nextClose.get())) {
|
||||
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
|
||||
PlayerKickHandler.kickPlayersAtClose(players, sched, nextClose.get());
|
||||
WarningBroadcaster.clearWarnings();
|
||||
sentCountdowns.clear();
|
||||
}
|
||||
} else {
|
||||
WarningBroadcaster.clearWarnings();
|
||||
sentCountdowns.clear();
|
||||
}
|
||||
} else {
|
||||
WarningBroadcaster.clearWarnings();
|
||||
sentCountdowns.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles countdown messages before closing.
|
||||
* Sends messages every second for the configured number of seconds before closing.
|
||||
*/
|
||||
private static void handleCountdown(net.minecraft.server.MinecraftServer server, ZonedDateTime now, ZonedDateTime nextClose) {
|
||||
int countdownSeconds = GeneralConfig.COUNTDOWN_SECONDS.get();
|
||||
if (countdownSeconds <= 0) return;
|
||||
|
||||
long secondsUntilClose = java.time.Duration.between(now, nextClose).getSeconds();
|
||||
|
||||
if (secondsUntilClose <= countdownSeconds && secondsUntilClose > 0) {
|
||||
int seconds = (int) secondsUntilClose;
|
||||
|
||||
// Only send if we haven't sent this countdown yet
|
||||
if (!sentCountdowns.contains(seconds)) {
|
||||
sentCountdowns.add(seconds);
|
||||
server.getPlayerList().broadcastSystemMessage(Messages.countdown(seconds), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.mrkayjaydee.playhours.events;
|
||||
|
||||
import com.mrkayjaydee.playhours.core.ScheduleService;
|
||||
import com.mrkayjaydee.playhours.text.Messages;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Handles warning broadcasts for approaching closing times.
|
||||
* Separates warning logic from tick scheduling.
|
||||
*/
|
||||
public final class WarningBroadcaster {
|
||||
private WarningBroadcaster() {}
|
||||
|
||||
private static final Set<Integer> sentForMinute = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Broadcasts warnings for approaching closing time.
|
||||
*
|
||||
* @param server the Minecraft server
|
||||
* @param scheduleService the schedule service
|
||||
* @param now the current time
|
||||
* @param nextClose the next closing time
|
||||
*/
|
||||
public static void broadcastWarnings(MinecraftServer server, ScheduleService scheduleService,
|
||||
ZonedDateTime now, ZonedDateTime nextClose) {
|
||||
long minutes = Math.max(0, java.time.Duration.between(now, nextClose).toMinutes());
|
||||
int minInt = (int) minutes;
|
||||
|
||||
for (int mark : scheduleService.getWarningMinutes()) {
|
||||
if (minInt == mark && sentForMinute.add(mark)) {
|
||||
String closeTime = ScheduleFormatter.formatNextClose(java.util.Optional.of(nextClose));
|
||||
server.getPlayerList().broadcastSystemMessage(Messages.warn(mark, closeTime), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the warning tracking set.
|
||||
*/
|
||||
public static void clearWarnings() {
|
||||
sentForMinute.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.mrkayjaydee.playhours.permissions;
|
||||
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.server.ServerLifecycleHooks;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Handles LuckPerms integration for permission checking.
|
||||
* Separates LuckPerms-specific logic from general permission checking.
|
||||
*/
|
||||
public final class LuckPermsIntegration {
|
||||
private LuckPermsIntegration() {}
|
||||
|
||||
// LuckPerms soft integration - detected at startup
|
||||
private static net.luckperms.api.LuckPerms luckPerms;
|
||||
static {
|
||||
try {
|
||||
luckPerms = net.luckperms.api.LuckPermsProvider.get();
|
||||
} catch (Throwable ignored) {
|
||||
// LuckPerms not present, will use vanilla fallback
|
||||
luckPerms = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if LuckPerms is available.
|
||||
*
|
||||
* @return true if LuckPerms is loaded and available
|
||||
*/
|
||||
public static boolean isAvailable() {
|
||||
return luckPerms != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a permission for an online player using LuckPerms.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @param permission the permission node
|
||||
* @return true if the player has the permission
|
||||
*/
|
||||
public static boolean hasPermission(ServerPlayer player, String permission) {
|
||||
if (!isAvailable()) return false;
|
||||
|
||||
var user = luckPerms.getUserManager().getUser(player.getUUID());
|
||||
if (user == null) return false;
|
||||
|
||||
var data = user.getCachedData().getPermissionData();
|
||||
var result = data.checkPermission(permission);
|
||||
return result.asBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a permission for an offline player by UUID using LuckPerms.
|
||||
* Uses a timeout to prevent blocking indefinitely.
|
||||
*
|
||||
* @param uuid the player UUID
|
||||
* @param permission the permission node
|
||||
* @return true if the player has the permission, false otherwise or on timeout
|
||||
*/
|
||||
public static boolean hasPermissionOffline(UUID uuid, String permission) {
|
||||
if (!isAvailable()) return false;
|
||||
|
||||
// Check if player is online first
|
||||
var server = ServerLifecycleHooks.getCurrentServer();
|
||||
ServerPlayer online = server.getPlayerList().getPlayer(uuid);
|
||||
if (online != null) {
|
||||
return hasPermission(online, permission);
|
||||
}
|
||||
|
||||
// Offline LP check (best-effort with timeout to avoid blocking)
|
||||
try {
|
||||
var future = luckPerms.getUserManager().loadUser(uuid);
|
||||
var user = future.get(PermissionConstants.LUCKPERMS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (user != null) {
|
||||
var result = user.getCachedData().getPermissionData().checkPermission(permission);
|
||||
return result.asBoolean();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// Timeout, interrupted, or other error - log and continue
|
||||
// This is acceptable as it's best-effort for offline checks
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.mrkayjaydee.playhours.permissions;
|
||||
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Main permission checking utility with LuckPerms soft integration.
|
||||
* Falls back to vanilla operator levels when LuckPerms is not present.
|
||||
*/
|
||||
public final class PermissionChecker {
|
||||
private PermissionChecker() {}
|
||||
|
||||
/**
|
||||
* Checks if a player has view permission.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if the player has view permission
|
||||
*/
|
||||
public static boolean hasView(ServerPlayer player) {
|
||||
return hasPermission(player, PermissionConstants.VIEW, PermissionConstants.VIEW_FALLBACK_LEVEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player has admin permission.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if the player has admin permission
|
||||
*/
|
||||
public static boolean hasAdmin(ServerPlayer player) {
|
||||
return hasPermission(player, PermissionConstants.ADMIN, PermissionConstants.ADMIN_FALLBACK_LEVEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player has exempt permission.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if the player has exempt permission
|
||||
*/
|
||||
public static boolean isExempt(ServerPlayer player) {
|
||||
return hasPermission(player, PermissionConstants.EXEMPT, PermissionConstants.ADMIN_FALLBACK_LEVEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an offline player has exempt permission by UUID.
|
||||
* Uses LuckPerms if available, with a timeout to prevent blocking indefinitely.
|
||||
*
|
||||
* @param uuid the player UUID
|
||||
* @return true if the player has exempt permission, false otherwise or on timeout
|
||||
*/
|
||||
public static boolean isExempt(UUID uuid) {
|
||||
if (LuckPermsIntegration.isAvailable()) {
|
||||
return LuckPermsIntegration.hasPermissionOffline(uuid, PermissionConstants.EXEMPT);
|
||||
}
|
||||
// Fallback: assume not exempt when offline or unavailable
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a permission for a player with fallback to vanilla operator levels.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @param permission the permission node
|
||||
* @param fallbackLevel the vanilla operator level to fall back to
|
||||
* @return true if the player has the permission
|
||||
*/
|
||||
public static boolean hasPermission(ServerPlayer player, String permission, int fallbackLevel) {
|
||||
if (LuckPermsIntegration.isAvailable()) {
|
||||
if (LuckPermsIntegration.hasPermission(player, permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return player.hasPermissions(fallbackLevel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.mrkayjaydee.playhours.permissions;
|
||||
|
||||
/**
|
||||
* Constants for permission nodes and timeouts.
|
||||
* Centralizes permission-related constants to avoid magic values.
|
||||
*/
|
||||
public final class PermissionConstants {
|
||||
private PermissionConstants() {}
|
||||
|
||||
// Permission nodes
|
||||
public static final String ADMIN = "playhours.admin";
|
||||
public static final String EXEMPT = "playhours.exempt";
|
||||
public static final String VIEW = "playhours.view";
|
||||
|
||||
// Timeouts and fallbacks
|
||||
public static final int LUCKPERMS_TIMEOUT_SECONDS = 2;
|
||||
public static final int ADMIN_FALLBACK_LEVEL = 2;
|
||||
public static final int VIEW_FALLBACK_LEVEL = 1;
|
||||
}
|
||||
69
src/main/java/com/mrkayjaydee/playhours/text/JsonParser.java
Normal file
69
src/main/java/com/mrkayjaydee/playhours/text/JsonParser.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package com.mrkayjaydee.playhours.text;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Simple JSON parser for flat string maps.
|
||||
* Handles basic JSON parsing for message bundles.
|
||||
*/
|
||||
public final class JsonParser {
|
||||
private JsonParser() {}
|
||||
|
||||
/**
|
||||
* Parses a simple JSON object containing string key-value pairs.
|
||||
*
|
||||
* @param json the JSON string to parse
|
||||
* @return a map of key-value pairs
|
||||
*/
|
||||
public static Map<String, String> parse(String json) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
String trimmed = json.trim();
|
||||
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return result;
|
||||
}
|
||||
|
||||
trimmed = trimmed.substring(1, trimmed.length() - 1).trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Split on commas not within quotes - simple approach assumes no escaped quotes or commas in values
|
||||
int i = 0;
|
||||
boolean inQuotes = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
java.util.List<String> parts = new java.util.ArrayList<>();
|
||||
|
||||
while (i < trimmed.length()) {
|
||||
char c = trimmed.charAt(i);
|
||||
if (c == '"') inQuotes = !inQuotes;
|
||||
if (c == ',' && !inQuotes) {
|
||||
parts.add(sb.toString());
|
||||
sb.setLength(0);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
sb.append(c);
|
||||
i++;
|
||||
}
|
||||
parts.add(sb.toString());
|
||||
|
||||
for (String part : parts) {
|
||||
String[] kv = part.split(":", 2);
|
||||
if (kv.length != 2) continue;
|
||||
String key = unquote(kv[0].trim());
|
||||
String value = unquote(kv[1].trim());
|
||||
result.put(key, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String unquote(String s) {
|
||||
if (s.startsWith("\"") && s.endsWith("\"")) {
|
||||
return s.substring(1, s.length() - 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.mrkayjaydee.playhours.text;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Handles loading of language files and locale management.
|
||||
* Separates locale loading logic from message formatting.
|
||||
*/
|
||||
public final class LocaleLoader {
|
||||
private LocaleLoader() {}
|
||||
|
||||
/**
|
||||
* Loads a language bundle from the classpath.
|
||||
*
|
||||
* @param locale the locale to load (e.g., "en_us", "fr_fr")
|
||||
* @return a map of message keys to translated strings
|
||||
*/
|
||||
public static Map<String, String> loadBundle(String locale) {
|
||||
String path = "assets/playhours/lang/" + locale + ".json";
|
||||
try (InputStream in = LocaleLoader.class.getClassLoader().getResourceAsStream(path)) {
|
||||
if (in == null) return getDefaultBundle();
|
||||
|
||||
String json = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
||||
.lines().collect(Collectors.joining("\n"));
|
||||
return JsonParser.parse(json);
|
||||
} catch (Exception e) {
|
||||
return getDefaultBundle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default message bundle with fallback messages.
|
||||
*
|
||||
* @return a map containing default English messages
|
||||
*/
|
||||
public static Map<String, String> getDefaultBundle() {
|
||||
Map<String, String> messages = new HashMap<>();
|
||||
messages.put("msg.access_denied", "Server closed. Next open: %openday% at %opentime%.");
|
||||
messages.put("msg.threshold_denied", "Server closing soon. Next open: %openday% at %opentime%.");
|
||||
messages.put("msg.warn", "Server closing in %minutes% minute%s% at %closetime%.");
|
||||
messages.put("msg.kick", "Server closed. Next open: %openday% at %opentime%.");
|
||||
messages.put("msg.force_open", "Hours overridden: FORCE_OPEN.");
|
||||
messages.put("msg.force_closed", "Hours overridden: FORCE_CLOSED.");
|
||||
messages.put("msg.status_line", "Mode: %mode%. %isopen%. Next close: %closetime%. Next open: %openday% at %opentime%.");
|
||||
messages.put("msg.status_open", "Server open");
|
||||
messages.put("msg.status_closed", "Server closed");
|
||||
messages.put("msg.countdown", "Closing in %seconds%s");
|
||||
|
||||
// Admin/command feedback messages
|
||||
messages.put("msg.config_not_ready", "PlayHours config not ready yet. Try again in a moment.");
|
||||
messages.put("msg.unexpected_error", "An unexpected error occurred. See server log.");
|
||||
messages.put("msg.config_reloaded", "PlayHours config reloaded.");
|
||||
messages.put("msg.invalid_time_range", "Invalid time range. Use: hh:mm AM-hh:mm PM");
|
||||
messages.put("msg.failed_clear_default_periods", "Failed to clear default periods.");
|
||||
messages.put("msg.settings_updated", "PlayHours settings updated.");
|
||||
|
||||
// Force mode display messages
|
||||
messages.put("msg.mode_normal", "Normal");
|
||||
messages.put("msg.mode_force_open", "Always Open");
|
||||
messages.put("msg.mode_force_closed", "Maintenance");
|
||||
|
||||
// Formatting and placeholder strings
|
||||
messages.put("msg.yes", "yes");
|
||||
messages.put("msg.no", "no");
|
||||
messages.put("msg.day_time_separator", " at ");
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.mrkayjaydee.playhours.text;
|
||||
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handles message formatting and placeholder replacement.
|
||||
* Separates formatting logic from message loading and locale management.
|
||||
*/
|
||||
public final class MessageFormatter {
|
||||
private MessageFormatter() {}
|
||||
|
||||
/**
|
||||
* Creates a formatted message component with placeholder replacement.
|
||||
*
|
||||
* @param template the message template with placeholders
|
||||
* @param tokens the token replacements
|
||||
* @return a formatted component
|
||||
*/
|
||||
public static Component formatMessage(String template, Map<String, String> tokens) {
|
||||
String formatted = applyTokens(template, tokens);
|
||||
return Component.literal(formatted).withStyle(ChatFormatting.YELLOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies token replacements to a message template.
|
||||
*
|
||||
* @param template the message template
|
||||
* @param tokens the token replacements
|
||||
* @return the formatted message string
|
||||
*/
|
||||
public static String applyTokens(String template, Map<String, String> tokens) {
|
||||
String result = template;
|
||||
for (Map.Entry<String, String> entry : tokens.entrySet()) {
|
||||
result = result.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is not blank and adds it to the map.
|
||||
*
|
||||
* @param map the map to add to
|
||||
* @param key the key
|
||||
* @param value the value to check
|
||||
*/
|
||||
public static void putIfNotBlank(Map<String, String> map, String key, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.mrkayjaydee.playhours.text;
|
||||
|
||||
/**
|
||||
* Constants for message keys used throughout the mod.
|
||||
* Centralizes message key definitions to avoid magic strings.
|
||||
*/
|
||||
public final class MessageKeys {
|
||||
private MessageKeys() {}
|
||||
|
||||
// Player messages
|
||||
public static final String ACCESS_DENIED = "msg.access_denied";
|
||||
public static final String THRESHOLD_DENIED = "msg.threshold_denied";
|
||||
public static final String WARN = "msg.warn";
|
||||
public static final String KICK = "msg.kick";
|
||||
public static final String FORCE_OPEN = "msg.force_open";
|
||||
public static final String FORCE_CLOSED = "msg.force_closed";
|
||||
public static final String STATUS_LINE = "msg.status_line";
|
||||
public static final String STATUS_OPEN = "msg.status_open";
|
||||
public static final String STATUS_CLOSED = "msg.status_closed";
|
||||
public static final String COUNTDOWN = "msg.countdown";
|
||||
|
||||
// Admin/command feedback messages
|
||||
public static final String CONFIG_NOT_READY = "msg.config_not_ready";
|
||||
public static final String UNEXPECTED_ERROR = "msg.unexpected_error";
|
||||
public static final String CONFIG_RELOADED = "msg.config_reloaded";
|
||||
public static final String INVALID_TIME_RANGE = "msg.invalid_time_range";
|
||||
public static final String FAILED_CLEAR_DEFAULT_PERIODS = "msg.failed_clear_default_periods";
|
||||
public static final String SETTINGS_UPDATED = "msg.settings_updated";
|
||||
|
||||
// MOTD messages
|
||||
public static final String MOTD_STATUS_OPEN = "msg.motd_status_open";
|
||||
public static final String MOTD_STATUS_CLOSED = "msg.motd_status_closed";
|
||||
public static final String MOTD_NEXT_OPEN = "msg.motd_next_open";
|
||||
public static final String MOTD_NEXT_CLOSE = "msg.motd_next_close";
|
||||
public static final String MOTD_COUNTDOWN = "msg.motd_countdown";
|
||||
public static final String MOTD_FORCE_OPEN = "msg.motd_force_open";
|
||||
public static final String MOTD_FORCE_CLOSED = "msg.motd_force_closed";
|
||||
|
||||
// Force mode display messages
|
||||
public static final String MODE_NORMAL = "msg.mode_normal";
|
||||
public static final String MODE_FORCE_OPEN = "msg.mode_force_open";
|
||||
public static final String MODE_FORCE_CLOSED = "msg.mode_force_closed";
|
||||
|
||||
// Formatting and placeholder strings
|
||||
public static final String YES = "msg.yes";
|
||||
public static final String NO = "msg.no";
|
||||
public static final String DAY_TIME_SEPARATOR = "msg.day_time_separator";
|
||||
}
|
||||
147
src/main/java/com/mrkayjaydee/playhours/text/Messages.java
Normal file
147
src/main/java/com/mrkayjaydee/playhours/text/Messages.java
Normal file
@@ -0,0 +1,147 @@
|
||||
package com.mrkayjaydee.playhours.text;
|
||||
|
||||
import com.mrkayjaydee.playhours.config.GeneralConfig;
|
||||
import com.mrkayjaydee.playhours.config.MessagesConfig;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Message system with locale support and config overrides.
|
||||
* Loads messages from lang files and allows per-message config overrides.
|
||||
*/
|
||||
public final class Messages {
|
||||
private Messages() {}
|
||||
|
||||
private static volatile String currentLocale = "en_us";
|
||||
private static volatile Map<String, String> bundle = new HashMap<>();
|
||||
// Cache of config overrides to avoid reading config at call sites before spec is ready
|
||||
private static volatile Map<String, String> overrides = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Reloads messages from config, setting the locale from ServerConfig.
|
||||
*/
|
||||
public static void reloadFromConfig() {
|
||||
String loc = GeneralConfig.MESSAGE_LOCALE.get();
|
||||
setLocale(loc == null || loc.isBlank() ? "en_us" : loc);
|
||||
// Snapshot overrides once per config reload
|
||||
Map<String, String> o = new HashMap<>();
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.ACCESS_DENIED, MessagesConfig.ACCESS_DENIED.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.THRESHOLD_DENIED, MessagesConfig.THRESHOLD_DENIED.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.WARN, MessagesConfig.WARN.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.KICK, MessagesConfig.KICK.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.FORCE_OPEN, MessagesConfig.FORCE_OPEN.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.FORCE_CLOSED, MessagesConfig.FORCE_CLOSED.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.STATUS_LINE, MessagesConfig.STATUS_LINE.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.STATUS_OPEN, MessagesConfig.STATUS_OPEN.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.STATUS_CLOSED, MessagesConfig.STATUS_CLOSED.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.COUNTDOWN, MessagesConfig.COUNTDOWN.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.CONFIG_NOT_READY, MessagesConfig.CONFIG_NOT_READY.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.UNEXPECTED_ERROR, MessagesConfig.UNEXPECTED_ERROR.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.CONFIG_RELOADED, MessagesConfig.CONFIG_RELOADED.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.INVALID_TIME_RANGE, MessagesConfig.INVALID_TIME_RANGE.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.FAILED_CLEAR_DEFAULT_PERIODS, MessagesConfig.FAILED_CLEAR_DEFAULT_PERIODS.get());
|
||||
MessageFormatter.putIfNotBlank(o, MessageKeys.SETTINGS_UPDATED, MessagesConfig.SETTINGS_UPDATED.get());
|
||||
overrides = o;
|
||||
}
|
||||
|
||||
public static void setLocale(String locale) {
|
||||
currentLocale = locale;
|
||||
bundle = LocaleLoader.loadBundle(locale);
|
||||
}
|
||||
|
||||
public static java.util.Locale getJavaLocale() {
|
||||
try {
|
||||
String[] parts = currentLocale.split("[_-]");
|
||||
if (parts.length == 1) return new java.util.Locale.Builder().setLanguage(parts[0]).build();
|
||||
if (parts.length >= 2) return new java.util.Locale.Builder().setLanguage(parts[0]).setRegion(parts[1].toUpperCase()).build();
|
||||
} catch (Throwable ignored) {}
|
||||
return java.util.Locale.ENGLISH;
|
||||
}
|
||||
|
||||
|
||||
public static Component accessDenied(String day, String time) {
|
||||
return fromKeyOrConfig(MessageKeys.ACCESS_DENIED, Map.of(
|
||||
"%openday%", day,
|
||||
"%opentime%", time
|
||||
));
|
||||
}
|
||||
|
||||
public static Component thresholdDenied(String day, String time) {
|
||||
return fromKeyOrConfig(MessageKeys.THRESHOLD_DENIED, Map.of(
|
||||
"%openday%", day,
|
||||
"%opentime%", time
|
||||
));
|
||||
}
|
||||
|
||||
public static Component warn(int minutes, String closeTime) {
|
||||
String s = minutes == 1 ? "" : "s";
|
||||
return fromKeyOrConfig(MessageKeys.WARN, Map.of(
|
||||
"%minutes%", String.valueOf(minutes),
|
||||
"%s%", s,
|
||||
"%closetime%", closeTime
|
||||
));
|
||||
}
|
||||
|
||||
public static Component kick(String day, String time) {
|
||||
return fromKeyOrConfig(MessageKeys.KICK, Map.of(
|
||||
"%openday%", day,
|
||||
"%opentime%", time
|
||||
));
|
||||
}
|
||||
|
||||
public static Component forceOpen() { return fromKeyOrConfig(MessageKeys.FORCE_OPEN, Map.of()); }
|
||||
public static Component forceClosed() { return fromKeyOrConfig(MessageKeys.FORCE_CLOSED, Map.of()); }
|
||||
|
||||
public static Component statusLine(String mode, boolean isOpen, String nextClose, String nextOpenDay, String nextOpenTime) {
|
||||
String statusText = isOpen ? translate(MessageKeys.STATUS_OPEN) : translate(MessageKeys.STATUS_CLOSED);
|
||||
return fromKeyOrConfig(MessageKeys.STATUS_LINE, Map.of(
|
||||
"%mode%", mode,
|
||||
"%isopen%", statusText,
|
||||
"%closetime%", nextClose,
|
||||
"%openday%", nextOpenDay,
|
||||
"%opentime%", nextOpenTime
|
||||
));
|
||||
}
|
||||
|
||||
public static Component countdown(int seconds) {
|
||||
return fromKeyOrConfig(MessageKeys.COUNTDOWN, Map.of(
|
||||
"%seconds%", String.valueOf(seconds)
|
||||
));
|
||||
}
|
||||
|
||||
// Admin/command feedback helpers
|
||||
public static Component configNotReady() { return fromKeyOrConfig(MessageKeys.CONFIG_NOT_READY, Map.of()); }
|
||||
public static Component unexpectedError() { return fromKeyOrConfig(MessageKeys.UNEXPECTED_ERROR, Map.of()); }
|
||||
public static Component configReloaded() { return fromKeyOrConfig(MessageKeys.CONFIG_RELOADED, Map.of()); }
|
||||
public static Component invalidTimeRange() { return fromKeyOrConfig(MessageKeys.INVALID_TIME_RANGE, Map.of()); }
|
||||
public static Component failedClearDefaultPeriods() { return fromKeyOrConfig(MessageKeys.FAILED_CLEAR_DEFAULT_PERIODS, Map.of()); }
|
||||
public static Component settingsUpdated() { return fromKeyOrConfig(MessageKeys.SETTINGS_UPDATED, Map.of()); }
|
||||
|
||||
private static Component fromKeyOrConfig(String key, Map<String, String> tokens) {
|
||||
String override = overrides.get(key);
|
||||
String base = override != null && !override.isBlank() ? override : translate(key);
|
||||
return MessageFormatter.formatMessage(base, tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to get a translated message string by key.
|
||||
* This is used for custom formatting where Component objects are not needed.
|
||||
*
|
||||
* @param key the message key (e.g., "msg.motd_status_open")
|
||||
* @return the translated string, or the key itself if not found
|
||||
*/
|
||||
public static String get(String key) {
|
||||
String override = overrides.get(key);
|
||||
if (override != null && !override.isBlank()) {
|
||||
return override;
|
||||
}
|
||||
return translate(key);
|
||||
}
|
||||
|
||||
private static String translate(String key) {
|
||||
return bundle.getOrDefault(key, LocaleLoader.getDefaultBundle().getOrDefault(key, key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ modLoader="javafml" #mandatory
|
||||
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
|
||||
loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
|
||||
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
|
||||
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
|
||||
license="${mod_license}"
|
||||
# A URL to refer people to when problems occur with this mod
|
||||
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
|
||||
@@ -26,20 +25,16 @@ displayName="${mod_name}" #mandatory
|
||||
# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/
|
||||
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
|
||||
# A URL for the "homepage" for this mod, displayed in the mod UI
|
||||
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
|
||||
#displayURL="https://example.invalid/" #optional
|
||||
# A file name (in the root of the mod JAR) containing a logo for display
|
||||
#logoFile="examplemod.png" #optional
|
||||
#logoFile="playhours.png" #optional
|
||||
# A text field displayed in the mod UI
|
||||
#credits="" #optional
|
||||
# A text field displayed in the mod UI
|
||||
authors="${mod_authors}" #optional
|
||||
# Display Test controls the display for your mod in the server connection screen
|
||||
# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
|
||||
# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
|
||||
# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
|
||||
# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
|
||||
# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
|
||||
#displayTest="MATCH_VERSION" # if nothing is specified, MATCH_VERSION is the default when clientSideOnly=false, otherwise IGNORE_ALL_VERSION when clientSideOnly=true (#optional)
|
||||
# For server-only mods, IGNORE_SERVER_VERSION avoids red X when client lacks the mod
|
||||
displayTest="IGNORE_SERVER_VERSION"
|
||||
|
||||
# The description text for the mod (multi line!) (#mandatory)
|
||||
description='''${mod_description}'''
|
||||
@@ -56,7 +51,7 @@ description='''${mod_description}'''
|
||||
# AFTER - This mod is loaded AFTER the dependency
|
||||
ordering="NONE"
|
||||
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
|
||||
side="BOTH"
|
||||
side="BOTH"
|
||||
# Here's another dependency
|
||||
[[dependencies.${mod_id}]]
|
||||
modId="minecraft"
|
||||
@@ -64,7 +59,7 @@ description='''${mod_description}'''
|
||||
# This version range declares a minimum of the current minecraft version up to but not including the next major version
|
||||
versionRange="${minecraft_version_range}"
|
||||
ordering="NONE"
|
||||
side="BOTH"
|
||||
side="BOTH"
|
||||
|
||||
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
|
||||
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
|
||||
|
||||
32
src/main/resources/assets/playhours/lang/en_us.json
Normal file
32
src/main/resources/assets/playhours/lang/en_us.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"msg.access_denied": "Server closed. Next open: %openday% at %opentime%.",
|
||||
"msg.threshold_denied": "Server closing soon. Next open: %openday% at %opentime%.",
|
||||
"msg.warn": "Server closing in %minutes% minute%s% at %closetime%.",
|
||||
"msg.kick": "Server closed. Next open: %openday% at %opentime%.",
|
||||
"msg.force_open": "Hours overridden: FORCE_OPEN.",
|
||||
"msg.force_closed": "Hours overridden: FORCE_CLOSED.",
|
||||
"msg.status_line": "Mode: %mode%. %isopen%. Next close: %closetime%. Next open: %openday% at %opentime%.",
|
||||
"msg.status_open": "Server open",
|
||||
"msg.status_closed": "Server closed",
|
||||
"msg.countdown": "Closing in %seconds%s",
|
||||
"msg.config_not_ready": "PlayHours config not ready yet. Try again in a moment.",
|
||||
"msg.unexpected_error": "An unexpected error occurred. See server log.",
|
||||
"msg.config_reloaded": "PlayHours config reloaded.",
|
||||
"msg.invalid_time_range": "Invalid time range. Use: hh:mm AM-hh:mm PM",
|
||||
"msg.failed_clear_default_periods": "Failed to clear default periods.",
|
||||
"msg.settings_updated": "PlayHours settings updated.",
|
||||
"msg.motd_status_open": "Open",
|
||||
"msg.motd_status_closed": "Closed",
|
||||
"msg.motd_next_open": "Opens %openday% at %opentime%",
|
||||
"msg.motd_next_close": "Closes at %closetime%",
|
||||
"msg.motd_countdown": "Closing in %minutes% min",
|
||||
"msg.motd_force_open": "Always Open",
|
||||
"msg.motd_force_closed": "Maintenance",
|
||||
"msg.mode_normal": "Normal",
|
||||
"msg.mode_force_open": "Always Open",
|
||||
"msg.mode_force_closed": "Maintenance",
|
||||
"msg.yes": "yes",
|
||||
"msg.no": "no",
|
||||
"msg.day_time_separator": " at "
|
||||
}
|
||||
|
||||
32
src/main/resources/assets/playhours/lang/fr_fr.json
Normal file
32
src/main/resources/assets/playhours/lang/fr_fr.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"msg.access_denied": "Serveur fermé. Prochaine ouverture : %openday% à %opentime%.",
|
||||
"msg.threshold_denied": "Fermeture imminente. Prochaine ouverture : %openday% à %opentime%.",
|
||||
"msg.warn": "Fermeture dans %minutes% minute%s% à %closetime%.",
|
||||
"msg.kick": "Serveur fermé. Prochaine ouverture : %openday% à %opentime%.",
|
||||
"msg.force_open": "Horaires forcés : FORCE_OPEN.",
|
||||
"msg.force_closed": "Horaires forcés : FORCE_CLOSED.",
|
||||
"msg.status_line": "Mode : %mode%. %isopen%. Prochaine fermeture : %closetime%. Prochaine ouverture : %openday% à %opentime%.",
|
||||
"msg.status_open": "Serveur ouvert",
|
||||
"msg.status_closed": "Serveur fermé",
|
||||
"msg.countdown": "Fermeture dans %seconds%s",
|
||||
"msg.config_not_ready": "La configuration PlayHours n'est pas encore prête. Réessayez dans un instant.",
|
||||
"msg.unexpected_error": "Une erreur inattendue s'est produite. Voir le journal du serveur.",
|
||||
"msg.config_reloaded": "Configuration PlayHours rechargée.",
|
||||
"msg.invalid_time_range": "Plage horaire invalide. Utilisez : hh:mm AM-hh:mm PM",
|
||||
"msg.failed_clear_default_periods": "Échec de l'effacement des périodes par défaut.",
|
||||
"msg.settings_updated": "Paramètres PlayHours mis à jour.",
|
||||
"msg.motd_status_open": "Ouvert",
|
||||
"msg.motd_status_closed": "Fermé",
|
||||
"msg.motd_next_open": "Ouverture %openday% à %opentime%",
|
||||
"msg.motd_next_close": "Fermeture à %closetime%",
|
||||
"msg.motd_countdown": "Fermeture dans %minutes% min",
|
||||
"msg.motd_force_open": "Toujours Ouvert",
|
||||
"msg.motd_force_closed": "Maintenance",
|
||||
"msg.mode_normal": "Normal",
|
||||
"msg.mode_force_open": "Toujours Ouvert",
|
||||
"msg.mode_force_closed": "Maintenance",
|
||||
"msg.yes": "oui",
|
||||
"msg.no": "non",
|
||||
"msg.day_time_separator": " à "
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"pack": {
|
||||
"description": {
|
||||
"text": "${mod_id} resources"
|
||||
"text": "${mod_name} resources"
|
||||
},
|
||||
"pack_format": 15
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user