195 Commits
v0.1 ... main

Author SHA1 Message Date
c0499c0b58 rework for 1.21, and it works! 2025-04-10 21:43:10 -07:00
abd0a47424 Concrete v0.16.0 and Gradle v8.1.1 2025-04-10 21:20:03 -07:00
099118e13f Remove unused imports. 2025-04-10 21:19:58 -07:00
772cc32099 Repair extension loading by placing them in common-plugin rather than foundation-shared. 2025-04-10 21:19:48 -07:00
6a05d5f29f Chaos utilities. 2023-04-04 21:13:27 -07:00
139743a4ba Implement chunk inversion system. 2023-04-03 01:01:28 -07:00
56886a24e1 Implement chaos module for rotating chunks when a player enters. 2023-04-02 01:51:26 -07:00
83ccc31222 kbity -> kitty 2023-03-31 14:37:57 -07:00
df5787e5b7 Implement optional automatic update mechanism. 2023-03-31 14:03:43 -07:00
b7ce799593 Pair programming with @Nanokie on a world swapper plugin :D 2023-03-30 21:30:21 -07:00
53bdc3a4cb tailscale: upgrade for reflective I/O support. 2023-03-30 17:01:33 -07:00
218fda5d4c Add option to use /proc/self/fd for Tailscale connections. 2023-03-28 20:12:30 -07:00
b84da6d1eb Tailscale Proxy Fix 2023-03-28 19:53:58 -07:00
2262ceb1d1 Implement usage of the new Tailscale channel library. 2023-03-26 21:21:04 -07:00
02a5d02dad Fix build of PersistentStoreCommand 2023-03-24 22:50:35 -07:00
2d90f294c8 Fix bugs in the usage of koin. 2023-03-24 22:31:41 -07:00
c036aaf61a Support for new manifest format in install.sh 2023-03-19 18:19:55 -07:00
a043e0852f Implement common configuration loading mechanism. 2023-03-19 16:35:09 -07:00
59fbea0a37 Make Tailscale work properly. 2023-03-19 15:56:25 -07:00
1035c83166 Tailscale Plugin :D 2023-03-19 01:44:06 -07:00
01a520777e Support for Concrete v0.15.0 that allows extended items. 2023-03-16 17:52:08 -07:00
3e50eb01a9 Update Service: Only consider bukkit-plugin items during update. 2023-03-15 21:45:56 -07:00
90690666c5 Implement extensible manifest for updates in Foundation. 2023-03-13 21:01:26 -07:00
58aa162aa3 Update Concrete to support the new extensible update manifest format. 2023-03-13 16:31:52 -07:00
681c984b0a Gradle v8.0.2 2023-03-05 17:35:22 -08:00
3d4862adc0 More stuff. 2023-03-05 17:33:54 -08:00
c973a1a3c6 README: Explain the differences between the common libraries. 2023-02-20 13:16:35 -08:00
92d6ccb431 Upgrade to Gradle v8.0.1 2023-02-19 21:30:04 -08:00
54454ca01f Upgrade to Gradle v8.0 2023-02-15 21:51:30 -08:00
ef822f9217 heimdall: implement precise block change collector 2023-02-09 03:44:43 -05:00
eaa3888821 Minor refactoring and cleanup. 2023-02-09 00:41:10 -05:00
e0823f7b15 Rewrite Heimdall block handling to support more event types and non-player block changes. 2023-02-07 23:41:22 -05:00
688106a6e6 Heimdall: Keep track of block data. 2023-02-07 20:36:30 -05:00
760b77364a Reform Heimdall to move event collectors to the event class themselves. 2023-02-07 19:51:25 -05:00
0fe76a73f9 WorldLoadFormat fix. 2023-02-07 19:15:31 -05:00
fdc7976586 Concrete v0.11.0 2023-02-07 18:28:25 -05:00
0224e48ec8 Supress DSL scope violation in build.gradle.kts 2023-02-07 18:18:10 -05:00
ac0a0b2bed Use dependency catalogs, and add support for using local concrete. 2023-02-07 14:16:17 -05:00
3f67e737c4 Move persistence to plugin shared. 2023-02-07 12:49:47 -05:00
5f9f6e5fa7 Fix issue in Gjallarhorn with world selection. 2023-02-07 09:47:19 -05:00
e8084d7283 Implement world reassembly from a Heimdall backup. 2023-02-07 09:01:43 -05:00
192c6cb511 Upgrade to Concrete v0.10.0 2023-02-07 06:44:53 -05:00
8ee6447c9b Reform dependencies. 2023-02-07 05:05:26 -05:00
d335a0b63f Reform dependency structure. 2023-02-07 04:52:54 -05:00
eb587dc299 Heimdall cleanup and refactor. 2023-02-07 03:51:42 -05:00
5e9ceebc53 Move AdvancementTitleCache to common-plugin 2023-02-05 21:54:03 -08:00
233c601595 Dependencies upgrade and upgrade to Kotlin 1.8.10 2023-02-05 21:49:53 -08:00
ee6d3b37c0 Versions Gradle plugin for dependency reports. 2023-02-05 21:25:10 -08:00
1b7a481d9d Archive tools as well. 2023-02-05 19:47:54 -08:00
f96948beb5 Add annotation for marking plugin main class. 2023-02-05 19:37:59 -08:00
2e05aef95c Cleanup heimdall code. 2023-02-05 19:34:21 -08:00
7d040c8dd8 Fix artifact names. 2023-02-05 19:18:17 -08:00
634e486e2e Upload Artifacts 2023-02-05 19:14:43 -08:00
c77e975a31 Add a script to ensure new lines exist in all files. 2023-02-05 19:08:13 -08:00
6803a456b3 Move some code to common-plugin. 2023-02-05 19:04:38 -08:00
ef6daa8467 /megatnt command 2023-02-04 22:59:00 -08:00
495601d085 Release build numbers and resilience in the update service. 2023-02-03 13:20:21 -08:00
0995e8813e Fix install script and mark it as working. 2023-02-03 12:56:23 -08:00
b9da64cbd1 Update default update URL to https://artifacts.gay.pizza/foundation 2023-02-03 12:52:36 -08:00
fdeff648f4 kbity 2023-02-02 23:16:37 -08:00
cbe647cce9 chaos: mega-tnt and chunk exporter fixes 2023-01-30 15:01:39 -08:00
452ec0d7da Fix code warings. 2023-01-28 22:11:57 -08:00
b653c179b7 Fix main classes and authors. 2023-01-28 22:09:07 -08:00
758ac2c7bc Build Workflow 2023-01-28 21:33:21 -08:00
811dc1f2bf Fix Backblaze B2 Uploader 2023-01-28 21:26:20 -08:00
22355c3847 Fix JDA library version 2023-01-28 21:16:13 -08:00
404d01c649 Repair GitHub Release Workflow 2023-01-28 21:09:40 -08:00
20a6359d5d Cleanup shell script: tools/organize-artifacts.sh 2023-01-28 20:49:42 -08:00
85b567f399 Fix Concrete usage. 2023-01-28 19:38:57 -08:00
7289e5cb9f Heimdall: It's back! 2023-01-28 19:35:10 -08:00
086f7dba10 chaos: boss bar 2023-01-28 19:01:20 -08:00
6c16884ae2 chaos: selection controller 2023-01-28 14:05:29 -08:00
d762bbc056 Fix newlines! 2023-01-28 00:21:49 -08:00
2119b89ae2 Chaos Plugin 2023-01-28 00:21:14 -08:00
c39c5a99d5 Ensure newlines in all files. 2023-01-27 22:49:29 -08:00
e5822db210 Upgrade to Concrete v0.7.0 2023-01-27 22:47:34 -08:00
d868f9c635 Typo'd the action name. 2023-01-26 21:30:13 -08:00
36b9da5cf8 Use GPS fork of s3 action. 2023-01-26 21:28:55 -08:00
59839cbbac Initial pass on release workflow. 2023-01-26 21:03:00 -08:00
cec3b1297a Remove heimdall and tool project. 2023-01-26 20:36:48 -08:00
cf2a812b75 Initial port to Concrete. 2023-01-26 09:08:48 -08:00
b650e9f8a7 Update Gradle wrapper. 2023-01-24 23:56:07 -08:00
83ae7df4a6 Initial renaming pass. 2023-01-24 21:37:24 -08:00
5d7bf94e5c Update to new domains. 2022-05-30 17:07:10 -07:00
89ad6d1ece Update to paper-api 1.18.2. 2022-03-06 03:21:00 +00:00
8289616762 Gjallarhorn: Graphical render session fix for frame not closing. 2022-02-25 00:17:14 -05:00
c9d4fe1733 Heimdall/Gjallarhorn: Minor changes to idle timeout. 2022-02-21 20:38:15 -05:00
e3d9eb80fc Heimdall: Actually set max lifetime and pool size. 2022-02-21 19:45:26 -05:00
a184d2e845 Heimdall: Set DB pool max lifetime lower to prevent stale connections. 2022-02-21 19:42:54 -05:00
6e1afb5e5c Heimdall: Fix bug where DB being disabled might cause errors. 2022-02-21 19:30:27 -05:00
1879df780b Refactor Manifest Generation into Gradle Plugin 2022-02-21 18:09:52 -05:00
f0c344ca1f Gjallarhorn: Implement combined chunk format for storing many chunks in one map. 2022-02-20 06:11:14 -05:00
6bcddb15b5 Gjallarhorn: First attempt at a graphical render system. 2022-02-20 05:35:47 -05:00
1afb1c7148 Gjallarhorn Improvements 2022-02-20 04:03:21 -05:00
86800e59f4 Heimdall/Gjallarhorn: Chunk Export Improvements and Chunk Export Renderer 2022-02-17 21:37:38 -05:00
ac2e99052d Implement Chunk Export 2022-02-16 23:48:51 -05:00
eb5cb1a229 Gjallarhorn: Attempt to clarify the mess that is ChangelogSlice. 2022-02-16 00:56:48 -05:00
74fed8c222 Gjallarhorn: Implement render loop. 2022-01-29 23:15:05 -05:00
ba18fcddbc Add anti-idle feature (Closes #21). 2022-01-29 06:02:15 +00:00
0da3202555 Add config defaults to prevent deserialize errors. 2022-01-29 04:57:01 +00:00
66ee0ba701 Persist all entities that have been leashed. 2022-01-24 17:22:46 -08:00
d4a06ea84a Gjallarhorn: Initial Player Position Code 2022-01-17 22:24:47 -05:00
54cd41e925 Gjallarhorn: Render pool rework and cleanup. 2022-01-17 21:07:40 -05:00
011e3100bf Bifrost: Don't announce uninteresting advancements. 2022-01-17 17:36:58 -05:00
9395f43e40 Bifrost: Implement player advancement notifications. Oh my god this was hard and it still is ugly. 2022-01-17 17:19:12 -05:00
d16b9b1138 Bifrost: Fix player death messages. 2022-01-16 20:37:24 -05:00
4ca241aa5b Add allowLeads gameplay feature to allow leads on all mobs. 2022-01-16 14:04:21 -08:00
25c72d1ce3 Bifrost: Fix removal of sendPlayerDeath in default config. 2022-01-16 16:38:53 -05:00
3115990352 Bifrost: Debug mode, air-gap development mode, and fix chat message encoding. 2022-01-16 16:37:57 -05:00
ea83ce5853 Add missing sendPlayerDeath to Bifrost config template. 2022-01-16 13:15:26 -08:00
2bfa39c6a2 Fix download for setupPaperServer. 2022-01-16 13:11:36 -08:00
cd518c6928 Bifrost: Add simple player death notifications. 2022-01-15 23:24:35 -05:00
9398ada817 Small amount of inspection cleanup. 2022-01-15 16:21:38 -05:00
93d1888537 Gradle: runPaperServer should read Main-Class from manifest. 2022-01-15 16:15:11 -05:00
ef13c2371c Core: Backup should use a 16KB buffer. 2022-01-15 16:10:23 -05:00
0a08436088 Core: Backup cleanup and fixes for Windows. 2022-01-15 15:08:22 -05:00
0d2e454941 Gradle: Utilize Gradle plugin creation DSL. 2022-01-15 14:43:24 -05:00
e9548c5a3d Heimdall: Implement Player Position Compression 2022-01-14 23:57:16 -05:00
9d156d250b Gradle: Implement smart downloads which avoid download if the file exists and is valid. 2022-01-14 20:36:15 -05:00
8f34209aff Gradle: Implement --update option for setupPaperServer, and add runPaperServer 2022-01-13 23:19:07 -05:00
01999eadd7 Gradle: Implement setupPaperServer action which downloads Paper and links plugin JARs. 2022-01-13 18:25:46 -05:00
763b61ba04 Add gameplay feature. 2022-01-13 06:02:21 +00:00
71f0b46728 Core: Add Enderman Griefing Disabler 2022-01-12 23:00:03 -05:00
4187b0f50c Add missing alias and try to improve tab completion. 2022-01-10 00:04:08 -08:00
203ecd1ca9 Blindly wrote a command. 2022-01-09 23:46:43 -08:00
3ac24f6912 Gjallarhorn: Implement trimming at the changelog level, resulting in really fast renderings. 2022-01-10 02:13:13 -05:00
dcec7cab54 Gjallarhorn: Use 3-byte RGB for images, and improve timelapse text. 2022-01-10 01:56:56 -05:00
c1a07f1001 Update GitLab path. 2022-01-09 16:23:18 -08:00
2d429ae04d Gjallarhorn: Block Color Key and Render Pool Enhancements 2022-01-09 04:03:07 -05:00
94d644916b Gjallarhorn: Dynamic Timelapse Slices 2022-01-08 23:17:59 -05:00
7a5a27d581 Gjallarhorn: Various fixes to new pipeline for production usage. 2022-01-08 22:00:59 -05:00
3f06845ac4 Gjallarhorn: Use Player Position Changelog in Player Position Export 2022-01-08 16:34:13 -05:00
a81b160675 Gjallarhorn: Reuse 2D graphics when building individual images. 2022-01-08 15:34:58 -05:00
0a96435669 Gjallarhorn: Render pool should ignore playback segments which are empty. 2022-01-08 15:28:41 -05:00
41547f2e14 Gjallarhorn: Rename block-changes command to block-change-timelapse 2022-01-08 15:25:10 -05:00
8caf3de634 Gjallarhorn: Implement block state global cache.
This reduces memory usage by reusing block state objects.
2022-01-08 15:04:16 -05:00
81a76da809 Gjallarhorn: Create render pool concept. 2022-01-08 14:57:56 -05:00
3350034060 Gjallarhorn: Refactor Color Gradient 2022-01-08 14:10:30 -05:00
8ea1ea1540 Gjallarhorn: Start support for player position changelogs and rename replay-block-log to block-changes 2022-01-08 02:40:09 -05:00
d54f434805 Gjallarhorn: Changelog state tracker should use global air block. 2022-01-08 02:23:14 -05:00
643567dfb5 Gjallarhorn: Introduce concept of block changelogs, which makes timelapse rendering more efficient. 2022-01-08 02:21:42 -05:00
08ba582931 Gjallarhorn: Refactor Rendering Code 2022-01-08 01:58:31 -05:00
9f8d417e5d Gjallarhorn: Fifteen Minute Timelapse Support 2022-01-08 01:32:47 -05:00
86f82692b4 Fix issue that prevented plugins from being published as artifacts. 2022-01-08 01:08:42 +00:00
927abe54b6 Update to Gradle v7.3.3. 2022-01-07 23:27:47 +00:00
a0669f815b Gjallarhorn: Timelapse Limiting and Coordinate Cropping 2022-01-07 08:29:41 -05:00
10cf0cadac Gjallarhorn: Parallel Rendering and Quad Image Improvements 2022-01-07 07:29:04 -05:00
bc2d3e28ae Gjallarhorn: Timelapse Mode 2022-01-07 06:15:26 -05:00
cc6fbaae83 Gjallarhorn: Heat Map Support 2022-01-04 02:19:24 -05:00
06eda8932a Add backup file ignore list. 2021-12-28 09:41:28 +00:00
e681df1e65 Heimdall: Player Names Table, Gjallarhorn: Block State Image Rendering 2021-12-27 23:56:20 -05:00
9386dc7c56 Adjust cron expression and config comments. 2021-12-27 21:08:43 +00:00
ff665c27f5 Initial Commit of Gjallarhorn: A Heimdall Analytics Tool 2021-12-26 03:33:23 -05:00
cbbefc94a2 Heimdall: Log event count at debug log level. 2021-12-24 19:41:09 -05:00
d7f094f765 Heimdall: Implement Entity Kill Tracking 2021-12-24 19:04:03 -05:00
e10fa42c68 Adjust cron expression and config comments. 2021-12-24 22:10:48 +00:00
767faba8d8 Heimdall: Implement Player Death and Player Advancement Tracking 2021-12-24 04:10:33 -05:00
c1f621aa7b Initial work on scheduled backups. 2021-12-24 08:38:57 +00:00
b2851d13b9 Heimdall: Implement world change events. 2021-12-24 03:32:07 -05:00
4017c3cb8c Heimdall: Add id column to player session tracking. 2021-12-24 02:49:53 -05:00
1985b3c507 Heimdall: Player Session Tracking 2021-12-24 02:42:13 -05:00
139249c1de Core: Properly register leaderboard under lb as well. 2021-12-24 02:02:04 -05:00
e00ef21db1 Core: Add back leaderboard command. 2021-12-24 02:00:16 -05:00
fae116a2a5 Heimdall: Correct timestamp resolution of all events. 2021-12-24 01:41:14 -05:00
9952c4c427 Heimdall: Block Place and Break Tracking 2021-12-24 01:32:40 -05:00
847f46273b Update README. 2021-12-24 06:12:24 +00:00
e0183127b4 Initial Rough Cut of Heimdall Tracking System 2021-12-24 00:08:38 -05:00
78566d08ad Refactor persistence into it's own feature. 2021-12-23 21:26:10 -05:00
fca1db8802 Add S3 support to backups, fixes #7. 2021-12-24 00:43:44 +00:00
da820b8a0d Disable Bifrost onDisable if plugin was not initialized. 2021-12-23 23:08:02 +00:00
2c98cacf96 Reorganize feature + module init to fix bug. 2021-12-23 23:05:29 +00:00
c854e7c47c Finalize package organization. 2021-12-23 22:51:42 +00:00
ec7810a11a Not sure how this happened. 2021-12-23 22:47:10 +00:00
13479b1ae3 Major refactoring to use Koin. 2021-12-23 22:44:17 +00:00
f8178c2307 DevUpdateServer: Change HTTP server stop delay to one second. 2021-12-23 02:55:51 -05:00
7f9bd32cc7 DevUpdateServer: Minor logging changes. 2021-12-23 02:51:06 -05:00
a7d7c9f818 DevUpdateServer: Properly handle update in callback. 2021-12-23 02:47:32 -05:00
4284791804 DevUpdateServer: Simple code cleanup change. 2021-12-23 02:45:53 -05:00
ad8c82725b leaderboard: improved format and more leaderboards 2021-12-23 02:38:59 -05:00
4e066d8f11 leaderboard: add tab completion 2021-12-23 02:32:14 -05:00
76019a62fc DevUpdateServer: Improve update code. 2021-12-23 02:22:12 -05:00
552ef608d9 Break off update plugins code into separate object. 2021-12-23 07:17:48 +00:00
46ba0a4a44 Opt-into ExperimentalSerializationApi. 2021-12-23 07:02:31 +00:00
e3402505fd DevServer: Parse pipeline payload for filtering. 2021-12-23 01:51:27 -05:00
f7e19b1509 Add /setspawn and /spawn. 2021-12-23 06:32:12 +00:00
7a30d066ac If token is empty (the default now), Bifrost won't do anything.
- Fixes #6.
2021-12-23 06:31:46 +00:00
139ce551dc DevUpdate Server for Test Server Updates 2021-12-22 23:45:41 -05:00
795e99ad4f Add toggles for chat bridge. 2021-12-23 04:10:40 +00:00
b8c8097f58 Add toggles for start, shutdown, player join and quit events. 2021-12-23 03:44:16 +00:00
4439fe74a6 Delete println that I stupidly left in tab completion for persistent store.
I' a println debugger and I'm PROUD https://youtu.be/-N0yXGVWS1Y
2021-12-22 22:22:02 -05:00
ecdb6a2898 Persistent Store Command Tweaks
- delete-all-entities command.
- Basic tab completion.
2021-12-22 22:06:34 -05:00
b91602d719 Add installation instructions to README. 2021-12-23 03:02:40 +00:00
b32b8efc84 Persistent store fixes and /pstore command
- Fixes find() and getAll() to run in a transaction.
- Add /pstore command which has debug utilities.
2021-12-22 21:37:01 -05:00
fb1fd1a6e5 Forgot to add mkdir after reworking logic. 2021-12-23 02:25:06 +00:00
6b4bd2a987 Bump version to v0.2. 2021-12-23 02:23:26 +00:00
220 changed files with 6563 additions and 1139 deletions

25
.github/workflows-disabled/build.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build check
- name: Organize Artifacts
run: ./tools/organize-artifacts.sh
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: foundation-build
path: |
artifacts/*

36
.github/workflows-disabled/release.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Release
on:
push:
branches:
- main
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build
env:
CONCRETE_BUILD_NUMBER: "${{ github.run_number }}"
- name: Organize Artifacts
run: ./tools/organize-artifacts.sh
- name: Upload to Backblaze
run: ./tools/gh-upload-backblaze.sh
env:
ARTIFACTS_KEY_ID: "${{ secrets.ARTIFACTS_KEY_ID }}"
ARTIFACTS_APP_KEY: "${{ secrets.ARTIFACTS_APP_KEY }}"
ARTIFACTS_BUCKET: "${{ secrets.ARTIFACTS_BUCKET }}"
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: foundation-build
path: |
artifacts/*

7
.gitignore vendored
View File

@ -116,3 +116,10 @@ run/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Foundation Server
/server
# Foundation build
/.concrete-local-path
/artifacts

View File

@ -1,18 +0,0 @@
image: gradle:7.3-jdk17
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
build:
stage: build
script: gradle --build-cache assemble
cache:
key: "$CI_COMMIT_REF_NAME"
policy: push
paths:
- build
- .gradle
artifacts:
paths:
- "build/manifests/update.json"
- "**/build/libs/*-plugin.jar"

215
LICENSE
View File

@ -1,202 +1,21 @@
MIT License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2023 Gay Pizza Specifications
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Definitions.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,7 +1,34 @@
# Foundation
Foundation is a set of plugins that implement the core functionality for a small community Minecraft
server.
## Plugins
* foundation-core - Core functionality
* foundation-bifrost - Discord chat bridge
* foundation-core: Core functionality
* foundation-bifrost: Discord chat bridge
* foundation-chaos: Simulate chaos inside a minecraft world
* foundation-heimdall: Event tracking
* foundation-tailscale: Connect the Minecraft Server to Tailscale
## Tools
* tool-gjallarhorn - Heimdall swiss army knife
## Libraries
* common-all: Common code for every Foundation module.
* common-plugin: Common code for every Foundation plugin. Included directly in the plugin jar.
* common-heimdall: Common code for Heimdall modules.
* foundation-shared: Common code for every Foundation plugin. Linked dynamically from Foundation Core.
## Installation
The following command downloads and runs a script that will fetch the latest update manifest, and
install all plugins available. It can also be used to update plugins to the latest version
available.
```bash
# Always validate the contents of a script from the internet!
bash -c "$(curl -sL https://github.com/GayPizzaSpecifications/foundation/raw/main/install.sh)"
```

View File

@ -1,24 +1,21 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.com.google.gson.Gson
import java.io.FileWriter
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
java
id("org.jetbrains.kotlin.jvm") version "1.6.10" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" apply false
id("com.github.johnrengelman.shadow") version "7.1.1" apply false
}
// Disable the JAR task for the root project.
tasks["jar"].enabled = false
alias(libs.plugins.concrete.root)
alias(libs.plugins.concrete.base) apply false
alias(libs.plugins.concrete.library) apply false
alias(libs.plugins.concrete.plugin) apply false
alias(libs.plugins.versions)
}
allprojects {
repositories {
mavenCentral()
maven {
name = "papermc-repo"
url = uri("https://papermc.io/repo/repository/maven-public/")
}
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/content/groups/public/")
@ -26,93 +23,23 @@ allprojects {
}
}
val manifestsDir = buildDir.resolve("manifests")
manifestsDir.mkdirs()
val gson = Gson()
tasks.create("updateManifests") {
// TODO: not using task dependencies, outputs, blah blah blah.
doLast {
val updateFile = manifestsDir.resolve("update.json")
val writer = FileWriter(updateFile)
writer.use {
val rootPath = rootProject.rootDir.toPath()
val updateManifest = subprojects.mapNotNull { project ->
val files = project.tasks.getByName("shadowJar").outputs
val paths = files.files.map { rootPath.relativize(it.toPath()).toString() }
if (paths.isNotEmpty()) project.name to mapOf(
"version" to project.version,
"artifacts" to paths,
)
else null
}.toMap()
gson.toJson(
updateManifest,
writer
)
}
}
}
tasks.assemble {
dependsOn("updateManifests")
}
version = "0.2"
subprojects {
plugins.apply("org.jetbrains.kotlin.jvm")
plugins.apply("org.jetbrains.kotlin.plugin.serialization")
plugins.apply("com.github.johnrengelman.shadow")
group = "gay.pizza.foundation"
version = "0.1"
group = "io.gorence"
// Add build number if running under CI.
val versionWithBuild = if (System.getenv("CI_PIPELINE_IID") != null) {
version as String + ".${System.getenv("CI_PIPELINE_IID")}"
} else {
"DEV"
}
version = versionWithBuild
dependencies {
// Kotlin dependencies
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
// Serialization
implementation("com.charleskorn.kaml:kaml:0.38.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
// Persistence
implementation("org.jetbrains.xodus:xodus-openAPI:1.3.232")
implementation("org.jetbrains.xodus:xodus-entity-store:1.3.232")
// Paper API
compileOnly("io.papermc.paper:paper-api:1.18.1-R0.1-SNAPSHOT")
}
java {
val javaVersion = JavaVersion.toVersion(17)
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
tasks.processResources {
val props = mapOf("version" to version)
inputs.properties(props)
filteringCharset = "UTF-8"
filesMatching("plugin.yml") {
expand(props)
tasks.withType<KotlinCompile> {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi")
}
}
tasks.withType<ShadowJar> {
archiveClassifier.set("plugin")
}
tasks.assemble {
dependsOn("shadowJar")
}
}
val paperServerVersion: String = project.properties["paperServerVersion"]?.toString() ?: "1.21"
concreteRoot {
minecraftServerPath.set("server")
paperServerVersionGroup.set(paperServerVersion)
paperApiVersion.set("1.21.4-R0.1-SNAPSHOT")
acceptServerEula.set(true)
}

View File

@ -0,0 +1,9 @@
plugins {
id("gay.pizza.foundation.concrete-base")
}
dependencies {
// Serialization
api(libs.kotlin.serialization.json)
api(libs.kotlin.serialization.yaml)
}

View File

@ -0,0 +1,3 @@
package gay.pizza.foundation.common
fun <T> Array<T>.without(value: T): List<T> = filter { it != value }

View File

@ -0,0 +1,11 @@
plugins {
id("gay.pizza.foundation.concrete-base")
}
dependencies {
api(project(":common-all"))
api(libs.postgresql)
api(libs.exposed.jdbc)
api(libs.exposed.java.time)
api(libs.hikaricp)
}

View File

@ -0,0 +1,9 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportedBlock(
val type: String,
val data: String? = null
)

View File

@ -0,0 +1,11 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportedChunk(
val blocks: List<ExportedBlock>,
val x: Int,
val z: Int,
val sections: List<ExportedChunkSection>
)

View File

@ -0,0 +1,10 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportedChunkSection(
val x: Int,
val z: Int,
val blocks: List<Int>
)

View File

@ -0,0 +1,19 @@
package gay.pizza.foundation.heimdall.load
import gay.pizza.foundation.heimdall.export.ExportedBlock
class ExportedBlockTable {
private val internalBlocks = mutableListOf<ExportedBlock>()
val blocks: List<ExportedBlock>
get() = internalBlocks
fun index(block: ExportedBlock): Int {
val existing = internalBlocks.indexOf(block)
if (existing >= 0) {
return existing
}
internalBlocks.add(block)
return internalBlocks.size - 1
}
}

View File

@ -0,0 +1,69 @@
package gay.pizza.foundation.heimdall.load
import kotlinx.serialization.Serializable
import kotlin.math.absoluteValue
@Serializable
data class OffsetList<L: List<T>, T>(
val offset: Int,
val data: L
) {
fun <K> toMap(toKey: (Int) -> K): Map<K, T> {
val map = mutableMapOf<K, T>()
for ((index, value) in data.withIndex()) {
val real = index + offset
val key = toKey(real)
map[key] = value
}
return map
}
fun <R> map(value: (T) -> R): ImmutableOffsetList<R> =
ImmutableOffsetList(offset, MutableList(data.size) { index -> value(data[index]) })
fun eachRealIndex(block: (Int, T) -> Unit) {
for ((fakeIndex, value) in data.withIndex()) {
val realIndex = fakeIndex + offset
block(realIndex, value)
}
}
companion object {
fun <K, T, V> transform(
map: Map<K, T>,
minAndTotal: (Map<K, T>) -> Pair<Int, Int>,
keyToInt: (K) -> Int,
valueTransform: (T) -> V
): ImmutableOffsetList<V?> {
val (min, total) = minAndTotal(map)
val offset = if (min < 0) min.absoluteValue else 0
val list = MutableList<V?>(total) { null }
for ((key, value) in map) {
val pkey = keyToInt(key)
val rkey = pkey + offset
list[rkey] = valueTransform(value)
}
return OffsetList(if (min < 0) min else 0, list)
}
}
}
typealias ImmutableOffsetList<T> = OffsetList<List<T>, T>
@Serializable
class WorldLoadCompactWorld(
override val name: String,
val data: ImmutableOffsetList<ImmutableOffsetList<ImmutableOffsetList<Int?>?>?>
) : WorldLoadWorld() {
override fun crawl(block: (Long, Long, Long, Int) -> Unit) {
data.eachRealIndex { x, zList ->
zList?.eachRealIndex { z, yList ->
yList?.eachRealIndex { y, index ->
if (index != null) {
block(x.toLong(), z.toLong(), y.toLong(), index)
}
}
}
}
}
}

View File

@ -0,0 +1,10 @@
package gay.pizza.foundation.heimdall.load
import gay.pizza.foundation.heimdall.export.ExportedBlock
import kotlinx.serialization.Serializable
@Serializable
class WorldLoadFormat(
val blockLookupTable: List<ExportedBlock>,
val worlds: Map<String, WorldLoadWorld>
)

View File

@ -0,0 +1,64 @@
package gay.pizza.foundation.heimdall.load
import kotlinx.serialization.Serializable
import kotlin.math.absoluteValue
@Serializable
class WorldLoadSimpleWorld(
override val name: String,
val blocks: Map<Long, Map<Long, Map<Long, Int>>>
) : WorldLoadWorld() {
fun compact(): WorldLoadCompactWorld {
val list = OffsetList.transform(
blocks,
minAndTotal = ::minAndTotal,
keyToInt = Long::toInt,
valueTransform = { zValue ->
OffsetList.transform(
zValue,
minAndTotal = ::minAndTotal,
keyToInt = Long::toInt,
valueTransform = { yValue ->
OffsetList.transform(
yValue,
minAndTotal = ::minAndTotal,
keyToInt = Long::toInt,
valueTransform = { it }
)
}
)
})
return WorldLoadCompactWorld(name, list)
}
private fun <T> minAndTotal(map: Map<Long, T>): Pair<Int, Int> {
val keys = map.keys
if (keys.isEmpty()) {
return 0 to 0
}
val min = keys.min()
val max = keys.max()
var total = 1L
if (max > 0) {
total += max
}
if (min < 0) {
total += min.absoluteValue
}
return min.toInt() to total.toInt()
}
override fun crawl(block: (Long, Long, Long, Int) -> Unit) {
for ((x, zBlocks) in blocks) {
for ((z, yBlocks) in zBlocks) {
for ((y, index) in yBlocks) {
block(x, z, y, index)
}
}
}
}
}

View File

@ -0,0 +1,10 @@
package gay.pizza.foundation.heimdall.load
import kotlinx.serialization.Serializable
@Serializable
sealed class WorldLoadWorld {
abstract val name: String
abstract fun crawl(block: (Long, Long, Long, Int) -> Unit)
}

View File

@ -0,0 +1,8 @@
package gay.pizza.foundation.heimdall.table
object BlockChangeTable : PlayerTimedLocalEventTable("block_changes") {
val block = text("block")
val data = text("data")
val cause = text("cause")
val inc = integer("inc")
}

View File

@ -0,0 +1,6 @@
package gay.pizza.foundation.heimdall.table
object EntityKillTable : PlayerTimedLocalEventTable("entity_kills") {
val entity = uuid("entity")
val entityType = text("entity_type")
}

View File

@ -0,0 +1,5 @@
package gay.pizza.foundation.heimdall.table
object PlayerAdvancementTable : PlayerTimedLocalEventTable("player_advancements") {
val advancement = text("advancement")
}

View File

@ -0,0 +1,6 @@
package gay.pizza.foundation.heimdall.table
object PlayerDeathTable : PlayerTimedLocalEventTable("player_deaths") {
val experience = double("experience")
val message = text("message").nullable()
}

View File

@ -0,0 +1,3 @@
package gay.pizza.foundation.heimdall.table
object PlayerPositionTable : PlayerTimedLocalEventTable("player_positions")

View File

@ -0,0 +1,12 @@
package gay.pizza.foundation.heimdall.table
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.timestamp
object PlayerSessionTable : Table("player_sessions") {
val id = uuid("id")
val player = uuid("player")
val name = text("name")
val startTime = timestamp("start")
val endTime = timestamp("end")
}

View File

@ -0,0 +1,7 @@
package gay.pizza.foundation.heimdall.table
abstract class PlayerTimedLocalEventTable(name: String) : TimedLocalEventTable(name) {
val player = uuid("player").nullable()
val pitch = double("pitch")
val yaw = double("yaw")
}

View File

@ -0,0 +1,8 @@
package gay.pizza.foundation.heimdall.table
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.timestamp
abstract class TimedEventTable(name: String) : Table(name) {
val time = timestamp("time")
}

View File

@ -0,0 +1,8 @@
package gay.pizza.foundation.heimdall.table
abstract class TimedLocalEventTable(name: String) : TimedEventTable(name) {
val world = uuid("world")
val x = double("x")
val y = double("y")
val z = double("z")
}

View File

@ -0,0 +1,9 @@
package gay.pizza.foundation.heimdall.table
object WorldChangeTable : TimedEventTable("world_changes") {
val player = uuid("player")
val fromWorld = uuid("from_world")
val toWorld = uuid("to_world")
val fromWorldName = text("from_world_name")
val toWorldName = text("to_world_name")
}

View File

@ -0,0 +1,8 @@
plugins {
id("gay.pizza.foundation.concrete-library")
}
dependencies {
api(project(":common-all"))
compileOnly(project(":foundation-shared"))
}

View File

@ -0,0 +1,34 @@
package gay.pizza.foundation.common
import gay.pizza.foundation.shared.IFoundationCore
import gay.pizza.foundation.shared.loadConfigurationWithDefault
import kotlinx.serialization.DeserializationStrategy
import org.bukkit.command.CommandExecutor
import org.bukkit.command.TabCompleter
import org.bukkit.plugin.java.JavaPlugin
abstract class BaseFoundationPlugin : JavaPlugin() {
fun registerCommandExecutor(name: String, executor: CommandExecutor) {
registerCommandExecutor(listOf(name), executor)
}
fun registerCommandExecutor(names: List<String>, executor: CommandExecutor) {
for (name in names) {
val command = getCommand(name) ?: throw Exception("Failed to get $name command")
command.setExecutor(executor)
if (executor is TabCompleter) {
command.tabCompleter = executor
}
}
}
inline fun <reified T> loadConfigurationWithDefault(
core: IFoundationCore,
deserializer: DeserializationStrategy<T>,
name: String
): T {
return loadConfigurationWithDefault(
slF4JLogger, deserializer,
core.pluginDataPath, name)
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.foundation.common
import org.bukkit.entity.Player
fun Player.chat(vararg messages: String): Unit = messages.forEach { message ->
chat(message)
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core
package gay.pizza.foundation.common
fun <T, R : Comparable<R>> Collection<T>.sortedBy(order: SortOrder, selector: (T) -> R?): List<T> =
if (order == SortOrder.Ascending) {

View File

@ -0,0 +1,11 @@
package gay.pizza.foundation.common
import gay.pizza.foundation.shared.IFoundationCore
import org.bukkit.Server
object FoundationCoreLoader {
fun get(server: Server): IFoundationCore {
return server.pluginManager.getPlugin("Foundation") as IFoundationCore?
?: throw RuntimeException("Foundation Core is not loaded!")
}
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core
package gay.pizza.foundation.common
import org.bukkit.Material
import org.bukkit.OfflinePlayer
@ -9,7 +9,7 @@ import org.bukkit.entity.EntityType
val Server.allPlayers: List<OfflinePlayer>
get() = listOf(onlinePlayers, offlinePlayers.filter { !isPlayerOnline(it) }.toList()).flatten()
fun Server.isPlayerOnline(player: OfflinePlayer) =
fun Server.isPlayerOnline(player: OfflinePlayer): Boolean =
onlinePlayers.any { onlinePlayer -> onlinePlayer.name == player.name }
fun Server.allPlayerStatisticsOf(
@ -17,7 +17,7 @@ fun Server.allPlayerStatisticsOf(
material: Material? = null,
entityType: EntityType? = null,
order: SortOrder = SortOrder.Ascending
) = allPlayers.map { player ->
): List<Pair<OfflinePlayer, Int>> = allPlayers.map { player ->
player to if (material != null) {
player.getStatistic(statistic, material)
} else if (entityType != null) {

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core
package gay.pizza.foundation.common
enum class SortOrder {
Ascending,

View File

@ -0,0 +1,12 @@
package gay.pizza.foundation.common
import org.bukkit.Location
import org.bukkit.World
import org.bukkit.entity.Entity
import org.bukkit.entity.Player
import kotlin.reflect.KClass
fun <T: Entity> World.spawn(location: Location, clazz: KClass<T>): T = spawn(location, clazz.java)
fun <T: Entity> Player.spawn(clazz: KClass<T>): T = spawn(clazz.java)
fun <T: Entity> Player.spawn(clazz: Class<T>): T = world.spawn(location, clazz)

View File

@ -1,7 +1,16 @@
plugins {
id("gay.pizza.foundation.concrete-plugin")
}
dependencies {
implementation("net.dv8tion:JDA:5.0.0-alpha.2") {
implementation(libs.discord.jda) {
exclude(module = "opus-java")
}
compileOnly(project(":foundation-core"))
implementation(project(":common-plugin"))
compileOnly(project(":foundation-shared"))
}
concreteItem {
dependency(project(":foundation-core"))
}

View File

@ -1,139 +0,0 @@
package cloud.kubelet.foundation.bifrost
import cloud.kubelet.foundation.bifrost.model.BifrostConfig
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
import com.charleskorn.kaml.Yaml
import io.papermc.paper.event.player.AsyncChatEvent
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.MessageBuilder
import net.dv8tion.jda.api.entities.TextChannel
import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.events.ReadyEvent
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.hooks.EventListener
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.TextComponent
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.plugin.java.JavaPlugin
import java.awt.Color
import kotlin.io.path.inputStream
class FoundationBifrostPlugin : JavaPlugin(), EventListener, Listener {
private lateinit var config: BifrostConfig
private lateinit var jda: JDA
private var isDev = false
override fun onEnable() {
isDev = description.version == "DEV"
val foundation = server.pluginManager.getPlugin("Foundation") as FoundationCorePlugin
slF4JLogger.info("Plugin data path: ${foundation.pluginDataPath}")
val configPath = Util.copyDefaultConfig<FoundationBifrostPlugin>(
slF4JLogger,
foundation.pluginDataPath,
"bifrost.yaml"
)
config = Yaml.default.decodeFromStream(BifrostConfig.serializer(), configPath.inputStream())
server.pluginManager.registerEvents(this, this)
jda = JDABuilder
.createDefault(config.authentication.token)
.addEventListeners(this)
.build()
}
override fun onDisable() {
onServerStop()
logger.info("Shutting down JDA")
jda.shutdown()
while (jda.status != JDA.Status.SHUTDOWN) {
Thread.sleep(100)
}
}
override fun onEvent(e: GenericEvent) {
when (e) {
is ReadyEvent -> {
val channel = getChannel() ?: return
if (isDev) return
channel.sendMessage(":white_check_mark: Server is ready!").queue()
}
is MessageReceivedEvent -> {
// Prevent this bot from receiving its own messages and creating a feedback loop.
if (e.author.id == jda.selfUser.id) return
// Only forward messages from the configured channel.
if (e.channel.id != config.channel.id) return
slF4JLogger.debug(
"${e.guild.name} - ${e.channel.name} - ${e.author.name}: ${e.message.contentDisplay}"
)
server.sendMessage(Component.text("${e.author.name} - ${e.message.contentDisplay}"))
}
}
}
private fun getChannel(): TextChannel? {
val channel = jda.getTextChannelById(config.channel.id)
if (channel == null) {
slF4JLogger.error("Failed to retrieve channel ${config.channel.id}")
}
return channel
}
private fun message(f: MessageBuilder.() -> Unit) = MessageBuilder().apply(f).build()
private fun MessageBuilder.embed(f: EmbedBuilder.() -> Unit) {
setEmbeds(EmbedBuilder().apply(f).build())
}
@EventHandler
private fun onPlayerJoin(e: PlayerJoinEvent) {
val channel = getChannel() ?: return
channel.sendMessage(message {
embed {
setAuthor("${e.player.name} joined the server")
setColor(Color.GREEN)
}
}).queue()
}
@EventHandler
private fun onPlayerQuit(e: PlayerQuitEvent) {
val channel = getChannel() ?: return
channel.sendMessage(message {
embed {
setAuthor("${e.player.name} left the server")
setColor(Color.RED)
}
}).queue()
}
@EventHandler
private fun onPlayerChat(e: AsyncChatEvent) {
val channel = getChannel() ?: return
val message = e.message()
if (message is TextComponent) {
channel.sendMessage("${e.player.name}: ${message.content()}").queue()
} else {
slF4JLogger.error("Not sure what to do here, message != TextComponent: ${message.javaClass}")
}
}
private fun onServerStop() {
val channel = getChannel() ?: return
if (isDev) return
channel.sendMessage(":octagonal_sign: Server is stopping!").queue()
}
}

View File

@ -1,19 +0,0 @@
package cloud.kubelet.foundation.bifrost.model
import kotlinx.serialization.Serializable
@Serializable
data class BifrostConfig(
val authentication: BifrostAuthentication,
val channel: BifrostChannel,
)
@Serializable
data class BifrostAuthentication(
val token: String,
)
@Serializable
data class BifrostChannel(
val id: String,
)

View File

@ -0,0 +1,188 @@
package gay.pizza.foundation.bifrost
import gay.pizza.foundation.bifrost.model.BifrostConfig
import gay.pizza.foundation.common.BaseFoundationPlugin
import gay.pizza.foundation.common.FoundationCoreLoader
import gay.pizza.foundation.shared.AdvancementTitleCache
import gay.pizza.foundation.shared.PluginMainClass
import io.papermc.paper.event.player.AsyncChatEvent
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel
import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.events.session.ReadyEvent
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder
import net.dv8tion.jda.api.utils.messages.MessageCreateData
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.player.PlayerAdvancementDoneEvent
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import java.awt.Color
import net.dv8tion.jda.api.hooks.EventListener as DiscordEventListener
import org.bukkit.event.Listener as BukkitEventListener
@PluginMainClass
class FoundationBifrostPlugin : BaseFoundationPlugin(), DiscordEventListener, BukkitEventListener {
private lateinit var config: BifrostConfig
private var jda: JDA? = null
private var isDev = false
override fun onEnable() {
isDev = description.version == "DEV"
val foundation = FoundationCoreLoader.get(server)
config = loadConfigurationWithDefault(
foundation,
BifrostConfig.serializer(),
"bifrost.yaml"
)
server.pluginManager.registerEvents(this, this)
if (config.authentication.token.isEmpty()) {
slF4JLogger.warn("Token empty, Bifrost will not connect to Discord.")
return
}
jda = JDABuilder
.createDefault(config.authentication.token)
.addEventListeners(this)
.build()
}
override fun onDisable() {
// Plugin was not initialized, don't do anything.
if (jda == null) return
onServerStop()
logger.info("Shutting down JDA")
jda?.shutdown()
while (jda != null && jda!!.status != JDA.Status.SHUTDOWN) {
Thread.sleep(100)
}
}
override fun onEvent(e: GenericEvent) {
when (e) {
is ReadyEvent -> {
onDiscordReady()
}
is MessageReceivedEvent -> {
if (!config.channel.bridge) return
// Prevent this bot from receiving its own messages and creating a feedback loop.
if (e.author.id == jda?.selfUser?.id) return
// Only forward messages from the configured channel.
if (e.channel.id != config.channel.id) return
slF4JLogger.debug(
"${e.guild.name} - ${e.channel.name} - ${e.author.name}: ${e.message.contentDisplay}"
)
server.sendMessage(Component.text("${e.author.name} - ${e.message.contentDisplay}"))
}
}
}
private fun getTextChannel(): TextChannel? {
if (jda == null) {
return null
}
val channel = jda?.getTextChannelById(config.channel.id)
if (channel == null) {
slF4JLogger.error("Failed to retrieve channel ${config.channel.id}")
}
return channel
}
private fun message(f: MessageCreateBuilder.() -> Unit) = MessageCreateBuilder().apply(f).build()
private fun MessageCreateBuilder.embed(f: EmbedBuilder.() -> Unit) {
setEmbeds(EmbedBuilder().apply(f).build())
}
private fun sendChannelMessage(message: MessageCreateData, debug: () -> String) {
val channel = getTextChannel()
channel?.sendMessage(message)?.queue()
if (config.enableDebugLog) {
slF4JLogger.info("Send '${debug()}' to Discord")
}
}
private fun sendChannelMessage(message: String): Unit = sendChannelMessage(message {
setContent(message)
}) { message }
private fun sendEmbedMessage(color: Color, message: String): Unit = sendChannelMessage(message {
embed {
setAuthor(message)
setColor(color)
}
}) { "[rgb:${color.rgb}] $message" }
@EventHandler(priority = EventPriority.MONITOR)
private fun onPlayerJoin(e: PlayerJoinEvent) {
if (!config.channel.sendPlayerJoin) return
sendEmbedMessage(Color.GREEN, "${e.player.name} joined the server")
}
@EventHandler(priority = EventPriority.MONITOR)
private fun onPlayerQuit(e: PlayerQuitEvent) {
if (!config.channel.sendPlayerQuit) return
sendEmbedMessage(Color.RED, "${e.player.name} left the server")
}
@EventHandler(priority = EventPriority.MONITOR)
private fun onPlayerChat(e: AsyncChatEvent) {
if (!config.channel.bridge) return
val message = e.message()
val messageAsText = LegacyComponentSerializer.legacySection().serialize(message)
sendChannelMessage("${e.player.name}: $messageAsText")
}
@EventHandler(priority = EventPriority.MONITOR)
private fun onPlayerDeath(e: PlayerDeathEvent) {
if (!config.channel.sendPlayerDeath) return
@Suppress("DEPRECATION")
var deathMessage = e.deathMessage
if (deathMessage.isNullOrBlank()) {
deathMessage = "${e.player.name} died"
}
sendEmbedMessage(Color.YELLOW, deathMessage)
}
@EventHandler(priority = EventPriority.MONITOR)
private fun onPlayerAdvancementDone(e: PlayerAdvancementDoneEvent) {
if (!config.channel.sendPlayerAdvancement) return
if (e.advancement.key.key.contains("recipe/")) {
return
}
val advancementDisplay = e.advancement.display ?: return
if (!advancementDisplay.doesAnnounceToChat()) {
return
}
val display = AdvancementTitleCache.of(e.advancement) ?: return
sendEmbedMessage(Color.CYAN, "${e.player.name} completed the advancement '${display}'")
}
private fun onDiscordReady() {
if (!config.channel.sendStart) return
if (isDev) return
sendChannelMessage(":white_check_mark: Server is ready!")
}
private fun onServerStop() {
if (!config.channel.sendShutdown) return
if (isDev) return
sendChannelMessage(":octagonal_sign: Server is stopping!")
}
}

View File

@ -0,0 +1,27 @@
package gay.pizza.foundation.bifrost.model
import kotlinx.serialization.Serializable
@Serializable
data class BifrostConfig(
val authentication: BifrostAuthentication,
val channel: BifrostChannel,
val enableDebugLog: Boolean = false
)
@Serializable
data class BifrostAuthentication(
val token: String,
)
@Serializable
data class BifrostChannel(
val id: String,
val bridge: Boolean = true,
val sendStart: Boolean = true,
val sendShutdown: Boolean = true,
val sendPlayerJoin: Boolean = true,
val sendPlayerQuit: Boolean = true,
val sendPlayerDeath: Boolean = true,
val sendPlayerAdvancement: Boolean = true
)

View File

@ -1,10 +1,25 @@
# Authentication configuration for the bridge.
authentication:
# Token from the Discord Bot developer's page.
token: abc123
# Token from the Discord Bot developer's page. If this is empty, the Bifrost plugin will do
# nothing.
token: ""
# Channel configuration for the bridge.
channel:
# Channel ID, can be copied by turning on Developer Mode in User Settings -> Advanced. The ID can
# then be copied by right-clicking the channel and selecting "Copy ID".
id: 123456789
# Toggles the chat message bridge.
bridge: true
# Toggles for common events that generate notifications that are sent to the channel.
sendStart: true
sendShutdown: true
sendPlayerJoin: true
sendPlayerQuit: true
sendPlayerDeath: true
sendPlayerAdvancement: true
# Enables logging of what is sent to Discord.
enableDebugLog: false

View File

@ -1,10 +1,11 @@
name: Foundation-Bifrost
version: '${version}'
main: cloud.kubelet.foundation.bifrost.FoundationBifrostPlugin
main: gay.pizza.foundation.bifrost.FoundationBifrostPlugin
api-version: 1.18
prefix: Foundation-Bifrost
load: STARTUP
depend:
- Foundation
authors:
- kubelet
- kubeliv
- azenla

View File

@ -0,0 +1,13 @@
plugins {
id("gay.pizza.foundation.concrete-plugin")
}
dependencies {
implementation(project(":common-all"))
implementation(project(":common-plugin"))
compileOnly(project(":foundation-shared"))
}
concreteItem {
dependency(project(":foundation-core"))
}

View File

@ -0,0 +1,94 @@
package gay.pizza.foundation.chaos
import gay.pizza.foundation.chaos.model.ChaosConfig
import gay.pizza.foundation.chaos.modules.ChaosModule
import gay.pizza.foundation.chaos.modules.ChaosModules
import org.bukkit.boss.BarColor
import org.bukkit.boss.BarStyle
import org.bukkit.boss.BossBar
import org.bukkit.event.EventHandler
import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.plugin.Plugin
import java.util.concurrent.atomic.AtomicBoolean
class ChaosController(val plugin: Plugin, val config: ChaosConfig) : Listener {
val state: AtomicBoolean = AtomicBoolean(false)
val selectorController = ChaosSelectorController(this, plugin)
val allModules = ChaosModules.all(plugin)
var allowedModules: List<ChaosModule> = emptyList()
private var activeModules = mutableSetOf<ChaosModule>()
var bossBar: BossBar? = null
fun load() {
if (state.get()) {
return
}
allowedModules = filterEnabledModules()
state.set(true)
selectorController.schedule()
bossBar = plugin.server.createBossBar("Chaos Mode", BarColor.RED, BarStyle.SOLID)
for (player in plugin.server.onlinePlayers) {
bossBar?.addPlayer(player)
}
}
private fun filterEnabledModules(): List<ChaosModule> = allModules.filter { module ->
val moduleConfig = config.modules[module.id()] ?: config.defaultModuleConfiguration
moduleConfig.enabled
}
fun activateAll() {
for (module in allowedModules) {
if (activeModules.contains(module)) {
continue
}
activate(module)
}
}
fun activate(module: ChaosModule) {
plugin.server.pluginManager.registerEvents(module, plugin)
module.activate()
activeModules.add(module)
updateBossBar()
}
fun deactivate(module: ChaosModule) {
HandlerList.unregisterAll(module)
module.deactivate()
activeModules.remove(module)
updateBossBar()
}
fun updateBossBar() {
val activeModuleText = activeModules.joinToString(", ") { it.name() }
bossBar?.setTitle("Chaos Mode: $activeModuleText")
}
fun deactivateAll() {
for (module in activeModules.toList()) {
deactivate(module)
}
}
@EventHandler
fun onPlayerJoin(event: PlayerJoinEvent) {
bossBar?.addPlayer(event.player)
}
fun unload() {
if (!state.get()) {
return
}
deactivateAll()
bossBar?.removeAll()
bossBar = null
state.set(false)
selectorController.cancel()
}
}

View File

@ -0,0 +1,27 @@
package gay.pizza.foundation.chaos
import org.bukkit.plugin.Plugin
import org.bukkit.scheduler.BukkitTask
class ChaosSelectorController(val controller: ChaosController, val plugin: Plugin) {
var task: BukkitTask? = null
fun schedule() {
cancel()
task = plugin.server.scheduler.runTaskTimer(controller.plugin, { ->
select()
}, 20, controller.config.selection.timerTicks)
}
fun select() {
controller.deactivateAll()
val module = controller.allowedModules.randomOrNull()
if (module != null) {
controller.activate(module)
}
}
fun cancel() {
task?.cancel()
}
}

View File

@ -0,0 +1,30 @@
package gay.pizza.foundation.chaos
import net.kyori.adventure.text.Component
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
class ChaosToggleCommand : CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
val plugin = sender.server.pluginManager.getPlugin("Foundation-Chaos") as FoundationChaosPlugin
if (!plugin.config.allowed) {
sender.sendMessage("Chaos is not allowed.")
return true
}
val controller = plugin.controller
if (controller.state.get()) {
controller.unload()
sender.server.broadcast(Component.text("Chaos Mode Disabled"))
} else {
controller.load()
sender.server.broadcast(Component.text("Chaos Mode Enabled"))
}
return true
}
}

View File

@ -0,0 +1,11 @@
package gay.pizza.foundation.chaos
import org.bukkit.Location
import org.bukkit.Server
import org.bukkit.entity.Player
fun Location.nearestPlayer(): Player? =
world?.players?.minByOrNull { it.location.distance(this) }
fun Server.randomPlayer(): Player? =
onlinePlayers.randomOrNull()

View File

@ -0,0 +1,25 @@
package gay.pizza.foundation.chaos
import gay.pizza.foundation.chaos.model.ChaosConfig
import gay.pizza.foundation.common.BaseFoundationPlugin
import gay.pizza.foundation.common.FoundationCoreLoader
import gay.pizza.foundation.shared.PluginMainClass
@PluginMainClass
class FoundationChaosPlugin : BaseFoundationPlugin() {
lateinit var config: ChaosConfig
val controller by lazy {
ChaosController(this, config)
}
override fun onEnable() {
val foundation = FoundationCoreLoader.get(server)
config = loadConfigurationWithDefault(
foundation,
ChaosConfig.serializer(),
"chaos.yaml"
)
registerCommandExecutor("chaos", ChaosToggleCommand())
}
}

View File

@ -0,0 +1,21 @@
package gay.pizza.foundation.chaos.model
import kotlinx.serialization.Serializable
@Serializable
class ChaosConfig(
val allowed: Boolean = true,
val defaultModuleConfiguration: ChaosModuleConfig,
val modules: Map<String, ChaosModuleConfig> = emptyMap(),
val selection: ChaosSelectionConfig
)
@Serializable
class ChaosModuleConfig(
val enabled: Boolean
)
@Serializable
class ChaosSelectionConfig(
val timerTicks: Long
)

View File

@ -0,0 +1,71 @@
package gay.pizza.foundation.chaos.modules
import org.bukkit.Chunk
import org.bukkit.ChunkSnapshot
import org.bukkit.World
import org.bukkit.block.Block
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import org.bukkit.scheduler.BukkitScheduler
val World.heightRange
get() = minHeight until maxHeight
inline fun forEachChunkPosition(crossinline each: (Int, Int) -> Unit) {
for (x in 0..15) {
for (z in 0..15) {
each(x, z)
}
}
}
inline fun Chunk.forEachPosition(crossinline each: (Int, Int, Int) -> Unit) {
for (x in 0..15) {
for (z in 0..15) {
for (y in world.heightRange) {
each(x, y, z)
}
}
}
}
inline fun Chunk.forEachBlock(crossinline each: (Int, Int, Int, Block) -> Unit) {
forEachPosition { x, y, z ->
each(x, y, z, getBlock(x, y, z))
}
}
fun Chunk.applyChunkSnapshot(snapshot: ChunkSnapshot) {
forEachPosition { x, y, z ->
val blockData = snapshot.getBlockData(x, y, z)
val block = getBlock(x, y, z)
block.blockData = blockData
}
}
fun Player.teleportHighestLocation() {
teleport(location.toHighestLocation().add(0.0, 1.0, 0.0))
}
fun <T> BukkitScheduler.scheduleUntilEmpty(
plugin: Plugin,
items: MutableList<T>,
ticksBetween: Long,
callback: (T) -> Unit
) {
fun performOne() {
if (items.isEmpty()) {
return
}
val item = items.removeAt(0)
callback(item)
if (items.isNotEmpty()) {
runTaskLater(plugin, { ->
performOne()
}, ticksBetween)
}
}
performOne()
}

View File

@ -0,0 +1,11 @@
package gay.pizza.foundation.chaos.modules
import org.bukkit.event.Listener
interface ChaosModule : Listener {
fun id(): String
fun name(): String
fun what(): String
fun activate() {}
fun deactivate() {}
}

View File

@ -0,0 +1,16 @@
package gay.pizza.foundation.chaos.modules
import org.bukkit.plugin.Plugin
object ChaosModules {
fun all(plugin: Plugin) = listOf(
NearestPlayerEntitySpawn(plugin),
TeleportAllEntitiesNearestPlayer(plugin),
KillRandomPlayer(plugin),
TntAllPlayers(plugin),
MegaTnt(plugin),
PlayerSwap(plugin),
WorldSwapper(plugin),
ChunkEnterRotate()
).shuffled()
}

View File

@ -0,0 +1,37 @@
package gay.pizza.foundation.chaos.modules
import org.bukkit.Chunk
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.player.PlayerMoveEvent
import java.util.WeakHashMap
class ChunkEnterRotate : ChaosModule {
override fun id(): String = "chunk-enter-rotate"
override fun name(): String = "Chunk Enter Rotate"
override fun what(): String = "Rotates the chunk when the player enters a chunk."
private val playerChunkMap = WeakHashMap<Player, Chunk>()
@EventHandler
fun onPotentialChunkMove(event: PlayerMoveEvent) {
val player = event.player
val currentChunk = event.player.chunk
val previousChunk = playerChunkMap.put(player, currentChunk)
if (previousChunk == null || previousChunk == currentChunk) {
return
}
rotateChunk(currentChunk)
if (!player.isFlying) {
player.teleportHighestLocation()
}
}
private fun rotateChunk(chunk: Chunk) {
val snapshot = chunk.chunkSnapshot
chunk.forEachBlock { x, y, z, rotatedBlock ->
val originalBlockData = snapshot.getBlockData(z, y, x)
rotatedBlock.blockData = originalBlockData
}
}
}

View File

@ -0,0 +1,14 @@
package gay.pizza.foundation.chaos.modules
import org.bukkit.plugin.Plugin
class KillRandomPlayer(val plugin: Plugin) : ChaosModule {
override fun id(): String = "kill-random-player"
override fun name(): String = "Random Kill"
override fun what(): String = "Kill a random player."
override fun activate() {
val player = plugin.server.onlinePlayers.randomOrNull() ?: return
player.damage(1000000.0)
}
}

View File

@ -0,0 +1,19 @@
package gay.pizza.foundation.chaos.modules
import gay.pizza.foundation.common.spawn
import org.bukkit.entity.TNTPrimed
import org.bukkit.plugin.Plugin
class MegaTnt(val plugin: Plugin) : ChaosModule {
override fun id(): String = "mega-tnt"
override fun name(): String = "Mega TNT"
override fun what(): String = "Spawn a massive TNT explosion"
override fun activate() {
for (player in plugin.server.onlinePlayers) {
val tnt = player.spawn(TNTPrimed::class)
tnt.fuseTicks = 1
tnt.yield = 10.0f
}
}
}

View File

@ -0,0 +1,22 @@
package gay.pizza.foundation.chaos.modules
import gay.pizza.foundation.chaos.nearestPlayer
import org.bukkit.event.EventHandler
import org.bukkit.event.entity.EntitySpawnEvent
import org.bukkit.plugin.Plugin
class NearestPlayerEntitySpawn(val plugin: Plugin) : ChaosModule {
override fun id(): String = "nearest-player-entity-spawn"
override fun name(): String = "Monster Me"
override fun what(): String = "Teleport all spawned entities to the nearest player"
@EventHandler
fun onMobSpawn(e: EntitySpawnEvent) {
val player = e.location.nearestPlayer()
if (player != null) {
e.entity.server.scheduler.runTask(plugin) { ->
e.entity.teleport(player)
}
}
}
}

View File

@ -0,0 +1,30 @@
package gay.pizza.foundation.chaos.modules
import org.bukkit.Location
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
class PlayerSwap(val plugin: Plugin) : ChaosModule {
override fun id(): String = "player-swap"
override fun name(): String = "Player Swap"
override fun what(): String = "Randomly swaps player positions."
override fun activate() {
for (world in plugin.server.worlds) {
if (world.playerCount <= 0) {
continue
}
val players = world.players
val map = mutableMapOf<Player, Location>()
for (player in players) {
val next = players.filter { it != player }.randomOrNull() ?: continue
map[player] = next.location.clone()
}
for ((player, next) in map) {
player.teleport(next)
}
}
}
}

View File

@ -0,0 +1,21 @@
package gay.pizza.foundation.chaos.modules
import gay.pizza.foundation.chaos.nearestPlayer
import org.bukkit.plugin.Plugin
class TeleportAllEntitiesNearestPlayer(val plugin: Plugin) : ChaosModule {
override fun id(): String = "teleport-all-entities-nearest-player"
override fun name(): String = "Monster Me Once"
override fun what(): String = "Teleport all entities to the nearest player"
override fun activate() {
for (world in plugin.server.worlds) {
for (entity in world.entities) {
val player = entity.location.nearestPlayer()
if (player != null) {
entity.teleport(player)
}
}
}
}
}

View File

@ -0,0 +1,17 @@
package gay.pizza.foundation.chaos.modules
import gay.pizza.foundation.common.spawn
import org.bukkit.entity.TNTPrimed
import org.bukkit.plugin.Plugin
class TntAllPlayers(val plugin: Plugin) : ChaosModule {
override fun id(): String = "tnt-all-players"
override fun name(): String = "TNT Us All"
override fun what(): String = "TNT All Players"
override fun activate() {
for (player in plugin.server.onlinePlayers) {
player.spawn(TNTPrimed::class)
}
}
}

View File

@ -0,0 +1,81 @@
package gay.pizza.foundation.chaos.modules
import gay.pizza.foundation.chaos.randomPlayer
import gay.pizza.foundation.common.without
import org.bukkit.Chunk
import org.bukkit.ChunkSnapshot
import org.bukkit.block.Block
import org.bukkit.block.data.BlockData
import org.bukkit.plugin.Plugin
class WorldSwapper(val plugin: Plugin) : ChaosModule {
override fun id(): String = "world-swapper"
override fun name(): String = "World Swapper"
override fun what(): String = "Swaps the world vertically on activation, and un-swaps it on deactivation."
var chunkInversions = mutableListOf<ChunkInversion>()
override fun activate() {
val player = plugin.server.randomPlayer() ?: return
val baseChunk = player.chunk
recordInvert(baseChunk)
player.teleport(player.location.toHighestLocation())
val chunksToInvert = player.world.loadedChunks.without(baseChunk).toMutableList()
println("Inverting ${chunksToInvert.size} chunks...")
plugin.server.scheduler.scheduleUntilEmpty(plugin, chunksToInvert, 5) { chunk ->
recordInvert(chunk)
}
}
fun recordInvert(chunk: Chunk) {
chunkInversions.add(invertChunk(chunk))
}
override fun deactivate() {
fun scheduleOne() {
if (chunkInversions.isEmpty()) {
return
}
val inversion = chunkInversions.removeAt(0)
plugin.server.scheduler.runTaskLater(plugin, { ->
inversion.revert()
scheduleOne()
}, 5)
}
scheduleOne()
}
fun invertChunk(chunk: Chunk): ChunkInversion {
val snapshot = chunk.chunkSnapshot
forEachChunkPosition { x, z ->
var sy = chunk.world.minHeight
var ey = chunk.world.maxHeight
while (sy != ey) {
sy++
ey--
val targetBlock = chunk.getBlock(x, sy, z)
val targetBlockData = targetBlock.blockData.clone()
val nextBlock = chunk.getBlock(x, ey, z)
val nextBlockData = nextBlock.blockData.clone()
invertSetBlockData(targetBlock, nextBlockData)
invertSetBlockData(nextBlock, targetBlockData)
}
}
return ChunkInversion(plugin, snapshot)
}
private fun invertSetBlockData(block: Block, data: BlockData) {
block.setBlockData(data, false)
}
class ChunkInversion(val plugin: Plugin, val snapshot: ChunkSnapshot) {
fun revert() {
val world = plugin.server.getWorld(snapshot.worldName) ?: return
val chunk = world.getChunkAt(snapshot.x, snapshot.z)
chunk.applyChunkSnapshot(snapshot)
}
}
}

View File

@ -0,0 +1,16 @@
# Whether enabling the chaos mode is allowed.
allowed: false
# The default module configuration for modules with
# no explicit configuration.
defaultModuleConfiguration:
enabled: true
# Module configuration.
modules:
nearest-player-entity-spawn:
enabled: true
teleport-all-entities-nearest-player:
enabled: true
# Chaos selection configuration.
selection:
# The number of ticks before a new selection is made.
timerTicks: 6000

View File

@ -0,0 +1,16 @@
name: Foundation-Chaos
version: '${version}'
main: gay.pizza.foundation.chaos.FoundationChaosPlugin
api-version: 1.18
prefix: Foundation-Chaos
load: STARTUP
depend:
- Foundation
authors:
- kubeliv
- azenla
commands:
chaos:
description: Chaos Toggle
usage: /chaos
permission: foundation.command.chaos

View File

@ -1,3 +1,16 @@
dependencies {
// TODO: might be able to ship all dependencies in core? are we duplicating classes in JARs?
plugins {
id("gay.pizza.foundation.concrete-plugin")
}
dependencies {
api(project(":common-all"))
api(project(":common-plugin"))
implementation(project(":foundation-shared"))
implementation(libs.aws.sdk.s3)
implementation(libs.quartz.core)
implementation(libs.guava)
implementation(libs.koin.core)
testImplementation(libs.koin.test)
}

View File

@ -1,131 +0,0 @@
package cloud.kubelet.foundation.core
import cloud.kubelet.foundation.core.command.*
import cloud.kubelet.foundation.core.persist.PersistentStore
import cloud.kubelet.foundation.core.persist.setAllProperties
import io.papermc.paper.event.player.AsyncChatEvent
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.TextComponent
import org.bukkit.GameMode
import org.bukkit.command.CommandExecutor
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.plugin.java.JavaPlugin
import java.nio.file.Path
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
class FoundationCorePlugin : JavaPlugin(), Listener {
internal val persistentStores = ConcurrentHashMap<String, PersistentStore>()
private lateinit var _pluginDataPath: Path
var pluginDataPath: Path
/**
* Data path of the core plugin.
* Can be used as a sanity check of sorts for dependencies to be sure the plugin is loaded.
*/
get() {
if (!::_pluginDataPath.isInitialized) {
throw Exception("FoundationCore is not loaded!")
}
return _pluginDataPath
}
private set(value) {
_pluginDataPath = value
}
/**
* Fetch a persistent store by name. Make sure the name is path-safe, descriptive and consistent across server runs.
*/
fun getPersistentStore(name: String) = persistentStores.getOrPut(name) { PersistentStore(this, name) }
private lateinit var chatLogStore: PersistentStore
override fun onEnable() {
pluginDataPath = dataFolder.toPath()
val backupPath = pluginDataPath.resolve(BACKUPS_DIRECTORY)
// Create Foundation plugin directories.
pluginDataPath.toFile().mkdir()
backupPath.toFile().mkdir()
// Register this as an event listener.
server.pluginManager.registerEvents(this, this)
// Register commands.
registerCommandExecutor("fbackup", BackupCommand(this, backupPath))
registerCommandExecutor("fupdate", UpdateCommand())
registerCommandExecutor(listOf("survival", "s"), GamemodeCommand(GameMode.SURVIVAL))
registerCommandExecutor(listOf("creative", "c"), GamemodeCommand(GameMode.CREATIVE))
registerCommandExecutor(listOf("adventure", "a"), GamemodeCommand(GameMode.ADVENTURE))
registerCommandExecutor(listOf("spectator", "sp"), GamemodeCommand(GameMode.SPECTATOR))
registerCommandExecutor(listOf("leaderboard", "lb"), LeaderboardCommand())
registerCommandExecutor(listOf("pstorestats"), StoreStatsCommand(this))
val log = slF4JLogger
log.info("Features:")
Util.printFeatureStatus(log, "Backup", BACKUP_ENABLED)
chatLogStore = getPersistentStore("chat-logs")
}
override fun onDisable() {
persistentStores.values.forEach { store -> store.close() }
persistentStores.clear()
}
private fun registerCommandExecutor(name: String, executor: CommandExecutor) {
registerCommandExecutor(listOf(name), executor)
}
private fun registerCommandExecutor(names: List<String>, executor: CommandExecutor) {
for (name in names) {
val command = getCommand(name) ?: throw Exception("Failed to get $name command")
command.setExecutor(executor)
}
}
// TODO: Disabling chat reformatting until I do something with it and figure out how to make it
// be less disruptive.
/*@EventHandler
private fun onChatMessage(e: ChatEvent) {
return
e.isCancelled = true
val name = e.player.displayName()
val component = Component.empty()
.append(leftBracket)
.append(name)
.append(rightBracket)
.append(Component.text(' '))
.append(e.message())
server.sendMessage(component)
}*/
@EventHandler
private fun logOnChatMessage(e: AsyncChatEvent) {
val player = e.player
val message = e.message()
if (message !is TextComponent) {
return
}
val content = message.content()
chatLogStore.create("ChatMessageEvent") {
setAllProperties(
"timestamp" to Instant.now().toEpochMilli(),
"player.id" to player.identity().uuid().toString(),
"player.name" to player.name,
"message.content" to content
)
}
}
companion object {
private const val BACKUPS_DIRECTORY = "backups"
private val leftBracket: Component = Component.text('[')
private val rightBracket: Component = Component.text(']')
const val BACKUP_ENABLED = true
}
}

View File

@ -1,7 +0,0 @@
package cloud.kubelet.foundation.core
import net.kyori.adventure.text.format.TextColor
object TextColors {
val AMARANTH_PINK = TextColor.fromHexString("#F7A8B8")!!
}

View File

@ -1,64 +0,0 @@
package cloud.kubelet.foundation.core
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor
import org.slf4j.Logger
import java.nio.file.Path
object Util {
private val leftBracket: Component = Component.text('[')
private val rightBracket: Component = Component.text(']')
private val whitespace: Component = Component.text(' ')
private val foundationName: Component = Component.text("Foundation")
fun printFeatureStatus(logger: Logger, feature: String?, state: Boolean) {
logger.info("{}: {}", feature, if (state) "Enabled" else "Disabled")
}
fun formatSystemMessage(message: String): Component {
return formatSystemMessage(TextColors.AMARANTH_PINK, message)
}
fun formatSystemMessage(prefixColor: TextColor, message: String): Component {
return leftBracket
.append(foundationName.color(prefixColor))
.append(rightBracket)
.append(whitespace)
.append(Component.text(message))
}
/**
* Copy the default configuration from the resource [resourceName] into the directory [targetPath].
* @param targetPath The output directory as a path, it must exist before calling this.
* @param resourceName Path to resource, it should be in the root of the `resources` directory,
* without the leading slash.
*/
inline fun <reified T> copyDefaultConfig(log: Logger, targetPath: Path, resourceName: String): Path {
if (resourceName.startsWith("/")) {
throw IllegalArgumentException("resourceName starts with slash")
}
if (!targetPath.toFile().exists()) {
throw Exception("Configuration output path does not exist!")
}
val outPath = targetPath.resolve(resourceName)
val outFile = outPath.toFile()
if (outFile.exists()) {
log.debug("Configuration file already exists.")
return outPath
}
val resourceStream = T::class.java.getResourceAsStream("/$resourceName")
?: throw Exception("Configuration resource does not exist!")
val outputStream = outFile.outputStream()
resourceStream.use {
outputStream.use {
log.info("Copied default configuration to $outPath")
resourceStream.copyTo(outputStream)
}
}
return outPath
}
}

View File

@ -1,143 +0,0 @@
package cloud.kubelet.foundation.core.command
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor
import org.bukkit.Server
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class BackupCommand(
private val plugin: FoundationCorePlugin,
private val backupPath: Path
) : CommandExecutor {
override fun onCommand(
sender: CommandSender, command: Command, label: String, args: Array<String>
): Boolean {
if (!FoundationCorePlugin.BACKUP_ENABLED) {
sender.sendMessage(
Component
.text("Backup is not enabled.")
.color(TextColor.fromHexString("#FF0000"))
)
return true
}
if (RUNNING.get()) {
sender.sendMessage(
Component
.text("Backup is already running.")
.color(TextColor.fromHexString("#FF0000"))
)
return true
}
try {
val server = sender.server
server.scheduler.runTaskAsynchronously(plugin, Runnable {
runBackup(server)
})
} catch (e: Exception) {
sender.sendMessage(String.format("Failed to backup: %s", e.message))
}
return true
}
private fun runBackup(server: Server) {
RUNNING.set(true)
server.sendMessage(Util.formatSystemMessage("Backup started."))
val backupFile =
backupPath.resolve(String.format("backup-%s.zip", Instant.now().toString())).toFile()
try {
FileOutputStream(backupFile).use { zipFileStream ->
ZipOutputStream(BufferedOutputStream(zipFileStream)).use { zipStream ->
backupPlugins(server, zipStream)
backupWorlds(server, zipStream)
}
}
} finally {
RUNNING.set(false)
server.sendMessage(Util.formatSystemMessage("Backup finished."))
}
}
private fun backupPlugins(server: Server, zipStream: ZipOutputStream) {
try {
addDirectoryToZip(zipStream, server.pluginsFolder.toPath())
} catch (e: IOException) {
// TODO: Add error handling.
e.printStackTrace()
}
}
private fun backupWorlds(server: Server, zipStream: ZipOutputStream) {
val worlds = server.worlds
for (world in worlds) {
val worldPath = world.worldFolder.toPath()
// Save the world, must be run on the main thread.
server.scheduler.runTask(plugin, Runnable {
world.save()
})
// Disable auto saving to prevent any world corruption while creating a ZIP.
world.isAutoSave = false
try {
addDirectoryToZip(zipStream, worldPath)
} catch (e: IOException) {
// TODO: Add error handling.
e.printStackTrace()
}
// Re-enable auto saving for this world.
world.isAutoSave = true
}
}
private fun addDirectoryToZip(zipStream: ZipOutputStream, directoryPath: Path) {
val paths = Files.walk(directoryPath)
.filter { path: Path? -> Files.isRegularFile(path) }
.toList()
val buffer = ByteArray(1024)
val backupsPath = backupPath.toRealPath()
for (path in paths) {
val realPath = path.toRealPath()
if (realPath.startsWith(backupsPath)) {
plugin.slF4JLogger.info("Skipping file for backup: {}", realPath)
continue
}
FileInputStream(path.toFile()).use { fileStream ->
val entry = ZipEntry(path.toString())
zipStream.putNextEntry(entry)
var n: Int
while (fileStream.read(buffer).also { n = it } > -1) {
zipStream.write(buffer, 0, n)
}
}
}
}
companion object {
private val RUNNING = AtomicBoolean()
}
}

View File

@ -1,22 +0,0 @@
package cloud.kubelet.foundation.core.command
import cloud.kubelet.foundation.core.FoundationCorePlugin
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
class StoreStatsCommand(private val plugin: FoundationCorePlugin) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
plugin.persistentStores.forEach { (name, store) ->
val counts = store.transact { tx ->
tx.entityTypes.associateWith { type -> tx.getAll(type).size() }.toSortedMap()
}
sender.sendMessage(
"Store $name ->",
*counts.map { " ${it.key} -> ${it.value} entries" }.toTypedArray()
)
}
return true
}
}

View File

@ -1,51 +0,0 @@
package cloud.kubelet.foundation.core.command
import cloud.kubelet.foundation.core.update.UpdateUtil
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import kotlin.io.path.name
import kotlin.io.path.toPath
class UpdateCommand : CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
val updateDir = sender.server.pluginsFolder.resolve("update")
if (!updateDir.exists()) {
sender.sendMessage("Error: Failed to create plugin update directory.")
return true
}
val updatePath = updateDir.toPath()
// TODO: Move to separate thread?
val modules = UpdateUtil.fetchManifest()
val plugins = sender.server.pluginManager.plugins.associateBy { it.name.lowercase() }
sender.sendMessage("Updates:")
modules.forEach { (name, manifest) ->
// Dumb naming problem. Don't want to fix it right now.
val plugin = if (name == "foundation-core") {
plugins["foundation"]
} else {
plugins[name.lowercase()]
}
if (plugin == null) {
sender.sendMessage("Plugin in manifest, but not installed: $name (${manifest.version})")
} else {
val fileName = plugin.javaClass.protectionDomain.codeSource.location.toURI().toPath().name
val artifactPath = manifest.artifacts.getOrNull(0) ?: return@forEach
sender.sendMessage("${plugin.name}: Updating ${plugin.description.version} to ${manifest.version}")
UpdateUtil.downloadArtifact(artifactPath, updatePath.resolve(fileName))
}
}
sender.sendMessage("Restart to take effect")
return true
}
}

View File

@ -1,35 +0,0 @@
package cloud.kubelet.foundation.core.persist
import cloud.kubelet.foundation.core.FoundationCorePlugin
import jetbrains.exodus.entitystore.Entity
import jetbrains.exodus.entitystore.EntityIterable
import jetbrains.exodus.entitystore.PersistentEntityStores
import jetbrains.exodus.entitystore.StoreTransaction
class PersistentStore(corePlugin: FoundationCorePlugin, fileStoreName: String) : AutoCloseable {
private val fileStorePath = corePlugin.pluginDataPath.resolve("persistence/${fileStoreName}")
private val entityStore = PersistentEntityStores.newInstance(fileStorePath.toFile())
fun <R> transact(block: (StoreTransaction) -> R): R {
var result: R? = null
entityStore.executeInTransaction { tx ->
result = block(tx)
}
return result!!
}
fun create(entityTypeName: String, populate: Entity.() -> Unit) = transact { tx ->
val entity = tx.newEntity(entityTypeName)
populate(entity)
}
fun getAll(entityTypeName: String) =
transact { tx -> tx.getAll(entityTypeName) }
fun <T> find(entityTypeName: String, propertyName: String, value: Comparable<T>): EntityIterable =
transact { tx -> tx.find(entityTypeName, propertyName, value) }
override fun close() {
entityStore.close()
}
}

View File

@ -1,9 +0,0 @@
package cloud.kubelet.foundation.core.update
import kotlinx.serialization.Serializable
@Serializable
data class ModuleManifest(
val version: String,
val artifacts: List<String>,
)

View File

@ -1,59 +0,0 @@
package cloud.kubelet.foundation.core.update
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Path
object UpdateUtil {
private val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build()
// TODO: Add environment variable override. Document it.
private const val basePath =
"https://git.gorence.io/lgorence/foundation/-/jobs/artifacts/main/raw"
private const val basePathQueryParams = "job=build"
private const val manifestPath = "build/manifests/update.json"
fun fetchManifest() = fetchFile(
getUrl(manifestPath), MapSerializer(String.serializer(), ModuleManifest.serializer()),
)
fun getUrl(path: String) = "$basePath/$path?$basePathQueryParams"
private inline fun <reified T> fetchFile(url: String, strategy: DeserializationStrategy<T>): T {
val request = HttpRequest
.newBuilder()
.GET()
.uri(URI.create(url))
.build()
val response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
)
return Json.decodeFromString(
strategy,
response.body()
)
}
fun downloadArtifact(path: String, outPath: Path) {
val request = HttpRequest
.newBuilder()
.GET()
.uri(URI.create(getUrl(path)))
.build()
val response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(outPath)
)
response.body()
}
}

View File

@ -0,0 +1,60 @@
package gay.pizza.foundation.concrete
import kotlinx.serialization.Serializable
/**
* The extensible update manifest format.
*/
@Serializable
data class ExtensibleManifest(
/**
* The items the manifest describes.
*/
val items: List<ExtensibleManifestItem>
)
/**
* An item in the update manifest.
*/
@Serializable
data class ExtensibleManifestItem(
/**
* The name of the item.
*/
val name: String,
/**
* The type of item.
*/
val type: String,
/**
* The version of the item.
*/
val version: String,
/**
* The dependencies of the item.
*/
val dependencies: List<String>,
/**
* The files that are required to install the item.
*/
val files: List<ExtensibleManifestItemFile>
)
/**
* A file built from the item.
*/
@Serializable
data class ExtensibleManifestItemFile(
/**
* The name of the file.
*/
val name: String,
/**
* A type of file.
*/
val type: String,
/**
* The relative path to download the file.
*/
val path: String
)

View File

@ -0,0 +1,61 @@
package gay.pizza.foundation.core
import gay.pizza.foundation.core.abstraction.FoundationPlugin
import gay.pizza.foundation.core.features.backup.BackupFeature
import gay.pizza.foundation.core.features.gameplay.GameplayFeature
import gay.pizza.foundation.core.features.persist.PersistenceFeature
import gay.pizza.foundation.core.features.player.PlayerFeature
import gay.pizza.foundation.core.features.scheduler.SchedulerFeature
import gay.pizza.foundation.core.features.stats.StatsFeature
import gay.pizza.foundation.core.features.update.UpdateFeature
import gay.pizza.foundation.core.features.world.WorldFeature
import gay.pizza.foundation.shared.IFoundationCore
import gay.pizza.foundation.shared.PluginMainClass
import gay.pizza.foundation.shared.PluginPersistence
import org.koin.dsl.module
import java.nio.file.Path
@PluginMainClass
class FoundationCorePlugin : IFoundationCore, FoundationPlugin() {
private lateinit var _pluginDataPath: Path
override var pluginDataPath: Path
/**
* Data path of the core plugin.
* Can be used as a check of sorts for dependencies to be sure the plugin is loaded.
*/
get() {
if (!::_pluginDataPath.isInitialized) {
throw Exception("Foundation Core is not loaded!")
}
return _pluginDataPath
}
private set(value) {
_pluginDataPath = value
}
override val persistence: PluginPersistence = PluginPersistence(this)
override fun onEnable() {
// Create core plugin directory.
pluginDataPath = dataFolder.toPath()
pluginDataPath.toFile().mkdir()
super.onEnable()
}
override fun createFeatures() = listOf(
SchedulerFeature(),
PersistenceFeature(),
BackupFeature(),
GameplayFeature(),
PlayerFeature(),
StatsFeature(),
UpdateFeature(),
WorldFeature(),
)
override fun createModule() = module {
single { this@FoundationCorePlugin }
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.foundation.core.abstraction
interface CoreFeature {
fun enable()
fun disable()
fun module() = org.koin.dsl.module {}
}

View File

@ -0,0 +1,17 @@
package gay.pizza.foundation.core.abstraction
import gay.pizza.foundation.core.FoundationCorePlugin
import org.bukkit.event.Listener
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
import org.quartz.Scheduler
abstract class Feature : CoreFeature, KoinComponent, Listener {
protected val plugin by inject<FoundationCorePlugin>()
protected val scheduler by inject<Scheduler>()
override fun enable() {}
override fun disable() {}
override fun module() = module {}
}

View File

@ -0,0 +1,68 @@
package gay.pizza.foundation.core.abstraction
import gay.pizza.foundation.common.BaseFoundationPlugin
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.core.module.Module
import org.koin.dsl.module
abstract class FoundationPlugin : BaseFoundationPlugin() {
private lateinit var pluginModule: Module
private lateinit var pluginApplication: KoinApplication
private lateinit var features: List<CoreFeature>
private lateinit var module: Module
override fun onEnable() {
pluginModule = module {
single { this@FoundationPlugin }
single { server }
single { config }
single { slF4JLogger }
}
features = createFeatures()
module = createModule()
// TODO: If we have another plugin using Koin, we may need to use context isolation and ensure
// it uses the same context so they can fetch stuff from us.
// https://insert-koin.io/docs/reference/koin-core/context-isolation
pluginApplication = startKoin {
modules(pluginModule)
modules(module)
}
// This is probably a bit of a hack.
pluginApplication.modules(module {
single { pluginApplication }
})
features.forEach {
pluginApplication.modules(it.module())
}
features.forEach {
try {
slF4JLogger.info("Enabling feature: ${it.javaClass.simpleName}")
it.enable()
// TODO: May replace this check with a method in the interface, CoreFeature would no-op.
if (it is Feature) {
server.pluginManager.registerEvents(it, this)
}
} catch (e: Exception) {
slF4JLogger.error("Failed to enable feature: ${it.javaClass.simpleName}", e)
}
}
}
override fun onDisable() {
features.forEach {
it.disable()
}
stopKoin()
}
protected open fun createModule() = module {}
protected abstract fun createFeatures(): List<CoreFeature>
}

View File

@ -0,0 +1,168 @@
package gay.pizza.foundation.core.features.backup
import gay.pizza.foundation.shared.Platform
import gay.pizza.foundation.core.FoundationCorePlugin
import gay.pizza.foundation.shared.MessageUtil
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor
import org.bukkit.Server
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
// TODO: Clean up dependency injection.
class BackupCommand(
private val plugin: FoundationCorePlugin,
private val backupFilePath: Path,
private val config: BackupConfig,
private val s3Client: S3Client,
) : CommandExecutor {
override fun onCommand(
sender: CommandSender, command: Command, label: String, args: Array<String>
): Boolean {
if (running.get()) {
sender.sendMessage(
Component
.text("Backup is already running.")
.color(TextColor.fromHexString("#FF0000"))
)
return true
}
val server = sender.server
server.scheduler.runTaskAsynchronously(plugin) { ->
runBackup(server, sender)
}
return true
}
// TODO: Pull backup creation code into a separate service.
private fun runBackup(server: Server, sender: CommandSender? = null) = try {
running.set(true)
server.scheduler.runTask(plugin) { ->
server.sendMessage(MessageUtil.formatSystemMessage("Backup started."))
}
val backupTime = Instant.now()
val backupIdentifier = if (Platform.isWindows()) {
backupTime.toEpochMilli().toString()
} else {
backupTime.toString()
}
val backupFileName = String.format("backup-%s.zip", backupIdentifier)
val backupPath = backupFilePath.resolve(backupFileName)
val backupFile = backupPath.toFile()
FileOutputStream(backupFile).use { zipFileStream ->
ZipOutputStream(BufferedOutputStream(zipFileStream)).use { zipStream ->
backupPlugins(server, zipStream)
backupWorlds(server, zipStream)
}
}
// TODO: Pull upload code out into a separate service.
if (config.s3.accessKeyId.isNotEmpty()) {
s3Client.putObject(
PutObjectRequest.builder().apply {
bucket(config.s3.bucket)
key("${config.s3.baseDirectory}/$backupFileName")
}.build(),
backupPath
)
}
Unit
} catch (e: Exception) {
if (sender != null) {
server.scheduler.runTask(plugin) { ->
sender.sendMessage(String.format("Failed to backup: %s", e.message))
}
}
plugin.slF4JLogger.warn("Failed to backup.", e)
} finally {
running.set(false)
server.scheduler.runTask(plugin) { ->
server.sendMessage(MessageUtil.formatSystemMessage("Backup finished."))
}
}
private fun backupPlugins(server: Server, zipStream: ZipOutputStream) {
try {
addDirectoryToZip(zipStream, server.pluginsFolder.toPath())
} catch (e: IOException) {
// TODO: Add error handling.
plugin.slF4JLogger.warn("Failed to backup plugins.", e)
}
}
private fun backupWorlds(server: Server, zipStream: ZipOutputStream) {
val worlds = server.worlds
for (world in worlds) {
val worldPath = world.worldFolder.toPath()
// Save the world, must be run on the main thread.
server.scheduler.runTask(plugin, Runnable {
world.save()
})
// Disable auto saving to prevent any world corruption while creating a ZIP.
world.isAutoSave = false
try {
addDirectoryToZip(zipStream, worldPath)
} catch (e: IOException) {
// TODO: Add error handling.
e.printStackTrace()
}
// Re-enable auto saving for this world.
world.isAutoSave = true
}
}
private fun addDirectoryToZip(zipStream: ZipOutputStream, directoryPath: Path) {
val matchers = config.ignore.map { FileSystems.getDefault().getPathMatcher("glob:$it") }
val paths = Files.walk(directoryPath)
.filter { path: Path -> Files.isRegularFile(path) }
.filter { path -> !matchers.any { it.matches(Paths.get(path.normalize().toString())) } }
.toList()
val buffer = ByteArray(16 * 1024)
val backupsPath = backupFilePath.toRealPath()
for (path in paths) {
val realPath = path.toRealPath()
if (realPath.startsWith(backupsPath)) {
plugin.slF4JLogger.info("Skipping file for backup: {}", realPath)
continue
}
FileInputStream(path.toFile()).use { fileStream ->
val entry = ZipEntry(path.toString())
zipStream.putNextEntry(entry)
var n: Int
while (fileStream.read(buffer).also { n = it } > -1) {
zipStream.write(buffer, 0, n)
}
}
}
}
companion object {
private val running = AtomicBoolean()
}
}

View File

@ -0,0 +1,27 @@
package gay.pizza.foundation.core.features.backup
import kotlinx.serialization.Serializable
@Serializable
data class BackupConfig(
val schedule: ScheduleConfig = ScheduleConfig(),
val ignore: List<String> = listOf(
"plugins/dynmap/web/**"
),
val s3: S3Config = S3Config(),
)
@Serializable
data class ScheduleConfig(
val cron: String = "",
)
@Serializable
data class S3Config(
val accessKeyId: String = "",
val secretAccessKey: String = "",
val region: String = "",
val endpointOverride: String = "",
val bucket: String = "",
val baseDirectory: String = "",
)

View File

@ -0,0 +1,76 @@
package gay.pizza.foundation.core.features.backup
import gay.pizza.foundation.core.abstraction.Feature
import gay.pizza.foundation.core.features.scheduler.cancel
import gay.pizza.foundation.core.features.scheduler.cron
import org.koin.core.component.inject
import org.koin.dsl.module
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import java.net.URI
class BackupFeature : Feature() {
private val s3Client by inject<S3Client>()
private val config by inject<BackupConfig>()
private lateinit var scheduleId: String
override fun enable() {
// Create backup directory.
val backupPath = plugin.pluginDataPath.resolve(BACKUPS_DIRECTORY)
backupPath.toFile().mkdir()
plugin.registerCommandExecutor("fbackup", BackupCommand(plugin, backupPath, config, s3Client))
if (config.schedule.cron.isNotEmpty()) {
// Assume the user never wants to modify the second. I'm not sure why this is enforced in Quartz.
val expr = "0 ${config.schedule.cron}"
scheduleId = scheduler.cron(expr) {
plugin.server.scheduler.runTask(plugin) { ->
plugin.server.dispatchCommand(plugin.server.consoleSender, "fbackup")
}
}
}
}
override fun disable() {
if (::scheduleId.isInitialized) {
scheduler.cancel(scheduleId)
}
}
override fun module() = module {
single {
plugin.loadConfigurationWithDefault(
plugin,
BackupConfig.serializer(),
"backup.yaml"
)
}
single {
val config = get<BackupConfig>()
val creds = StaticCredentialsProvider.create(
AwsSessionCredentials.create(config.s3.accessKeyId, config.s3.secretAccessKey, "")
)
val builder = S3Client.builder().credentialsProvider(creds)
if (config.s3.endpointOverride.isNotEmpty()) {
builder.endpointOverride(URI.create(config.s3.endpointOverride))
}
if (config.s3.region.isNotEmpty()) {
builder.region(Region.of(config.s3.region))
} else {
builder.region(Region.US_WEST_1)
}
builder.build()
}
}
companion object {
private const val BACKUPS_DIRECTORY = "backups"
}
}

View File

@ -0,0 +1,15 @@
package gay.pizza.foundation.core.features.gameplay
import kotlinx.serialization.Serializable
@Serializable
data class GameplayConfig(
val mobs: MobsConfig = MobsConfig(),
)
@Serializable
data class MobsConfig(
val disableEndermanGriefing: Boolean = false,
val disableFreezeDamage: Boolean = false,
val allowLeads: Boolean = false,
)

View File

@ -0,0 +1,83 @@
package gay.pizza.foundation.core.features.gameplay
import gay.pizza.foundation.core.abstraction.Feature
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.entity.EntityType
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Mob
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.entity.EntityChangeBlockEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.player.PlayerInteractEntityEvent
import org.bukkit.inventory.ItemStack
import org.koin.core.component.inject
import org.koin.dsl.module
class GameplayFeature : Feature() {
private val config by inject<GameplayConfig>()
override fun module() = module {
single {
plugin.loadConfigurationWithDefault(
plugin,
GameplayConfig.serializer(),
"gameplay.yaml"
)
}
}
@EventHandler(priority = EventPriority.HIGHEST)
private fun onEntityDamage(e: EntityDamageEvent) {
// If freeze damage is disabled, cancel the event.
if (config.mobs.disableFreezeDamage) {
if (e.entity is Mob && e.cause == EntityDamageEvent.DamageCause.FREEZE) {
e.isCancelled = true
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
private fun onEntityChangeBlock(event: EntityChangeBlockEvent) {
// If enderman griefing is disabled, cancel the event.
if (config.mobs.disableEndermanGriefing) {
if (event.entity.type == EntityType.ENDERMAN) {
event.isCancelled = true
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
private fun onPlayerInteractEntity(event: PlayerInteractEntityEvent) {
val mainHandItem = event.player.inventory.itemInMainHand
val hasLead = mainHandItem.type == Material.LEAD
val livingEntity = event.rightClicked as? LivingEntity
// If leads are allowed on all mobs, then start leading the mob.
if (config.mobs.allowLeads && hasLead && livingEntity != null) {
// Something to do with Bukkit, leashes must happen after the event.
Bukkit.getScheduler().runTask(plugin) { ->
// If the entity is already leashed, don't do anything.
if (livingEntity.isLeashed) return@runTask
// Interacted with the entity, don't despawn it.
livingEntity.removeWhenFarAway = false
val leashSuccess = livingEntity.setLeashHolder(event.player)
if (leashSuccess) {
val newStack = if (mainHandItem.amount == 1) {
null
} else {
ItemStack(mainHandItem.type, mainHandItem.amount - 1)
}
event.player.inventory.setItemInMainHand(newStack)
}
}
event.isCancelled = true
return
}
}
}

View File

@ -0,0 +1,21 @@
package gay.pizza.foundation.core.features.persist
import gay.pizza.foundation.core.FoundationCorePlugin
import gay.pizza.foundation.core.abstraction.Feature
import gay.pizza.foundation.shared.PluginPersistence
import org.koin.core.component.inject
import org.koin.core.module.Module
import org.koin.dsl.module
class PersistenceFeature : Feature() {
private val persistence by inject<PluginPersistence>()
private val core by inject<FoundationCorePlugin>()
override fun disable() {
persistence.unload()
}
override fun module(): Module = module {
single { core.persistence }
}
}

View File

@ -0,0 +1,94 @@
package gay.pizza.foundation.core.features.persist
import gay.pizza.foundation.core.features.stats.StatsFeature
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
class PersistentStoreCommand(
private val statsFeature: StatsFeature
) : CommandExecutor, TabCompleter {
private val allSubCommands = mutableListOf("stats", "sample", "delete-all-entities")
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
if (args.isEmpty()) {
sender.sendMessage("Invalid Command Usage.")
return true
}
when (args[0]) {
"stats" -> {
statsFeature.persistence.stores.forEach { (name, store) ->
val counts = store.transact {
entityTypes.associateWith { type -> getAll(type).size() }.toSortedMap()
}
sender.sendMessage(
"Store $name ->",
*counts.map { " ${it.key} -> ${it.value} entries" }.toTypedArray()
)
}
}
"sample" -> {
if (args.size != 3) {
sender.sendMessage("Invalid Subcommand Usage.")
return true
}
val storeName = args[1]
val entityTypeName = args[2]
val store = statsFeature.persistence.store(storeName)
store.transact {
val entities = getAll(entityTypeName).take(3)
for (entity in entities) {
sender.sendMessage(
"Entity ${entity.id.localId} ->",
*entity.propertyNames.map { " ${it}: ${entity.getProperty(it)}" }.toTypedArray()
)
}
}
}
"delete-all-entities" -> {
if (args.size != 3) {
sender.sendMessage("Invalid Subcommand Usage.")
return true
}
val storeName = args[1]
val entityTypeName = args[2]
val store = statsFeature.persistence.store(storeName)
store.transact {
store.deleteAllEntities(entityTypeName)
}
sender.sendMessage("Deleted all entities for $storeName $entityTypeName")
}
else -> {
sender.sendMessage("Unknown Subcommand.")
}
}
return true
}
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: Array<out String>
): MutableList<String> = when {
args.isEmpty() -> {
allSubCommands
}
args.size == 1 -> {
allSubCommands.filter { it.startsWith(args[0]) }.toMutableList()
}
else -> {
mutableListOf()
}
}
}

View File

@ -1,7 +1,7 @@
package cloud.kubelet.foundation.core.persist
package gay.pizza.foundation.core.features.persist
import jetbrains.exodus.entitystore.Entity
fun <T : Comparable<*>> Entity.setAllProperties(vararg entries: Pair<String, T>) = entries.forEach { entry ->
fun <T : Comparable<*>> Entity.setAllProperties(vararg entries: Pair<String, T>): Unit = entries.forEach { entry ->
setProperty(entry.first, entry.second)
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.command
package gay.pizza.foundation.core.features.player
import org.bukkit.GameMode
import org.bukkit.command.Command

View File

@ -0,0 +1,25 @@
package gay.pizza.foundation.core.features.player
import gay.pizza.foundation.common.chat
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class GooseCommand : CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>): Boolean {
if (sender !is Player) {
sender.sendMessage("Player is required for this command.")
return true
}
sender.chat(
"Goose is the most beautiful kitty to ever exist <3",
"I don't know who Nat is but there is no way she can compare to Goose."
)
return true
}
}

View File

@ -0,0 +1,50 @@
package gay.pizza.foundation.core.features.player
import org.bukkit.WeatherType
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
import org.bukkit.entity.Player
class LocalWeatherCommand : CommandExecutor, TabCompleter {
private val weatherTypes = WeatherType.values().associateBy { it.name.lowercase() }
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
if (sender !is Player) {
sender.sendMessage("You are not a player.")
return true
}
if (args.size != 1) {
return false
}
val name = args[0].lowercase()
val weatherType = weatherTypes[name]
if (weatherType == null) {
sender.sendMessage("Not a valid weather type.")
return true
}
sender.setPlayerWeather(weatherType)
sender.sendMessage("Weather set to \"$name\"")
return true
}
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: Array<out String>
): List<String> = when {
args.isEmpty() -> weatherTypes.keys.toList()
args.size == 1 -> weatherTypes.filterKeys { it.startsWith(args[0]) }.keys.toList()
else -> listOf()
}
}

View File

@ -0,0 +1,25 @@
package gay.pizza.foundation.core.features.player
import gay.pizza.foundation.common.spawn
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import org.bukkit.entity.TNTPrimed
class MegaTntCommand : CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>): Boolean {
if (sender !is Player) {
sender.sendMessage("Player is required for this command.")
return true
}
val tnt = sender.spawn(TNTPrimed::class)
tnt.fuseTicks = 1
tnt.yield = 50.0f
return true
}
}

View File

@ -0,0 +1,17 @@
package gay.pizza.foundation.core.features.player
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PlayerConfig(
@SerialName("anti-idle")
val antiIdle: AntiIdleConfig = AntiIdleConfig(),
)
@Serializable
data class AntiIdleConfig(
val enabled: Boolean = false,
val idleDuration: Int = 3600,
val ignore: List<String> = listOf(),
)

View File

@ -0,0 +1,82 @@
package gay.pizza.foundation.core.features.player
import com.google.common.cache.Cache
import com.google.common.cache.CacheBuilder
import com.google.common.cache.RemovalCause
import gay.pizza.foundation.core.abstraction.Feature
import net.kyori.adventure.text.Component
import org.bukkit.GameMode
import org.bukkit.event.EventHandler
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerKickEvent
import org.bukkit.event.player.PlayerMoveEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.koin.core.component.inject
import java.time.Duration
class PlayerFeature : Feature() {
private val config by inject<PlayerConfig>()
private lateinit var playerActivity: Cache<String, String>
override fun enable() {
playerActivity = CacheBuilder.newBuilder()
.expireAfterWrite(Duration.ofSeconds(config.antiIdle.idleDuration.toLong()))
.removalListener<String, String> z@{
if (!config.antiIdle.enabled) return@z
if (it.cause == RemovalCause.EXPIRED) {
if (!config.antiIdle.ignore.contains(it.key!!)) {
plugin.server.scheduler.runTask(plugin) { ->
plugin.server.getPlayer(it.key!!)
?.kick(Component.text("Kicked for idling"), PlayerKickEvent.Cause.IDLING)
}
}
}
}.build()
// Expire player activity tokens occasionally.
plugin.server.scheduler.scheduleSyncRepeatingTask(plugin, {
playerActivity.cleanUp()
}, 20, 100)
plugin.registerCommandExecutor(listOf("survival", "s"), GamemodeCommand(GameMode.SURVIVAL))
plugin.registerCommandExecutor(listOf("creative", "c"), GamemodeCommand(GameMode.CREATIVE))
plugin.registerCommandExecutor(listOf("adventure", "a"), GamemodeCommand(GameMode.ADVENTURE))
plugin.registerCommandExecutor(listOf("spectator", "sp"), GamemodeCommand(GameMode.SPECTATOR))
plugin.registerCommandExecutor(listOf("localweather", "lw"), LocalWeatherCommand())
plugin.registerCommandExecutor(listOf("goose", "the_most_wonderful_kitty_ever"), GooseCommand())
plugin.registerCommandExecutor(listOf("megatnt"), MegaTntCommand())
}
override fun module() = org.koin.dsl.module {
single {
plugin.loadConfigurationWithDefault(
plugin,
PlayerConfig.serializer(),
"player.yaml"
)
}
}
@EventHandler
private fun onPlayerJoin(e: PlayerJoinEvent) {
if (!config.antiIdle.enabled) return
playerActivity.put(e.player.name, e.player.name)
}
@EventHandler
private fun onPlayerQuit(e: PlayerQuitEvent) {
if (!config.antiIdle.enabled) return
playerActivity.invalidate(e.player.name)
}
@EventHandler
private fun onPlayerMove(e: PlayerMoveEvent) {
if (!config.antiIdle.enabled) return
if (e.hasChangedPosition() || e.hasChangedOrientation()) {
playerActivity.put(e.player.name, e.player.name)
}
}
}

View File

@ -0,0 +1,30 @@
package gay.pizza.foundation.core.features.scheduler
import org.quartz.CronScheduleBuilder.cronSchedule
import org.quartz.JobBuilder.newJob
import org.quartz.JobDataMap
import org.quartz.Scheduler
import org.quartz.TriggerBuilder.newTrigger
import org.quartz.TriggerKey.triggerKey
import java.util.UUID
fun Scheduler.cron(cronExpression: String, f: () -> Unit): String {
val id = UUID.randomUUID().toString()
val job = newJob(SchedulerRunner::class.java).apply {
setJobData(JobDataMap().apply {
set("function", f)
})
}.build()
val trigger = newTrigger()
.withIdentity(triggerKey(id))
.withSchedule(cronSchedule(cronExpression))
.build()
scheduleJob(job, trigger)
return id
}
fun Scheduler.cancel(id: String) {
unscheduleJob(triggerKey(id))
}

View File

@ -0,0 +1,22 @@
package gay.pizza.foundation.core.features.scheduler
import gay.pizza.foundation.core.abstraction.CoreFeature
import org.koin.dsl.module
import org.quartz.Scheduler
import org.quartz.impl.StdSchedulerFactory
class SchedulerFeature : CoreFeature {
private val scheduler: Scheduler = StdSchedulerFactory.getDefaultScheduler()
override fun enable() {
scheduler.start()
}
override fun disable() {
scheduler.shutdown(true)
}
override fun module() = module {
single { scheduler }
}
}

View File

@ -0,0 +1,12 @@
package gay.pizza.foundation.core.features.scheduler
import org.quartz.Job
import org.quartz.JobExecutionContext
class SchedulerRunner : Job {
override fun execute(context: JobExecutionContext) {
@Suppress("UNCHECKED_CAST")
val function = context.jobDetail.jobDataMap["function"] as () -> Unit
function()
}
}

View File

@ -1,18 +1,23 @@
package cloud.kubelet.foundation.core.command
package gay.pizza.foundation.core.features.stats
import cloud.kubelet.foundation.core.SortOrder
import cloud.kubelet.foundation.core.allPlayerStatisticsOf
import gay.pizza.foundation.common.SortOrder
import gay.pizza.foundation.common.allPlayerStatisticsOf
import org.bukkit.Statistic
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
class LeaderboardCommand : CommandExecutor {
class LeaderboardCommand : CommandExecutor, TabCompleter {
private val leaderboards = listOf(
LeaderboardType("player-kills", Statistic.PLAYER_KILLS, "Player Kills", "kills"),
LeaderboardType("mob-kills", Statistic.MOB_KILLS, "Mob Kills", "kills"),
LeaderboardType("animals-bred", Statistic.ANIMALS_BRED, "Animals Bred", "animals"),
LeaderboardType("chest-opens", Statistic.CHEST_OPENED, "Chest Opens", "opens")
LeaderboardType("chest-opens", Statistic.CHEST_OPENED, "Chest Opens", "opens"),
LeaderboardType("raid-wins", Statistic.RAID_WIN, "Raid Wins", "wins"),
LeaderboardType("item-enchants", Statistic.ITEM_ENCHANTED, "Item Enchants", "enchants"),
LeaderboardType("damage-dealt", Statistic.DAMAGE_DEALT, "Damage Dealt", "damage"),
LeaderboardType("fish-caught", Statistic.FISH_CAUGHT, "Fish Caught", "fish")
)
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
@ -30,10 +35,28 @@ class LeaderboardCommand : CommandExecutor {
val topFivePlayers = statistics.take(5)
sender.sendMessage(
"${leaderboardType.friendlyName} Leaderboard:",
*topFivePlayers.map { "* ${it.first.name}: ${it.second} ${leaderboardType.unit}" }.toTypedArray()
*topFivePlayers.withIndex()
.map { "(#${it.index + 1}) ${it.value.first.name}: ${it.value.second} ${leaderboardType.unit}" }.toTypedArray()
)
return true
}
class LeaderboardType(val id: String, val statistic: Statistic, val friendlyName: String, val unit: String)
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: Array<out String>
): MutableList<String> = when {
args.isEmpty() -> {
leaderboards.map { it.id }.toMutableList()
}
args.size == 1 -> {
leaderboards.map { it.id }.filter { it.startsWith(args[0]) }.toMutableList()
}
else -> {
mutableListOf()
}
}
}

View File

@ -0,0 +1,45 @@
package gay.pizza.foundation.core.features.stats
import gay.pizza.foundation.core.abstraction.Feature
import gay.pizza.foundation.core.features.persist.PersistentStoreCommand
import gay.pizza.foundation.core.features.persist.setAllProperties
import gay.pizza.foundation.shared.PersistentStore
import gay.pizza.foundation.shared.PluginPersistence
import io.papermc.paper.event.player.AsyncChatEvent
import net.kyori.adventure.text.TextComponent
import org.bukkit.event.EventHandler
import org.koin.core.component.inject
import java.time.Instant
@Suppress("IdentifierGrammar")
class StatsFeature : Feature() {
internal val persistence by inject<PluginPersistence>()
private lateinit var chatLogStore: PersistentStore
override fun enable() {
chatLogStore = persistence.store("chat-logs")
plugin.registerCommandExecutor(listOf("leaderboard", "lb"), LeaderboardCommand())
plugin.registerCommandExecutor("pstore", PersistentStoreCommand(this))
}
@EventHandler
private fun logOnChatMessage(e: AsyncChatEvent) {
val player = e.player
val message = e.message()
if (message !is TextComponent) {
return
}
val content = message.content()
chatLogStore.create("ChatMessageEvent") {
setAllProperties(
"timestamp" to Instant.now().toEpochMilli(),
"player.id" to player.identity().uuid().toString(),
"player.name" to player.name,
"message.content" to content
)
}
}
}

View File

@ -0,0 +1,24 @@
package gay.pizza.foundation.core.features.update
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.plugin.Plugin
class UpdateCommand(val plugin: Plugin) : CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
val shouldRestart = args.isNotEmpty() && args[0] == "restart"
UpdateService.updatePlugins(plugin, sender, onFinish = { updated ->
if (!updated) return@updatePlugins
if (shouldRestart) {
sender.server.shutdown()
}
})
return true
}
}

View File

@ -0,0 +1,13 @@
package gay.pizza.foundation.core.features.update
import kotlinx.serialization.Serializable
@Serializable
data class UpdateConfig(
val autoUpdateSchedule: AutoUpdateSchedule
)
@Serializable
class AutoUpdateSchedule(
val cron: String = ""
)

View File

@ -0,0 +1,41 @@
package gay.pizza.foundation.core.features.update
import gay.pizza.foundation.core.abstraction.Feature
import gay.pizza.foundation.core.features.scheduler.cancel
import gay.pizza.foundation.core.features.scheduler.cron
import org.koin.core.component.inject
import org.koin.core.module.Module
import org.koin.dsl.module
class UpdateFeature : Feature() {
private val config by inject<UpdateConfig>()
lateinit var autoUpdateScheduleId: String
override fun enable() {
plugin.registerCommandExecutor("fupdate", UpdateCommand(plugin))
if (config.autoUpdateSchedule.cron.isNotEmpty()) {
autoUpdateScheduleId = scheduler.cron(config.autoUpdateSchedule.cron) {
plugin.server.scheduler.runTask(plugin) { ->
plugin.server.dispatchCommand(plugin.server.consoleSender, "fupdate restart")
}
}
}
}
override fun disable() {
if (::autoUpdateScheduleId.isInitialized) {
scheduler.cancel(autoUpdateScheduleId)
}
}
override fun module(): Module = module {
single {
plugin.loadConfigurationWithDefault(
plugin,
UpdateConfig.serializer(),
"update.yaml"
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More