83 Commits

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
260 changed files with 3786 additions and 2431 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/*

4
.gitignore vendored
View File

@ -119,3 +119,7 @@ run/
# 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

@ -7,12 +7,21 @@ server.
* 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
@ -21,5 +30,5 @@ available.
```bash
# Always validate the contents of a script from the internet!
bash -c "$(curl -sL https://git.mystic.run/minecraft/foundation/-/raw/main/install.sh)"
bash -c "$(curl -sL https://github.com/GayPizzaSpecifications/foundation/raw/main/install.sh)"
```

View File

@ -1,20 +1,20 @@
import cloud.kubelet.foundation.gradle.FoundationProjectPlugin
import cloud.kubelet.foundation.gradle.isFoundationPlugin
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
java
id("cloud.kubelet.foundation.gradle")
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"
url = uri("https://papermc.io/repo/repository/maven-public/")
}
maven {
name = "sonatype"
@ -23,75 +23,23 @@ allprojects {
}
}
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")
plugins.apply(FoundationProjectPlugin::class)
group = "lgbt.mystic"
dependencies {
// Kotlin dependencies
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
// Core libraries.
implementation("io.insert-koin:koin-core:3.1.4")
testImplementation("io.insert-koin:koin-test:3.1.4")
// 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.2-R0.1-SNAPSHOT")
}
java {
val javaVersion = JavaVersion.toVersion(17)
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
group = "gay.pizza.foundation"
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs =
freeCompilerArgs + "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi"
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi")
}
}
tasks.processResources {
val props = mapOf("version" to version)
inputs.properties(props)
filteringCharset = "UTF-8"
filesMatching("plugin.yml") {
expand(props)
}
}
if (project.isFoundationPlugin()) {
tasks.withType<ShadowJar> {
archiveClassifier.set("plugin")
}
}
tasks.assemble {
dependsOn("shadowJar")
}
}
foundation {
val paperServerVersion: String = project.properties["paperServerVersion"]?.toString() ?: "1.21"
concreteRoot {
minecraftServerPath.set("server")
paperVersionGroup.set("1.18")
paperServerVersionGroup.set(paperServerVersion)
paperApiVersion.set("1.21.4-R0.1-SNAPSHOT")
acceptServerEula.set(true)
}

View File

@ -1,28 +0,0 @@
plugins {
`kotlin-dsl`
kotlin("plugin.serialization") version "1.5.31"
}
repositories {
gradlePluginPortal()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.6.10")
implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.1")
implementation("com.google.code.gson:gson:2.8.9")
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
gradlePlugin {
plugins {
create("foundation") {
id = "cloud.kubelet.foundation.gradle"
implementationClass = "cloud.kubelet.foundation.gradle.FoundationGradlePlugin"
}
}
}

View File

@ -1,8 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.provider.Property
interface FoundationExtension {
val paperVersionGroup: Property<String>
val minecraftServerPath: Property<String>
}

View File

@ -1,7 +0,0 @@
package cloud.kubelet.foundation.gradle
import com.google.gson.Gson
object FoundationGlobals {
val gson = Gson()
}

View File

@ -1,24 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.create
class FoundationGradlePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create<FoundationExtension>("foundation")
val setupPaperServer = project.tasks.create<SetupPaperServer>("setupPaperServer")
project.afterEvaluate { ->
setupPaperServer.dependsOn(*project.subprojects
.filter { it.name.startsWith("foundation-") }
.map { it.tasks.getByName("shadowJar") }
.toTypedArray()
)
}
val runPaperServer = project.tasks.create<RunPaperServer>("runPaperServer")
runPaperServer.dependsOn(setupPaperServer)
val updateManifests = project.tasks.create<UpdateManifestTask>("updateManifests")
project.tasks.getByName("assemble").dependsOn(updateManifests)
}
}

View File

@ -1,16 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
class FoundationProjectPlugin : Plugin<Project> {
override fun apply(project: Project) {
val versionWithBuild = if (System.getenv("CI_PIPELINE_IID") != null) {
project.rootProject.version.toString() + ".${System.getenv("CI_PIPELINE_IID")}"
} else {
"DEV"
}
project.version = versionWithBuild
}
}

View File

@ -1,46 +0,0 @@
package cloud.kubelet.foundation.gradle
import com.google.gson.Gson
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class PaperVersionClient(
val client: HttpClient = HttpClient.newHttpClient(),
private val gson: Gson = FoundationGlobals.gson
) {
private val apiBaseUrl = URI.create("https://papermc.io/api/v2/")
fun getVersionBuilds(group: String): List<PaperBuild> {
val response = client.send(
HttpRequest.newBuilder()
.GET()
.uri(apiBaseUrl.resolve("projects/paper/version_group/${group}/builds"))
.build(),
HttpResponse.BodyHandlers.ofString()
)
val body = response.body()
val root = gson.fromJson(body, PaperVersionRoot::class.java)
return root.builds
}
fun resolveDownloadUrl(build: PaperBuild, download: PaperVersionDownload): URI =
apiBaseUrl.resolve("projects/paper/versions/${build.version}/builds/${build.build}/downloads/${download.name}")
data class PaperVersionRoot(
val builds: List<PaperBuild>
)
data class PaperBuild(
val version: String,
val build: Int,
val downloads: Map<String, PaperVersionDownload>
)
data class PaperVersionDownload(
val name: String,
val sha256: String
)
}

View File

@ -1,33 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.getByType
import java.io.File
import java.util.jar.JarFile
open class RunPaperServer : DefaultTask() {
init {
outputs.upToDateWhen { false }
}
@TaskAction
fun runPaperServer() {
val foundation = project.extensions.getByType<FoundationExtension>()
val minecraftServerDirectory = project.file(foundation.minecraftServerPath.get())
val paperJarFile = minecraftServerDirectory.resolve("paper.jar")
val mainClassName = readMainClass(paperJarFile)
project.javaexec {
classpath(paperJarFile.absolutePath)
workingDir(minecraftServerDirectory)
args("nogui")
mainClass.set(mainClassName)
}
}
private fun readMainClass(file: File): String = JarFile(file).use { jar ->
jar.manifest.mainAttributes.getValue("Main-Class")!!
}
}

View File

@ -1,69 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import org.gradle.kotlin.dsl.getByType
import java.io.File
import java.nio.file.Files
open class SetupPaperServer : DefaultTask() {
init {
outputs.upToDateWhen { false }
}
@get:Input
@set:Option(option = "update", description = "Update Paper Server")
var shouldUpdatePaperServer = false
private val paperVersionClient = PaperVersionClient()
@TaskAction
fun downloadPaperTask() {
val foundation = project.extensions.getByType<FoundationExtension>()
val minecraftServerDirectory = project.file(foundation.minecraftServerPath.get())
if (!minecraftServerDirectory.exists()) {
minecraftServerDirectory.mkdirs()
}
val paperJarFile = project.file("${foundation.minecraftServerPath.get()}/paper.jar")
if (!paperJarFile.exists() || shouldUpdatePaperServer) {
downloadLatestBuild(foundation.paperVersionGroup.get(), paperJarFile)
}
val paperPluginsDirectory = minecraftServerDirectory.resolve("plugins")
if (!paperPluginsDirectory.exists()) {
paperPluginsDirectory.mkdirs()
}
for (project in project.subprojects) {
if (!project.name.startsWith("foundation-")) {
continue
}
val pluginJarFile = project.buildDir.resolve("libs/${project.name}-DEV-plugin.jar")
val pluginLinkFile = paperPluginsDirectory.resolve("${project.name}.jar")
if (pluginLinkFile.exists()) {
pluginLinkFile.delete()
}
Files.createSymbolicLink(pluginLinkFile.toPath(), pluginJarFile.toPath())
}
}
private fun downloadLatestBuild(paperVersionGroup: String, paperJarFile: File) {
val builds = paperVersionClient.getVersionBuilds(paperVersionGroup)
val build = builds.last()
val download = build.downloads["application"]!!
val url = paperVersionClient.resolveDownloadUrl(build, download)
val downloader = SmartDownloader(paperJarFile.toPath(), url, download.sha256)
if (downloader.download()) {
logger.lifecycle("Installed Paper Server ${build.version} build ${build.build}")
} else {
logger.lifecycle("Paper Server ${build.version} build ${build.build} is up-to-date")
}
}
}

View File

@ -1,78 +0,0 @@
package cloud.kubelet.foundation.gradle
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
class SmartDownloader(
private val localFilePath: Path,
private val remoteDownloadUrl: URI,
private val sha256: String
) {
fun download(): Boolean {
val hashResult = checkLocalFileHash()
if (hashResult != HashResult.ValidHash) {
downloadRemoteFile()
return false
}
return true
}
private fun downloadRemoteFile() {
val url = remoteDownloadUrl.toURL()
val remoteFileStream = url.openStream()
val localFileStream = Files.newOutputStream(localFilePath)
remoteFileStream.transferTo(localFileStream)
val hashResult = checkLocalFileHash()
if (hashResult != HashResult.ValidHash) {
throw RuntimeException("Download of $remoteDownloadUrl did not result in valid hash.")
}
}
private fun checkLocalFileHash(): HashResult {
if (!Files.exists(localFilePath)) {
return HashResult.DoesNotExist
}
val digest = MessageDigest.getInstance("SHA-256")
val localFileStream = Files.newInputStream(localFilePath)
val buffer = ByteArray(16 * 1024)
while (true) {
val size = localFileStream.read(buffer)
if (size <= 0) {
break
}
val bytes = buffer.take(size).toByteArray()
digest.update(bytes)
}
val sha256Bytes = digest.digest()
val localSha256Hash = bytesToHex(sha256Bytes)
return if (localSha256Hash.equals(sha256, ignoreCase = true)) {
HashResult.ValidHash
} else {
HashResult.InvalidHash
}
}
private fun bytesToHex(hash: ByteArray): String {
val hexString = StringBuilder(2 * hash.size)
for (i in hash.indices) {
val hex = Integer.toHexString(0xff and hash[i].toInt())
if (hex.length == 1) {
hexString.append('0')
}
hexString.append(hex)
}
return hexString.toString()
}
enum class HashResult {
DoesNotExist,
InvalidHash,
ValidHash
}
}

View File

@ -1,32 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.nio.file.Files
import java.nio.file.Path
open class UpdateManifestTask : DefaultTask() {
@TaskAction
fun update() {
val manifestsDir = ensureManifestsDir()
val updateFile = manifestsDir.resolve("update.json")
val rootPath = project.rootProject.rootDir.toPath()
val updateManifest = project.findPluginProjects().mapNotNull { project ->
val paths = project.shadowJarOutputs.allFilesRelativeToPath(rootPath)
if (paths.isNotEmpty()) {
project.name to mapOf(
"version" to project.version,
"artifacts" to paths.map { it.toUnixString() }
)
} else null
}.toMap()
Files.writeString(updateFile, FoundationGlobals.gson.toJson(updateManifest))
}
private fun ensureManifestsDir(): Path {
val manifestsDir = project.buildDir.resolve("manifests")
manifestsDir.mkdirs()
return manifestsDir.toPath()
}
}

View File

@ -1,17 +0,0 @@
package cloud.kubelet.foundation.gradle
import org.gradle.api.Project
import org.gradle.api.tasks.TaskOutputs
import java.nio.file.FileSystems
import java.nio.file.Path
fun Project.isFoundationPlugin() = name.startsWith("foundation-")
fun Project.findPluginProjects() = rootProject.subprojects.filter { project -> project.isFoundationPlugin() }
val Project.shadowJarOutputs: TaskOutputs
get() = project.tasks.getByName("shadowJar").outputs
fun TaskOutputs.allFilesRelativeToPath(root: Path): List<Path> = files.map { root.relativize(it.toPath()) }
fun Path.toUnixString() = toString().replace(FileSystems.getDefault().separator, "/")

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

@ -1,9 +1,10 @@
package cloud.kubelet.foundation.heimdall.export
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

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.heimdall.export
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
data class ExportedChunkSection(
val x: Int,
val z: Int,
val blocks: List<ExportedBlock>
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

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.heimdall.table
package gay.pizza.foundation.heimdall.table
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.timestamp

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,20 +1,20 @@
package cloud.kubelet.foundation.bifrost
package gay.pizza.foundation.bifrost
import cloud.kubelet.foundation.bifrost.model.BifrostConfig
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
import cloud.kubelet.foundation.core.util.AdvancementTitleCache
import com.charleskorn.kaml.Yaml
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.MessageBuilder
import net.dv8tion.jda.api.entities.Message
import net.dv8tion.jda.api.entities.TextChannel
import net.dv8tion.jda.api.entities.channel.concrete.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.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
@ -23,30 +23,24 @@ 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 org.bukkit.plugin.java.JavaPlugin
import java.awt.Color
import kotlin.io.path.inputStream
import net.dv8tion.jda.api.hooks.EventListener as DiscordEventListener
import org.bukkit.event.Listener as BukkitEventListener
class FoundationBifrostPlugin : JavaPlugin(), DiscordEventListener, 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 = server.pluginManager.getPlugin("Foundation") as FoundationCorePlugin
slF4JLogger.info("Plugin data path: ${foundation.pluginDataPath}")
val configPath = Util.copyDefaultConfig<FoundationBifrostPlugin>(
slF4JLogger,
foundation.pluginDataPath,
val foundation = FoundationCoreLoader.get(server)
config = loadConfigurationWithDefault(
foundation,
BifrostConfig.serializer(),
"bifrost.yaml"
)
config = Yaml.default.decodeFromStream(BifrostConfig.serializer(), configPath.inputStream())
server.pluginManager.registerEvents(this, this)
if (config.authentication.token.isEmpty()) {
slF4JLogger.warn("Token empty, Bifrost will not connect to Discord.")
@ -105,12 +99,12 @@ class FoundationBifrostPlugin : JavaPlugin(), DiscordEventListener, BukkitEventL
return channel
}
private fun message(f: MessageBuilder.() -> Unit) = MessageBuilder().apply(f).build()
private fun MessageBuilder.embed(f: EmbedBuilder.() -> Unit) {
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: Message, debug: () -> String) {
private fun sendChannelMessage(message: MessageCreateData, debug: () -> String) {
val channel = getTextChannel()
channel?.sendMessage(message)?.queue()
@ -158,7 +152,7 @@ class FoundationBifrostPlugin : JavaPlugin(), DiscordEventListener, BukkitEventL
if (!config.channel.sendPlayerDeath) return
@Suppress("DEPRECATION")
var deathMessage = e.deathMessage
if (deathMessage == null || deathMessage.isBlank()) {
if (deathMessage.isNullOrBlank()) {
deathMessage = "${e.player.name} died"
}
sendEmbedMessage(Color.YELLOW, deathMessage)

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.bifrost.model
package gay.pizza.foundation.bifrost.model
import kotlinx.serialization.Serializable

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,7 +1,16 @@
dependencies {
// TODO: might be able to ship all dependencies in core? are we duplicating classes in JARs?
implementation("software.amazon.awssdk:s3:2.17.102")
implementation("org.quartz-scheduler:quartz:2.3.2")
implementation("com.google.guava:guava:31.0.1-jre")
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,57 +0,0 @@
package cloud.kubelet.foundation.core
import cloud.kubelet.foundation.core.abstraction.FoundationPlugin
import cloud.kubelet.foundation.core.features.backup.BackupFeature
import cloud.kubelet.foundation.core.features.dev.DevFeature
import cloud.kubelet.foundation.core.features.gameplay.GameplayFeature
import cloud.kubelet.foundation.core.features.persist.PersistenceFeature
import cloud.kubelet.foundation.core.features.player.PlayerFeature
import cloud.kubelet.foundation.core.features.scheduler.SchedulerFeature
import cloud.kubelet.foundation.core.features.stats.StatsFeature
import cloud.kubelet.foundation.core.features.update.UpdateFeature
import cloud.kubelet.foundation.core.features.world.WorldFeature
import org.koin.dsl.module
import java.nio.file.Path
class FoundationCorePlugin : FoundationPlugin() {
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
}
override fun onEnable() {
// Create core plugin directory.
pluginDataPath = dataFolder.toPath()
pluginDataPath.toFile().mkdir()
super.onEnable()
}
override fun createFeatures() = listOf(
SchedulerFeature(),
PersistenceFeature(),
BackupFeature(),
DevFeature(),
GameplayFeature(),
PlayerFeature(),
StatsFeature(),
UpdateFeature(),
WorldFeature(),
)
override fun createModule() = module {
single { this@FoundationCorePlugin }
}
}

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,65 +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 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
}
fun isPlatformWindows(): Boolean {
val os = System.getProperty("os.name")
return os != null && os.lowercase().startsWith("windows")
}
}

View File

@ -1,33 +0,0 @@
package cloud.kubelet.foundation.core.abstraction
import cloud.kubelet.foundation.core.FoundationCorePlugin
import org.bukkit.command.CommandExecutor
import org.bukkit.command.TabCompleter
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 {}
protected fun registerCommandExecutor(name: String, executor: CommandExecutor) {
registerCommandExecutor(listOf(name), executor)
}
protected fun registerCommandExecutor(names: List<String>, executor: CommandExecutor) {
for (name in names) {
val command = plugin.getCommand(name) ?: throw Exception("Failed to get $name command")
command.setExecutor(executor)
if (executor is TabCompleter) {
command.tabCompleter = executor
}
}
}
}

View File

@ -1,16 +0,0 @@
package cloud.kubelet.foundation.core.features.dev
import cloud.kubelet.foundation.core.abstraction.Feature
class DevFeature : Feature() {
private lateinit var devUpdateServer: DevUpdateServer
override fun enable() {
devUpdateServer = DevUpdateServer(plugin)
devUpdateServer.enable()
}
override fun disable() {
devUpdateServer.disable()
}
}

View File

@ -1,10 +0,0 @@
package cloud.kubelet.foundation.core.features.dev
import kotlinx.serialization.Serializable
@Serializable
class DevUpdateConfig(
val port: Int = 8484,
val token: String,
val ipAllowList: List<String> = listOf("*")
)

View File

@ -1,13 +0,0 @@
package cloud.kubelet.foundation.core.features.dev
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable
class DevUpdatePayload(
@SerialName("object_kind")
val objectKind: String,
@SerialName("object_attributes")
val objectAttributes: Map<String, JsonElement>
)

View File

@ -1,117 +0,0 @@
package cloud.kubelet.foundation.core.features.dev
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
import cloud.kubelet.foundation.core.features.update.UpdateService
import com.charleskorn.kaml.Yaml
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonPrimitive
import java.net.InetSocketAddress
import kotlin.io.path.inputStream
class DevUpdateServer(val plugin: FoundationCorePlugin) {
private lateinit var config: DevUpdateConfig
private var server: HttpServer? = null
private val json = Json {
prettyPrint = true
prettyPrintIndent = " "
ignoreUnknownKeys = true
}
fun enable() {
val configPath = Util.copyDefaultConfig<FoundationCorePlugin>(
plugin.slF4JLogger,
plugin.pluginDataPath,
"devupdate.yaml"
)
config = Yaml.default.decodeFromStream(DevUpdateConfig.serializer(), configPath.inputStream())
start()
}
private fun start() {
if (config.token.isEmpty()) {
return
}
if (config.token.length < 8) {
plugin.slF4JLogger.warn("DevUpdateServer Token was too short (must be 8 or more characters)")
return
}
val server = HttpServer.create()
server.createContext("/").setHandler { exchange ->
handle(exchange)
}
server.bind(InetSocketAddress("0.0.0.0", config.port), 0)
server.start()
this.server = server
plugin.slF4JLogger.info("DevUpdateServer listening on port ${config.port}")
}
private fun handle(exchange: HttpExchange) {
val ip = exchange.remoteAddress.address.hostAddress
if (!config.ipAllowList.contains("*") && !config.ipAllowList.contains(ip)) {
plugin.slF4JLogger.warn("DevUpdateServer received request from IP $ip which is not allowed.")
exchange.close()
return
}
plugin.slF4JLogger.info("DevUpdateServer Request $ip ${exchange.requestMethod} ${exchange.requestURI.path}")
if (exchange.requestMethod != "POST") {
exchange.respond(405, "Method not allowed.")
return
}
if (exchange.requestURI.path != "/webhook/update") {
exchange.respond(404, "Not Found.")
return
}
if (exchange.requestURI.query != config.token) {
exchange.respond(401, "Unauthorized.")
return
}
val payload: DevUpdatePayload
try {
payload = json.decodeFromStream(exchange.requestBody)
} catch (e: Exception) {
plugin.slF4JLogger.error("Failed to decode request body.", e)
exchange.respond(400, "Bad Request")
return
}
if (payload.objectKind != "pipeline" ||
payload.objectAttributes["ref"]?.jsonPrimitive?.content != "main" ||
payload.objectAttributes["status"]?.jsonPrimitive?.content != "success"
) {
exchange.respond(200, "Event was not relevant for update.")
return
}
exchange.respond(200, "Success.")
plugin.slF4JLogger.info("DevUpdate Started")
UpdateService.updatePlugins(plugin.server.consoleSender) {
plugin.server.scheduler.runTask(plugin) { ->
plugin.server.shutdown()
}
}
}
fun disable() {
server?.stop(1)
}
private fun HttpExchange.respond(code: Int, content: String) {
val encoded = content.encodeToByteArray()
sendResponseHeaders(code, encoded.size.toLong())
responseBody.write(encoded)
responseBody.close()
close()
}
}

View File

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

View File

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

View File

@ -1,17 +0,0 @@
package cloud.kubelet.foundation.core.features.update
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
class UpdateCommand : CommandExecutor {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
UpdateService.updatePlugins(sender)
return true
}
}

View File

@ -1,9 +0,0 @@
package cloud.kubelet.foundation.core.features.update
import cloud.kubelet.foundation.core.abstraction.Feature
class UpdateFeature : Feature() {
override fun enable() {
registerCommandExecutor("fupdate", UpdateCommand())
}
}

View File

@ -1,46 +0,0 @@
package cloud.kubelet.foundation.core.features.update
import org.bukkit.command.CommandSender
import kotlin.io.path.name
import kotlin.io.path.toPath
// TODO: Switch to a class and use dependency injection with koin.
object UpdateService {
fun updatePlugins(sender: CommandSender, onFinish: (() -> Unit)? = null) {
val updateDir = sender.server.pluginsFolder.resolve("update")
updateDir.mkdir()
if (!updateDir.exists()) {
sender.sendMessage("Error: Failed to create plugin update directory.")
return
}
val updatePath = updateDir.toPath()
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")
if (onFinish != null) onFinish()
}.start()
}
}

View File

@ -1,59 +0,0 @@
package cloud.kubelet.foundation.core.features.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.mystic.run/minecraft/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

@ -1,10 +0,0 @@
package cloud.kubelet.foundation.core.features.world
import cloud.kubelet.foundation.core.abstraction.Feature
class WorldFeature : Feature() {
override fun enable() {
registerCommandExecutor("setspawn", SetSpawnCommand())
registerCommandExecutor("spawn", SpawnCommand())
}
}

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

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.abstraction
package gay.pizza.foundation.core.abstraction
interface CoreFeature {
fun enable()

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

@ -1,13 +1,13 @@
package cloud.kubelet.foundation.core.abstraction
package gay.pizza.foundation.core.abstraction
import org.bukkit.plugin.java.JavaPlugin
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 : JavaPlugin() {
abstract class FoundationPlugin : BaseFoundationPlugin() {
private lateinit var pluginModule: Module
private lateinit var pluginApplication: KoinApplication
private lateinit var features: List<CoreFeature>

View File

@ -1,7 +1,8 @@
package cloud.kubelet.foundation.core.features.backup
package gay.pizza.foundation.core.features.backup
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
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
@ -26,14 +27,14 @@ import java.util.zip.ZipOutputStream
// TODO: Clean up dependency injection.
class BackupCommand(
private val plugin: FoundationCorePlugin,
private val backupsPath: Path,
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()) {
if (running.get()) {
sender.sendMessage(
Component
.text("Backup is already running.")
@ -51,20 +52,20 @@ class BackupCommand(
// TODO: Pull backup creation code into a separate service.
private fun runBackup(server: Server, sender: CommandSender? = null) = try {
RUNNING.set(true)
running.set(true)
server.scheduler.runTask(plugin) { ->
server.sendMessage(Util.formatSystemMessage("Backup started."))
server.sendMessage(MessageUtil.formatSystemMessage("Backup started."))
}
val backupTime = Instant.now()
val backupIdentifier = if (Util.isPlatformWindows()) {
val backupIdentifier = if (Platform.isWindows()) {
backupTime.toEpochMilli().toString()
} else {
backupTime.toString()
}
val backupFileName = String.format("backup-%s.zip", backupIdentifier)
val backupPath = backupsPath.resolve(backupFileName)
val backupPath = backupFilePath.resolve(backupFileName)
val backupFile = backupPath.toFile()
FileOutputStream(backupFile).use { zipFileStream ->
@ -93,9 +94,9 @@ class BackupCommand(
}
plugin.slF4JLogger.warn("Failed to backup.", e)
} finally {
RUNNING.set(false)
running.set(false)
server.scheduler.runTask(plugin) { ->
server.sendMessage(Util.formatSystemMessage("Backup finished."))
server.sendMessage(MessageUtil.formatSystemMessage("Backup finished."))
}
}
@ -139,7 +140,7 @@ class BackupCommand(
.filter { path -> !matchers.any { it.matches(Paths.get(path.normalize().toString())) } }
.toList()
val buffer = ByteArray(16 * 1024)
val backupsPath = backupsPath.toRealPath()
val backupsPath = backupFilePath.toRealPath()
for (path in paths) {
val realPath = path.toRealPath()
@ -162,6 +163,6 @@ class BackupCommand(
}
companion object {
private val RUNNING = AtomicBoolean()
private val running = AtomicBoolean()
}
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.features.backup
package gay.pizza.foundation.core.features.backup
import kotlinx.serialization.Serializable

View File

@ -1,11 +1,8 @@
package cloud.kubelet.foundation.core.features.backup
package gay.pizza.foundation.core.features.backup
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
import cloud.kubelet.foundation.core.abstraction.Feature
import cloud.kubelet.foundation.core.features.scheduler.cancel
import cloud.kubelet.foundation.core.features.scheduler.cron
import com.charleskorn.kaml.Yaml
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
@ -13,7 +10,6 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import java.net.URI
import kotlin.io.path.inputStream
class BackupFeature : Feature() {
private val s3Client by inject<S3Client>()
@ -25,10 +21,10 @@ class BackupFeature : Feature() {
val backupPath = plugin.pluginDataPath.resolve(BACKUPS_DIRECTORY)
backupPath.toFile().mkdir()
registerCommandExecutor("fbackup", BackupCommand(plugin, backupPath, config, s3Client))
plugin.registerCommandExecutor("fbackup", BackupCommand(plugin, backupPath, config, s3Client))
if (config.schedule.cron.isNotEmpty()) {
// Assume user never wants to modify second. I'm not sure why this is enforced in Quartz.
// 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) { ->
@ -46,14 +42,10 @@ class BackupFeature : Feature() {
override fun module() = module {
single {
val configPath = Util.copyDefaultConfig<FoundationCorePlugin>(
plugin.slF4JLogger,
plugin.pluginDataPath,
"backup.yaml",
)
return@single Yaml.default.decodeFromStream(
plugin.loadConfigurationWithDefault(
plugin,
BackupConfig.serializer(),
configPath.inputStream()
"backup.yaml"
)
}
single {

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.features.gameplay
package gay.pizza.foundation.core.features.gameplay
import kotlinx.serialization.Serializable

View File

@ -1,9 +1,6 @@
package cloud.kubelet.foundation.core.features.gameplay
package gay.pizza.foundation.core.features.gameplay
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
import cloud.kubelet.foundation.core.abstraction.Feature
import com.charleskorn.kaml.Yaml
import gay.pizza.foundation.core.abstraction.Feature
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.entity.EntityType
@ -17,21 +14,16 @@ import org.bukkit.event.player.PlayerInteractEntityEvent
import org.bukkit.inventory.ItemStack
import org.koin.core.component.inject
import org.koin.dsl.module
import kotlin.io.path.inputStream
class GameplayFeature : Feature() {
private val config by inject<GameplayConfig>()
override fun module() = module {
single {
val configPath = Util.copyDefaultConfig<FoundationCorePlugin>(
plugin.slF4JLogger,
plugin.pluginDataPath,
"gameplay.yaml",
)
return@single Yaml.default.decodeFromStream(
plugin.loadConfigurationWithDefault(
plugin,
GameplayConfig.serializer(),
configPath.inputStream()
"gameplay.yaml"
)
}
}
@ -60,12 +52,10 @@ class GameplayFeature : Feature() {
private fun onPlayerInteractEntity(event: PlayerInteractEntityEvent) {
val mainHandItem = event.player.inventory.itemInMainHand
val hasLead = mainHandItem.type == Material.LEAD
val isLivingEntity = event.rightClicked is LivingEntity
val livingEntity = event.rightClicked as? LivingEntity
// If leads are allowed on all mobs, then start leading the mob.
if (config.mobs.allowLeads && hasLead && isLivingEntity) {
val livingEntity = event.rightClicked as LivingEntity
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.

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

@ -1,6 +1,6 @@
package cloud.kubelet.foundation.core.features.persist
package gay.pizza.foundation.core.features.persist
import cloud.kubelet.foundation.core.features.stats.StatsFeature
import gay.pizza.foundation.core.features.stats.StatsFeature
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
@ -24,7 +24,7 @@ class PersistentStoreCommand(
when (args[0]) {
"stats" -> {
statsFeature.persistence.value.stores.forEach { (name, store) ->
statsFeature.persistence.stores.forEach { (name, store) ->
val counts = store.transact {
entityTypes.associateWith { type -> getAll(type).size() }.toSortedMap()
}
@ -43,7 +43,7 @@ class PersistentStoreCommand(
val storeName = args[1]
val entityTypeName = args[2]
val store = statsFeature.persistence.value.store(storeName)
val store = statsFeature.persistence.store(storeName)
store.transact {
val entities = getAll(entityTypeName).take(3)
for (entity in entities) {
@ -62,7 +62,7 @@ class PersistentStoreCommand(
val storeName = args[1]
val entityTypeName = args[2]
val store = statsFeature.persistence.value.store(storeName)
val store = statsFeature.persistence.store(storeName)
store.transact {
store.deleteAllEntities(entityTypeName)
}

View File

@ -1,7 +1,7 @@
package cloud.kubelet.foundation.core.features.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)
}

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