mirror of
https://github.com/PlayPro/CoreProtect.git
synced 2026-11-04 16:40:13 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dffa04fc4c | ||
|
|
2d484dffff | ||
|
|
38b6d87220 | ||
|
|
bc46794bba | ||
|
|
9d93f9ca44 | ||
|
|
0e3577da0d | ||
|
|
61f0282596 | ||
|
|
55576f9c3a | ||
|
|
5407e582a1 | ||
|
|
d76b254af4 | ||
|
|
e24d36bc82 | ||
|
|
616ed04ca7 | ||
|
|
2dc66e1464 | ||
|
|
4b1e9ab334 | ||
|
|
d5db22c23e | ||
|
|
1490dc7c89 | ||
|
|
af78ea00d4 | ||
|
|
4c8038cd26 | ||
|
|
29d22d5a58 | ||
|
|
4e70d63978 | ||
|
|
00e7b3819b | ||
|
|
079b97405b | ||
|
|
48c9bf3c0f | ||
|
|
804e398eb1 | ||
|
|
18643de5a1 | ||
|
|
2863de7f30 | ||
|
|
80caab5938 | ||
|
|
4274f88d0a | ||
|
|
2f50109bdd | ||
|
|
6e9c8302fa | ||
|
|
7582ab4d8e | ||
|
|
ac94648185 | ||
|
|
6a27fbfaba | ||
|
|
d5cad31d6c | ||
|
|
e518b2ae02 | ||
|
|
0d96edbf2b | ||
|
|
bdc53a1250 | ||
|
|
35f64f09a4 | ||
|
|
fc5a12c0b7 | ||
|
|
335fec2036 | ||
|
|
72d5655ee9 | ||
|
|
97fced4cb6 | ||
|
|
96eec2b768 | ||
|
|
bba6f9f522 | ||
|
|
3314d993a6 | ||
|
|
68739a2596 | ||
|
|
4e5ddd2334 | ||
|
|
bbe5917ec5 | ||
|
|
78fbf12d9e | ||
|
|
56ba90b58b | ||
|
|
91f08590d0 | ||
|
|
3af1079b51 | ||
|
|
37be5ebf15 | ||
|
|
5db696103a | ||
|
|
2f719054eb | ||
|
|
46b56d577a | ||
|
|
0ec76561dc | ||
|
|
9cf12ee5bf | ||
|
|
5318bd2031 | ||
|
|
94ecdb3427 | ||
|
|
d664ece2a8 | ||
|
|
11b4795bf3 | ||
|
|
45da5eee0c | ||
|
|
645bdb52d8 | ||
|
|
0ac4761a3a | ||
|
|
678b27c714 | ||
|
|
5e036528a9 | ||
|
|
dbcdbd853d | ||
|
|
9e3a1ec546 | ||
|
|
58587ed671 | ||
|
|
79ba793626 | ||
|
|
852574db6b | ||
|
|
2765bf502a | ||
|
|
1db3a39711 | ||
|
|
6b1de1b947 | ||
|
|
2251c758ac | ||
|
|
9dd653c72b | ||
|
|
ac20d00d62 | ||
|
|
a27026c7a6 | ||
|
|
3e04b389f2 | ||
|
|
d93827f184 | ||
|
|
ce556227c0 | ||
|
|
6a40286a59 | ||
|
|
f55bb32cbf | ||
|
|
5b0e4c3f7a | ||
|
|
17a9cb5a4a | ||
|
|
d58bf0732d | ||
|
|
9e1b74a49c | ||
|
|
8f6de4d2af | ||
|
|
af4296c1e6 | ||
|
|
06863f197c | ||
|
|
e263556f70 | ||
|
|
867fb3d0f3 | ||
|
|
37a67f2594 | ||
|
|
36b30bc5f3 | ||
|
|
5b012504a5 | ||
|
|
0944d31357 | ||
|
|
464aabbd1e | ||
|
|
f0f355f1ba | ||
|
|
7dd70c45d5 | ||
|
|
548acc16dd | ||
|
|
6a5c46673c | ||
|
|
edea5f105b | ||
|
|
e97878927e | ||
|
|
9b57624726 | ||
|
|
d9f728d917 | ||
|
|
6138dffe7a | ||
|
|
2405e7cf76 | ||
|
|
d2fb0bd092 | ||
|
|
b95fb65dde | ||
|
|
335e1deca1 | ||
|
|
6d74208f6a | ||
|
|
fa7ed2a397 | ||
|
|
aa788e454c | ||
|
|
eb3866cc28 | ||
|
|
834281d5f3 | ||
|
|
b1edbbbf02 | ||
|
|
e3be1520fa | ||
|
|
29f25cf1de | ||
|
|
8adde82882 | ||
|
|
f2eb0b0331 | ||
|
|
0bb1d5dcb7 | ||
|
|
d47457fa90 | ||
|
|
078adf535f | ||
|
|
49262d9e28 | ||
|
|
5ff8072ddc | ||
|
|
a34e56e9d6 | ||
|
|
b745a6099a | ||
|
|
2a9f1234b3 | ||
|
|
eefdd95776 | ||
|
|
b0b14cd270 | ||
|
|
a95c0f93b0 | ||
|
|
ccb7d5d85c | ||
|
|
79d904af88 | ||
|
|
43a8986ca8 | ||
|
|
6d1a4ac520 | ||
|
|
438d6680a8 | ||
|
|
3cb2f205d9 | ||
|
|
fda960a18c | ||
|
|
6731831283 | ||
|
|
130bb803e3 | ||
|
|
bca5fe5305 | ||
|
|
aa2d0a1e46 | ||
|
|
197e5b34a4 | ||
|
|
3d1594e116 | ||
|
|
c25621622e | ||
|
|
7ad4a77149 | ||
|
|
e4c0ea88df | ||
|
|
39f66412d6 | ||
|
|
bab8dca58a | ||
|
|
d95e70eebb | ||
|
|
3151a25f3e | ||
|
|
52a0e45c61 | ||
|
|
956828b5d0 | ||
|
|
b5f534fd2c | ||
|
|
4ecab0b101 | ||
|
|
2f196fb385 | ||
|
|
32e074666d | ||
|
|
6f86e08849 | ||
|
|
3a95881ea4 | ||
|
|
04575029c9 | ||
|
|
9d04c025be | ||
|
|
a0ae242dac | ||
|
|
4c2c304329 | ||
|
|
943d471269 | ||
|
|
948ba32e77 | ||
|
|
7d47684afb | ||
|
|
3da2bc554d | ||
|
|
ec699a5a34 | ||
|
|
9a37a0d33a | ||
|
|
5ed0863a87 | ||
|
|
2f4eeeffca | ||
|
|
b3f5959af4 | ||
|
|
eee8d1c39b | ||
|
|
15aaaf8051 | ||
|
|
2429967b79 | ||
|
|
68d3f19d93 | ||
|
|
e2f8da9070 | ||
|
|
b7a662d137 | ||
|
|
cddbc398a6 | ||
|
|
1893eac47a | ||
|
|
305e6de3ac | ||
|
|
5a8b0f93d3 | ||
|
|
1a9b542517 | ||
|
|
8d5b0a20b6 | ||
|
|
39e82953b4 | ||
|
|
bdfd6b1576 | ||
|
|
2291e0d9c7 | ||
|
|
a8aeeca62c | ||
|
|
e7197123aa | ||
|
|
6e6f8d203b | ||
|
|
cb09ddeb0b | ||
|
|
b7a452bf03 | ||
|
|
7f7de03f54 | ||
|
|
ec96019bc2 | ||
|
|
5b2e8883ea | ||
|
|
6571e91c56 | ||
|
|
434d42b547 | ||
|
|
ba96dd1d19 | ||
|
|
7a730a76fc | ||
|
|
6994ec2ac4 | ||
|
|
3914e5e989 | ||
|
|
ab939cfa6f | ||
|
|
39005ee978 | ||
|
|
acadd24c70 | ||
|
|
ab4c405dd2 | ||
|
|
fe0274b069 | ||
|
|
1bd0a0f8b6 | ||
|
|
b6acf109d4 | ||
|
|
6d1ddb8580 | ||
|
|
6d60951cb9 | ||
|
|
3f7c892153 | ||
|
|
962ab485ee | ||
|
|
559ee73edf | ||
|
|
5c561c866d | ||
|
|
78d8301751 | ||
|
|
688b4b930d | ||
|
|
cf04a42d3e | ||
|
|
bd5c1f33b7 | ||
|
|
a658c32ce4 | ||
|
|
592a4f25e4 | ||
|
|
29256b47e6 | ||
|
|
9924a9376e | ||
|
|
cebcc8655a | ||
|
|
3be769ed96 | ||
|
|
3ffece26a3 | ||
|
|
7eaa88eb7f | ||
|
|
c4d3dda0ad | ||
|
|
8b0e735313 | ||
|
|
34ada262c2 | ||
|
|
d7aa696c28 | ||
|
|
67033ac08d | ||
|
|
2c30ba626a | ||
|
|
4ae0affe4b | ||
|
|
0d47338038 | ||
|
|
afbbbd5aab | ||
|
|
cce8c9eb51 | ||
|
|
eaae2afaf8 | ||
|
|
a1458a38be | ||
|
|
7ccb3eff34 | ||
|
|
def5a99096 | ||
|
|
a92a050d2f | ||
|
|
14b0a712de | ||
|
|
cdc8082c3e | ||
|
|
829bcc530d | ||
|
|
aa2a3b9e8e | ||
|
|
eee96fb5f9 | ||
|
|
fc91bc558c | ||
|
|
127170d7b6 | ||
|
|
7caa617a34 | ||
|
|
7c7013f49f | ||
|
|
4d311f5248 | ||
|
|
f5ac148b57 | ||
|
|
e852d75264 | ||
|
|
383caedff5 | ||
|
|
bd449d1b89 | ||
|
|
c706cf5b18 | ||
|
|
d69fffbce0 | ||
|
|
ff36b6e0e6 | ||
|
|
a90b0ea18a | ||
|
|
7dbd928edc | ||
|
|
99130fdcdb | ||
|
|
42a2965cd1 | ||
|
|
971ff57496 | ||
|
|
6c04867eb8 | ||
|
|
b83b39feae | ||
|
|
80a0f24e10 | ||
|
|
9cadca9acb | ||
|
|
0c389e92f3 | ||
|
|
db94ad70eb | ||
|
|
eff43b7d6f | ||
|
|
f0aad273f1 | ||
|
|
bf0e9cf661 | ||
|
|
71b673756f | ||
|
|
2f794d6398 | ||
|
|
f3a1d95b77 | ||
|
|
8c79b3a867 | ||
|
|
d87ecaee42 | ||
|
|
dc4e9f8d60 | ||
|
|
c145fca68c | ||
|
|
b0856b51a5 | ||
|
|
7386f4bd50 | ||
|
|
0f0ceccb7b | ||
|
|
893ce9af46 | ||
|
|
609990e344 | ||
|
|
38166fd381 |
No files matched your search
@@ -10,19 +10,14 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
distribution: 'temurin'
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.m2
|
||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-m2
|
||||
cache: 'maven'
|
||||
- name: Build
|
||||
run: mvn -B verify
|
||||
+13
@@ -173,11 +173,15 @@ Desktop.ini
|
||||
## Misc.
|
||||
#############
|
||||
|
||||
.claude
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
.cursorrules
|
||||
.windsurfrules
|
||||
.clinerules
|
||||
context.json
|
||||
context.yml
|
||||
issues.md
|
||||
run.sh
|
||||
run_*.sh
|
||||
no-log4j2.xml
|
||||
@@ -213,8 +217,17 @@ hs_err_pid*
|
||||
# Documentation dev folder
|
||||
docs/dev/
|
||||
|
||||
# symlinks
|
||||
CoreProtect-TestAPI
|
||||
CoreProtect-Fabric
|
||||
CoreProtect-Hytale
|
||||
publish/
|
||||
|
||||
# Extensions
|
||||
/src/main/java/net/coreprotect/utility/extensions
|
||||
!/src/main/java/net/coreprotect/utility/extensions/Extensions.java
|
||||
/.apt_generated/
|
||||
/.apt_generated_tests/
|
||||
|
||||
# Server side files
|
||||
server/*
|
||||
@@ -1,55 +1,206 @@
|
||||

|
||||
|
||||
[](LICENSE)
|
||||
[](https://github.com/PlayPro/CoreProtect/blob/master/LICENSE)
|
||||
[](https://github.com/PlayPro/CoreProtect/actions)
|
||||
[](https://app.netlify.com/sites/coreprotect/deploys)
|
||||
[](https://www.codefactor.io/repository/github/playpro/coreprotect)
|
||||
[](https://discord.gg/b4DZ4jy)
|
||||
|
||||
CoreProtect
|
||||
===========
|
||||
# CoreProtect
|
||||
|
||||
CoreProtect is a blazing fast data logging and anti-griefing tool for Minecraft servers.
|
||||
CoreProtect is a fast, efficient data logging and anti-griefing tool. Inspect, lookup, rollback, and restore any amount of damage without impacting your server performance.
|
||||
|
||||
For a detailed description of the plugin, please visit [coreprotect.net](https://coreprotect.net).
|
||||
CoreProtect is the [#1 anti-griefing plugin](https://bstats.org/plugin/bukkit/CoreProtect), and has been actively developed since early 2012.
|
||||
|
||||
| Quick Links | |
|
||||
| --- | --- |
|
||||
| CoreProtect Discord: | [discord.gg/b4DZ4jy](https://discord.gg/b4DZ4jy) |
|
||||
| CoreProtect Patreon: | [patreon.com/coreprotect](https://www.patreon.com/coreprotect) |
|
||||
| CoreProtect Documentation: | [docs.coreprotect.net](https://docs.coreprotect.net) |
|
||||
| Downloads for MC 1.14 - 1.21: | [coreprotect.net/latest](https://coreprotect.net/latest/) |
|
||||
| Downloads for MC 1.14 - 26.2: | [coreprotect.net/latest](https://coreprotect.net/latest/) |
|
||||
| Downloads for MC 1.8 - 1.12: | [coreprotect.net/legacy](https://coreprotect.net/legacy/) |
|
||||
| Downloads for Fabric: | [coreprotect.net/fabric](https://coreprotect.net/fabric) |
|
||||
| Downloads for Hytale: | [coreprotect.net/hytale](https://coreprotect.net/hytale) |
|
||||
|
||||
bStats
|
||||
------
|
||||
[](https://bstats.org/plugin/bukkit/CoreProtect)
|
||||
## API
|
||||
|
||||
API
|
||||
------
|
||||
### [API Documentation](https://docs.coreprotect.net/api/)
|
||||
|
||||
### Dependency Information
|
||||
Maven
|
||||
|
||||
Maven:
|
||||
|
||||
```xml
|
||||
<repository>
|
||||
<id>playpro-repo</id>
|
||||
<url>https://maven.playpro.com</url>
|
||||
</repository>
|
||||
```
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.coreprotect</groupId>
|
||||
<artifactId>coreprotect</artifactId>
|
||||
<version>23.1</version>
|
||||
<version>24.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Contributing
|
||||
------
|
||||
## Donation Keys
|
||||
|
||||
To support the project and obtain a donation key, visit [coreprotect.net/donate](https://coreprotect.net/donate/).
|
||||
|
||||
## Other Plugins
|
||||
|
||||
* DarkerNights: [spigotmc.org/resources/darkernights.87814](https://www.spigotmc.org/resources/darkernights.87814/)
|
||||
* TransitTubes: [patreon.com/posts/75731668](https://www.patreon.com/posts/75731668)
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
[](https://youtu.be/JwijCiueZ3Y)
|
||||
|
||||
## Core Features
|
||||
|
||||
* Fast, efficient data logging.
|
||||
* Rollbacks and restores with lag-free processing.
|
||||
* Multi-threaded to ensure server performance is never impacted.
|
||||
* No configuration required. Install the plugin, and you're good to go.
|
||||
* Embedded DuckDB columnar storage by default, with optional ClickHouse, SQLite, and MySQL support.
|
||||
* Supports Bukkit, Spigot, Paper, Folia, MultiPaper, and more.
|
||||
* Permission system support and advanced lookup permissions.
|
||||
* Easy-to-use commands and inspector.
|
||||
* Advanced lookup filters and operation limits by radius, world, or WorldEdit selection.
|
||||
* Paginated results with clickable pagination.
|
||||
* Preview rollbacks and restores, and undo rollbacks by restoring the same data.
|
||||
* Rollback or restore specific users, multiple users, or global activity.
|
||||
* Inventory rollback support.
|
||||
* Purge old data by time, world, and block type.
|
||||
* Multi-world support.
|
||||
* Configurable global and per-world logging settings.
|
||||
* Safe default parameters.
|
||||
* Include or exclude specific blocks, items, entities, or users, plus logging blacklists.
|
||||
* WorldEdit and supported FAWE logging.
|
||||
* Enhanced item lookup tooltips.
|
||||
* Localization and translation support.
|
||||
* Automatic update checking and error reporting.
|
||||
* Developer and networking API support.
|
||||
* *...and much more!*
|
||||
|
||||
## What does it log?
|
||||
|
||||
### Blocks and World Changes
|
||||
|
||||
* Blocks placed or broken by players.
|
||||
* Natural block breakage, such as attached blocks breaking when their support is removed.
|
||||
* Block movement, including falling sand, gravel, and piston movement.
|
||||
* Water and lava placement, removal, and flow.
|
||||
* Liquid tracking, associating flowing liquids with the player who placed the source.
|
||||
* Fire ignition from flint and steel or fire charges, block burning, and fire fade.
|
||||
* Optional natural fire extinguishing.
|
||||
* TNT, Creeper, Ghast, and other explosions, plus primed TNT removal.
|
||||
* Entities changing blocks, including Endermen, Ender Dragons, and zombies breaking doors.
|
||||
* Leaf decay.
|
||||
* Tree growth linked back to the player who planted the sapling.
|
||||
* Mushroom, vine, amethyst, chorus, and bamboo growth.
|
||||
* Sculk spread from sculk catalysts.
|
||||
* Nether portal and other natural portal generation.
|
||||
* Crops trampled by players and turtle eggs trampled by players or entities.
|
||||
* Snow generated by snow golems.
|
||||
* Liquid-formed blocks such as obsidian, cobblestone, and concrete.
|
||||
* Flowers placed in flower pots.
|
||||
* Dragon egg teleportation.
|
||||
* Suspicious sand and gravel brushing or destruction.
|
||||
* Custom blocks, such as CraftEngine blocks.
|
||||
|
||||
### Containers, Inventories, and Items
|
||||
|
||||
* Items taken from or placed into vanilla or supported third-party containers.
|
||||
* Hopper, dropper, and dispenser transactions, including container transfers.
|
||||
* Player inventory transactions, including items dropped, thrown, shot, or picked up.
|
||||
* Items broken, created, destroyed, deposited, or withdrawn by players.
|
||||
* Items placed on campfires and lectern book transactions.
|
||||
* Chiseled bookshelf, decorated pot, and shelf inventory transactions.
|
||||
* Copper Golem chest and jukebox transactions, plus crafter slot changes.
|
||||
* Bundle actions through the `#bundle` tag.
|
||||
* Items crafted or traded with villagers.
|
||||
* Allay item exchanges.
|
||||
* Paintings and item frames.
|
||||
|
||||
### Entities and Player Activity
|
||||
|
||||
* Entity deaths and detailed death reasons.
|
||||
* Player-caused entity kills, including animals and monsters.
|
||||
* Armor stands and End Crystals.
|
||||
* Villagers killed by lightning, gossip data, and job-site memories.
|
||||
* Thrown eggs, snowballs, potions, ender pearls, tridents, and wind charges.
|
||||
* Player interactions with doors, buttons, levers, containers, bookshelves, and jukeboxes.
|
||||
* Sign text, color, glow, wax, and modern double-sided signs.
|
||||
* Player chat messages and commands.
|
||||
* Player sessions including logins and logouts, player deaths, and username changes.
|
||||
* Skull skin texture data on Paper servers and custom skull textures.
|
||||
* WorldEdit changes and supported FAWE clipboard pastes.
|
||||
* *...and more!*
|
||||
|
||||
## How to use the inspector
|
||||
|
||||
Once you have the inspector enabled with `/core inspect` or `/co i`, you can do the following:
|
||||
|
||||
* Left-click a block to see who placed that block.
|
||||
* Right-click a block to see what adjacent block was removed.
|
||||
* Right-click while placing a block in a location to see what block was removed at that location.
|
||||
* Right-click while placing a block in another block to see who placed it. For example, place dirt in water to see who placed the water.
|
||||
* Right-click a door, button, lever, container, or similar block to see who last used it.
|
||||
|
||||
## A Few Reviews
|
||||
|
||||
* *"It's easy to use and lightning fast when it comes to rollbacks."*
|
||||
* *"I've been running a MC server since before Bukkit, so I've used just about every block logger out there. CoreProtect is the best and the fastest."*
|
||||
* *"Go with CoreProtect. It's fast, updated frequently, and logs WorldEdit."*
|
||||
* *"Overall it felt more natural, simpler, and much faster to use CoreProtect than Prism, which just doesn't compare."*
|
||||
* *"CoreProtect brings complex logging and rollbacks, whilst at the same time keeping a low profile."*
|
||||
* *Featured on [Linus Tech Tips](https://www.userfolio.com/uploads/coreprotect-linus.png).*
|
||||
|
||||
## Useful Links
|
||||
|
||||
**Documentation**
|
||||
https://docs.coreprotect.net
|
||||
|
||||
**Commands**
|
||||
https://docs.coreprotect.net/commands/
|
||||
|
||||
**Permissions**
|
||||
https://docs.coreprotect.net/permissions/
|
||||
|
||||
**API Documentation**
|
||||
https://docs.coreprotect.net/api/
|
||||
|
||||
**Feedback & Support**
|
||||
https://github.com/PlayPro/CoreProtect/issues
|
||||
|
||||
**Discord**
|
||||
Join us on Discord: https://discord.gg/b4DZ4jy
|
||||
|
||||
**Donate**
|
||||
Enjoy using CoreProtect and want to show your support? [Join our Patreon!](https://www.patreon.com/coreprotect)
|
||||
|
||||
**Sponsors**
|
||||
Thanks to [HostHorde](https://www.hosthorde.com) for sponsoring CoreProtect!
|
||||
|
||||
## Bug Reports
|
||||
|
||||
For any bug reports, please submit a ticket here:
|
||||
https://github.com/PlayPro/CoreProtect/issues
|
||||
|
||||
Thanks for your support!
|
||||
|
||||
## bStats
|
||||
|
||||
[](https://bstats.org/plugin/bukkit/CoreProtect)
|
||||
|
||||
## Contributing
|
||||
|
||||
CoreProtect is an open source project, and gladly accepts community contributions.
|
||||
|
||||
If you'd like to contribute, please read our contributing guidelines here: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
[](CONTRIBUTING.md#code-of-conduct)
|
||||
[](CONTRIBUTING.md#code-of-conduct)
|
||||
+3
-3
@@ -4,8 +4,8 @@ The CoreProtect API enables you to log your own block changes, perform lookups,
|
||||
|
||||
| API Details | |
|
||||
| --- | --- |
|
||||
| **API Version:** | 10 |
|
||||
| **Plugin Version:** | v22.4+ |
|
||||
| **API Version:** | 13 |
|
||||
| **Plugin Version:** | v25.0+ |
|
||||
| **Maven:** | [maven.playpro.com](https://maven.playpro.com) |
|
||||
|
||||
*Documentation for the API version 10 can be found [here](/api/version/v10/).*
|
||||
*Documentation for the API version 13 can be found [here](/api/version/v13/).*
|
||||
@@ -5,7 +5,7 @@ The CoreProtect API enables you to log your own block changes, perform lookups,
|
||||
| API Details | |
|
||||
| --- | --- |
|
||||
| **API Version:** | 11 |
|
||||
| **Plugin Version:** | v24.0+ |
|
||||
| **Plugin Version:** | v23.1+ |
|
||||
| **Maven:** | [maven.playpro.com](https://maven.playpro.com) |
|
||||
|
||||
---
|
||||
@@ -28,8 +28,8 @@ CoreProtectPreLogEvent(String user)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Ensure you're using CoreProtect 24.0 or higher. Add it as an external jar to your plugin in your IDE.
|
||||
Alternatively, if using Maven, you can add it via the repository https://maven.playpro.com (net.coreprotect, 24.0).
|
||||
Ensure you're using CoreProtect 23.1 or higher. Add it as an external jar to your plugin in your IDE.
|
||||
Alternatively, if using Maven, you can add it via the repository https://maven.playpro.com (net.coreprotect, 23.1).
|
||||
|
||||
The first thing you need to do is get access to CoreProtect. You can do this by using code similar to the following:
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,964 @@
|
||||
# API Version 13
|
||||
|
||||
The CoreProtect API enables plugins to log changes, look up recorded activity, and perform rollbacks and restores.
|
||||
|
||||
| API Details | |
|
||||
| --- | --- |
|
||||
| **API Version** | 13 |
|
||||
| **Plugin Version** | v25.0+ |
|
||||
| **Maven** | [maven.playpro.com](https://maven.playpro.com) |
|
||||
|
||||
## Upgrading from API v12
|
||||
|
||||
All API v12 methods remain available. API v13 adds the following:
|
||||
|
||||
| Addition | Description |
|
||||
| --- | --- |
|
||||
| `blockLookup(LookupOptions)` | Searches block breaks, placements, and interactions at an exact location, within a radius, in a world, or across all worlds. |
|
||||
| `entityLookup(LookupOptions)` and `EntityResult` | Searches entity spawns and kills, with action and entity-type filters. |
|
||||
| `LookupOptions.world(World)` | Selects a whole world for spatial typed lookups. |
|
||||
| User and material lists | Adds `users`, `excludeUsers`, `includeMaterials`, and `excludeMaterials` to typed lookup options. |
|
||||
| Typed action filters | Adds `blockActions`, `entityActions`, `containerActions`, `itemActions`, `inventoryActions`, and `sessionActions`. |
|
||||
| Text prefix filters | Adds `textStartsWithAny` and `textStartsWithNone` for chat, command, and sign lookups. |
|
||||
| Entity-spawn action | Adds `CoreProtectAction.ENTITY_SPAWN` with ID `13`. Legacy lookup, rollback, and restore methods accept this action explicitly. |
|
||||
| Entity result parsing | `BlockResult` and `ParseResult` expose `getEntityType()` for entity kills and spawns. Their material and block-data getters return `null` for entity rows. |
|
||||
| Tracked containers | Container and inventory lookups include transactions in tracked boats and minecarts. |
|
||||
| Pre-log events | Adds `CoreProtectPreLogEvent.Action.ENTITY_SPAWN` and `ENTITY_INTERACTION`. |
|
||||
|
||||
The existing `blockLookup(Block, LookupOptions)` overload retains entity events for compatibility. The new `blockLookup(LookupOptions)` overload returns only block events; use `entityLookup` for entities.
|
||||
|
||||
Entity and tracked-container searches can match persisted current/final positions as well as original event positions. See [entity lookups](#entity-lookups) and [container lookups](#container-lookups) for the coordinates returned by each method.
|
||||
|
||||
## Getting Started
|
||||
|
||||
CoreProtect 25.0 or higher is required to use API v13.
|
||||
|
||||
### Add the dependency
|
||||
|
||||
Compile against a CoreProtect build that provides API v13. You can add the CoreProtect jar to your IDE or use the Maven repository:
|
||||
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>playpro</id>
|
||||
<url>https://maven.playpro.com</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.coreprotect</groupId>
|
||||
<artifactId>CoreProtect</artifactId>
|
||||
<version>25.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
Use the published version matching the CoreProtect build you target. For prerelease builds, use that build's published version instead of `25.0`. CoreProtect runs as a separate server plugin; do not package its classes inside your plugin.
|
||||
|
||||
Add CoreProtect to your plugin's `plugin.yml` so it loads first:
|
||||
|
||||
```yaml
|
||||
depend: [CoreProtect]
|
||||
```
|
||||
|
||||
Use `softdepend: [CoreProtect]` instead if your plugin also works without CoreProtect, and only initialize the integration when CoreProtect is present.
|
||||
|
||||
### Obtain the API
|
||||
|
||||
The following helper belongs in your `JavaPlugin` class:
|
||||
|
||||
```java
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import net.coreprotect.CoreProtect;
|
||||
import net.coreprotect.CoreProtectAPI;
|
||||
|
||||
private CoreProtectAPI getCoreProtect() {
|
||||
Plugin plugin = getServer().getPluginManager().getPlugin("CoreProtect");
|
||||
if (!(plugin instanceof CoreProtect)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CoreProtectAPI api = ((CoreProtect) plugin).getAPI();
|
||||
if (!api.isEnabled() || api.APIVersion() < 13) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
```
|
||||
|
||||
Call it during your plugin's initialization:
|
||||
|
||||
```java
|
||||
CoreProtectAPI api = getCoreProtect();
|
||||
if (api != null) {
|
||||
api.testAPI();
|
||||
}
|
||||
```
|
||||
|
||||
Options and action enums are in `net.coreprotect.api`. Typed result classes are in `net.coreprotect.api.result`. Events are in `net.coreprotect.event`.
|
||||
|
||||
### Threading and return values
|
||||
|
||||
Database lookups run synchronously on the calling thread. Run database searches asynchronously to avoid blocking the server thread. Both `performRollback` and `performRestore` **must** be called asynchronously; CoreProtect schedules the required world updates internally.
|
||||
|
||||
Capture locations and any required Bukkit state on the appropriate server thread before starting database work. Logging methods that inspect live blocks, players, or inventories should run on that server thread. A `BlockState` overload does not make every logging operation safe to call asynchronously.
|
||||
|
||||
Typed lookups and the legacy `blockLookup`, `performLookup`, `performPartialLookup`, `performRollback`, and `performRestore` methods return `null` when the API is disabled. `queueLookup` and the legacy `sessionLookup(String, int)` instead return an empty list. Legacy searches can also return `null` for invalid search parameters. An empty list can mean no matching records or an operation that could not proceed; it is not a separate success status. Logging methods return a boolean indicating whether the request was accepted, not whether it has already been written to the database.
|
||||
|
||||
API rollback and restore methods perform no changes when database maintenance or another conflicting operation prevents them from starting. This includes an active database purge.
|
||||
|
||||
## API Overview
|
||||
|
||||
### Available methods
|
||||
|
||||
The table below lists the supported methods on `CoreProtectAPI`. Click a method to jump to its parameters and behavior.
|
||||
|
||||
| Method | Return type | Purpose |
|
||||
| --- | --- | --- |
|
||||
| [`APIVersion()`](#apiversion) | `int` | Returns `13` for this API version. |
|
||||
| [`isEnabled()`](#isenabled) | `boolean` | Checks whether the API is enabled in CoreProtect's configuration. |
|
||||
| [`testAPI()`](#testapi) | `void` | Prints an API test message to the console. |
|
||||
| [`blockLookup(Block, int)`](#legacy-block-session-and-queue-lookups) | `List<String[]>` | Legacy lookup at a block. |
|
||||
| [`blockLookup(Block, LookupOptions)`](#block-lookups) | `List<BlockResult>` | Typed lookup at a block, retaining legacy entity events. |
|
||||
| [`blockLookup(LookupOptions)`](#block-lookups) | `List<BlockResult>` | Typed block-only lookup. |
|
||||
| [`entityLookup(LookupOptions)`](#entity-lookups) | `List<EntityResult>` | Entity spawn and kill history. |
|
||||
| [`containerLookup(Location, int)`](#container-lookups) | `List<ContainerResult>` | Container history at a location. |
|
||||
| [`containerLookup(LookupOptions)`](#container-lookups) | `List<ContainerResult>` | Container history with shared filters. |
|
||||
| [`itemLookup(LookupOptions)`](#item-lookups) | `List<ItemResult>` | World item transactions. |
|
||||
| [`inventoryLookup(LookupOptions)`](#inventory-lookups) | `List<InventoryResult>` | Transactions affecting player inventories. |
|
||||
| [`chatLookup(String, int)`](#chat-and-command-lookups) / [`chatLookup(LookupOptions)`](#chat-and-command-lookups) | `List<MessageResult>` | Chat history. |
|
||||
| [`commandLookup(String, int)`](#chat-and-command-lookups) / [`commandLookup(LookupOptions)`](#chat-and-command-lookups) | `List<MessageResult>` | Command history. |
|
||||
| [`signLookup(Location, int)`](#sign-lookups) / [`signLookup(LookupOptions)`](#sign-lookups) | `List<SignResult>` | Written sign text. |
|
||||
| [`sessionLookup(String, int)`](#legacy-block-session-and-queue-lookups) | `List<String[]>` | Legacy login/logout history. |
|
||||
| [`sessionLookup(LookupOptions)`](#session-lookups) | `List<SessionResult>` | Typed login/logout history. |
|
||||
| [`usernameLookup(String, int)`](#username-lookups) / [`usernameLookup(LookupOptions)`](#username-lookups) | `List<UsernameResult>` | Username history. |
|
||||
| [`queueLookup(Block)`](#legacy-block-session-and-queue-lookups) | `List<String[]>` | Pending block changes in the consumer queue. |
|
||||
| [`parseResult(String[])`](#parsing-legacy-rows) | `CoreProtectAPI.ParseResult` | Parses a legacy result row. |
|
||||
| [`performLookup(...)`](#full-and-partial-searches) / [`performPartialLookup(...)`](#full-and-partial-searches) | `List<String[]>` | Legacy block/entity searches, with optional pagination. |
|
||||
| [`performRollback(...)`](#rollback-and-restore-behavior) / [`performRestore(...)`](#rollback-and-restore-behavior) | `List<String[]>` | Rolls back or restores matching changes. |
|
||||
| [`hasPlaced(String, Block, int, int)`](#placement-and-removal-checks) / [`hasRemoved(String, Block, int, int)`](#placement-and-removal-checks) | `boolean` | Checks a player's persisted block history. |
|
||||
| [`logChat(Player, String)`](#chat-and-commands) / [`logCommand(Player, String)`](#chat-and-commands) | `boolean` | Queues player messages or commands. |
|
||||
| [`logPlacement(...)`](#block-placement-and-removal) / [`logRemoval(...)`](#block-placement-and-removal) | `boolean` | Queues block changes. |
|
||||
| [`logContainerTransaction(String, Location)`](#container-transactions) | `boolean` | Captures a block inventory before your changes. |
|
||||
| [`logInteraction(String, Location)`](#block-interactions) | `boolean` | Queues a block interaction. |
|
||||
| [`performPurge(int)`](#purging-history) | `void` | Dispatches a database purge command. |
|
||||
|
||||
### Shared typed lookup options
|
||||
|
||||
Create options with `LookupOptions.builder()` and finish with `build()`:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.user("Notch")
|
||||
.time(3600)
|
||||
.radius(location, 5)
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
```
|
||||
|
||||
| Builder method | Behavior |
|
||||
| --- | --- |
|
||||
| `user(String user)` | Restricts the search to one user. `null`, an empty string, or `#global` imposes no single-user restriction. |
|
||||
| `users(List<String> users)` | Includes any matching user in the list. Combines with `user` as an additional restriction. |
|
||||
| `excludeUsers(List<String> users)` | Excludes matching users, including users selected by either inclusion option. |
|
||||
| `time(int seconds)` | Searches back this many seconds. Omitted, zero, or negative values impose no time restriction for typed lookups. |
|
||||
| `location(Location location)` | Searches the exact block coordinates and world. |
|
||||
| `radius(Location location, int radius)` | A positive radius searches an inclusive X/Z square around the location, at all heights. Zero or a negative radius searches the exact block coordinates. |
|
||||
| `world(World world)` | Searches the entire selected world. |
|
||||
| `limit(int offset, int count)` | Skips `offset` matching rows and returns at most `count`. The offset is zero-based. Both values must be nonnegative for the limit to apply; a count of zero returns no rows. |
|
||||
| `includeMaterials(List<Material> materials)` | Includes any matching logged block/item material in supported lookup types. |
|
||||
| `excludeMaterials(List<Material> materials)` | Excludes matching logged block/item materials. |
|
||||
| `includeEntities(List<EntityType> entities)` | Includes any matching entity type in `entityLookup`. |
|
||||
| `excludeEntities(List<EntityType> entities)` | Excludes matching entity types in `entityLookup`. |
|
||||
| `blockActions(List<BlockAction> actions)` | Selects block actions. |
|
||||
| `entityActions(List<EntityAction> actions)` | Selects entity actions. |
|
||||
| `containerActions(List<ContainerAction> actions)` | Selects container transaction actions. |
|
||||
| `itemActions(List<ItemAction> actions)` | Selects world item transaction actions. |
|
||||
| `inventoryActions(List<InventoryAction> actions)` | Selects source-specific inventory transactions. |
|
||||
| `sessionActions(List<SessionAction> actions)` | Selects login/logout actions. |
|
||||
| `textStartsWithAny(List<String> prefixes)` | Requires text to begin with at least one supplied prefix. |
|
||||
| `textStartsWithNone(List<String> prefixes)` | Rejects text beginning with any supplied prefix. |
|
||||
|
||||
The last call to `world`, `location`, or `radius` determines the search area. With no area specified, spatial typed lookups search all worlds. The `blockLookup(Block, LookupOptions)` overload always uses the supplied block's location instead.
|
||||
|
||||
All filters that apply to a lookup are combined. Empty filter lists retain the default behavior. List setters copy their inputs and reject null lists or null entries. A built options object's lists cannot be modified. Passing `null` instead of a `LookupOptions` object uses the defaults.
|
||||
|
||||
Within `users`, an empty string or `#global` removes that list's user restriction. Within `excludeUsers`, either value excludes everyone. Use an empty list when you want no exclusion.
|
||||
|
||||
Results are returned in descending event order. Limits apply after filtering; omitting a limit permits all matching rows. Use a bounded time range and pagination for large histories. Offset pagination does not hold a snapshot across separate API calls, so new records can shift later pages.
|
||||
|
||||
| Lookup | User/time/limit | Spatial filters | Material filters | Additional filters |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Block | Yes | Yes; supplied `Block` overrides options | Yes | `blockActions` |
|
||||
| Entity | Yes | Yes | No | `entityActions`, `includeEntities`, `excludeEntities` |
|
||||
| Container | Yes | Yes | Yes | `containerActions` |
|
||||
| Item | Yes | Yes | Yes | `itemActions` |
|
||||
| Inventory | Yes | Yes | Yes | `inventoryActions` |
|
||||
| Chat / command | Yes | Yes | No | Text prefixes |
|
||||
| Sign | Yes | Yes | No | Text prefixes |
|
||||
| Session | Yes | Yes | No | `sessionActions` |
|
||||
| Username | Yes | No | No | None |
|
||||
|
||||
Filters outside a lookup's scope are ignored. Username history has no location, radius, or world.
|
||||
|
||||
### Text prefix filters
|
||||
|
||||
`textStartsWithAny` matches any included prefix. `textStartsWithNone` rejects any excluded prefix, so exclusions take precedence. Either option can be used alone.
|
||||
|
||||
The API uses the same SQL matching and escaping as the in-game `f:` / `filter:` option, including the database's case-handling rules. Each API list entry is already one literal prefix: commas, leading `-`, and leading/trailing spaces are preserved. `%`, `_`, `~`, and `*` are not wildcards. The API does not parse command-style comma separators or exclusion markers.
|
||||
|
||||
Prefixes must contain at least three Unicode code points. Shorter prefixes throw `IllegalArgumentException` when the builder method is called. Empty lists impose no restriction.
|
||||
|
||||
Sign filters check each line on the logged face. An inclusion match on any line includes the entry; an exclusion match on any line excludes the whole entry. Text on the opposite face does not affect the match.
|
||||
|
||||
### Action filters
|
||||
|
||||
Use the enum associated with the lookup. Multiple values match any listed action; an omitted or empty list retains that lookup's defaults.
|
||||
|
||||
| Lookup | Enum values and IDs | Default selection |
|
||||
| --- | --- | --- |
|
||||
| Block | `BlockAction.BREAK` = `0`, `PLACE` = `1`, `INTERACTION` = `2` | All three for `blockLookup(options)`; the supplied-block overload also retains entity events unless narrowed. |
|
||||
| Entity | `EntityAction.KILL` = `3`, `SPAWN` = `13` | Both. |
|
||||
| Container | `ContainerAction.REMOVE` = `0`, `ADD` = `1` | Both. |
|
||||
| Session | `SessionAction.LOGOUT` = `0`, `LOGIN` = `1` | Both. |
|
||||
| Item | See below. | `DROP`, `PICKUP`, `REMOVE_ENDER`, `ADD_ENDER`, `THROW`, `SHOOT`. |
|
||||
| Inventory | See below. | Block placements, container transactions, and all supported item transactions. |
|
||||
|
||||
| `ItemAction` | ID | Result action string |
|
||||
| --- | --- | --- |
|
||||
| `DROP` | 2 | `drop` |
|
||||
| `PICKUP` | 3 | `pickup` |
|
||||
| `REMOVE_ENDER` | 4 | `withdraw` |
|
||||
| `ADD_ENDER` | 5 | `deposit` |
|
||||
| `THROW` | 6 | `throw` |
|
||||
| `SHOOT` | 7 | `shoot` |
|
||||
| `BREAK` | 8 | `break` |
|
||||
| `DESTROY` | 9 | `destroy` |
|
||||
| `CREATE` | 10 | `create` |
|
||||
| `SELL` | 11 | `sell` |
|
||||
| `BUY` | 12 | `buy` |
|
||||
|
||||
`BREAK`, `DESTROY`, `CREATE`, `SELL`, and `BUY` require explicit selection in `itemLookup`. They are included by default in `inventoryLookup`.
|
||||
|
||||
`InventoryAction` selects both a source and its underlying action:
|
||||
|
||||
| Source | `InventoryAction` values |
|
||||
| --- | --- |
|
||||
| Block | `BLOCK_PLACE` |
|
||||
| Container | `CONTAINER_REMOVE`, `CONTAINER_ADD` |
|
||||
| Item | `ITEM_DROP`, `ITEM_PICKUP`, `ITEM_REMOVE_ENDER`, `ITEM_ADD_ENDER`, `ITEM_THROW`, `ITEM_SHOOT`, `ITEM_BREAK`, `ITEM_DESTROY`, `ITEM_CREATE`, `ITEM_SELL`, `ITEM_BUY` |
|
||||
|
||||
Each action enum exposes `id()`. `InventoryAction` also exposes `sourceId()`. Container remove/add describes the container's contents: taking an item out of a container adds it to the player's inventory, and depositing an item removes it from the player's inventory.
|
||||
|
||||
### CoreProtectAction
|
||||
|
||||
`CoreProtectAction` names the general lookup categories. It exposes `id()`, `actionString()`, and `fromId(int)`; an unrecognized ID maps to `UNKNOWN`.
|
||||
|
||||
| Constant | ID | Action string |
|
||||
| --- | --- | --- |
|
||||
| `BLOCK_BREAK` | 0 | `break` |
|
||||
| `BLOCK_PLACE` | 1 | `place` |
|
||||
| `INTERACTION` | 2 | `click` |
|
||||
| `ENTITY_KILL` | 3 | `kill` |
|
||||
| `CONTAINER` | 4 | `container` |
|
||||
| `CHAT` | 6 | `chat` |
|
||||
| `COMMAND` | 7 | `command` |
|
||||
| `SESSION` | 8 | `session` |
|
||||
| `USERNAME` | 9 | `username` |
|
||||
| `SIGN` | 10 | `sign` |
|
||||
| `ITEM` | 11 | `item` |
|
||||
| `ENTITY_SPAWN` | 13 | `spawn` |
|
||||
| `UNKNOWN` | -1 | `unknown` |
|
||||
|
||||
The legacy `performLookup`, `performPartialLookup`, `performRollback`, and `performRestore` methods accept only block/entity action IDs `0`, `1`, `2`, `3`, and `13`. Use the dedicated typed methods for the other categories.
|
||||
|
||||
A result's `getActionId()` uses that result type's action values. For example, `SessionResult` uses `0`/`1` for logout/login, and `InventoryResult` uses `0`/`1` for removal/addition to the player's inventory. Do not interpret every result ID through `CoreProtectAction.fromId()`.
|
||||
|
||||
## API Status Methods
|
||||
|
||||
### APIVersion()
|
||||
|
||||
`int APIVersion()` returns the API version supported by the installed CoreProtect plugin. This version returns `13`. Check for `13` or higher before using API v13 methods; the API version is separate from the plugin's release version, such as `25.0`.
|
||||
|
||||
### isEnabled()
|
||||
|
||||
`boolean isEnabled()` returns `true` when the server has the CoreProtect API enabled in its configuration, and `false` when it is disabled. Check this before using the API.
|
||||
|
||||
### testAPI()
|
||||
|
||||
`void testAPI()` prints an API test message to the server console. With the default English language configuration, the message is:
|
||||
|
||||
```text
|
||||
[CoreProtect] API test successful.
|
||||
```
|
||||
|
||||
The message follows CoreProtect's language configuration. This method returns no value and does not test database reads or writes; use it to confirm that your plugin can call the API.
|
||||
|
||||
## Typed Lookup Methods
|
||||
|
||||
For convenience overloads taking `int time`, the argument is how many seconds to search back. As with `LookupOptions.time`, zero or negative values impose no time restriction.
|
||||
|
||||
### Block lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `blockLookup(LookupOptions options)` | `List<BlockResult>` |
|
||||
| `blockLookup(Block block, LookupOptions options)` | `List<BlockResult>` |
|
||||
|
||||
The options-only method searches logged block breaks, placements, and interactions. It supports exact locations, X/Z radius searches, whole-world searches, and searches across all worlds.
|
||||
|
||||
The supplied-block method always searches that block's coordinates and world. Location, radius, and world options are ignored. It retains entity kills and spawns stored at those coordinates unless an explicit `blockActions` filter narrows the search.
|
||||
|
||||
Material filters compare the logged material, regardless of the block currently at the location. Including materials selects matching block events. Excluding materials preserves entity events in the supplied-block overload. Use `getEntityType()` to read entity rows; `getType()` and `getBlockData()` return `null` for them.
|
||||
|
||||
### Entity lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `entityLookup(LookupOptions options)` | `List<EntityResult>` |
|
||||
|
||||
Returns entity spawns and kills. Use `entityActions` to select `SPAWN` or `KILL`, and `includeEntities` / `excludeEntities` to filter Bukkit `EntityType` values. Empty lists select both actions and all entity types.
|
||||
|
||||
For spawn events, location/radius filters match either the original spawn location or the persisted current/final location. Whole-world filters similarly match either world. Kill events match the location where the kill was logged. Results always expose the **original event coordinates and world**, even when a tracked position matched the filter.
|
||||
|
||||
These synchronous lookups use persisted tracking data and do not wait for a live-entity scan. Material filters and other lookup-specific action filters do not affect entity lookups.
|
||||
|
||||
The in-game `a:block` filters present placed and destroyed boats/minecarts as block changes. That command/display alias does not change API action IDs: their spawn and kill rows remain `ENTITY_SPAWN` (`13`) and `ENTITY_KILL` (`3`).
|
||||
|
||||
### Container lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `containerLookup(Location location, int time)` | `List<ContainerResult>` |
|
||||
| `containerLookup(LookupOptions options)` | `List<ContainerResult>` |
|
||||
|
||||
Searches item additions to and removals from containers. The location/time overload searches an exact location. The options overload also supports radius, world, user, material, action, and pagination filters.
|
||||
|
||||
Both methods include tracked boat and minecart container transactions. Their location filters match either the transaction's original location or the entity's persisted current/final location. Typed results for these tracked containers expose the **persisted current/final coordinates and world**. Ordinary block containers expose the transaction location.
|
||||
|
||||
Tracked positions are checkpointed when transactions are logged and during relevant entity lifecycle changes. Lookups do not wait for a live-entity scan. `containerActions` applies to both block containers and tracked entity containers.
|
||||
|
||||
### Item lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `itemLookup(LookupOptions options)` | `List<ItemResult>` |
|
||||
|
||||
Searches world item transactions, including drops, pickups, ender-chest transfers, throws, and shots by default. Use `itemActions` to select specific actions, including item breaks, destruction, creation, sales, and purchases. Those last five categories are not included in the default item lookup.
|
||||
|
||||
### Inventory lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `inventoryLookup(LookupOptions options)` | `List<InventoryResult>` |
|
||||
|
||||
Searches transactions affecting player inventories. The default combines block placements, container transactions, and all supported world item transactions. Use `inventoryActions` to select source-specific events.
|
||||
|
||||
Results normalize the action to removal from (`0`) or addition to (`1`) the player's inventory. Use `getTransactionActionId()` and `getSourceId()` together to identify the underlying event. For example, a container removal is an inventory addition.
|
||||
|
||||
Tracked boat and minecart transactions follow the location rules described under container lookups and remain part of the public `container` source.
|
||||
|
||||
### Chat and command lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `chatLookup(String user, int time)` | `List<MessageResult>` |
|
||||
| `chatLookup(LookupOptions options)` | `List<MessageResult>` |
|
||||
| `commandLookup(String user, int time)` | `List<MessageResult>` |
|
||||
| `commandLookup(LookupOptions options)` | `List<MessageResult>` |
|
||||
|
||||
Searches logged chat or commands. The user/time overloads accept `null` or `#global` for all users. The options overloads also support spatial filters, user lists, pagination, and [text prefix filters](#text-prefix-filters).
|
||||
|
||||
Commands include their leading `/`. `MessageResult.getActionId()` is `6` for chat and `7` for commands.
|
||||
|
||||
### Sign lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `signLookup(Location location, int time)` | `List<SignResult>` |
|
||||
| `signLookup(LookupOptions options)` | `List<SignResult>` |
|
||||
|
||||
Searches logged written sign text. Returned rows use the placed/written action (`1`) and contain nonempty text; this method is not a listing of sign breaks or all sign state changes.
|
||||
|
||||
The location/time overload searches an exact location. The options overload supports user, spatial, text-prefix, and pagination filters. Results contain both sides' saved lines and identify which face was logged.
|
||||
|
||||
### Session lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `sessionLookup(LookupOptions options)` | `List<SessionResult>` |
|
||||
|
||||
Searches player logins and logouts. Both are included by default. Use `sessionActions` to select `LOGIN` or `LOGOUT`, and shared options for user, time, spatial, and pagination filters.
|
||||
|
||||
The older `sessionLookup(String user, int time)` overload returns raw `String[]` rows; see [legacy block, session, and queue lookups](#legacy-block-session-and-queue-lookups).
|
||||
|
||||
### Username lookups
|
||||
|
||||
| Signature | Returns |
|
||||
| --- | --- |
|
||||
| `usernameLookup(String user, int time)` | `List<UsernameResult>` |
|
||||
| `usernameLookup(LookupOptions options)` | `List<UsernameResult>` |
|
||||
|
||||
Searches the usernames recorded for player UUIDs. User filters accept current usernames, historical usernames, or UUID strings and resolve the associated username history. `null` or `#global` selects all users.
|
||||
|
||||
Use `getUsername()` for the historical logged name and `getUuid()` for its UUID. `getPlayer()` returns the current known username when available, with the logged name as a fallback.
|
||||
|
||||
Only user, time, and limit filters apply. Location, radius, world, material, entity, action, and text filters are ignored.
|
||||
|
||||
## Reading Results
|
||||
|
||||
### Shared fields
|
||||
|
||||
All typed result classes and `ParseResult` implement `CoreProtectResult`:
|
||||
|
||||
| Method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getPlayer()` | `String` | Username associated with the event. See username history for its current-name behavior. |
|
||||
| `getTimestamp()` | `long` | Unix timestamp in **milliseconds**. Lookup `time` parameters are in **seconds**. |
|
||||
| `getX()`, `getY()`, `getZ()` | `int` | Block coordinates, with the tracked-location behavior described above. |
|
||||
| `worldName()` | `String` | World name. Username results return an empty string and zero coordinates. |
|
||||
| `getActionId()` | `int` | Action ID in the result type's action family. |
|
||||
| `getActionString()` | `String` | Action name, such as `break`, `spawn`, `add`, or `login`. |
|
||||
|
||||
### BlockResult
|
||||
|
||||
| Additional method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getType()` | `Material` | Logged block material; `null` for entity rows or an unresolved material. |
|
||||
| `getBlockData()` | `BlockData` | Logged block state; `null` for entity rows. |
|
||||
| `getEntityType()` | `EntityType` | Entity type for kill/spawn rows; `null` for block rows. |
|
||||
| `getData()` | `int` | Deprecated legacy block data value. Prefer `getBlockData()`. |
|
||||
| `isRolledBack()` | `boolean` | Whether the block/entity change is currently rolled back. |
|
||||
|
||||
Action strings are `break`, `place`, `click`, `kill`, or `spawn` as applicable.
|
||||
|
||||
### EntityResult
|
||||
|
||||
| Additional method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getEntityType()` | `EntityType` | Logged entity type, including `PLAYER` for player-kill records. |
|
||||
| `isRolledBack()` | `boolean` | Whether the entity change is currently rolled back. |
|
||||
|
||||
Actions are `KILL` (`3`, `kill`) and `SPAWN` (`13`, `spawn`).
|
||||
|
||||
### ContainerResult and ItemResult
|
||||
|
||||
| Additional method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getType()` | `Material` | Logged item material, or `null` if unresolved. |
|
||||
| `getAmount()` | `int` | Number of items. |
|
||||
| `getMetadata()` | `byte[]` | Copy of the raw item metadata, or `null` when absent. |
|
||||
| `isRolledBack()` | `boolean` | Whether the transaction is currently rolled back. |
|
||||
| `ContainerResult.getData()` | `int` | Stored item data value. |
|
||||
|
||||
Container actions are `0` / `remove` and `1` / `add`. Item actions use the IDs and strings in the [item action table](#action-filters).
|
||||
|
||||
### InventoryResult
|
||||
|
||||
| Additional method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getType()` | `Material` | Item material, with block sources normalized to an inventory item. |
|
||||
| `getAmount()` | `int` | Number of items. |
|
||||
| `getMetadata()` | `byte[]` | Copy of raw item metadata, or `null`. |
|
||||
| `getData()` | `int` | Deprecated legacy item data value. |
|
||||
| `getTransactionActionId()` | `int` | Underlying transaction action. Interpret together with its source. |
|
||||
| `getTransactionActionString()` | `String` | Transaction action string, such as `remove`, `drop`, or `buy`. |
|
||||
| `getSourceId()` | `int` | Public source: `0` = block, `1` = container, `2` = item. |
|
||||
| `getSource()` | `String` | `block`, `container`, or `item`. |
|
||||
| `isRolledBack()` | `boolean` | Whether the inventory change is currently rolled back. |
|
||||
|
||||
The shared `getActionId()` / `getActionString()` report `0` / `remove` or `1` / `add` from the player's inventory perspective. Tracked entity-container transactions use source `1` / `container`.
|
||||
|
||||
### MessageResult
|
||||
|
||||
`getMessage()` returns the chat message or command as a `String`. Actions are `6` / `chat` and `7` / `command`.
|
||||
|
||||
### SignResult
|
||||
|
||||
| Additional method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getLine(int line)` | `String` | Zero-based saved line: `0`–`3` front, `4`–`7` back. An out-of-range index returns an empty string. |
|
||||
| `getLines()` | `String[]` | Copy of all eight saved lines. |
|
||||
| `getMessage()` | `String` | Nonempty lines on the logged face joined with spaces. Nonempty messages end with a space; a face with no text returns an empty string. |
|
||||
| `isFront()` | `boolean` | Whether the logged face is the front. |
|
||||
| `getColor()` | `int` | Stored front text color. |
|
||||
| `getColorSecondary()` | `int` | Stored back text color. |
|
||||
| `getData()` | `int` | Raw sign-state flags. |
|
||||
| `isFrontGlowing()` / `isBackGlowing()` | `boolean` | Whether the corresponding side glows. |
|
||||
| `isWaxed()` | `boolean` | Whether the sign is waxed. |
|
||||
|
||||
The returned action is `1` / `place`.
|
||||
|
||||
### SessionResult
|
||||
|
||||
Uses the shared fields. Actions are `0` / `logout` and `1` / `login`.
|
||||
|
||||
### UsernameResult
|
||||
|
||||
`getUsername()` returns the logged name and `getUuid()` returns the UUID as a `String`. The shared action is `9` / `username`. These results have no world or coordinates.
|
||||
|
||||
## Legacy Lookups, Rollbacks, and Restores
|
||||
|
||||
### Full and partial searches
|
||||
|
||||
`performLookup` searches persisted block and entity history using the supplied filters. `performPartialLookup` performs the same search with an offset and maximum result count for pagination. Use `parseResult` to read each returned row.
|
||||
|
||||
The following methods return `List<String[]>`:
|
||||
|
||||
```text
|
||||
performLookup(time, restrictUsers, excludeUsers, restrictBlocks, excludeBlocks, actionList, radius, radiusLocation)
|
||||
performPartialLookup(time, restrictUsers, excludeUsers, restrictBlocks, excludeBlocks, actionList, radius, radiusLocation, limitOffset, limitCount)
|
||||
performRollback(time, restrictUsers, excludeUsers, restrictBlocks, excludeBlocks, actionList, radius, radiusLocation)
|
||||
performRestore(time, restrictUsers, excludeUsers, restrictBlocks, excludeBlocks, actionList, radius, radiusLocation)
|
||||
```
|
||||
|
||||
| Parameter | Type | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `time` | `int` | How far back to search in seconds. Use a positive value; these legacy methods do not share the typed options' zero-means-all-time convention. |
|
||||
| `restrictUsers` | `List<String>` | Users to include. `null` or an empty list selects all users and requires a positive radius and location. |
|
||||
| `excludeUsers` | `List<String>` | Users to exclude; `null` imposes no exclusion. |
|
||||
| `restrictBlocks` | `List<Object>` | Bukkit `Material` or `EntityType` values to include; `null` imposes no type restriction. |
|
||||
| `excludeBlocks` | `List<Object>` | Bukkit `Material` or `EntityType` values to exclude; `null` imposes no type exclusion. |
|
||||
| `actionList` | `List<Integer>` | Block/entity action IDs. Accepted IDs are `0`, `1`, `2`, `3`, and `13`. |
|
||||
| `radius` | `int` | Positive X/Z radius at all heights. Zero or a negative value disables the radius. |
|
||||
| `radiusLocation` | `Location` | Center of a positive radius. May be `null` for user-restricted searches without a radius. |
|
||||
| `limitOffset` | `int` | Zero-based matching-row offset for `performPartialLookup`. |
|
||||
| `limitCount` | `int` | Maximum rows for `performPartialLookup`. Use nonnegative offset/count values. |
|
||||
|
||||
With no explicit actions or type restrictions, the action list defaults to block breaks and placements. Material restrictions infer block breaks/placements; entity-type restrictions infer entity kills. To include entity spawns, explicitly select `CoreProtectAction.ENTITY_SPAWN.id()`.
|
||||
|
||||
Unsupported explicit action IDs are ignored. If none remain, the method returns an empty list. An unrestricted global search without a positive radius, or a positive radius without a location, returns `null`.
|
||||
|
||||
For legacy lookups, a supplied location also restricts the world even when the radius is disabled. Use typed options for a clear whole-world or exact-location search.
|
||||
|
||||
### Rollback and restore behavior
|
||||
|
||||
`performRollback` reverses the selected changes. `performRestore` reapplies previously rolled-back changes. Both require an asynchronous caller and return raw result rows. They do not accept `LookupOptions`.
|
||||
|
||||
Entity spawns are opt-in through action ID `13`. This preserves legacy default block behavior; in-game rollbacks/restores with no action filter can include entity spawns when `rollback-entities` is enabled. Spawn lookup selection can match original or tracked positions, while rollback/restore world selection uses the tracked current/final world.
|
||||
|
||||
An empty result can also indicate that CoreProtect could not start the operation, including during a purge, a conflicting database operation, or when called from the primary server thread. Do not treat it as a guaranteed successful rollback of zero rows.
|
||||
|
||||
### Legacy block, session, and queue lookups
|
||||
|
||||
| Signature | Returns | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `blockLookup(Block block, int time)` | `List<String[]>` | Searches persisted block-table history at the supplied block coordinates, including entity rows. `time` is seconds to look back. |
|
||||
| `sessionLookup(String user, int time)` | `List<String[]>` | Searches persisted login/logout history for the user. `time` is seconds to look back. |
|
||||
| `queueLookup(Block block)` | `List<String[]>` | Searches pending block breaks and placements at the block's location that have not yet been persisted. |
|
||||
|
||||
The legacy block and session overloads impose no time restriction when `time` is zero or negative. The legacy session overload requires a concrete username; `null`, an empty string, or `#global` returns no results. Use typed session options to search all users.
|
||||
|
||||
Queue lookups cover pending block changes, not every logging category. A newly queued event might not yet appear in a database lookup. Queue result timestamps reflect the time of the queue lookup, not the original event time, so they cannot reliably enforce an event-age filter.
|
||||
|
||||
### Parsing legacy rows
|
||||
|
||||
Call `api.parseResult(row)` for rows returned by these legacy methods. It returns `CoreProtectAPI.ParseResult`, which extends `net.coreprotect.api.result.ParseResult` and preserves the existing nested-class import.
|
||||
|
||||
`ParseResult` implements the [shared result fields](#shared-fields) and adds:
|
||||
|
||||
| Method | Return type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `getType()` | `Material` | Logged block material; `null` for entity or session rows. |
|
||||
| `getBlockData()` | `BlockData` | Logged block data; `null` for entity or session rows. |
|
||||
| `getEntityType()` | `EntityType` | Entity type for entity rows; `null` for block or session rows. |
|
||||
| `isRolledBack()` | `boolean` | Whether the block/entity change is rolled back; false for session rows. |
|
||||
| `getData()` | `int` | Deprecated legacy data value. |
|
||||
| `getTime()` | `int` | Deprecated Unix timestamp in seconds. Prefer `getTimestamp()` for milliseconds. |
|
||||
|
||||
Block/entity action strings are lowercase `break`, `place`, `click`, `kill`, and `spawn`. Session rows use `logout` and `login`. Use the typed result classes for their corresponding lookup methods instead of passing typed results to `parseResult`.
|
||||
|
||||
## Logging and Utility Methods
|
||||
|
||||
### Block placement and removal
|
||||
|
||||
All of these overloads return `boolean`:
|
||||
|
||||
| Signature | Data recorded |
|
||||
| --- | --- |
|
||||
| `logPlacement(String user, BlockState blockState)` | Placement using the supplied block-state snapshot. |
|
||||
| `logPlacement(String user, Location location, Material type, BlockData blockData)` | Placement of the supplied material and optional block data. |
|
||||
| `logPlacement(String user, Location location, BlockData blockData)` | Placement using the current block's material and supplied non-null block data. |
|
||||
| `logRemoval(String user, BlockState blockState)` | Removal using the supplied block-state snapshot. |
|
||||
| `logRemoval(String user, Location location, Material type, BlockData blockData)` | Removal of the supplied material and optional block data, with container-break handling. |
|
||||
| `logRemoval(String user, Location location, BlockData blockData)` | Removal using the current block's material and supplied non-null block data, with container-break handling. |
|
||||
|
||||
`user` identifies the player or plugin responsible for the change. Capture removal data **before** changing the block. For placements, supply the state/material being placed. The explicit-material overloads allow `null` block data; the `Location, BlockData` overloads require non-null data.
|
||||
|
||||
Location-based removal checks the existing block for container contents. The `BlockState` overload does not perform that separate container-break check. Call methods that inspect live blocks on the appropriate server thread.
|
||||
|
||||
Deprecated placement/removal overloads taking `(String user, Location location, Material type, byte data)` remain for older integrations. Use `BlockData` for new code.
|
||||
|
||||
### Chat and commands
|
||||
|
||||
| Signature | Behavior |
|
||||
| --- | --- |
|
||||
| `logChat(Player player, String message)` | Queues a nonempty chat message. Messages beginning with `/` are rejected. |
|
||||
| `logCommand(Player player, String command)` | Queues a nonempty command, which must begin with `/`. |
|
||||
|
||||
Both return `boolean`, require the API to be enabled, and honor the world's corresponding player-message or player-command logging setting. Supply a valid player with a world.
|
||||
|
||||
### Container transactions
|
||||
|
||||
`boolean logContainerTransaction(String user, Location location)` captures a block inventory so CoreProtect can detect your subsequent changes. Call it **immediately before** modifying the inventory, on the appropriate server thread.
|
||||
|
||||
`user` is the username or plugin actor credited with adding or removing the items. `location` is the block container's location.
|
||||
|
||||
This method is for a block container. Tracked entity-container lookup support does not add an entity overload to this logging method.
|
||||
|
||||
### Block interactions
|
||||
|
||||
`boolean logInteraction(String user, Location location)` records an interaction with the block currently at the location. It reads the block's material and block data.
|
||||
|
||||
`user` is the username or plugin actor credited with the interaction. `location` identifies the affected block.
|
||||
|
||||
### Placement and removal checks
|
||||
|
||||
| Signature | Behavior |
|
||||
| --- | --- |
|
||||
| `hasPlaced(String user, Block block, int time, int offset)` | Checks whether the user placed a block at these coordinates during the requested window. |
|
||||
| `hasRemoved(String user, Block block, int time, int offset)` | Checks whether the user removed a block at these coordinates during the requested window. |
|
||||
|
||||
Both return `boolean` and compare usernames without case sensitivity. `time` is how many seconds to search back; `offset` excludes the most recent seconds from that window. Use `offset = 0` to include the latest persisted records. These methods search persisted history; use `queueLookup` separately for pending block changes.
|
||||
|
||||
### Purging history
|
||||
|
||||
`void performPurge(int time)` dispatches the console command `co purge t:<time>s`. The argument is the age in seconds beyond which history should be purged: `120` selects records older than two minutes.
|
||||
|
||||
Call this command-dispatching method on the appropriate server thread. It returns no completion result and uses the normal command's purge behavior and restrictions.
|
||||
|
||||
## Available Events
|
||||
|
||||
### CoreProtectPreLogEvent
|
||||
|
||||
`net.coreprotect.event.CoreProtectPreLogEvent` is a cancellable **asynchronous** event emitted by participating loggers before they write a record. Keep listeners safe for asynchronous execution and avoid accessing live Bukkit world state from the listener.
|
||||
|
||||
| Getter | Meaning | Setter |
|
||||
| --- | --- | --- |
|
||||
| `getUser()` | User under which the action will be logged. | `setUser(String)` |
|
||||
| `getLocation()` | Location associated with the record. | `setLocation(Location)` |
|
||||
| `getAction()` | `CoreProtectPreLogEvent.Action` category. | None |
|
||||
| `getActionId()` | Logger-specific action value, or `-1` when not applicable. | None |
|
||||
| `getMaterial()` | Material when applicable; otherwise `null`. | None |
|
||||
| `getEntityType()` | Entity type when applicable; otherwise `null`. | None |
|
||||
| `getMessage()` | Message metadata when applicable; otherwise `null`. | None |
|
||||
| `isCancelled()` | Whether this record has been cancelled. | `setCancelled(boolean)` |
|
||||
|
||||
`setUser` rejects null or empty names. `setLocation` rejects null. Cancelling prevents the record from being logged; it does not undo the gameplay action.
|
||||
|
||||
The action enum contains:
|
||||
|
||||
| Category | Meaning |
|
||||
| --- | --- |
|
||||
| `UNKNOWN` | Unspecified action, including compatibility constructors. |
|
||||
| `BLOCK_BREAK`, `BLOCK_PLACE` | Block removal or placement. |
|
||||
| `PLAYER_INTERACTION` | Player interaction with a block. |
|
||||
| `ENTITY_KILL`, `PLAYER_KILL` | Entity or player death. |
|
||||
| `ENTITY_SPAWN` | Entity spawn. |
|
||||
| `ENTITY_INTERACTION` | Interaction with a tracked entity. |
|
||||
| `SIGN_TEXT` | Sign text logging. |
|
||||
| `CONTAINER_TRANSACTION` | Container item transaction. |
|
||||
| `ITEM_TRANSACTION` | World item transaction. |
|
||||
| `PLAYER_COMMAND` | Player command. |
|
||||
|
||||
Use `getAction()` to identify the category before interpreting the action ID. Container, item, and sign events use their own stored action values; player-command events use `-1`. Command events expose their command as the message. Entity-interaction events expose the interaction action name. Sign-text events do not provide their lines through `getMessage()`.
|
||||
|
||||
The event is not emitted for every logging category; for example, chat and session logging do not emit it.
|
||||
|
||||
The metadata constructor is:
|
||||
|
||||
```text
|
||||
CoreProtectPreLogEvent(String user, Location location, Action action, int actionId, Material material, EntityType entityType, String message)
|
||||
```
|
||||
|
||||
`CoreProtectPreLogEvent(String user, Location location)` remains available. The older `CoreProtectPreLogEvent(String user)` constructor is deprecated and supplies a null location. Listeners should tolerate absent optional metadata.
|
||||
|
||||
## Examples
|
||||
|
||||
These examples use an enabled API v13 instance named `api` and your plugin instance named `plugin`. Import `java.util.List`, the required Bukkit types, and the relevant classes from `net.coreprotect.api` and `net.coreprotect.api.result`.
|
||||
|
||||
Capture `location`, `world`, and other live Bukkit values before starting asynchronous work. In the examples that show only a lookup body, run that body asynchronously as demonstrated below. Database queries return lists directly; they do not create a background task themselves.
|
||||
|
||||
### Search blocks asynchronously
|
||||
|
||||
Find the first 100 block changes by Notch in the last minute, excluding dirt and grass blocks:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.user("Notch")
|
||||
.time(60)
|
||||
.excludeMaterials(List.of(Material.DIRT, Material.GRASS_BLOCK))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
List<BlockResult> results = api.blockLookup(options);
|
||||
if (results != null) {
|
||||
for (BlockResult result : results) {
|
||||
plugin.getLogger().info(result.getPlayer() + " " + result.getActionString()
|
||||
+ " " + result.getType() + " at " + result.worldName()
|
||||
+ " " + result.getX() + "," + result.getY() + "," + result.getZ());
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Use `.radius(location, 5)` to restrict this search to an X/Z area, or `.world(world)` for a whole world. To use the exact-block compatibility overload:
|
||||
|
||||
```java
|
||||
List<BlockResult> results = api.blockLookup(block, LookupOptions.builder()
|
||||
.time(60)
|
||||
.blockActions(List.of(BlockAction.BREAK, BlockAction.PLACE))
|
||||
.limit(0, 100)
|
||||
.build());
|
||||
```
|
||||
|
||||
### Find entity spawns
|
||||
|
||||
Find zombie and skeleton spawns within 50 blocks of a captured location:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.time(3600)
|
||||
.radius(location, 50)
|
||||
.entityActions(List.of(EntityAction.SPAWN))
|
||||
.includeEntities(List.of(EntityType.ZOMBIE, EntityType.SKELETON))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<EntityResult> results = api.entityLookup(options);
|
||||
```
|
||||
|
||||
The search can match a tracked position, but each result still reports its original spawn coordinates.
|
||||
|
||||
### Find container deposits
|
||||
|
||||
Find diamonds added to containers within five blocks of a location:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.time(3600)
|
||||
.radius(location, 5)
|
||||
.includeMaterials(List.of(Material.DIAMOND))
|
||||
.containerActions(List.of(ContainerAction.ADD))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<ContainerResult> results = api.containerLookup(options);
|
||||
```
|
||||
|
||||
For an exact-location lookup without additional filters, use `api.containerLookup(location, 60)`.
|
||||
|
||||
### Find item and inventory transactions
|
||||
|
||||
Look up dropped and picked-up items:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.user("Notch")
|
||||
.time(3600)
|
||||
.itemActions(List.of(ItemAction.DROP, ItemAction.PICKUP))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<ItemResult> results = api.itemLookup(options);
|
||||
```
|
||||
|
||||
Find inventory removals caused by container deposits or block placements:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.user("Notch")
|
||||
.time(3600)
|
||||
.inventoryActions(List.of(InventoryAction.CONTAINER_ADD, InventoryAction.BLOCK_PLACE))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<InventoryResult> results = api.inventoryLookup(options);
|
||||
if (results != null) {
|
||||
for (InventoryResult result : results) {
|
||||
plugin.getLogger().info(result.getActionString() + " " + result.getAmount()
|
||||
+ " " + result.getType() + " via " + result.getSource()
|
||||
+ ":" + result.getTransactionActionString());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Filter commands by prefix
|
||||
|
||||
Find `/shop ...` and `/market ...` commands from the last hour, excluding commands beginning with `/shop admin`:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.time(3600)
|
||||
.textStartsWithAny(List.of("/shop ", "/market "))
|
||||
.textStartsWithNone(List.of("/shop admin"))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<MessageResult> results = api.commandLookup(options);
|
||||
```
|
||||
|
||||
The trailing spaces in the inclusion prefixes are significant: bare `/shop` and `/market` commands do not match. Use `chatLookup(options)` to apply the same kind of filters to chat, or `chatLookup("Notch", 60)` for an unfiltered user/time search.
|
||||
|
||||
### Find sign text
|
||||
|
||||
Find signs whose logged face has a line beginning with `Diamond Shop`:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.time(3600)
|
||||
.radius(location, 10)
|
||||
.textStartsWithAny(List.of("Diamond Shop"))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<SignResult> results = api.signLookup(options);
|
||||
if (results != null) {
|
||||
for (SignResult result : results) {
|
||||
plugin.getLogger().info((result.isFront() ? "Front: " : "Back: ") + result.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `api.signLookup(location, 60)` for an exact-location search without additional filters.
|
||||
|
||||
### Read sessions and username history
|
||||
|
||||
Find logins for Notch in the last day:
|
||||
|
||||
```java
|
||||
LookupOptions options = LookupOptions.builder()
|
||||
.user("Notch")
|
||||
.time(24 * 60 * 60)
|
||||
.sessionActions(List.of(SessionAction.LOGIN))
|
||||
.limit(0, 100)
|
||||
.build();
|
||||
List<SessionResult> results = api.sessionLookup(options);
|
||||
```
|
||||
|
||||
Read username history for the same period:
|
||||
|
||||
```java
|
||||
List<UsernameResult> results = api.usernameLookup("Notch", 24 * 60 * 60);
|
||||
if (results != null) {
|
||||
for (UsernameResult result : results) {
|
||||
plugin.getLogger().info(result.getUuid() + " used " + result.getUsername());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parse a legacy lookup
|
||||
|
||||
Read the last minute of Notch's block history and parse its coordinates:
|
||||
|
||||
```java
|
||||
List<String[]> rows = api.performLookup(60, List.of("Notch"), null,
|
||||
null, null, null, 0, null);
|
||||
if (rows != null) {
|
||||
for (String[] row : rows) {
|
||||
CoreProtectAPI.ParseResult result = api.parseResult(row);
|
||||
plugin.getLogger().info(result.getActionString() + " at "
|
||||
+ result.getX() + "," + result.getY() + "," + result.getZ());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a page of results within five blocks of a location:
|
||||
|
||||
```java
|
||||
List<String[]> rows = api.performPartialLookup(60, null, null,
|
||||
null, null, List.of(CoreProtectAction.BLOCK_BREAK.id(), CoreProtectAction.BLOCK_PLACE.id()),
|
||||
5, location, 0, 100);
|
||||
```
|
||||
|
||||
The deprecated single-user overloads of `performLookup`, `performPartialLookup`, `performRollback`, and `performRestore` remain available. Prefer the list-based signatures above for new integrations.
|
||||
|
||||
### Roll back changes asynchronously
|
||||
|
||||
Reverse Notch's block changes and entity spawns from the last minute within five blocks of a captured location:
|
||||
|
||||
```java
|
||||
List<Integer> actions = List.of(CoreProtectAction.BLOCK_BREAK.id(),
|
||||
CoreProtectAction.BLOCK_PLACE.id(), CoreProtectAction.ENTITY_SPAWN.id());
|
||||
Location center = location.clone();
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
List<String[]> rows = api.performRollback(60, List.of("Notch"), null,
|
||||
null, null, actions, 5, center);
|
||||
if (rows != null) {
|
||||
plugin.getLogger().info("Rollback returned " + rows.size() + " rows.");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To restore changes, use `performRestore` with the same parameter structure in an asynchronous task. An empty result alone does not distinguish no matches from an operation that could not start.
|
||||
|
||||
### Include pending block changes in a placement check
|
||||
|
||||
```java
|
||||
boolean placed = api.hasPlaced("Notch", block, 60, 0);
|
||||
if (!placed) {
|
||||
List<String[]> pending = api.queueLookup(block);
|
||||
if (pending != null) {
|
||||
for (String[] row : pending) {
|
||||
CoreProtectAPI.ParseResult result = api.parseResult(row);
|
||||
if (result.getActionId() == CoreProtectAction.BLOCK_PLACE.id()
|
||||
&& result.getPlayer().equalsIgnoreCase("Notch")) {
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("Recent placement found: " + placed);
|
||||
```
|
||||
|
||||
The persisted check covers the last 60 seconds; the pending check cannot enforce that age because queue rows use the lookup time as their timestamp. Use `hasRemoved` and `CoreProtectAction.BLOCK_BREAK.id()` for removal checks. A nonzero `offset` on the persisted check can exclude the most recent seconds when needed by your integration.
|
||||
|
||||
### Log a block replacement
|
||||
|
||||
Run this on the appropriate server thread, while the original block still exists:
|
||||
|
||||
```java
|
||||
api.logRemoval("Notch", block.getLocation(), block.getType(), block.getBlockData());
|
||||
block.setType(Material.STONE);
|
||||
api.logPlacement("Notch", block.getState());
|
||||
```
|
||||
|
||||
Use the actual responsible username or a plugin-specific actor name. If CoreProtect already records the gameplay event, additional API logging can create duplicate history.
|
||||
|
||||
### Log inventory changes
|
||||
|
||||
For a block-backed `inventory`, capture its contents immediately before changing them:
|
||||
|
||||
```java
|
||||
api.logContainerTransaction("Notch", inventory.getLocation());
|
||||
inventory.addItem(itemStack);
|
||||
```
|
||||
|
||||
### Listen for pre-log events
|
||||
|
||||
This listener cancels block-placement records attributed to a plugin's preview actor:
|
||||
|
||||
```java
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import net.coreprotect.event.CoreProtectPreLogEvent;
|
||||
|
||||
public final class PreviewLogListener implements Listener {
|
||||
@EventHandler
|
||||
public void onPreLog(CoreProtectPreLogEvent event) {
|
||||
if (event.getAction() == CoreProtectPreLogEvent.Action.BLOCK_PLACE
|
||||
&& "MyPluginPreview".equals(event.getUser())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register it during plugin initialization:
|
||||
|
||||
```java
|
||||
plugin.getServer().getPluginManager().registerEvents(new PreviewLogListener(), plugin);
|
||||
```
|
||||
|
||||
The listener executes asynchronously. Cancelling it suppresses the log record, not the original block placement.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Automatic Purging
|
||||
|
||||
Automatic purging removes old CoreProtect data on a daily schedule, helping keep database growth under control without requiring manual `/co purge` runs.
|
||||
|
||||
> **Note:** Automatic purging is not enabled by default. This feature is exclusive to CoreProtect 24.0+ [Patreon builds](https://patreon.com/coreprotect).
|
||||
|
||||
## Configuration
|
||||
|
||||
To enable automatic purging, set `auto-purge` in `config.yml` to the amount of data you want to keep:
|
||||
|
||||
```yaml
|
||||
auto-purge: 180d
|
||||
```
|
||||
|
||||
This example keeps the most recent 180 days of CoreProtect data and automatically removes older data.
|
||||
|
||||
Supported values use the same style as CoreProtect command times, such as `30d`, `12w`, or `6mo`. The minimum automatic purge value is `30d`. Set `auto-purge: false` to disable automatic purging.
|
||||
|
||||
## Schedule
|
||||
|
||||
Automatic purging runs once per day using your server's local time. By default, it runs at midnight.
|
||||
|
||||
To change the daily runtime, manually add `auto-purge-time: 03:30` to `config.yml`.<br>
|
||||
Use 24-hour `HH:mm` server time.
|
||||
|
||||
After changing `auto-purge` or `auto-purge-time`, use `/co reload` or restart the server. Changes apply to the next scheduled run.
|
||||
|
||||
When automatic purging is enabled, CoreProtect logs the next scheduled run when the server starts and again after an automatic purge completes.
|
||||
|
||||
## How It Works
|
||||
|
||||
Automatic purging runs in the background. SQLite, MySQL, and DuckDB remove old rows incrementally in small chunks with short pauses between database work. ClickHouse uses its columnar retention path, dropping fully covered monthly partitions for an unfiltered time purge and synchronously removing rows from any partially covered partition.
|
||||
|
||||
Automatic purging does not rebuild SQLite, optimize MySQL, or run a ClickHouse `OPTIMIZE FINAL`. DuckDB performs a checkpoint after cleanup. Deleting rows helps control future growth, but it may not immediately reduce the database file or table size on disk.
|
||||
|
||||
Only one automatic purge can run per CoreProtect instance at a time. If the server shuts down, a manual purge starts, a database migration or conversion starts, or the consumer is manually paused, the automatic purge stops safely and can continue during the next scheduled run.
|
||||
|
||||
ClickHouse rejects automatic and manual purges while `database-lock` is disabled. For a shared namespace, stop every installation, enable `database-lock` on one installation, restart or reload it, and finish the purge before restoring shared-writer mode.
|
||||
|
||||
## Important Notes
|
||||
|
||||
* **Patreon exclusive:** Only available in CoreProtect 24.0+ [Patreon builds](http://patreon.com/coreprotect)
|
||||
* **Not enabled by default:** Set `auto-purge` in `config.yml` to enable automatic purging
|
||||
* **Background cleanup:** Automatic purging is designed to run while the server remains usable
|
||||
|
||||
## Existing Databases
|
||||
|
||||
If enabling automatic purging on a new database, no additional action is required.
|
||||
|
||||
If enabling automatic purging on an existing database, it is recommended to run a manual purge first using the same time value:
|
||||
|
||||
```text
|
||||
/co purge t:180d
|
||||
```
|
||||
|
||||
For MySQL, add `#optimize` if you want to reclaim disk space during the initial manual purge:
|
||||
|
||||
```text
|
||||
/co purge t:180d #optimize
|
||||
```
|
||||
|
||||
This lets the manual purge reduce the existing database size, while automatic purging helps keep the database from growing beyond the configured retention period afterward.
|
||||
|
||||
ClickHouse normally reclaims complete old partitions without `#optimize`. A manual ClickHouse purge accepts `#optimize`, but it runs the expensive `OPTIMIZE TABLE ... FINAL` operation and is generally unnecessary. DuckDB checkpoints automatically after a manual or automatic purge; `#optimize` has no additional effect for DuckDB.
|
||||
|
||||
## Status
|
||||
|
||||
Use `/co status` to see how many rows have been automatically purged since the last server restart.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Automatic purging won't start:**
|
||||
|
||||
* Verify you're using a CoreProtect 24.0+ [Patreon build](http://patreon.com/coreprotect)
|
||||
* Verify `auto-purge` is set to a valid value of at least `30d`
|
||||
* Check the server console for automatic purge scheduling messages
|
||||
+57
-15
@@ -16,7 +16,7 @@ ___
|
||||
| [/co reload](#co-reload) | Reload the configuration file |
|
||||
| [/co status](#co-status) | View the plugin status |
|
||||
| [/co consumer](#co-consumer) | Toggle consumer processing |
|
||||
| [/co migrate-db](#co-migrate-db) | Migrate between SQLite and MySQL |
|
||||
| [/co migrate-db](#co-migrate-db) | Migrate between database backends |
|
||||
|
||||
### Alias Commands
|
||||
|
||||
@@ -56,6 +56,7 @@ Perform a lookup. Nearly all of the parameters are optional.
|
||||
| [`a:<action>`](#aaction) | Restrict the lookup to a certain action. |
|
||||
| [`i:<include>`](#iinclude) | Include specific blocks/entities in the lookup. |
|
||||
| [`e:<exclude>`](#eexclude) | Exclude blocks/entities from the lookup. |
|
||||
| [`f:<filter>`](#ffilter) | Include or exclude chat, command, or sign text by prefix. |
|
||||
| [`#<hashtag>`](#hashtag) | Add a hashtag to perform additional actions. |
|
||||
|
||||
#### Pagination
|
||||
@@ -94,7 +95,7 @@ Purge old block data. Useful for freeing up space on your HDD if you don't need
|
||||
|
||||
| Command | Parameters |
|
||||
| --- | --- |
|
||||
| /co purge | `t:<time> r:<world> i:<include>` |
|
||||
| /co purge | `t:<time> r:<world/radius> i:<include> e:<exclude> a:kill` |
|
||||
|
||||
For example, `/co purge t:30d` will delete all data older than one month, and only keep the last 30 days of data.
|
||||
|
||||
@@ -105,15 +106,34 @@ For example, `/co purge t:30d` will delete all data older than one month, and on
|
||||
You can optionally specify a world in CoreProtect v19+.
|
||||
For example, `/co purge t:30d r:#world_nether` will delete all data older than one month in the Nether, without removing data in any other worlds.
|
||||
|
||||
**Purging by Radius**
|
||||
A radius can limit a purge of specific block types or entity kills to the area around you. It only applies to block and entity kill data, so it must be combined with `i:` or `a:kill`, and it can only be used in-game. A radius always applies to the world you are in, so it cannot be combined with a world such as `r:#world_nether`. The radius is limited by `max-radius` in the config, like other commands.
|
||||
For example, `/co purge t:30d r:50 i:zombie` will delete zombie kills older than one month within 50 blocks of you, without removing any other data. Use `r:50x10` to also limit the height to 10 blocks above and below you.
|
||||
|
||||
**Purging Blocks**
|
||||
You can optionally specify block types in CoreProtect v23+.
|
||||
For example, `/co purge t:30d i:stone,dirt` will delete all stone and dirt data older than one month, without removing other block data.
|
||||
|
||||
**MySQL Optimization**
|
||||
In CoreProtect v2.15+, adding "#optimize" to the end of the command (e.g. `/co purge t:30d #optimize`) will also optimize your tables and reclaim disk space.
|
||||
This option is only available when using MySQL, as SQLite purges do this by default.
|
||||
**Purging Entity Kills**
|
||||
Entity kill logs can be purged separately from other data on SQLite, MySQL, and DuckDB. Each kill also removes the saved entity data used to restore the mob on rollback.
|
||||
|
||||
*Please note adding the #optimize option will significantly slow down your purge, and is generally unnecessary.*
|
||||
* `/co purge t:30d a:kill` deletes all entity kills older than one month, and keeps all other data.
|
||||
* `/co purge t:30d i:zombie,skeleton` deletes zombie and skeleton kills older than one month. Entity and block types can be combined in `i:`.
|
||||
* `/co purge t:30d e:villager,wolf` deletes all data older than one month, but keeps villager and wolf kills.
|
||||
* `/co purge t:30d a:kill e:villager r:#world` deletes entity kills older than one month in the overworld, except villager kills.
|
||||
|
||||
`e:` only accepts entity types. It cannot be combined with entity types in `i:`, or with only block types in `i:`, because a block restriction already keeps every kill. `a:kill` cannot be combined with block types in `i:`.
|
||||
|
||||
**Database Optimization**
|
||||
|
||||
In CoreProtect v2.15+, adding `#optimize` to the end of the command (for example, `/co purge t:30d #optimize`) will also optimize supported database tables and reclaim unused disk space. How this option is handled depends on the database backend:
|
||||
|
||||
* SQLite already rebuilds the database from retained data and reclaims unused file space as part of a manual purge, so `#optimize` is not needed.
|
||||
* MySQL normally deletes matching rows. Adding `#optimize` also removes saved entity data in the purged time range that no entity kill references anymore (this requires the `CREATE TEMPORARY TABLES` privilege), then optimizes its tables to reclaim unused space.
|
||||
* DuckDB deletes matching rows in one transaction and checkpoints afterward. `#optimize` has no additional effect.
|
||||
* ClickHouse drops fully covered monthly partitions for an unfiltered time purge and synchronously removes rows from partial or filtered partitions. Adding `#optimize` also runs `OPTIMIZE TABLE ... FINAL`.
|
||||
|
||||
`#optimize` can significantly slow MySQL and ClickHouse purges and is generally unnecessary.
|
||||
|
||||
___
|
||||
|
||||
@@ -130,17 +150,21 @@ Console command to pause or resume consumer queue processing.
|
||||
___
|
||||
|
||||
### /co migrate-db
|
||||
Migrate your CoreProtect database between SQLite and MySQL. Console-only command.
|
||||
Migrate data from the active database backend to a different backend. This is a console-only command.
|
||||
|
||||
| Command | Parameters |
|
||||
| --- | --- |
|
||||
| /co migrate-db | `<sqlite|mysql>` |
|
||||
| /co migrate-db | `<sqlite|mysql|duckdb|clickhouse> [--full-validation]` |
|
||||
|
||||
By default, migration validates all table statistics and compares approximately 1% of each large history table, with full comparisons for small and reference tables. Add `--full-validation` to compare every copied row.
|
||||
|
||||
The target namespace must contain no CoreProtect data; a DuckDB target must use a new database file, and `database-lock` must remain enabled. After a successful migration, CoreProtect automatically updates `database-type` in `config.yml` before queued writes resume.
|
||||
|
||||
> **Note:** This feature is exclusive to CoreProtect 23.0+ Patreon builds for Patron supporters.
|
||||
> **Note:** Migrations between SQLite and MySQL require a CoreProtect 23.0+ Patreon build. Any migration involving DuckDB or ClickHouse requires CoreProtect 25.0+.
|
||||
|
||||
For complete migration instructions, safety guidelines, and troubleshooting information, see the [Database Migration documentation](/database-migration/).
|
||||
|
||||
___
|
||||
For complete migration instructions, safety guidelines, and troubleshooting information, see the [Database Migration documentation](/database-migration/).
|
||||
|
||||
___
|
||||
|
||||
|
||||
## Parameter Details
|
||||
@@ -201,9 +225,10 @@ ___
|
||||
| `a:-inventory` | items removed from player inventories |
|
||||
| `a:item` | items dropped, thrown, picked up, deposited, or withdrawn by players |
|
||||
| `a:+item` | items picked up or withdrawn by players |
|
||||
| `a:-item` | items dropped, thrown, or deposited by players |
|
||||
| `a:kill` | mobs/animals killed |
|
||||
| `a:session` | player logins/logouts |
|
||||
| `a:-item` | items dropped, thrown, or deposited by players |
|
||||
| `a:kill` | mobs/animals killed |
|
||||
| `a:spawn` | entities placed or spawned by players |
|
||||
| `a:session` | player logins/logouts |
|
||||
| `a:+session` | player logins |
|
||||
| `a:-session` | player logouts |
|
||||
| `a:sign` | messages written on signs |
|
||||
@@ -231,6 +256,20 @@ ___
|
||||
|
||||
---
|
||||
|
||||
### `f:<filter>`
|
||||
|
||||
*Filter message prefixes when using `a:chat`, `a:command`, or `a:sign`.*
|
||||
|
||||
* Example: `a:command f:/ban` *(commands starting with "/ban")*
|
||||
* Example: `a:command f:/ban,/kick` *(commands starting with either prefix)*
|
||||
* Example: `a:command f:/ban,-/banlist` *(include "/ban" and exclude "/banlist")*
|
||||
* Example: `a:chat f:-hello` *(exclude messages starting with "hello")*
|
||||
* Example: `a:sign f:Diamond Shop,-Diamond Shop Closed` *(filter prefixes on the recorded sign face)*
|
||||
|
||||
> Separate filters with commas. Included prefixes are alternatives; any matching excluded prefix removes the result. Exclusions can be used on their own. Each prefix requires at least three characters, excluding the leading `-` marker. Spaces are allowed, and `*`, `%`, `_`, and `~` are matched literally.
|
||||
|
||||
---
|
||||
|
||||
### `#<hashtag>`
|
||||
|
||||
Add a hashtag to the end of your command to perform additional actions.
|
||||
@@ -242,6 +281,7 @@ Add a hashtag to the end of your command to perform additional actions.
|
||||
| --- | --- |
|
||||
| `#preview` | Preview a rollback/restore |
|
||||
| `#count` | Return the number of rows found in a lookup query |
|
||||
| `#summary` | Return the row count and per-user material totals |
|
||||
| `#verbose` | Display additional information during a rollback/restore |
|
||||
| `#silent` | Display minimal information during a rollback/restore |
|
||||
|
||||
@@ -294,5 +334,7 @@ Lookup commands are generally the same as rollback commands. The primary differe
|
||||
*(lookup all logins ever done by Notch)*
|
||||
* `/co lookup u:Notch a:username`
|
||||
*(lookup previous usernames used by Notch)*
|
||||
* `/co lookup r:50 t:7d a:block #summary`
|
||||
*(show the matching row count and material totals for each user within the same area and time range)*
|
||||
|
||||
___
|
||||
+76
-7
@@ -2,6 +2,59 @@
|
||||
|
||||
The CoreProtect configuration file can be found within the CoreProtect folder, at `config.yml`.
|
||||
|
||||
## Database Storage
|
||||
|
||||
New installations use embedded DuckDB by default. Upgrading an existing SQLite or MySQL installation does not switch it to DuckDB: if `database-type` is absent, CoreProtect derives it from the existing legacy `use-mysql` setting and automatically writes the matching `sqlite` or `mysql` value. No manual change is required. Once present, `database-type` takes precedence over `use-mysql`, and changes take effect after `/co reload` or a restart.
|
||||
|
||||
| Value | Storage | Requirements |
|
||||
| --- | --- | --- |
|
||||
| `duckdb` | Embedded columnar database; the default for new installations | CoreProtect 25.0+ |
|
||||
| `clickhouse` | External columnar database | CoreProtect 25.0+ and a reachable ClickHouse 26.1+ server |
|
||||
| `sqlite` | Embedded legacy relational database | None beyond CoreProtect |
|
||||
| `mysql` | External legacy relational database | A reachable MySQL server |
|
||||
|
||||
The server downloads the DuckDB JDBC driver automatically as a plugin library, while the ClickHouse JDBC driver is packaged inside CoreProtect; neither requires manual driver installation. DuckDB includes its database engine, while ClickHouse requires a separate server.
|
||||
|
||||
Changing `database-type` selects a separate dataset; it does not copy existing data. CoreProtect 23.0+ Patreon builds can migrate between SQLite and MySQL; any migration involving DuckDB or ClickHouse requires CoreProtect 25.0+. The target namespace must contain no CoreProtect data; DuckDB requires a new target file, and `database-lock` must remain enabled. See the [database migration guide](/database-migration/) for preparation and switchover instructions.
|
||||
|
||||
### DuckDB
|
||||
|
||||
The default settings are suitable for most servers:
|
||||
|
||||
```yaml
|
||||
database-type: duckdb
|
||||
duckdb-memory-limit: 512MB
|
||||
duckdb-threads: 3
|
||||
duckdb-max-temp-directory-size: 10GB
|
||||
```
|
||||
|
||||
DuckDB stores its data in `plugins/CoreProtect/database.duckdb`; CoreProtect manages it directly, so no DuckDB service is needed. `duckdb-memory-limit` controls the buffer manager, although total native memory can be higher, and `duckdb-threads` limits query threads. `duckdb-max-temp-directory-size` caps spill data without preallocating space or limiting the database file itself. Queries that exceed both memory and available spill space can fail.
|
||||
|
||||
On a pristine installation where the CoreProtect data folder contains no existing files, CoreProtect verifies that DuckDB's native library can run before creating the database file. If the operating system, architecture, or native runtime is incompatible, CoreProtect records `database-type: sqlite` in the generated `config.yml`, logs a warning, and starts with SQLite instead. Existing or preconfigured installations and explicitly configured DuckDB databases never fall back automatically; an initialization error stops database startup so CoreProtect cannot silently open a separate empty history.
|
||||
|
||||
### ClickHouse
|
||||
|
||||
Configure the ClickHouse HTTP endpoint and credentials before selecting it:
|
||||
|
||||
```yaml
|
||||
database-type: clickhouse
|
||||
table-prefix: co_
|
||||
clickhouse-host: 127.0.0.1
|
||||
clickhouse-port: 8123
|
||||
clickhouse-database: default
|
||||
clickhouse-username: default
|
||||
clickhouse-password: ""
|
||||
clickhouse-tls: false
|
||||
```
|
||||
|
||||
Create the configured database first with a database engine that provides persistent UUID-backed table identities. `Atomic` is the normal choice for self-hosted ClickHouse and is the default there; for example, `CREATE DATABASE coreprotect ENGINE = Atomic`. CoreProtect checks this capability before creating any tables, then creates and validates its prefixed tables and views. The account needs permission to read `system.databases` and `system.tables`, create and read/write the CoreProtect objects, run mutations and partition drops, and drop or truncate purge-target tables. Server-wide database-creation permission is not required; `#optimize` additionally requires table optimization permission.
|
||||
|
||||
To share a ClickHouse database and prefix, disable `database-lock` on every installation. Use the same CoreProtect version and direct endpoint to one physical ClickHouse server, with separate CoreProtect data directories. Load balancing across independent nodes, replicated tables, and distributed tables are unsupported. Synchronize system clocks; events from different installations within the same second have no guaranteed order. Use globally unique world names unless their coordinate histories should be combined.
|
||||
|
||||
Players must log in with a UUID before UUID-less activity under a changed or reassigned name. UUIDs preserve account IDs across name changes and distinguish successive owners of a reused name. Name lookups use the latest UUID-bearing observation; ownership is unspecified when observations conflict within one second. Simultaneous name changes are unsupported. Cached names from other servers may remain until the player is observed locally or CoreProtect reloads.
|
||||
|
||||
Purges require `database-lock`. Stop all installations sharing the database and prefix, enable the lock on the installation performing the purge, then restart or reload it before purging. Also stop all installations before migration. Migration updates only the initiating installation's configuration; update the others before restarting them.
|
||||
|
||||
## Per-World Configuration
|
||||
|
||||
If you'd like to modify the logging settings for a specific world, simply do the following:
|
||||
@@ -21,15 +74,31 @@ Secondary configuration files override the value specified in config.yml. If you
|
||||
To disable logging for specific users, blocks or commands, simply do the following:
|
||||
|
||||
1. In the CoreProtect plugin directory, create a file named `blacklist.txt`.
|
||||
2. Enter the names of the users (or commands) you'd like to disable logging for (each username on a new line).
|
||||
2. Enter the names of the users, commands, blocks, or entities you'd like to disable logging for (each entry on a new line).
|
||||
3. Either restart your server, or type "/co reload" in-game.
|
||||
|
||||
This can be used to disable logging for non-player users, such as "#creeper". For example, if you'd like to disable logging for the user "Notch", TNT explosions, stone blocks, and the "/help" command, the blacklist.txt file would look like this:
|
||||
The blacklist supports disabling logs for:
|
||||
|
||||
- Users, which includes Players and non-player users, such as "#creeper"
|
||||
- Commands, such as `/help`
|
||||
- Blocks, such as minecraft:stone. Only `block` actions are affected.
|
||||
- Entities, such as minecraft:creeper, which will disable logging the death for that entity. *Note: renamed entities will be logged even if blacklisted.*
|
||||
- Filters can also be specified for a particular user, by using the `@` symbol after the specific item, block, or entity namespaced ID. The format is `id@user`. This will filter all `block`, `kill`, `item` and `container` actions involving that particular block, item or mob, only when caused by the specified player or non-player user.
|
||||
- Items and container actions are only affected by filtered blacklist entries, not by generic item or block IDs.
|
||||
|
||||
*Please note that you must include the namespace (e.g. minecraft:) for blocks, entities and items.*
|
||||
|
||||
An example blacklist.txt file would look like this:
|
||||
|
||||
```text
|
||||
Notch
|
||||
#tnt
|
||||
/help
|
||||
minecraft:stone
|
||||
Notch ; User
|
||||
#tnt ; TNT explosions
|
||||
/help ; Help command
|
||||
minecraft:stone ; Stone blocks
|
||||
minecraft:creeper ; Creeper entity
|
||||
minecraft:shears@#dispenser ; Shears being dispensed
|
||||
```
|
||||
|
||||
*Please note that to disable logging for blocks, CoreProtect v23+ is required, and you must include the namespace. For example, to disable logging for dirt, you must add it as "minecraft:dirt".*
|
||||
|
||||
*Please note that to disable logging for blocks, CoreProtect v23+ is required.*
|
||||
*To disable logging for entities or to use filtering, CoreProtect v24+ is required.*
|
||||
+68
-99
@@ -1,145 +1,114 @@
|
||||
# Database Migration (SQLite ↔ MySQL)
|
||||
# Database Migration
|
||||
|
||||
CoreProtect 23.0+ includes the ability to migrate your database between SQLite and MySQL without losing any data.
|
||||
CoreProtect 23.0+ can migrate data between SQLite and MySQL while retaining the source history. CoreProtect 25.0+ adds DuckDB and ClickHouse as migration sources and destinations. Changing `database-type` by itself does not migrate any data.
|
||||
|
||||
> **Note:** Database migration functionality is exclusive to CoreProtect 23.0+ Patreon builds for Patron supporters.
|
||||
> **Note:** Database migration is available only in [Patreon builds](https://patreon.com/coreprotect) for Patron supporters.
|
||||
|
||||
## Overview
|
||||
|
||||
The `/co migrate-db` command allows you to seamlessly transfer all your CoreProtect data from SQLite to MySQL or vice versa. This is particularly useful when:
|
||||
|
||||
* Starting with SQLite and wanting to upgrade to MySQL for better performance
|
||||
* Moving from MySQL back to SQLite for simpler server setups
|
||||
* Transferring data between different server configurations
|
||||
Database migration can be used to move from SQLite or DuckDB to an external database, return to an embedded database for simpler operation, or switch between any of the supported storage engines. The active backend is always the source, and the command argument selects the destination.
|
||||
|
||||
## Command Usage
|
||||
|
||||
| Command | Parameters | Description |
|
||||
| --- | --- | --- |
|
||||
| `/co migrate-db` | `<sqlite|mysql>` | Migrate to the specified database type |
|
||||
| `/co migrate-db` | `<sqlite|mysql|duckdb|clickhouse> [--full-validation]` | Migrate the active database to the selected backend |
|
||||
|
||||
**Examples:**
|
||||
The command must be run from the server console, and the destination must differ from the active source.
|
||||
|
||||
* `/co migrate-db mysql` - Migrate from SQLite to MySQL
|
||||
* `/co migrate-db sqlite` - Migrate from MySQL to SQLite
|
||||
For example:
|
||||
|
||||
> **Console Only:** This command can only be executed from the server console, not from in-game.
|
||||
```text
|
||||
co migrate-db duckdb
|
||||
co migrate-db clickhouse
|
||||
co migrate-db duckdb --full-validation
|
||||
```
|
||||
|
||||
---
|
||||
## Before You Start
|
||||
|
||||
## Migration Process
|
||||
1. Stop the server and make a normal backup of the source database and `config.yml`.
|
||||
2. Leave `database-type` set to the source backend and keep `database-lock: true`.
|
||||
3. Configure the target while the server is stopped, then start the server with the source backend still selected. MySQL and ClickHouse connection settings must be loaded before the migration starts.
|
||||
4. Use a target namespace that contains no CoreProtect data. Unrelated tables in the same external database are not affected.
|
||||
5. Verify that the target has enough storage and that its account has permission to create and modify the required tables.
|
||||
6. Run the migration during a quiet maintenance period, with no purge, rollback, reload, or other database operation in progress.
|
||||
|
||||
### Step 1: Preparation
|
||||
Prepare the target as follows:
|
||||
|
||||
**Before starting the migration:**
|
||||
| Target | Preparation |
|
||||
| --- | --- |
|
||||
| SQLite | Move or archive an existing `plugins/CoreProtect/database.db` if it contains CoreProtect data. CoreProtect creates the file and schema when needed. |
|
||||
| DuckDB | `plugins/CoreProtect/database.duckdb` must be a new database file so its row ID sequences can be initialized from the source. |
|
||||
| MySQL | Configure the `mysql-*` options and create the configured database and account. The selected table-prefix namespace must contain no CoreProtect data. |
|
||||
| ClickHouse | Use ClickHouse 26.1 or newer, create the configured database with persistent UUID-backed table identities (`Atomic` is the normal self-hosted choice), create the account, configure the `clickhouse-*` options, and use a table-prefix namespace containing no CoreProtect data. |
|
||||
|
||||
1. Ensure your server is running with your current CoreProtect database
|
||||
2. Configure your new database in `config.yml` (ensure `use-mysql` is updated correctly)
|
||||
3. **Important:** Do NOT restart your server or use `/co reload` after modifying the config
|
||||
`table-prefix` is shared by MySQL and ClickHouse. If the source is MySQL or ClickHouse, leave its prefix unchanged; an external target will use that same prefix. If the source is SQLite or DuckDB, you may select the external target prefix before starting the migration session. Embedded SQLite and DuckDB targets always use `co_`.
|
||||
|
||||
### Step 2: Execute Migration
|
||||
## During Migration
|
||||
|
||||
#### 1. **Run the migration command from your server console:**
|
||||
```
|
||||
co migrate-db <mysql|sqlite>
|
||||
```
|
||||
CoreProtect pauses database persistence, waits for active database work to finish, and copies data in bounded batches. The console shows a throttled copy progress bar with the number of successfully written records, overall percentage, current table, transfer rate, and estimated copy time remaining. It also reports each non-empty table as its copy completes. Verification, activation, and the automatic `config.yml` update follow the copy, so the ETA does not include those final stages.
|
||||
|
||||
#### 2. **Monitor the progress:**
|
||||
* The migration will display detailed progress information
|
||||
* Large databases may take considerable time to complete
|
||||
* Progress bars and speed indicators will show current status
|
||||
New events logged while migration is running remain in the in-memory consumer queue. After successful activation, they are written to the target; if migration fails and the source is restored, persistence resumes there instead. Queued events are not stored for recovery after a process restart, and database-backed commands may report that the database is busy. Large migrations can take hours and use significant CPU, memory, disk, and network bandwidth, so keep server activity low and do not stop or restart the server until the command reports success or failure.
|
||||
|
||||
#### 3. **Do not interrupt the process:**
|
||||
* Avoid restarting your server during migration
|
||||
* Let the process complete naturally
|
||||
Do not run `/co reload`, start a purge or rollback, or otherwise change database configuration while migration is running. CoreProtect normally rejects these conflicting operations, but they should not be used as part of the migration procedure.
|
||||
|
||||
### Step 3: Post-Migration
|
||||
## Data Integrity
|
||||
|
||||
**After successful completion:**
|
||||
The migration copies all logical CoreProtect history and reference data, including rollback state and entity tracking. It preserves copied row IDs. MySQL, DuckDB, and ClickHouse targets also preserve allocator high-water marks; SQLite continues after the largest surviving row ID, matching its existing allocation behavior.
|
||||
|
||||
1. **Automatic switchover:** CoreProtect will automatically begin using the new database
|
||||
2. **Verify configuration:** Ensure `use-mysql` is set correctly in your `config.yml` as per step 1
|
||||
3. **Test functionality:** Perform basic CoreProtect operations to verify everything works
|
||||
4. **Clean up:** Once satisfied, you may manually delete your old database files
|
||||
CoreProtect checks every table's row count, minimum and maximum row IDs, and applicable allocator high-water marks before activation. By default, it also compares approximately 1% of rows in each large history table, including their complete metadata. Tables with at most 100,000 rows and reference tables (`*_map`, `user`, `world`, and `version`) receive full comparisons.
|
||||
|
||||
---
|
||||
Samples are random contiguous ranges distributed across each million rows actually copied, rather than across the numeric row-ID space. Deleted-ID gaps therefore do not bias selection. Only selected ranges are reread for content verification, and the console reports the validation mode and progress against the number of rows selected for comparison. A small temporary file containing sample boundaries is removed after success or failure.
|
||||
|
||||
## Important Considerations
|
||||
Sampled validation can miss differences outside the selected ranges. Add `--full-validation` to compare every copied row instead. Any detected mismatch prevents activation in either mode. Sampling reduces content comparisons, but storage-engine scans and statistics checks can still make verification expensive.
|
||||
|
||||
### Migration Safety
|
||||
Database-lock metadata is initialized separately; ClickHouse targets also initialize their own current version row. For a ClickHouse source, the current logical version of each row is copied; superseded internal ClickHouse revisions are storage-engine history rather than separate CoreProtect records.
|
||||
|
||||
* **Non-destructive process:** The migration does not modify or delete your existing database
|
||||
* **Interrupted migration:** If something goes wrong, simply delete the new database and continue using the old one
|
||||
* **Data verification:** The process includes automatic verification to ensure data integrity
|
||||
Source history rows are not deleted or replaced. Migration may update operational metadata such as database-lock heartbeats and allocator state.
|
||||
|
||||
### Performance & Requirements
|
||||
## After a Successful Migration
|
||||
|
||||
* **Processing time:** Large databases with millions of records may take hours to migrate
|
||||
* **Resource usage:** The migration process is resource-intensive and may impact server performance
|
||||
* **Parallel processing:** SQLite to MySQL migrations use optimized parallel processing for better speed
|
||||
1. CoreProtect atomically changes `database-type` in `config.yml` to the target before queued writes resume. If the legacy `use-mysql` option exists, it is updated at the same time (`true` only for MySQL). Comments and unrelated settings are preserved.
|
||||
2. The running server immediately begins persisting queued and new events to the target. Keep the server in maintenance mode until the restart check below succeeds.
|
||||
3. Review the completion message and server log, then test `/co status` and a representative lookup.
|
||||
4. Restart once and verify that CoreProtect reconnects to the same target and can still read old and new records. No manual database selector edit is required.
|
||||
5. Resume normal activity and test a newly logged change.
|
||||
6. Keep the source backup until the migrated database has been verified. Archive or remove it only when you are satisfied that the migration is complete.
|
||||
|
||||
### Restrictions & Limitations
|
||||
When a ClickHouse source or target namespace is shared, stop every peer first and run the migration from exactly one installation with `database-lock` enabled. Before restarting any peer that will use the migrated target namespace, update its database selector, connection, and prefix to match the migrated installation.
|
||||
|
||||
* **Console only:** Cannot be executed from in-game chat
|
||||
* **Interruption handling:** If interrupted, the target database must be manually wiped before restarting
|
||||
* **Patreon exclusive:** Only available in CoreProtect 23.0+ Patreon builds
|
||||
## Failure and Recovery
|
||||
|
||||
---
|
||||
The command does not resume a partial migration. A failed or interrupted attempt can leave rows, schema objects, or allocator state in the target, so clean only the failed target namespace before retrying. For DuckDB, stop the server and remove the failed target file before starting over. Never clean or replace the source database as part of retrying.
|
||||
|
||||
If target activation or the atomic `config.yml` update fails after verification, CoreProtect restores the source when possible and does not resume writes on the target. The selector is changed only after the target has initialized successfully, so an interruption before that change restarts on the source, while an interruption after it restarts on the verified target. If neither the target nor source can be initialized, persistence is halted to avoid writing to an unknown database state. Correct the connection or configuration problem and restart the server; any events still queued only in memory cannot survive that restart.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
### Migration Does Not Start
|
||||
|
||||
**Migration won't start:**
|
||||
* Confirm that the command is being run from the console on a CoreProtect 23.0+ Patreon build, or 25.0+ if DuckDB or ClickHouse is involved.
|
||||
* Confirm that the selected target differs from the active source backend.
|
||||
* Keep `database-lock` enabled and wait for any purge, rollback, reload, or previous migration to finish.
|
||||
* Check that the target namespace contains no CoreProtect data. DuckDB must use a new database file.
|
||||
|
||||
* Verify you're using a CoreProtect 23.0+ Patreon build
|
||||
* Ensure you're not trying to migrate to the same database type
|
||||
* Check that no other CoreProtect operations are running
|
||||
### Connection or Schema Failure
|
||||
|
||||
**Migration interrupted:**
|
||||
* Confirm that the target settings were loaded while `database-type` still selected the source.
|
||||
* Verify the target host, port, database, credentials, table prefix, and available storage.
|
||||
* Confirm that the target account can create, read, insert, update or mutate, and delete or drop the required objects.
|
||||
* For ClickHouse, confirm the server is version 26.1 or newer, no other process is using the same CoreProtect data directory, and every other CoreProtect installation sharing the source or target namespace is stopped.
|
||||
|
||||
* Manually delete/wipe the target database
|
||||
* Verify server stability before restarting migration
|
||||
* Consider migrating during low-activity periods
|
||||
### Verification Failure or Interruption
|
||||
|
||||
**Performance issues:**
|
||||
* Review the server log for the table and row reported by the failure.
|
||||
* Keep the source database and backup intact.
|
||||
* Clean the partial target namespace, use a new DuckDB file when applicable, and retry during a quieter maintenance period.
|
||||
|
||||
* Monitor server resources during migration
|
||||
* Consider temporarily reducing server activity
|
||||
* Large tables may cause temporary slowdowns
|
||||
### Automatic Configuration Update Fails
|
||||
|
||||
**Data verification failures:**
|
||||
Check that `plugins/CoreProtect/config.yml` is a regular, writable file on a filesystem that supports atomic replacement. CoreProtect reports the migration as failed and restores the source before persistence resumes. The verified target still contains the copied data, so clean only that failed target namespace before retrying; do not manually select it or delete the source.
|
||||
|
||||
* Check database connectivity and permissions
|
||||
* Review server logs for specific error messages
|
||||
* Ensure sufficient disk space on target database
|
||||
## Getting Help
|
||||
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues with database migration:
|
||||
|
||||
1. Check server logs for detailed error messages
|
||||
2. Verify database permissions and connectivity
|
||||
3. Ensure adequate system resources (RAM, CPU, disk space)
|
||||
4. Contact support through the CoreProtect Discord with specific error details
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
The migration process includes several advanced features:
|
||||
|
||||
* Batch processing with dynamic sizing based on performance
|
||||
* Automatic retry mechanisms for temporary failures
|
||||
* Data integrity verification comparing source and target records
|
||||
* Progress tracking with estimated completion times
|
||||
* Graceful error handling with detailed logging
|
||||
|
||||
The migration handles all CoreProtect data types including:
|
||||
|
||||
* Block changes and rollbacks
|
||||
* Container transactions
|
||||
* Player interactions and sessions
|
||||
* Chat messages and commands
|
||||
* User data and statistics
|
||||
* All metadata and configuration data
|
||||
If a migration fails, keep both databases intact and review the complete error and surrounding server log. When asking for help in the [CoreProtect Discord](https://discord.gg/b4DZ4jy), include your CoreProtect version, source and destination database types, the complete migration error, and the relevant log excerpt.
|
||||
+6
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
For more information about CoreProtect, visit [coreprotect.net](http://coreprotect.net).
|
||||
|
||||
Looking for another platform? Visit [CoreProtect for Fabric](https://coreprotect.net/fabric) or [CoreProtect for Hytale](https://coreprotect.net/hytale).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Please visit one of the following links for more information.
|
||||
@@ -19,7 +21,10 @@ A list of all language codes that can be used with CoreProtect.
|
||||
Log your own block changes, perform lookups, rollbacks, restores, and more.
|
||||
|
||||
[**Database Migration**](/database-migration/)
|
||||
Migrate your CoreProtect database between SQLite and MySQL.
|
||||
Migrate your CoreProtect database between supported backends.
|
||||
|
||||
[**Automatic Purging**](/auto-purge/)
|
||||
Automatically remove old CoreProtect data on a daily schedule.
|
||||
|
||||
[**Compatible Third-Party Plugins & Tools**](/tools-integrations/)
|
||||
A list of plugins and tools that are compatible with CoreProtect.
|
||||
|
||||
+9
-1
@@ -39,6 +39,9 @@ The following permissions can be used to restrict functionality within the plugi
|
||||
* **coreprotect.consumer** *(default: op)*
|
||||
Allows access to the CoreProtect consumer command.
|
||||
|
||||
* **coreprotect.give** *(default: false)*
|
||||
Allows access to the CoreProtect give command.
|
||||
|
||||
* **coreprotect.networking** *(default: op)*
|
||||
Allows access to the CoreProtect networking API.
|
||||
|
||||
@@ -46,6 +49,8 @@ The following permissions can be used to restrict functionality within the plugi
|
||||
|
||||
## Child Permissions
|
||||
|
||||
> **Note:** Child permissions are not presently supported by [CoreProtect for Hytale](https://coreprotect.net/hytale).
|
||||
|
||||
* **coreprotect.lookup.block** *(default: op)*
|
||||
Can be optionally used as a negative permission to prevent block lookups.
|
||||
|
||||
@@ -70,6 +75,9 @@ The following permissions can be used to restrict functionality within the plugi
|
||||
* **coreprotect.lookup.kill** *(default: op)*
|
||||
Can be optionally used as a negative permission to prevent entity kill lookups.
|
||||
|
||||
* **coreprotect.lookup.spawn** *(default: op)*
|
||||
Can be optionally used as a negative permission to prevent entity spawn lookups.
|
||||
|
||||
* **coreprotect.lookup.near** *(default: op)*
|
||||
Can be optionally used as a negative permission to prevent lookups via the near command.
|
||||
|
||||
@@ -93,4 +101,4 @@ The following permissions can be used to restrict functionality within the plugi
|
||||
Allows access to the CoreProtect "/core" command.
|
||||
|
||||
* **coreprotect.coreprotect** *(default: false)*
|
||||
Allows access to the CoreProtect "/coreprotect" command.
|
||||
Allows access to the CoreProtect "/coreprotect" command.
|
||||
+29
-1
@@ -17,11 +17,18 @@ CONSUMER_TOGGLED: "Consumer queue processing has been {paused|resumed}."
|
||||
CONTAINER_HEADER: "Container Transactions"
|
||||
CPU_CORES: "CPU cores."
|
||||
DATABASE_BUSY: "Database busy. Please try again later."
|
||||
DATABASE_FALLBACK: "Unable to load {0}. Using {1}; config.yml updated."
|
||||
DATABASE_INDEX_ERROR: "Unable to validate database indexes."
|
||||
DATABASE_LOCKED_1: "Database locked. Waiting up to 15 seconds..."
|
||||
DATABASE_LOCKED_2: "Database is already in use. Please try again."
|
||||
DATABASE_LOCKED_3: "To disable database locking, set \"database-lock: false\"."
|
||||
DATABASE_LOCKED_4: "Disabling database locking can result in data corruption."
|
||||
DATABASE_PERSISTENCE_HALTED: "Database writes halted. Please restart the server."
|
||||
DATABASE_QUEUE_LOSS: "Queued data will be lost when the server stops."
|
||||
DATABASE_RECOVERY_FAILED: "Database recovery failed after {0} attempts."
|
||||
DATABASE_RECOVERY_STARTED: "Database became unavailable. Attempting database recovery."
|
||||
DATABASE_RECOVERY_SUCCESS: "Database recovery completed. Queued writes will resume."
|
||||
DATABASE_TYPE_OVERRIDE: "Option \"database-type\" overrides \"use-mysql\"."
|
||||
DATABASE_UNREACHABLE: "Database is unreachable. Discarding data and shutting down."
|
||||
DEVELOPMENT_BRANCH: "Development branch detected, skipping patch scripts."
|
||||
DIRT_BLOCK: "Placed a temporary safety block under you."
|
||||
@@ -36,10 +43,12 @@ FIRST_VERSION: "Initial DB: {0}"
|
||||
GLOBAL_LOOKUP: "Don't specify a radius to do a global lookup."
|
||||
GLOBAL_ROLLBACK: "Use \"{0}\" to do a global {rollback|restore}"
|
||||
HELP_ACTION_1: "Restrict the lookup to a certain action."
|
||||
HELP_ACTION_2: "Examples: [a:block], [a:+block], [a:-block] [a:click], [a:container], [a:inventory], [a:item], [a:kill], [a:chat], [a:command], [a:sign], [a:session], [a:username]"
|
||||
HELP_ACTION_2: "Examples: [a:block], [a:+block], [a:-block] [a:click], [a:container], [a:inventory], [a:item], [a:kill], [a:spawn], [a:chat], [a:command], [a:sign], [a:session], [a:username]"
|
||||
HELP_COMMAND: "Display more info for that command."
|
||||
HELP_EXCLUDE_1: "Exclude blocks/users."
|
||||
HELP_EXCLUDE_2: "Examples: [e:stone], [e:Notch], [e:stone,Notch]"
|
||||
HELP_FILTER_1: "Filter chat, command, or sign text."
|
||||
HELP_FILTER_2: "Examples: [a:command f:/co], [a:sign f:Shop]"
|
||||
HELP_HEADER: "{0} Help"
|
||||
HELP_INCLUDE_1: "Include specific blocks/entities."
|
||||
HELP_INCLUDE_2: "Examples: [i:stone], [i:zombie], [i:stone,wood,bedrock]"
|
||||
@@ -66,6 +75,11 @@ HELP_PARAMS_6: "Include specific blocks/entities in the {lookup|rollback|restore
|
||||
HELP_PARAMS_7: "Exclude blocks/users from the {lookup|rollback|restore}."
|
||||
HELP_PURGE_1: "Delete data older than specified time."
|
||||
HELP_PURGE_2: "For example, \"{0}\" will delete all data older than one month, and only keep the last 30 days of data."
|
||||
HELP_PURGE_3: "Specify the amount of time to purge."
|
||||
HELP_PURGE_4: "Specify a world or radius to limit the purge to."
|
||||
HELP_PURGE_5: "Include specific blocks or entity kills in the purge."
|
||||
HELP_PURGE_6: "Keep entity kills of these types."
|
||||
HELP_PURGE_7: "Only purge entity kills."
|
||||
HELP_PURGE_COMMAND: "Delete old block data."
|
||||
HELP_RADIUS_1: "Specify a radius area."
|
||||
HELP_RADIUS_2: "Examples: [r:10] (Only make changes within 10 blocks of you)"
|
||||
@@ -94,6 +108,8 @@ INVALID_CONTAINER: "Please inspect a valid container first."
|
||||
INVALID_DONATION_KEY: "Invalid donation key."
|
||||
INVALID_INCLUDE: "\"{0}\" is an invalid block/entity name."
|
||||
INVALID_INCLUDE_COMBO: "That is an invalid block/entity combination."
|
||||
INVALID_ITEM_ID: "Please enter a valid item id."
|
||||
INVALID_PARAMETER: "\"{0}\" is not a supported parameter."
|
||||
INVALID_RADIUS: "Please enter a valid radius."
|
||||
INVALID_SELECTION: "{0} selection not found."
|
||||
INVALID_USERNAME: "\"{0}\" is an invalid username."
|
||||
@@ -108,6 +124,10 @@ LOGGING_ITEMS: "{0} items left to log. Please wait..."
|
||||
LOGGING_TIME_LIMIT: "Logging time limit reached. Discarding data and shutting down."
|
||||
LOOKUP_BLOCK: "{0} {placed|broke} {1}."
|
||||
LOOKUP_CONTAINER: "{0} {added|removed} {1} {2}."
|
||||
LOOKUP_ENTITY_INTERACTION: "{0} {clicked|sheared|leashed|unleashed} {1}."
|
||||
LOOKUP_ENTITY_INTERACTION_ORIGIN: "Origin location"
|
||||
LOOKUP_ENTITY_ORIGIN: "Spawn location"
|
||||
LOOKUP_ENTITY_SPAWN: "{0} spawned {1}."
|
||||
LOOKUP_HEADER: "{0} Lookup Results"
|
||||
LOOKUP_INTERACTION: "{0} {clicked|killed} {1}."
|
||||
LOOKUP_ITEM: "{0} {picked up|dropped} {1} {2}."
|
||||
@@ -119,7 +139,9 @@ LOOKUP_SEARCHING: "Lookup searching. Please wait..."
|
||||
LOOKUP_STORAGE: "{0} {deposited|withdrew} {1} {2}."
|
||||
LOOKUP_TIME: "{0} ago"
|
||||
LOOKUP_USERNAME: "{0} logged in as {1}."
|
||||
MAXIMUM_PURGE_RADIUS: "The maximum purge radius is {0}."
|
||||
MAXIMUM_RADIUS: "The maximum {lookup|rollback|restore} radius is {0}."
|
||||
MESSAGE_FILTER_TOO_SHORT: "Minimum message filter length is {0} characters."
|
||||
MISSING_ACTION_USER: "To use that action, please specify a user."
|
||||
MISSING_LOOKUP_TIME: "Please specify the amount of time to {lookup|rollback|restore}."
|
||||
MISSING_LOOKUP_USER: "Please specify a user or {block|radius} to lookup."
|
||||
@@ -142,6 +164,8 @@ PATCH_PROCESSING: "Processing new data. Please wait..."
|
||||
PATCH_SKIP_UPDATE: "Skipping {table|index} {update|creation|removal} on {0}."
|
||||
PATCH_STARTED: "Performing {0} upgrade. Please wait..."
|
||||
PATCH_SUCCESS: "Successfully upgraded to {0}."
|
||||
PATCH_TABLE_COMPLETED: "Successfully updated {0} table."
|
||||
PATCH_TABLE_STARTED: "Updating {0} table. Please wait..."
|
||||
PATCH_UPGRADING: "Database upgrade in progress. Please wait..."
|
||||
PLEASE_SELECT: "Please select: \"{0}\" or \"{1}\"."
|
||||
PREVIEW_CANCELLED: "Preview cancelled."
|
||||
@@ -163,6 +187,7 @@ PURGE_ROWS: "{0} {row|rows} of data deleted."
|
||||
PURGE_STARTED: "Data purge started on \"{0}\"."
|
||||
PURGE_SUCCESS: "Data purge successful."
|
||||
RAM_STATS: "{0}GB / {1}GB RAM"
|
||||
RELOAD_FAILED: "Database reload failed. Please try again."
|
||||
RELOAD_STARTED: "Reloading configuration - please wait."
|
||||
RELOAD_SUCCESS: "Configuration successfully reloaded."
|
||||
ROLLBACK_ABORTED: "Rollback or restore aborted."
|
||||
@@ -180,8 +205,10 @@ ROLLBACK_STARTED: "{Rollback|Restore|Preview} started on \"{0}\"."
|
||||
ROLLBACK_TIME: "Time range: {0}."
|
||||
ROLLBACK_WORLD_ACTION: "Restricted to {world|action} \"{0}\"."
|
||||
SIGN_HEADER: "Sign Messages"
|
||||
STATUS_AUTO_PURGE: "Cleanup: {0} {row|rows} auto purged since restart."
|
||||
STATUS_CONSUMER: "Consumer: {0} {item|items} in queue."
|
||||
STATUS_DATABASE: "Database: Using {0}."
|
||||
STATUS_DATABASE_STATE: "({writes halted|unreachable})"
|
||||
STATUS_INTEGRATION: "{0}: Integration {enabled|disabled}."
|
||||
STATUS_LICENSE: "License: {0}"
|
||||
STATUS_SYSTEM: "System: {0}"
|
||||
@@ -203,6 +230,7 @@ UPDATE_NOTICE: "Notice: {0} is now available."
|
||||
UPGRADE_IN_PROGRESS: "Upgrade in progress. Please try again later."
|
||||
USER_NOT_FOUND: "User \"{0}\" not found."
|
||||
USER_OFFLINE: "The user \"{0}\" is not online."
|
||||
USING_DATABASE: "Using {0} for data storage."
|
||||
USING_MYSQL: "Using MySQL for data storage."
|
||||
USING_SQLITE: "Using SQLite for data storage."
|
||||
VALID_DONATION_KEY: "Valid donation key."
|
||||
|
||||
+144
-126
@@ -1,202 +1,220 @@
|
||||
# CoreProtect Language File (ru)
|
||||
|
||||
ACTION_NOT_SUPPORTED: "Данное действие не поддерживается этой командой."
|
||||
ACTION_NOT_SUPPORTED: "Это действие не поддерживается данной командой."
|
||||
AMOUNT_BLOCK: "{0} {блок|блоков}"
|
||||
AMOUNT_CHUNK: "{0} {чанк|чанков}"
|
||||
AMOUNT_ENTITY: "{0} {существо|существ}"
|
||||
AMOUNT_ITEM: "{0} {предмет|предметов}"
|
||||
API_TEST: "Тест API прошёл успешно!"
|
||||
CACHE_ERROR: "ВНИМАНИЕ: Ошибка при проверке кеша {0}."
|
||||
CACHE_RELOAD: "Принудительная перезагрузка кеша {отображения|мира} из базы данных."
|
||||
API_TEST: "Тестирование API завершено успешно!"
|
||||
CACHE_ERROR: "ВНИМАНИЕ: Ошибка при проверке кэша {0}."
|
||||
CACHE_RELOAD: "Принудительная перезагрузка кэша {сопоставлений|мира} из базы данных."
|
||||
CHECK_CONFIG: "Пожалуйста, проверьте config.yml"
|
||||
COMMAND_CONSOLE: "Пожалуйста, запустите команду из консоли."
|
||||
COMMAND_CONSOLE: "Пожалуйста, запустите эту команду из консоли."
|
||||
COMMAND_NOT_FOUND: "Команда «{0}» не найдена."
|
||||
COMMAND_THROTTLED: "Подождите немного и попробуйте еще раз."
|
||||
CONSUMER_ERROR: "Обработка потребительской очереди уже {приостановлено|возобновлено}."
|
||||
CONSUMER_TOGGLED: "Обработка очереди потребителей была {приостановлено|возобновлено}."
|
||||
CONTAINER_HEADER: "Контейнерные транзакции"
|
||||
COMMAND_THROTTLED: "Пожалуйста, подождите немного и попробуйте ещё раз."
|
||||
CONSUMER_ERROR: "Обработка очереди потребителя уже {приостановлена|возобновлена}."
|
||||
CONSUMER_TOGGLED: "Обработка очереди потребителя была {приостановлена|возобновлена}."
|
||||
CONTAINER_HEADER: "Транзакции контейнеров"
|
||||
CPU_CORES: "Ядер процессора."
|
||||
DATABASE_BUSY: "База данных занята. Пожалуйста, повторите попытку позже."
|
||||
DATABASE_INDEX_ERROR: "Невозможно проверить индексы базы данных."
|
||||
DATABASE_LOCKED_1: "База данных заблокирована. Подождите около 15 секунд..."
|
||||
DATABASE_LOCKED_2: "База данных уже используется. Пожалуйста, попробуйте еще раз."
|
||||
DATABASE_INDEX_ERROR: "Не удалось проверить индексы базы данных."
|
||||
DATABASE_LOCKED_1: "База данных заблокирована. Ожидание до 15 секунд..."
|
||||
DATABASE_LOCKED_2: "База данных уже используется. Пожалуйста, попробуйте ещё раз."
|
||||
DATABASE_LOCKED_3: "Чтобы отключить блокировку базы данных, установите «database-lock: false»."
|
||||
DATABASE_LOCKED_4: "Отключение блокировки базы данных может привести к повреждению данных."
|
||||
DATABASE_UNREACHABLE: "База данных недоступна. Удаление данных и завершение работы."
|
||||
DEVELOPMENT_BRANCH: "Обнаружена ветвь разработки, скрипты исправлений пропущены."
|
||||
DIRT_BLOCK: "Под вами установлен блок грязи."
|
||||
DISABLE_SUCCESS: "{0} был успешно отключён!"
|
||||
ENABLE_FAILED: "{0} не удалось запустить."
|
||||
ENABLE_SUCCESS: "{0} был успешно запущен!"
|
||||
DATABASE_UNREACHABLE: "База данных недоступна. Сброс данных и завершение работы."
|
||||
DEVELOPMENT_BRANCH: "Обнаружена ветка разработки, выполнение скриптов обновления пропущено."
|
||||
DIRT_BLOCK: "Под вами размещён временный безопасный блок."
|
||||
DISABLE_SUCCESS: "Успешно! {0} отключён."
|
||||
DONATION_KEY_REQUIRED: "Для этой команды требуется действующий ключ пожертвования."
|
||||
ENABLE_FAILED: "Не удалось запустить {0}."
|
||||
ENABLE_SUCCESS: "{0} успешно запущен!"
|
||||
ENJOY_COREPROTECT: "Нравится {0}? Присоединяйтесь к нашему Discord!"
|
||||
FINISHING_CONVERSION: "Завершение преобразования данных. Пожалуйста, подождите..."
|
||||
FINISHING_LOGGING: "Завершение регистрации данных. Пожалуйста, подождите..."
|
||||
FINISHING_LOGGING: "Завершение записи данных. Пожалуйста, подождите..."
|
||||
FIRST_VERSION: "Исходная БД: {0}"
|
||||
GLOBAL_LOOKUP: "Не указывайте радиус для глобального поиска."
|
||||
GLOBAL_ROLLBACK: "Используйте «{0}», чтобы сделать {откат|восстановление} глобально"
|
||||
HELP_ACTION_1: "Ограничьте поиск определенным действием."
|
||||
GLOBAL_ROLLBACK: "Используйте «{0}» для глобального {отката|восстановления}."
|
||||
HELP_ACTION_1: "Ограничить поиск определённым действием."
|
||||
HELP_ACTION_2: "Например: [a:block], [a:+block], [a:-block] [a:click], [a:container], [a:inventory], [a:item], [a:kill], [a:chat], [a:command], [a:sign], [a:session], [a:username]"
|
||||
HELP_COMMAND: "Показать дополнительную информацию."
|
||||
HELP_EXCLUDE_1: "Исключите блоки/пользователей."
|
||||
HELP_COMMAND: "Показать подробную информацию о команде."
|
||||
HELP_EXCLUDE_1: "Исключить блоки или пользователей."
|
||||
HELP_EXCLUDE_2: "Например: [e:stone], [e:Notch], [e:stone,Notch]"
|
||||
HELP_HEADER: "{0} Помощь"
|
||||
HELP_INCLUDE_1: "Включите определенные блоки/сущности."
|
||||
HELP_HEADER: "{0} Справка"
|
||||
HELP_INCLUDE_1: "Включить определённые блоки или сущности."
|
||||
HELP_INCLUDE_2: "Например: [i:stone], [i:zombie], [i:stone,wood,bedrock]"
|
||||
HELP_INSPECT_1: "С включенным режимом инспектора вы можете сделать следующее:"
|
||||
HELP_INSPECT_2: "Щелкните левой кнопкой мыши по блоку, чтобы увидеть, кто разместил этот блок.."
|
||||
HELP_INSPECT_3: "Щелкните блок правой кнопкой мыши, чтобы увидеть, какой соседний блок был сломан.."
|
||||
HELP_INSPECT_4: "Поместите блок, чтобы увидеть, какой блок был сломан в этом месте."
|
||||
HELP_INSPECT_5: "Поместите блок в жидкость (и т.п.), чтобы увидеть, кто его поместил."
|
||||
HELP_INSPECT_6: "Щелкните правой кнопкой мыши дверь, сундук и т.д., чтобы узнать, кто последний использовал их."
|
||||
HELP_INSPECT_7: "Подсказка: Вы можете использовать «/co i» для быстрого переключения режима инспектора."
|
||||
HELP_INSPECT_COMMAND: "Включает или выключает режим инспектора блоков."
|
||||
HELP_LIST: "Отображает список всех команд."
|
||||
HELP_LOOKUP_1: "Команда быстрого доступа."
|
||||
HELP_LOOKUP_2: "Используйте после проверки блока для просмотра журналов."
|
||||
HELP_LOOKUP_COMMAND: "Глобальный поиск данных блоков."
|
||||
HELP_INSPECT_1: "С включённым инспектором вы можете делать следующее:"
|
||||
HELP_INSPECT_2: "Нажмите ЛКМ по блоку, чтобы узнать, кто его установил."
|
||||
HELP_INSPECT_3: "Нажмите ПКМ по блоку, чтобы узнать, какой соседний блок был сломан."
|
||||
HELP_INSPECT_4: "Установите блок, чтобы узнать, какой блок был сломан в этом месте."
|
||||
HELP_INSPECT_5: "Установите блок в жидкость (и т.п.), чтобы узнать, кто её разлил."
|
||||
HELP_INSPECT_6: "Нажмите ПКМ по двери, сундуку и т.д., чтобы узнать, кто последний их использовал."
|
||||
HELP_INSPECT_7: "Подсказка: используйте «/co i» для быстрого доступа."
|
||||
HELP_INSPECT_COMMAND: "Включает или выключает инспектор блоков."
|
||||
HELP_LIST: "Отобразить список всех команд."
|
||||
HELP_LOOKUP_1: "Быстрый доступ к команде."
|
||||
HELP_LOOKUP_2: "Используйте после проверки блока для просмотра логов."
|
||||
HELP_LOOKUP_COMMAND: "Расширенный поиск данных блоков."
|
||||
HELP_NO_INFO: "Информация о команде «{0}» не найдена."
|
||||
HELP_PARAMETER: "Пожалуйста, просмотрите «{0}» для получения подробной информации о параметрах."
|
||||
HELP_PARAMS_1: "Выполните {поиск|откат|восстановление}."
|
||||
HELP_PARAMS_2: "Укажите пользователей для {поиска|отката|восстановления}."
|
||||
HELP_PARAMS_3: "Укажите время для {поиска|отката|восстановления}."
|
||||
HELP_PARAMS_4: "Укажите область радиуса, чтобы ограничить {поиск|откат|восстановление}."
|
||||
HELP_PARAMS_5: "Ограничьте {поиск|откат|восстановление} определенным действием."
|
||||
HELP_PARAMS_6: "Включите определенные блоки/объекты в {поиск|откат|восстановление}."
|
||||
HELP_PARAMS_7: "Исключите блоки/пользователей из {поиска|отката|восстановления}."
|
||||
HELP_PARAMETER: "Пожалуйста, обратитесь к «{0}» для получения подробной информации о параметрах."
|
||||
HELP_PARAMS_1: "Выполнить {поиск|откат|восстановление}."
|
||||
HELP_PARAMS_2: "Указать пользователей для {поиска|отката|восстановления}."
|
||||
HELP_PARAMS_3: "Указать промежуток времени для {поиска|отката|восстановления}."
|
||||
HELP_PARAMS_4: "Указать радиус области, чтобы ограничить {поиск|откат|восстановление}."
|
||||
HELP_PARAMS_5: "Ограничить {поиск|откат|восстановление} определённым действием."
|
||||
HELP_PARAMS_6: "Включить определённые блоки/сущности в {поиск|откат|восстановление}."
|
||||
HELP_PARAMS_7: "Исключить блоки/пользователей из {поиска|отката|восстановления}."
|
||||
HELP_PURGE_1: "Удалить данные старше указанного времени."
|
||||
HELP_PURGE_2: "Например, «{0}» удалит все данные старше одного месяца и сохранит данные только за последние 30 дней."
|
||||
HELP_PURGE_COMMAND: "Удалит старые данные блока."
|
||||
HELP_RADIUS_1: "Укажите радиус области."
|
||||
HELP_RADIUS_2: "Например: [r:10] (Внесёт изменения только в пределах 10 блоков от вас)"
|
||||
HELP_RELOAD_COMMAND: "Перезагружает файл конфигурации."
|
||||
HELP_RESTORE_COMMAND: "Восстановит данные блока."
|
||||
HELP_ROLLBACK_COMMAND: "Откатит данные блока."
|
||||
HELP_PURGE_2: "Например, «{0}» удалит все данные старше одного месяца, оставив данные только за последние 30 дней."
|
||||
HELP_PURGE_3: "Указать время для очистки."
|
||||
HELP_PURGE_4: "Указать мир, чтобы ограничить очистку."
|
||||
HELP_PURGE_5: "Включить определённые блоки в очистку."
|
||||
HELP_PURGE_COMMAND: "Удаление старых данных блоков."
|
||||
HELP_RADIUS_1: "Указать радиус области."
|
||||
HELP_RADIUS_2: "Например: [r:10] (изменения только в радиусе 10 блоков от вас)"
|
||||
HELP_RELOAD_COMMAND: "Перезагружает конфигурационный файл."
|
||||
HELP_RESTORE_COMMAND: "Восстановление данных блоков."
|
||||
HELP_ROLLBACK_COMMAND: "Откат данных блоков."
|
||||
HELP_STATUS: "Просмотр статуса плагина и информации о версии."
|
||||
HELP_STATUS_COMMAND: "Отображает статус плагина."
|
||||
HELP_TELEPORT: "Телепорт в локацию."
|
||||
HELP_TIME_1: "Укажите время поиска."
|
||||
HELP_TELEPORT: "Телепортироваться к локации."
|
||||
HELP_TIME_1: "Указать время для поиска."
|
||||
HELP_TIME_2: "Например: [t:2w,5d,7h,2m,10s], [t:5d2h], [t:2.50h]"
|
||||
HELP_USER_1: "Укажите пользовател(я/ей) для поиска."
|
||||
HELP_USER_1: "Указать пользователей для поиска."
|
||||
HELP_USER_2: "Например: [u:Notch], [u:Notch,#enderman]"
|
||||
INCOMPATIBLE_ACTION: "«{0}» нельзя использовать с этим действием."
|
||||
INSPECTOR_ERROR: "Режим инспектора уже {включён|выключен}."
|
||||
INSPECTOR_TOGGLED: "Режим инспектора теперь {включён|выключен}."
|
||||
INTEGRATION_ERROR: "Невозможно {инициализировать|отключить} регистрацию {0}."
|
||||
INTEGRATION_SUCCESS: "{0} регистрация успешно {инициализирована|отключена}."
|
||||
INTEGRATION_VERSION: "Обнаружена недопустимая версия {0}."
|
||||
INTERACTIONS_HEADER: "Взаимодействие с игроками"
|
||||
INCOMPATIBLE_ACTION: "Параметр «{0}» нельзя использовать с этим действием."
|
||||
INSPECTOR_ERROR: "Инспектор уже {включён|выключен}."
|
||||
INSPECTOR_TOGGLED: "Инспектор теперь {включён|выключен}."
|
||||
INTEGRATION_ERROR: "Не удалось {инициализировать|отключить} логирование {0}."
|
||||
INTEGRATION_SUCCESS: "Логирование {0} успешно {инициализировано|отключено}."
|
||||
INTEGRATION_VERSION: "Обнаружена несовместимая версия {0}."
|
||||
INTERACTIONS_HEADER: "Взаимодействие игроков"
|
||||
INVALID_ACTION: "Это недопустимое действие."
|
||||
INVALID_BRANCH_1: "Неверная версия плагина (ветка не задана)."
|
||||
INVALID_BRANCH_1: "Некорректная версия плагина (ветка не задана)."
|
||||
INVALID_BRANCH_2: "Чтобы продолжить, установите ветку проекта на «development»."
|
||||
INVALID_BRANCH_3: "Запуск кода который находится в разработке может привести к повреждению данных."
|
||||
INVALID_CONTAINER: "Пожалуйста, сначала проверьте действующий контейнер."
|
||||
INVALID_BRANCH_3: "Запуск кода из ветки разработки может привести к повреждению данных."
|
||||
INVALID_CONTAINER: "Пожалуйста, сначала осмотрите корректный контейнер."
|
||||
INVALID_DONATION_KEY: "Недействительный ключ пожертвования."
|
||||
INVALID_INCLUDE: "«{0}» неверное имя блока/сущности."
|
||||
INVALID_INCLUDE_COMBO: "Это недопустимая комбинация блока/сущности."
|
||||
INVALID_RADIUS: "Пожалуйста, введите допустимый радиус."
|
||||
INVALID_SELECTION: "Выбор {0} не найден."
|
||||
INVALID_USERNAME: "«{0}» неверное имя пользователя."
|
||||
INVALID_WORLD: "Укажите действительный мир."
|
||||
INVALID_INCLUDE: "«{0}» - некорректное имя блока/сущности."
|
||||
INVALID_INCLUDE_COMBO: "Некорректная комбинация блока/сущности."
|
||||
INVALID_ITEM_ID: "Пожалуйста, введите корректный ID предмета."
|
||||
INVALID_PARAMETER: "Параметр «{0}» не поддерживается."
|
||||
INVALID_RADIUS: "Пожалуйста, введите корректный радиус."
|
||||
INVALID_SELECTION: "Выделение {0} не найдено."
|
||||
INVALID_USERNAME: "«{0}» - некорректное имя пользователя."
|
||||
INVALID_WORLD: "Пожалуйста, укажите существующий мир."
|
||||
LATEST_VERSION: "Последняя версия: {0}"
|
||||
LINK_DISCORD: "Discord: {0}"
|
||||
LINK_DOWNLOAD: "Загрузить: {0}"
|
||||
LINK_DOWNLOAD: "Скачать: {0}"
|
||||
LINK_PATREON: "Patreon: {0}"
|
||||
LINK_WIKI_BLOCK: "Названия блоков: {0}"
|
||||
LINK_WIKI_ENTITY: "Названия сущностей: {0}"
|
||||
LOGGING_ITEMS: "{0} предметы регистрируются. Пожалуйста, подождите..."
|
||||
LOGGING_TIME_LIMIT: "Достигнуто ограничение по времени регистрации. Удаление данных и завершение работы."
|
||||
LOOKUP_BLOCK: "{0} {поставил|сломал} {1}."
|
||||
LOGGING_ITEMS: "Осталось записать предметов: {0}. Пожалуйста, подождите..."
|
||||
LOGGING_TIME_LIMIT: "Превышен лимит времени записи. Сброс данных и завершение работы."
|
||||
LOOKUP_BLOCK: "{0} {установил|сломал} {1}."
|
||||
LOOKUP_CONTAINER: "{0} {добавил|забрал} {1} {2}."
|
||||
LOOKUP_HEADER: "{0} Результаты поиска"
|
||||
LOOKUP_INTERACTION: "{0} {нажал|убил} {1}."
|
||||
LOOKUP_ITEM: "{0} {подобрал|выкинул} {1} {2}."
|
||||
LOOKUP_INTERACTION: "{0} {взаимодействовал с|убил} {1}."
|
||||
LOOKUP_ITEM: "{0} {подобрал|выбросил} {1} {2}."
|
||||
LOOKUP_LOGIN: "{0} {вошёл|вышел}."
|
||||
LOOKUP_PAGE: "Страница {0}"
|
||||
LOOKUP_PROJECTILE: "{0} {бросил|выстрелил} {1} {2}."
|
||||
LOOKUP_ROWS_FOUND: "{0} {строка|строк} найдено."
|
||||
LOOKUP_SEARCHING: "Ведётся поиск. Пожалуйста подождите..."
|
||||
LOOKUP_STORAGE: "{0} {положил|забрал} {1} {2}."
|
||||
LOOKUP_PROJECTILE: "{0} {бросил|выпустил} {1} {2}."
|
||||
LOOKUP_ROWS_FOUND: "Найдено строк: {0}."
|
||||
LOOKUP_SEARCHING: "Выполняется поиск. Пожалуйста, подождите..."
|
||||
LOOKUP_STORAGE: "{0} {поместил|забрал} {1} {2}."
|
||||
LOOKUP_TIME: "{0} назад"
|
||||
LOOKUP_USERNAME: "{0} зарегистрирован как {1}."
|
||||
LOOKUP_USERNAME: "{0} вошёл как {1}."
|
||||
MAXIMUM_RADIUS: "Максимальный радиус для {поиска|отката|восстановления} - {0}."
|
||||
MISSING_ACTION_USER: "Чтобы использовать это действие, укажите пользователя."
|
||||
MISSING_LOOKUP_TIME: "Пожалуйста, укажите количество времени, чтобы использовать {поиск|откат|восстановление}."
|
||||
MISSING_LOOKUP_USER: "Укажите пользователя или {блок|радиус} для поиска."
|
||||
MISSING_PARAMETERS: "Пожалуйста используйте «{0}»."
|
||||
MISSING_ROLLBACK_RADIUS: "Вы не указали радиус {отката|восстановления}."
|
||||
MISSING_LOOKUP_TIME: "Пожалуйста, укажите промежуток времени для {поиска|отката|восстановления}."
|
||||
MISSING_LOOKUP_USER: "Пожалуйста, укажите пользователя или {блок|радиус} для поиска."
|
||||
MISSING_PARAMETERS: "Пожалуйста, используйте «{0}»."
|
||||
MISSING_ROLLBACK_RADIUS: "Вы не указали радиус для {отката|восстановления}."
|
||||
MISSING_ROLLBACK_USER: "Вы не указали пользователя для {отката|восстановления}."
|
||||
MYSQL_UNAVAILABLE: "Невозможно подключиться к серверу MySQL."
|
||||
MYSQL_UNAVAILABLE: "Не удалось подключиться к серверу MySQL."
|
||||
NETWORK_CONNECTION: "Подключение от {0} {успешно|не удалось}. Используется {1} {2}."
|
||||
NETWORK_TEST: "Тестовые данные сети успешно отправлены."
|
||||
NO_DATA: "Данные не найдены в {0}."
|
||||
NO_DATA_LOCATION: "В этом месте {данные|транзакции|взаимодействия|сообщения} не обнаружены.."
|
||||
NO_PERMISSION: "У вас нет прав чтобы сделать это."
|
||||
NO_DATA_LOCATION: "В этом месте {данные|транзакции|взаимодействия|сообщения} не найдены."
|
||||
NO_PERMISSION: "У вас недостаточно прав для выполнения этого действия."
|
||||
NO_RESULTS: "Результаты не найдены."
|
||||
NO_RESULTS_PAGE: "Для этой страницы {результатов|данных} не найдено."
|
||||
NO_ROLLBACK: "{Ожидаемый|Предыдущий} откат/восстановление не найдены."
|
||||
PATCH_INTERRUPTED: "Обновление прервано. Попробую снова при перезапуске."
|
||||
NO_ROLLBACK: "Не найдено {ожидающих|предыдущих} откатов/восстановлений."
|
||||
PATCH_INTERRUPTED: "Обновление прервано. Попытка будет повторена при перезапуске."
|
||||
PATCH_OUTDATED_1: "Невозможно обновить базы данных старше {0}."
|
||||
PATCH_OUTDATED_2: "Пожалуйста, обновите поддерживаемую версию CoreProtect."
|
||||
PATCH_OUTDATED_2: "Пожалуйста, обновите CoreProtect до поддерживаемой версии."
|
||||
PATCH_PROCESSING: "Обработка новых данных. Пожалуйста, подождите..."
|
||||
PATCH_SKIP_UPDATE: "{Пропущена таблица|Пропущен индекс} {обновление|создание|удаление} в {0}."
|
||||
PATCH_SKIP_UPDATE: "{Таблица|Индекс}: пропущено {обновление|создание|удаление} в {0}."
|
||||
PATCH_STARTED: "Выполняется обновление {0}. Пожалуйста, подождите..."
|
||||
PATCH_SUCCESS: "Успешно обновлен до {0}."
|
||||
PATCH_SUCCESS: "Успешно обновлено до {0}."
|
||||
PATCH_TABLE_COMPLETED: "Таблица {0} успешно обновлена."
|
||||
PATCH_TABLE_STARTED: "Обновление таблицы {0}. Пожалуйста, подождите..."
|
||||
PATCH_UPGRADING: "Выполняется обновление базы данных. Пожалуйста, подождите..."
|
||||
PLEASE_SELECT: "Пожалуйста выберите: «{0}» или «{1}»."
|
||||
PREVIEW_CANCELLED: "Предварительный просмотр отменен."
|
||||
PREVIEW_CANCELLING: "Отмена предварительного просмотра..."
|
||||
PREVIEW_IN_GAME: "Предварительный просмотр откатов доступен только в игре."
|
||||
PREVIEW_TRANSACTION: "Вы не можете просмотреть {контейнер|инвентарь} транзакции."
|
||||
PLEASE_SELECT: "Пожалуйста, выберите: «{0}» или «{1}»."
|
||||
PREVIEW_CANCELLED: "Предпросмотр отменён."
|
||||
PREVIEW_CANCELLING: "Отмена предпросмотра..."
|
||||
PREVIEW_IN_GAME: "Предпросмотр откатов доступен только в игре."
|
||||
PREVIEW_TRANSACTION: "{Контейнер|Инвентарь}: предпросмотр транзакции недоступен."
|
||||
PRIMARY_THREAD_ERROR: "Этот метод API нельзя использовать в основном потоке."
|
||||
PURGE_ABORTED: "Очистка не удалась. База данных может быть повреждена."
|
||||
PURGE_ERROR: "Невозможно обработать данные {0}!"
|
||||
PURGE_ERROR: "Не удалось обработать данные {0}!"
|
||||
PURGE_FAILED: "Очистка не удалась. Пожалуйста, повторите попытку позже."
|
||||
PURGE_IN_PROGRESS: "Выполняется чистка. Пожалуйста, повторите попытку позже."
|
||||
PURGE_MINIMUM_TIME: "Удалить можно только данные старше {0} {дней|часов}."
|
||||
PURGE_NOTICE_1: "Учтите, что это может занять некоторое время."
|
||||
PURGE_NOTICE_2: "Не перезагружайте сервер до завершения."
|
||||
PURGE_IN_PROGRESS: "Выполняется очистка. Пожалуйста, повторите попытку позже."
|
||||
PURGE_MINIMUM_TIME: "Вы можете очистить только данные старше {0} {дней|часов}."
|
||||
PURGE_NOTICE_1: "Пожалуйста, учтите, что это может занять некоторое время."
|
||||
PURGE_NOTICE_2: "Не перезапускайте сервер до завершения процесса."
|
||||
PURGE_OPTIMIZING: "Оптимизация базы данных. Пожалуйста, подождите..."
|
||||
PURGE_PROCESSING: "Обработка данных {0}..."
|
||||
PURGE_REPAIRING: "Попытка исправления. Это может занять некоторое время..."
|
||||
PURGE_ROWS: "{0} {строка|строки} данных очищены."
|
||||
PURGE_STARTED: "Очистка данных началась «{0}»."
|
||||
PURGE_SUCCESS: "Очистка данных прошла успешно."
|
||||
RELOAD_STARTED: "Перезагрузка конфигурации - подождите."
|
||||
PURGE_ROWS: "{0} {строка|строк} данных удалено."
|
||||
PURGE_STARTED: "Начата очистка данных для «{0}»."
|
||||
PURGE_SUCCESS: "Очистка данных успешно завершена."
|
||||
RAM_STATS: "{0} ГБ / {1} ГБ ОЗУ"
|
||||
RELOAD_STARTED: "Перезагрузка конфигурации - пожалуйста, подождите."
|
||||
RELOAD_SUCCESS: "Конфигурация успешно перезагружена."
|
||||
ROLLBACK_ABORTED: "Откат или восстановление прерваны."
|
||||
ROLLBACK_CHUNKS_FOUND: "Найдено {0} {чанк|чанков} для изменения."
|
||||
ROLLBACK_CHUNKS_MODIFIED: "Изменено {0}/{1} {чанк|чанков}."
|
||||
ROLLBACK_COMPLETED: "{Откат|Восстановление|Предварительный просмотр} завершен для «{0}»."
|
||||
ROLLBACK_EXCLUDED_USERS: "{Исключен пользователь|Исключены пользователи}: «{0}»."
|
||||
ROLLBACK_INCLUDE: "{Включён|Исключён} {блок|сущность|цель} {типа|типов}: «{0}»."
|
||||
ROLLBACK_IN_PROGRESS: "Откат/восстановление уже выполняется."
|
||||
ROLLBACK_LENGTH: "Заняло времени: {0} {секунду|секунд}."
|
||||
ROLLBACK_MODIFIED: "{Изменено|Изменены} {0}."
|
||||
ROLLBACK_COMPLETED: "{Откат|Восстановление|Предпросмотр} завершён для «{0}»."
|
||||
ROLLBACK_EXCLUDED_USERS: "{Исключённый пользователь|Исключённые пользователи}: «{0}»."
|
||||
ROLLBACK_INCLUDE: "{Включено|Исключено} ({блок|сущность|цель}) {типа|типов}: «{0}»."
|
||||
ROLLBACK_IN_PROGRESS: "Откат или восстановление уже выполняется."
|
||||
ROLLBACK_LENGTH: "Затрачено времени: {0} {секунда|секунд}."
|
||||
ROLLBACK_MODIFIED: "{Изменено|Изменяется} {0}."
|
||||
ROLLBACK_RADIUS: "Радиус: {0} {блок|блоков}."
|
||||
ROLLBACK_SELECTION: "Радиус установлен «{0}»."
|
||||
ROLLBACK_STARTED: "{Откат|Восстановление|Предварительный просмотр} начат «{0}»."
|
||||
ROLLBACK_SELECTION: "Радиус установлен на «{0}»."
|
||||
ROLLBACK_STARTED: "{Откат|Восстановление|Предпросмотр} запущен для «{0}»."
|
||||
ROLLBACK_TIME: "Временной интервал: {0}."
|
||||
ROLLBACK_WORLD_ACTION: "Только {мир|действие} «{0}»."
|
||||
SIGN_HEADER: "Сообщения на табличке"
|
||||
STATUS_CONSUMER: "Потребитель: {0} {предмет|предметы} в очереди."
|
||||
STATUS_DATABASE: "База данных: Использует {0}."
|
||||
STATUS_INTEGRATION: "{0}: Интеграция {включена|отключена}."
|
||||
ROLLBACK_WORLD_ACTION: "Ограничено {миром|действием} «{0}»."
|
||||
SIGN_HEADER: "Сообщения на табличках"
|
||||
STATUS_AUTO_PURGE: "Очистка: автоудалено {0} {строка|строк} с момента перезапуска."
|
||||
STATUS_CONSUMER: "Очередь потребителя: {0} {предмет|предметов}."
|
||||
STATUS_DATABASE: "База данных: используется {0}."
|
||||
STATUS_INTEGRATION: "{0}: интеграция {включена|отключена}."
|
||||
STATUS_LICENSE: "Лицензия: {0}"
|
||||
STATUS_SYSTEM: "Система: {0}"
|
||||
STATUS_VERSION: "Версия: {0}"
|
||||
TELEPORTED: "Телепортирование в {0}."
|
||||
TELEPORTED: "Телепортация в {0}."
|
||||
TELEPORTED_SAFETY: "Телепортируем вас в безопасное место."
|
||||
TELEPORT_PLAYERS: "Команду телепортации могут использовать только игроки."
|
||||
TIME_DAYS: "{0} {дня|дней}"
|
||||
TIME_DAYS: "{0} {день|дней}"
|
||||
TIME_HOURS: "{0} {час|часов}"
|
||||
TIME_MINUTES: "{0} {минута|минут}"
|
||||
TIME_MONTHS: "{0} {месяц|месяцев}"
|
||||
TIME_SECONDS: "{0} {секунда|секунд}"
|
||||
TIME_UNITS: "{/m|/h|/d}"
|
||||
TIME_WEEKS: "{0} {неделя|недель}"
|
||||
TIME_YEARS: "{0} {год|лет}"
|
||||
UPDATE_ERROR: "Произошла ошибка при проверке обновлений."
|
||||
UPDATE_HEADER: "{0} Обновление"
|
||||
UPDATE_NOTICE: "Примечание: теперь доступен {0}."
|
||||
UPDATE_NOTICE: "Примечание: теперь доступна версия {0}."
|
||||
UPGRADE_IN_PROGRESS: "Выполняется обновление. Пожалуйста, повторите попытку позже."
|
||||
USER_NOT_FOUND: "Пользователь «{0}» не найден."
|
||||
USER_OFFLINE: "Пользователь \"{0}\" не в сети."
|
||||
USER_OFFLINE: "Пользователь «{0}» не в сети."
|
||||
USING_MYSQL: "Для хранения данных используется MySQL."
|
||||
USING_SQLITE: "Для хранения данных используется SQLite."
|
||||
VALID_DONATION_KEY: "Действующий ключ пожертвования."
|
||||
VERSION_INCOMPATIBLE: "{0} {1} не поддерживается."
|
||||
VERSION_NOTICE: "Версия {0} уже доступна!"
|
||||
VERSION_REQUIRED: "{0} {1} или выше требуется."
|
||||
VERSION_REQUIRED: "Требуется {0} {1} или выше."
|
||||
WORLD_NOT_FOUND: "Мир «{0}» не найден."
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# CoreProtect v24.0 Language Cache (tt)
|
||||
|
||||
ACTION_NOT_SUPPORTED: "Бу гамәл хупланмый."
|
||||
AMOUNT_BLOCK: "{0} {блок|блоклар}"
|
||||
AMOUNT_CHUNK: "{0} {кисәк|кисәкчәләр}"
|
||||
AMOUNT_ENTITY: "{0} {зат|затлар}"
|
||||
AMOUNT_ITEM: "{0} {әйбер|әйберләр}"
|
||||
API_TEST: "API тесты уңышлы үтте."
|
||||
CACHE_ERROR: "КИСӘТҮ: Валидацияләү вакытында хата килеп чыкты {0} кэш."
|
||||
CACHE_RELOAD: "Мәҗбүри яңадан йөкләү {картага төшерү|дөнья} мәгълүмат базасыннан алынган кэшлар."
|
||||
CHECK_CONFIG: "Зинһар, config.yml файлын тикшерегез"
|
||||
COMMAND_CONSOLE: "Зинһар, команданы консольдән башкарыгыз."
|
||||
COMMAND_NOT_FOUND: "Команда \"{0}\" табылмады.\""
|
||||
COMMAND_THROTTLED: "Зинһар, бераз көтегез һәм кабатлап карагыз."
|
||||
CONSUMER_ERROR: "Кулланучылар чиратлары эшкәртелә инде {туктатылган|дәвам иттерелгән}."
|
||||
CONSUMER_TOGGLED: "Кулланучылар чиратларын эшкәртү {туктатылган|дәвам иттерелгән}."
|
||||
CONTAINER_HEADER: "Контейнер транзакцияләре"
|
||||
CPU_CORES: "Үзәк процессор үзәкләре."
|
||||
DATABASE_BUSY: "Мәгълүмат базасы мәшгуль. Зинһар, соңрак кабатлап карагыз."
|
||||
DATABASE_INDEX_ERROR: "Мәгълүмат базасы индексларын тикшереп булмый."
|
||||
DATABASE_LOCKED_1: "Мәгълүмат базасы бикләнгән. 15 секундка кадәр көтәм..."
|
||||
DATABASE_LOCKED_2: "Мәгълүмат базасы инде кулланыла. Зинһар, кабатлап карагыз."
|
||||
DATABASE_LOCKED_3: "Мәгълүмат базасын блоклауны сүндерү өчен, \"database-lock: false\" куегыз."
|
||||
DATABASE_LOCKED_4: "Мәгълүмат базасын блоклауны сүндерү мәгълүматларның бозылуына китерергә мөмкин."
|
||||
DATABASE_UNREACHABLE: "Мәгълүматлар базасына кереп булмый. Мәгълүматларны юкка чыгаралар һәм сүндерәләр."
|
||||
DEVELOPMENT_BRANCH: "Үсеш тармагы ачыкланды, патч скриптларын үткәреп җибәрә."
|
||||
DIRT_BLOCK: "Сезнең астыгызга вакытлыча куркынычсызлык блогы куйдылар."
|
||||
DISABLE_SUCCESS: "Уңышлы! Инвалид {0}"
|
||||
ENABLE_FAILED: "{0} башлый алмады."
|
||||
ENABLE_SUCCESS: "{0} хәзер активлаштырылган!"
|
||||
ENJOY_COREPROTECT: "Рәхәтләнегез {0}Безнең белән кушылыгыз Discord!"
|
||||
FINISHING_CONVERSION: "Мәгълүматларны үзгәртү тәмамлана. Зинһар, көтегез..."
|
||||
FINISHING_LOGGING: "Мәгълүматларны теркәү тәмамлана. Зинһар, көтегез..."
|
||||
FIRST_VERSION: "Башлангыч мәгълүмат базасы: {0}"
|
||||
GLOBAL_LOOKUP: "Глобаль эзләү өчен радиус күрсәтмәгез."
|
||||
GLOBAL_ROLLBACK: "Куллану \"{0}\" глобаль эш башкарырга {кире кайтару|торгызу}"
|
||||
HELP_ACTION_1: "Эзләүне гамәл белән чикләгез."
|
||||
HELP_ACTION_2: "Мисаллар: [a:block], [a:+block], [a:-block] [a:click], [a:container], [a:inventory], [a:item], [a:kill], [a:chat], [a:command], [a:sign], [a:session], [a:username]"
|
||||
HELP_COMMAND: "Өстәмә мәгълүматны күрсәтегез."
|
||||
HELP_EXCLUDE_1: "Блокларны/кулланучыларны чыгарыгыз."
|
||||
HELP_EXCLUDE_2: "Мисаллар: [e:stone], [e:Notch], [e:stone,Notch]"
|
||||
HELP_HEADER: "{0} Ярдәм"
|
||||
HELP_INCLUDE_1: "Блокларны/объектларны кертегез."
|
||||
HELP_INCLUDE_2: "Мисаллар: [i:stone], [i:zombie], [i:stone,wood,bedrock]"
|
||||
HELP_INSPECT_1: "Инспекторны кабызганда, сез:"
|
||||
HELP_INSPECT_2: "Урынны карау өчен сул төймә белән басыгыз."
|
||||
HELP_INSPECT_3: "Күршедәге бетерүләрне карау өчен уң төймә белән басыгыз."
|
||||
HELP_INSPECT_4: "Алып ташлауны күрү өчен блок урнаштырыгыз."
|
||||
HELP_INSPECT_5: "Урынын күрү өчен, сыеклыкка блок салыгыз."
|
||||
HELP_INSPECT_6: "Күкрәк транзакцияләрен карау өчен уң төймә белән басыгыз."
|
||||
HELP_INSPECT_7: "Киңәш: Сез бары тик \"/co i\" куллана аласыз."
|
||||
HELP_INSPECT_COMMAND: "Блок инспекторын кабыза/яба."
|
||||
HELP_LIST: "Барлык командалар исемлеген күрсәтә."
|
||||
HELP_LOOKUP_1: "Команда кыска юлы"
|
||||
HELP_LOOKUP_2: "Өстәмә битләрне карау."
|
||||
HELP_LOOKUP_COMMAND: "Блок мәгълүматларын эзләү."
|
||||
HELP_NO_INFO: "Команда өчен мәгълүмат \"{0}\" табылмады.\""
|
||||
HELP_PARAMETER: "Зинһар, карагыз \"{0}\" параметрлар турында тулырак мәгълүмат алу өчен."
|
||||
HELP_PARAMS_1: "Башкарыгыз {эзләү|кайтару|торгызу}."
|
||||
HELP_PARAMS_2: "Кулланучыларны күрсәтегез {эзләү|кайтару|торгызу}."
|
||||
HELP_PARAMS_3: "Вакытны күрсәтегез {эзләү|кайтару|торгызу}."
|
||||
HELP_PARAMS_4: "Чикләү {эзләү|кайтару|торгызу} радиуска кадәр."
|
||||
HELP_PARAMS_5: "Чикләү {эзләү|кайтару|торгызу} гамәленә."
|
||||
HELP_PARAMS_6: "Блокларны/объектларны кертегез {эзләү|кайтару|торгызу}."
|
||||
HELP_PARAMS_7: "Блокларны/кулланучыларны чыгарыгыз {эзләү|кайтару|торгызу}."
|
||||
HELP_PURGE_1: "Иске мәгълүматларны бетерегез."
|
||||
HELP_PURGE_2: "Мәсәлән, \"{0}\" бер айдан иске барлык мәгълүматларны бетерәчәк һәм соңгы 30 көнлек мәгълүматларны гына саклаячак.\""
|
||||
HELP_PURGE_COMMAND: "Иске блок мәгълүматларын бетерегез."
|
||||
HELP_RADIUS_1: "Радиус мәйданын күрсәтегез."
|
||||
HELP_RADIUS_2: "Мисаллар: [r:10] (Үзгәрешләрне сездән 10 квартал эчендә генә кертегез)"
|
||||
HELP_RELOAD_COMMAND: "Конфигурация файлын яңадан йөкли."
|
||||
HELP_RESTORE_COMMAND: "Блок мәгълүматларын торгызу."
|
||||
HELP_ROLLBACK_COMMAND: "Кире кайтару блогы мәгълүматлары."
|
||||
HELP_STATUS: "Плагинның статусын карагыз."
|
||||
HELP_STATUS_COMMAND: "Плагин статусын күрсәтә."
|
||||
HELP_TELEPORT: "Телепорт."
|
||||
HELP_TIME_1: "Вакытны күрсәтегез."
|
||||
HELP_TIME_2: "Мисаллар: [t:2w,5d,7h,2m,10s], [t:5d2h], [t:2.50h]"
|
||||
HELP_USER_1: "Кулланучыларны күрсәтегез."
|
||||
HELP_USER_2: "Мисаллар: [u:Notch], [u:Notch,#enderman]"
|
||||
INCOMPATIBLE_ACTION: "\"{0}\" бу гамәл белән кулланыла алмый."
|
||||
INSPECTOR_ERROR: "Инспектор инде {активлаштырылган|сүндерелгән}."
|
||||
INSPECTOR_TOGGLED: "Инспектор хәзер {активлаштырылган|сүндерелгән}."
|
||||
INTEGRATION_ERROR: "Мөмкин түгел {башлангыч хәлгә китерү|сүндерү} {0} агач эшкәртү"
|
||||
INTEGRATION_SUCCESS: "{0} уңышлы теркәлү {башлангыч хәлгә китерелгән|сүндерелгән}."
|
||||
INTEGRATION_VERSION: "Яраксыз {0} версиясе табылды."
|
||||
INTERACTIONS_HEADER: "Уенчыларның үзара бәйләнеше"
|
||||
INVALID_ACTION: "Бу дөрес гамәл түгел."
|
||||
INVALID_BRANCH_1: "Плагин версиясе дөрес түгел (тармак куелмаган)."
|
||||
INVALID_BRANCH_2: "Дәвам итү өчен, проект тармагын \"эшләү\"гә куегыз."
|
||||
INVALID_BRANCH_3: "Эшләү кодын эшләтеп җибәрү мәгълүматларның бозылуына китерергә мөмкин."
|
||||
INVALID_CONTAINER: "Зинһар, башта яраклы контейнерны тикшерегез."
|
||||
INVALID_DONATION_KEY: "Иганә ачкычы дөрес түгел."
|
||||
INVALID_INCLUDE: "\"{0}\" - яраксыз кыйммәт."
|
||||
INVALID_INCLUDE_COMBO: "Бу яраксыз тип комбинациясе."
|
||||
INVALID_RADIUS: "Зинһар, дөрес радиус кертегез."
|
||||
INVALID_SELECTION: "{0} сайлау табылмады."
|
||||
INVALID_USERNAME: "\"{0}\" - яраксыз кулланучы исеме."
|
||||
INVALID_WORLD: "Зинһар, дөрес дөнья күрсәтегез."
|
||||
LATEST_VERSION: "Соңгы версия: {0}"
|
||||
LINK_DISCORD: "Discord: {0}"
|
||||
LINK_DOWNLOAD: "Йөкләү: {0}"
|
||||
LINK_PATREON: "Patreon: {0}"
|
||||
LINK_WIKI_BLOCK: "Блок исемнәре: {0}"
|
||||
LINK_WIKI_ENTITY: "Объект исемнәре: {0}"
|
||||
LOGGING_ITEMS: "{0} Элемтәгә кертергә калган әйберләр. Зинһар, көтегез..."
|
||||
LOGGING_TIME_LIMIT: "Журналлаштыру вакыты чикләнде. Мәгълүматлар юкка чыгарыла һәм сүндерелә."
|
||||
LOOKUP_BLOCK: "{0} {урнаштырылган|ватык} {1}."
|
||||
LOOKUP_CONTAINER: "{0} {өстәлде|алып ташланды} {1} {2}."
|
||||
LOOKUP_HEADER: "{0} Нәтиҗәләр"
|
||||
LOOKUP_INTERACTION: "{0} {басылды|үтерелде} {1}."
|
||||
LOOKUP_ITEM: "{0} {алды|ташлады} {1} {2}."
|
||||
LOOKUP_LOGIN: "{0} теркәлгән {керү|чыгу}."
|
||||
LOOKUP_PAGE: "Бит {0}"
|
||||
LOOKUP_PROJECTILE: "{0} {ыргытты|атты} {1} {2}."
|
||||
LOOKUP_ROWS_FOUND: "{0} {рәт|рәтләр} табылган."
|
||||
LOOKUP_SEARCHING: "Эзләү эзләнә. Зинһар, көтегез..."
|
||||
LOOKUP_STORAGE: "{0} {салынган|алынган} {1} {2}."
|
||||
LOOKUP_TIME: "{0} элек"
|
||||
LOOKUP_USERNAME: "{0} буларак керде {1}."
|
||||
MAXIMUM_RADIUS: "Максималь {эзләү|кайтару|торгызу} радиус {0}."
|
||||
MISSING_ACTION_USER: "Бу гамәлне куллану өчен, зинһар, кулланучыны күрсәтегез."
|
||||
MISSING_LOOKUP_TIME: "Зинһар, вакытны күрсәтегез {эзләү|кайтару|торгызу}."
|
||||
MISSING_LOOKUP_USER: "Зинһар, кулланучыны күрсәтегез яки {блок|радиус}."
|
||||
MISSING_PARAMETERS: "Зинһар, \" кулланыгыз{0}\"."
|
||||
MISSING_ROLLBACK_RADIUS: "Сез күрсәтмәдегез {кире кайтару|торгызу} радиус."
|
||||
MISSING_ROLLBACK_USER: "Сез күрсәтмәдегез {кире кайтару|торгызу} кулланучы."
|
||||
MYSQL_UNAVAILABLE: "Тоташып булмый MySQL сервер."
|
||||
NETWORK_CONNECTION: "Тоташу {0} {уңышлы|уңышсыз}Куллану {1} {2}."
|
||||
NETWORK_TEST: "Челтәр сынау мәгълүматлары уңышлы җибәрелде."
|
||||
NO_DATA: "Бернинди мәгълүмат табылмады {0}."
|
||||
NO_DATA_LOCATION: "Юк {мәгълүматлар|транзакцияләр|үзара бәйләнешләр|хәбәрләр} табылган."
|
||||
NO_PERMISSION: "Сезнең моны эшләргә рөхсәтегез юк."
|
||||
NO_RESULTS: "Нәтиҗәләр табылмады."
|
||||
NO_RESULTS_PAGE: "Юк {нәтиҗәләр|мәгълүматлар} шул бит өчен табылды."
|
||||
NO_ROLLBACK: "Юк {көтелгән|алдагы} кире кайтару/торгызу табылды."
|
||||
PATCH_INTERRUPTED: "Яңарту өзелде. Кабат эшләтеп җибәргәндә тагын бер кат тырышып карыйм."
|
||||
PATCH_OUTDATED_1: "Искерәк мәгълүмат базаларын яңартып булмый {0}."
|
||||
PATCH_OUTDATED_2: "Зинһар, ярдәм ителә торган версия белән яңартыгыз CoreProtect."
|
||||
PATCH_PROCESSING: "Яңа мәгълүматлар эшкәртелә. Зинһар, көтегез..."
|
||||
PATCH_SKIP_UPDATE: "Сикереп бару {таблица|индекс} {яңарту|булдыру|бетерү} өстендә {0}."
|
||||
PATCH_STARTED: "Башкару {0} яңарту. Зинһар, көтегез..."
|
||||
PATCH_SUCCESS: "Уңышлы рәвештә яңартылды {0}."
|
||||
PATCH_UPGRADING: "Мәгълүмат базасы яңартыла. Зинһар, көтегез..."
|
||||
PLEASE_SELECT: "Зинһар, сайлагыз: \"{0}\" яки \"{1}\"."
|
||||
PREVIEW_CANCELLED: "Алдан карау юкка чыгарылды."
|
||||
PREVIEW_CANCELLING: "Алдан карауны юкка чыгару..."
|
||||
PREVIEW_IN_GAME: "Сез кире кайтаруларны уен эчендә генә карый аласыз."
|
||||
PREVIEW_TRANSACTION: "Алдан карап булмый {контейнер|инвентаризация} транзакцияләр."
|
||||
PRIMARY_THREAD_ERROR: "Бу API ысулын төп җептә кулланып булмый."
|
||||
PURGE_ABORTED: "Чистарту уңышсыз тәмамланды. Мәгълүмат базасы бозылган булырга мөмкин."
|
||||
PURGE_ERROR: "Эшкәртеп булмый {0} мәгълүматлар!"
|
||||
PURGE_FAILED: "Чистарту уңышсыз тәмамланды. Зинһар, соңрак кабатлап карагыз."
|
||||
PURGE_IN_PROGRESS: "Чистарту бара. Зинһар, соңрак кабатлап карагыз."
|
||||
PURGE_MINIMUM_TIME: "Сез бары тик искерәк мәгълүматларны гына бетерә аласыз {0} {көннәр|сәгатьләр}."
|
||||
PURGE_NOTICE_1: "Зинһар, игътибар итегез, бу бераз вакыт алырга мөмкин."
|
||||
PURGE_NOTICE_2: "Серверны тәмамланганчы яңадан эшләтеп җибәрмәгез."
|
||||
PURGE_OPTIMIZING: "Мәгълүмат базасы оптимальләштерелә. Зинһар, көтегез..."
|
||||
PURGE_PROCESSING: "Эшкәртү {0} мәгълүматлар..."
|
||||
PURGE_REPAIRING: "Төзәтергә тырышам. Бу бераз вакыт алырга мөмкин..."
|
||||
PURGE_ROWS: "{0} {рәт|рәтләр} бетерелгән мәгълүматлар саны."
|
||||
PURGE_STARTED: "Мәгълүматларны чистарту башланды \"{0}\"."
|
||||
PURGE_SUCCESS: "Мәгълүматларны чистарту уңышлы тәмамланды."
|
||||
RAM_STATS: "{0}ГБ / {1}ГБ оператив хәтер"
|
||||
RELOAD_STARTED: "Конфигурация яңадан йөкләнә - зинһар, көтегез."
|
||||
RELOAD_SUCCESS: "Конфигурация уңышлы яңадан йөкләнде."
|
||||
ROLLBACK_ABORTED: "Кире кайтару яки торгызу туктатылды."
|
||||
ROLLBACK_CHUNKS_FOUND: "Табылды {0} {кисәк|кисәк} үзгәртү өчен."
|
||||
ROLLBACK_CHUNKS_MODIFIED: "Үзгәртелгән {0}/{1} {кисәк|кисәкчәләр}."
|
||||
ROLLBACK_COMPLETED: "{Кире кайтару|Төзетү|Алдан карау} өчен тәмамланды \"{0}\"."
|
||||
ROLLBACK_EXCLUDED_USERS: "Чыгарылган {кулланучы|кулланучылар}: \"{0}\"."
|
||||
ROLLBACK_INCLUDE: "{Кертелгән|Чыгарылган} {блок|объект|максат} {төр|төрләр}: \"{0}\"."
|
||||
ROLLBACK_IN_PROGRESS: "Кире кайтару/торгызу процессы инде бара."
|
||||
ROLLBACK_LENGTH: "Кирәкле вакыт: {0} {секунд|секунд}."
|
||||
ROLLBACK_MODIFIED: "{Үзгәртелгән|Үзгәртелә} {0}."
|
||||
ROLLBACK_RADIUS: "Радиус: {0} {блок|блоклар}."
|
||||
ROLLBACK_SELECTION: "Радиус \"га куелды{0}\"."
|
||||
ROLLBACK_STARTED: "{Кире кайтару|Төзетү|Алдан карау} башланды \"{0}\"."
|
||||
ROLLBACK_TIME: "Вакыт диапазоны: {0}."
|
||||
ROLLBACK_WORLD_ACTION: "Чикләнгән {дөнья|гамәл} \"{0}\"."
|
||||
SIGN_HEADER: "Хәбәрләргә кул кую"
|
||||
STATUS_CONSUMER: "Кулланучы: {0} {әйбер|әйберләр} чиратта"
|
||||
STATUS_DATABASE: "Мәгълүмат базасы: Куллану {0}."
|
||||
STATUS_INTEGRATION: "{0}: Интеграция {активлаштырылган|сүндерелгән}."
|
||||
STATUS_LICENSE: "Лицензия: {0}"
|
||||
STATUS_SYSTEM: "Система: {0}"
|
||||
STATUS_VERSION: "Версия: {0}"
|
||||
TELEPORTED: "Телепортацияләнгән {0}."
|
||||
TELEPORTED_SAFETY: "Сезне куркынычсыз урынга телепортладылар."
|
||||
TELEPORT_PLAYERS: "Телепорт командасын бары тик уенчылар гына куллана ала."
|
||||
TIME_DAYS: "{0} {көн|көннәр}"
|
||||
TIME_HOURS: "{0} {сәгать|сәгатьләр}"
|
||||
TIME_MINUTES: "{0} {минут|минут}"
|
||||
TIME_SECONDS: "{0} {секунд|секунд}"
|
||||
TIME_WEEKS: "{0} {атна|атналар}"
|
||||
UPDATE_ERROR: "Яңартуларны тикшергәндә хата килеп чыкты."
|
||||
UPDATE_HEADER: "{0} Яңарту"
|
||||
UPDATE_NOTICE: "Игътибар: {0} хәзер мөмкин."
|
||||
UPGRADE_IN_PROGRESS: "Яңарту бара. Зинһар, соңрак кабатлап карагыз."
|
||||
USER_NOT_FOUND: "Кулланучы \"{0}\" табылмады.\""
|
||||
USER_OFFLINE: "Кулланучы \"{0}\" онлайн түгел.\""
|
||||
USING_MYSQL: "Куллану MySQL мәгълүматларны саклау өчен."
|
||||
USING_SQLITE: "Куллану SQLite мәгълүматларны саклау өчен."
|
||||
VALID_DONATION_KEY: "Гамәлдәге иганә ачкычы."
|
||||
VERSION_NOTICE: "Версия {0} хәзер мөмкин."
|
||||
VERSION_REQUIRED: "{0} {1} яки аннан да югарырак таләп ителә."
|
||||
WORLD_NOT_FOUND: "Дөнья \"{0}\" табылмады.\""
|
||||
@@ -2,13 +2,14 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>net.coreprotect</groupId>
|
||||
<artifactId>CoreProtect</artifactId>
|
||||
<version>23.1</version>
|
||||
<version>24.1</version>
|
||||
<properties>
|
||||
<project.branch></project.branch>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>true</skipTests>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<maven.compiler.release>11</maven.compiler.release>
|
||||
<clickhouse.version>0.10.0</clickhouse.version>
|
||||
<duckdb.version>1.4.5.0</duckdb.version>
|
||||
</properties>
|
||||
<build>
|
||||
<resources>
|
||||
@@ -26,10 +27,9 @@
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<version>3.15.0</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
<release>${maven.compiler.release}</release>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -47,10 +47,11 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.5.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Multi-Release>true</Multi-Release>
|
||||
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
@@ -59,7 +60,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<version>3.6.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -73,6 +74,8 @@
|
||||
<excludes>
|
||||
<exclude>**/*.java</exclude>
|
||||
<exclude>**/*.c</exclude>
|
||||
<exclude>module-info.class</exclude>
|
||||
<exclude>META-INF/versions/**/module-info.class</exclude>
|
||||
<exclude>META-INF/maven/**/pom.properties</exclude>
|
||||
<exclude>META-INF/services/**</exclude>
|
||||
</excludes>
|
||||
@@ -105,7 +108,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<version>3.5.5</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<plugin.version>${project.version}</plugin.version>
|
||||
@@ -116,7 +119,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.6.1</version>
|
||||
<version>3.6.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>validate</phase>
|
||||
@@ -152,7 +155,7 @@
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<version>3.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install-validation</id>
|
||||
@@ -209,32 +212,32 @@
|
||||
<dependencies>
|
||||
<!--
|
||||
paper-api: io.papermc.paper
|
||||
paper-api (<= 1.16): com.destroystokyo.paper
|
||||
paper-api (<= 1.16): com.destroystokyo.paper / 1.16.1-R0.1-SNAPSHOT
|
||||
spigot-api: org.spigotmc
|
||||
bukkit: org.bukkit
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.21.11-R0.1-SNAPSHOT</version>
|
||||
<version>26.3.build.38-alpha</version> <!-- https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/ -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bstats</groupId>
|
||||
<artifactId>bstats-bukkit</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>3.2.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fastasyncworldedit</groupId>
|
||||
<artifactId>FastAsyncWorldEdit-Core</artifactId>
|
||||
<version>2.13.1</version>
|
||||
<version>2.15.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fastasyncworldedit</groupId>
|
||||
<artifactId>FastAsyncWorldEdit-Bukkit</artifactId>
|
||||
<version>2.13.1</version>
|
||||
<version>2.15.0</version>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -246,22 +249,35 @@
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>5.0.1</version>
|
||||
<version>7.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.6.2</version>
|
||||
<groupId>org.duckdb</groupId>
|
||||
<artifactId>duckdb_jdbc</artifactId>
|
||||
<version>${duckdb.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
<version>${clickhouse.version}</version>
|
||||
<classifier>all</classifier>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.19.0</version>
|
||||
<version>2.25.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>2.19.0</version>
|
||||
<version>2.25.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.DeadSilenceIV</groupId>
|
||||
@@ -273,31 +289,31 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.10.1</version>
|
||||
<version>5.14.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.10.1</version>
|
||||
<version>5.14.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<version>5.10.1</version>
|
||||
<version>5.14.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.16.1</version>
|
||||
<version>5.23.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>5.16.1</version>
|
||||
<version>5.23.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -309,25 +325,31 @@
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.45.1.0</version>
|
||||
<version>3.53.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.12</version>
|
||||
<version>2.0.17</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>1.16.1</version>
|
||||
<version>1.18.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy-agent</artifactId>
|
||||
<version>1.16.1</version>
|
||||
<version>1.18.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.coreprotect;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
@@ -16,8 +17,6 @@ import net.coreprotect.utility.Chat;
|
||||
public final class CoreProtect extends JavaPlugin {
|
||||
|
||||
private static CoreProtect instance;
|
||||
private boolean advancedChestsEnabled = false;
|
||||
|
||||
/**
|
||||
* Get the instance of CoreProtect
|
||||
*
|
||||
@@ -44,7 +43,6 @@ public final class CoreProtect extends JavaPlugin {
|
||||
instance = this;
|
||||
ConfigHandler.path = this.getDataFolder().getPath() + File.separator;
|
||||
|
||||
advancedChestsEnabled = getServer().getPluginManager().getPlugin("AdvancedChests") != null;
|
||||
// Initialize plugin using the initialization service
|
||||
boolean initialized = PluginInitializationService.initializePlugin(this);
|
||||
|
||||
@@ -61,6 +59,7 @@ public final class CoreProtect extends JavaPlugin {
|
||||
}
|
||||
|
||||
public boolean isAdvancedChestsEnabled() {
|
||||
return advancedChestsEnabled;
|
||||
Plugin advancedChests = getServer().getPluginManager().getPlugin("AdvancedChests");
|
||||
return advancedChests != null && advancedChests.isEnabled();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
@@ -18,17 +19,37 @@ import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import net.coreprotect.api.BlockAPI;
|
||||
import net.coreprotect.api.EntityAPI;
|
||||
import net.coreprotect.api.InventoryAPI;
|
||||
import net.coreprotect.api.ItemAPI;
|
||||
import net.coreprotect.api.LookupOptions;
|
||||
import net.coreprotect.api.MessageAPI;
|
||||
import net.coreprotect.api.QueueLookup;
|
||||
import net.coreprotect.api.SessionLookup;
|
||||
import net.coreprotect.api.SignAPI;
|
||||
import net.coreprotect.api.UsernameAPI;
|
||||
import net.coreprotect.api.result.BlockResult;
|
||||
import net.coreprotect.api.result.ContainerResult;
|
||||
import net.coreprotect.api.result.EntityResult;
|
||||
import net.coreprotect.api.result.InventoryResult;
|
||||
import net.coreprotect.api.result.ItemResult;
|
||||
import net.coreprotect.api.result.MessageResult;
|
||||
import net.coreprotect.api.result.SignResult;
|
||||
import net.coreprotect.api.result.SessionResult;
|
||||
import net.coreprotect.api.result.UsernameResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.consumer.Queue;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.Lookup;
|
||||
import net.coreprotect.database.rollback.Rollback;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.listener.player.InventoryChangeListener;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* The main API class for CoreProtect.
|
||||
@@ -41,7 +62,8 @@ public class CoreProtectAPI extends Queue {
|
||||
/**
|
||||
* Current version of the API
|
||||
*/
|
||||
private static final int API_VERSION = 11;
|
||||
private static final int API_VERSION = 13;
|
||||
private static final AtomicLong API_ROLLBACK_SEQUENCE = new AtomicLong();
|
||||
|
||||
public static class ParseResult extends net.coreprotect.api.result.ParseResult {
|
||||
|
||||
@@ -68,7 +90,7 @@ public class CoreProtectAPI extends Queue {
|
||||
|
||||
if (list != null) {
|
||||
for (Object value : list) {
|
||||
if (value instanceof Material || value instanceof EntityType) {
|
||||
if (value instanceof Material || value instanceof EntityType || value instanceof String) {
|
||||
result.put(value, false);
|
||||
}
|
||||
else if (value instanceof Integer) {
|
||||
@@ -81,6 +103,10 @@ public class CoreProtectAPI extends Queue {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isSupportedLegacyAction(Integer action) {
|
||||
return action != null && (action == LookupActions.BLOCK_BREAK || action == LookupActions.BLOCK_PLACE || action == LookupActions.INTERACTION || action == LookupActions.ENTITY_KILL || action == LookupActions.ENTITY_SPAWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current API version.
|
||||
*
|
||||
@@ -106,6 +132,50 @@ public class CoreProtectAPI extends Queue {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed block lookup at the specified block.
|
||||
*
|
||||
* @param block
|
||||
* The block to look up
|
||||
* @param options
|
||||
* Lookup options. World, location, and radius are ignored because the block supplies the exact location.
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<BlockResult> blockLookup(Block block, LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return BlockAPI.performLookup(block, options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup of block breaks, placements, and interactions using shared lookup options. Entity events are excluded.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<BlockResult> blockLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return BlockAPI.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup of entity spawn and kill events.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results in an EntityResult format
|
||||
*/
|
||||
public List<EntityResult> entityLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return EntityAPI.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a lookup on the queue data for the specified block.
|
||||
*
|
||||
@@ -117,6 +187,64 @@ public class CoreProtectAPI extends Queue {
|
||||
return QueueLookup.performLookup(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a container lookup at the specified location.
|
||||
*
|
||||
* @param location
|
||||
* The location to look up
|
||||
* @param time
|
||||
* Time constraint in seconds
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<ContainerResult> containerLookup(Location location, int time) {
|
||||
if (isEnabled()) {
|
||||
return BlockAPI.performContainerLookup(location, time);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a container lookup with shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<ContainerResult> containerLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return BlockAPI.performContainerLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup on world item transactions.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<ItemResult> itemLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return ItemAPI.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup on player inventory transactions.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<InventoryResult> inventoryLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return InventoryAPI.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a lookup on session data for the specified user.
|
||||
*
|
||||
@@ -130,6 +258,140 @@ public class CoreProtectAPI extends Queue {
|
||||
return SessionLookup.performLookup(user, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup on session data.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<SessionResult> sessionLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return SessionLookup.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a username history lookup for the specified user.
|
||||
*
|
||||
* @param user
|
||||
* The user or UUID to look up, or #global/null for all users
|
||||
* @param time
|
||||
* Time constraint in seconds
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<UsernameResult> usernameLookup(String user, int time) {
|
||||
if (isEnabled()) {
|
||||
return UsernameAPI.performLookup(user, time);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a username history lookup with shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options. User, time, and limit are applied; location and radius are ignored.
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<UsernameResult> usernameLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return UsernameAPI.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a chat message lookup for the specified user.
|
||||
*
|
||||
* @param user
|
||||
* The user to look up, or #global/null for all users
|
||||
* @param time
|
||||
* Time constraint in seconds
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<MessageResult> chatLookup(String user, int time) {
|
||||
if (isEnabled()) {
|
||||
return MessageAPI.performChatLookup(user, time);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a chat message lookup with shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<MessageResult> chatLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return MessageAPI.performChatLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a command lookup for the specified user.
|
||||
*
|
||||
* @param user
|
||||
* The user to look up, or #global/null for all users
|
||||
* @param time
|
||||
* Time constraint in seconds
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<MessageResult> commandLookup(String user, int time) {
|
||||
if (isEnabled()) {
|
||||
return MessageAPI.performCommandLookup(user, time);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a command lookup with shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<MessageResult> commandLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return MessageAPI.performCommandLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a sign text lookup at the specified location.
|
||||
*
|
||||
* @param location
|
||||
* The location to look up
|
||||
* @param time
|
||||
* Time constraint in seconds
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<SignResult> signLookup(Location location, int time) {
|
||||
if (isEnabled()) {
|
||||
return SignAPI.performLookup(location, time);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a sign text lookup with shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results or null if API is disabled
|
||||
*/
|
||||
public List<SignResult> signLookup(LookupOptions options) {
|
||||
if (isEnabled()) {
|
||||
return SignAPI.performLookup(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a user has placed a block at the specified location.
|
||||
*
|
||||
@@ -272,7 +534,8 @@ public class CoreProtectAPI extends Queue {
|
||||
return false;
|
||||
}
|
||||
|
||||
Queue.queuePlayerInteraction(user, location.getBlock().getState(), location.getBlock().getType());
|
||||
Block block = location.getBlock();
|
||||
Queue.queuePlayerInteraction(user, block.getLocation(), block.getType(), block.getBlockData().getAsString());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -341,6 +604,27 @@ public class CoreProtectAPI extends Queue {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a block placement by a user with specific block data.
|
||||
*
|
||||
* @param user
|
||||
* The username
|
||||
* @param location
|
||||
* The location
|
||||
* @param blockData
|
||||
* The block data
|
||||
* @return True if the placement was logged
|
||||
*/
|
||||
public boolean logPlacement(String user, Location location, BlockData blockData) {
|
||||
if (!isEnabled() || !isValidUserAndLocation(user, location) || blockData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Block block = location.getBlock();
|
||||
Queue.queueBlockPlace(user, block.getState(), block.getType(), null, block.getType(), -1, 0, blockData.getAsString());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a block placement by a user with a specific material and data value.
|
||||
*
|
||||
@@ -412,6 +696,28 @@ public class CoreProtectAPI extends Queue {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a block removal by a user with specific block data.
|
||||
*
|
||||
* @param user
|
||||
* The username
|
||||
* @param location
|
||||
* The location
|
||||
* @param blockData
|
||||
* The block data
|
||||
* @return True if the removal was logged
|
||||
*/
|
||||
public boolean logRemoval(String user, Location location, BlockData blockData) {
|
||||
if (!isEnabled() || !isValidUserAndLocation(user, location) || blockData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Block block = location.getBlock();
|
||||
Database.containerBreakCheck(user, block.getType(), block, null, location);
|
||||
Queue.queueBlockBreak(user, block.getState(), block.getType(), blockData.getAsString(), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a block removal by a user with a specific material and data value.
|
||||
*
|
||||
@@ -725,16 +1031,15 @@ public class CoreProtectAPI extends Queue {
|
||||
List<String[]> result = new ArrayList<>();
|
||||
List<String> uuids = new ArrayList<>();
|
||||
|
||||
if (restrictUsers == null) {
|
||||
restrictUsers = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (excludeUsers == null) {
|
||||
excludeUsers = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (actionList == null) {
|
||||
actionList = new ArrayList<>();
|
||||
restrictUsers = restrictUsers == null ? new ArrayList<>() : new ArrayList<>(restrictUsers);
|
||||
excludeUsers = excludeUsers == null ? new ArrayList<>() : new ArrayList<>(excludeUsers);
|
||||
boolean explicitActions = actionList != null && !actionList.isEmpty();
|
||||
actionList = actionList == null ? new ArrayList<>() : new ArrayList<>(actionList);
|
||||
if (explicitActions) {
|
||||
actionList.removeIf(actionListItem -> !isSupportedLegacyAction(actionListItem));
|
||||
if (actionList.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
List<Object> restrictBlocks = new ArrayList<>(restrictBlocksMap.keySet());
|
||||
@@ -744,24 +1049,22 @@ public class CoreProtectAPI extends Queue {
|
||||
|
||||
for (Object argBlock : restrictBlocks) {
|
||||
if (argBlock instanceof Material && !addedMaterial) {
|
||||
actionList.add(0);
|
||||
actionList.add(1);
|
||||
actionList.add(LookupActions.BLOCK_BREAK);
|
||||
actionList.add(LookupActions.BLOCK_PLACE);
|
||||
addedMaterial = true;
|
||||
}
|
||||
else if (argBlock instanceof EntityType && !addedEntity) {
|
||||
actionList.add(3);
|
||||
actionList.add(LookupActions.ENTITY_KILL);
|
||||
addedEntity = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actionList.isEmpty()) {
|
||||
actionList.add(0);
|
||||
actionList.add(1);
|
||||
actionList.add(LookupActions.BLOCK_BREAK);
|
||||
actionList.add(LookupActions.BLOCK_PLACE);
|
||||
}
|
||||
|
||||
actionList.removeIf(actionListItem -> actionListItem > 3);
|
||||
|
||||
if (restrictUsers.isEmpty()) {
|
||||
restrictUsers.add("#global");
|
||||
}
|
||||
@@ -818,8 +1121,19 @@ public class CoreProtectAPI extends Queue {
|
||||
}
|
||||
else {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
boolean verbose = false;
|
||||
result = Rollback.performRollbackRestore(statement, null, uuids, restrictUsers, null, restrictBlocks, excludeBlocks, excludeUsers, actionList, location, argRadius, startTime, endTime, restrictWorld, false, verbose, action, 0);
|
||||
String rollbackKey = "#api-" + API_ROLLBACK_SEQUENCE.incrementAndGet();
|
||||
if (Consumer.claimRollback(rollbackKey) == Consumer.OperationStartResult.STARTED) {
|
||||
try {
|
||||
boolean verbose = false;
|
||||
result = Rollback.performRollbackRestore(statement, null, uuids, restrictUsers, null, restrictBlocks, excludeBlocks, excludeUsers, actionList, location, argRadius, startTime, endTime, restrictWorld, false, verbose, action, 0);
|
||||
}
|
||||
finally {
|
||||
Consumer.releaseRollback(rollbackKey);
|
||||
}
|
||||
}
|
||||
else { // only print exception on development branch
|
||||
ErrorReporter.report(new IllegalStateException("API rollback skipped: another database operation is active or persistence is halted"), ConfigHandler.EDITION_BRANCH.contains("-dev"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
Chat.console(Phrase.build(Phrase.PRIMARY_THREAD_ERROR));
|
||||
@@ -830,7 +1144,7 @@ public class CoreProtectAPI extends Queue {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
|
||||
import net.coreprotect.api.result.BlockResult;
|
||||
import net.coreprotect.api.result.ContainerResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.DuckDBLookupQuery;
|
||||
import net.coreprotect.database.LocationQuery;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.utility.BlockUtils;
|
||||
import net.coreprotect.utility.DatabaseUtils;
|
||||
import net.coreprotect.utility.StringUtils;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for block-related lookups in the CoreProtect database.
|
||||
@@ -66,7 +74,9 @@ public class BlockAPI {
|
||||
}
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
String query = "SELECT time,user,action,type,data,blockdata,rolled_back FROM " + ConfigHandler.prefix + "block " + WorldUtils.getWidIndex("block") + "WHERE wid = '" + worldId + "' AND x = '" + x + "' AND z = '" + z + "' AND y = '" + y + "' AND time > '" + checkTime + "' ORDER BY rowid DESC";
|
||||
String table = DuckDBLookupQuery.spatialTable(connection, "block", worldId, x, x, z, z, "spatial_rows");
|
||||
String index = ConfigHandler.databaseType.isDuckDB() ? "" : WorldUtils.getWidIndex("block");
|
||||
String query = "SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",action,type,data,blockdata,rolled_back FROM " + table + " " + index + "WHERE " + LocationQuery.predicate("wid", " = " + worldId) + " AND " + LocationQuery.predicate("x", " = " + x) + " AND " + LocationQuery.predicate("z", " = " + z) + " AND y = " + y + " AND time > " + checkTime + " ORDER BY " + ConfigHandler.getDescendingEventOrder();
|
||||
|
||||
try (ResultSet results = statement.executeQuery(query)) {
|
||||
while (results.next()) {
|
||||
@@ -75,14 +85,10 @@ public class BlockAPI {
|
||||
String resultAction = results.getString("action");
|
||||
int resultType = results.getInt("type");
|
||||
String resultData = results.getString("data");
|
||||
byte[] resultBlockData = results.getBytes("blockdata");
|
||||
byte[] resultBlockData = DatabaseUtils.getBytes(results, "blockdata");
|
||||
String resultRolledBack = results.getString("rolled_back");
|
||||
|
||||
if (ConfigHandler.playerIdCacheReversed.get(resultUserId) == null) {
|
||||
UserStatement.loadName(connection, resultUserId);
|
||||
}
|
||||
|
||||
String resultUser = ConfigHandler.playerIdCacheReversed.get(resultUserId);
|
||||
String resultUser = UserStatement.getName(connection, resultUserId);
|
||||
String blockData = BlockUtils.byteDataToString(resultBlockData, resultType);
|
||||
|
||||
String[] lookupData = new String[] { resultTime, resultUser, String.valueOf(x), String.valueOf(y), String.valueOf(z), String.valueOf(resultType), resultData, resultAction, resultRolledBack, String.valueOf(worldId), blockData };
|
||||
@@ -93,9 +99,211 @@ public class BlockAPI {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup of block-related actions at the specified block.
|
||||
*
|
||||
* @param block
|
||||
* The block to look up
|
||||
* @param options
|
||||
* Lookup options. World, location, and radius are ignored because the block supplies the exact location.
|
||||
* @return List of results in a BlockResult format
|
||||
*/
|
||||
public static List<BlockResult> performLookup(Block block, LookupOptions options) {
|
||||
if (!Config.getGlobal().API_ENABLED || block == null || block.getWorld() == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
return performLookup(LookupOptions.builder().location(block.getLocation())
|
||||
.user(options.getUser()).users(options.getUsers()).excludeUsers(options.getExcludeUsers())
|
||||
.time(options.getTime()).limit(options.getLimitOffset(), options.getLimitCount())
|
||||
.includeMaterials(options.getIncludeMaterials()).excludeMaterials(options.getExcludeMaterials())
|
||||
.blockActions(options.getBlockActions()).build(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup of block-related actions using shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of results in a BlockResult format
|
||||
*/
|
||||
public static List<BlockResult> performLookup(LookupOptions options) {
|
||||
return performLookup(options, true);
|
||||
}
|
||||
|
||||
private static List<BlockResult> performLookup(LookupOptions options, boolean blocksOnly) {
|
||||
List<BlockResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",action,type,data,blockdata,rolled_back,wid,x,y,z FROM ");
|
||||
query.append(filter.table(connection, "block", "")).append(' ');
|
||||
if (filter.hasLocation() && !ConfigHandler.databaseType.isDuckDB()) {
|
||||
query.append(WorldUtils.getWidIndex("block"));
|
||||
}
|
||||
filter.appendWhere(query);
|
||||
filter.appendBlockMaterialWhere(query);
|
||||
int[] actions = blocksOnly && options.getBlockActions().isEmpty()
|
||||
? new int[] { BlockAction.BREAK.id(), BlockAction.PLACE.id(), BlockAction.INTERACTION.id() }
|
||||
: options.getBlockActions().stream().mapToInt(BlockAction::id).toArray();
|
||||
LookupFilter.appendActionWhere(query, "", actions);
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
filter.bind(statement);
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseBlockResult(connection, results, WorldUtils.getWorldName(results.getInt("wid"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a lookup of container transactions at the specified location.
|
||||
*
|
||||
* @param location
|
||||
* The location to look up
|
||||
* @param offset
|
||||
* Time constraint in seconds (0 means no time constraint)
|
||||
* @return List of results in a ContainerResult format
|
||||
*/
|
||||
public static List<ContainerResult> performContainerLookup(Location location, int offset) {
|
||||
if (location == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return performContainerLookup(LookupOptions.builder().time(offset).location(location).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a lookup of container transactions using shared lookup options.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options. Tracked entity-container rows match their original or persisted current/final location and return the persisted
|
||||
* current/final location.
|
||||
* @return List of results in a ContainerResult format
|
||||
*/
|
||||
public static List<ContainerResult> performContainerLookup(LookupOptions options) {
|
||||
List<ContainerResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean snapshot = filter.beginDuckDBSnapshot(connection);
|
||||
try {
|
||||
StringBuilder containerWhere = new StringBuilder();
|
||||
filter.appendWhere(containerWhere, "container_rows");
|
||||
filter.appendMaterialWhere(containerWhere, "container_rows");
|
||||
int[] actions = options.getContainerActions().stream().mapToInt(ContainerAction::id).toArray();
|
||||
LookupFilter.appendActionWhere(containerWhere, "container_rows", actions);
|
||||
StringBuilder entityWhere = new StringBuilder();
|
||||
filter.appendEntityContainerWhere(entityWhere, "entity_rows", "spawn_rows");
|
||||
filter.appendMaterialWhere(entityWhere, "entity_rows");
|
||||
LookupFilter.appendActionWhere(entityWhere, "entity_rows", actions);
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT * FROM (");
|
||||
query.append("SELECT 0 AS source,container_rows.rowid AS id,container_rows.time,container_rows.").append(ConfigHandler.databaseType.getUserColumn()).append(",container_rows.wid,container_rows.x,container_rows.y,container_rows.z,container_rows.action,container_rows.type,container_rows.data,container_rows.amount,container_rows.metadata,container_rows.rolled_back FROM ")
|
||||
.append(filter.table(connection, "container", "container_rows")).append(' ').append(containerWhere);
|
||||
query.append(" UNION ALL ");
|
||||
query.append("SELECT 1 AS source,entity_rows.rowid AS id,entity_rows.time,entity_rows.").append(ConfigHandler.databaseType.getUserColumn()).append(",spawn_rows.current_wid AS wid,spawn_rows.x,spawn_rows.y,spawn_rows.z,entity_rows.action,entity_rows.type,entity_rows.data,entity_rows.amount,entity_rows.metadata,entity_rows.rolled_back FROM ")
|
||||
.append(filter.entityContainerTable(connection, "entity_rows")).append(" JOIN ").append(ConfigHandler.prefix).append("entity_spawn spawn_rows ON spawn_rows.rowid=entity_rows.entity_spawn_rowid ").append(entityWhere);
|
||||
query.append(") AS container_lookup ORDER BY time DESC,source DESC,id DESC");
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
int parameterIndex = filter.bind(statement);
|
||||
filter.bindEntityContainer(statement, parameterIndex);
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseContainerResult(connection, results));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
filter.endDuckDBSnapshot(connection, snapshot);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ContainerResult parseContainerResult(Connection connection, ResultSet results) throws Exception {
|
||||
int resultUserId = results.getInt("user");
|
||||
String resultUser = UserStatement.getName(connection, resultUserId);
|
||||
|
||||
return new ContainerResult(
|
||||
results.getLong("time"), resultUser, WorldUtils.getWorldName(results.getInt("wid")), (int) Math.floor(results.getDouble("x")), (int) Math.floor(results.getDouble("y")), (int) Math.floor(results.getDouble("z")),
|
||||
results.getInt("type"), results.getInt("data"), results.getInt("amount"), DatabaseUtils.getBytes(results, "metadata"),
|
||||
results.getInt("action"), results.getInt("rolled_back")
|
||||
);
|
||||
}
|
||||
|
||||
private static BlockResult parseBlockResult(Connection connection, ResultSet results, String worldName) throws Exception {
|
||||
int resultUserId = results.getInt("user");
|
||||
String resultUser = UserStatement.getName(connection, resultUserId);
|
||||
|
||||
int resultType = results.getInt("type");
|
||||
String blockData = BlockUtils.byteDataToString(DatabaseUtils.getBytes(results, "blockdata"), resultType);
|
||||
|
||||
return new BlockResult(
|
||||
results.getLong("time"), resultUser, worldName, results.getInt("x"), results.getInt("y"), results.getInt("z"),
|
||||
resultType, results.getInt("data"), blockData, results.getInt("action"), results.getInt("rolled_back")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
|
||||
/**
|
||||
* Block actions used by typed block lookup filters.
|
||||
*/
|
||||
public enum BlockAction {
|
||||
BREAK(LookupActions.BLOCK_BREAK),
|
||||
PLACE(LookupActions.BLOCK_PLACE),
|
||||
INTERACTION(LookupActions.INTERACTION);
|
||||
|
||||
private final int id;
|
||||
|
||||
BlockAction(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
|
||||
/**
|
||||
* Container transaction actions used by typed container lookup filters.
|
||||
*/
|
||||
public enum ContainerAction {
|
||||
REMOVE(ItemTransactionActions.REMOVE),
|
||||
ADD(ItemTransactionActions.ADD);
|
||||
|
||||
private final int id;
|
||||
|
||||
ContainerAction(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
|
||||
/**
|
||||
* CoreProtect action identifiers used by lookup filters.
|
||||
*/
|
||||
public enum CoreProtectAction {
|
||||
BLOCK_BREAK(LookupActions.BLOCK_BREAK),
|
||||
BLOCK_PLACE(LookupActions.BLOCK_PLACE),
|
||||
INTERACTION(LookupActions.INTERACTION),
|
||||
ENTITY_KILL(LookupActions.ENTITY_KILL),
|
||||
CONTAINER(LookupActions.CONTAINER),
|
||||
CHAT(LookupActions.CHAT),
|
||||
COMMAND(LookupActions.COMMAND),
|
||||
SESSION(LookupActions.SESSION),
|
||||
USERNAME(LookupActions.USERNAME),
|
||||
SIGN(LookupActions.SIGN),
|
||||
ITEM(LookupActions.ITEM),
|
||||
UNKNOWN(-1),
|
||||
ENTITY_SPAWN(LookupActions.ENTITY_SPAWN);
|
||||
|
||||
private final int id;
|
||||
|
||||
CoreProtectAction(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String actionString() {
|
||||
return LookupActions.getActionString(id);
|
||||
}
|
||||
|
||||
public static CoreProtectAction fromId(int id) {
|
||||
for (CoreProtectAction action : values()) {
|
||||
if (action.id == id) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import net.coreprotect.api.result.EntityResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
|
||||
/**
|
||||
* Provides API methods for looking up entity spawn and kill events.
|
||||
*/
|
||||
public class EntityAPI {
|
||||
|
||||
private EntityAPI() {
|
||||
throw new IllegalStateException("API class");
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed entity lookup, matching original or persisted current/final spawn locations.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of entity results at their original event locations
|
||||
*/
|
||||
public static List<EntityResult> performLookup(LookupOptions options) {
|
||||
List<EntityResult> result = new ArrayList<>();
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean snapshot = filter.beginDuckDBSnapshot(connection);
|
||||
try {
|
||||
StringBuilder query = new StringBuilder("SELECT entity_rows.time,entity_rows." + ConfigHandler.databaseType.getUserColumn() + ",entity_rows.wid,entity_rows.x,entity_rows.y,entity_rows.z,entity_rows.type,entity_rows.action,entity_rows.rolled_back FROM ");
|
||||
query.append(filter.entityTable(connection, "entity_rows"));
|
||||
query.append(' ');
|
||||
filter.appendEntityWhere(connection, query, "entity_rows");
|
||||
int[] actions = options.getEntityActions().isEmpty()
|
||||
? new int[] { LookupActions.ENTITY_KILL, LookupActions.ENTITY_SPAWN }
|
||||
: options.getEntityActions().stream().mapToInt(EntityAction::id).toArray();
|
||||
LookupFilter.appendActionWhere(query, "entity_rows", actions);
|
||||
if (!options.getIncludeEntities().isEmpty()) {
|
||||
query.append(" AND ").append(entityPredicate(options.getIncludeEntities()));
|
||||
}
|
||||
if (!options.getExcludeEntities().isEmpty()) {
|
||||
query.append(" AND NOT ").append(entityPredicate(options.getExcludeEntities()));
|
||||
}
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder().replace("time", "entity_rows.time").replace("rowid", "entity_rows.rowid"));
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
filter.bindTrackedEntity(statement, 1);
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(new EntityResult(
|
||||
results.getLong("time"), UserStatement.getName(connection, results.getInt("user")), WorldUtils.getWorldName(results.getInt("wid")),
|
||||
results.getInt("x"), results.getInt("y"), results.getInt("z"), results.getInt("type"), results.getInt("action"), results.getInt("rolled_back")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
filter.endDuckDBSnapshot(connection, snapshot);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String entityPredicate(List<EntityType> entities) {
|
||||
StringJoiner names = new StringJoiner(",");
|
||||
for (EntityType entity : entities) {
|
||||
String name = entity.name().toLowerCase(Locale.ROOT);
|
||||
names.add("'" + name + "'");
|
||||
names.add("'minecraft:" + name + "'");
|
||||
}
|
||||
String predicate = "entity_rows.type IN (SELECT id FROM " + ConfigHandler.prefix + "entity_map WHERE LOWER(entity) IN (" + names + "))";
|
||||
if (entities.contains(EntityType.PLAYER)) {
|
||||
predicate += " OR (entity_rows.action=" + LookupActions.ENTITY_KILL + " AND entity_rows.type=0)";
|
||||
}
|
||||
return "(" + predicate + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
|
||||
/**
|
||||
* Entity actions used by typed entity lookup filters.
|
||||
*/
|
||||
public enum EntityAction {
|
||||
KILL(LookupActions.ENTITY_KILL),
|
||||
SPAWN(LookupActions.ENTITY_SPAWN);
|
||||
|
||||
private final int id;
|
||||
|
||||
EntityAction(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import net.coreprotect.api.result.InventoryResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.model.item.InventorySources;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.DatabaseUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for normalized player inventory transaction lookups. Tracked entity-container rows match their original or persisted
|
||||
* current/final location and expose the persisted current/final location as container-source results.
|
||||
*/
|
||||
public class InventoryAPI {
|
||||
|
||||
private InventoryAPI() {
|
||||
throw new IllegalStateException("API class");
|
||||
}
|
||||
|
||||
public static List<InventoryResult> performLookup(LookupOptions options) {
|
||||
List<InventoryResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean snapshot = filter.beginDuckDBSnapshot(connection);
|
||||
try {
|
||||
StringBuilder whereBuilder = new StringBuilder();
|
||||
filter.appendWhere(whereBuilder);
|
||||
StringBuilder blockWhereBuilder = new StringBuilder(whereBuilder);
|
||||
filter.appendMaterialWhere(blockWhereBuilder, "", true);
|
||||
filter.appendMaterialWhere(whereBuilder);
|
||||
String where = whereBuilder.toString();
|
||||
StringBuilder entityWhereBuilder = new StringBuilder();
|
||||
filter.appendEntityContainerWhere(entityWhereBuilder, "entity_rows", "spawn_rows");
|
||||
filter.appendMaterialWhere(entityWhereBuilder, "entity_rows");
|
||||
String query = buildQuery(
|
||||
blockWhereBuilder.toString(),
|
||||
where,
|
||||
entityWhereBuilder.toString(),
|
||||
options.getInventoryActions(),
|
||||
options.hasLimit(),
|
||||
filter.table(connection, "block", ""),
|
||||
filter.table(connection, "container", ""),
|
||||
filter.entityContainerTable(connection, "entity_rows"),
|
||||
filter.table(connection, "item", "")
|
||||
);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query)) {
|
||||
int parameterIndex = filter.bind(statement);
|
||||
parameterIndex = filter.bind(statement, parameterIndex);
|
||||
parameterIndex = filter.bindEntityContainer(statement, parameterIndex);
|
||||
parameterIndex = filter.bind(statement, parameterIndex);
|
||||
if (options.hasLimit()) {
|
||||
statement.setInt(parameterIndex++, options.getLimitCount());
|
||||
statement.setInt(parameterIndex, options.getLimitOffset());
|
||||
}
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseInventoryResult(connection, results));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
filter.endDuckDBSnapshot(connection, snapshot);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String buildQuery(String blockWhere, String where, String entityWhere, List<InventoryAction> actions, boolean hasLimit, String blockTable, String containerTable, String entityContainerTable, String itemTable) {
|
||||
StringBuilder query = new StringBuilder("SELECT * FROM (");
|
||||
query.append("SELECT 0 AS source,rowid AS id,time,").append(ConfigHandler.databaseType.getUserColumn()).append(",wid,x,y,z,type,data,1 AS amount,meta AS metadata,action,rolled_back FROM ")
|
||||
.append(blockTable).append(' ').append(blockWhere).append(" AND action = 1");
|
||||
query.append(" UNION ALL ");
|
||||
query.append("SELECT 1 AS source,rowid AS id,time,").append(ConfigHandler.databaseType.getUserColumn()).append(",wid,x,y,z,type,data,amount,metadata,action,rolled_back FROM ")
|
||||
.append(containerTable).append(' ').append(where);
|
||||
query.append(" UNION ALL ");
|
||||
query.append("SELECT ").append(InventorySources.ENTITY_CONTAINER).append(" AS source,entity_rows.rowid AS id,entity_rows.time,entity_rows.").append(ConfigHandler.databaseType.getUserColumn()).append(",spawn_rows.current_wid AS wid,spawn_rows.x,spawn_rows.y,spawn_rows.z,entity_rows.type,entity_rows.data,entity_rows.amount,entity_rows.metadata,entity_rows.action,entity_rows.rolled_back FROM ")
|
||||
.append(entityContainerTable).append(" JOIN ").append(ConfigHandler.prefix).append("entity_spawn spawn_rows ON spawn_rows.rowid=entity_rows.entity_spawn_rowid ").append(entityWhere);
|
||||
query.append(" UNION ALL ");
|
||||
query.append("SELECT 2 AS source,rowid AS id,time,").append(ConfigHandler.databaseType.getUserColumn()).append(",wid,x,y,z,type,0 AS data,amount,data AS metadata,action,rolled_back FROM ")
|
||||
.append(itemTable).append(' ').append(where);
|
||||
query.append(") AS inventory_lookup");
|
||||
appendActionWhere(query, actions);
|
||||
query.append(" ORDER BY time DESC, source DESC, id DESC");
|
||||
if (hasLimit) {
|
||||
query.append(" LIMIT ? OFFSET ?");
|
||||
}
|
||||
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
private static void appendActionWhere(StringBuilder query, List<InventoryAction> actions) {
|
||||
if (!actions.isEmpty()) {
|
||||
StringJoiner predicates = new StringJoiner(" OR ");
|
||||
for (InventoryAction action : actions) {
|
||||
String sources = String.valueOf(action.sourceId());
|
||||
if (action.sourceId() == InventorySources.CONTAINER) {
|
||||
sources += "," + InventorySources.ENTITY_CONTAINER;
|
||||
}
|
||||
predicates.add("(source IN (" + sources + ") AND action = " + action.id() + ")");
|
||||
}
|
||||
query.append(" WHERE (").append(predicates).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
private static InventoryResult parseInventoryResult(Connection connection, ResultSet results) throws Exception {
|
||||
int userId = results.getInt("user");
|
||||
String username = UserStatement.getName(connection, userId);
|
||||
|
||||
int source = results.getInt("source");
|
||||
if (source == InventorySources.ENTITY_CONTAINER) {
|
||||
source = InventorySources.CONTAINER;
|
||||
}
|
||||
|
||||
return new InventoryResult(
|
||||
results.getLong("time"), username, WorldUtils.getWorldName(results.getInt("wid")),
|
||||
(int) Math.floor(results.getDouble("x")), (int) Math.floor(results.getDouble("y")), (int) Math.floor(results.getDouble("z")),
|
||||
results.getInt("type"), results.getInt("data"), results.getInt("amount"), DatabaseUtils.getBytes(results, "metadata"),
|
||||
results.getInt("action"), results.getInt("rolled_back"), source
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.model.item.InventorySources;
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
|
||||
/**
|
||||
* Source-specific transaction actions used by typed inventory lookup filters.
|
||||
* Container actions include tracked entity-container transactions.
|
||||
*/
|
||||
public enum InventoryAction {
|
||||
BLOCK_PLACE(InventorySources.BLOCK, LookupActions.BLOCK_PLACE),
|
||||
CONTAINER_REMOVE(InventorySources.CONTAINER, ItemTransactionActions.REMOVE),
|
||||
CONTAINER_ADD(InventorySources.CONTAINER, ItemTransactionActions.ADD),
|
||||
ITEM_DROP(InventorySources.ITEM, ItemTransactionActions.DROP),
|
||||
ITEM_PICKUP(InventorySources.ITEM, ItemTransactionActions.PICKUP),
|
||||
ITEM_REMOVE_ENDER(InventorySources.ITEM, ItemTransactionActions.REMOVE_ENDER),
|
||||
ITEM_ADD_ENDER(InventorySources.ITEM, ItemTransactionActions.ADD_ENDER),
|
||||
ITEM_THROW(InventorySources.ITEM, ItemTransactionActions.THROW),
|
||||
ITEM_SHOOT(InventorySources.ITEM, ItemTransactionActions.SHOOT),
|
||||
ITEM_BREAK(InventorySources.ITEM, ItemTransactionActions.BREAK),
|
||||
ITEM_DESTROY(InventorySources.ITEM, ItemTransactionActions.DESTROY),
|
||||
ITEM_CREATE(InventorySources.ITEM, ItemTransactionActions.CREATE),
|
||||
ITEM_SELL(InventorySources.ITEM, ItemTransactionActions.SELL),
|
||||
ITEM_BUY(InventorySources.ITEM, ItemTransactionActions.BUY);
|
||||
|
||||
private final int sourceId;
|
||||
private final int id;
|
||||
|
||||
InventoryAction(int sourceId, int id) {
|
||||
this.sourceId = sourceId;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int sourceId() {
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.coreprotect.api.result.ItemResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.logger.ItemLogger;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.DatabaseUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for world item transaction lookups.
|
||||
*/
|
||||
public class ItemAPI {
|
||||
|
||||
private ItemAPI() {
|
||||
throw new IllegalStateException("API class");
|
||||
}
|
||||
|
||||
public static List<ItemResult> performLookup(LookupOptions options) {
|
||||
List<ItemResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,type,data,amount,action,rolled_back FROM ");
|
||||
query.append(filter.table(connection, "item", "")).append(' ');
|
||||
if (filter.hasLocation() && !ConfigHandler.databaseType.isDuckDB()) {
|
||||
query.append(WorldUtils.getWidIndex("item"));
|
||||
}
|
||||
filter.appendWhere(query);
|
||||
filter.appendMaterialWhere(query);
|
||||
if (options.getItemActions().isEmpty()) {
|
||||
query.append(" AND action NOT IN (")
|
||||
.append(ItemLogger.ITEM_BREAK).append(",")
|
||||
.append(ItemLogger.ITEM_DESTROY).append(",")
|
||||
.append(ItemLogger.ITEM_CREATE).append(",")
|
||||
.append(ItemLogger.ITEM_SELL).append(",")
|
||||
.append(ItemLogger.ITEM_BUY).append(")");
|
||||
}
|
||||
else {
|
||||
LookupFilter.appendActionWhere(query, "", options.getItemActions().stream().mapToInt(ItemAction::id).toArray());
|
||||
}
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
filter.bind(statement);
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseItemResult(connection, results));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ItemResult parseItemResult(Connection connection, ResultSet results) throws Exception {
|
||||
int userId = results.getInt("user");
|
||||
String username = UserStatement.getName(connection, userId);
|
||||
|
||||
return new ItemResult(
|
||||
results.getLong("time"), username, WorldUtils.getWorldName(results.getInt("wid")),
|
||||
results.getInt("x"), results.getInt("y"), results.getInt("z"),
|
||||
results.getInt("type"), results.getInt("amount"), DatabaseUtils.getBytes(results, "data"),
|
||||
results.getInt("action"), results.getInt("rolled_back")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
|
||||
/**
|
||||
* Item transaction actions used by typed item lookup filters.
|
||||
*/
|
||||
public enum ItemAction {
|
||||
DROP(ItemTransactionActions.DROP),
|
||||
PICKUP(ItemTransactionActions.PICKUP),
|
||||
REMOVE_ENDER(ItemTransactionActions.REMOVE_ENDER),
|
||||
ADD_ENDER(ItemTransactionActions.ADD_ENDER),
|
||||
THROW(ItemTransactionActions.THROW),
|
||||
SHOOT(ItemTransactionActions.SHOOT),
|
||||
BREAK(ItemTransactionActions.BREAK),
|
||||
DESTROY(ItemTransactionActions.DESTROY),
|
||||
CREATE(ItemTransactionActions.CREATE),
|
||||
SELL(ItemTransactionActions.SELL),
|
||||
BUY(ItemTransactionActions.BUY);
|
||||
|
||||
private final int id;
|
||||
|
||||
ItemAction(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.DuckDBLookupQuery;
|
||||
import net.coreprotect.database.DuckDBSpatialIndex;
|
||||
import net.coreprotect.database.LocationQuery;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.ItemUtils;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
import net.coreprotect.utility.StringUtils;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
|
||||
final class LookupFilter {
|
||||
private final Integer userId;
|
||||
private final int checkTime;
|
||||
private final Location location;
|
||||
private final World world;
|
||||
private final int radius;
|
||||
private final int limitOffset;
|
||||
private final int limitCount;
|
||||
private final List<Material> includeMaterials;
|
||||
private final List<Material> excludeMaterials;
|
||||
private final Map<Integer, Material> materialTypes;
|
||||
private final String includeUserIds;
|
||||
private final String excludeUserIds;
|
||||
|
||||
private LookupFilter(Integer userId, int checkTime, Location location, World world, int radius, int limitOffset, int limitCount, List<Material> includeMaterials, List<Material> excludeMaterials, Map<Integer, Material> materialTypes, String includeUserIds, String excludeUserIds) {
|
||||
this.userId = userId;
|
||||
this.checkTime = checkTime;
|
||||
this.location = location;
|
||||
this.world = location == null ? world : location.getWorld();
|
||||
this.radius = radius;
|
||||
this.limitOffset = limitOffset;
|
||||
this.limitCount = limitCount;
|
||||
this.includeMaterials = includeMaterials;
|
||||
this.excludeMaterials = excludeMaterials;
|
||||
this.materialTypes = materialTypes;
|
||||
this.includeUserIds = includeUserIds;
|
||||
this.excludeUserIds = excludeUserIds;
|
||||
}
|
||||
|
||||
static LookupFilter fromOptions(Connection connection, LookupOptions options) throws Exception {
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
Integer userId = MessageAPI.getUserId(connection, options.getUser());
|
||||
int checkTime = 0;
|
||||
if (options.getTime() > 0) {
|
||||
checkTime = (int) (System.currentTimeMillis() / 1000L) - options.getTime();
|
||||
}
|
||||
|
||||
Map<Integer, Material> materialTypes = new HashMap<>();
|
||||
if (!options.getIncludeMaterials().isEmpty() || !options.getExcludeMaterials().isEmpty()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT id, material FROM " + ConfigHandler.prefix + "material_map");
|
||||
ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
materialTypes.put(results.getInt("id"), MaterialUtils.getTypeFromStoredName(results.getString("material")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new LookupFilter(userId, checkTime, options.getLocation(), options.getWorld(), options.getRadius(), options.getLimitOffset(), options.getLimitCount(),
|
||||
options.getIncludeMaterials(), options.getExcludeMaterials(), materialTypes,
|
||||
userIds(connection, options.getUsers()), userIds(connection, options.getExcludeUsers()));
|
||||
}
|
||||
|
||||
boolean hasInvalidUser() {
|
||||
return userId != null && userId == -1;
|
||||
}
|
||||
|
||||
boolean hasInvalidLocation() {
|
||||
return location != null && location.getWorld() == null;
|
||||
}
|
||||
|
||||
boolean hasLocation() {
|
||||
return location != null;
|
||||
}
|
||||
|
||||
boolean beginDuckDBSnapshot(Connection connection) throws Exception {
|
||||
if (!ConfigHandler.databaseType.isDuckDB() || location == null || !connection.getAutoCommit()) {
|
||||
return false;
|
||||
}
|
||||
connection.setAutoCommit(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
void endDuckDBSnapshot(Connection connection, boolean started) throws Exception {
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
connection.rollback();
|
||||
}
|
||||
finally {
|
||||
connection.setAutoCommit(true);
|
||||
}
|
||||
}
|
||||
|
||||
void appendWhere(StringBuilder query) {
|
||||
appendWhere(query, "");
|
||||
}
|
||||
|
||||
void appendWhere(StringBuilder query, String alias) {
|
||||
String qualifier = alias.isEmpty() ? "" : alias + ".";
|
||||
query.append("WHERE ").append(qualifier).append("time > ?");
|
||||
|
||||
if (userId != null) {
|
||||
query.append(" AND ").append(qualifier).append(ConfigHandler.databaseType.getUserColumn()).append(" = ?");
|
||||
}
|
||||
appendUserWhere(query, alias, includeUserIds, excludeUserIds);
|
||||
|
||||
if (world != null) {
|
||||
query.append(" AND ").append(LocationQuery.predicate(qualifier + "wid", " = ?"));
|
||||
}
|
||||
if (location != null) {
|
||||
if (radius > 0) {
|
||||
query.append(" AND ").append(LocationQuery.predicate(qualifier + "x", " >= ?"))
|
||||
.append(" AND ").append(LocationQuery.predicate(qualifier + "x", " <= ?"))
|
||||
.append(" AND ").append(LocationQuery.predicate(qualifier + "z", " >= ?"))
|
||||
.append(" AND ").append(LocationQuery.predicate(qualifier + "z", " <= ?"));
|
||||
}
|
||||
else {
|
||||
query.append(" AND ").append(LocationQuery.predicate(qualifier + "x", " = ?"))
|
||||
.append(" AND ").append(qualifier).append("y = ? AND ").append(LocationQuery.predicate(qualifier + "z", " = ?"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void appendEntityContainerWhere(StringBuilder query, String transactionAlias, String entityAlias) {
|
||||
appendTrackedEntityWhere(query, transactionAlias, entityAlias, null);
|
||||
}
|
||||
|
||||
void appendEntityWhere(Connection connection, StringBuilder query, String alias) {
|
||||
appendTrackedEntityWhere(query, alias, "spawn_rows", table(connection, "block", "original_rows"));
|
||||
}
|
||||
|
||||
private void appendTrackedEntityWhere(StringBuilder query, String transactionAlias, String entityAlias, String originalTable) {
|
||||
String transaction = transactionAlias + ".";
|
||||
String entity = entityAlias + ".";
|
||||
query.append("WHERE ").append(transaction).append("time > ?");
|
||||
if (userId != null) {
|
||||
query.append(" AND ").append(transaction).append(ConfigHandler.databaseType.getUserColumn()).append(" = ?");
|
||||
}
|
||||
appendUserWhere(query, transactionAlias, includeUserIds, excludeUserIds);
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String original = originalTable == null ? transaction : "original_rows.";
|
||||
String tracked = ") OR (";
|
||||
String ending = "))";
|
||||
if (originalTable != null) {
|
||||
String trackingId = ConfigHandler.databaseType.isClickHouse() ? "accurateCastOrNull(linked_rows.data, 'UInt64')" : "linked_rows.data";
|
||||
String trackedRows = "SELECT " + entity + "block_rowid FROM " + ConfigHandler.prefix + "entity_spawn " + entityAlias
|
||||
+ " INNER JOIN " + ConfigHandler.prefix + "block linked_rows ON linked_rows.rowid=" + entity + "block_rowid AND " + trackingId + "=" + entity + "rowid"
|
||||
+ " AND linked_rows.action=" + LookupActions.ENTITY_SPAWN + " WHERE (";
|
||||
if (location == null) {
|
||||
query.append(" AND (").append(LocationQuery.predicate(transaction + "wid", " = ?"))
|
||||
.append(" OR ").append(transaction).append("rowid IN (").append(trackedRows).append(entity).append("current_wid = ?)))");
|
||||
return;
|
||||
}
|
||||
// Separate location candidates keep both spatial indexes usable, including on MySQL.
|
||||
query.append(" AND ").append(transaction).append("rowid IN (SELECT rowid FROM (SELECT original_rows.rowid FROM ").append(originalTable).append(" WHERE (");
|
||||
tracked = ") UNION ALL " + trackedRows;
|
||||
ending = ")) entity_locations)";
|
||||
}
|
||||
else {
|
||||
query.append(" AND ((");
|
||||
}
|
||||
|
||||
query.append(LocationQuery.predicate(original + "wid", " = ?"));
|
||||
if (location == null) {
|
||||
query.append(tracked).append(entity).append("current_wid = ?").append(ending);
|
||||
return;
|
||||
}
|
||||
|
||||
if (radius > 0) {
|
||||
query.append(" AND ").append(LocationQuery.predicate(original + "x", " >= ?"))
|
||||
.append(" AND ").append(LocationQuery.predicate(original + "x", " <= ?"))
|
||||
.append(" AND ").append(LocationQuery.predicate(original + "z", " >= ?"))
|
||||
.append(" AND ").append(LocationQuery.predicate(original + "z", " <= ?"));
|
||||
}
|
||||
else {
|
||||
query.append(" AND ").append(LocationQuery.predicate(original + "x", " = ?"))
|
||||
.append(" AND ").append(original).append("y = ? AND ").append(LocationQuery.predicate(original + "z", " = ?"));
|
||||
}
|
||||
|
||||
query.append(tracked).append(entity).append("current_wid = ?");
|
||||
if (radius > 0) {
|
||||
query.append(" AND ").append(entity).append("x >= ? AND ").append(entity).append("x < ? AND ").append(entity).append("z >= ? AND ").append(entity).append("z < ?");
|
||||
}
|
||||
else {
|
||||
query.append(" AND ").append(entity).append("x >= ? AND ").append(entity).append("x < ? AND ").append(entity).append("y >= ? AND ").append(entity).append("y < ? AND ").append(entity).append("z >= ? AND ").append(entity).append("z < ?");
|
||||
}
|
||||
query.append(ending);
|
||||
}
|
||||
|
||||
static void appendActionWhere(StringBuilder query, String alias, int[] actions) {
|
||||
if (actions.length > 0) {
|
||||
StringJoiner ids = new StringJoiner(",");
|
||||
for (int action : actions) {
|
||||
ids.add(String.valueOf(action));
|
||||
}
|
||||
query.append(" AND ").append(alias.isEmpty() ? "" : alias + ".").append("action IN (").append(ids).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
void appendMaterialWhere(StringBuilder query) {
|
||||
appendMaterialWhere(query, "");
|
||||
}
|
||||
|
||||
void appendMaterialWhere(StringBuilder query, String alias) {
|
||||
appendMaterialWhere(query, alias, false);
|
||||
}
|
||||
|
||||
void appendMaterialWhere(StringBuilder query, String alias, boolean inventoryBlock) {
|
||||
String qualifier = alias.isEmpty() ? "" : alias + ".";
|
||||
if (!includeMaterials.isEmpty()) {
|
||||
query.append(" AND ").append(qualifier).append("type IN (").append(materialIds(includeMaterials, inventoryBlock)).append(")");
|
||||
}
|
||||
if (!excludeMaterials.isEmpty()) {
|
||||
query.append(" AND ").append(qualifier).append("type NOT IN (").append(materialIds(excludeMaterials, inventoryBlock)).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
void appendBlockMaterialWhere(StringBuilder query) {
|
||||
String entityActions = LookupActions.ENTITY_KILL + "," + LookupActions.ENTITY_SPAWN;
|
||||
if (!includeMaterials.isEmpty()) {
|
||||
query.append(" AND action NOT IN (").append(entityActions).append(") AND ").append(blockMaterialPredicate(includeMaterials));
|
||||
}
|
||||
if (!excludeMaterials.isEmpty()) {
|
||||
query.append(" AND (action IN (").append(entityActions).append(") OR NOT ").append(blockMaterialPredicate(excludeMaterials)).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
private String blockMaterialPredicate(List<Material> materials) {
|
||||
List<Material> blockMaterials = new ArrayList<>(materials);
|
||||
blockMaterials.removeAll(List.of(Material.STONE));
|
||||
return "(type IN (" + materialIds(blockMaterials, false) + ") OR " + legacyStonePredicate(materials) + ")";
|
||||
}
|
||||
|
||||
private String legacyStonePredicate(List<Material> materials) {
|
||||
StringJoiner stoneData = new StringJoiner(",");
|
||||
for (int data = 1; data <= 6; data++) {
|
||||
Material material = Material.getMaterial(StringUtils.nameFilter("stone", data).toUpperCase(Locale.ROOT));
|
||||
if (materials.contains(material)) {
|
||||
stoneData.add(String.valueOf(data));
|
||||
}
|
||||
}
|
||||
|
||||
StringJoiner predicates = new StringJoiner(" OR ");
|
||||
if (materials.contains(Material.STONE)) {
|
||||
predicates.add("COALESCE(data,0) NOT BETWEEN 1 AND 6");
|
||||
}
|
||||
if (stoneData.length() > 0) {
|
||||
predicates.add("COALESCE(data,0) IN (" + stoneData + ")");
|
||||
}
|
||||
if (predicates.length() == 0) {
|
||||
return "1 = 0";
|
||||
}
|
||||
|
||||
return "(type IN (" + materialIds(List.of(Material.STONE), false) + ") AND (" + predicates + "))";
|
||||
}
|
||||
|
||||
String table(Connection connection, String table, String alias) {
|
||||
if (location == null) {
|
||||
return ConfigHandler.prefix + table + alias(alias);
|
||||
}
|
||||
|
||||
int x = location.getBlockX();
|
||||
int z = location.getBlockZ();
|
||||
int minimumX = radius > 0 ? MessageAPI.clampToInt((long) x - radius) : x;
|
||||
int maximumX = radius > 0 ? MessageAPI.clampToInt((long) x + radius) : x;
|
||||
int minimumZ = radius > 0 ? MessageAPI.clampToInt((long) z - radius) : z;
|
||||
int maximumZ = radius > 0 ? MessageAPI.clampToInt((long) z + radius) : z;
|
||||
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
|
||||
return DuckDBLookupQuery.spatialTable(connection, table, worldId, minimumX, maximumX, minimumZ, maximumZ, alias);
|
||||
}
|
||||
|
||||
String entityContainerTable(Connection connection, String alias) throws Exception {
|
||||
return trackedEntityTable(connection, "entity_container", alias);
|
||||
}
|
||||
|
||||
String entityTable(Connection connection, String alias) throws Exception {
|
||||
return trackedEntityTable(connection, "block", alias);
|
||||
}
|
||||
|
||||
private String trackedEntityTable(Connection connection, String table, String alias) throws Exception {
|
||||
if (location == null || !ConfigHandler.databaseType.isDuckDB()) {
|
||||
return ConfigHandler.prefix + table + alias(alias);
|
||||
}
|
||||
|
||||
int x = location.getBlockX();
|
||||
int z = location.getBlockZ();
|
||||
int minimumX = radius > 0 ? MessageAPI.clampToInt((long) x - radius) : x;
|
||||
int maximumX = radius > 0 ? MessageAPI.clampToInt((long) x + radius) : x;
|
||||
int minimumZ = radius > 0 ? MessageAPI.clampToInt((long) z - radius) : z;
|
||||
int maximumZ = radius > 0 ? MessageAPI.clampToInt((long) z + radius) : z;
|
||||
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
|
||||
boolean block = table.equals("block");
|
||||
List<Long> rowIds = loadCurrentEntityRowIds(connection, block ? "block_rowid" : "rowid");
|
||||
return DuckDBSpatialIndex.tableExpression(
|
||||
connection,
|
||||
ConfigHandler.prefix,
|
||||
table,
|
||||
worldId,
|
||||
minimumX,
|
||||
maximumX,
|
||||
minimumZ,
|
||||
maximumZ,
|
||||
block ? Collections.emptySet() : rowIds.stream().map(Long::intValue).collect(Collectors.toList()),
|
||||
block ? rowIds : Collections.emptySet(),
|
||||
alias
|
||||
);
|
||||
}
|
||||
|
||||
private List<Long> loadCurrentEntityRowIds(Connection connection, String column) throws Exception {
|
||||
List<Long> rowIds = new ArrayList<>();
|
||||
StringBuilder query = new StringBuilder("SELECT ").append(column).append(" FROM ").append(ConfigHandler.prefix).append("entity_spawn WHERE current_wid=?");
|
||||
if (radius > 0) {
|
||||
query.append(" AND x>=? AND x<? AND z>=? AND z<?");
|
||||
}
|
||||
else {
|
||||
query.append(" AND x>=? AND x<? AND y>=? AND y<? AND z>=? AND z<?");
|
||||
}
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
int parameterIndex = 1;
|
||||
int x = location.getBlockX();
|
||||
int y = location.getBlockY();
|
||||
int z = location.getBlockZ();
|
||||
statement.setInt(parameterIndex++, WorldUtils.getWorldId(location.getWorld().getName()));
|
||||
if (radius > 0) {
|
||||
int minimumX = MessageAPI.clampToInt((long) x - radius);
|
||||
int maximumX = MessageAPI.clampToInt((long) x + radius);
|
||||
int minimumZ = MessageAPI.clampToInt((long) z - radius);
|
||||
int maximumZ = MessageAPI.clampToInt((long) z + radius);
|
||||
statement.setInt(parameterIndex++, minimumX);
|
||||
statement.setLong(parameterIndex++, (long) maximumX + 1L);
|
||||
statement.setInt(parameterIndex++, minimumZ);
|
||||
statement.setLong(parameterIndex, (long) maximumZ + 1L);
|
||||
}
|
||||
else {
|
||||
statement.setInt(parameterIndex++, x);
|
||||
statement.setLong(parameterIndex++, (long) x + 1L);
|
||||
statement.setInt(parameterIndex++, y);
|
||||
statement.setLong(parameterIndex++, (long) y + 1L);
|
||||
statement.setInt(parameterIndex++, z);
|
||||
statement.setLong(parameterIndex, (long) z + 1L);
|
||||
}
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next() && rowIds.size() <= 4_096) {
|
||||
long rowId = resultSet.getLong(1);
|
||||
if (!resultSet.wasNull()) {
|
||||
rowIds.add(rowId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rowIds;
|
||||
}
|
||||
|
||||
void appendLimit(StringBuilder query) {
|
||||
if (limitOffset >= 0 && limitCount >= 0) {
|
||||
query.append(" LIMIT ").append(limitCount).append(" OFFSET ").append(limitOffset);
|
||||
}
|
||||
}
|
||||
|
||||
int bind(PreparedStatement statement) throws Exception {
|
||||
return bind(statement, 1);
|
||||
}
|
||||
|
||||
int bind(PreparedStatement statement, int parameterIndex) throws Exception {
|
||||
statement.setInt(parameterIndex++, checkTime);
|
||||
|
||||
if (userId != null) {
|
||||
statement.setInt(parameterIndex++, userId);
|
||||
}
|
||||
|
||||
if (world != null) {
|
||||
statement.setInt(parameterIndex++, WorldUtils.getWorldId(world.getName()));
|
||||
}
|
||||
|
||||
if (location != null) {
|
||||
int x = location.getBlockX();
|
||||
int y = location.getBlockY();
|
||||
int z = location.getBlockZ();
|
||||
if (radius > 0) {
|
||||
statement.setInt(parameterIndex++, MessageAPI.clampToInt((long) x - radius));
|
||||
statement.setInt(parameterIndex++, MessageAPI.clampToInt((long) x + radius));
|
||||
statement.setInt(parameterIndex++, MessageAPI.clampToInt((long) z - radius));
|
||||
statement.setInt(parameterIndex++, MessageAPI.clampToInt((long) z + radius));
|
||||
}
|
||||
else {
|
||||
statement.setInt(parameterIndex++, x);
|
||||
statement.setInt(parameterIndex++, y);
|
||||
statement.setInt(parameterIndex++, z);
|
||||
}
|
||||
}
|
||||
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
int bindEntityContainer(PreparedStatement statement, int parameterIndex) throws Exception {
|
||||
return bindTrackedEntity(statement, parameterIndex);
|
||||
}
|
||||
|
||||
int bindTrackedEntity(PreparedStatement statement, int parameterIndex) throws Exception {
|
||||
statement.setInt(parameterIndex++, checkTime);
|
||||
if (userId != null) {
|
||||
statement.setInt(parameterIndex++, userId);
|
||||
}
|
||||
if (world == null) {
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
if (location == null) {
|
||||
int worldId = WorldUtils.getWorldId(world.getName());
|
||||
statement.setInt(parameterIndex++, worldId);
|
||||
statement.setInt(parameterIndex++, worldId);
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
int x = location.getBlockX();
|
||||
int y = location.getBlockY();
|
||||
int z = location.getBlockZ();
|
||||
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
|
||||
statement.setInt(parameterIndex++, worldId);
|
||||
if (radius > 0) {
|
||||
int minimumX = MessageAPI.clampToInt((long) x - radius);
|
||||
int maximumX = MessageAPI.clampToInt((long) x + radius);
|
||||
int minimumZ = MessageAPI.clampToInt((long) z - radius);
|
||||
int maximumZ = MessageAPI.clampToInt((long) z + radius);
|
||||
statement.setInt(parameterIndex++, minimumX);
|
||||
statement.setInt(parameterIndex++, maximumX);
|
||||
statement.setInt(parameterIndex++, minimumZ);
|
||||
statement.setInt(parameterIndex++, maximumZ);
|
||||
statement.setInt(parameterIndex++, worldId);
|
||||
statement.setInt(parameterIndex++, minimumX);
|
||||
statement.setLong(parameterIndex++, (long) maximumX + 1L);
|
||||
statement.setInt(parameterIndex++, minimumZ);
|
||||
statement.setLong(parameterIndex++, (long) maximumZ + 1L);
|
||||
}
|
||||
else {
|
||||
statement.setInt(parameterIndex++, x);
|
||||
statement.setInt(parameterIndex++, y);
|
||||
statement.setInt(parameterIndex++, z);
|
||||
statement.setInt(parameterIndex++, worldId);
|
||||
statement.setInt(parameterIndex++, x);
|
||||
statement.setLong(parameterIndex++, (long) x + 1L);
|
||||
statement.setInt(parameterIndex++, y);
|
||||
statement.setLong(parameterIndex++, (long) y + 1L);
|
||||
statement.setInt(parameterIndex++, z);
|
||||
statement.setLong(parameterIndex++, (long) z + 1L);
|
||||
}
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
static String userIds(Connection connection, List<String> users) throws Exception {
|
||||
StringJoiner result = new StringJoiner(",");
|
||||
for (String user : users) {
|
||||
Integer id = MessageAPI.getUserId(connection, user);
|
||||
if (id == null) {
|
||||
// An empty name or #global matches every user.
|
||||
return null;
|
||||
}
|
||||
result.add(String.valueOf(id));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
static void appendUserWhere(StringBuilder query, String alias, String includeUserIds, String excludeUserIds) {
|
||||
String column = (alias.isEmpty() ? "" : alias + ".") + ConfigHandler.databaseType.getUserColumn();
|
||||
if (includeUserIds != null && !includeUserIds.isEmpty()) {
|
||||
query.append(" AND ").append(column).append(" IN (").append(includeUserIds).append(")");
|
||||
}
|
||||
if (excludeUserIds == null) {
|
||||
query.append(" AND 1 = 0");
|
||||
}
|
||||
else if (!excludeUserIds.isEmpty()) {
|
||||
query.append(" AND ").append(column).append(" NOT IN (").append(excludeUserIds).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
private String materialIds(List<Material> materials, boolean inventoryBlock) {
|
||||
StringJoiner result = new StringJoiner(",");
|
||||
for (Map.Entry<Integer, Material> entry : materialTypes.entrySet()) {
|
||||
Material material = inventoryBlock ? ItemUtils.itemFilter(entry.getValue(), true) : entry.getValue();
|
||||
if (material != null && materials.contains(material)) {
|
||||
result.add(String.valueOf(entry.getKey()));
|
||||
}
|
||||
}
|
||||
return result.length() == 0 ? "-1" : result.toString();
|
||||
}
|
||||
|
||||
private static String alias(String alias) {
|
||||
return alias == null || alias.isEmpty() ? "" : " AS " + alias;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
/**
|
||||
* Shared options for typed lookup API methods.
|
||||
*/
|
||||
public final class LookupOptions {
|
||||
private final String user;
|
||||
private final int time;
|
||||
private final int radius;
|
||||
private final Location location;
|
||||
private final World world;
|
||||
private final int limitOffset;
|
||||
private final int limitCount;
|
||||
private final List<Material> includeMaterials;
|
||||
private final List<Material> excludeMaterials;
|
||||
private final List<String> users;
|
||||
private final List<String> excludeUsers;
|
||||
private final List<String> textStartsWithAny;
|
||||
private final List<String> textStartsWithNone;
|
||||
private final List<ContainerAction> containerActions;
|
||||
private final List<ItemAction> itemActions;
|
||||
private final List<InventoryAction> inventoryActions;
|
||||
private final List<BlockAction> blockActions;
|
||||
private final List<SessionAction> sessionActions;
|
||||
private final List<EntityAction> entityActions;
|
||||
private final List<EntityType> includeEntities;
|
||||
private final List<EntityType> excludeEntities;
|
||||
|
||||
private LookupOptions(Builder builder) {
|
||||
this.user = builder.user;
|
||||
this.time = builder.time;
|
||||
this.radius = builder.radius;
|
||||
this.location = builder.location;
|
||||
this.world = builder.world;
|
||||
this.limitOffset = builder.limitOffset;
|
||||
this.limitCount = builder.limitCount;
|
||||
this.includeMaterials = builder.includeMaterials;
|
||||
this.excludeMaterials = builder.excludeMaterials;
|
||||
this.users = builder.users;
|
||||
this.excludeUsers = builder.excludeUsers;
|
||||
this.textStartsWithAny = builder.textStartsWithAny;
|
||||
this.textStartsWithNone = builder.textStartsWithNone;
|
||||
this.containerActions = builder.containerActions;
|
||||
this.itemActions = builder.itemActions;
|
||||
this.inventoryActions = builder.inventoryActions;
|
||||
this.blockActions = builder.blockActions;
|
||||
this.sessionActions = builder.sessionActions;
|
||||
this.entityActions = builder.entityActions;
|
||||
this.includeEntities = builder.includeEntities;
|
||||
this.excludeEntities = builder.excludeEntities;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public int getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public int getRadius() {
|
||||
return radius;
|
||||
}
|
||||
|
||||
public Location getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
public int getLimitOffset() {
|
||||
return limitOffset;
|
||||
}
|
||||
|
||||
public int getLimitCount() {
|
||||
return limitCount;
|
||||
}
|
||||
|
||||
public boolean hasLimit() {
|
||||
return limitOffset >= 0 && limitCount >= 0;
|
||||
}
|
||||
|
||||
public List<Material> getIncludeMaterials() {
|
||||
return includeMaterials;
|
||||
}
|
||||
|
||||
public List<Material> getExcludeMaterials() {
|
||||
return excludeMaterials;
|
||||
}
|
||||
|
||||
public List<String> getUsers() {
|
||||
return users;
|
||||
}
|
||||
|
||||
public List<String> getExcludeUsers() {
|
||||
return excludeUsers;
|
||||
}
|
||||
|
||||
public List<String> getTextStartsWithAny() {
|
||||
return textStartsWithAny;
|
||||
}
|
||||
|
||||
public List<String> getTextStartsWithNone() {
|
||||
return textStartsWithNone;
|
||||
}
|
||||
|
||||
public List<ContainerAction> getContainerActions() {
|
||||
return containerActions;
|
||||
}
|
||||
|
||||
public List<ItemAction> getItemActions() {
|
||||
return itemActions;
|
||||
}
|
||||
|
||||
public List<InventoryAction> getInventoryActions() {
|
||||
return inventoryActions;
|
||||
}
|
||||
|
||||
public List<BlockAction> getBlockActions() {
|
||||
return blockActions;
|
||||
}
|
||||
|
||||
public List<SessionAction> getSessionActions() {
|
||||
return sessionActions;
|
||||
}
|
||||
|
||||
public List<EntityAction> getEntityActions() {
|
||||
return entityActions;
|
||||
}
|
||||
|
||||
public List<EntityType> getIncludeEntities() {
|
||||
return includeEntities;
|
||||
}
|
||||
|
||||
public List<EntityType> getExcludeEntities() {
|
||||
return excludeEntities;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String user;
|
||||
private int time;
|
||||
private int radius = -1;
|
||||
private Location location;
|
||||
private World world;
|
||||
private int limitOffset = -1;
|
||||
private int limitCount = -1;
|
||||
private List<Material> includeMaterials = List.of();
|
||||
private List<Material> excludeMaterials = List.of();
|
||||
private List<String> users = List.of();
|
||||
private List<String> excludeUsers = List.of();
|
||||
private List<String> textStartsWithAny = List.of();
|
||||
private List<String> textStartsWithNone = List.of();
|
||||
private List<ContainerAction> containerActions = List.of();
|
||||
private List<ItemAction> itemActions = List.of();
|
||||
private List<InventoryAction> inventoryActions = List.of();
|
||||
private List<BlockAction> blockActions = List.of();
|
||||
private List<SessionAction> sessionActions = List.of();
|
||||
private List<EntityAction> entityActions = List.of();
|
||||
private List<EntityType> includeEntities = List.of();
|
||||
private List<EntityType> excludeEntities = List.of();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder user(String user) {
|
||||
this.user = user;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder time(int time) {
|
||||
this.time = time;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder location(Location location) {
|
||||
this.location = location;
|
||||
this.world = null;
|
||||
this.radius = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder radius(Location location, int radius) {
|
||||
this.location = location;
|
||||
this.world = null;
|
||||
this.radius = radius;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder world(World world) {
|
||||
this.world = world;
|
||||
this.location = null;
|
||||
this.radius = -1;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder limit(int offset, int count) {
|
||||
this.limitOffset = offset;
|
||||
this.limitCount = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeMaterials(List<Material> materials) {
|
||||
this.includeMaterials = List.copyOf(materials);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder excludeMaterials(List<Material> materials) {
|
||||
this.excludeMaterials = List.copyOf(materials);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder users(List<String> users) {
|
||||
this.users = List.copyOf(users);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder excludeUsers(List<String> users) {
|
||||
this.excludeUsers = List.copyOf(users);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes chat, command and sign text matching any literal prefix.
|
||||
* Empty means no inclusion restriction. Other lookup types ignore this option.
|
||||
* @throws IllegalArgumentException if a prefix has fewer than three Unicode code points
|
||||
*/
|
||||
public Builder textStartsWithAny(List<String> prefixes) {
|
||||
this.textStartsWithAny = textPrefixes(prefixes);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes chat, command and sign text matching any literal prefix.
|
||||
* Exclusions take precedence over inclusions. A leading '-' is literal.
|
||||
* @throws IllegalArgumentException if a prefix has fewer than three Unicode code points
|
||||
*/
|
||||
public Builder textStartsWithNone(List<String> prefixes) {
|
||||
this.textStartsWithNone = textPrefixes(prefixes);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static List<String> textPrefixes(List<String> prefixes) {
|
||||
List<String> copy = List.copyOf(prefixes);
|
||||
for (String prefix : copy) {
|
||||
if (prefix.codePointCount(0, prefix.length()) < 3) {
|
||||
throw new IllegalArgumentException("Text filter prefixes must contain at least three code points");
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
public Builder containerActions(List<ContainerAction> actions) {
|
||||
this.containerActions = List.copyOf(actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder itemActions(List<ItemAction> actions) {
|
||||
this.itemActions = List.copyOf(actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder inventoryActions(List<InventoryAction> actions) {
|
||||
this.inventoryActions = List.copyOf(actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder blockActions(List<BlockAction> actions) {
|
||||
this.blockActions = List.copyOf(actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sessionActions(List<SessionAction> actions) {
|
||||
this.sessionActions = List.copyOf(actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder entityActions(List<EntityAction> actions) {
|
||||
this.entityActions = List.copyOf(actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeEntities(List<EntityType> entities) {
|
||||
this.includeEntities = List.copyOf(entities);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder excludeEntities(List<EntityType> entities) {
|
||||
this.excludeEntities = List.copyOf(entities);
|
||||
return this;
|
||||
}
|
||||
|
||||
public LookupOptions build() {
|
||||
return new LookupOptions(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.coreprotect.api.result.MessageResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.LookupRaw;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
|
||||
/**
|
||||
* Provides API methods for player chat and command lookups.
|
||||
*/
|
||||
public class MessageAPI {
|
||||
private MessageAPI() {
|
||||
throw new IllegalStateException("API class");
|
||||
}
|
||||
|
||||
public static List<MessageResult> performChatLookup(String user, int offset) {
|
||||
return performChatLookup(LookupOptions.builder().user(user).time(offset).build());
|
||||
}
|
||||
|
||||
public static List<MessageResult> performChatLookup(LookupOptions options) {
|
||||
return performLookup("chat", CoreProtectAction.CHAT, options);
|
||||
}
|
||||
|
||||
public static List<MessageResult> performCommandLookup(String user, int offset) {
|
||||
return performCommandLookup(LookupOptions.builder().user(user).time(offset).build());
|
||||
}
|
||||
|
||||
public static List<MessageResult> performCommandLookup(LookupOptions options) {
|
||||
return performLookup("command", CoreProtectAction.COMMAND, options);
|
||||
}
|
||||
|
||||
private static List<MessageResult> performLookup(String table, CoreProtectAction action, LookupOptions options) {
|
||||
List<MessageResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,message FROM ");
|
||||
query.append(filter.table(connection, table, "")).append(' ');
|
||||
if (filter.hasLocation() && !ConfigHandler.databaseType.isDuckDB()) {
|
||||
query.append(WorldUtils.getWidIndex(table));
|
||||
}
|
||||
filter.appendWhere(query);
|
||||
List<String> messageBindings = new ArrayList<>();
|
||||
query = new StringBuilder(LookupRaw.appendMessageFilters(query.toString(),
|
||||
options == null ? List.of() : options.getTextStartsWithAny(),
|
||||
options == null ? List.of() : options.getTextStartsWithNone(), table, messageBindings));
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
int parameterIndex = filter.bind(statement);
|
||||
for (String binding : messageBindings) {
|
||||
statement.setString(parameterIndex++, binding);
|
||||
}
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseMessageResult(connection, results, action.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Integer getUserId(Connection connection, String user) throws Exception {
|
||||
if (user == null || user.isEmpty() || user.equalsIgnoreCase("#global")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UserStatement.findId(connection, user);
|
||||
}
|
||||
|
||||
private static MessageResult parseMessageResult(Connection connection, ResultSet results, int actionId) throws Exception {
|
||||
int userId = results.getInt("user");
|
||||
String username = UserStatement.getName(connection, userId);
|
||||
|
||||
return new MessageResult(
|
||||
results.getLong("time"), username, WorldUtils.getWorldName(results.getInt("wid")),
|
||||
results.getInt("x"), results.getInt("y"), results.getInt("z"),
|
||||
results.getString("message"), actionId
|
||||
);
|
||||
}
|
||||
|
||||
static int clampToInt(long value) {
|
||||
if (value > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
else if (value < Integer.MIN_VALUE) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
return (int) value;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package net.coreprotect.api;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Location;
|
||||
@@ -18,6 +18,7 @@ import net.coreprotect.consumer.process.Process;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
import net.coreprotect.utility.StringUtils;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for looking up block-related actions in the processing queue.
|
||||
@@ -53,26 +54,27 @@ public class QueueLookup extends Queue {
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine total count of actions in the consumer queues
|
||||
int consumerCount = calculateConsumerCount();
|
||||
|
||||
if (consumerCount == 0) {
|
||||
return result;
|
||||
ArrayList<Object[]> consumerData;
|
||||
Map<Integer, String[]> users;
|
||||
Map<Integer, Object> consumerObject;
|
||||
synchronized (Queue.class) {
|
||||
synchronized (Consumer.consumer_id) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
consumerData = new ArrayList<>(Consumer.consumer.get(currentConsumer));
|
||||
users = new HashMap<>(Consumer.consumerUsers.get(currentConsumer));
|
||||
consumerObject = new HashMap<>(Consumer.consumerObjects.get(currentConsumer));
|
||||
}
|
||||
}
|
||||
|
||||
// Get data from the current consumer
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
ArrayList<Object[]> consumerData = Consumer.consumer.get(currentConsumer);
|
||||
Map<Integer, String[]> users = Consumer.consumerUsers.get(currentConsumer);
|
||||
Map<Integer, Object> consumerObject = Consumer.consumerObjects.get(currentConsumer);
|
||||
if (consumerData.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Current block location for comparison with actions in the queue
|
||||
Location blockLocation = block.getLocation();
|
||||
|
||||
// Check for block actions in the processing queue
|
||||
ListIterator<Object[]> iterator = consumerData.listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
Object[] data = iterator.next();
|
||||
for (Object[] data : consumerData) {
|
||||
int id = (int) data[0];
|
||||
int action = (int) data[1];
|
||||
|
||||
@@ -106,28 +108,12 @@ public class QueueLookup extends Queue {
|
||||
Collections.reverse(result);
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total count of actions in the consumer queues.
|
||||
*
|
||||
* @return The total count of actions in the consumer queues
|
||||
*/
|
||||
private static int calculateConsumerCount() {
|
||||
int currentConsumerSize = Process.getCurrentConsumerSize();
|
||||
if (currentConsumerSize == 0) {
|
||||
return Consumer.getConsumerSize(0) + Consumer.getConsumerSize(1);
|
||||
}
|
||||
else {
|
||||
int consumerId = (Consumer.currentConsumer == 1) ? 1 : 0;
|
||||
return Consumer.getConsumerSize(consumerId) + currentConsumerSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an action in the queue pertains to the specified block location.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import net.coreprotect.model.action.SessionActions;
|
||||
|
||||
/**
|
||||
* Session actions used by typed session lookup filters.
|
||||
*/
|
||||
public enum SessionAction {
|
||||
LOGOUT(SessionActions.LOGOUT),
|
||||
LOGIN(SessionActions.LOGIN);
|
||||
|
||||
private final int id;
|
||||
|
||||
SessionAction(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import net.coreprotect.api.result.SessionResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for looking up player session data in the CoreProtect database.
|
||||
@@ -62,8 +65,10 @@ public class SessionLookup {
|
||||
int time = (int) (System.currentTimeMillis() / 1000L);
|
||||
int checkTime = calculateCheckTime(time, offset);
|
||||
|
||||
// Get user ID from cache or load it
|
||||
int userId = getUserId(connection, user);
|
||||
Integer userId = MessageAPI.getUserId(connection, user);
|
||||
if (userId == null || userId < 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Query session data from database
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
@@ -78,7 +83,62 @@ public class SessionLookup {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a typed lookup of session-related actions.
|
||||
*
|
||||
* @param options
|
||||
* Lookup options
|
||||
* @return List of typed session results, empty list if API is disabled or no results found
|
||||
*/
|
||||
public static List<SessionResult> performLookup(LookupOptions options) {
|
||||
List<SessionResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,action FROM ");
|
||||
query.append(filter.table(connection, "session", "")).append(' ');
|
||||
if (filter.hasLocation() && !ConfigHandler.databaseType.isDuckDB()) {
|
||||
query.append(WorldUtils.getWidIndex("session"));
|
||||
}
|
||||
filter.appendWhere(query);
|
||||
LookupFilter.appendActionWhere(query, "", options.getSessionActions().stream().mapToInt(SessionAction::id).toArray());
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
filter.bind(statement);
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseSessionResult(connection, results));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -97,26 +157,6 @@ public class SessionLookup {
|
||||
return (offset > 0) ? currentTime - offset : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user ID for the specified username.
|
||||
* If the user ID is not in the cache, it will be loaded from the database.
|
||||
*
|
||||
* @param connection
|
||||
* The database connection
|
||||
* @param username
|
||||
* The username to get the ID for
|
||||
* @return The user ID
|
||||
*/
|
||||
private static int getUserId(Connection connection, String username) {
|
||||
String lowerUsername = username.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (ConfigHandler.playerIdCache.get(lowerUsername) == null) {
|
||||
UserStatement.loadId(connection, username, null);
|
||||
}
|
||||
|
||||
return ConfigHandler.playerIdCache.get(lowerUsername);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL query for retrieving session data.
|
||||
*
|
||||
@@ -127,7 +167,7 @@ public class SessionLookup {
|
||||
* @return The SQL query string
|
||||
*/
|
||||
private static String buildSessionQuery(int userId, int checkTime) {
|
||||
return "SELECT time,user,wid,x,y,z,action FROM " + ConfigHandler.prefix + "session WHERE user = '" + userId + "' AND time > '" + checkTime + "' ORDER BY rowid DESC";
|
||||
return "SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,action FROM " + ConfigHandler.prefix + "session WHERE " + ConfigHandler.databaseType.getUserColumn() + " = " + userId + " AND time > " + checkTime + " ORDER BY " + ConfigHandler.getDescendingEventOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,13 +192,19 @@ public class SessionLookup {
|
||||
String resultZ = results.getString("z");
|
||||
String resultAction = results.getString("action");
|
||||
|
||||
// Get username from cache or load it
|
||||
if (ConfigHandler.playerIdCacheReversed.get(resultUserId) == null) {
|
||||
UserStatement.loadName(connection, resultUserId);
|
||||
}
|
||||
String resultUser = ConfigHandler.playerIdCacheReversed.get(resultUserId);
|
||||
String resultUser = UserStatement.getName(connection, resultUserId);
|
||||
|
||||
// Create and return the session data array
|
||||
return new String[] { resultTime, resultUser, resultX, resultY, resultZ, resultWorldId, type, resultAction };
|
||||
}
|
||||
|
||||
private static SessionResult parseSessionResult(Connection connection, ResultSet results) throws Exception {
|
||||
int userId = results.getInt("user");
|
||||
String username = UserStatement.getName(connection, userId);
|
||||
|
||||
return new SessionResult(
|
||||
results.getLong("time"), username, WorldUtils.getWorldName(results.getInt("wid")),
|
||||
results.getInt("x"), results.getInt("y"), results.getInt("z"), results.getInt("action")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import net.coreprotect.api.result.SignResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.LookupRaw;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.model.action.SignActions;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for sign text lookups.
|
||||
*/
|
||||
public class SignAPI {
|
||||
|
||||
private SignAPI() {
|
||||
throw new IllegalStateException("API class");
|
||||
}
|
||||
|
||||
public static List<SignResult> performLookup(Location location, int offset) {
|
||||
if (location == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return performLookup(LookupOptions.builder().time(offset).location(location).build());
|
||||
}
|
||||
|
||||
public static List<SignResult> performLookup(LookupOptions options) {
|
||||
List<SignResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
LookupFilter filter = LookupFilter.fromOptions(connection, options);
|
||||
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,action,color,color_secondary,data,waxed,face,line_1,line_2,line_3,line_4,line_5,line_6,line_7,line_8 FROM ");
|
||||
query.append(filter.table(connection, "sign", "")).append(' ');
|
||||
if (filter.hasLocation() && !ConfigHandler.databaseType.isDuckDB()) {
|
||||
query.append(WorldUtils.getWidIndex("sign"));
|
||||
}
|
||||
filter.appendWhere(query);
|
||||
List<String> messageBindings = new ArrayList<>();
|
||||
query = new StringBuilder(LookupRaw.appendMessageFilters(query.toString(),
|
||||
options == null ? List.of() : options.getTextStartsWithAny(),
|
||||
options == null ? List.of() : options.getTextStartsWithNone(), "sign", messageBindings));
|
||||
query.append(" AND action = '").append(SignActions.PLACE).append("' AND (LENGTH(line_1) > 0 OR LENGTH(line_2) > 0 OR LENGTH(line_3) > 0 OR LENGTH(line_4) > 0 OR LENGTH(line_5) > 0 OR LENGTH(line_6) > 0 OR LENGTH(line_7) > 0 OR LENGTH(line_8) > 0)");
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
|
||||
filter.appendLimit(query);
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
int parameterIndex = filter.bind(statement);
|
||||
for (String binding : messageBindings) {
|
||||
statement.setString(parameterIndex++, binding);
|
||||
}
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseSignResult(connection, results));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SignResult parseSignResult(Connection connection, ResultSet results) throws Exception {
|
||||
int userId = results.getInt("user");
|
||||
String username = UserStatement.getName(connection, userId);
|
||||
|
||||
String[] lines = new String[] {
|
||||
valueOrEmpty(results.getString("line_1")),
|
||||
valueOrEmpty(results.getString("line_2")),
|
||||
valueOrEmpty(results.getString("line_3")),
|
||||
valueOrEmpty(results.getString("line_4")),
|
||||
valueOrEmpty(results.getString("line_5")),
|
||||
valueOrEmpty(results.getString("line_6")),
|
||||
valueOrEmpty(results.getString("line_7")),
|
||||
valueOrEmpty(results.getString("line_8"))
|
||||
};
|
||||
|
||||
return new SignResult(
|
||||
results.getLong("time"), username, WorldUtils.getWorldName(results.getInt("wid")),
|
||||
results.getInt("x"), results.getInt("y"), results.getInt("z"),
|
||||
results.getInt("action"), results.getInt("color"), results.getInt("color_secondary"),
|
||||
results.getInt("data"), results.getInt("waxed") == 1, results.getInt("face") == 0, lines
|
||||
);
|
||||
}
|
||||
|
||||
private static String valueOrEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package net.coreprotect.api;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import net.coreprotect.api.result.UsernameResult;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.utility.DatabaseUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
/**
|
||||
* Provides API methods for username history lookups.
|
||||
*/
|
||||
public class UsernameAPI {
|
||||
|
||||
private UsernameAPI() {
|
||||
throw new IllegalStateException("API class");
|
||||
}
|
||||
|
||||
public static List<UsernameResult> performLookup(String user, int offset) {
|
||||
return performLookup(LookupOptions.builder().user(user).time(offset).build());
|
||||
}
|
||||
|
||||
public static List<UsernameResult> performLookup(LookupOptions options) {
|
||||
List<UsernameResult> result = new ArrayList<>();
|
||||
|
||||
if (!Config.getGlobal().API_ENABLED) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = LookupOptions.builder().build();
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
if (connection == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Set<String> uuids = getUuids(connection, options.getUser());
|
||||
if (uuids == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Set<String> includedUuids = getUuids(connection, options.getUsers());
|
||||
if (includedUuids != null && includedUuids.isEmpty() && !options.getUsers().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Set<String> excludedUuids = getUuids(connection, options.getExcludeUsers());
|
||||
if (excludedUuids == null) {
|
||||
return result;
|
||||
}
|
||||
// A NULL in NOT IN would also exclude unrelated users.
|
||||
excludedUuids.remove(null);
|
||||
|
||||
int checkTime = 0;
|
||||
if (options.getTime() > 0) {
|
||||
checkTime = (int) (System.currentTimeMillis() / 1000L) - options.getTime();
|
||||
}
|
||||
|
||||
StringBuilder query = new StringBuilder("SELECT time,uuid," + ConfigHandler.databaseType.getUserColumn() + " FROM ");
|
||||
query.append(ConfigHandler.prefix).append("username_log WHERE time > ?");
|
||||
appendUuidFilter(query, uuids, false);
|
||||
appendUuidFilter(query, includedUuids, false);
|
||||
appendUuidFilter(query, excludedUuids, true);
|
||||
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
|
||||
if (options.hasLimit()) {
|
||||
query.append(" LIMIT ").append(options.getLimitCount()).append(" OFFSET ").append(options.getLimitOffset());
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
|
||||
int parameterIndex = 1;
|
||||
statement.setInt(parameterIndex++, checkTime);
|
||||
for (String uuid : uuids) {
|
||||
statement.setString(parameterIndex++, uuid);
|
||||
}
|
||||
if (includedUuids != null) {
|
||||
for (String uuid : includedUuids) {
|
||||
statement.setString(parameterIndex++, uuid);
|
||||
}
|
||||
}
|
||||
for (String uuid : excludedUuids) {
|
||||
statement.setString(parameterIndex++, uuid);
|
||||
}
|
||||
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(parseUsernameResult(connection, results));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void appendUuidFilter(StringBuilder query, Set<String> uuids, boolean excluded) {
|
||||
if (uuids == null || uuids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
query.append(excluded ? " AND uuid NOT IN (" : " AND uuid IN (");
|
||||
for (int index = 0; index < uuids.size(); index++) {
|
||||
if (index > 0) {
|
||||
query.append(",");
|
||||
}
|
||||
query.append("?");
|
||||
}
|
||||
query.append(")");
|
||||
}
|
||||
|
||||
private static Set<String> getUuids(Connection connection, String user) throws Exception {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
|
||||
if (user == null || user.isEmpty() || user.equalsIgnoreCase("#global")) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String userMatch = DatabaseUtils.caseInsensitiveEquals(ConfigHandler.databaseType.getUserColumn());
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT uuid FROM " + ConfigHandler.prefix + "user WHERE " + userMatch)) {
|
||||
statement.setString(1, user);
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(results.getString("uuid"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT uuid FROM " + ConfigHandler.prefix + "username_log WHERE " + userMatch)) {
|
||||
statement.setString(1, user);
|
||||
try (ResultSet results = statement.executeQuery()) {
|
||||
while (results.next()) {
|
||||
result.add(results.getString("uuid"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (looksLikeUuid(user)) {
|
||||
result.add(user);
|
||||
}
|
||||
|
||||
return result.isEmpty() ? null : result;
|
||||
}
|
||||
|
||||
private static Set<String> getUuids(Connection connection, List<String> users) throws Exception {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (String user : users) {
|
||||
Set<String> matches = getUuids(connection, user);
|
||||
if (matches != null) {
|
||||
if (matches.isEmpty()) {
|
||||
// An empty name or #global matches every user.
|
||||
return null;
|
||||
}
|
||||
result.addAll(matches);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean looksLikeUuid(String value) {
|
||||
return value.length() == 36 && value.charAt(8) == '-' && value.charAt(13) == '-' && value.charAt(18) == '-' && value.charAt(23) == '-';
|
||||
}
|
||||
|
||||
private static UsernameResult parseUsernameResult(Connection connection, ResultSet results) throws Exception {
|
||||
String uuid = results.getString("uuid");
|
||||
String username = results.getString("user");
|
||||
String player = UserStatement.getNameByUuid(connection, uuid);
|
||||
if (player == null) {
|
||||
player = username;
|
||||
}
|
||||
|
||||
return new UsernameResult(results.getLong("time"), uuid, username, player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.BlockUtils;
|
||||
import net.coreprotect.utility.BlockTypeUtils;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
import net.coreprotect.utility.StringUtils;
|
||||
|
||||
/**
|
||||
* Represents a logged block action.
|
||||
*/
|
||||
public class BlockResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int type;
|
||||
private final int data;
|
||||
private final String blockData;
|
||||
private final int actionId;
|
||||
private final int rolledBack;
|
||||
|
||||
public BlockResult(long time, String username, String world, int x, int y, int z, int type, int data, String blockData, int actionId, int rolledBack) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.blockData = blockData;
|
||||
this.actionId = actionId;
|
||||
this.rolledBack = rolledBack;
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return LookupActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public Material getType() {
|
||||
if (isEntityAction()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Material material = MaterialUtils.getType(type);
|
||||
if (material == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String typeName = material.name().toLowerCase(Locale.ROOT);
|
||||
return MaterialUtils.getType(StringUtils.nameFilter(typeName, data));
|
||||
}
|
||||
|
||||
public EntityType getEntityType() {
|
||||
if (!isEntityAction()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (actionId == LookupActions.ENTITY_KILL && type == 0) {
|
||||
return EntityType.PLAYER;
|
||||
}
|
||||
|
||||
return EntityUtils.getEntityType(type);
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
if (isEntityAction()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockData == null || blockData.isEmpty()) {
|
||||
Material material = getType();
|
||||
if (material != null) {
|
||||
return material.createBlockData();
|
||||
}
|
||||
|
||||
return BlockUtils.createBlockData(type);
|
||||
}
|
||||
|
||||
BlockData result = BlockTypeUtils.createBlockDataFromString(blockData);
|
||||
return result != null ? result : Bukkit.getServer().createBlockData(blockData);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public boolean isRolledBack() {
|
||||
return rolledBack == 1 || rolledBack == 3;
|
||||
}
|
||||
|
||||
private boolean isEntityAction() {
|
||||
return actionId == LookupActions.ENTITY_KILL || actionId == LookupActions.ENTITY_SPAWN;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
|
||||
/**
|
||||
* Represents the result of a container action lookup with typed fields.
|
||||
*/
|
||||
public class ContainerResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int type;
|
||||
private final int data;
|
||||
private final int amount;
|
||||
private final byte[] metadata;
|
||||
private final int action;
|
||||
private final int rolledBack;
|
||||
|
||||
/**
|
||||
* Creates a new ContainerResult with the specified parameters.
|
||||
*
|
||||
* @param time The timestamp in seconds
|
||||
* @param username The username of the player who performed the action
|
||||
* @param world The world name where the action occurred
|
||||
* @param x The X coordinate
|
||||
* @param y The Y coordinate
|
||||
* @param z The Z coordinate
|
||||
* @param type The material type ID
|
||||
* @param data The data value
|
||||
* @param amount The amount of items
|
||||
* @param metadata The item metadata
|
||||
* @param action The action ID
|
||||
* @param rolledBack Whether the action has been rolled back
|
||||
*/
|
||||
public ContainerResult(long time, String username, String world, int x, int y, int z,
|
||||
int type, int data, int amount, byte[] metadata, int action, int rolledBack) {
|
||||
this.time = time;
|
||||
this.username = username != null ? username : "";
|
||||
this.world = world != null ? world : "";
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.amount = amount;
|
||||
this.metadata = metadata != null ? metadata.clone() : null;
|
||||
this.action = action;
|
||||
this.rolledBack = rolledBack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the action ID.
|
||||
*
|
||||
* @return The action ID
|
||||
*/
|
||||
public int getActionId() {
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the action as a human-readable string.
|
||||
*
|
||||
* @return "remove", "add", or "unknown"
|
||||
*/
|
||||
public String getActionString() {
|
||||
return ItemTransactionActions.getActionString(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data value.
|
||||
*
|
||||
* @return The data value
|
||||
*/
|
||||
public int getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount of items.
|
||||
*
|
||||
* @return The amount
|
||||
*/
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the username of the player who performed the action.
|
||||
*
|
||||
* @return The player username
|
||||
*/
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp in milliseconds.
|
||||
*
|
||||
* @return The timestamp in milliseconds
|
||||
*/
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the material type of the item.
|
||||
*
|
||||
* @return The Material, or null if unknown
|
||||
*/
|
||||
public Material getType() {
|
||||
return MaterialUtils.getType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the X coordinate.
|
||||
*
|
||||
* @return The X coordinate
|
||||
*/
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Y coordinate.
|
||||
*
|
||||
* @return The Y coordinate
|
||||
*/
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Z coordinate.
|
||||
*
|
||||
* @return The Z coordinate
|
||||
*/
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item metadata.
|
||||
*
|
||||
* @return A copy of the metadata array, or null if no metadata
|
||||
*/
|
||||
public byte[] getMetadata() {
|
||||
return metadata != null ? metadata.clone() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this action has been rolled back.
|
||||
*
|
||||
* @return true if rolled back, false otherwise
|
||||
*/
|
||||
public boolean isRolledBack() {
|
||||
return rolledBack == 1 || rolledBack == 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the world name where the action occurred.
|
||||
*
|
||||
* @return The world name
|
||||
*/
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
/**
|
||||
* Common fields shared by typed CoreProtect lookup results.
|
||||
*/
|
||||
public interface CoreProtectResult {
|
||||
int getActionId();
|
||||
|
||||
String getActionString();
|
||||
|
||||
String getPlayer();
|
||||
|
||||
long getTimestamp();
|
||||
|
||||
int getX();
|
||||
|
||||
int getY();
|
||||
|
||||
int getZ();
|
||||
|
||||
String worldName();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
|
||||
/**
|
||||
* Represents a logged entity spawn or kill at its original event location.
|
||||
*/
|
||||
public class EntityResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int type;
|
||||
private final int actionId;
|
||||
private final int rolledBack;
|
||||
|
||||
public EntityResult(long time, String username, String world, int x, int y, int z, int type, int actionId, int rolledBack) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.type = type;
|
||||
this.actionId = actionId;
|
||||
this.rolledBack = rolledBack;
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return LookupActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public EntityType getEntityType() {
|
||||
if (actionId == LookupActions.ENTITY_KILL && type == 0) {
|
||||
return EntityType.PLAYER;
|
||||
}
|
||||
return EntityUtils.getEntityType(type);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public boolean isRolledBack() {
|
||||
return rolledBack == 1 || rolledBack == 3;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
import net.coreprotect.model.item.InventorySources;
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
import net.coreprotect.utility.ItemUtils;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
|
||||
/**
|
||||
* Represents a normalized player inventory transaction.
|
||||
*/
|
||||
public class InventoryResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int type;
|
||||
private final int data;
|
||||
private final int amount;
|
||||
private final byte[] metadata;
|
||||
private final int actionId;
|
||||
private final int transactionActionId;
|
||||
private final int rolledBack;
|
||||
private final int sourceId;
|
||||
|
||||
public InventoryResult(long time, String username, String world, int x, int y, int z, int type, int data, int amount, byte[] metadata, int transactionActionId, int rolledBack, int sourceId) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.amount = amount;
|
||||
this.metadata = metadata != null ? metadata.clone() : null;
|
||||
this.transactionActionId = transactionActionId;
|
||||
this.rolledBack = rolledBack;
|
||||
this.sourceId = sourceId;
|
||||
this.actionId = ItemTransactionActions.getInventoryActionId(transactionActionId);
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return ItemTransactionActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public byte[] getMetadata() {
|
||||
return metadata != null ? metadata.clone() : null;
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public int getSourceId() {
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return InventorySources.getSourceString(sourceId);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public int getTransactionActionId() {
|
||||
return transactionActionId;
|
||||
}
|
||||
|
||||
public String getTransactionActionString() {
|
||||
return ItemTransactionActions.getActionString(transactionActionId);
|
||||
}
|
||||
|
||||
public Material getType() {
|
||||
Material material = MaterialUtils.getType(type);
|
||||
if (sourceId == InventorySources.BLOCK) {
|
||||
return ItemUtils.itemFilter(material, true);
|
||||
}
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public boolean isRolledBack() {
|
||||
return rolledBack == 2 || rolledBack == 3;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
|
||||
/**
|
||||
* Represents a logged world item transaction.
|
||||
*/
|
||||
public class ItemResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int type;
|
||||
private final int amount;
|
||||
private final byte[] metadata;
|
||||
private final int actionId;
|
||||
private final int rolledBack;
|
||||
|
||||
public ItemResult(long time, String username, String world, int x, int y, int z, int type, int amount, byte[] metadata, int actionId, int rolledBack) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.type = type;
|
||||
this.amount = amount;
|
||||
this.metadata = metadata != null ? metadata.clone() : null;
|
||||
this.actionId = actionId;
|
||||
this.rolledBack = rolledBack;
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return ItemTransactionActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public byte[] getMetadata() {
|
||||
return metadata != null ? metadata.clone() : null;
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public Material getType() {
|
||||
return MaterialUtils.getType(type);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public boolean isRolledBack() {
|
||||
return rolledBack == 1 || rolledBack == 3;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
|
||||
/**
|
||||
* Represents a logged player chat message or command.
|
||||
*/
|
||||
public class MessageResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final String message;
|
||||
private final int actionId;
|
||||
|
||||
public MessageResult(long time, String username, String world, int x, int y, int z, String message, int actionId) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.message = message;
|
||||
this.actionId = actionId;
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return LookupActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,20 @@ import java.util.Locale;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import net.coreprotect.api.SessionLookup;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.model.action.SessionActions;
|
||||
import net.coreprotect.model.item.InventorySources;
|
||||
import net.coreprotect.utility.BlockUtils;
|
||||
import net.coreprotect.utility.BlockTypeUtils;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
import net.coreprotect.utility.StringUtils;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
|
||||
public class ParseResult {
|
||||
public class ParseResult implements CoreProtectResult {
|
||||
private final String[] parse;
|
||||
|
||||
public ParseResult(String[] data) {
|
||||
@@ -26,31 +32,10 @@ public class ParseResult {
|
||||
public String getActionString() {
|
||||
int actionID = Integer.parseInt(parse[7]);
|
||||
if (parse.length < 13 && Integer.parseInt(parse[6]) == SessionLookup.ID) {
|
||||
switch (actionID) {
|
||||
case 0:
|
||||
return "logout";
|
||||
case 1:
|
||||
return "login";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
return SessionActions.getActionString(actionID);
|
||||
}
|
||||
|
||||
String result = "unknown";
|
||||
if (actionID == 0) {
|
||||
result = "break";
|
||||
}
|
||||
else if (actionID == 1) {
|
||||
result = "place";
|
||||
}
|
||||
else if (actionID == 2) {
|
||||
result = "click";
|
||||
}
|
||||
else if (actionID == 3) {
|
||||
result = "kill";
|
||||
}
|
||||
|
||||
return result;
|
||||
return LookupActions.getActionString(actionID);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@@ -77,17 +62,18 @@ public class ParseResult {
|
||||
}
|
||||
|
||||
int actionID = this.getActionId();
|
||||
if (isEntityAction(actionID)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int type = Integer.parseInt(parse[5]);
|
||||
String typeName;
|
||||
|
||||
if (actionID == 3) {
|
||||
typeName = EntityUtils.getEntityType(type).name();
|
||||
}
|
||||
else {
|
||||
typeName = MaterialUtils.getType(type).name().toLowerCase(Locale.ROOT);
|
||||
typeName = StringUtils.nameFilter(typeName, this.getData());
|
||||
Material material = MaterialUtils.getType(type);
|
||||
if (material == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String typeName = material.name().toLowerCase(Locale.ROOT);
|
||||
typeName = StringUtils.nameFilter(typeName, this.getData());
|
||||
return MaterialUtils.getType(typeName);
|
||||
}
|
||||
|
||||
@@ -96,11 +82,35 @@ public class ParseResult {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEntityAction(this.getActionId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String blockData = parse[12];
|
||||
if (blockData == null || blockData.length() == 0) {
|
||||
return getType().createBlockData();
|
||||
Material type = getType();
|
||||
if (type != null) {
|
||||
return type.createBlockData();
|
||||
}
|
||||
|
||||
return BlockUtils.createBlockData(Integer.parseInt(parse[5]));
|
||||
}
|
||||
return Bukkit.getServer().createBlockData(blockData);
|
||||
|
||||
BlockData result = BlockTypeUtils.createBlockDataFromString(blockData);
|
||||
return result != null ? result : Bukkit.getServer().createBlockData(blockData);
|
||||
}
|
||||
|
||||
public EntityType getEntityType() {
|
||||
if (parse.length < 13 || !isEntityAction(this.getActionId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int type = Integer.parseInt(parse[5]);
|
||||
if (this.getActionId() == LookupActions.ENTITY_KILL && type == 0) {
|
||||
return EntityType.PLAYER;
|
||||
}
|
||||
|
||||
return EntityUtils.getEntityType(type);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
@@ -126,4 +136,11 @@ public class ParseResult {
|
||||
public String worldName() {
|
||||
return WorldUtils.getWorldName(Integer.parseInt(parse.length < 13 ? parse[5] : parse[9]));
|
||||
}
|
||||
|
||||
private boolean isEntityAction(int actionId) {
|
||||
if (actionId == LookupActions.ENTITY_KILL || actionId == LookupActions.ENTITY_SPAWN) {
|
||||
return true;
|
||||
}
|
||||
return actionId == LookupActions.INTERACTION && parse.length > 14 && parse[13] != null && Integer.parseInt(parse[13]) == InventorySources.ENTITY_INTERACTION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import net.coreprotect.model.action.SessionActions;
|
||||
|
||||
/**
|
||||
* Represents a logged player login or logout session event.
|
||||
*/
|
||||
public class SessionResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int actionId;
|
||||
|
||||
public SessionResult(long time, String username, String world, int x, int y, int z, int actionId) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.actionId = actionId;
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return SessionActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import net.coreprotect.model.action.SignActions;
|
||||
import net.coreprotect.utility.BlockUtils;
|
||||
|
||||
/**
|
||||
* Represents logged sign text.
|
||||
*/
|
||||
public class SignResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String username;
|
||||
private final String world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int actionId;
|
||||
private final int color;
|
||||
private final int colorSecondary;
|
||||
private final int data;
|
||||
private final boolean waxed;
|
||||
private final boolean front;
|
||||
private final String[] lines;
|
||||
|
||||
public SignResult(long time, String username, String world, int x, int y, int z, int actionId, int color, int colorSecondary, int data, boolean waxed, boolean front, String[] lines) {
|
||||
this.time = time;
|
||||
this.username = username;
|
||||
this.world = world;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.actionId = actionId;
|
||||
this.color = color;
|
||||
this.colorSecondary = colorSecondary;
|
||||
this.data = data;
|
||||
this.waxed = waxed;
|
||||
this.front = front;
|
||||
this.lines = lines.clone();
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return SignActions.getActionString(actionId);
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public int getColorSecondary() {
|
||||
return colorSecondary;
|
||||
}
|
||||
|
||||
public int getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getLine(int line) {
|
||||
if (line < 0 || line >= lines.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return lines[line];
|
||||
}
|
||||
|
||||
public String[] getLines() {
|
||||
return lines.clone();
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
StringBuilder message = new StringBuilder();
|
||||
int startLine = front ? 0 : 4;
|
||||
int endLine = front ? 4 : 8;
|
||||
|
||||
for (int index = startLine; index < endLine; index++) {
|
||||
String line = lines[index];
|
||||
if (line != null && !line.isEmpty()) {
|
||||
message.append(line);
|
||||
if (!line.endsWith(" ")) {
|
||||
message.append(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public boolean isBackGlowing() {
|
||||
return BlockUtils.isSideGlowing(false, data);
|
||||
}
|
||||
|
||||
public boolean isFront() {
|
||||
return front;
|
||||
}
|
||||
|
||||
public boolean isFrontGlowing() {
|
||||
return BlockUtils.isSideGlowing(true, data);
|
||||
}
|
||||
|
||||
public boolean isWaxed() {
|
||||
return waxed;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package net.coreprotect.api.result;
|
||||
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
|
||||
/**
|
||||
* Represents a logged username used by a player UUID.
|
||||
*/
|
||||
public class UsernameResult implements CoreProtectResult {
|
||||
private final long time;
|
||||
private final String uuid;
|
||||
private final String username;
|
||||
private final String player;
|
||||
|
||||
public UsernameResult(long time, String uuid, String username, String player) {
|
||||
this.time = time;
|
||||
this.uuid = uuid;
|
||||
this.username = username;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public int getActionId() {
|
||||
return LookupActions.USERNAME;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return LookupActions.getActionString(getActionId());
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return time * 1000L;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public String worldName() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,27 @@
|
||||
package net.coreprotect.bukkit;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.bukkit.Art;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -22,6 +32,11 @@ import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Painting;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
@@ -56,35 +71,56 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
public static final int BUKKIT_V1_19 = 19;
|
||||
public static final int BUKKIT_V1_20 = 20;
|
||||
public static final int BUKKIT_V1_21 = 21;
|
||||
public static final int BUKKIT_V1_21_5 = 21005;
|
||||
public static final int BUKKIT_V1_21_11 = 21011;
|
||||
public static final int BUKKIT_V26_0 = 26000;
|
||||
public static final int BUKKIT_V26_1 = 26010;
|
||||
public static final int BUKKIT_V26_2 = 26020;
|
||||
|
||||
public static int getAdapterVersion(int major, int minor) {
|
||||
return getAdapterVersion(major, minor, 0);
|
||||
}
|
||||
|
||||
public static int getAdapterVersion(int major, int minor, int patch) {
|
||||
if (major == 1) {
|
||||
return minor == 21 && patch >= 5 ? (minor * 1000) + patch : minor;
|
||||
}
|
||||
|
||||
return (major * 1000) + (minor * 10) + patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the appropriate Bukkit adapter based on the server version.
|
||||
* This method should be called during plugin initialization.
|
||||
*/
|
||||
public static void loadAdapter() {
|
||||
switch (ConfigHandler.SERVER_VERSION) {
|
||||
case BUKKIT_V1_13:
|
||||
case BUKKIT_V1_14:
|
||||
case BUKKIT_V1_15:
|
||||
case BUKKIT_V1_16:
|
||||
ADAPTER = new BukkitAdapter();
|
||||
break;
|
||||
case BUKKIT_V1_17:
|
||||
ADAPTER = new Bukkit_v1_17();
|
||||
break;
|
||||
case BUKKIT_V1_18:
|
||||
ADAPTER = new Bukkit_v1_18();
|
||||
break;
|
||||
case BUKKIT_V1_19:
|
||||
ADAPTER = new Bukkit_v1_19();
|
||||
break;
|
||||
case BUKKIT_V1_20:
|
||||
ADAPTER = new Bukkit_v1_20();
|
||||
break;
|
||||
case BUKKIT_V1_21:
|
||||
default:
|
||||
ADAPTER = new Bukkit_v1_21();
|
||||
break;
|
||||
int bukkitVersion = ConfigHandler.SERVER_VERSION;
|
||||
if (bukkitVersion >= BUKKIT_V26_2) {
|
||||
ADAPTER = new Bukkit_v26_2();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_21_11) {
|
||||
ADAPTER = new Bukkit_v1_21_11();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_21_5) {
|
||||
ADAPTER = new Bukkit_v1_21_5();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_21) {
|
||||
ADAPTER = new Bukkit_v1_21();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_20) {
|
||||
ADAPTER = new Bukkit_v1_20();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_19) {
|
||||
ADAPTER = new Bukkit_v1_19();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_18) {
|
||||
ADAPTER = new Bukkit_v1_18();
|
||||
}
|
||||
else if (bukkitVersion >= BUKKIT_V1_17) {
|
||||
ADAPTER = new Bukkit_v1_17();
|
||||
}
|
||||
else {
|
||||
ADAPTER = new BukkitAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +139,11 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
|
||||
// -------------------- Entity methods --------------------
|
||||
|
||||
@Override
|
||||
public boolean isChunkEntitiesLoaded(Chunk chunk) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getEntityMeta(LivingEntity entity, List<Object> info) {
|
||||
return false;
|
||||
@@ -113,6 +154,78 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses registry-backed entity variants without exposing the entity interface to legacy
|
||||
* server bytecode remapping.
|
||||
*/
|
||||
public static boolean getRegistryVariant(BukkitInterface adapter, Entity entity, List<Object> info, String getterName) {
|
||||
try {
|
||||
Object variant = entity.getClass().getMethod(getterName).invoke(entity);
|
||||
if (variant == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
info.add(adapter.getRegistryKey(variant));
|
||||
return true;
|
||||
}
|
||||
catch (ReflectiveOperationException | LinkageError e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores registry-backed entity variants without exposing the entity interface to legacy
|
||||
* server bytecode remapping.
|
||||
*/
|
||||
public static boolean setRegistryVariant(BukkitInterface adapter, Entity entity, Object value, String getterName, String setterName) {
|
||||
try {
|
||||
Class<?> variantClass = entity.getClass().getMethod(getterName).getReturnType();
|
||||
Object variant = value instanceof String ? adapter.getRegistryValue((String) value, variantClass) : value;
|
||||
if (!variantClass.isInstance(variant)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entity.getClass().getMethod(setterName, variantClass).invoke(entity, variant);
|
||||
return true;
|
||||
}
|
||||
catch (ReflectiveOperationException | LinkageError e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getLegacyEnumValue(Class<?> type, String name) {
|
||||
if (!type.isEnum()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
Object value = Enum.valueOf((Class<? extends Enum>) type.asSubclass(Enum.class), name);
|
||||
return value;
|
||||
}
|
||||
catch (IllegalArgumentException | LinkageError exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMerchantRecipeMeta(MerchantRecipe recipe, List<Object> recipeData) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMerchantRecipeMeta(MerchantRecipe recipe, List<?> recipeData) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshVillagerBrain(Villager villager) {
|
||||
try {
|
||||
Object handle = villager.getClass().getMethod("getHandle").invoke(villager);
|
||||
Object level = villager.getWorld().getClass().getMethod("getHandle").invoke(villager.getWorld());
|
||||
invokeSingleArgumentMethod(handle, "refreshBrain", level);
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getWolfVariant(org.bukkit.entity.Wolf wolf, List<Object> info) {
|
||||
// Base implementation does nothing - Wolf variants only exist in 1.21+
|
||||
@@ -145,6 +258,11 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getItemName(ItemMeta itemMeta) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Material getPlantSeeds(Material material) {
|
||||
switch (material) {
|
||||
@@ -182,6 +300,24 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return itemStack;
|
||||
}
|
||||
|
||||
private static void invokeSingleArgumentMethod(Object target, String methodName, Object argument) {
|
||||
if (target == null || argument == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Method method : target.getClass().getMethods()) {
|
||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
if (method.getName().equals(methodName) && parameterTypes.length == 1 && parameterTypes[0].isInstance(argument)) {
|
||||
try {
|
||||
method.invoke(target, argument);
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Block methods --------------------
|
||||
|
||||
@Override
|
||||
@@ -221,6 +357,21 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return Material.AIR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBlockType(String key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData createBlockData(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData createBlockDataFromString(String blockData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvisible(Material material) {
|
||||
return BlockUtils.isAir(material);
|
||||
@@ -248,6 +399,43 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return ItemFrame.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Entity> T spawn(World world, Location location, Class<T> entityClass, Consumer<? super T> function) {
|
||||
// Bukkit 1.20.1 and earlier only have World#spawn(Location, Class, org.bukkit.util.Consumer), which is deprecated for removal in the current API
|
||||
try {
|
||||
Class<?> consumerClass = Class.forName("org.bukkit.util.Consumer");
|
||||
Object consumer = Proxy.newProxyInstance(consumerClass.getClassLoader(), new Class<?>[] { consumerClass }, (proxy, method, args) -> {
|
||||
if (method.getDeclaringClass() == Object.class) {
|
||||
switch (method.getName()) {
|
||||
case "equals":
|
||||
return proxy == args[0];
|
||||
case "hashCode":
|
||||
return System.identityHashCode(proxy);
|
||||
default:
|
||||
return consumerClass.getName();
|
||||
}
|
||||
}
|
||||
function.accept(entityClass.cast(args[0]));
|
||||
return null;
|
||||
});
|
||||
Method spawnMethod = World.class.getMethod("spawn", Location.class, Class.class, consumerClass);
|
||||
return entityClass.cast(spawnMethod.invoke(world, location, entityClass, consumer));
|
||||
}
|
||||
catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
}
|
||||
if (cause instanceof Error) {
|
||||
throw (Error) cause;
|
||||
}
|
||||
throw new IllegalStateException(cause);
|
||||
}
|
||||
catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDecoratedPot(Material material) {
|
||||
return false;
|
||||
@@ -306,6 +494,11 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getSignInteractionSide(Sign sign, Player player) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlowing(Sign sign, boolean isFront) {
|
||||
return false;
|
||||
@@ -316,6 +509,17 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldLogExplosion(Event event){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Material getExplodedBlock(BlockExplodeEvent event){
|
||||
// accoding to the Bukkit docs this will always return air
|
||||
return event.getBlock().getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGlowing(Sign sign, boolean isFront, boolean isGlowing) {
|
||||
// Base implementation does nothing
|
||||
@@ -356,18 +560,58 @@ public class BukkitAdapter implements BukkitInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPaintingArtKey(Painting painting) {
|
||||
try {
|
||||
return painting.getArt().name();
|
||||
}
|
||||
catch (IncompatibleClassChangeError e) {
|
||||
return painting.getArt().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Art getPaintingArt(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Art.getByName(name.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCrafter(InventoryType type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isBundle(Material material) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCopperChest(Material material) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShelf(Material material){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Location> getShelfInteractionLocations(Block block, BlockFace blockFace) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Material> copperChestMaterials() {
|
||||
return EMPTY_SET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Material> shelfMaterials() {
|
||||
return EMPTY_SET;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,15 @@ package net.coreprotect.bukkit;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.bukkit.Art;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -14,6 +19,11 @@ import org.bukkit.entity.Arrow;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Painting;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
@@ -74,6 +84,33 @@ public interface BukkitInterface {
|
||||
*/
|
||||
Material getBucketContents(Material material);
|
||||
|
||||
/**
|
||||
* Checks whether the runtime supports and contains a block type key.
|
||||
*
|
||||
* @param key
|
||||
* The namespaced block key
|
||||
* @return true if the block type exists, false otherwise
|
||||
*/
|
||||
boolean hasBlockType(String key);
|
||||
|
||||
/**
|
||||
* Creates default block data for a block type key.
|
||||
*
|
||||
* @param key
|
||||
* The namespaced block key
|
||||
* @return The block data, or null if unsupported/unavailable
|
||||
*/
|
||||
BlockData createBlockData(String key);
|
||||
|
||||
/**
|
||||
* Creates block data from a serialized block data string.
|
||||
*
|
||||
* @param blockData
|
||||
* The serialized block data
|
||||
* @return The block data, or null if unsupported/unavailable
|
||||
*/
|
||||
BlockData createBlockDataFromString(String blockData);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Material type checking methods
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -132,6 +169,19 @@ public interface BukkitInterface {
|
||||
*/
|
||||
boolean isChiseledBookshelf(Material material);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a material is a shelf of any wood kind.
|
||||
*
|
||||
* @param material
|
||||
* The material to check
|
||||
* @return true if the material is a shelf, false otherwise
|
||||
*/
|
||||
boolean isShelf(Material material);
|
||||
|
||||
List<Location> getShelfInteractionLocations(Block block, BlockFace blockFace);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a material is a bookshelf book.
|
||||
*
|
||||
@@ -141,6 +191,17 @@ public interface BukkitInterface {
|
||||
*/
|
||||
boolean isBookshelfBook(Material material);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a material is a bundle.
|
||||
*
|
||||
* @param material
|
||||
* The material to check
|
||||
* @return true if the material is a bundle, false otherwise
|
||||
*/
|
||||
boolean isBundle(Material material);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the seeds material for a plant material.
|
||||
*
|
||||
@@ -193,6 +254,8 @@ public interface BukkitInterface {
|
||||
*/
|
||||
boolean setItemMeta(Material rowType, ItemStack itemstack, List<Map<String, Object>> map);
|
||||
|
||||
String getItemName(ItemMeta itemMeta);
|
||||
|
||||
/**
|
||||
* Gets a book from a chiseled bookshelf.
|
||||
*
|
||||
@@ -219,6 +282,15 @@ public interface BukkitInterface {
|
||||
// Entity methods
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether a loaded chunk's entity data is ready to query.
|
||||
*
|
||||
* @param chunk
|
||||
* The loaded chunk
|
||||
* @return true when entity data is ready
|
||||
*/
|
||||
boolean isChunkEntitiesLoaded(Chunk chunk);
|
||||
|
||||
/**
|
||||
* Gets metadata from a living entity.
|
||||
*
|
||||
@@ -243,6 +315,12 @@ public interface BukkitInterface {
|
||||
*/
|
||||
boolean setEntityMeta(Entity entity, Object value, int count);
|
||||
|
||||
void addMerchantRecipeMeta(MerchantRecipe recipe, List<Object> recipeData);
|
||||
|
||||
void setMerchantRecipeMeta(MerchantRecipe recipe, List<?> recipeData);
|
||||
|
||||
void refreshVillagerBrain(Villager villager);
|
||||
|
||||
/**
|
||||
* Gets the wolf variant and adds it to the info list.
|
||||
* Only implemented in Minecraft 1.21+.
|
||||
@@ -301,6 +379,21 @@ public interface BukkitInterface {
|
||||
*/
|
||||
Class<?> getFrameClass(Material material);
|
||||
|
||||
/**
|
||||
* Spawns an entity, running a function on it before it is added to the world.
|
||||
*
|
||||
* @param world
|
||||
* The world
|
||||
* @param location
|
||||
* The spawn location
|
||||
* @param entityClass
|
||||
* The entity class
|
||||
* @param function
|
||||
* The function to run before the entity is added
|
||||
* @return The spawned entity
|
||||
*/
|
||||
<T extends Entity> T spawn(World world, Location location, Class<T> entityClass, Consumer<? super T> function);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Sign methods
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -402,6 +495,29 @@ public interface BukkitInterface {
|
||||
*/
|
||||
boolean isSignFront(SignChangeEvent event);
|
||||
|
||||
Boolean getSignInteractionSide(Sign sign, Player player);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether an explosion event should be logged or not. (i.e. wind charge explosions)
|
||||
*
|
||||
* @param event
|
||||
* The explosion event (Block or Entity ExplodeEvent)
|
||||
* @return true if the explosion should affect blocks
|
||||
*/
|
||||
boolean shouldLogExplosion(Event event);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the material of the block that exploded
|
||||
*
|
||||
* @param event
|
||||
* The block explosion event
|
||||
* @return the material of the block that caused the explosion
|
||||
*/
|
||||
Material getExplodedBlock(BlockExplodeEvent event);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Registry methods
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -426,6 +542,10 @@ public interface BukkitInterface {
|
||||
*/
|
||||
Object getRegistryValue(String key, Object tClass);
|
||||
|
||||
String getPaintingArtKey(Painting painting);
|
||||
|
||||
Art getPaintingArt(String name);
|
||||
|
||||
/**
|
||||
* Parses a legacy material name.
|
||||
*
|
||||
@@ -441,4 +561,6 @@ public interface BukkitInterface {
|
||||
|
||||
Set<Material> copperChestMaterials();
|
||||
|
||||
Set<Material> shelfMaterials();
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
@@ -110,9 +111,16 @@ public class Bukkit_v1_17 extends BukkitAdapter {
|
||||
if (entity instanceof Axolotl) {
|
||||
Axolotl axolotl = (Axolotl) entity;
|
||||
if (count == 0) {
|
||||
org.bukkit.entity.Axolotl.Variant variant = (org.bukkit.entity.Axolotl.Variant) value;
|
||||
axolotl.setVariant(variant);
|
||||
return true;
|
||||
try {
|
||||
org.bukkit.entity.Axolotl.Variant variant = value instanceof String
|
||||
? org.bukkit.entity.Axolotl.Variant.valueOf((String) value)
|
||||
: (org.bukkit.entity.Axolotl.Variant) value;
|
||||
axolotl.setVariant(variant);
|
||||
return true;
|
||||
}
|
||||
catch (IllegalArgumentException | ClassCastException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (entity instanceof Goat) {
|
||||
@@ -152,9 +160,15 @@ public class Bukkit_v1_17 extends BukkitAdapter {
|
||||
return super.getItemMeta(itemMeta, list, metadata, slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBundle(Material material) {
|
||||
return material == Material.BUNDLE;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean setItemMeta(Material rowType, ItemStack itemstack, List<Map<String, Object>> map) {
|
||||
if (rowType == Material.BUNDLE) {
|
||||
if (BukkitAdapter.ADAPTER.isBundle(rowType)) {
|
||||
BundleMeta meta = (BundleMeta) itemstack.getItemMeta();
|
||||
for (Map<String, Object> itemData : map) {
|
||||
ItemStack itemStack = ItemUtils.unserializeItemStack(itemData);
|
||||
@@ -240,4 +254,9 @@ public class Bukkit_v1_17 extends BukkitAdapter {
|
||||
public boolean isInvisible(Material material) {
|
||||
return material.isAir() || material == Material.LIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkEntitiesLoaded(Chunk chunk) {
|
||||
return chunk.isEntitiesLoaded();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package net.coreprotect.bukkit;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Entity;
|
||||
@@ -196,7 +197,16 @@ public class Bukkit_v1_19 extends Bukkit_v1_18 {
|
||||
if (count == 0) {
|
||||
// Convert string registry key to variant if needed
|
||||
if (value instanceof String) {
|
||||
value = BukkitAdapter.ADAPTER.getRegistryValue((String) value, Frog.Variant.class);
|
||||
String key = (String) value;
|
||||
value = BukkitAdapter.ADAPTER.getRegistryValue(key, Frog.Variant.class);
|
||||
if (value == null) {
|
||||
int separator = key.indexOf(':');
|
||||
String name = separator < 0 ? key : key.substring(separator + 1).toUpperCase(Locale.ROOT).replace('.', '_').replace('-', '_');
|
||||
value = BukkitAdapter.getLegacyEnumValue(Frog.Variant.class, name);
|
||||
}
|
||||
if (!(value instanceof Frog.Variant)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
frog.setVariant((Frog.Variant) value);
|
||||
return true;
|
||||
|
||||
@@ -2,20 +2,29 @@ package net.coreprotect.bukkit;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.ChiseledBookshelf;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.block.sign.Side;
|
||||
import org.bukkit.entity.Arrow;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.MerchantRecipe;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionType;
|
||||
@@ -34,7 +43,10 @@ import net.coreprotect.model.BlockGroup;
|
||||
public class Bukkit_v1_20 extends Bukkit_v1_19 {
|
||||
|
||||
private Boolean hasClickedPosition;
|
||||
private Boolean hasSignTargetSide;
|
||||
private Boolean hasBasePotionType;
|
||||
private Boolean hasItemName;
|
||||
private Boolean hasSpawnFunction;
|
||||
|
||||
/**
|
||||
* Initializes the Bukkit_v1_20 adapter with 1.20-specific block groups and mappings.
|
||||
@@ -115,6 +127,21 @@ public class Bukkit_v1_20 extends Bukkit_v1_19 {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getSignInteractionSide(Sign sign, Player player) {
|
||||
if (hasSignTargetSide == null) {
|
||||
try {
|
||||
Sign.class.getMethod("getTargetSide", Player.class);
|
||||
hasSignTargetSide = true;
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
hasSignTargetSide = false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasSignTargetSide ? sign.getTargetSide(player) == sign.getSide(Side.FRONT) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGlowing(Sign sign, boolean isFront, boolean isGlowing) {
|
||||
Side side = isFront ? Side.FRONT : Side.BACK;
|
||||
@@ -261,6 +288,37 @@ public class Bukkit_v1_20 extends Bukkit_v1_19 {
|
||||
return event.getSide().equals(Side.FRONT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getItemName(ItemMeta itemMeta) {
|
||||
if (hasItemName == null) {
|
||||
try {
|
||||
ItemMeta.class.getMethod("getItemName");
|
||||
hasItemName = true;
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
hasItemName = false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasItemName && itemMeta.hasItemName() ? itemMeta.getItemName() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Entity> T spawn(World world, Location location, Class<T> entityClass, Consumer<? super T> function) {
|
||||
if (hasSpawnFunction == null) {
|
||||
try {
|
||||
// Check if World#spawn accepts java.util.function.Consumer (Bukkit 1.20.2+)
|
||||
World.class.getMethod("spawn", Location.class, Class.class, Consumer.class);
|
||||
hasSpawnFunction = true;
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
hasSpawnFunction = false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasSpawnFunction ? world.spawn(location, entityClass, function) : super.spawn(world, location, entityClass, function);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack getArrowMeta(Arrow arrow, ItemStack itemStack) {
|
||||
try {
|
||||
@@ -297,4 +355,23 @@ public class Bukkit_v1_20 extends Bukkit_v1_19 {
|
||||
return super.getArrowMeta(arrow, itemStack);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Material getExplodedBlock(BlockExplodeEvent event){
|
||||
// accoding to the Bukkit docs this will always return air
|
||||
return event.getExplodedBlockState().getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMerchantRecipeMeta(MerchantRecipe recipe, List<Object> recipeData) {
|
||||
recipeData.add(recipe.getDemand());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMerchantRecipeMeta(MerchantRecipe recipe, List<?> recipeData) {
|
||||
if (recipeData.size() > 7 && recipeData.get(7) instanceof Number) {
|
||||
recipe.setDemand(((Number) recipeData.get(7)).intValue());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +1,36 @@
|
||||
package net.coreprotect.bukkit;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bukkit.Art;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ExplosionResult;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.BlockType;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Painting;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
|
||||
|
||||
import net.coreprotect.model.BlockGroup;
|
||||
import net.coreprotect.utility.BlockTypeUtils;
|
||||
|
||||
/**
|
||||
* Bukkit adapter implementation for Minecraft 1.21.
|
||||
@@ -23,9 +40,11 @@ import net.coreprotect.model.BlockGroup;
|
||||
* - Registry handling for named objects
|
||||
* - Updated interaction blocks
|
||||
*/
|
||||
public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
public class Bukkit_v1_21 extends Bukkit_v1_20 {
|
||||
|
||||
public static Set<Material> COPPER_CHESTS = new HashSet<>(Arrays.asList());
|
||||
public static Set<Material> SHELVES = new HashSet<>(Arrays.asList());
|
||||
public static Set<Material> BUNDLES = new HashSet<>(Arrays.asList());
|
||||
|
||||
/**
|
||||
* Initializes the Bukkit_v1_21 adapter with 1.21-specific block groups and mappings.
|
||||
@@ -34,9 +53,11 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
public Bukkit_v1_21() {
|
||||
initializeBlockGroups();
|
||||
initializeTrapdoorBlocks();
|
||||
initializeBundles();
|
||||
BlockGroup.INTERACT_BLOCKS.addAll(copperChestMaterials());
|
||||
BlockGroup.CONTAINERS.addAll(copperChestMaterials());
|
||||
BlockGroup.UPDATE_STATE.addAll(copperChestMaterials());
|
||||
BlockGroup.CONTAINERS.addAll(shelfMaterials());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +87,24 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the bundles group to enable the ability to roll back dyed bundles correctly.
|
||||
* It needs to check whether dyed bundles exist because they were added in 1.21.2.
|
||||
*/
|
||||
public void initializeBundles(){
|
||||
if (BUNDLES.isEmpty()) {
|
||||
Tag<Material> bundleTag = getMaterialTag(Tag.REGISTRY_ITEMS, "bundles");
|
||||
if (bundleTag != null) {
|
||||
BUNDLES.addAll(bundleTag.getValues());
|
||||
}
|
||||
|
||||
if (BUNDLES.isEmpty()) {
|
||||
addMaterialsEndingWith(BUNDLES, "_BUNDLE");
|
||||
}
|
||||
BUNDLES.add(Material.BUNDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to add a block to a block group if it's not already present.
|
||||
*
|
||||
@@ -80,6 +119,43 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBlockType(String key) {
|
||||
return getBlockType(key) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData createBlockData(String key) {
|
||||
try {
|
||||
BlockType blockType = getBlockType(key);
|
||||
return blockType == null ? null : blockType.createBlockData();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData createBlockDataFromString(String blockData) {
|
||||
try {
|
||||
BlockType blockType = getBlockType(BlockTypeUtils.getBlockDataKey(blockData));
|
||||
if (blockType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String states = BlockTypeUtils.getBlockDataStates(blockData);
|
||||
return states == null ? blockType.createBlockData() : blockType.createBlockData(states);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BlockType getBlockType(String key) {
|
||||
NamespacedKey namespacedKey = NamespacedKey.fromString(BlockTypeUtils.normalizeKey(key));
|
||||
return namespacedKey == null ? null : Registry.BLOCK.get(namespacedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the EntityType corresponding to a Material.
|
||||
* Maps Material to its equivalent EntityType for entity handling.
|
||||
@@ -111,6 +187,7 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
return ((Keyed) value).getKey().toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a registry value from a key string and class.
|
||||
* Used for deserializing registry objects.
|
||||
@@ -129,6 +206,51 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
return Bukkit.getRegistry((Class) tClass).get(namespacedKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPaintingArtKey(Painting painting) {
|
||||
try {
|
||||
NamespacedKey key = Registry.ART.getKey(painting.getArt());
|
||||
if (key != null) {
|
||||
return normalizePaintingArtKey(key.toString());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
|
||||
return normalizePaintingArtKey(super.getPaintingArtKey(painting));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Art getPaintingArt(String name) {
|
||||
NamespacedKey key = NamespacedKey.fromString(normalizePaintingArtLookupKey(name));
|
||||
if (key != null) {
|
||||
Art art = Registry.ART.get(key);
|
||||
if (art != null) {
|
||||
return art;
|
||||
}
|
||||
}
|
||||
|
||||
return super.getPaintingArt(name);
|
||||
}
|
||||
|
||||
private String normalizePaintingArtKey(String name) {
|
||||
if (name == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String normalized = name.toLowerCase(Locale.ROOT).trim();
|
||||
if (normalized.startsWith("minecraft:")) {
|
||||
return normalized.substring("minecraft:".length());
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizePaintingArtLookupKey(String name) {
|
||||
String normalized = normalizePaintingArtKey(name);
|
||||
return normalized.contains(":") ? normalized : "minecraft:" + normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the wolf variant and adds it to the info list.
|
||||
* This functionality is specific to Minecraft 1.21+.
|
||||
@@ -177,6 +299,17 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShelf(Material material) {
|
||||
return SHELVES.contains(material);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBundle(Material material) {
|
||||
return BUNDLES.contains(material);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<Material> copperChestMaterials() {
|
||||
if (COPPER_CHESTS.isEmpty()) {
|
||||
@@ -198,4 +331,117 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
|
||||
|
||||
return COPPER_CHESTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Material> shelfMaterials() {
|
||||
if (SHELVES.isEmpty()) {
|
||||
Tag<Material> shelfTag = getMaterialTag(Tag.REGISTRY_BLOCKS, "wooden_shelves");
|
||||
if (shelfTag != null) {
|
||||
SHELVES.addAll(shelfTag.getValues());
|
||||
}
|
||||
|
||||
if (SHELVES.isEmpty()) {
|
||||
addMaterialsEndingWith(SHELVES, "_SHELF");
|
||||
}
|
||||
}
|
||||
|
||||
return SHELVES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Location> getShelfInteractionLocations(Block block, BlockFace blockFace) {
|
||||
List<Location> locations = new ArrayList<>();
|
||||
BlockData blockData = block.getBlockData();
|
||||
BlockFace facing = getShelfFacing(blockData);
|
||||
if (facing == null) {
|
||||
locations.add(block.getLocation());
|
||||
return locations;
|
||||
}
|
||||
|
||||
if (blockFace != facing) {
|
||||
return locations;
|
||||
}
|
||||
|
||||
String sideChain = getShelfSideChain(blockData);
|
||||
if ("UNCONNECTED".equals(sideChain)) {
|
||||
locations.add(block.getLocation());
|
||||
return locations;
|
||||
}
|
||||
|
||||
Block center = block;
|
||||
int directionX = facing.getModX();
|
||||
int directionZ = facing.getModZ();
|
||||
if ("LEFT".equals(sideChain)) {
|
||||
center = center.getRelative(directionZ, 0, -directionX);
|
||||
}
|
||||
else if ("RIGHT".equals(sideChain)) {
|
||||
center = center.getRelative(-directionZ, 0, directionX);
|
||||
}
|
||||
|
||||
String centerSideChain = getShelfSideChain(center.getBlockData());
|
||||
if (centerSideChain == null) {
|
||||
locations.add(block.getLocation());
|
||||
return locations;
|
||||
}
|
||||
|
||||
locations.add(center.getLocation());
|
||||
if (!"CENTER".equals(centerSideChain)) {
|
||||
locations.add(block.getLocation());
|
||||
return locations;
|
||||
}
|
||||
|
||||
locations.add(center.getRelative(-directionZ, 0, directionX).getLocation());
|
||||
locations.add(center.getRelative(directionZ, 0, -directionX).getLocation());
|
||||
return locations;
|
||||
}
|
||||
|
||||
private static Tag<Material> getMaterialTag(String registry, String key) {
|
||||
return Bukkit.getTag(registry, NamespacedKey.minecraft(key), Material.class);
|
||||
}
|
||||
|
||||
private static void addMaterialsEndingWith(Set<Material> materials, String suffix) {
|
||||
for (Material material : Material.values()) {
|
||||
if (material.name().endsWith(suffix)) {
|
||||
materials.add(material);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockFace getShelfFacing(BlockData blockData) {
|
||||
Object facing = invokeNoArgumentMethod(blockData, "getFacing");
|
||||
return facing instanceof BlockFace ? (BlockFace) facing : null;
|
||||
}
|
||||
|
||||
private static String getShelfSideChain(BlockData blockData) {
|
||||
Object sideChain = invokeNoArgumentMethod(blockData, "getSideChain");
|
||||
return sideChain == null ? null : sideChain.toString();
|
||||
}
|
||||
|
||||
private static Object invokeNoArgumentMethod(Object target, String methodName) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = target.getClass().getMethod(methodName);
|
||||
return method.invoke(target);
|
||||
}
|
||||
catch (ReflectiveOperationException | LinkageError e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldLogExplosion(Event event){
|
||||
ExplosionResult result = null;
|
||||
|
||||
if (event instanceof EntityExplodeEvent){
|
||||
result = ((EntityExplodeEvent)event).getExplosionResult();
|
||||
} else if (event instanceof BlockExplodeEvent){
|
||||
result = ((BlockExplodeEvent)event).getExplosionResult();
|
||||
}
|
||||
return !(result == ExplosionResult.KEEP ||
|
||||
result == ExplosionResult.TRIGGER_BLOCK
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.coreprotect.bukkit;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.ZombieNautilus;
|
||||
|
||||
/**
|
||||
* Bukkit adapter implementation for Minecraft 1.21.11.
|
||||
*/
|
||||
public class Bukkit_v1_21_11 extends Bukkit_v1_21_5 {
|
||||
|
||||
@Override
|
||||
public boolean getEntityMeta(LivingEntity entity, List<Object> info) {
|
||||
if (entity instanceof ZombieNautilus) {
|
||||
ZombieNautilus nautilus = (ZombieNautilus) entity;
|
||||
info.add(getRegistryKey(nautilus.getVariant()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.getEntityMeta(entity, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setEntityMeta(Entity entity, Object value, int count) {
|
||||
if (entity instanceof ZombieNautilus && count == 0) {
|
||||
ZombieNautilus.Variant variant = getZombieNautilusVariant(value);
|
||||
if (variant == null) {
|
||||
return false;
|
||||
}
|
||||
ZombieNautilus nautilus = (ZombieNautilus) entity;
|
||||
nautilus.setVariant(variant);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.setEntityMeta(entity, value, count);
|
||||
}
|
||||
|
||||
private ZombieNautilus.Variant getZombieNautilusVariant(Object value) {
|
||||
try {
|
||||
Object variant = value instanceof String ? getRegistryValue((String) value, ZombieNautilus.Variant.class) : value;
|
||||
return variant instanceof ZombieNautilus.Variant ? (ZombieNautilus.Variant) variant : null;
|
||||
}
|
||||
catch (IllegalArgumentException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package net.coreprotect.bukkit;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.entity.Chicken;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Pig;
|
||||
|
||||
/**
|
||||
* Bukkit adapter implementation for Minecraft 1.21.5.
|
||||
*/
|
||||
public class Bukkit_v1_21_5 extends Bukkit_v1_21 {
|
||||
|
||||
@Override
|
||||
public boolean getEntityMeta(LivingEntity entity, List<Object> info) {
|
||||
if (entity instanceof Chicken) {
|
||||
Chicken chicken = (Chicken) entity;
|
||||
info.add(getRegistryKey(chicken.getVariant()));
|
||||
return true;
|
||||
}
|
||||
else if (entity.getType() == EntityType.COW) {
|
||||
return BukkitAdapter.getRegistryVariant(this, entity, info, "getVariant");
|
||||
}
|
||||
else if (entity instanceof Pig) {
|
||||
Pig pig = (Pig) entity;
|
||||
info.add(getRegistryKey(pig.getVariant()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.getEntityMeta(entity, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setEntityMeta(Entity entity, Object value, int count) {
|
||||
if (entity instanceof Chicken && count == 0) {
|
||||
Chicken.Variant variant = getChickenVariant(value);
|
||||
if (variant == null) {
|
||||
return false;
|
||||
}
|
||||
Chicken chicken = (Chicken) entity;
|
||||
chicken.setVariant(variant);
|
||||
return true;
|
||||
}
|
||||
else if (entity.getType() == EntityType.COW && count == 0) {
|
||||
return BukkitAdapter.setRegistryVariant(this, entity, value, "getVariant", "setVariant");
|
||||
}
|
||||
else if (entity instanceof Pig && count == 1) {
|
||||
Pig.Variant variant = getPigVariant(value);
|
||||
if (variant == null) {
|
||||
return false;
|
||||
}
|
||||
Pig pig = (Pig) entity;
|
||||
pig.setVariant(variant);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.setEntityMeta(entity, value, count);
|
||||
}
|
||||
|
||||
private Chicken.Variant getChickenVariant(Object value) {
|
||||
Object variant = value instanceof String ? getRegistryValue((String) value, Chicken.Variant.class) : value;
|
||||
return variant instanceof Chicken.Variant ? (Chicken.Variant) variant : null;
|
||||
}
|
||||
|
||||
private Pig.Variant getPigVariant(Object value) {
|
||||
Object variant = value instanceof String ? getRegistryValue((String) value, Pig.Variant.class) : value;
|
||||
return variant instanceof Pig.Variant ? (Pig.Variant) variant : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.coreprotect.bukkit;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
import net.coreprotect.model.BlockGroup;
|
||||
|
||||
/**
|
||||
* Bukkit adapter implementation for Minecraft 26.2.
|
||||
*/
|
||||
public class Bukkit_v26_2 extends Bukkit_v1_21_11 {
|
||||
|
||||
public Bukkit_v26_2() {
|
||||
initializeBlockGroups();
|
||||
}
|
||||
|
||||
private void initializeBlockGroups() {
|
||||
BlockGroup.TRACK_TOP_BOTTOM.add(Material.SULFUR_SPIKE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class ApplyCommand {
|
||||
protected static void runCommand(CommandSender user, Command command, boolean permission, String[] args) {
|
||||
@@ -41,7 +42,7 @@ public class ApplyCommand {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class CancelCommand {
|
||||
protected static void runCommand(CommandSender user, Command command, boolean permission, String[] args) {
|
||||
@@ -41,7 +42,7 @@ public class CancelCommand {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.Extensions;
|
||||
import net.coreprotect.utility.VersionUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class CommandHandler implements CommandExecutor {
|
||||
private static CommandHandler instance;
|
||||
@@ -54,7 +55,7 @@ public class CommandHandler implements CommandExecutor {
|
||||
else if (user.hasPermission("coreprotect.purge") && corecommand.equals("purge")) {
|
||||
permission = true;
|
||||
}
|
||||
else if (user.hasPermission("coreprotect.lookup") && (corecommand.equals("l") || corecommand.equals("lookup") || corecommand.equals("page") || corecommand.equals("near"))) {
|
||||
else if (user.hasPermission("coreprotect.lookup") && (corecommand.equals("l") || corecommand.equals("lookup") || corecommand.equals("page"))) {
|
||||
permission = true;
|
||||
}
|
||||
else if (user.hasPermission("coreprotect.lookup.near") && corecommand.equals("near")) {
|
||||
@@ -75,6 +76,9 @@ public class CommandHandler implements CommandExecutor {
|
||||
else if (user.hasPermission("coreprotect.networking") && corecommand.equals("network-debug")) {
|
||||
permission = true;
|
||||
}
|
||||
else if (user.hasPermission("coreprotect.give") && corecommand.equals("give")) {
|
||||
permission = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (corecommand.equals("rollback") || corecommand.equals("restore") || corecommand.equals("rb") || corecommand.equals("rs") || corecommand.equals("ro") || corecommand.equals("re")) {
|
||||
@@ -119,6 +123,9 @@ public class CommandHandler implements CommandExecutor {
|
||||
else if (corecommand.equals("network-debug")) {
|
||||
NetworkDebugCommand.runCommand(user, permission, argumentArray);
|
||||
}
|
||||
else if (corecommand.equals("give")) {
|
||||
GiveCommand.runCommand(user, command, permission, argumentArray);
|
||||
}
|
||||
else if (corecommand.equals("migrate-db")) {
|
||||
if (!VersionUtils.validDonationKey()) {
|
||||
Chat.sendMessage(user, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DONATION_KEY_REQUIRED));
|
||||
@@ -138,25 +145,26 @@ public class CommandHandler implements CommandExecutor {
|
||||
if (user.isOp() && versionAlert.get(user.getName()) == null) {
|
||||
String latestVersion = NetworkHandler.latestVersion();
|
||||
String latestEdgeVersion = NetworkHandler.latestEdgeVersion();
|
||||
if (latestVersion != null || latestEdgeVersion != null) {
|
||||
boolean communityEdition = VersionUtils.isCommunityEdition();
|
||||
if (latestVersion != null || (latestEdgeVersion != null && !communityEdition)) {
|
||||
versionAlert.put(user.getName(), true);
|
||||
class updateAlert implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
Chat.sendMessage(user, Color.WHITE + "----- " + Color.DARK_AQUA + Phrase.build(Phrase.UPDATE_HEADER, "CoreProtect" + (VersionUtils.isCommunityEdition() ? " " + ConfigHandler.COMMUNITY_EDITION : "")) + Color.WHITE + " -----");
|
||||
Chat.sendMessage(user, Color.WHITE + "----- " + Color.DARK_AQUA + Phrase.build(Phrase.UPDATE_HEADER, "CoreProtect" + (communityEdition ? " " + ConfigHandler.COMMUNITY_EDITION : "")) + Color.WHITE + " -----");
|
||||
if (latestVersion != null) {
|
||||
Chat.sendMessage(user, Color.DARK_AQUA + Phrase.build(Phrase.UPDATE_NOTICE, Color.WHITE, "CoreProtect CE v" + latestVersion));
|
||||
Chat.sendMessage(user, Color.DARK_AQUA + Phrase.build(Phrase.LINK_DOWNLOAD, Color.WHITE, "www.coreprotect.net/download/"));
|
||||
}
|
||||
else if (!VersionUtils.isCommunityEdition()) {
|
||||
else {
|
||||
Chat.sendMessage(user, Color.DARK_AQUA + Phrase.build(Phrase.UPDATE_NOTICE, Color.WHITE, "CoreProtect v" + latestEdgeVersion));
|
||||
Chat.sendMessage(user, Color.DARK_AQUA + Phrase.build(Phrase.LINK_DOWNLOAD, Color.WHITE, "www.coreprotect.net/latest/"));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import net.coreprotect.command.parser.*;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import net.coreprotect.command.parser.ActionParser;
|
||||
import net.coreprotect.command.parser.LocationParser;
|
||||
import net.coreprotect.command.parser.MaterialParser;
|
||||
import net.coreprotect.command.parser.TimeParser;
|
||||
import net.coreprotect.command.parser.UserParser;
|
||||
import net.coreprotect.command.parser.WorldParser;
|
||||
|
||||
/**
|
||||
* Main parser class for CoreProtect commands.
|
||||
* Delegates to specialized parser classes for specific functionality.
|
||||
@@ -44,6 +38,22 @@ public class CommandParser {
|
||||
return ActionParser.parseAction(inputArguments);
|
||||
}
|
||||
|
||||
protected static List<Integer> parseAction(String[] inputArguments, boolean allowMultiple) {
|
||||
return ActionParser.parseAction(inputArguments, allowMultiple);
|
||||
}
|
||||
|
||||
protected static ActionParser.ParseResult parseActions(String[] inputArguments, boolean allowMultiple) {
|
||||
return ActionParser.parseActions(inputArguments, allowMultiple);
|
||||
}
|
||||
|
||||
protected static MessageFilterParser.ParseResult parseMessageFilters(String[] inputArguments) {
|
||||
return MessageFilterParser.parse(inputArguments);
|
||||
}
|
||||
|
||||
protected static RollbackStateParser.ParseResult parseRollbackState(String[] inputArguments) {
|
||||
return RollbackStateParser.parse(inputArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse coordinates from command arguments
|
||||
*
|
||||
@@ -70,6 +80,10 @@ public class CommandParser {
|
||||
return ActionParser.parseCount(inputArguments);
|
||||
}
|
||||
|
||||
protected static boolean parseSummary(String[] inputArguments) {
|
||||
return ActionParser.parseSummary(inputArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse excluded materials and entities from command arguments
|
||||
*
|
||||
@@ -326,4 +340,7 @@ public class CommandParser {
|
||||
return input.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
protected static Integer parseGivableItemId(String[] inputArguments) {
|
||||
return GivableItemIdParser.parseGivableItemId(inputArguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.coreprotect.command;
|
||||
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.ChatMessage;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ItemUtils;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public class GiveCommand {
|
||||
public static void runCommand(CommandSender sender, Command command, boolean permission, String[] args) {
|
||||
if (!permission) {
|
||||
Chat.sendMessage(sender, new ChatMessage(Phrase.build(Phrase.NO_PERMISSION)).build());
|
||||
return;
|
||||
}
|
||||
|
||||
Integer itemId = CommandParser.parseGivableItemId(args);
|
||||
if (itemId == null) {
|
||||
Chat.sendMessage(sender, new ChatMessage(Phrase.build(Phrase.MISSING_PARAMETERS, Color.WHITE, "/" + command.getName() + " give <itemId>")).build());
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack item = ItemUtils.getGivableItem(itemId);
|
||||
if (item == null) {
|
||||
Chat.sendMessage(sender, new ChatMessage(Phrase.build(Phrase.INVALID_ITEM_ID)).build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player)) {
|
||||
Chat.sendMessage(sender, new ChatMessage(Phrase.build(Phrase.ACTION_NOT_SUPPORTED)).build());
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
player.getInventory().addItem(item);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class HelpCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "a:<action> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PARAMS_5, Selector.FIRST));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "i:<include> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PARAMS_6, Selector.FIRST));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "e:<exclude> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PARAMS_7, Selector.FIRST));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "f:<filter> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_FILTER_1));
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + Phrase.build(Phrase.HELP_PARAMETER, "/co help <param>"));
|
||||
}
|
||||
else if (helpcommand.equals("rollback") || helpcommand.equals("rollbacks") || helpcommand.equals("rb") || helpcommand.equals("ro")) {
|
||||
@@ -67,8 +68,13 @@ public class HelpCommand {
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + Phrase.build(Phrase.HELP_PARAMETER, "/co help params"));
|
||||
}
|
||||
else if (helpcommand.equals("purge") || helpcommand.equals("purges")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "/co purge t:<time> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_1));
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + "" + Phrase.build(Phrase.HELP_PURGE_2, "/co purge t:30d"));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "/co purge " + Color.GREY + "<params> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_1));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "t:<time> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_3));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "r:<world/radius> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_4));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "i:<include> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_5));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "e:<exclude> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_6));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "| " + Color.GREY + "a:kill " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_PURGE_7));
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + Phrase.build(Phrase.HELP_PURGE_2, "/co purge t:30d"));
|
||||
}
|
||||
else if (helpcommand.equals("reload")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "/co reload " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_RELOAD_COMMAND));
|
||||
@@ -95,6 +101,10 @@ public class HelpCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "/co lookup a:<action> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_ACTION_1));
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + Phrase.build(Phrase.HELP_ACTION_2));
|
||||
}
|
||||
else if (helpcommand.equals("f") || helpcommand.equals("filter") || helpcommand.equals("ffilter")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "/co lookup a:<chat|command|sign> f:<filter> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_FILTER_1));
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + Phrase.build(Phrase.HELP_FILTER_2));
|
||||
}
|
||||
else if (helpcommand.equals("i") || helpcommand.equals("include") || helpcommand.equals("iinclude") || helpcommand.equals("b") || helpcommand.equals("block") || helpcommand.equals("blocks") || helpcommand.equals("bblock") || helpcommand.equals("bblocks")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "/co lookup i:<include> " + Color.WHITE + "- " + Phrase.build(Phrase.HELP_INCLUDE_1));
|
||||
Chat.sendMessage(player, Color.GREY + Color.ITALIC + Phrase.build(Phrase.HELP_INCLUDE_2));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.coreprotect.command;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -17,26 +18,40 @@ import org.bukkit.entity.Player;
|
||||
|
||||
import net.coreprotect.command.lookup.BlockLookupThread;
|
||||
import net.coreprotect.command.lookup.ChestTransactionLookupThread;
|
||||
import net.coreprotect.command.lookup.EntityInteractionLookupThread;
|
||||
import net.coreprotect.command.lookup.StandardLookupThread;
|
||||
import net.coreprotect.command.parser.ActionParser;
|
||||
import net.coreprotect.command.parser.MessageFilterParser;
|
||||
import net.coreprotect.command.parser.RollbackStateParser;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
import net.coreprotect.model.action.EntityActionFilter;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.model.lookup.LookupOutputMode;
|
||||
import net.coreprotect.model.lookup.LookupRollbackState;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.ChatMessage;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class LookupCommand {
|
||||
public static void runCommand(CommandSender player, Command command, boolean permission, String[] args) {
|
||||
int resultc = args.length;
|
||||
MessageFilterParser.ParseResult messageFilterResult = CommandParser.parseMessageFilters(args);
|
||||
args = messageFilterResult.getArguments();
|
||||
List<String> argFilters = messageFilterResult.getFilters();
|
||||
args = CommandParser.parsePage(args);
|
||||
Location lo = CommandParser.parseLocation(player, args);
|
||||
// List<String> arg_uuids = new ArrayList<String>();
|
||||
List<String> argUsers = CommandParser.parseUsers(args);
|
||||
Integer[] argRadius = CommandParser.parseRadius(args, player, lo);
|
||||
int argNoisy = CommandParser.parseNoisy(args);
|
||||
List<Integer> argAction = CommandParser.parseAction(args);
|
||||
ActionParser.ParseResult actionResult = CommandParser.parseActions(args, true);
|
||||
List<Integer> argAction = actionResult.getActions();
|
||||
EntityActionFilter argEntityActionFilter = actionResult.getEntityActionFilter();
|
||||
List<Object> argBlocks = CommandParser.parseRestricted(player, args, argAction);
|
||||
Map<Object, Boolean> argExclude = CommandParser.parseExcluded(player, args, argAction);
|
||||
List<String> argExcludeUsers = CommandParser.parseExcludedUsers(player, args);
|
||||
@@ -46,7 +61,11 @@ public class LookupCommand {
|
||||
long endTime = argTime[1];
|
||||
int argWid = CommandParser.parseWorld(args, true, true);
|
||||
int parseRows = CommandParser.parseRows(args);
|
||||
boolean count = CommandParser.parseCount(args);
|
||||
boolean summary = CommandParser.parseSummary(args);
|
||||
LookupOutputMode outputMode = summary ? LookupOutputMode.SUMMARY
|
||||
: CommandParser.parseCount(args) ? LookupOutputMode.COUNT : LookupOutputMode.DETAIL;
|
||||
RollbackStateParser.ParseResult rollbackStateResult = CommandParser.parseRollbackState(args);
|
||||
LookupRollbackState rollbackState = rollbackStateResult.getState();
|
||||
boolean worldedit = CommandParser.parseWorldEdit(args);
|
||||
boolean forceglobal = CommandParser.parseForceGlobal(args);
|
||||
boolean pageLookup = false;
|
||||
@@ -55,6 +74,11 @@ public class LookupCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rollbackStateResult.isConflicting()) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_PARAMETER, "#rolledback + #restored"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args[0].toLowerCase(Locale.ROOT).equals("page") && (args.length != 2 || !args[1].equals(args[1].replaceAll("[^0-9]", "")))) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_PARAMETERS, Color.WHITE, "/co page <page>"));
|
||||
return;
|
||||
@@ -67,15 +91,16 @@ public class LookupCommand {
|
||||
boolean hasBlock = false;
|
||||
boolean hasEntity = false;
|
||||
for (Object arg : argBlocks) {
|
||||
if (arg instanceof Material) {
|
||||
if (arg instanceof Material || arg instanceof String) {
|
||||
hasBlock = true;
|
||||
}
|
||||
else if (arg instanceof EntityType) {
|
||||
hasEntity = true;
|
||||
if (argAction.size() == 0) {
|
||||
argAction.add(3);
|
||||
argAction.add(LookupActions.ENTITY_KILL);
|
||||
argAction.add(LookupActions.ENTITY_SPAWN);
|
||||
}
|
||||
else if (!argAction.contains(3)) {
|
||||
else if (!argEntityActionFilter.includesAnyEntity(argAction, true) && !argAction.contains(LookupActions.INTERACTION)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_INCLUDE_COMBO));
|
||||
return;
|
||||
}
|
||||
@@ -84,15 +109,16 @@ public class LookupCommand {
|
||||
|
||||
/* check for invalid block/entity combinations (exclude) */
|
||||
for (Object arg : argExclude.keySet()) {
|
||||
if (arg instanceof Material) {
|
||||
if (arg instanceof Material || arg instanceof String) {
|
||||
hasBlock = true;
|
||||
}
|
||||
else if (arg instanceof EntityType) {
|
||||
hasEntity = true;
|
||||
if (argAction.size() == 0) {
|
||||
argAction.add(3);
|
||||
argAction.add(LookupActions.ENTITY_KILL);
|
||||
argAction.add(LookupActions.ENTITY_SPAWN);
|
||||
}
|
||||
else if (!argAction.contains(3)) {
|
||||
else if (!argEntityActionFilter.includesAnyEntity(argAction, true) && !argAction.contains(LookupActions.INTERACTION)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_INCLUDE_COMBO));
|
||||
return;
|
||||
}
|
||||
@@ -143,7 +169,7 @@ public class LookupCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (argAction.contains(6) || argAction.contains(7) || argAction.contains(8) || argAction.contains(9) || argAction.contains(10)) {
|
||||
if (argAction.contains(LookupActions.CHAT) || argAction.contains(LookupActions.COMMAND) || argAction.contains(LookupActions.SESSION) || argAction.contains(LookupActions.USERNAME) || argAction.contains(LookupActions.SIGN)) {
|
||||
pageLookup = true;
|
||||
}
|
||||
|
||||
@@ -169,6 +195,22 @@ public class LookupCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_ACTION));
|
||||
return;
|
||||
}
|
||||
if (rollbackState != LookupRollbackState.ANY && (argAction.contains(LookupActions.CHAT) || argAction.contains(LookupActions.COMMAND) || argAction.contains(LookupActions.SESSION) || argAction.contains(LookupActions.USERNAME) || argAction.contains(LookupActions.SIGN))) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INCOMPATIBLE_ACTION, rollbackState == LookupRollbackState.ROLLED_BACK ? "#rolledback" : "#restored"));
|
||||
return;
|
||||
}
|
||||
if (messageFilterResult.isSpecified()) {
|
||||
if (messageFilterResult.hasInvalidLength()) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MESSAGE_FILTER_TOO_SHORT, Integer.toString(MessageFilterParser.MINIMUM_FILTER_CODE_POINTS)));
|
||||
return;
|
||||
}
|
||||
boolean messageLookup = !argAction.isEmpty() && argAction.stream().allMatch(action -> action == LookupActions.CHAT || action == LookupActions.COMMAND);
|
||||
boolean signLookup = !argAction.isEmpty() && argAction.stream().allMatch(action -> action == LookupActions.SIGN);
|
||||
if (!messageLookup && !signLookup) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INCOMPATIBLE_ACTION, "f:<filter>"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (worldedit && argRadius == null) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_SELECTION, "WorldEdit"));
|
||||
return;
|
||||
@@ -189,53 +231,57 @@ public class LookupCommand {
|
||||
allPermission = true;
|
||||
}
|
||||
if (!allPermission) {
|
||||
if (!pageLookup && (argAction.size() == 0 || (argAction.size() == 1 && (argAction.contains(0) || argAction.contains(1)))) && !player.hasPermission("coreprotect.lookup.block")) {
|
||||
if (!pageLookup && (argAction.isEmpty() || argAction.contains(LookupActions.BLOCK_BREAK) || argAction.contains(LookupActions.BLOCK_PLACE)) && !player.hasPermission("coreprotect.lookup.block")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(2) && !player.hasPermission("coreprotect.lookup.click")) {
|
||||
if (argAction.contains(LookupActions.INTERACTION) && !player.hasPermission("coreprotect.lookup.click")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(3) && !player.hasPermission("coreprotect.lookup.kill")) {
|
||||
if (argAction.contains(LookupActions.ENTITY_KILL) && !player.hasPermission("coreprotect.lookup.kill")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(4) && !argAction.contains(11) && !player.hasPermission("coreprotect.lookup.container")) {
|
||||
if (argAction.contains(LookupActions.ENTITY_SPAWN) && !player.hasPermission("coreprotect.lookup.spawn")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(6) && !player.hasPermission("coreprotect.lookup.chat")) {
|
||||
if (argAction.contains(LookupActions.CONTAINER) && !argAction.contains(LookupActions.ITEM) && !player.hasPermission("coreprotect.lookup.container")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(7) && !player.hasPermission("coreprotect.lookup.command")) {
|
||||
if (argAction.contains(LookupActions.CHAT) && !player.hasPermission("coreprotect.lookup.chat")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(8) && !player.hasPermission("coreprotect.lookup.session")) {
|
||||
if (argAction.contains(LookupActions.COMMAND) && !player.hasPermission("coreprotect.lookup.command")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(9) && !player.hasPermission("coreprotect.lookup.username")) {
|
||||
if (argAction.contains(LookupActions.SESSION) && !player.hasPermission("coreprotect.lookup.session")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(10) && !player.hasPermission("coreprotect.lookup.sign")) {
|
||||
if (argAction.contains(LookupActions.USERNAME) && !player.hasPermission("coreprotect.lookup.username")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(11) && !argAction.contains(4) && !player.hasPermission("coreprotect.lookup.item")) {
|
||||
if (argAction.contains(LookupActions.SIGN) && !player.hasPermission("coreprotect.lookup.sign")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(4) && argAction.contains(11) && !player.hasPermission("coreprotect.lookup.inventory")) {
|
||||
if (argAction.contains(LookupActions.ITEM) && !argAction.contains(LookupActions.CONTAINER) && !player.hasPermission("coreprotect.lookup.item")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
if (LookupActions.isInventoryLookup(argAction) && !player.hasPermission("coreprotect.lookup.inventory")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (argAction.contains(6) || argAction.contains(7) || argAction.contains(8) || argAction.contains(9) || argAction.contains(10)) {
|
||||
if (argAction.contains(9) && (argRadius != null || argWid > 0 || worldedit)) {
|
||||
if (argAction.contains(LookupActions.CHAT) || argAction.contains(LookupActions.COMMAND) || argAction.contains(LookupActions.SESSION) || argAction.contains(LookupActions.USERNAME) || argAction.contains(LookupActions.SIGN)) {
|
||||
if (argAction.contains(LookupActions.USERNAME) && (argRadius != null || argWid > 0 || worldedit)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INCOMPATIBLE_ACTION, "r:"));
|
||||
return;
|
||||
}
|
||||
@@ -249,12 +295,12 @@ public class LookupCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (startTime <= 0 && !pageLookup && type == 4 && (argBlocks.size() > 0 || argUsers.size() > 0)) {
|
||||
if (startTime <= 0 && argRadius == null && !pageLookup && type == 4 && (argBlocks.size() > 0 || argUsers.size() > 0)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_LOOKUP_TIME, Selector.FIRST));
|
||||
return;
|
||||
}
|
||||
|
||||
if (argAction.contains(4) && argAction.contains(11)) { // a:inventory
|
||||
if (LookupActions.isInventoryLookup(argAction)) {
|
||||
if (argUsers.size() == 0) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_ACTION_USER));
|
||||
return;
|
||||
@@ -336,7 +382,7 @@ public class LookupCommand {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
else if (type == 2 || type == 3 || type == 7 || type == 8) {
|
||||
else if (type == 2 || type == 3 || type == 7 || type == 8 || type == 9) {
|
||||
boolean defaultRe = true;
|
||||
int page = 1;
|
||||
int re = 7;
|
||||
@@ -378,6 +424,13 @@ public class LookupCommand {
|
||||
}
|
||||
|
||||
// String bc = x+"."+y+"."+z+"."+wid+"."+rstring+"."+lookup_user;
|
||||
if (type == 9) {
|
||||
Runnable runnable = new EntityInteractionLookupThread(player, command, page, re);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
return;
|
||||
}
|
||||
|
||||
String lcommand = ConfigHandler.lookupCommand.get(player.getName());
|
||||
String[] data = lcommand.split("\\.");
|
||||
int x = Integer.parseInt(data[0]);
|
||||
@@ -390,21 +443,25 @@ public class LookupCommand {
|
||||
}
|
||||
|
||||
String bc = x + "." + y + "." + z + "." + wid + "." + lookupType + "." + re;
|
||||
Integer entitySpawnRowId = type == 2 && data.length > 6 ? Integer.valueOf(data[6]) : null;
|
||||
if (entitySpawnRowId != null) {
|
||||
bc += "." + entitySpawnRowId;
|
||||
}
|
||||
ConfigHandler.lookupCommand.put(player.getName(), bc);
|
||||
|
||||
String world = WorldUtils.getWorldName(wid);
|
||||
final Block block = Bukkit.getServer().getWorld(world).getBlockAt(x, y, z);
|
||||
final BlockState blockState = block.getState();
|
||||
|
||||
Runnable runnable = new BlockLookupThread(player, command, block, blockState, page, re, type);
|
||||
Runnable runnable = new BlockLookupThread(player, command, block, blockState, page, re, type, entitySpawnRowId);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
else if (type == 4 || type == 5) {
|
||||
boolean defaultRe = true;
|
||||
int pa = 1;
|
||||
int re = 4;
|
||||
if (argAction.contains(6) || argAction.contains(7) || argAction.contains(9) || (argAction.contains(4) && argAction.contains(11))) {
|
||||
int re = outputMode == LookupOutputMode.SUMMARY ? 8 : 4;
|
||||
if (outputMode != LookupOutputMode.SUMMARY && (argAction.contains(LookupActions.CHAT) || argAction.contains(LookupActions.COMMAND) || argAction.contains(LookupActions.USERNAME) || LookupActions.isInventoryLookup(argAction))) {
|
||||
re = 7;
|
||||
}
|
||||
if (parseRows > 0) {
|
||||
@@ -480,7 +537,7 @@ public class LookupCommand {
|
||||
}
|
||||
c++;
|
||||
|
||||
if (argAction.contains(4) && argAction.contains(11)) {
|
||||
if (LookupActions.isInventoryLookup(argAction)) {
|
||||
if (ruser.startsWith("#")) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_USERNAME, ruser));
|
||||
return;
|
||||
@@ -517,8 +574,12 @@ public class LookupCommand {
|
||||
argExclude = ConfigHandler.lookupElist.get(player.getName());
|
||||
argExcludeUsers = ConfigHandler.lookupEUserlist.get(player.getName());
|
||||
argAction = ConfigHandler.lookupAlist.get(player.getName());
|
||||
argEntityActionFilter = ConfigHandler.lookupEntityActionFilter.getOrDefault(player.getName(), EntityActionFilter.DEFAULT);
|
||||
argFilters = ConfigHandler.lookupFlist.getOrDefault(player.getName(), Collections.emptyList());
|
||||
argRadius = ConfigHandler.lookupRadius.get(player.getName());
|
||||
ts = ConfigHandler.lookupTime.get(player.getName());
|
||||
outputMode = ConfigHandler.lookupOutputMode.getOrDefault(player.getName(), LookupOutputMode.DETAIL);
|
||||
rollbackState = ConfigHandler.lookupRollbackState.getOrDefault(player.getName(), LookupRollbackState.ANY);
|
||||
startTime = 1;
|
||||
endTime = 0;
|
||||
}
|
||||
@@ -531,13 +592,13 @@ public class LookupCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (rollbackusers.size() == 1 && rollbackusers.contains("#global") && argAction.contains(9)) {
|
||||
if (rollbackusers.size() == 1 && rollbackusers.contains("#global") && argAction.contains(LookupActions.USERNAME)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_PARAMETERS, "/co l a:username u:<user>"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rollbackusers.contains("#container")) {
|
||||
if (argAction.contains(6) || argAction.contains(7) || argAction.contains(8) || argAction.contains(9) || argAction.contains(10) || argAction.contains(11)) {
|
||||
if (argAction.contains(LookupActions.CHAT) || argAction.contains(LookupActions.COMMAND) || argAction.contains(LookupActions.SESSION) || argAction.contains(LookupActions.USERNAME) || argAction.contains(LookupActions.SIGN) || argAction.contains(LookupActions.ITEM)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_USERNAME, "#container"));
|
||||
return;
|
||||
}
|
||||
@@ -578,8 +639,6 @@ public class LookupCommand {
|
||||
}
|
||||
|
||||
try {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Color.ITALIC + Phrase.build(Phrase.LOOKUP_SEARCHING));
|
||||
|
||||
if (timeStart == -1) {
|
||||
if (startTime <= 0) {
|
||||
timeStart = 0;
|
||||
@@ -595,12 +654,14 @@ public class LookupCommand {
|
||||
}
|
||||
}
|
||||
|
||||
Runnable runnable = new StandardLookupThread(player, command, rollbackusers, argBlocks, argExclude, argExcludeUsers, argAction, argRadius, lo, x, y, z, wid, argWid, timeStart, timeEnd, argNoisy, argExcluded, argRestricted, pa, re, type, ts, count);
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Color.ITALIC + Phrase.build(Phrase.LOOKUP_SEARCHING));
|
||||
|
||||
Runnable runnable = new StandardLookupThread(player, command, rollbackusers, argBlocks, argExclude, argExcludeUsers, argAction, argEntityActionFilter, argFilters, argRadius, lo, x, y, z, wid, argWid, timeStart, timeEnd, argNoisy, argExcluded, argRestricted, pa, re, type, ts, outputMode, rollbackState);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -622,4 +683,5 @@ public class LookupCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_PARAMETERS, "/co l <params>"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.listener.channel.PluginChannelListener;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class NetworkDebugCommand {
|
||||
protected static void runCommand(CommandSender player, boolean permission, String[] args) {
|
||||
@@ -19,7 +20,7 @@ public class NetworkDebugCommand {
|
||||
PluginChannelListener.getInstance().sendTest(player, args.length == 2 ? args[1] : "");
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,15 +1,25 @@
|
||||
package net.coreprotect.command;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.DuckDBRecovery;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.thread.NetworkHandler;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.EntitySpawnTracking;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.LookupThrottle;
|
||||
|
||||
public class ReloadCommand {
|
||||
|
||||
private static final long CONNECTION_DRAIN_TIMEOUT_MILLIS = 60_000L;
|
||||
|
||||
protected static void runCommand(final CommandSender player, boolean permission, String[] args) {
|
||||
if (permission) {
|
||||
if (ConfigHandler.converterRunning) {
|
||||
@@ -20,47 +30,91 @@ public class ReloadCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.PURGE_IN_PROGRESS));
|
||||
return;
|
||||
}
|
||||
if (ConfigHandler.lookupThrottle.get(player.getName()) != null) {
|
||||
Object[] lookupThrottle = ConfigHandler.lookupThrottle.get(player.getName());
|
||||
if ((boolean) lookupThrottle[0] || ((System.currentTimeMillis() - (long) lookupThrottle[1])) < 100) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
if (!LookupThrottle.tryAcquire(player.getName(), 100)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { true, System.currentTimeMillis() });
|
||||
|
||||
class BasicThread implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (Consumer.isPaused) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_STARTED));
|
||||
}
|
||||
while (Consumer.isPaused) {
|
||||
Thread.sleep(1);
|
||||
}
|
||||
Consumer.isPaused = true;
|
||||
|
||||
ConfigHandler.performInitialization(false);
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_SUCCESS));
|
||||
|
||||
Thread networkHandler = new Thread(new NetworkHandler(false, false));
|
||||
networkHandler.start();
|
||||
performReload(player);
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
finally {
|
||||
LookupThrottle.release(player.getName());
|
||||
}
|
||||
|
||||
Consumer.isPaused = false;
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { false, System.currentTimeMillis() });
|
||||
}
|
||||
}
|
||||
Runnable runnable = new BasicThread();
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
try {
|
||||
Runnable runnable = new BasicThread();
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
catch (RuntimeException | Error e) {
|
||||
LookupThrottle.release(player.getName());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized boolean performReload(CommandSender player) throws InterruptedException {
|
||||
final boolean resumePersistenceAfterCancellation = !Consumer.isDatabaseReloadPaused();
|
||||
boolean resumePersistence = resumePersistenceAfterCancellation;
|
||||
Consumer.OperationStartResult startResult = Consumer.beginDatabaseReload();
|
||||
if (startResult != Consumer.OperationStartResult.STARTED) {
|
||||
Phrase phrase = startResult == Consumer.OperationStartResult.PURGE_RUNNING ? Phrase.PURGE_IN_PROGRESS
|
||||
: startResult == Consumer.OperationStartResult.ROLLBACK_RUNNING ? Phrase.ROLLBACK_IN_PROGRESS
|
||||
: startResult == Consumer.OperationStartResult.PERSISTENCE_HALTED ? Phrase.DATABASE_PERSISTENCE_HALTED : Phrase.DATABASE_BUSY;
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(phrase));
|
||||
return false;
|
||||
}
|
||||
long deadline = System.nanoTime() + CONNECTION_DRAIN_TIMEOUT_MILLIS * 1_000_000L;
|
||||
try {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_STARTED));
|
||||
if (!Consumer.lockDatabaseReload(CONNECTION_DRAIN_TIMEOUT_MILLIS)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_FAILED));
|
||||
return false;
|
||||
}
|
||||
if (!Database.awaitConnectionDrain(remainingMillis(deadline))) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_FAILED));
|
||||
return false;
|
||||
}
|
||||
resumePersistence = false;
|
||||
boolean initialized;
|
||||
try {
|
||||
initialized = ConfigHandler.performInitialization(false);
|
||||
}
|
||||
catch (CancellationException e) {
|
||||
resumePersistence = resumePersistenceAfterCancellation;
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_FAILED));
|
||||
return false;
|
||||
}
|
||||
if (!initialized) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_FAILED));
|
||||
return false;
|
||||
}
|
||||
EntitySpawnTracking.invalidateDatabaseVerification();
|
||||
DuckDBRecovery.reset();
|
||||
resumePersistence = true;
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.RELOAD_SUCCESS));
|
||||
|
||||
Thread networkHandler = new Thread(new NetworkHandler(false, false));
|
||||
networkHandler.start();
|
||||
return true;
|
||||
}
|
||||
finally {
|
||||
Consumer.endDatabaseReload(resumePersistence);
|
||||
}
|
||||
}
|
||||
|
||||
private static long remainingMillis(long deadline) {
|
||||
return Math.max(0L, (deadline - System.nanoTime()) / 1_000_000L);
|
||||
}
|
||||
}
|
||||
@@ -19,26 +19,40 @@ import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import net.coreprotect.command.parser.ActionParser;
|
||||
import net.coreprotect.config.Config;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.database.ContainerRollback;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.lookup.PlayerLookup;
|
||||
import net.coreprotect.database.rollback.Rollback;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
import net.coreprotect.model.action.EntityActionFilter;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.LookupThrottle;
|
||||
|
||||
public class RollbackRestoreCommand {
|
||||
public static void runCommand(CommandSender player, Command command, boolean permission, String[] args, Location argLocation, long forceStart, long forceEnd) {
|
||||
String lookupOnlyFlag = findLookupOnlyFlag(args);
|
||||
if (lookupOnlyFlag != null) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_PARAMETER, lookupOnlyFlag));
|
||||
return;
|
||||
}
|
||||
|
||||
Location lo = (argLocation != null ? argLocation : CommandParser.parseLocation(player, args));
|
||||
List<String> argUuids = new ArrayList<>();
|
||||
List<String> argUsers = CommandParser.parseUsers(args);
|
||||
Integer[] argRadius = CommandParser.parseRadius(args, player, lo);
|
||||
int argNoisy = CommandParser.parseNoisy(args);
|
||||
List<Integer> argAction = CommandParser.parseAction(args);
|
||||
ActionParser.ParseResult actionResult = CommandParser.parseActions(args, false);
|
||||
List<Integer> argAction = actionResult.getActions();
|
||||
EntityActionFilter argEntityActionFilter = actionResult.getEntityActionFilter();
|
||||
List<Object> argBlocks = CommandParser.parseRestricted(player, args, argAction);
|
||||
Map<Object, Boolean> argExclude = CommandParser.parseExcluded(player, args, argAction);
|
||||
List<String> argExcludeUsers = CommandParser.parseExcludedUsers(player, args);
|
||||
@@ -47,7 +61,6 @@ public class RollbackRestoreCommand {
|
||||
long startTime = argTime[0];
|
||||
long endTime = argTime[1];
|
||||
int argWid = CommandParser.parseWorld(args, true, true);
|
||||
boolean count = CommandParser.parseCount(args);
|
||||
boolean worldedit = CommandParser.parseWorldEdit(args);
|
||||
boolean forceglobal = CommandParser.parseForceGlobal(args);
|
||||
int preview = CommandParser.parsePreview(args);
|
||||
@@ -61,15 +74,16 @@ public class RollbackRestoreCommand {
|
||||
boolean hasBlock = false;
|
||||
boolean hasEntity = false;
|
||||
for (Object arg : argBlocks) {
|
||||
if (arg instanceof Material) {
|
||||
if (arg instanceof Material || arg instanceof String) {
|
||||
hasBlock = true;
|
||||
}
|
||||
else if (arg instanceof EntityType) {
|
||||
hasEntity = true;
|
||||
if (argAction.size() == 0) {
|
||||
argAction.add(3);
|
||||
argAction.add(LookupActions.ENTITY_KILL);
|
||||
argAction.add(LookupActions.ENTITY_SPAWN);
|
||||
}
|
||||
else if (!argAction.contains(3)) {
|
||||
else if (!argEntityActionFilter.includesAnyEntity(argAction, true)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_INCLUDE_COMBO));
|
||||
return;
|
||||
}
|
||||
@@ -78,15 +92,16 @@ public class RollbackRestoreCommand {
|
||||
|
||||
/* check for invalid block/entity combinations (exclude) */
|
||||
for (Object arg : argExclude.keySet()) {
|
||||
if (arg instanceof Material) {
|
||||
if (arg instanceof Material || arg instanceof String) {
|
||||
hasBlock = true;
|
||||
}
|
||||
else if (arg instanceof EntityType) {
|
||||
hasEntity = true;
|
||||
if (argAction.size() == 0) {
|
||||
argAction.add(3);
|
||||
argAction.add(LookupActions.ENTITY_KILL);
|
||||
argAction.add(LookupActions.ENTITY_SPAWN);
|
||||
}
|
||||
else if (!argAction.contains(3)) {
|
||||
else if (!argEntityActionFilter.includesAnyEntity(argAction, true)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_INCLUDE_COMBO));
|
||||
return;
|
||||
}
|
||||
@@ -98,10 +113,6 @@ public class RollbackRestoreCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count) {
|
||||
LookupCommand.runCommand(player, command, permission, args);
|
||||
return;
|
||||
}
|
||||
if (ConfigHandler.converterRunning) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.UPGRADE_IN_PROGRESS));
|
||||
return;
|
||||
@@ -119,6 +130,10 @@ public class RollbackRestoreCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.PREVIEW_IN_GAME));
|
||||
return;
|
||||
}
|
||||
if (preview > 0 && argAction.contains(LookupActions.SIGN)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.ACTION_NOT_SUPPORTED));
|
||||
return;
|
||||
}
|
||||
if (argAction.contains(-1)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_ACTION));
|
||||
return;
|
||||
@@ -154,7 +169,8 @@ public class RollbackRestoreCommand {
|
||||
final int finalAction = a;
|
||||
|
||||
int DEFAULT_RADIUS = Config.getGlobal().DEFAULT_RADIUS;
|
||||
if ((player instanceof Player || player instanceof BlockCommandSender) && argRadius == null && DEFAULT_RADIUS > 0 && !forceglobal && !argAction.contains(11)) {
|
||||
boolean exactEntityContainer = argUsers.contains("#container") && ConfigHandler.lookupEntityContainer.get(player.getName()) != null;
|
||||
if ((player instanceof Player || player instanceof BlockCommandSender) && argRadius == null && DEFAULT_RADIUS > 0 && !forceglobal && !argAction.contains(LookupActions.ITEM) && !argAction.contains(5) && !exactEntityContainer) {
|
||||
Location location = lo;
|
||||
int xmin = location.getBlockX() - DEFAULT_RADIUS;
|
||||
int xmax = location.getBlockX() + DEFAULT_RADIUS;
|
||||
@@ -181,7 +197,7 @@ public class RollbackRestoreCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argAction.contains(4) && argAction.contains(11)) { // a:inventory
|
||||
if (LookupActions.isInventoryLookup(argAction)) {
|
||||
if (argUsers.size() == 0) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_ACTION_USER));
|
||||
return;
|
||||
@@ -192,7 +208,7 @@ public class RollbackRestoreCommand {
|
||||
argExclude.put(Material.FARMLAND, false);
|
||||
argExcludeUsers.add("#hopper");
|
||||
}
|
||||
else if (!argAction.contains(4) && Config.getGlobal().EXCLUDE_TNT && !argExclude.containsKey(Material.TNT) && !argBlocks.contains(Material.TNT)) {
|
||||
else if (!argAction.contains(LookupActions.CONTAINER) && !argAction.contains(LookupActions.SIGN) && Config.getGlobal().EXCLUDE_TNT && !argExclude.containsKey(Material.TNT) && !argBlocks.contains(Material.TNT)) {
|
||||
argExclude.put(Material.TNT, true);
|
||||
}
|
||||
|
||||
@@ -207,17 +223,17 @@ public class RollbackRestoreCommand {
|
||||
}
|
||||
}
|
||||
if (argAction.size() > 0) {
|
||||
if (argAction.contains(4)) {
|
||||
if (argAction.contains(LookupActions.CONTAINER)) {
|
||||
if (argUsers.contains("#global") || (argUsers.size() == 0 && argRadius == null)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_ACTION_USER));
|
||||
return;
|
||||
}
|
||||
else if (preview > 0) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.PREVIEW_TRANSACTION, !argAction.contains(11) ? Selector.FIRST : Selector.SECOND));
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.PREVIEW_TRANSACTION, !argAction.contains(LookupActions.ITEM) ? Selector.FIRST : Selector.SECOND));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (argAction.contains(8) || (argAction.contains(11) && !argAction.contains(4)) || (!argAction.contains(0) && !argAction.contains(1) && !argAction.contains(3) && !argAction.contains(4))) {
|
||||
if (argAction.contains(LookupActions.SESSION) || (argAction.contains(LookupActions.ITEM) && !argAction.contains(LookupActions.CONTAINER)) || (!argAction.contains(LookupActions.BLOCK_BREAK) && !argAction.contains(LookupActions.BLOCK_PLACE) && !argAction.contains(LookupActions.ENTITY_KILL) && !argAction.contains(LookupActions.ENTITY_SPAWN) && !argAction.contains(LookupActions.CONTAINER) && !argAction.contains(LookupActions.SIGN))) {
|
||||
if (finalAction == 0) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.ACTION_NOT_SUPPORTED));
|
||||
}
|
||||
@@ -244,7 +260,7 @@ public class RollbackRestoreCommand {
|
||||
}
|
||||
c++;
|
||||
|
||||
if (argAction.contains(4) && argAction.contains(11)) {
|
||||
if (LookupActions.isInventoryLookup(argAction)) {
|
||||
Player onlineUser = Bukkit.getServer().getPlayer(ruser);
|
||||
if (onlineUser == null || !onlineUser.isOnline()) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.USER_OFFLINE, ruser));
|
||||
@@ -258,7 +274,7 @@ public class RollbackRestoreCommand {
|
||||
int y = 0;
|
||||
int z = 0;
|
||||
if (rollbackusers.contains("#container")) {
|
||||
if (argAction.contains(11)) {
|
||||
if (argAction.contains(LookupActions.ITEM)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_USERNAME, "#container"));
|
||||
return;
|
||||
}
|
||||
@@ -291,19 +307,21 @@ public class RollbackRestoreCommand {
|
||||
argRadius = null;
|
||||
argWid = 0;
|
||||
lo = new Location(Bukkit.getServer().getWorld(WorldUtils.getWorldName(wid)), x, y, z);
|
||||
Block block = lo.getBlock();
|
||||
if (block.getState() instanceof Chest) {
|
||||
BlockFace[] blockFaces = new BlockFace[] { BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST };
|
||||
for (BlockFace face : blockFaces) {
|
||||
if (block.getRelative(face, 1).getState() instanceof Chest) {
|
||||
Block relative = block.getRelative(face, 1);
|
||||
int x2 = relative.getX();
|
||||
int z2 = relative.getZ();
|
||||
double newX = (x + x2) / 2.0;
|
||||
double newZ = (z + z2) / 2.0;
|
||||
lo.setX(newX);
|
||||
lo.setZ(newZ);
|
||||
break;
|
||||
if (!exactEntityContainer) {
|
||||
Block block = lo.getBlock();
|
||||
if (block.getState() instanceof Chest) {
|
||||
BlockFace[] blockFaces = new BlockFace[] { BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST };
|
||||
for (BlockFace face : blockFaces) {
|
||||
if (block.getRelative(face, 1).getState() instanceof Chest) {
|
||||
Block relative = block.getRelative(face, 1);
|
||||
int x2 = relative.getX();
|
||||
int z2 = relative.getZ();
|
||||
double newX = (x + x2) / 2.0;
|
||||
double newZ = (z + z2) / 2.0;
|
||||
lo.setX(newX);
|
||||
lo.setZ(newZ);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,16 +356,28 @@ public class RollbackRestoreCommand {
|
||||
final Location locationFinal = lo;
|
||||
final int finalArgWid = argWid;
|
||||
final List<Integer> finalArgAction = argAction;
|
||||
final EntityActionFilter finalEntityActionFilter = argEntityActionFilter;
|
||||
final String[] finalArgs = args;
|
||||
final int finalPreview = preview;
|
||||
|
||||
ConfigHandler.activeRollbacks.put(player.getName(), true);
|
||||
Consumer.OperationStartResult startResult = Consumer.claimRollback(player.getName(), finalPreview == 0);
|
||||
if (startResult != Consumer.OperationStartResult.STARTED) {
|
||||
Phrase phrase = startResult == Consumer.OperationStartResult.PURGE_RUNNING ? Phrase.PURGE_IN_PROGRESS
|
||||
: startResult == Consumer.OperationStartResult.PERSISTENCE_HALTED ? Phrase.DATABASE_PERSISTENCE_HALTED
|
||||
: startResult == Consumer.OperationStartResult.RELOAD_RUNNING ? Phrase.DATABASE_BUSY : Phrase.ROLLBACK_IN_PROGRESS;
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(phrase));
|
||||
return;
|
||||
}
|
||||
|
||||
class BasicThread2 implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!LookupThrottle.tryAcquire(player.getName(), 100)) {
|
||||
Consumer.releaseRollback(player.getName());
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
try (Connection connection = Database.getConnection(false, 1000)) {
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { true, System.currentTimeMillis() });
|
||||
int action = finalAction;
|
||||
Location location = locationFinal;
|
||||
if (connection != null) {
|
||||
@@ -424,13 +454,14 @@ public class RollbackRestoreCommand {
|
||||
Chat.sendMessage(player2, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.ROLLBACK_STARTED, users, Selector.SECOND));
|
||||
}
|
||||
|
||||
if (finalArgAction.contains(5)) {
|
||||
ContainerRollback.performContainerRollbackRestore(statement, player2, uuidList, rollbackusers2, rtime, blist, elist, euserlist, finalArgAction, location, radius, finalTimeStart, finalTimeEnd, restrictWorld, false, verbose, action);
|
||||
boolean completed = true;
|
||||
if (finalArgAction.contains(5) && ConfigHandler.lookupEntityContainer.get(player2.getName()) == null) {
|
||||
completed = ContainerRollback.performContainerRollbackRestore(statement, player2, uuidList, rollbackusers2, rtime, blist, elist, euserlist, finalArgAction, location, radius, finalTimeStart, finalTimeEnd, restrictWorld, false, verbose, action);
|
||||
}
|
||||
else {
|
||||
Rollback.performRollbackRestore(statement, player2, uuidList, rollbackusers2, rtime, blist, elist, euserlist, finalArgAction, location, radius, finalTimeStart, finalTimeEnd, restrictWorld, false, verbose, action, finalPreview);
|
||||
completed = Rollback.performRollbackRestore(statement, player2, uuidList, rollbackusers2, rtime, blist, elist, euserlist, finalArgAction, finalEntityActionFilter, location, radius, finalTimeStart, finalTimeEnd, restrictWorld, false, verbose, action, finalPreview) != null;
|
||||
}
|
||||
if (finalPreview < 2) {
|
||||
if (completed && finalPreview < 2) {
|
||||
List<Object> list = new ArrayList<>();
|
||||
list.add(finalTimeStart);
|
||||
list.add(finalTimeEnd);
|
||||
@@ -449,20 +480,26 @@ public class RollbackRestoreCommand {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
if (ConfigHandler.activeRollbacks.get(player2.getName()) != null) {
|
||||
ConfigHandler.activeRollbacks.remove(player2.getName());
|
||||
finally {
|
||||
Consumer.releaseRollback(player2.getName());
|
||||
LookupThrottle.release(player2.getName());
|
||||
}
|
||||
ConfigHandler.lookupThrottle.put(player2.getName(), new Object[] { false, System.currentTimeMillis() });
|
||||
}
|
||||
}
|
||||
Runnable runnable = new BasicThread2();
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
try {
|
||||
thread.start();
|
||||
}
|
||||
catch (Exception e) {
|
||||
Consumer.releaseRollback(player.getName());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -487,4 +524,15 @@ public class RollbackRestoreCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
private static String findLookupOnlyFlag(String[] args) {
|
||||
for (int index = 1; index < args.length; index++) {
|
||||
String raw = args[index];
|
||||
String argument = raw.trim().toLowerCase(Locale.ROOT).replace("\\", "").replace("'", "");
|
||||
if (argument.equals("#count") || argument.equals("#sum") || argument.equals("#summary") || argument.equals("count") || argument.equals("sum")) {
|
||||
return raw.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.SystemUtils;
|
||||
import net.coreprotect.utility.VersionUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class StatusCommand {
|
||||
private static ConcurrentHashMap<String, Boolean> alert = new ConcurrentHashMap<>();
|
||||
@@ -66,16 +67,18 @@ public class StatusCommand {
|
||||
|
||||
// Using MySQL/SQLite (Database Size: 587MB)
|
||||
|
||||
String firstVersion = Patch.getFirstVersion();
|
||||
String firstVersion = ConfigHandler.databaseType.isClickHouse() ? "" : Patch.getFirstVersion();
|
||||
if (firstVersion.length() > 0) {
|
||||
firstVersion = " (" + Phrase.build(Phrase.FIRST_VERSION, firstVersion) + ")";
|
||||
}
|
||||
if (Config.getGlobal().MYSQL) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_DATABASE, Color.WHITE, "MySQL") + firstVersion);
|
||||
String databaseName = ConfigHandler.databaseType.getDisplayName();
|
||||
if (Consumer.isPersistenceHalted()) {
|
||||
databaseName += " " + Color.RED + Phrase.build(Phrase.STATUS_DATABASE_STATE, Selector.FIRST) + Color.WHITE;
|
||||
}
|
||||
else {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_DATABASE, Color.WHITE, "SQLite") + firstVersion);
|
||||
else if (!ConfigHandler.databaseReachable) {
|
||||
databaseName += " " + Color.RED + Phrase.build(Phrase.STATUS_DATABASE_STATE, Selector.SECOND) + Color.WHITE;
|
||||
}
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_DATABASE, Color.WHITE, databaseName) + firstVersion);
|
||||
|
||||
if (ConfigHandler.worldeditEnabled) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_INTEGRATION, Color.WHITE, "WorldEdit", Selector.FIRST));
|
||||
@@ -104,13 +107,16 @@ public class StatusCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_CONSUMER, Color.WHITE, String.format("%,d", consumerCount), (consumerCount == 1 ? Selector.FIRST : Selector.SECOND)));
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
long autoPurgeRowsPurged = ConfigHandler.autoPurgeRowsPurged.get();
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_AUTO_PURGE, Color.WHITE, String.format("%,d", autoPurgeRowsPurged), (autoPurgeRowsPurged == 1 ? Selector.FIRST : Selector.SECOND)));
|
||||
|
||||
try {
|
||||
String cpuInfo = "";
|
||||
String cpuInfo = "x" + Runtime.getRuntime().availableProcessors() + " " + Phrase.build(Phrase.CPU_CORES);
|
||||
if (ConfigHandler.processorInfo != null) {
|
||||
String modelName = ConfigHandler.processorInfo.getProcessorIdentifier().getName();
|
||||
String modelName = ConfigHandler.processorInfo.getName();
|
||||
if (modelName.contains(" CPU")) {
|
||||
String[] split = modelName.split(" CPU")[0].split(" ");
|
||||
modelName = split[split.length - 1];
|
||||
@@ -120,7 +126,7 @@ public class StatusCommand {
|
||||
modelName = split[split.length - 1];
|
||||
}
|
||||
|
||||
String cpuSpeed = String.valueOf(ConfigHandler.processorInfo.getMaxFreq());
|
||||
String cpuSpeed = String.valueOf(ConfigHandler.processorInfo.getMaxFrequency());
|
||||
double speedVal = Long.valueOf(cpuSpeed) / 1000000000.0;
|
||||
|
||||
// Fix for Apple Silicon processors reporting 0 GHz
|
||||
@@ -132,10 +138,9 @@ public class StatusCommand {
|
||||
}
|
||||
|
||||
cpuSpeed = String.format("%.2f", speedVal);
|
||||
cpuInfo = "x" + Runtime.getRuntime().availableProcessors() + " " + cpuSpeed + "GHz " + modelName + ".";
|
||||
}
|
||||
else {
|
||||
cpuInfo = "x" + Runtime.getRuntime().availableProcessors() + " " + Phrase.build(Phrase.CPU_CORES);
|
||||
if (speedVal >= 0.01) {
|
||||
cpuInfo = "x" + Runtime.getRuntime().availableProcessors() + " " + cpuSpeed + "GHz " + modelName + ".";
|
||||
}
|
||||
}
|
||||
|
||||
int mb = 1024 * 1024;
|
||||
@@ -150,7 +155,7 @@ public class StatusCommand {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + Phrase.build(Phrase.STATUS_SYSTEM, Color.WHITE, systemInformation));
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
|
||||
// Functions.sendMessage(player, Color.DARK_AQUA + "Website: " + Color.WHITE + "www.coreprotect.net/updates/");
|
||||
@@ -173,7 +178,7 @@ public class StatusCommand {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@ import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.StringUtil;
|
||||
|
||||
import net.coreprotect.command.parser.RollbackStateParser;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
|
||||
public class TabHandler implements TabCompleter {
|
||||
|
||||
// private static String[] COMMANDS = new String[] { "help", "inspect", "rollback", "restore", "lookup", "purge", "reload", "status", "near", "undo" }; // max 10!
|
||||
private static final String[] HELP = new String[] { "inspect", "rollback", "restore", "lookup", "purge", "teleport", "status", "params", "users", "time", "radius", "action", "include", "exclude" };
|
||||
private static final String[] PARAMS = new String[] { "user:", "time:", "radius:", "action:", "include:", "exclude:", "#container" };
|
||||
private static final String[] ACTIONS = new String[] { "block", "+block", "-block", "click", "kill", "+container", "-container", "container", "chat", "command", "+inventory", "-inventory", "inventory", "item", "+item", "-item", "sign", "session", "+session", "-session", "username" };
|
||||
private static final String[] HELP = new String[] { "inspect", "rollback", "restore", "lookup", "purge", "teleport", "status", "params", "users", "time", "radius", "action", "include", "exclude", "filter" };
|
||||
private static final String[] PARAMS = new String[] { "user:", "time:", "radius:", "action:", "include:", "exclude:", "filter:", "#container" };
|
||||
private static final String[] ACTIONS = new String[] { "block", "+block", "-block", "click", "kill", "spawn", "+container", "-container", "container", "chat", "command", "+inventory", "-inventory", "inventory", "item", "+item", "-item", "sign", "session", "+session", "-session", "username" };
|
||||
private static final String[] NUMBERS = new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
|
||||
private static final String[] TIMES = new String[] { "w", "d", "h", "m", "s" };
|
||||
private static ArrayList<String> materials = null;
|
||||
@@ -43,7 +44,7 @@ public class TabHandler implements TabCompleter {
|
||||
String currentArg = args[args.length - 1].toLowerCase(Locale.ROOT).trim();
|
||||
String lastArg = args.length > 1 ? args[args.length - 2].toLowerCase(Locale.ROOT).trim() : "";
|
||||
|
||||
ParamState paramState = getParamState(args);
|
||||
ParamState paramState = getParamState(sender, args);
|
||||
|
||||
// Handle param-specific completions
|
||||
if (isActionParam(lastArg, currentArg) && hasLookupPermission(sender)) {
|
||||
@@ -150,20 +151,24 @@ public class TabHandler implements TabCompleter {
|
||||
boolean hasAction;
|
||||
boolean hasInclude;
|
||||
boolean hasExclude;
|
||||
boolean hasFilter;
|
||||
boolean hasRadius;
|
||||
boolean hasTime;
|
||||
boolean hasContainer;
|
||||
boolean hasCount;
|
||||
boolean hasSummary;
|
||||
boolean hasRollbackState;
|
||||
boolean hasPreview;
|
||||
boolean hasPage;
|
||||
boolean validContainer;
|
||||
boolean pageLookup;
|
||||
}
|
||||
|
||||
private ParamState getParamState(String[] args) {
|
||||
private ParamState getParamState(CommandSender sender, String[] args) {
|
||||
ParamState state = new ParamState();
|
||||
String senderName = sender.getName();
|
||||
|
||||
if (ConfigHandler.lookupType.get(args[0]) != null && ConfigHandler.lookupPage.get(args[0]) != null) {
|
||||
if (ConfigHandler.lookupType.get(senderName) != null && ConfigHandler.lookupPage.get(senderName) != null) {
|
||||
state.pageLookup = true;
|
||||
}
|
||||
|
||||
@@ -175,9 +180,18 @@ public class TabHandler implements TabCompleter {
|
||||
else if (arg.equals("#count") || arg.equals("#sum")) {
|
||||
state.hasCount = true;
|
||||
}
|
||||
else if (arg.equals("#summary")) {
|
||||
state.hasSummary = true;
|
||||
}
|
||||
else if (RollbackStateParser.isModifier(arg)) {
|
||||
state.hasRollbackState = true;
|
||||
}
|
||||
else if (arg.equals("#preview")) {
|
||||
state.hasPreview = true;
|
||||
}
|
||||
else if (arg.startsWith("f:") || arg.startsWith("filter:")) {
|
||||
state.hasFilter = true;
|
||||
}
|
||||
else if ((!arg.contains(":") && !args[i - 1].contains(":") && args.length > (i + 1)) || arg.contains("u:") || arg.contains("user:") || arg.contains("users:") || arg.contains("p:")) {
|
||||
state.hasUser = true;
|
||||
}
|
||||
@@ -202,13 +216,14 @@ public class TabHandler implements TabCompleter {
|
||||
}
|
||||
|
||||
if (!state.hasContainer) {
|
||||
if (ConfigHandler.lookupType.get(args[0]) != null) {
|
||||
int lookupType = ConfigHandler.lookupType.get(args[0]);
|
||||
Integer lookupType = ConfigHandler.lookupType.get(senderName);
|
||||
if (lookupType != null) {
|
||||
if (lookupType == 1) {
|
||||
state.validContainer = true;
|
||||
}
|
||||
else if (lookupType == 5) {
|
||||
if (ConfigHandler.lookupUlist.get(args[0]).contains("#container")) {
|
||||
List<String> lookupUsers = ConfigHandler.lookupUlist.get(senderName);
|
||||
if (lookupUsers != null && lookupUsers.contains("#container")) {
|
||||
state.validContainer = true;
|
||||
}
|
||||
}
|
||||
@@ -432,7 +447,7 @@ public class TabHandler implements TabCompleter {
|
||||
return StringUtil.copyPartialMatches(argument1, completions, new ArrayList<>(completions.size()));
|
||||
}
|
||||
else if (argument0.equals("purge") && sender.hasPermission("coreprotect.purge")) {
|
||||
List<String> completions = new ArrayList<>(Arrays.asList("t:", "r:", "i:"));
|
||||
List<String> completions = new ArrayList<>(Arrays.asList("t:", "r:", "i:", "e:", "a:kill"));
|
||||
return StringUtil.copyPartialMatches(argument1, completions, new ArrayList<>(completions.size()));
|
||||
}
|
||||
else if (hasLookupCommand(argument0, sender)) {
|
||||
@@ -449,10 +464,10 @@ public class TabHandler implements TabCompleter {
|
||||
String argument2 = arg2.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (argument1.startsWith("t:")) {
|
||||
List<String> completions = new ArrayList<>(Arrays.asList("r:", "i:"));
|
||||
List<String> completions = new ArrayList<>(Arrays.asList("r:", "i:", "e:", "a:kill"));
|
||||
return StringUtil.copyPartialMatches(argument2, completions, new ArrayList<>(completions.size()));
|
||||
}
|
||||
else if (argument1.startsWith("r:") || argument1.startsWith("i:")) {
|
||||
else if (argument1.startsWith("r:") || argument1.startsWith("i:") || argument1.startsWith("e:") || argument1.startsWith("a:")) {
|
||||
List<String> completions = new ArrayList<>(Arrays.asList("t:"));
|
||||
return StringUtil.copyPartialMatches(argument2, completions, new ArrayList<>(completions.size()));
|
||||
}
|
||||
@@ -480,6 +495,9 @@ public class TabHandler implements TabCompleter {
|
||||
else if (param.equals("exclude:") && !state.hasExclude) {
|
||||
params.add(param);
|
||||
}
|
||||
else if (param.equals("filter:") && !state.hasFilter && (lastArgument.equals("l") || lastArgument.equals("lookup"))) {
|
||||
params.add(param);
|
||||
}
|
||||
else if (param.equals("radius:") && !state.hasRadius) {
|
||||
params.add(param);
|
||||
}
|
||||
@@ -493,9 +511,14 @@ public class TabHandler implements TabCompleter {
|
||||
if (firstParam && state.pageLookup && (lastArgument.equals("l") || lastArgument.equals("lookup"))) {
|
||||
params.add("page:");
|
||||
}
|
||||
else if (!firstParam && argument.startsWith("#")) {
|
||||
if (!state.hasCount) {
|
||||
if (argument.startsWith("#")) {
|
||||
if (!state.hasCount && !state.hasSummary && (lastArgument.equals("l") || lastArgument.equals("lookup"))) {
|
||||
params.add("#count");
|
||||
params.add("#summary");
|
||||
}
|
||||
if (!state.hasRollbackState && (lastArgument.equals("l") || lastArgument.equals("lookup"))) {
|
||||
params.add("#rolledback");
|
||||
params.add("#restored");
|
||||
}
|
||||
if (!state.hasPreview) {
|
||||
params.add("#preview");
|
||||
|
||||
@@ -14,7 +14,6 @@ import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.ChatMessage;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.Teleport;
|
||||
import net.coreprotect.utility.Util;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
|
||||
public class TeleportCommand {
|
||||
@@ -56,6 +55,10 @@ public class TeleportCommand {
|
||||
World world = location.getWorld();
|
||||
if (wid > -1) {
|
||||
world = Bukkit.getServer().getWorld(WorldUtils.getWorldName(wid));
|
||||
if (world == null) {
|
||||
Chat.sendMessage(player, new ChatMessage(Phrase.build(Phrase.WORLD_NOT_FOUND, worldName)).build());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String x = null;
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class UndoCommand {
|
||||
protected static void runCommand(CommandSender user, Command command, boolean permission, String[] args) {
|
||||
@@ -47,7 +48,7 @@ public class UndoCommand {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.sk89q.worldedit.regions.Region;
|
||||
import com.sk89q.worldedit.world.World;
|
||||
|
||||
import net.coreprotect.worldedit.WorldEditLogger;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class WorldEditHandler {
|
||||
|
||||
@@ -49,7 +50,7 @@ public class WorldEditHandler {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.bukkit.block.BlockState;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.lookup.BlockLookup;
|
||||
import net.coreprotect.database.lookup.InteractionLookup;
|
||||
@@ -16,6 +15,8 @@ import net.coreprotect.database.lookup.SignMessageLookup;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.LookupThrottle;
|
||||
|
||||
public class BlockLookupThread implements Runnable {
|
||||
private final CommandSender player;
|
||||
@@ -25,8 +26,9 @@ public class BlockLookupThread implements Runnable {
|
||||
private final int page;
|
||||
private final int limit;
|
||||
private final int type;
|
||||
private final Integer entitySpawnRowId;
|
||||
|
||||
public BlockLookupThread(CommandSender player, Command command, Block block, BlockState blockState, int page, int limit, int type) {
|
||||
public BlockLookupThread(CommandSender player, Command command, Block block, BlockState blockState, int page, int limit, int type, Integer entitySpawnRowId) {
|
||||
this.player = player;
|
||||
this.command = command;
|
||||
this.block = block;
|
||||
@@ -34,12 +36,17 @@ public class BlockLookupThread implements Runnable {
|
||||
this.page = page;
|
||||
this.limit = limit;
|
||||
this.type = type;
|
||||
this.entitySpawnRowId = entitySpawnRowId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!LookupThrottle.tryAcquire(player.getName(), 50)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(true)) {
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { true, System.currentTimeMillis() });
|
||||
if (connection != null) {
|
||||
Statement statement = connection.createStatement();
|
||||
if (type == 8) {
|
||||
@@ -64,7 +71,7 @@ public class BlockLookupThread implements Runnable {
|
||||
blockdata = InteractionLookup.performLookup(command.getName(), statement, block, player, 0, page, limit);
|
||||
}
|
||||
else {
|
||||
blockdata = BlockLookup.performLookup(command.getName(), statement, blockState, player, 0, page, limit);
|
||||
blockdata = BlockLookup.performLookup(command.getName(), statement, blockState, player, 0, page, limit, entitySpawnRowId);
|
||||
}
|
||||
if (blockdata.contains("\n")) {
|
||||
for (String b : blockdata.split("\n")) {
|
||||
@@ -82,9 +89,10 @@ public class BlockLookupThread implements Runnable {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
finally {
|
||||
LookupThrottle.release(player.getName());
|
||||
}
|
||||
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { false, System.currentTimeMillis() });
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import net.coreprotect.database.lookup.ChestTransactionLookup;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.LookupThrottle;
|
||||
|
||||
public class ChestTransactionLookupThread implements Runnable {
|
||||
private final CommandSender player;
|
||||
@@ -32,11 +34,16 @@ public class ChestTransactionLookupThread implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!LookupThrottle.tryAcquire(player.getName(), 50)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(true)) {
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { true, System.currentTimeMillis() });
|
||||
if (connection != null) {
|
||||
Statement statement = connection.createStatement();
|
||||
List<String> blockData = ChestTransactionLookup.performLookup(command.getName(), statement, location, player, page, limit, false);
|
||||
Integer entitySpawnRowId = ConfigHandler.lookupEntityContainer.get(player.getName());
|
||||
List<String> blockData = ChestTransactionLookup.performLookup(command.getName(), statement, location, player, page, limit, false, entitySpawnRowId);
|
||||
for (String data : blockData) {
|
||||
Chat.sendComponent(player, data);
|
||||
}
|
||||
@@ -47,9 +54,10 @@ public class ChestTransactionLookupThread implements Runnable {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
finally {
|
||||
LookupThrottle.release(player.getName());
|
||||
}
|
||||
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { false, System.currentTimeMillis() });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package net.coreprotect.command.lookup;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.lookup.EntityInteractionLookup;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.LookupThrottle;
|
||||
|
||||
public final class EntityInteractionLookupThread implements Runnable {
|
||||
private final CommandSender player;
|
||||
private final Command command;
|
||||
private final int page;
|
||||
private final int limit;
|
||||
|
||||
public EntityInteractionLookupThread(CommandSender player, Command command, int page, int limit) {
|
||||
this.player = player;
|
||||
this.command = command;
|
||||
this.page = page;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!LookupThrottle.tryAcquire(player.getName(), 50)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(true)) {
|
||||
if (connection == null) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
|
||||
Integer entitySpawnRowId = ConfigHandler.lookupEntityInteraction.get(player.getName());
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
List<String> results = EntityInteractionLookup.performLookup(command.getName(), statement, player, page, limit, entitySpawnRowId);
|
||||
for (String result : results) {
|
||||
Chat.sendComponent(player, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
finally {
|
||||
LookupThrottle.release(player.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,15 @@ import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
@@ -20,23 +26,43 @@ import com.google.common.base.Strings;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.Lookup;
|
||||
import net.coreprotect.database.logger.ItemLogger;
|
||||
import net.coreprotect.database.lookup.EntityInteractionLookup;
|
||||
import net.coreprotect.database.lookup.PlayerLookup;
|
||||
import net.coreprotect.database.statement.EntitySpawnStatement;
|
||||
import net.coreprotect.database.statement.UserStatement;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
import net.coreprotect.listener.channel.PluginChannelHandshakeListener;
|
||||
import net.coreprotect.listener.channel.PluginChannelListener;
|
||||
import net.coreprotect.model.action.EntityActionFilter;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.model.action.SessionActions;
|
||||
import net.coreprotect.model.entity.EntitySpawnRecord;
|
||||
import net.coreprotect.model.item.InventorySources;
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
import net.coreprotect.model.lookup.EntityLookupContext;
|
||||
import net.coreprotect.model.lookup.LookupCursor;
|
||||
import net.coreprotect.model.lookup.LookupOutputMode;
|
||||
import net.coreprotect.model.lookup.LookupPage;
|
||||
import net.coreprotect.model.lookup.LookupRollbackState;
|
||||
import net.coreprotect.model.lookup.LookupSummaryPage;
|
||||
import net.coreprotect.model.lookup.LookupSummaryRow;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.ChatUtils;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
import net.coreprotect.utility.EntitySpawnTracking;
|
||||
import net.coreprotect.utility.ItemUtils;
|
||||
import net.coreprotect.utility.MaterialUtils;
|
||||
import net.coreprotect.utility.StringUtils;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.LookupThrottle;
|
||||
|
||||
public class StandardLookupThread implements Runnable {
|
||||
private static final int SUMMARY_QUERY_TIMEOUT_SECONDS = 30;
|
||||
private static final AtomicBoolean SUMMARY_LOOKUP_ACTIVE = new AtomicBoolean(false);
|
||||
|
||||
private final CommandSender player;
|
||||
private final Command command;
|
||||
private final List<String> rollbackUsers;
|
||||
@@ -44,6 +70,8 @@ public class StandardLookupThread implements Runnable {
|
||||
private final Map<Object, Boolean> excludedBlocks;
|
||||
private final List<String> excludedUsers;
|
||||
private final List<Integer> actions;
|
||||
private final EntityActionFilter entityActionFilter;
|
||||
private final List<String> messageFilters;
|
||||
private final Integer[] radius;
|
||||
private final Location location;
|
||||
private final int x;
|
||||
@@ -60,9 +88,10 @@ public class StandardLookupThread implements Runnable {
|
||||
private final int displayResults;
|
||||
private final int typeLookup;
|
||||
private final String rtime;
|
||||
private final boolean count;
|
||||
private final LookupOutputMode outputMode;
|
||||
private final LookupRollbackState rollbackState;
|
||||
|
||||
public StandardLookupThread(CommandSender player, Command command, List<String> rollbackUsers, List<Object> blockList, Map<Object, Boolean> excludedBlocks, List<String> excludedUsers, List<Integer> actions, Integer[] radius, Location location, int x, int y, int z, int worldId, int argWorldId, long timeStart, long timeEnd, int noisy, int excluded, int restricted, int page, int displayResults, int typeLookup, String rtime, boolean count) {
|
||||
public StandardLookupThread(CommandSender player, Command command, List<String> rollbackUsers, List<Object> blockList, Map<Object, Boolean> excludedBlocks, List<String> excludedUsers, List<Integer> actions, EntityActionFilter entityActionFilter, List<String> messageFilters, Integer[] radius, Location location, int x, int y, int z, int worldId, int argWorldId, long timeStart, long timeEnd, int noisy, int excluded, int restricted, int page, int displayResults, int typeLookup, String rtime, LookupOutputMode outputMode, LookupRollbackState rollbackState) {
|
||||
this.player = player;
|
||||
this.command = command;
|
||||
this.rollbackUsers = rollbackUsers;
|
||||
@@ -70,6 +99,8 @@ public class StandardLookupThread implements Runnable {
|
||||
this.excludedBlocks = excludedBlocks;
|
||||
this.excludedUsers = excludedUsers;
|
||||
this.actions = actions;
|
||||
this.entityActionFilter = entityActionFilter;
|
||||
this.messageFilters = messageFilters;
|
||||
this.radius = radius;
|
||||
this.location = location;
|
||||
this.x = x;
|
||||
@@ -86,15 +117,33 @@ public class StandardLookupThread implements Runnable {
|
||||
this.displayResults = displayResults;
|
||||
this.typeLookup = typeLookup;
|
||||
this.rtime = rtime;
|
||||
this.count = count;
|
||||
this.outputMode = outputMode;
|
||||
this.rollbackState = rollbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try (Connection connection = Database.getConnection(true)) {
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { true, System.currentTimeMillis() });
|
||||
boolean summaryLookup = outputMode == LookupOutputMode.SUMMARY;
|
||||
if (summaryLookup && !SUMMARY_LOOKUP_ACTIVE.compareAndSet(false, true)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LookupThrottle.tryAcquire(player.getName(), 50)) {
|
||||
if (summaryLookup) {
|
||||
SUMMARY_LOOKUP_ACTIVE.set(false);
|
||||
}
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Database.getConnection(true)) {
|
||||
List<String> uuidList = new ArrayList<>();
|
||||
Integer entityContainerId = actions.contains(5) ? ConfigHandler.lookupEntityContainer.get(player.getName()) : null;
|
||||
ConfigHandler.lookupEntityInteraction.remove(player.getName());
|
||||
if (!actions.contains(5)) {
|
||||
ConfigHandler.lookupEntityContainer.remove(player.getName());
|
||||
}
|
||||
Location finalLocation = location;
|
||||
boolean exists = false;
|
||||
String bc = x + "." + y + "." + z + "." + worldId + "." + timeStart + "." + timeEnd + "." + noisy + "." + excluded + "." + restricted + "." + argWorldId + "." + displayResults;
|
||||
@@ -107,21 +156,31 @@ public class StandardLookupThread implements Runnable {
|
||||
ConfigHandler.lookupBlist.put(player.getName(), blockList);
|
||||
ConfigHandler.lookupUlist.put(player.getName(), rollbackUsers);
|
||||
ConfigHandler.lookupAlist.put(player.getName(), actions);
|
||||
ConfigHandler.lookupEntityActionFilter.put(player.getName(), entityActionFilter);
|
||||
ConfigHandler.lookupFlist.put(player.getName(), messageFilters);
|
||||
ConfigHandler.lookupRadius.put(player.getName(), radius);
|
||||
ConfigHandler.lookupOutputMode.put(player.getName(), outputMode == LookupOutputMode.COUNT ? LookupOutputMode.DETAIL : outputMode);
|
||||
ConfigHandler.lookupRollbackState.put(player.getName(), rollbackState);
|
||||
if (typeLookup != 5 || outputMode != LookupOutputMode.DETAIL || !ConfigHandler.databaseType.isColumnar()) {
|
||||
ConfigHandler.lookupCursor.remove(player.getName());
|
||||
}
|
||||
|
||||
if (connection != null) {
|
||||
Statement statement = connection.createStatement();
|
||||
if (summaryLookup) {
|
||||
statement.setQueryTimeout(SUMMARY_QUERY_TIMEOUT_SECONDS);
|
||||
}
|
||||
String baduser = "";
|
||||
for (String check : rollbackUsers) {
|
||||
if ((!check.equals("#global") && !check.equals("#container")) || actions.contains(9)) {
|
||||
if ((!check.equals("#global") && !check.equals("#container")) || actions.contains(LookupActions.USERNAME)) {
|
||||
exists = PlayerLookup.playerExists(connection, check);
|
||||
if (!exists) {
|
||||
baduser = check;
|
||||
break;
|
||||
}
|
||||
else if (actions.contains(9)) {
|
||||
if (ConfigHandler.uuidCache.get(check.toLowerCase(Locale.ROOT)) != null) {
|
||||
String uuid = ConfigHandler.uuidCache.get(check.toLowerCase(Locale.ROOT));
|
||||
else if (actions.contains(LookupActions.USERNAME)) {
|
||||
String uuid = UserStatement.getUuid(connection, check);
|
||||
if (uuid != null) {
|
||||
uuidList.add(uuid);
|
||||
}
|
||||
}
|
||||
@@ -148,7 +207,7 @@ public class StandardLookupThread implements Runnable {
|
||||
|
||||
if (exists) {
|
||||
List<String> userList = new ArrayList<>();
|
||||
if (!actions.contains(9)) {
|
||||
if (!actions.contains(LookupActions.USERNAME)) {
|
||||
userList = rollbackUsers;
|
||||
}
|
||||
|
||||
@@ -168,15 +227,34 @@ public class StandardLookupThread implements Runnable {
|
||||
finalLocation = new Location(Bukkit.getServer().getWorld(WorldUtils.getWorldName(worldId)), x, y, z);
|
||||
}
|
||||
|
||||
Long[] rowData = new Long[] { 0L, 0L, 0L, 0L };
|
||||
EntityLookupContext entityContext = EntityLookupContext.legacy(Collections.emptySet(), Collections.emptySet());
|
||||
boolean includeEntitySpawns = entityActionFilter.includesAnySpawn(actions, true);
|
||||
boolean includeEntityContainers = entityContainerId != null || actions.contains(LookupActions.CONTAINER) || LookupActions.isInventoryLookup(actions) || actions.isEmpty();
|
||||
boolean includeEntityInteractions = actions.isEmpty() || actions.contains(LookupActions.INTERACTION);
|
||||
if (!ConfigHandler.databaseType.isDuckDB() && (includeEntitySpawns || includeEntityContainers || includeEntityInteractions) && radius != null && finalLocation != null && finalLocation.getWorld() != null) {
|
||||
Set<UUID> databaseCandidates = includeEntityContainers || includeEntityInteractions ? EntitySpawnStatement.loadActiveUuids(connection, finalLocation, radius) : EntitySpawnStatement.loadActiveUuids(connection, finalLocation, radius, timeStart, timeEnd);
|
||||
EntitySpawnTracking.LoadedEntityRadius loadedEntities = EntitySpawnTracking.findLoadedEntities(finalLocation, radius, databaseCandidates);
|
||||
entityContext = EntityLookupContext.legacy(loadedEntities.getInside(), loadedEntities.getLoadedCandidates());
|
||||
}
|
||||
|
||||
Long[] rowData = new Long[] { 0L, 0L, 0L, 0L, 0L };
|
||||
long rowMax = (long) page * displayResults;
|
||||
long pageStart = rowMax - displayResults;
|
||||
long rows = 0L;
|
||||
long recordRows = 0L;
|
||||
boolean checkRows = true;
|
||||
LookupPage lookupPage = null;
|
||||
List<LookupSummaryRow> summaryRows = null;
|
||||
|
||||
if (typeLookup == 5 && page > 1) {
|
||||
rowData = ConfigHandler.lookupRows.get(player.getName());
|
||||
rows = rowData[3];
|
||||
if (rowData == null || rowData.length < 5) {
|
||||
rowData = new Long[] { 0L, 0L, 0L, 0L, 0L };
|
||||
}
|
||||
rows = rowData[4];
|
||||
if (outputMode == LookupOutputMode.SUMMARY) {
|
||||
recordRows = rowData[0];
|
||||
}
|
||||
|
||||
if (pageStart < rows) {
|
||||
checkRows = false;
|
||||
@@ -184,19 +262,103 @@ public class StandardLookupThread implements Runnable {
|
||||
}
|
||||
|
||||
if (checkRows) {
|
||||
rows = Lookup.countLookupRows(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, finalLocation, radius, rowData, timeStart, timeEnd, restrict_world, true);
|
||||
rowData[3] = rows;
|
||||
if (outputMode == LookupOutputMode.SUMMARY) {
|
||||
rows = 0L;
|
||||
recordRows = Lookup.countLookupRows(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, messageFilters, entityContext, finalLocation, radius, rowData, timeStart, timeEnd, restrict_world, true, entityContainerId, rollbackState);
|
||||
if (recordRows > 0 && pageStart == 0 && Lookup.supportsSummaryWindowFunctions(statement)) {
|
||||
LookupSummaryPage summaryPage = Lookup.performSummaryLookupPage(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, entityContext, finalLocation, radius, timeStart, timeEnd, (int) pageStart, displayResults, restrict_world, entityContainerId, rollbackState);
|
||||
rows = summaryPage.getTotalRows();
|
||||
summaryRows = summaryPage.getRows();
|
||||
}
|
||||
else if (recordRows > 0) {
|
||||
rows = Lookup.countSummaryRows(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, entityContext, finalLocation, radius, timeStart, timeEnd, restrict_world, entityContainerId, rollbackState);
|
||||
}
|
||||
rowData[0] = recordRows;
|
||||
rowData[1] = 0L;
|
||||
rowData[2] = 0L;
|
||||
rowData[3] = 0L;
|
||||
}
|
||||
else {
|
||||
if (pageStart == 0 && outputMode == LookupOutputMode.DETAIL && ConfigHandler.databaseType.isDuckDB()) {
|
||||
lookupPage = Lookup.performDuckDBLookupPage(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, messageFilters, entityContext, finalLocation, radius, rowData, timeStart, timeEnd, displayResults, restrict_world, true, entityContainerId, rollbackState);
|
||||
rows = lookupPage.getTotalRows();
|
||||
}
|
||||
else {
|
||||
rows = Lookup.countLookupRows(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, messageFilters, entityContext, finalLocation, radius, rowData, timeStart, timeEnd, restrict_world, true, entityContainerId, rollbackState);
|
||||
}
|
||||
}
|
||||
rowData[4] = rows;
|
||||
ConfigHandler.lookupRows.put(player.getName(), rowData);
|
||||
}
|
||||
if (count) {
|
||||
String row_format = NumberFormat.getInstance().format(rows);
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.LOOKUP_ROWS_FOUND, row_format, (rows == 1 ? Selector.FIRST : Selector.SECOND)));
|
||||
if (lookupPage == null && outputMode == LookupOutputMode.DETAIL && ConfigHandler.databaseType.isColumnar() && pageStart < rows) {
|
||||
LookupCursor cursor = ConfigHandler.lookupCursor.get(player.getName());
|
||||
lookupPage = Lookup.performLookupPage(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, messageFilters, entityContext, finalLocation, radius, rowData, timeStart, timeEnd, (int) pageStart, displayResults, rows, cursor, restrict_world, true, entityContainerId, rollbackState);
|
||||
}
|
||||
if (lookupPage != null) {
|
||||
LookupCursor nextCursor = lookupPage.getNextCursor();
|
||||
if (nextCursor == null) {
|
||||
ConfigHandler.lookupCursor.remove(player.getName());
|
||||
}
|
||||
else {
|
||||
ConfigHandler.lookupCursor.put(player.getName(), nextCursor);
|
||||
}
|
||||
}
|
||||
if (outputMode == LookupOutputMode.COUNT) {
|
||||
outputRowCount(rows);
|
||||
}
|
||||
else if (outputMode == LookupOutputMode.SUMMARY && (pageStart < rows || (pageStart == 0 && recordRows > 0))) {
|
||||
if (summaryRows == null) {
|
||||
summaryRows = rows > 0
|
||||
? Lookup.performSummaryLookup(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, entityContext, finalLocation, radius, timeStart, timeEnd, (int) pageStart, displayResults, restrict_world, entityContainerId, rollbackState)
|
||||
: Collections.emptyList();
|
||||
}
|
||||
outputSummary(connection, summaryRows, rows, recordRows);
|
||||
}
|
||||
else if (pageStart < rows) {
|
||||
List<String[]> lookupList = Lookup.performPartialLookup(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, finalLocation, radius, rowData, timeStart, timeEnd, (int) pageStart, displayResults, restrict_world, true);
|
||||
List<String[]> lookupList = lookupPage == null
|
||||
? Lookup.performPartialLookup(statement, player, uuidList, userList, blockList, excludedBlocks, excludedUsers, actions, entityActionFilter, messageFilters, entityContext, finalLocation, radius, rowData, timeStart, timeEnd, (int) pageStart, displayResults, restrict_world, true, entityContainerId, rollbackState)
|
||||
: lookupPage.getRows();
|
||||
if (lookupList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Integer, EntitySpawnRecord> entitySpawnRecords = Collections.emptyMap();
|
||||
Map<UUID, Location> loadedEntityLocations = Collections.emptyMap();
|
||||
Set<Integer> entitySpawnRowIds = new HashSet<>();
|
||||
for (String[] data : lookupList) {
|
||||
if (data.length > 7 && data[6] != null && data[7] != null && Integer.parseInt(data[7]) == LookupActions.ENTITY_SPAWN) {
|
||||
entitySpawnRowIds.add(Integer.parseInt(data[6]));
|
||||
}
|
||||
if (data.length > 14 && data[13] != null && data[14] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_CONTAINER) {
|
||||
entitySpawnRowIds.add(Integer.parseInt(data[14]));
|
||||
}
|
||||
if (data.length > 14 && data[13] != null && data[14] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION) {
|
||||
entitySpawnRowIds.add(Integer.parseInt(data[14]));
|
||||
}
|
||||
}
|
||||
if (!entitySpawnRowIds.isEmpty()) {
|
||||
entitySpawnRecords = EntitySpawnStatement.loadLocationRecords(connection, entitySpawnRowIds);
|
||||
Set<UUID> entitySpawnUuids = new HashSet<>();
|
||||
for (EntitySpawnRecord record : entitySpawnRecords.values()) {
|
||||
entitySpawnUuids.add(record.getUuid());
|
||||
}
|
||||
try {
|
||||
loadedEntityLocations = EntitySpawnTracking.findLoadedLocations(entitySpawnUuids);
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
Map<String[], EntityDisplayLocation> entityDisplayLocations = new IdentityHashMap<>();
|
||||
for (String[] data : lookupList) {
|
||||
EntityDisplayLocation displayLocation = resolveEntityDisplayLocation(data, entitySpawnRecords, loadedEntityLocations);
|
||||
if (displayLocation != null) {
|
||||
entityDisplayLocations.put(data, displayLocation);
|
||||
}
|
||||
}
|
||||
|
||||
Chat.sendMessage(player, Color.WHITE + "----- " + Color.DARK_AQUA + Phrase.build(Phrase.LOOKUP_HEADER, "CoreProtect" + Color.WHITE + " | " + Color.DARK_AQUA) + Color.WHITE + " -----");
|
||||
if (actions.contains(6) || actions.contains(7)) { // Chat/command
|
||||
if (actions.contains(LookupActions.CHAT) || actions.contains(LookupActions.COMMAND)) {
|
||||
for (String[] data : lookupList) {
|
||||
String time = data[0];
|
||||
String dplayer = data[1];
|
||||
@@ -212,7 +374,7 @@ public class StandardLookupThread implements Runnable {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (actions.contains(8)) { // login/logouts
|
||||
else if (actions.contains(LookupActions.SESSION)) {
|
||||
for (String[] data : lookupList) {
|
||||
String time = data[0];
|
||||
String dplayer = data[1];
|
||||
@@ -231,23 +393,23 @@ public class StandardLookupThread implements Runnable {
|
||||
leftPadding = leftPadding + Color.WHITE + Strings.padStart("", (timeLength - 50) / 4, ' ');
|
||||
}
|
||||
|
||||
String tag = (action != 0 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Color.DARK_AQUA + Phrase.build(Phrase.LOOKUP_LOGIN, Color.DARK_AQUA + dplayer + Color.WHITE, (action != 0 ? Selector.FIRST : Selector.SECOND)));
|
||||
String tag = (action != SessionActions.LOGOUT ? Color.GREEN + "+" : Color.RED + "-");
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Color.DARK_AQUA + Phrase.build(Phrase.LOOKUP_LOGIN, Color.DARK_AQUA + dplayer + Color.WHITE, (action != SessionActions.LOGOUT ? Selector.FIRST : Selector.SECOND)));
|
||||
Chat.sendComponent(player, Color.WHITE + leftPadding + Color.GREY + "^ " + ChatUtils.getCoordinates(command.getName(), wid, dataX, dataY, dataZ, true, true) + "");
|
||||
PluginChannelListener.getInstance().sendInfoData(player, Integer.parseInt(time), Phrase.LOOKUP_LOGIN, (action != 0 ? Selector.FIRST : Selector.SECOND), dplayer, -1, dataX, dataY, dataZ, wid);
|
||||
PluginChannelListener.getInstance().sendInfoData(player, Integer.parseInt(time), Phrase.LOOKUP_LOGIN, (action != SessionActions.LOGOUT ? Selector.FIRST : Selector.SECOND), dplayer, -1, dataX, dataY, dataZ, wid);
|
||||
}
|
||||
}
|
||||
else if (actions.contains(9)) { // username-changes
|
||||
else if (actions.contains(LookupActions.USERNAME)) {
|
||||
for (String[] data : lookupList) {
|
||||
String time = data[0];
|
||||
String user = ConfigHandler.uuidCacheReversed.get(data[1]);
|
||||
String user = UserStatement.getNameByUuid(connection, data[1]);
|
||||
String username = data[2];
|
||||
String timeago = ChatUtils.getTimeSince(Integer.parseInt(time), unixtimestamp, true);
|
||||
Chat.sendComponent(player, timeago + " " + Color.WHITE + "- " + Phrase.build(Phrase.LOOKUP_USERNAME, Color.DARK_AQUA + user + Color.WHITE, Color.DARK_AQUA + username + Color.WHITE));
|
||||
PluginChannelListener.getInstance().sendUsernameData(player, Integer.parseInt(time), user, username);
|
||||
}
|
||||
}
|
||||
else if (actions.contains(10)) { // sign messages
|
||||
else if (actions.contains(LookupActions.SIGN)) {
|
||||
for (String[] data : lookupList) {
|
||||
String time = data[0];
|
||||
String dplayer = data[1];
|
||||
@@ -271,7 +433,7 @@ public class StandardLookupThread implements Runnable {
|
||||
PluginChannelListener.getInstance().sendMessageData(player, Integer.parseInt(time), dplayer, message, true, dataX, dataY, dataZ, wid);
|
||||
}
|
||||
}
|
||||
else if (actions.contains(4) && actions.contains(11)) { // inventory transactions
|
||||
else if (LookupActions.isInventoryLookup(actions)) {
|
||||
for (String[] data : lookupList) {
|
||||
String time = data[0];
|
||||
String dplayer = data[1];
|
||||
@@ -283,41 +445,50 @@ public class StandardLookupThread implements Runnable {
|
||||
int dataX = Integer.parseInt(data[2]);
|
||||
int dataY = Integer.parseInt(data[3]);
|
||||
int dataZ = Integer.parseInt(data[4]);
|
||||
String rbd = ((Integer.parseInt(data[8]) == 2 || Integer.parseInt(data[8]) == 3) ? Color.STRIKETHROUGH : "");
|
||||
EntityDisplayLocation entityLocation = entityDisplayLocations.get(data);
|
||||
if (entityLocation != null) {
|
||||
wid = entityLocation.worldId;
|
||||
dataX = entityLocation.x;
|
||||
dataY = entityLocation.y;
|
||||
dataZ = entityLocation.z;
|
||||
}
|
||||
String rbd = MaterialUtils.rolledBack(Integer.parseInt(data[8]), true) == 1 ? Color.STRIKETHROUGH : "";
|
||||
String timeago = ChatUtils.getTimeSince(Integer.parseInt(time), unixtimestamp, true);
|
||||
Material blockType = ItemUtils.itemFilter(MaterialUtils.getType(dtype), (Integer.parseInt(data[13]) == 0));
|
||||
String dname = StringUtils.nameFilter(blockType.name().toLowerCase(Locale.ROOT), ddata);
|
||||
byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1);
|
||||
String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount);
|
||||
Integer itemId = ItemUtils.makeGivableItem(ItemUtils.getItemStack(metadata, dtype, amount));
|
||||
|
||||
String selector = Selector.FIRST;
|
||||
String tag = Color.WHITE + "-";
|
||||
if (daction == 2 || daction == 3) { // LOOKUP_ITEM
|
||||
selector = (daction != 2 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != 2 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
if (daction == ItemTransactionActions.DROP || daction == ItemTransactionActions.PICKUP) {
|
||||
selector = (daction != ItemTransactionActions.DROP ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != ItemTransactionActions.DROP ? Color.GREEN + "+" : Color.RED + "-");
|
||||
}
|
||||
else if (daction == 4 || daction == 5) { // LOOKUP_STORAGE
|
||||
selector = (daction == 4 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == 4 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
else if (daction == ItemTransactionActions.REMOVE_ENDER || daction == ItemTransactionActions.ADD_ENDER) {
|
||||
selector = (daction == ItemTransactionActions.REMOVE_ENDER ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == ItemTransactionActions.REMOVE_ENDER ? Color.GREEN + "+" : Color.RED + "-");
|
||||
}
|
||||
else if (daction == 6 || daction == 7) { // LOOKUP_PROJECTILE
|
||||
else if (daction == ItemTransactionActions.THROW || daction == ItemTransactionActions.SHOOT) {
|
||||
selector = Selector.SECOND;
|
||||
tag = Color.RED + "-";
|
||||
}
|
||||
else if (daction == ItemLogger.ITEM_BREAK || daction == ItemLogger.ITEM_DESTROY || daction == ItemLogger.ITEM_CREATE) {
|
||||
selector = (daction == ItemLogger.ITEM_CREATE ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == ItemLogger.ITEM_CREATE ? Color.GREEN + "+" : Color.RED + "-");
|
||||
else if (daction == ItemTransactionActions.BREAK || daction == ItemTransactionActions.DESTROY || daction == ItemTransactionActions.CREATE) {
|
||||
selector = (daction == ItemTransactionActions.CREATE ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == ItemTransactionActions.CREATE ? Color.GREEN + "+" : Color.RED + "-");
|
||||
}
|
||||
else if (daction == ItemLogger.ITEM_SELL || daction == ItemLogger.ITEM_BUY) { // LOOKUP_TRADE
|
||||
selector = (daction == ItemLogger.ITEM_BUY ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == ItemLogger.ITEM_BUY ? Color.GREEN + "+" : Color.RED + "-");
|
||||
else if (daction == ItemTransactionActions.SELL || daction == ItemTransactionActions.BUY) { // LOOKUP_TRADE
|
||||
selector = (daction == ItemTransactionActions.BUY ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == ItemTransactionActions.BUY ? Color.GREEN + "+" : Color.RED + "-");
|
||||
}
|
||||
else { // LOOKUP_CONTAINER
|
||||
selector = (daction == 0 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == 0 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
selector = (daction == ItemTransactionActions.REMOVE ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction == ItemTransactionActions.REMOVE ? Color.GREEN + "+" : Color.RED + "-");
|
||||
}
|
||||
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Phrase.build(Phrase.LOOKUP_CONTAINER, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, "x" + amount, ChatUtils.createTooltip(Color.DARK_AQUA + rbd + dname, tooltip) + Color.WHITE, selector));
|
||||
String coordinateInfo = entityLocation == null ? "" : entityLocation.getOriginTooltip();
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Phrase.build(Phrase.LOOKUP_CONTAINER, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, "x" + amount, ChatUtils.createTooltip(Color.DARK_AQUA + rbd + dname, tooltip) + coordinateInfo + ChatUtils.filterComponent(player.hasPermission("coreprotect.give"), ChatUtils.createGiveItemComponent(Color.GREY + "(↓)", command.getName(), itemId)) + Color.WHITE, selector));
|
||||
PluginChannelListener.getInstance().sendData(player, Integer.parseInt(time), Phrase.LOOKUP_CONTAINER, selector, dplayer, dname, amount, dataX, dataY, dataZ, wid, rbd, true, tag.contains("+"));
|
||||
}
|
||||
}
|
||||
@@ -325,7 +496,7 @@ public class StandardLookupThread implements Runnable {
|
||||
for (String[] data : lookupList) {
|
||||
int drb = Integer.parseInt(data[8]);
|
||||
String rbd = "";
|
||||
if (drb == 1 || drb == 3) {
|
||||
if (MaterialUtils.rolledBack(drb, actions.contains(LookupActions.ITEM)) == 1) {
|
||||
rbd = Color.STRIKETHROUGH;
|
||||
}
|
||||
|
||||
@@ -337,8 +508,18 @@ public class StandardLookupThread implements Runnable {
|
||||
int dtype = Integer.parseInt(data[5]);
|
||||
int ddata = Integer.parseInt(data[6]);
|
||||
int daction = Integer.parseInt(data[7]);
|
||||
boolean entityInteraction = data[13] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION;
|
||||
boolean placedEntitySpawn = daction == LookupActions.ENTITY_SPAWN && EntitySpawnTracking.isPlacedEntityType(EntityUtils.getEntityType(dtype));
|
||||
boolean placedEntityKill = daction == LookupActions.ENTITY_KILL && EntitySpawnTracking.isPlacedEntityType(EntityUtils.getEntityType(dtype));
|
||||
int wid = Integer.parseInt(data[9]);
|
||||
int amount = Integer.parseInt(data[10]);
|
||||
EntityDisplayLocation entityLocation = entityDisplayLocations.get(data);
|
||||
if (entityLocation != null) {
|
||||
wid = entityLocation.worldId;
|
||||
dataX = entityLocation.x;
|
||||
dataY = entityLocation.y;
|
||||
dataZ = entityLocation.z;
|
||||
}
|
||||
String tag = Color.WHITE + "-";
|
||||
|
||||
String timeago = ChatUtils.getTimeSince(Integer.parseInt(time), unixtimestamp, true);
|
||||
@@ -353,12 +534,12 @@ public class StandardLookupThread implements Runnable {
|
||||
|
||||
String dname = "";
|
||||
boolean isPlayer = false;
|
||||
if (daction == 3 && !actions.contains(11) && amount == -1) {
|
||||
if (dtype == 0) {
|
||||
if (ConfigHandler.playerIdCacheReversed.get(ddata) == null) {
|
||||
UserStatement.loadName(connection, ddata);
|
||||
}
|
||||
dname = ConfigHandler.playerIdCacheReversed.get(ddata);
|
||||
if (entityInteraction) {
|
||||
dname = EntityInteractionLookup.entityName(dtype);
|
||||
}
|
||||
else if ((daction == LookupActions.ENTITY_KILL || daction == LookupActions.ENTITY_SPAWN) && !actions.contains(LookupActions.ITEM) && amount == -1) {
|
||||
if (daction == LookupActions.ENTITY_KILL && dtype == 0) {
|
||||
dname = UserStatement.getName(connection, ddata);
|
||||
isPlayer = true;
|
||||
}
|
||||
else {
|
||||
@@ -366,10 +547,9 @@ public class StandardLookupThread implements Runnable {
|
||||
}
|
||||
}
|
||||
else {
|
||||
dname = MaterialUtils.getType(dtype).name().toLowerCase(Locale.ROOT);
|
||||
dname = StringUtils.nameFilter(dname, ddata);
|
||||
dname = MaterialUtils.getBlockDisplayName(dtype, ddata);
|
||||
}
|
||||
if (dname.length() > 0 && !isPlayer) {
|
||||
if (dname.length() > 0 && !isPlayer && !dname.contains(":")) {
|
||||
dname = "minecraft:" + dname.toLowerCase(Locale.ROOT) + "";
|
||||
}
|
||||
|
||||
@@ -384,49 +564,69 @@ public class StandardLookupThread implements Runnable {
|
||||
Phrase phrase = Phrase.LOOKUP_BLOCK;
|
||||
String selector = Selector.FIRST;
|
||||
String action = "a:block";
|
||||
if (actions.contains(4) || actions.contains(5) || actions.contains(11) || amount > -1) {
|
||||
if (actions.contains(LookupActions.CONTAINER) || actions.contains(5) || actions.contains(LookupActions.ITEM) || amount > -1) {
|
||||
byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1);
|
||||
String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount);
|
||||
Integer itemId = ItemUtils.makeGivableItem(ItemUtils.getItemStack(metadata, dtype, amount));
|
||||
|
||||
if (daction == 2 || daction == 3) {
|
||||
if (daction == ItemTransactionActions.DROP || daction == ItemTransactionActions.PICKUP) {
|
||||
phrase = Phrase.LOOKUP_ITEM; // {picked up|dropped}
|
||||
selector = (daction != 2 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != 2 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
selector = (daction != ItemTransactionActions.DROP ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != ItemTransactionActions.DROP ? Color.GREEN + "+" : Color.RED + "-");
|
||||
action = "a:item";
|
||||
}
|
||||
else if (daction == 4 || daction == 5) {
|
||||
else if (daction == ItemTransactionActions.REMOVE_ENDER || daction == ItemTransactionActions.ADD_ENDER) {
|
||||
phrase = Phrase.LOOKUP_STORAGE; // {deposited|withdrew}
|
||||
selector = (daction != 4 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != 4 ? Color.RED + "-" : Color.GREEN + "+");
|
||||
selector = (daction != ItemTransactionActions.REMOVE_ENDER ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != ItemTransactionActions.REMOVE_ENDER ? Color.RED + "-" : Color.GREEN + "+");
|
||||
action = "a:item";
|
||||
}
|
||||
else if (daction == 6 || daction == 7) {
|
||||
else if (daction == ItemTransactionActions.THROW || daction == ItemTransactionActions.SHOOT) {
|
||||
phrase = Phrase.LOOKUP_PROJECTILE; // {threw|shot}
|
||||
selector = (daction != 7 ? Selector.FIRST : Selector.SECOND);
|
||||
selector = (daction != ItemTransactionActions.SHOOT ? Selector.FIRST : Selector.SECOND);
|
||||
tag = Color.RED + "-";
|
||||
action = "a:item";
|
||||
}
|
||||
else {
|
||||
phrase = Phrase.LOOKUP_CONTAINER; // {added|removed}
|
||||
selector = (daction != 0 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != 0 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
selector = (daction != ItemTransactionActions.REMOVE ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != ItemTransactionActions.REMOVE ? Color.GREEN + "+" : Color.RED + "-");
|
||||
action = "a:container";
|
||||
}
|
||||
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Phrase.build(phrase, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, "x" + amount, ChatUtils.createTooltip(Color.DARK_AQUA + rbd + dname, tooltip) + Color.WHITE, selector));
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Phrase.build(phrase, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, "x" + amount, ChatUtils.createTooltip(Color.DARK_AQUA + rbd + dname, tooltip) + ChatUtils.filterComponent(player.hasPermission("coreprotect.give"), ChatUtils.createGiveItemComponent(Color.GREY + "(↓)", command.getName(), itemId)) + Color.WHITE, selector));
|
||||
PluginChannelListener.getInstance().sendData(player, Integer.parseInt(time), phrase, selector, dplayer, dname, (tag.contains("+") ? 1 : -1), dataX, dataY, dataZ, wid, rbd, action.contains("container"), tag.contains("+"));
|
||||
}
|
||||
else {
|
||||
if (daction == 2 || daction == 3) {
|
||||
phrase = Phrase.LOOKUP_INTERACTION; // {clicked|killed}
|
||||
selector = (daction != 3 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != 3 ? Color.WHITE + "-" : Color.RED + "-");
|
||||
action = (daction == 2 ? "a:click" : "a:kill");
|
||||
if (entityInteraction) {
|
||||
phrase = Phrase.LOOKUP_ENTITY_INTERACTION;
|
||||
selector = EntityInteractionLookup.actionSelector(ddata);
|
||||
action = "a:click";
|
||||
}
|
||||
else if (daction == LookupActions.ENTITY_SPAWN) {
|
||||
phrase = placedEntitySpawn ? Phrase.LOOKUP_BLOCK : Phrase.LOOKUP_ENTITY_SPAWN;
|
||||
selector = Selector.FIRST;
|
||||
tag = Color.GREEN + "+";
|
||||
action = placedEntitySpawn ? "a:block" : "a:spawn";
|
||||
}
|
||||
else if (daction == LookupActions.INTERACTION || daction == LookupActions.ENTITY_KILL) {
|
||||
if (placedEntityKill) {
|
||||
phrase = Phrase.LOOKUP_BLOCK;
|
||||
selector = Selector.SECOND;
|
||||
tag = Color.RED + "-";
|
||||
action = "a:block";
|
||||
}
|
||||
else {
|
||||
phrase = Phrase.LOOKUP_INTERACTION; // {clicked|killed}
|
||||
selector = (daction != LookupActions.ENTITY_KILL ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != LookupActions.ENTITY_KILL ? Color.WHITE + "-" : Color.RED + "-");
|
||||
action = (daction == LookupActions.INTERACTION ? "a:click" : "a:kill");
|
||||
}
|
||||
}
|
||||
else {
|
||||
phrase = Phrase.LOOKUP_BLOCK; // {placed|broke}
|
||||
selector = (daction != 0 ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != 0 ? Color.GREEN + "+" : Color.RED + "-");
|
||||
selector = (daction != LookupActions.BLOCK_BREAK ? Selector.FIRST : Selector.SECOND);
|
||||
tag = (daction != LookupActions.BLOCK_BREAK ? Color.GREEN + "+" : Color.RED + "-");
|
||||
}
|
||||
|
||||
Chat.sendComponent(player, timeago + " " + tag + " " + Phrase.build(phrase, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, Color.DARK_AQUA + rbd + dname + Color.WHITE, selector));
|
||||
@@ -434,18 +634,20 @@ public class StandardLookupThread implements Runnable {
|
||||
}
|
||||
|
||||
action = (actions.size() == 0 ? " (" + action + ")" : "");
|
||||
Chat.sendComponent(player, Color.WHITE + leftPadding + Color.GREY + "^ " + ChatUtils.getCoordinates(command.getName(), wid, dataX, dataY, dataZ, true, true) + Color.GREY + Color.ITALIC + action);
|
||||
String coordinates = ChatUtils.getCoordinates(command.getName(), wid, dataX, dataY, dataZ, true, true);
|
||||
String coordinateInfo = entityLocation == null ? "" : entityLocation.getOriginTooltip();
|
||||
Chat.sendComponent(player, Color.WHITE + leftPadding + Color.GREY + "^ " + coordinates + Color.GREY + Color.ITALIC + action + coordinateInfo);
|
||||
}
|
||||
}
|
||||
if (rows > displayResults) {
|
||||
int total_pages = (int) Math.ceil(rows / (displayResults + 0.0));
|
||||
if (actions.contains(6) || actions.contains(7) || actions.contains(9) || (actions.contains(4) && actions.contains(11))) {
|
||||
if (actions.contains(LookupActions.CHAT) || actions.contains(LookupActions.COMMAND) || actions.contains(LookupActions.USERNAME) || LookupActions.isInventoryLookup(actions)) {
|
||||
Chat.sendMessage(player, "-----");
|
||||
}
|
||||
Chat.sendComponent(player, ChatUtils.getPageNavigation(command.getName(), page, total_pages));
|
||||
}
|
||||
}
|
||||
else if (rows > 0) {
|
||||
else if (rows > 0 || (outputMode == LookupOutputMode.SUMMARY && recordRows > 0)) {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_RESULTS_PAGE, Selector.FIRST));
|
||||
}
|
||||
else {
|
||||
@@ -462,9 +664,133 @@ public class StandardLookupThread implements Runnable {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
finally {
|
||||
if (summaryLookup) {
|
||||
SUMMARY_LOOKUP_ACTIVE.set(false);
|
||||
}
|
||||
LookupThrottle.release(player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void outputRowCount(long rows) {
|
||||
String formattedRows = NumberFormat.getInstance().format(rows);
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.LOOKUP_ROWS_FOUND, formattedRows, rows == 1 ? Selector.FIRST : Selector.SECOND));
|
||||
}
|
||||
|
||||
private void outputSummary(Connection connection, List<LookupSummaryRow> summaryRows, long totalRows, long recordRows) {
|
||||
if (summaryRows.isEmpty()) {
|
||||
if (page == 1 && recordRows > 0) {
|
||||
outputRowCount(recordRows);
|
||||
}
|
||||
else {
|
||||
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_RESULTS_PAGE, Selector.FIRST));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { false, System.currentTimeMillis() });
|
||||
NumberFormat numberFormat = NumberFormat.getInstance();
|
||||
String rowsFound = Phrase.build(Phrase.LOOKUP_ROWS_FOUND, numberFormat.format(recordRows), recordRows == 1 ? Selector.FIRST : Selector.SECOND);
|
||||
Chat.sendMessage(player, Color.WHITE + "----- " + Color.DARK_AQUA + "CoreProtect" + Color.WHITE + " | " + Color.DARK_AQUA + rowsFound + Color.WHITE + " -----");
|
||||
for (LookupSummaryRow row : summaryRows) {
|
||||
String userName = UserStatement.getName(connection, row.getUserId());
|
||||
if (userName == null || userName.isEmpty()) {
|
||||
userName = "unknown";
|
||||
}
|
||||
|
||||
String materialName = MaterialUtils.getBlockDisplayName(row.getMaterialId(), 0);
|
||||
if (materialName == null || materialName.isEmpty()) {
|
||||
materialName = MaterialUtils.getBlockNameShort(row.getMaterialId());
|
||||
}
|
||||
if (materialName == null || materialName.isEmpty()) {
|
||||
materialName = "#" + row.getMaterialId();
|
||||
}
|
||||
if (materialName.startsWith("minecraft:")) {
|
||||
materialName = materialName.substring("minecraft:".length());
|
||||
}
|
||||
|
||||
long removedAmount = row.getRemovedAmount();
|
||||
long placedAmount = row.getPlacedAmount();
|
||||
long netAmount = row.getAmount();
|
||||
String formattedNetAmount = (netAmount >= 0 ? "+" : "") + numberFormat.format(netAmount);
|
||||
Chat.sendComponent(player, Color.DARK_AQUA + userName + Color.WHITE + ": " + Color.RED + "-" + numberFormat.format(removedAmount)
|
||||
+ Color.GREY + " / " + Color.GREEN + "+" + numberFormat.format(placedAmount) + Color.WHITE + " = " + formattedNetAmount
|
||||
+ Color.WHITE + " " + Color.DARK_AQUA + materialName + Color.WHITE);
|
||||
}
|
||||
|
||||
if (totalRows > displayResults) {
|
||||
int totalPages = (int) Math.ceil(totalRows / (displayResults + 0.0));
|
||||
Chat.sendComponent(player, ChatUtils.getPageNavigation(command.getName(), page, totalPages));
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityDisplayLocation resolveEntityDisplayLocation(String[] data, Map<Integer, EntitySpawnRecord> records, Map<UUID, Location> loadedLocations) {
|
||||
if (data.length <= 14 || data[7] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int trackingRowId = 0;
|
||||
if (data[13] != null && (Integer.parseInt(data[13]) == InventorySources.ENTITY_CONTAINER || Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION) && data[14] != null) {
|
||||
trackingRowId = Integer.parseInt(data[14]);
|
||||
}
|
||||
else if (data[6] != null && Integer.parseInt(data[7]) == LookupActions.ENTITY_SPAWN) {
|
||||
trackingRowId = Integer.parseInt(data[6]);
|
||||
}
|
||||
if (trackingRowId == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EntitySpawnRecord record = records.get(trackingRowId);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int currentWorldId = record.getWorldId();
|
||||
int currentX = (int) Math.floor(record.getX());
|
||||
int currentY = (int) Math.floor(record.getY());
|
||||
int currentZ = (int) Math.floor(record.getZ());
|
||||
Location loadedLocation = loadedLocations.get(record.getUuid());
|
||||
if (loadedLocation != null && loadedLocation.getWorld() != null) {
|
||||
currentWorldId = WorldUtils.getWorldId(loadedLocation.getWorld().getName());
|
||||
currentX = loadedLocation.getBlockX();
|
||||
currentY = loadedLocation.getBlockY();
|
||||
currentZ = loadedLocation.getBlockZ();
|
||||
}
|
||||
|
||||
boolean interactionOrigin = data[13] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION;
|
||||
return new EntityDisplayLocation(currentWorldId, currentX, currentY, currentZ, Integer.parseInt(data[9]), Integer.parseInt(data[2]), Integer.parseInt(data[3]), Integer.parseInt(data[4]), interactionOrigin);
|
||||
}
|
||||
|
||||
private static final class EntityDisplayLocation {
|
||||
private final int worldId;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final int originWorldId;
|
||||
private final int originX;
|
||||
private final int originY;
|
||||
private final int originZ;
|
||||
private final boolean interactionOrigin;
|
||||
|
||||
private EntityDisplayLocation(int worldId, int x, int y, int z, int originWorldId, int originX, int originY, int originZ, boolean interactionOrigin) {
|
||||
this.worldId = worldId;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.originWorldId = originWorldId;
|
||||
this.originX = originX;
|
||||
this.originY = originY;
|
||||
this.originZ = originZ;
|
||||
this.interactionOrigin = interactionOrigin;
|
||||
}
|
||||
|
||||
private String getOriginTooltip() {
|
||||
if (worldId == originWorldId && x == originX && y == originY && z == originZ) {
|
||||
return "";
|
||||
}
|
||||
Phrase originPhrase = interactionOrigin ? Phrase.LOOKUP_ENTITY_INTERACTION_ORIGIN : Phrase.LOOKUP_ENTITY_ORIGIN;
|
||||
return ChatUtils.getCoordinateTooltip(originWorldId, originX, originY, originZ, Phrase.build(originPhrase), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import net.coreprotect.model.action.EntityActionFilter;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.model.action.SessionActions;
|
||||
import net.coreprotect.model.item.ItemTransactionActions;
|
||||
|
||||
/**
|
||||
* Parser for action-related command arguments
|
||||
*/
|
||||
@@ -32,8 +37,17 @@ public class ActionParser {
|
||||
* @return A list of action integers
|
||||
*/
|
||||
public static List<Integer> parseAction(String[] inputArguments) {
|
||||
return parseAction(inputArguments, false);
|
||||
}
|
||||
|
||||
public static List<Integer> parseAction(String[] inputArguments, boolean allowMultiple) {
|
||||
return parseActions(inputArguments, allowMultiple).getActions();
|
||||
}
|
||||
|
||||
public static ParseResult parseActions(String[] inputArguments, boolean allowMultiple) {
|
||||
String[] argumentArray = inputArguments.clone();
|
||||
List<Integer> result = new ArrayList<>();
|
||||
EntityActionFilter entityActionFilter = EntityActionFilter.DEFAULT;
|
||||
int count = 0;
|
||||
int next = 0;
|
||||
for (String argument : argumentArray) {
|
||||
@@ -42,90 +56,112 @@ public class ActionParser {
|
||||
argument = argument.replaceAll("\\\\", "");
|
||||
argument = argument.replaceAll("'", "");
|
||||
|
||||
if (next == 1 && MessageFilterParser.isLookupTerminator(argument)) {
|
||||
next = 0;
|
||||
}
|
||||
if (argument.equals("a:") || argument.equals("action:")) {
|
||||
next = 1;
|
||||
}
|
||||
else if (next == 1 || argument.startsWith("a:") || argument.startsWith("action:")) {
|
||||
result.clear();
|
||||
entityActionFilter = EntityActionFilter.NONE;
|
||||
argument = argument.replaceAll("action:", "");
|
||||
argument = argument.replaceAll("a:", "");
|
||||
if (argument.startsWith("#")) {
|
||||
argument = argument.replaceFirst("#", "");
|
||||
}
|
||||
if (allowMultiple && argument.contains(",")) {
|
||||
for (String action : argument.split(",", -1)) {
|
||||
if (!action.isEmpty()) {
|
||||
ParseResult parsedAction = parseActions(new String[] { "lookup", "a:" + action }, false);
|
||||
result.addAll(parsedAction.getActions());
|
||||
entityActionFilter = entityActionFilter.merge(parsedAction.getEntityActionFilter());
|
||||
}
|
||||
}
|
||||
next = 0;
|
||||
continue;
|
||||
}
|
||||
if (argument.equals("broke") || argument.equals("break") || argument.equals("remove") || argument.equals("destroy") || argument.equals("block-break") || argument.equals("block-remove") || argument.equals("-block") || argument.equals("-blocks") || argument.equals("block-")) {
|
||||
result.add(0);
|
||||
result.add(LookupActions.BLOCK_BREAK);
|
||||
entityActionFilter = EntityActionFilter.ALIASED;
|
||||
}
|
||||
else if (argument.equals("placed") || argument.equals("place") || argument.equals("block-place") || argument.equals("+block") || argument.equals("+blocks") || argument.equals("block+")) {
|
||||
result.add(1);
|
||||
result.add(LookupActions.BLOCK_PLACE);
|
||||
entityActionFilter = EntityActionFilter.ALIASED;
|
||||
}
|
||||
else if (argument.equals("block") || argument.equals("blocks") || argument.equals("block-change") || argument.equals("change") || argument.equals("changes")) {
|
||||
result.add(0);
|
||||
result.add(1);
|
||||
result.add(LookupActions.BLOCK_BREAK);
|
||||
result.add(LookupActions.BLOCK_PLACE);
|
||||
entityActionFilter = EntityActionFilter.ALIASED;
|
||||
}
|
||||
else if (argument.equals("click") || argument.equals("clicks") || argument.equals("interact") || argument.equals("interaction") || argument.equals("player-interact") || argument.equals("player-interaction") || argument.equals("player-click")) {
|
||||
result.add(2);
|
||||
result.add(LookupActions.INTERACTION);
|
||||
}
|
||||
else if (argument.equals("death") || argument.equals("deaths") || argument.equals("entity-death") || argument.equals("entity-deaths") || argument.equals("kill") || argument.equals("kills") || argument.equals("entity-kill") || argument.equals("entity-kills")) {
|
||||
result.add(3);
|
||||
result.add(LookupActions.ENTITY_KILL);
|
||||
}
|
||||
else if (argument.equals("spawn") || argument.equals("spawns") || argument.equals("entity-spawn") || argument.equals("entity-spawns")) {
|
||||
result.add(LookupActions.ENTITY_SPAWN);
|
||||
entityActionFilter = EntityActionFilter.SPAWNED;
|
||||
}
|
||||
else if (argument.equals("container") || argument.equals("container-change") || argument.equals("containers") || argument.equals("chest") || argument.equals("transaction") || argument.equals("transactions")) {
|
||||
result.add(4);
|
||||
result.add(LookupActions.CONTAINER);
|
||||
}
|
||||
else if (argument.equals("-container") || argument.equals("container-") || argument.equals("remove-container")) {
|
||||
result.add(4);
|
||||
result.add(0);
|
||||
result.add(LookupActions.CONTAINER);
|
||||
result.add(ItemTransactionActions.REMOVE);
|
||||
}
|
||||
else if (argument.equals("+container") || argument.equals("container+") || argument.equals("container-add") || argument.equals("add-container")) {
|
||||
result.add(4);
|
||||
result.add(1);
|
||||
result.add(LookupActions.CONTAINER);
|
||||
result.add(ItemTransactionActions.ADD);
|
||||
}
|
||||
else if (argument.equals("chat") || argument.equals("chats")) {
|
||||
result.add(6);
|
||||
else if (argument.equals("chat") || argument.equals("chats") || argument.equals("message") || argument.equals("messages")) {
|
||||
result.add(LookupActions.CHAT);
|
||||
}
|
||||
else if (argument.equals("command") || argument.equals("commands")) {
|
||||
result.add(7);
|
||||
result.add(LookupActions.COMMAND);
|
||||
}
|
||||
else if (argument.equals("logins") || argument.equals("login") || argument.equals("+session") || argument.equals("+sessions") || argument.equals("session+") || argument.equals("+connection") || argument.equals("connection+")) {
|
||||
result.add(8);
|
||||
result.add(1);
|
||||
result.add(LookupActions.SESSION);
|
||||
result.add(SessionActions.LOGIN);
|
||||
}
|
||||
else if (argument.equals("logout") || argument.equals("logouts") || argument.equals("-session") || argument.equals("-sessions") || argument.equals("session-") || argument.equals("-connection") || argument.equals("connection-")) {
|
||||
result.add(8);
|
||||
result.add(0);
|
||||
result.add(LookupActions.SESSION);
|
||||
result.add(SessionActions.LOGOUT);
|
||||
}
|
||||
else if (argument.equals("session") || argument.equals("sessions") || argument.equals("connection") || argument.equals("connections")) {
|
||||
result.add(8);
|
||||
result.add(LookupActions.SESSION);
|
||||
}
|
||||
else if (argument.equals("username") || argument.equals("usernames") || argument.equals("user") || argument.equals("users") || argument.equals("name") || argument.equals("names") || argument.equals("uuid") || argument.equals("uuids") || argument.equals("username-change") || argument.equals("username-changes") || argument.equals("name-change") || argument.equals("name-changes")) {
|
||||
result.add(9);
|
||||
result.add(LookupActions.USERNAME);
|
||||
}
|
||||
else if (argument.equals("sign") || argument.equals("signs")) {
|
||||
result.add(10);
|
||||
result.add(LookupActions.SIGN);
|
||||
}
|
||||
else if (argument.equals("inv") || argument.equals("inventory") || argument.equals("inventories")) {
|
||||
result.add(4); // container
|
||||
result.add(11); // item
|
||||
result.add(LookupActions.CONTAINER);
|
||||
result.add(LookupActions.ITEM);
|
||||
}
|
||||
else if (argument.equals("-inv") || argument.equals("inv-") || argument.equals("-inventory") || argument.equals("inventory-") || argument.equals("-inventories")) {
|
||||
result.add(4);
|
||||
result.add(11);
|
||||
result.add(1);
|
||||
result.add(LookupActions.CONTAINER);
|
||||
result.add(LookupActions.ITEM);
|
||||
result.add(ItemTransactionActions.ADD);
|
||||
}
|
||||
else if (argument.equals("+inv") || argument.equals("inv+") || argument.equals("+inventory") || argument.equals("inventory+") || argument.equals("+inventories")) {
|
||||
result.add(4);
|
||||
result.add(11);
|
||||
result.add(0);
|
||||
result.add(LookupActions.CONTAINER);
|
||||
result.add(LookupActions.ITEM);
|
||||
result.add(ItemTransactionActions.REMOVE);
|
||||
}
|
||||
else if (argument.equals("item") || argument.equals("items")) {
|
||||
result.add(11);
|
||||
result.add(LookupActions.ITEM);
|
||||
}
|
||||
else if (argument.equals("-item") || argument.equals("item-") || argument.equals("-items") || argument.equals("items-") || argument.equals("drop") || argument.equals("drops") || argument.equals("deposit") || argument.equals("deposits") || argument.equals("deposited")) {
|
||||
result.add(11);
|
||||
result.add(0);
|
||||
result.add(LookupActions.ITEM);
|
||||
result.add(ItemTransactionActions.REMOVE);
|
||||
}
|
||||
else if (argument.equals("+item") || argument.equals("item+") || argument.equals("+items") || argument.equals("items+") || argument.equals("pickup") || argument.equals("pickups") || argument.equals("withdraw") || argument.equals("withdraws") || argument.equals("withdrew")) {
|
||||
result.add(11);
|
||||
result.add(1);
|
||||
result.add(LookupActions.ITEM);
|
||||
result.add(ItemTransactionActions.ADD);
|
||||
}
|
||||
else {
|
||||
result.add(-1);
|
||||
@@ -138,7 +174,26 @@ public class ActionParser {
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return result;
|
||||
return new ParseResult(result, entityActionFilter);
|
||||
}
|
||||
|
||||
public static final class ParseResult {
|
||||
|
||||
private final List<Integer> actions;
|
||||
private final EntityActionFilter entityActionFilter;
|
||||
|
||||
private ParseResult(List<Integer> actions, EntityActionFilter entityActionFilter) {
|
||||
this.actions = actions;
|
||||
this.entityActionFilter = entityActionFilter;
|
||||
}
|
||||
|
||||
public List<Integer> getActions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
public EntityActionFilter getEntityActionFilter() {
|
||||
return entityActionFilter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,6 +222,23 @@ public class ActionParser {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse summary output flag from command arguments
|
||||
*
|
||||
* @param inputArguments
|
||||
* The command arguments
|
||||
* @return true if the summary flag is present
|
||||
*/
|
||||
public static boolean parseSummary(String[] inputArguments) {
|
||||
for (int index = 1; index < inputArguments.length; index++) {
|
||||
String argument = inputArguments[index].trim().toLowerCase(Locale.ROOT).replace("\\", "").replace("'", "");
|
||||
if (argument.equals("#summary")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse noisy flag from command arguments
|
||||
*
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.coreprotect.command.parser;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class GivableItemIdParser {
|
||||
|
||||
protected static final Pattern PATTERN = Pattern.compile("#([0-9]+)");
|
||||
|
||||
public static Integer parseGivableItemId(String[] inputArguments) {
|
||||
for (String argument : inputArguments) {
|
||||
Matcher matcher = PATTERN.matcher(argument);
|
||||
if (matcher.find()) {
|
||||
return Integer.parseInt(matcher.group(1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.util.Locale;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.BlockCommandSender;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -95,18 +96,21 @@ public class LocationParser {
|
||||
}
|
||||
}
|
||||
if (cCount > 1) {
|
||||
if (location == null && worldId > 0) {
|
||||
if (worldId > 0) {
|
||||
location = new Location(Bukkit.getWorld(WorldUtils.getWorldName(worldId)), 0, 0, 0);
|
||||
}
|
||||
if (location != null) {
|
||||
int worldMaxHeight = location.getWorld().getMaxHeight() - 1;
|
||||
int worldMinHeight = BukkitAdapter.ADAPTER.getMinHeight(location.getWorld());
|
||||
World world = location.getWorld();
|
||||
if (world != null) {
|
||||
int worldMaxHeight = world.getMaxHeight() - 1;
|
||||
int worldMinHeight = BukkitAdapter.ADAPTER.getMinHeight(world);
|
||||
|
||||
if (y < worldMinHeight) {
|
||||
y = Double.valueOf(worldMinHeight);
|
||||
}
|
||||
if (y > worldMaxHeight) {
|
||||
y = Double.valueOf(worldMaxHeight);
|
||||
if (y < worldMinHeight) {
|
||||
y = Double.valueOf(worldMinHeight);
|
||||
}
|
||||
if (y > worldMaxHeight) {
|
||||
y = Double.valueOf(worldMaxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
location.setX(x);
|
||||
|
||||
@@ -14,6 +14,8 @@ import org.bukkit.entity.EntityType;
|
||||
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.model.BlockGroup;
|
||||
import net.coreprotect.model.action.LookupActions;
|
||||
import net.coreprotect.utility.BlockTypeUtils;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
@@ -62,9 +64,12 @@ public class MaterialParser {
|
||||
for (String i3 : i2) {
|
||||
if (!checkTags(i3, restricted)) {
|
||||
Material i3_material = MaterialUtils.getType(i3);
|
||||
if (i3_material != null && (i3_material.isBlock() || argAction.contains(4))) {
|
||||
if (i3_material != null && (i3_material.isBlock() || argAction.contains(LookupActions.CONTAINER))) {
|
||||
restricted.add(i3_material);
|
||||
}
|
||||
else if (!argAction.contains(LookupActions.CONTAINER) && BlockTypeUtils.hasBlockType(i3)) {
|
||||
restricted.add(BlockTypeUtils.normalizeKey(i3));
|
||||
}
|
||||
else {
|
||||
EntityType i3_entity = EntityUtils.getEntityType(i3);
|
||||
if (i3_entity != null) {
|
||||
@@ -91,9 +96,12 @@ public class MaterialParser {
|
||||
else {
|
||||
if (!checkTags(argument, restricted)) {
|
||||
Material material = MaterialUtils.getType(argument);
|
||||
if (material != null && (material.isBlock() || argAction.contains(4))) {
|
||||
if (material != null && (material.isBlock() || argAction.contains(LookupActions.CONTAINER))) {
|
||||
restricted.add(material);
|
||||
}
|
||||
else if (!argAction.contains(LookupActions.CONTAINER) && BlockTypeUtils.hasBlockType(argument)) {
|
||||
restricted.add(BlockTypeUtils.normalizeKey(argument));
|
||||
}
|
||||
else {
|
||||
EntityType entityType = EntityUtils.getEntityType(argument);
|
||||
if (entityType != null) {
|
||||
@@ -154,9 +162,12 @@ public class MaterialParser {
|
||||
for (String i3 : i2) {
|
||||
if (!checkTags(i3, excluded)) {
|
||||
Material i3_material = MaterialUtils.getType(i3);
|
||||
if (i3_material != null && (i3_material.isBlock() || argAction.contains(4))) {
|
||||
if (i3_material != null && (i3_material.isBlock() || argAction.contains(LookupActions.CONTAINER))) {
|
||||
excluded.put(i3_material, false);
|
||||
}
|
||||
else if (!argAction.contains(LookupActions.CONTAINER) && BlockTypeUtils.hasBlockType(i3)) {
|
||||
excluded.put(BlockTypeUtils.normalizeKey(i3), false);
|
||||
}
|
||||
else {
|
||||
EntityType i3_entity = EntityUtils.getEntityType(i3);
|
||||
if (i3_entity != null) {
|
||||
@@ -178,9 +189,12 @@ public class MaterialParser {
|
||||
else {
|
||||
if (!checkTags(argument, excluded)) {
|
||||
Material iMaterial = MaterialUtils.getType(argument);
|
||||
if (iMaterial != null && (iMaterial.isBlock() || argAction.contains(4))) {
|
||||
if (iMaterial != null && (iMaterial.isBlock() || argAction.contains(LookupActions.CONTAINER))) {
|
||||
excluded.put(iMaterial, false);
|
||||
}
|
||||
else if (!argAction.contains(LookupActions.CONTAINER) && BlockTypeUtils.hasBlockType(argument)) {
|
||||
excluded.put(BlockTypeUtils.normalizeKey(argument), false);
|
||||
}
|
||||
else {
|
||||
EntityType iEntity = EntityUtils.getEntityType(argument);
|
||||
if (iEntity != null) {
|
||||
@@ -211,11 +225,15 @@ public class MaterialParser {
|
||||
public static Map<String, Set<Material>> getTags() {
|
||||
Map<String, Set<Material>> tagMap = new HashMap<>();
|
||||
tagMap.put("#button", BlockGroup.BUTTONS);
|
||||
tagMap.put("#sign", BlockGroup.SIGNS);
|
||||
tagMap.put("#container", BlockGroup.CONTAINERS);
|
||||
tagMap.put("#door", BlockGroup.DOORS);
|
||||
tagMap.put("#natural", BlockGroup.NATURAL_BLOCKS);
|
||||
tagMap.put("#pressure_plate", BlockGroup.PRESSURE_PLATES);
|
||||
tagMap.put("#shulker_box", BlockGroup.SHULKER_BOXES);
|
||||
if (!BlockGroup.BUNDLES.isEmpty()) {
|
||||
tagMap.put("#bundle", BlockGroup.BUNDLES);
|
||||
}
|
||||
return tagMap;
|
||||
}
|
||||
|
||||
@@ -296,6 +314,9 @@ public class MaterialParser {
|
||||
if (material != null) {
|
||||
isBlock = true;
|
||||
}
|
||||
else if (BlockTypeUtils.hasBlockType(argument)) {
|
||||
isBlock = true;
|
||||
}
|
||||
else {
|
||||
EntityType entityType = EntityUtils.getEntityType(argument);
|
||||
if (entityType != null) {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package net.coreprotect.command.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class MessageFilterParser {
|
||||
public static final int MINIMUM_FILTER_CODE_POINTS = 3;
|
||||
|
||||
private static final List<String> VALUE_KEYS = List.of(
|
||||
"a:",
|
||||
"action:",
|
||||
"b:",
|
||||
"block:",
|
||||
"blocks:",
|
||||
"c:",
|
||||
"coord:",
|
||||
"coords:",
|
||||
"cord:",
|
||||
"cords:",
|
||||
"coordinate:",
|
||||
"coordinates:",
|
||||
"cordinate:",
|
||||
"cordinates:",
|
||||
"e:",
|
||||
"exclude:",
|
||||
"f:",
|
||||
"filter:",
|
||||
"i:",
|
||||
"include:",
|
||||
"item:",
|
||||
"items:",
|
||||
"location:",
|
||||
"p:",
|
||||
"page:",
|
||||
"position:",
|
||||
"r:",
|
||||
"radius:",
|
||||
"rows:",
|
||||
"t:",
|
||||
"time:",
|
||||
"u:",
|
||||
"user:",
|
||||
"users:"
|
||||
);
|
||||
|
||||
private MessageFilterParser() {
|
||||
throw new IllegalStateException("Parser class");
|
||||
}
|
||||
|
||||
public static ParseResult parse(String[] inputArguments) {
|
||||
if (inputArguments == null || inputArguments.length == 0) {
|
||||
return new ParseResult(new String[0], Collections.emptyList(), false);
|
||||
}
|
||||
|
||||
List<String> arguments = new ArrayList<>(inputArguments.length);
|
||||
Set<String> filters = new LinkedHashSet<>();
|
||||
boolean specified = false;
|
||||
|
||||
for (int index = 0; index < inputArguments.length; index++) {
|
||||
String argument = Objects.toString(inputArguments[index], "").trim();
|
||||
if (!isFilterParameter(argument)) {
|
||||
arguments.add(argument);
|
||||
continue;
|
||||
}
|
||||
|
||||
specified = true;
|
||||
StringBuilder collected = new StringBuilder(argument);
|
||||
while (index + 1 < inputArguments.length && !isLookupTerminator(inputArguments[index + 1])) {
|
||||
String next = Objects.toString(inputArguments[++index], "").trim();
|
||||
if (!next.isEmpty()) {
|
||||
collected.append(' ').append(next);
|
||||
}
|
||||
}
|
||||
|
||||
String merged = collected.toString();
|
||||
arguments.add(merged);
|
||||
addFilters(filters, value(merged));
|
||||
}
|
||||
|
||||
return new ParseResult(arguments.toArray(new String[0]), new ArrayList<>(filters), specified);
|
||||
}
|
||||
|
||||
public static boolean isLookupTerminator(String raw) {
|
||||
String normalized = normalize(raw);
|
||||
if (isControl(normalized)) {
|
||||
return true;
|
||||
}
|
||||
for (String key : VALUE_KEYS) {
|
||||
if (normalized.startsWith(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isFilterParameter(String raw) {
|
||||
String normalized = normalize(raw);
|
||||
return normalized.equals("f:") || normalized.startsWith("f:") || normalized.equals("filter:") || normalized.startsWith("filter:");
|
||||
}
|
||||
|
||||
private static boolean isControl(String normalized) {
|
||||
return normalized.equals("#count") || normalized.equals("#sum") || normalized.equals("count") || normalized.equals("sum") || normalized.equals("#summary")
|
||||
|| normalized.equals("n") || normalized.equals("noisy") || normalized.equals("v") || normalized.equals("verbose")
|
||||
|| normalized.equals("#v") || normalized.equals("#verbose") || normalized.equals("#silent")
|
||||
|| normalized.equals("#container") || normalized.equals("#preview") || normalized.equals("#preview_cancel") || normalized.equals("#preview-cancel")
|
||||
|| RollbackStateParser.isModifier(normalized);
|
||||
}
|
||||
|
||||
private static String normalize(String raw) {
|
||||
return Objects.toString(raw, "").trim().toLowerCase(Locale.ROOT).replace("\\", "").replace("'", "");
|
||||
}
|
||||
|
||||
private static String value(String argument) {
|
||||
int separator = argument.indexOf(':');
|
||||
return separator < 0 ? "" : argument.substring(separator + 1);
|
||||
}
|
||||
|
||||
private static void addFilters(Set<String> filters, String value) {
|
||||
for (String part : value.split(",", -1)) {
|
||||
String filter = part.trim();
|
||||
if (!filter.isEmpty()) {
|
||||
filters.add(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ParseResult {
|
||||
private final String[] arguments;
|
||||
private final List<String> filters;
|
||||
private final boolean specified;
|
||||
|
||||
private ParseResult(String[] arguments, List<String> filters, boolean specified) {
|
||||
this.arguments = arguments;
|
||||
this.filters = Collections.unmodifiableList(filters);
|
||||
this.specified = specified;
|
||||
}
|
||||
|
||||
public String[] getArguments() {
|
||||
return arguments.clone();
|
||||
}
|
||||
|
||||
public List<String> getFilters() {
|
||||
return filters;
|
||||
}
|
||||
|
||||
public boolean isSpecified() {
|
||||
return specified;
|
||||
}
|
||||
|
||||
public boolean hasInvalidLength() {
|
||||
if (filters.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String filter : filters) {
|
||||
int start = filter.startsWith("-") ? 1 : 0;
|
||||
if (filter.codePointCount(start, filter.length()) < MINIMUM_FILTER_CODE_POINTS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package net.coreprotect.command.parser;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import net.coreprotect.model.lookup.LookupRollbackState;
|
||||
|
||||
public final class RollbackStateParser {
|
||||
|
||||
private RollbackStateParser() {
|
||||
throw new IllegalStateException("Parser class");
|
||||
}
|
||||
|
||||
public static ParseResult parse(String[] inputArguments) {
|
||||
boolean rolledBack = false;
|
||||
boolean restored = false;
|
||||
|
||||
for (int index = 1; index < inputArguments.length; index++) {
|
||||
String argument = normalize(inputArguments[index]);
|
||||
if (isRolledBackModifier(argument)) {
|
||||
rolledBack = true;
|
||||
}
|
||||
else if (isRestoredModifier(argument)) {
|
||||
restored = true;
|
||||
}
|
||||
}
|
||||
|
||||
LookupRollbackState state = LookupRollbackState.ANY;
|
||||
if (rolledBack) {
|
||||
state = LookupRollbackState.ROLLED_BACK;
|
||||
}
|
||||
else if (restored) {
|
||||
state = LookupRollbackState.NOT_ROLLED_BACK;
|
||||
}
|
||||
return new ParseResult(state, rolledBack && restored);
|
||||
}
|
||||
|
||||
public static boolean isModifier(String argument) {
|
||||
String normalized = normalize(argument);
|
||||
return isRolledBackModifier(normalized) || isRestoredModifier(normalized);
|
||||
}
|
||||
|
||||
private static boolean isRolledBackModifier(String argument) {
|
||||
return argument.equals("#rolledback") || argument.equals("#rollbacked");
|
||||
}
|
||||
|
||||
private static boolean isRestoredModifier(String argument) {
|
||||
return argument.equals("#restored");
|
||||
}
|
||||
|
||||
private static String normalize(String argument) {
|
||||
return argument.trim().toLowerCase(Locale.ROOT).replace("\\", "").replace("'", "");
|
||||
}
|
||||
|
||||
public static final class ParseResult {
|
||||
private final LookupRollbackState state;
|
||||
private final boolean conflicting;
|
||||
|
||||
private ParseResult(LookupRollbackState state, boolean conflicting) {
|
||||
this.state = state;
|
||||
this.conflicting = conflicting;
|
||||
}
|
||||
|
||||
public LookupRollbackState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public boolean isConflicting() {
|
||||
return conflicting;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class UserParser {
|
||||
* The user string to process
|
||||
*/
|
||||
private static void parseUser(List<String> users, String user) {
|
||||
List<String> badUsers = Arrays.asList("n", "noisy", "v", "verbose", "#v", "#verbose", "#silent", "#preview", "#preview_cancel", "#count", "#sum");
|
||||
List<String> badUsers = Arrays.asList("n", "noisy", "v", "verbose", "#v", "#verbose", "#silent", "#preview", "#preview_cancel", "#count", "#sum", "#summary", "#rolledback", "#rollbacked", "#restored");
|
||||
String check = user.replaceAll("[\\s'\"]", "");
|
||||
if (check.equals(user) && check.length() > 0) {
|
||||
if (user.equalsIgnoreCase("#global")) {
|
||||
|
||||
@@ -45,7 +45,8 @@ public class WorldParser {
|
||||
else if (inputProcessed.startsWith("#")) {
|
||||
world_id = WorldUtils.matchWorld(inputProcessed);
|
||||
if (world_id == -1 && !requireLoaded) {
|
||||
world_id = ConfigHandler.worlds.getOrDefault(argument.replaceFirst("#", ""), -1);
|
||||
String worldName = argument.replaceFirst("#", "");
|
||||
world_id = ConfigHandler.resolveIdentifierId(ConfigHandler.CacheType.WORLDS, worldName, false);
|
||||
}
|
||||
}
|
||||
next = 0;
|
||||
|
||||
@@ -12,21 +12,27 @@ import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
import net.coreprotect.CoreProtect;
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.language.Language;
|
||||
import net.coreprotect.thread.Scheduler;
|
||||
import net.coreprotect.utility.VersionUtils;
|
||||
|
||||
public class Config extends Language {
|
||||
|
||||
private static final Map<String, String[]> HEADERS = new HashMap<>();
|
||||
private static final Map<String, String> DEFAULT_VALUES = new LinkedHashMap<>();
|
||||
private static final Map<String, Config> CONFIG_BY_WORLD_NAME = new HashMap<>();
|
||||
private static final Map<String, Config> CONFIG_BY_WORLD_NAME = new ConcurrentHashMap<>();
|
||||
private static final String DEFAULT_FILE_HEADER = "# CoreProtect Config";
|
||||
private static final Pattern NON_DIGIT_PATTERN = Pattern.compile("[^0-9]");
|
||||
public static final String LINE_SEPARATOR = "\n";
|
||||
|
||||
private static final Config GLOBAL = new Config();
|
||||
@@ -34,22 +40,35 @@ public class Config extends Language {
|
||||
private Config defaults;
|
||||
|
||||
public String DONATION_KEY;
|
||||
public String DATABASE_TYPE;
|
||||
public String CLICKHOUSE_HOST;
|
||||
public String CLICKHOUSE_DATABASE;
|
||||
public String CLICKHOUSE_USERNAME;
|
||||
public String CLICKHOUSE_PASSWORD;
|
||||
public String DUCKDB_MEMORY_LIMIT;
|
||||
public String DUCKDB_MAX_TEMP_DIRECTORY_SIZE;
|
||||
public String SQLITE_DATABASE;
|
||||
public String PREFIX;
|
||||
public String MYSQL_HOST;
|
||||
public String MYSQL_DATABASE;
|
||||
public String MYSQL_USERNAME;
|
||||
public String MYSQL_PASSWORD;
|
||||
public String LANGUAGE;
|
||||
public String AUTO_PURGE;
|
||||
public String AUTO_PURGE_TIME;
|
||||
public boolean ENABLE_SSL;
|
||||
public boolean CLICKHOUSE_TLS;
|
||||
public boolean DISABLE_WAL;
|
||||
public boolean HOVER_EVENTS;
|
||||
public boolean DATABASE_LOCK;
|
||||
public boolean LOG_CANCELLED_CHAT;
|
||||
public boolean HOPPER_FILTER_META;
|
||||
public boolean DUPLICATE_SUPPRESSION;
|
||||
public boolean EXCLUDE_TNT;
|
||||
public boolean NETWORK_DEBUG;
|
||||
public boolean MYSQL;
|
||||
public boolean CHECK_UPDATES;
|
||||
public boolean ERROR_REPORTING;
|
||||
public boolean API_ENABLED;
|
||||
public boolean VERBOSE;
|
||||
public boolean ROLLBACK_ITEMS;
|
||||
@@ -60,11 +79,14 @@ public class Config extends Language {
|
||||
public boolean NATURAL_BREAK;
|
||||
public boolean BLOCK_MOVEMENT;
|
||||
public boolean PISTONS;
|
||||
public boolean DISPENSERS;
|
||||
public boolean BLOCK_BURN;
|
||||
public boolean BLOCK_IGNITE;
|
||||
public boolean FIRE_EXTINGUISH;
|
||||
public boolean EXPLOSIONS;
|
||||
public boolean ENTITY_CHANGE;
|
||||
public boolean ENTITY_KILLS;
|
||||
public boolean ENTITY_SPAWNS;
|
||||
public boolean SIGN_TEXT;
|
||||
public boolean BUCKETS;
|
||||
public boolean LEAF_DECAY;
|
||||
@@ -88,21 +110,35 @@ public class Config extends Language {
|
||||
public boolean USERNAME_CHANGES;
|
||||
public boolean WORLDEDIT;
|
||||
public int MAXIMUM_POOL_SIZE;
|
||||
public int CLICKHOUSE_PORT;
|
||||
public int CLICKHOUSE_CONSUMER_DELAY;
|
||||
public int MYSQL_PORT;
|
||||
public int DEFAULT_RADIUS;
|
||||
public int DUCKDB_THREADS;
|
||||
public int MAX_RADIUS;
|
||||
|
||||
static {
|
||||
DEFAULT_VALUES.put("donation-key", "");
|
||||
DEFAULT_VALUES.put("use-mysql", "false");
|
||||
DEFAULT_VALUES.put("database-type", "duckdb");
|
||||
DEFAULT_VALUES.put("table-prefix", "co_");
|
||||
DEFAULT_VALUES.put("mysql-host", "127.0.0.1");
|
||||
DEFAULT_VALUES.put("mysql-port", "3306");
|
||||
DEFAULT_VALUES.put("mysql-database", "database");
|
||||
DEFAULT_VALUES.put("mysql-username", "root");
|
||||
DEFAULT_VALUES.put("mysql-password", "");
|
||||
DEFAULT_VALUES.put("clickhouse-host", "127.0.0.1");
|
||||
DEFAULT_VALUES.put("clickhouse-port", "8123");
|
||||
DEFAULT_VALUES.put("clickhouse-database", "default");
|
||||
DEFAULT_VALUES.put("clickhouse-username", "default");
|
||||
DEFAULT_VALUES.put("clickhouse-password", "");
|
||||
DEFAULT_VALUES.put("clickhouse-tls", "false");
|
||||
DEFAULT_VALUES.put("duckdb-memory-limit", "512MB");
|
||||
DEFAULT_VALUES.put("duckdb-threads", "3");
|
||||
DEFAULT_VALUES.put("duckdb-max-temp-directory-size", "10GB");
|
||||
DEFAULT_VALUES.put("language", "en");
|
||||
DEFAULT_VALUES.put("auto-purge", "false");
|
||||
DEFAULT_VALUES.put("check-updates", "true");
|
||||
DEFAULT_VALUES.put("error-reporting", "true");
|
||||
DEFAULT_VALUES.put("api-enabled", "true");
|
||||
DEFAULT_VALUES.put("verbose", "true");
|
||||
DEFAULT_VALUES.put("default-radius", "10");
|
||||
@@ -115,11 +151,14 @@ public class Config extends Language {
|
||||
DEFAULT_VALUES.put("natural-break", "true");
|
||||
DEFAULT_VALUES.put("block-movement", "true");
|
||||
DEFAULT_VALUES.put("pistons", "true");
|
||||
DEFAULT_VALUES.put("dispensers", "true");
|
||||
DEFAULT_VALUES.put("block-burn", "true");
|
||||
DEFAULT_VALUES.put("block-ignite", "true");
|
||||
DEFAULT_VALUES.put("fire-extinguish", "false");
|
||||
DEFAULT_VALUES.put("explosions", "true");
|
||||
DEFAULT_VALUES.put("entity-change", "true");
|
||||
DEFAULT_VALUES.put("entity-kills", "true");
|
||||
DEFAULT_VALUES.put("entity-spawns", "true");
|
||||
DEFAULT_VALUES.put("sign-text", "true");
|
||||
DEFAULT_VALUES.put("buckets", "true");
|
||||
DEFAULT_VALUES.put("leaf-decay", "true");
|
||||
@@ -143,26 +182,34 @@ public class Config extends Language {
|
||||
DEFAULT_VALUES.put("worldedit", "true");
|
||||
|
||||
HEADERS.put("donation-key", new String[] { "# CoreProtect is donationware. Obtain a donation key from coreprotect.net/donate/" });
|
||||
HEADERS.put("use-mysql", new String[] { "# MySQL is optional and not required.", "# If you prefer to use MySQL, enable the following and fill out the fields." });
|
||||
HEADERS.put("database-type", new String[] { "# Database engine used by CoreProtect. Valid values are duckdb, clickhouse, sqlite, and mysql.", "# Run /co reload or restart the server after changing the database engine or connection target." });
|
||||
HEADERS.put("mysql-host", new String[] { "# Connection settings for MySQL." });
|
||||
HEADERS.put("clickhouse-host", new String[] { "# Connection settings for ClickHouse 26.1 or newer.", "# The configured database must already exist; CoreProtect creates its prefixed tables and views.", "# Multiple writers require database-lock disabled, the same version and prefix, separate data directories, and direct connections to one physical server.", "# Keep writer clocks synchronized; a shared prefix is one logical namespace for worlds and players.", "# Do not reassign usernames, and record UUID-bearing logins before UUID-less activity under a changed name.", "# Stop every writer before migration or purge; purge requires database-lock on the remaining server.", "# Replicated, distributed, and load-balanced independent ClickHouse nodes are unsupported." });
|
||||
HEADERS.put("duckdb-memory-limit", new String[] { "# Resource limits for the embedded DuckDB database.", "# The memory limit controls DuckDB's buffer manager; the temporary limit caps spill data and is not preallocated." });
|
||||
HEADERS.put("language", new String[] { "# If modified, will automatically attempt to translate languages phrases.", "# List of language codes: https://coreprotect.net/languages/" });
|
||||
HEADERS.put("auto-purge", new String[] { "# Automatically purge data older than the configured time.", "# Examples: 30d, 12w, 6mo. Set to false to disable." });
|
||||
HEADERS.put("check-updates", new String[] { "# If enabled, CoreProtect will check for updates when your server starts up.", "# If an update is available, you'll be notified via your server console.", });
|
||||
HEADERS.put("error-reporting", new String[] { "# Automatically sends errors to the plugin author." });
|
||||
HEADERS.put("api-enabled", new String[] { "# If enabled, other plugins will be able to utilize the CoreProtect API.", });
|
||||
HEADERS.put("verbose", new String[] { "# If enabled, extra data is displayed during rollbacks and restores.", "# Can be manually triggered by adding \"#verbose\" to your rollback command." });
|
||||
HEADERS.put("default-radius", new String[] { "# If no radius is specified in a rollback or restore, this value will be", "# used as the radius. Set to \"0\" to disable automatically adding a radius." });
|
||||
HEADERS.put("max-radius", new String[] { "# The maximum radius that can be used in a command. Set to \"0\" to disable.", "# To run a rollback or restore without a radius, you can use \"r:#global\"." });
|
||||
HEADERS.put("rollback-items", new String[] { "# If enabled, items taken from containers (etc) will be included in rollbacks." });
|
||||
HEADERS.put("rollback-entities", new String[] { "# If enabled, entities, such as killed animals, will be included in rollbacks." });
|
||||
HEADERS.put("rollback-entities", new String[] { "# If enabled, entity kills and player-attributed entity spawns will be included in rollbacks." });
|
||||
HEADERS.put("skip-generic-data", new String[] { "# If enabled, generic data, like zombies burning in daylight, won't be logged." });
|
||||
HEADERS.put("block-place", new String[] { "# Logs blocks placed by players." });
|
||||
HEADERS.put("block-break", new String[] { "# Logs blocks broken by players." });
|
||||
HEADERS.put("natural-break", new String[] { "# Logs blocks that break off of other blocks; for example, a sign or torch", "# falling off of a dirt block that a player breaks. This is required for", "# beds/doors to properly rollback." });
|
||||
HEADERS.put("block-movement", new String[] { "# Properly track block movement, such as sand or gravel falling." });
|
||||
HEADERS.put("pistons", new String[] { "# Properly track blocks moved by pistons." });
|
||||
HEADERS.put("dispensers", new String[] { "# Logs block placement and removal events caused by dispensers." });
|
||||
HEADERS.put("block-burn", new String[] { "# Logs blocks that burn up in a fire." });
|
||||
HEADERS.put("block-ignite", new String[] { "# Logs when a block naturally ignites, such as from fire spreading." });
|
||||
HEADERS.put("fire-extinguish", new String[] { "# Logs when fire naturally extinguishes." });
|
||||
HEADERS.put("explosions", new String[] { "# Logs explosions, such as TNT and Creepers." });
|
||||
HEADERS.put("entity-change", new String[] { "# Track when an entity changes a block, such as an Enderman destroying blocks." });
|
||||
HEADERS.put("entity-kills", new String[] { "# Logs killed entities, such as killed cows and enderman." });
|
||||
HEADERS.put("entity-spawns", new String[] { "# Logs entities placed or spawned by players, such as boats and spawn-egg mobs." });
|
||||
HEADERS.put("sign-text", new String[] { "# Logs text on signs. If disabled, signs will be blank when rolled back." });
|
||||
HEADERS.put("buckets", new String[] { "# Logs lava and water sources placed/removed by players who are using buckets." });
|
||||
HEADERS.put("leaf-decay", new String[] { "# Logs natural tree leaf decay." });
|
||||
@@ -193,11 +240,21 @@ public class Config extends Language {
|
||||
this.DATABASE_LOCK = this.getBoolean("database-lock", true);
|
||||
this.LOG_CANCELLED_CHAT = this.getBoolean("log-cancelled-chat", true);
|
||||
this.HOPPER_FILTER_META = this.getBoolean("hopper-filter-meta", false);
|
||||
this.DUPLICATE_SUPPRESSION = this.getBoolean("duplicate-suppression", true);
|
||||
this.EXCLUDE_TNT = this.getBoolean("exclude-tnt", false);
|
||||
this.NETWORK_DEBUG = this.getBoolean("network-debug", false);
|
||||
this.UNKNOWN_LOGGING = this.getBoolean("unknown-logging", false);
|
||||
this.MAXIMUM_POOL_SIZE = this.getInt("maximum-pool-size", 10);
|
||||
this.DONATION_KEY = this.getString("donation-key");
|
||||
this.DATABASE_TYPE = this.getString("database-type");
|
||||
this.CLICKHOUSE_HOST = this.getString("clickhouse-host");
|
||||
this.CLICKHOUSE_PORT = this.getInt("clickhouse-port");
|
||||
this.CLICKHOUSE_CONSUMER_DELAY = Math.max(500,
|
||||
this.getInt("clickhouse-consumer-delay", 2500));
|
||||
this.CLICKHOUSE_DATABASE = this.getString("clickhouse-database");
|
||||
this.CLICKHOUSE_USERNAME = this.getString("clickhouse-username");
|
||||
this.CLICKHOUSE_PASSWORD = this.getString("clickhouse-password");
|
||||
this.CLICKHOUSE_TLS = this.getBoolean("clickhouse-tls");
|
||||
this.MYSQL = this.getBoolean("use-mysql");
|
||||
this.PREFIX = this.getString("table-prefix");
|
||||
this.MYSQL_HOST = this.getString("mysql-host");
|
||||
@@ -205,8 +262,15 @@ public class Config extends Language {
|
||||
this.MYSQL_DATABASE = this.getString("mysql-database");
|
||||
this.MYSQL_USERNAME = this.getString("mysql-username");
|
||||
this.MYSQL_PASSWORD = this.getString("mysql-password");
|
||||
this.DUCKDB_MEMORY_LIMIT = this.getString("duckdb-memory-limit");
|
||||
this.DUCKDB_THREADS = this.getInt("duckdb-threads", 3);
|
||||
this.DUCKDB_MAX_TEMP_DIRECTORY_SIZE = this.getString("duckdb-max-temp-directory-size");
|
||||
this.SQLITE_DATABASE = this.getString("sqlite-database");
|
||||
this.LANGUAGE = this.getString("language");
|
||||
this.AUTO_PURGE = this.getString("auto-purge");
|
||||
this.AUTO_PURGE_TIME = this.getString("auto-purge-time");
|
||||
this.CHECK_UPDATES = this.getBoolean("check-updates");
|
||||
this.ERROR_REPORTING = this.getBoolean("error-reporting");
|
||||
this.API_ENABLED = this.getBoolean("api-enabled");
|
||||
this.VERBOSE = this.getBoolean("verbose");
|
||||
this.DEFAULT_RADIUS = this.getInt("default-radius");
|
||||
@@ -219,11 +283,14 @@ public class Config extends Language {
|
||||
this.NATURAL_BREAK = this.getBoolean("natural-break");
|
||||
this.BLOCK_MOVEMENT = this.getBoolean("block-movement");
|
||||
this.PISTONS = this.getBoolean("pistons");
|
||||
this.DISPENSERS = this.getBoolean("dispensers");
|
||||
this.BLOCK_BURN = this.getBoolean("block-burn");
|
||||
this.BLOCK_IGNITE = this.getBoolean("block-ignite");
|
||||
this.FIRE_EXTINGUISH = this.getBoolean("fire-extinguish");
|
||||
this.EXPLOSIONS = this.getBoolean("explosions");
|
||||
this.ENTITY_CHANGE = this.getBoolean("entity-change");
|
||||
this.ENTITY_KILLS = this.getBoolean("entity-kills");
|
||||
this.ENTITY_SPAWNS = this.getBoolean("entity-spawns");
|
||||
this.SIGN_TEXT = this.getBoolean("sign-text");
|
||||
this.BUCKETS = this.getBoolean("buckets");
|
||||
this.LEAF_DECAY = this.getBoolean("leaf-decay");
|
||||
@@ -262,12 +329,7 @@ public class Config extends Language {
|
||||
}
|
||||
|
||||
public static Config getConfig(final String worldName) {
|
||||
Config ret = CONFIG_BY_WORLD_NAME.get(worldName);
|
||||
if (ret == null) {
|
||||
ret = CONFIG_BY_WORLD_NAME.getOrDefault(worldName, GLOBAL);
|
||||
CONFIG_BY_WORLD_NAME.put(worldName, ret);
|
||||
}
|
||||
return ret;
|
||||
return CONFIG_BY_WORLD_NAME.getOrDefault(worldName, GLOBAL);
|
||||
}
|
||||
|
||||
public Config() {
|
||||
@@ -315,7 +377,7 @@ public class Config extends Language {
|
||||
return dfl;
|
||||
}
|
||||
|
||||
configured = configured.replaceAll("[^0-9]", "");
|
||||
configured = NON_DIGIT_PATTERN.matcher(configured).replaceAll("");
|
||||
|
||||
return configured.isEmpty() ? dfl : Integer.parseInt(configured);
|
||||
}
|
||||
@@ -329,6 +391,10 @@ public class Config extends Language {
|
||||
this.config.clear();
|
||||
}
|
||||
|
||||
public boolean hasOption(String key) {
|
||||
return this.config.containsKey(key);
|
||||
}
|
||||
|
||||
public void loadDefaults() {
|
||||
this.clearConfig();
|
||||
this.readValues();
|
||||
@@ -369,6 +435,7 @@ public class Config extends Language {
|
||||
final Config temp = new Config();
|
||||
temp.loadDefaults();
|
||||
temp.addMissingOptions(globalFile);
|
||||
map.put("config", Files.readAllBytes(globalFile.toPath()));
|
||||
}
|
||||
|
||||
for (final File worldConfigFile : configFolder.listFiles((File file) -> file.getName().endsWith(".yml"))) {
|
||||
@@ -389,8 +456,12 @@ public class Config extends Language {
|
||||
// we call reloads asynchronously
|
||||
// for now this solution is good enough to ensure we only modify on the main thread
|
||||
final CompletableFuture<Void> complete = new CompletableFuture<>();
|
||||
final CompletableFuture<Void> shutdown = Consumer.databaseReloadShutdownSignal();
|
||||
|
||||
Scheduler.runTask(CoreProtect.getInstance(), () -> {
|
||||
if (shutdown.isDone()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
parseConfig(data);
|
||||
}
|
||||
@@ -404,12 +475,15 @@ public class Config extends Language {
|
||||
complete.complete(null);
|
||||
});
|
||||
|
||||
CompletableFuture.anyOf(complete, shutdown).join();
|
||||
if (!complete.isDone()) {
|
||||
complete.cancel(false);
|
||||
throw new CancellationException("Configuration reload cancelled for shutdown");
|
||||
}
|
||||
complete.join();
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_BY_WORLD_NAME.clear();
|
||||
|
||||
// we need to load global first since it is used for config defaults
|
||||
final byte[] defaultData = data.get("config");
|
||||
if (defaultData != null) {
|
||||
@@ -424,6 +498,7 @@ public class Config extends Language {
|
||||
GLOBAL.loadDefaults();
|
||||
}
|
||||
|
||||
final Map<String, Config> worldConfigs = new HashMap<>();
|
||||
for (final Map.Entry<String, byte[]> entry : data.entrySet()) {
|
||||
final String worldName = entry.getKey();
|
||||
if (worldName.equals("config")) {
|
||||
@@ -441,8 +516,12 @@ public class Config extends Language {
|
||||
throw new RuntimeException(ex); // shouldn't happen
|
||||
}
|
||||
|
||||
CONFIG_BY_WORLD_NAME.put(worldName, config);
|
||||
worldConfigs.put(worldName, config);
|
||||
}
|
||||
|
||||
// Replace entries instead of clearing first, so a configured world never reads the global config mid reload
|
||||
CONFIG_BY_WORLD_NAME.putAll(worldConfigs);
|
||||
CONFIG_BY_WORLD_NAME.keySet().retainAll(worldConfigs.keySet());
|
||||
}
|
||||
|
||||
public void addMissingOptions(final File file) throws IOException {
|
||||
@@ -456,13 +535,19 @@ public class Config extends Language {
|
||||
|
||||
for (final Map.Entry<String, String> entry : DEFAULT_VALUES.entrySet()) {
|
||||
final String key = entry.getKey();
|
||||
final String defaultValue = entry.getValue();
|
||||
String defaultValue = entry.getValue();
|
||||
|
||||
final String configuredValue = this.config.get(key);
|
||||
|
||||
if (configuredValue != null) {
|
||||
continue;
|
||||
}
|
||||
if (key.equals("auto-purge") && VersionUtils.isCommunityEdition()) {
|
||||
continue;
|
||||
}
|
||||
if (key.equals("database-type") && !writeHeader) {
|
||||
defaultValue = this.getBoolean("use-mysql") ? "mysql" : "sqlite";
|
||||
}
|
||||
|
||||
final String[] header = HEADERS.get(key);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.TreeMap;
|
||||
import net.coreprotect.CoreProtect;
|
||||
import net.coreprotect.language.Language;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class ConfigFile extends Config {
|
||||
|
||||
@@ -201,7 +202,7 @@ public class ConfigFile extends Config {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ public class ConfigFile extends Config {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,237 @@
|
||||
package net.coreprotect.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.AclFileAttributeView;
|
||||
import java.nio.file.attribute.DosFileAttributeView;
|
||||
import java.nio.file.attribute.DosFileAttributes;
|
||||
import java.nio.file.attribute.FileOwnerAttributeView;
|
||||
import java.nio.file.attribute.PosixFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFileAttributes;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import net.coreprotect.database.DatabaseType;
|
||||
|
||||
public final class DatabaseConfigWriter {
|
||||
|
||||
private DatabaseConfigWriter() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
public static synchronized void persistDatabaseType(DatabaseType type) throws IOException {
|
||||
Path configFile = Paths.get(ConfigHandler.path).resolve(ConfigFile.CONFIG);
|
||||
if (!Files.isRegularFile(configFile, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("CoreProtect config is not a regular file: " + configFile);
|
||||
}
|
||||
|
||||
byte[] original = Files.readAllBytes(configFile);
|
||||
byte[] updated = updateDatabaseType(original, type);
|
||||
if (Arrays.equals(original, updated)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path directory = configFile.toAbsolutePath().getParent();
|
||||
if (directory == null) {
|
||||
throw new IOException("CoreProtect config has no parent directory: " + configFile);
|
||||
}
|
||||
|
||||
Path temporary = Files.createTempFile(directory, ".config.yml.", ".tmp");
|
||||
boolean moved = false;
|
||||
try {
|
||||
Files.write(temporary, updated, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
}
|
||||
copyAttributes(configFile, temporary);
|
||||
try {
|
||||
Files.move(temporary, configFile, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
catch (AtomicMoveNotSupportedException exception) {
|
||||
throw new IOException("The CoreProtect config filesystem does not support atomic replacement", exception);
|
||||
}
|
||||
moved = true;
|
||||
forceDirectory(directory);
|
||||
}
|
||||
finally {
|
||||
if (!moved) {
|
||||
Files.deleteIfExists(temporary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] updateDatabaseType(byte[] data, DatabaseType type) {
|
||||
String content = new String(data, StandardCharsets.UTF_8);
|
||||
String databaseType = type.name().toLowerCase(Locale.ROOT);
|
||||
String lineSeparator = detectLineSeparator(content);
|
||||
boolean terminalLineSeparator = content.endsWith("\n") || content.endsWith("\r");
|
||||
boolean foundDatabaseType = false;
|
||||
StringBuilder updated = new StringBuilder(content.length() + 32);
|
||||
|
||||
int offset = 0;
|
||||
while (offset < content.length()) {
|
||||
int lineEnd = offset;
|
||||
while (lineEnd < content.length() && content.charAt(lineEnd) != '\r' && content.charAt(lineEnd) != '\n') {
|
||||
lineEnd++;
|
||||
}
|
||||
int nextOffset = lineEnd;
|
||||
if (nextOffset < content.length()) {
|
||||
char separator = content.charAt(nextOffset++);
|
||||
if (separator == '\r' && nextOffset < content.length() && content.charAt(nextOffset) == '\n') {
|
||||
nextOffset++;
|
||||
}
|
||||
}
|
||||
|
||||
String line = content.substring(offset, lineEnd);
|
||||
String replaced = replaceScalar(line, "database-type", databaseType);
|
||||
if (replaced != null) {
|
||||
foundDatabaseType = true;
|
||||
updated.append(replaced);
|
||||
}
|
||||
else {
|
||||
replaced = replaceScalar(line, "use-mysql", Boolean.toString(type.isMySQL()));
|
||||
updated.append(replaced == null ? line : replaced);
|
||||
}
|
||||
updated.append(content, lineEnd, nextOffset);
|
||||
offset = nextOffset;
|
||||
}
|
||||
|
||||
if (!foundDatabaseType) {
|
||||
if (updated.length() > 0 && updated.charAt(updated.length() - 1) != '\n' && updated.charAt(updated.length() - 1) != '\r') {
|
||||
updated.append(lineSeparator);
|
||||
}
|
||||
updated.append("database-type: ").append(databaseType);
|
||||
if (terminalLineSeparator) {
|
||||
updated.append(lineSeparator);
|
||||
}
|
||||
}
|
||||
return updated.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String replaceScalar(String line, String expectedKey, String value) {
|
||||
int first = 0;
|
||||
while (first < line.length() && Character.isWhitespace(line.charAt(first))) {
|
||||
first++;
|
||||
}
|
||||
if (first == line.length() || line.charAt(first) == '#') {
|
||||
return null;
|
||||
}
|
||||
|
||||
int colon = line.indexOf(':', first);
|
||||
if (colon < 0 || !line.substring(first, colon).trim().equalsIgnoreCase(expectedKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int valueStart = colon + 1;
|
||||
while (valueStart < line.length() && Character.isWhitespace(line.charAt(valueStart))) {
|
||||
valueStart++;
|
||||
}
|
||||
int comment = findComment(line, valueStart);
|
||||
int valueEnd = comment < 0 ? line.length() : comment;
|
||||
while (valueEnd > valueStart && Character.isWhitespace(line.charAt(valueEnd - 1))) {
|
||||
valueEnd--;
|
||||
}
|
||||
|
||||
String replacement = value;
|
||||
if (valueStart < valueEnd && (line.charAt(valueStart) == '\'' || line.charAt(valueStart) == '"')) {
|
||||
char quote = line.charAt(valueStart);
|
||||
replacement = quote + value + quote;
|
||||
}
|
||||
String whitespace = line.substring(colon + 1, valueStart);
|
||||
if (whitespace.isEmpty()) {
|
||||
whitespace = " ";
|
||||
}
|
||||
String suffix = line.substring(valueEnd);
|
||||
if (comment == valueStart && suffix.startsWith("#")) {
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
return line.substring(0, colon + 1) + whitespace + replacement + suffix;
|
||||
}
|
||||
|
||||
private static int findComment(String line, int start) {
|
||||
boolean singleQuoted = false;
|
||||
boolean doubleQuoted = false;
|
||||
boolean escaped = false;
|
||||
for (int index = start; index < line.length(); index++) {
|
||||
char character = line.charAt(index);
|
||||
if (doubleQuoted && character == '\\' && !escaped) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (character == '"' && !singleQuoted && !escaped) {
|
||||
doubleQuoted = !doubleQuoted;
|
||||
}
|
||||
else if (character == '\'' && !doubleQuoted) {
|
||||
singleQuoted = !singleQuoted;
|
||||
}
|
||||
else if (character == '#' && !singleQuoted && !doubleQuoted && (index == start || Character.isWhitespace(line.charAt(index - 1)))) {
|
||||
return index;
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String detectLineSeparator(String content) {
|
||||
int newline = content.indexOf('\n');
|
||||
if (newline >= 0) {
|
||||
return newline > 0 && content.charAt(newline - 1) == '\r' ? "\r\n" : "\n";
|
||||
}
|
||||
return content.indexOf('\r') >= 0 ? "\r" : System.lineSeparator();
|
||||
}
|
||||
|
||||
private static void copyAttributes(Path source, Path target) throws IOException {
|
||||
boolean ownerCopied = false;
|
||||
PosixFileAttributeView sourcePosix = Files.getFileAttributeView(source, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
PosixFileAttributeView targetPosix = Files.getFileAttributeView(target, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (sourcePosix != null && targetPosix != null) {
|
||||
PosixFileAttributes attributes = sourcePosix.readAttributes();
|
||||
targetPosix.setOwner(attributes.owner());
|
||||
targetPosix.setGroup(attributes.group());
|
||||
targetPosix.setPermissions(attributes.permissions());
|
||||
ownerCopied = true;
|
||||
}
|
||||
|
||||
AclFileAttributeView sourceAcl = Files.getFileAttributeView(source, AclFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
AclFileAttributeView targetAcl = Files.getFileAttributeView(target, AclFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (sourceAcl != null && targetAcl != null) {
|
||||
targetAcl.setOwner(sourceAcl.getOwner());
|
||||
targetAcl.setAcl(sourceAcl.getAcl());
|
||||
ownerCopied = true;
|
||||
}
|
||||
|
||||
if (!ownerCopied) {
|
||||
FileOwnerAttributeView sourceOwner = Files.getFileAttributeView(source, FileOwnerAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
FileOwnerAttributeView targetOwner = Files.getFileAttributeView(target, FileOwnerAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (sourceOwner != null && targetOwner != null) {
|
||||
targetOwner.setOwner(sourceOwner.getOwner());
|
||||
}
|
||||
}
|
||||
|
||||
DosFileAttributeView sourceDos = Files.getFileAttributeView(source, DosFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
DosFileAttributeView targetDos = Files.getFileAttributeView(target, DosFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (sourceDos != null && targetDos != null) {
|
||||
DosFileAttributes attributes = sourceDos.readAttributes();
|
||||
targetDos.setArchive(attributes.isArchive());
|
||||
targetDos.setHidden(attributes.isHidden());
|
||||
targetDos.setSystem(attributes.isSystem());
|
||||
targetDos.setReadOnly(attributes.isReadOnly());
|
||||
}
|
||||
}
|
||||
|
||||
private static void forceDirectory(Path directory) {
|
||||
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
|
||||
channel.force(true);
|
||||
}
|
||||
catch (IOException | UnsupportedOperationException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.BlockState;
|
||||
@@ -14,15 +19,43 @@ import org.bukkit.inventory.ItemStack;
|
||||
import net.coreprotect.CoreProtect;
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.consumer.process.Process;
|
||||
import net.coreprotect.database.Database;
|
||||
import net.coreprotect.database.DuckDBRecovery;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.utility.Chat;
|
||||
import net.coreprotect.utility.Color;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
|
||||
public class Consumer extends Process implements Runnable, Thread.UncaughtExceptionHandler {
|
||||
|
||||
public enum OperationStartResult {
|
||||
STARTED,
|
||||
PURGE_RUNNING,
|
||||
ROLLBACK_RUNNING,
|
||||
RELOAD_RUNNING,
|
||||
PERSISTENCE_HALTED,
|
||||
INTERRUPTED
|
||||
}
|
||||
|
||||
private static Thread consumerThread = null;
|
||||
private static final Object pauseMonitor = new Object();
|
||||
private static boolean operationPaused = false;
|
||||
private static boolean maintenancePaused = false;
|
||||
private static final ReentrantReadWriteLock databaseLifecycle = new ReentrantReadWriteLock(true);
|
||||
private static final Object rollbackPurgeGate = new Object();
|
||||
private static long pendingRollbackPublications = 0;
|
||||
private static volatile boolean backgroundPurgeRunning = false;
|
||||
private static volatile boolean backgroundPurgePausesPersistence = false;
|
||||
private static volatile boolean databaseReloadPaused = false;
|
||||
private static volatile boolean databaseReloadRunning = false;
|
||||
private static volatile boolean databaseReloadBlockedForShutdown = false;
|
||||
private static CompletableFuture<Void> databaseReloadShutdownSignal = new CompletableFuture<>();
|
||||
public static volatile int currentConsumer = 0;
|
||||
public static volatile boolean isPaused = false;
|
||||
public static volatile boolean transacting = false;
|
||||
public static volatile boolean interrupt = false;
|
||||
protected static volatile boolean pausedSuccess = false;
|
||||
private static volatile boolean persistenceHalted = false;
|
||||
|
||||
public static ConcurrentHashMap<Integer, ArrayList<Object[]>> consumer = new ConcurrentHashMap<>(4, 0.75f, 2);
|
||||
// public static ConcurrentHashMap<Integer, Integer[]> consumer_id = new ConcurrentHashMap<>();
|
||||
@@ -51,14 +84,33 @@ public class Consumer extends Process implements Runnable, Thread.UncaughtExcept
|
||||
Thread.sleep(30000); // 30 seconds
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static int newConsumerId(int consumer) {
|
||||
int id = Consumer.consumer_id.get(consumer)[0];
|
||||
Consumer.consumer_id.put(consumer, new Integer[] { id + 1, 1 });
|
||||
return id;
|
||||
protected static long reserveConsumer() {
|
||||
return reserveConsumers(1);
|
||||
}
|
||||
|
||||
protected static long reserveConsumers(int count) {
|
||||
synchronized (Consumer.consumer_id) {
|
||||
int consumer = Consumer.currentConsumer;
|
||||
Integer[] state = Consumer.consumer_id.get(consumer);
|
||||
int id = state[0];
|
||||
Consumer.consumer_id.put(consumer, new Integer[] { id + count, state[1] + count });
|
||||
return ((long) consumer << 32) | (id & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void completeReservation(long reservation, int count) {
|
||||
int consumer = (int) (reservation >>> 32);
|
||||
synchronized (Consumer.consumer_id) {
|
||||
Integer[] state = Consumer.consumer_id.get(consumer);
|
||||
if (count <= 0 || state == null || state[1] < count) {
|
||||
throw new IllegalStateException("Invalid consumer reservation completion");
|
||||
}
|
||||
Consumer.consumer_id.put(consumer, new Integer[] { state[0], state[1] - count });
|
||||
}
|
||||
}
|
||||
|
||||
public static int getConsumerSize(int id) {
|
||||
@@ -69,7 +121,39 @@ public class Consumer extends Process implements Runnable, Thread.UncaughtExcept
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean hasPendingConsumerData() {
|
||||
synchronized (consumer_id) {
|
||||
for (int id = 0; id < 2; id++) {
|
||||
Integer[] state = consumer_id.get(id);
|
||||
ArrayList<Object[]> data = consumer.get(id);
|
||||
if ((state != null && state[1] > 0) || (data != null && !data.isEmpty())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
persistenceHalted = false;
|
||||
pendingRollbackPublications = 0;
|
||||
databaseReloadBlockedForShutdown = false;
|
||||
databaseReloadShutdownSignal.complete(null);
|
||||
databaseReloadShutdownSignal = new CompletableFuture<>();
|
||||
}
|
||||
synchronized (pauseMonitor) {
|
||||
operationPaused = false;
|
||||
maintenancePaused = false;
|
||||
isPaused = false;
|
||||
pauseMonitor.notifyAll();
|
||||
}
|
||||
databaseReloadPaused = false;
|
||||
databaseReloadRunning = false;
|
||||
backgroundPurgeRunning = false;
|
||||
backgroundPurgePausesPersistence = false;
|
||||
resetPreparationFailures();
|
||||
DuckDBRecovery.reset();
|
||||
Consumer.consumer.put(0, new ArrayList<>());
|
||||
Consumer.consumer.put(1, new ArrayList<>());
|
||||
Consumer.consumer_id.put(0, new Integer[] { 0, 0 });
|
||||
@@ -98,50 +182,457 @@ public class Consumer extends Process implements Runnable, Thread.UncaughtExcept
|
||||
return consumerThread != null && consumerThread.isAlive();
|
||||
}
|
||||
|
||||
private static void pauseConsumer(int process_id) {
|
||||
public static boolean isPersistenceHalted() {
|
||||
return persistenceHalted;
|
||||
}
|
||||
|
||||
public static boolean claimLookupPause() throws InterruptedException {
|
||||
synchronized (pauseMonitor) {
|
||||
while (isPaused && !persistenceHalted) {
|
||||
pauseMonitor.wait(1L);
|
||||
}
|
||||
if (persistenceHalted) {
|
||||
return false;
|
||||
}
|
||||
operationPaused = true;
|
||||
isPaused = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean tryClaimConsumerPause() {
|
||||
synchronized (pauseMonitor) {
|
||||
if (isPaused || persistenceHalted) {
|
||||
return false;
|
||||
}
|
||||
operationPaused = true;
|
||||
isPaused = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void releaseOperationPause() {
|
||||
synchronized (pauseMonitor) {
|
||||
operationPaused = false;
|
||||
isPaused = maintenancePaused;
|
||||
pauseMonitor.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public static void setMaintenancePaused(boolean paused) {
|
||||
synchronized (pauseMonitor) {
|
||||
maintenancePaused = paused;
|
||||
isPaused = operationPaused || maintenancePaused;
|
||||
pauseMonitor.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public static void lockDatabaseAccess() {
|
||||
databaseLifecycle.readLock().lock();
|
||||
}
|
||||
|
||||
public static void unlockDatabaseAccess() {
|
||||
databaseLifecycle.readLock().unlock();
|
||||
}
|
||||
|
||||
public static void lockDatabaseMaintenance() {
|
||||
databaseLifecycle.writeLock().lock();
|
||||
}
|
||||
|
||||
public static void lockDatabaseMaintenanceInterruptibly() throws InterruptedException {
|
||||
databaseLifecycle.writeLock().lockInterruptibly();
|
||||
}
|
||||
|
||||
public static void unlockDatabaseMaintenance() {
|
||||
databaseLifecycle.writeLock().unlock();
|
||||
}
|
||||
|
||||
public static boolean isDatabaseReloadBlocked() {
|
||||
return databaseReloadPaused && !databaseLifecycle.isWriteLockedByCurrentThread();
|
||||
}
|
||||
|
||||
public static boolean isDatabaseReloadPaused() {
|
||||
return databaseReloadPaused;
|
||||
}
|
||||
|
||||
public static boolean isDatabaseReloadRunning() {
|
||||
return databaseReloadRunning;
|
||||
}
|
||||
|
||||
public static boolean isBackgroundPurgeRunning() {
|
||||
return backgroundPurgeRunning;
|
||||
}
|
||||
|
||||
public static void requireDatabaseReload() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
databaseReloadPaused = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static OperationStartResult beginDatabaseReload() {
|
||||
return beginDatabaseReload(false);
|
||||
}
|
||||
|
||||
public static OperationStartResult beginDatabaseRecovery() {
|
||||
return beginDatabaseReload(true);
|
||||
}
|
||||
|
||||
private static OperationStartResult beginDatabaseReload(boolean allowActiveRollbacks) {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (databaseReloadBlockedForShutdown) {
|
||||
return OperationStartResult.INTERRUPTED;
|
||||
}
|
||||
if (persistenceHalted) {
|
||||
return OperationStartResult.PERSISTENCE_HALTED;
|
||||
}
|
||||
if (databaseReloadRunning) {
|
||||
return OperationStartResult.RELOAD_RUNNING;
|
||||
}
|
||||
if (ConfigHandler.purgeRunning || backgroundPurgeRunning) {
|
||||
return OperationStartResult.PURGE_RUNNING;
|
||||
}
|
||||
if (!allowActiveRollbacks && (!ConfigHandler.activeRollbacks.isEmpty() || pendingRollbackPublications > 0)) {
|
||||
return OperationStartResult.ROLLBACK_RUNNING;
|
||||
}
|
||||
databaseReloadRunning = true;
|
||||
databaseReloadPaused = true;
|
||||
}
|
||||
return OperationStartResult.STARTED;
|
||||
}
|
||||
|
||||
public static void blockDatabaseReloadForShutdown() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
databaseReloadBlockedForShutdown = true;
|
||||
databaseReloadShutdownSignal.complete(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> databaseReloadShutdownSignal() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
return databaseReloadShutdownSignal.copy();
|
||||
}
|
||||
}
|
||||
|
||||
public static void lockDatabaseReload() {
|
||||
databaseLifecycle.writeLock().lock();
|
||||
}
|
||||
|
||||
public static boolean lockDatabaseReload(long timeoutMillis) throws InterruptedException {
|
||||
return databaseLifecycle.writeLock().tryLock(Math.max(0L, timeoutMillis), java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public static void endDatabaseReload(boolean resumePersistence) {
|
||||
try {
|
||||
while ((ConfigHandler.serverRunning || ConfigHandler.converterRunning || ConfigHandler.migrationRunning) && (Consumer.isPaused || ConfigHandler.pauseConsumer || ConfigHandler.purgeRunning || Consumer.consumer_id.get(process_id)[1] == 1)) {
|
||||
if (databaseLifecycle.isWriteLockedByCurrentThread()) {
|
||||
databaseLifecycle.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (resumePersistence) {
|
||||
databaseReloadPaused = false;
|
||||
if (!ConfigHandler.converterRunning) {
|
||||
setMaintenancePaused(false);
|
||||
}
|
||||
}
|
||||
databaseReloadRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void haltPersistence() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (persistenceHalted) {
|
||||
return;
|
||||
}
|
||||
persistenceHalted = true;
|
||||
}
|
||||
ConfigHandler.databaseReachable = false;
|
||||
Chat.sendConsoleMessage(Color.RED + "[CoreProtect] " + Phrase.build(Phrase.DATABASE_PERSISTENCE_HALTED));
|
||||
Chat.sendConsoleMessage(Color.RED + "[CoreProtect] " + Phrase.build(Phrase.DATABASE_QUEUE_LOSS));
|
||||
}
|
||||
|
||||
public static OperationStartResult claimPurge() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (persistenceHalted) {
|
||||
return OperationStartResult.PERSISTENCE_HALTED;
|
||||
}
|
||||
if (databaseReloadPaused) {
|
||||
return OperationStartResult.RELOAD_RUNNING;
|
||||
}
|
||||
if (ConfigHandler.purgeRunning || backgroundPurgeRunning) {
|
||||
return OperationStartResult.PURGE_RUNNING;
|
||||
}
|
||||
if (!ConfigHandler.activeRollbacks.isEmpty() || pendingRollbackPublications > 0) {
|
||||
return OperationStartResult.ROLLBACK_RUNNING;
|
||||
}
|
||||
ConfigHandler.purgeRunning = true;
|
||||
return OperationStartResult.STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
public static void releasePurge() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
ConfigHandler.purgeRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static OperationStartResult claimBackgroundPurge() {
|
||||
boolean pausePersistence;
|
||||
synchronized (rollbackPurgeGate) {
|
||||
pausePersistence = ConfigHandler.databaseType.isClickHouse();
|
||||
OperationStartResult result = beginBackgroundPurge(pausePersistence);
|
||||
if (result != OperationStartResult.STARTED) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return lockBackgroundPurge(pausePersistence);
|
||||
}
|
||||
|
||||
public static OperationStartResult claimBackgroundPurge(boolean pausePersistence) {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
OperationStartResult result = beginBackgroundPurge(pausePersistence);
|
||||
if (result != OperationStartResult.STARTED) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return lockBackgroundPurge(pausePersistence);
|
||||
}
|
||||
|
||||
private static OperationStartResult beginBackgroundPurge(boolean pausePersistence) {
|
||||
if (persistenceHalted) {
|
||||
return OperationStartResult.PERSISTENCE_HALTED;
|
||||
}
|
||||
if (databaseReloadPaused) {
|
||||
return OperationStartResult.RELOAD_RUNNING;
|
||||
}
|
||||
if (ConfigHandler.purgeRunning || backgroundPurgeRunning) {
|
||||
return OperationStartResult.PURGE_RUNNING;
|
||||
}
|
||||
if (!ConfigHandler.activeRollbacks.isEmpty() || pendingRollbackPublications > 0) {
|
||||
return OperationStartResult.ROLLBACK_RUNNING;
|
||||
}
|
||||
backgroundPurgeRunning = true;
|
||||
backgroundPurgePausesPersistence = pausePersistence;
|
||||
return OperationStartResult.STARTED;
|
||||
}
|
||||
|
||||
private static OperationStartResult lockBackgroundPurge(boolean pausePersistence) {
|
||||
if (pausePersistence) {
|
||||
try {
|
||||
databaseLifecycle.writeLock().lockInterruptibly();
|
||||
}
|
||||
catch (InterruptedException exception) {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
backgroundPurgePausesPersistence = false;
|
||||
backgroundPurgeRunning = false;
|
||||
}
|
||||
Thread.currentThread().interrupt();
|
||||
return OperationStartResult.INTERRUPTED;
|
||||
}
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (persistenceHalted) {
|
||||
databaseLifecycle.writeLock().unlock();
|
||||
backgroundPurgePausesPersistence = false;
|
||||
backgroundPurgeRunning = false;
|
||||
return OperationStartResult.PERSISTENCE_HALTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
return OperationStartResult.STARTED;
|
||||
}
|
||||
|
||||
public static void releaseBackgroundPurge() {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (!backgroundPurgeRunning) {
|
||||
return;
|
||||
}
|
||||
if (backgroundPurgePausesPersistence) {
|
||||
databaseLifecycle.writeLock().unlock();
|
||||
}
|
||||
backgroundPurgePausesPersistence = false;
|
||||
backgroundPurgeRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static OperationStartResult claimRollback(String user) {
|
||||
return claimRollback(user, true);
|
||||
}
|
||||
|
||||
public static OperationStartResult claimRollback(String user, boolean requiresPersistence) {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (requiresPersistence && persistenceHalted) {
|
||||
return OperationStartResult.PERSISTENCE_HALTED;
|
||||
}
|
||||
if (databaseReloadPaused) {
|
||||
return OperationStartResult.RELOAD_RUNNING;
|
||||
}
|
||||
if (ConfigHandler.purgeRunning || backgroundPurgeRunning) {
|
||||
return OperationStartResult.PURGE_RUNNING;
|
||||
}
|
||||
if (ConfigHandler.activeRollbacks.containsKey(user)) {
|
||||
return OperationStartResult.ROLLBACK_RUNNING;
|
||||
}
|
||||
ConfigHandler.activeRollbacks.put(user, true);
|
||||
return OperationStartResult.STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
public static void releaseRollback(String user) {
|
||||
synchronized (rollbackPurgeGate) {
|
||||
ConfigHandler.activeRollbacks.remove(user);
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerRollbackPublications(int count) {
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
synchronized (rollbackPurgeGate) {
|
||||
pendingRollbackPublications += count;
|
||||
}
|
||||
}
|
||||
|
||||
public static void completeRollbackPublications(int count) {
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
synchronized (rollbackPurgeGate) {
|
||||
if (pendingRollbackPublications < count) {
|
||||
throw new IllegalStateException("Invalid rollback publication completion");
|
||||
}
|
||||
pendingRollbackPublications -= count;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isShutdownDrainStopped() {
|
||||
return databaseReloadBlockedForShutdown && !ConfigHandler.shutdownDrainRunning;
|
||||
}
|
||||
|
||||
private static boolean pauseConsumer(int process_id) throws InterruptedException {
|
||||
try {
|
||||
while (Consumer.consumer_id.get(process_id)[1] > 0 || Consumer.isPaused
|
||||
|| ((ConfigHandler.serverRunning || ConfigHandler.converterRunning || ConfigHandler.migrationRunning) && (ConfigHandler.pauseConsumer || ConfigHandler.purgeRunning))) {
|
||||
if (persistenceHalted || isShutdownDrainStopped()) {
|
||||
return false;
|
||||
}
|
||||
pausedSuccess = true;
|
||||
Thread.sleep(100);
|
||||
}
|
||||
return !persistenceHalted && !isShutdownDrainStopped();
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
finally {
|
||||
pausedSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean lockConsumerDatabase(Lock lock) throws InterruptedException {
|
||||
while (!persistenceHalted && !isShutdownDrainStopped()) {
|
||||
if (lock.tryLock(100L, TimeUnit.MILLISECONDS)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void processConsumerBatch(int processId, boolean lastRun) throws InterruptedException {
|
||||
Lock databaseLock = databaseLifecycle.readLock();
|
||||
if (!lockConsumerDatabase(databaseLock)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
boolean exclusive = requiresEntityUuidMaintenance(processId);
|
||||
if (exclusive) {
|
||||
databaseLock.unlock();
|
||||
databaseLock = null;
|
||||
Lock writeLock = databaseLifecycle.writeLock();
|
||||
if (!lockConsumerDatabase(writeLock)) {
|
||||
return;
|
||||
}
|
||||
databaseLock = writeLock;
|
||||
}
|
||||
if (databaseReloadPaused || isPaused || persistenceHalted || isShutdownDrainStopped()) {
|
||||
return;
|
||||
}
|
||||
if (exclusive && !Database.awaitConnectionDrain(0L)) {
|
||||
return;
|
||||
}
|
||||
Process.processConsumer(processId, lastRun);
|
||||
}
|
||||
finally {
|
||||
if (databaseLock != null) {
|
||||
databaseLock.unlock();
|
||||
}
|
||||
}
|
||||
pausedSuccess = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean lastRun = false;
|
||||
boolean[] drained = { true, true };
|
||||
|
||||
while (ConfigHandler.serverRunning || ConfigHandler.converterRunning || !lastRun) {
|
||||
while (!isShutdownDrainStopped()
|
||||
&& (ConfigHandler.serverRunning || ConfigHandler.converterRunning || !lastRun || hasPendingConsumerData())) {
|
||||
if (!ConfigHandler.serverRunning && !ConfigHandler.converterRunning) {
|
||||
lastRun = true;
|
||||
}
|
||||
try {
|
||||
int process_id = 0;
|
||||
if (currentConsumer == 0) {
|
||||
currentConsumer = 1;
|
||||
if (persistenceHalted) {
|
||||
if (lastRun) {
|
||||
break;
|
||||
}
|
||||
if (databaseReloadBlockedForShutdown) {
|
||||
LockSupport.parkNanos(100_000_000L);
|
||||
}
|
||||
else {
|
||||
process_id = 1;
|
||||
currentConsumer = 0;
|
||||
errorDelay();
|
||||
}
|
||||
Thread.sleep(500);
|
||||
pauseConsumer(process_id);
|
||||
Process.processConsumer(process_id, lastRun);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (DuckDBRecovery.isPending()) {
|
||||
DuckDBRecovery.recoverIfRequested();
|
||||
if (DuckDBRecovery.isPending()) {
|
||||
Thread.sleep(500L);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
int process_id = 0;
|
||||
synchronized (Consumer.consumer_id) {
|
||||
if (currentConsumer == 0) {
|
||||
currentConsumer = 1;
|
||||
}
|
||||
else {
|
||||
process_id = 1;
|
||||
currentConsumer = 0;
|
||||
}
|
||||
}
|
||||
Thread.sleep(consumerDelay(lastRun || !drained[0] || !drained[1]));
|
||||
if (!pauseConsumer(process_id)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
processConsumerBatch(process_id, lastRun);
|
||||
}
|
||||
finally {
|
||||
drained[process_id] = getConsumerSize(process_id) == 0;
|
||||
}
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
errorDelay();
|
||||
ErrorReporter.report(e);
|
||||
if (ConfigHandler.serverRunning || ConfigHandler.converterRunning) {
|
||||
errorDelay();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
Bukkit.getPluginManager().disablePlugin(CoreProtect.getInstance());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package net.coreprotect.consumer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
@@ -9,10 +13,13 @@ import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.CreatureSpawner;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Bisected;
|
||||
import org.bukkit.block.data.Bisected.Half;
|
||||
import org.bukkit.block.data.Waterlogged;
|
||||
import org.bukkit.block.data.type.Bed;
|
||||
import org.bukkit.block.data.type.Bed.Part;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
@@ -24,26 +31,73 @@ import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.consumer.process.Process;
|
||||
import net.coreprotect.listener.block.BlockUtil;
|
||||
import net.coreprotect.model.BlockGroup;
|
||||
import net.coreprotect.model.entity.EntityContainerRollbackUpdate;
|
||||
import net.coreprotect.model.entity.EntityContainerTransaction;
|
||||
import net.coreprotect.model.entity.EntityInteraction;
|
||||
import net.coreprotect.model.entity.EntityInteractionAction;
|
||||
import net.coreprotect.model.entity.EntityInteractionOrigin;
|
||||
import net.coreprotect.model.entity.EntitySpawnData;
|
||||
import net.coreprotect.thread.CacheHandler;
|
||||
import net.coreprotect.thread.Scheduler;
|
||||
import net.coreprotect.utility.BlockUtils;
|
||||
import net.coreprotect.utility.EntitySpawnTracking;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.WorldUtils;
|
||||
import net.coreprotect.worldedit.WorldEditBlockState;
|
||||
|
||||
public class Queue {
|
||||
|
||||
protected static synchronized int modifyForceContainer(String id, ItemStack[] container) {
|
||||
private static final int MAX_ROLLBACK_ROWS_PER_RECORD = 100_000;
|
||||
|
||||
public static synchronized void addForceContainer(String id, ItemStack[] container) {
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigHandler.forceContainer.computeIfAbsent(id, k -> new ArrayList<>()).add(container);
|
||||
}
|
||||
|
||||
public static synchronized void setForceContainer(String id, ItemStack[] container) {
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ItemStack[]> forceList = new ArrayList<>();
|
||||
forceList.add(container);
|
||||
ConfigHandler.forceContainer.put(id, forceList);
|
||||
}
|
||||
|
||||
public static ItemStack[] pollForceContainer(String id) {
|
||||
return pollForceContainer(id, 0);
|
||||
}
|
||||
|
||||
public static synchronized ItemStack[] pollForceContainer(String id, int index) {
|
||||
List<ItemStack[]> forceList = ConfigHandler.forceContainer.get(id);
|
||||
if (forceList == null) {
|
||||
return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (container == null) {
|
||||
forceList.remove(0);
|
||||
}
|
||||
else {
|
||||
forceList.add(container);
|
||||
ItemStack[] container = index < 0 || index >= forceList.size() ? null : forceList.remove(index);
|
||||
if (forceList.isEmpty()) {
|
||||
ConfigHandler.forceContainer.remove(id);
|
||||
}
|
||||
|
||||
return forceList.size();
|
||||
return container;
|
||||
}
|
||||
|
||||
public static synchronized ItemStack[] peekForceContainer(String id) {
|
||||
List<ItemStack[]> forceList = ConfigHandler.forceContainer.get(id);
|
||||
return (forceList == null || forceList.isEmpty()) ? null : forceList.get(0);
|
||||
}
|
||||
|
||||
public static synchronized int getForceContainerSize(String id) {
|
||||
List<ItemStack[]> forceList = ConfigHandler.forceContainer.get(id);
|
||||
return forceList == null ? 0 : forceList.size();
|
||||
}
|
||||
|
||||
public static synchronized void removeForceContainer(String id) {
|
||||
ConfigHandler.forceContainer.remove(id);
|
||||
}
|
||||
|
||||
protected static synchronized int getChestId(String id) {
|
||||
@@ -52,34 +106,84 @@ public class Queue {
|
||||
return chestId;
|
||||
}
|
||||
|
||||
protected static synchronized int getItemId(String id) {
|
||||
int chestId = ConfigHandler.loggingItem.getOrDefault(id, -1) + 1;
|
||||
ConfigHandler.loggingItem.put(id, chestId);
|
||||
return chestId;
|
||||
// The Queue lock keeps ItemTransactionProcess.discard from clearing an item before its generation is registered,
|
||||
// and compute keeps the append atomic with the consumer's take in ItemLogger
|
||||
protected static synchronized int addPendingItems(ConcurrentHashMap<String, List<ItemStack>> pendingItems, String id, ItemStack... items) {
|
||||
pendingItems.compute(id, (key, list) -> {
|
||||
List<ItemStack> result = list == null ? new ArrayList<>(items.length) : list;
|
||||
Collections.addAll(result, items);
|
||||
return result;
|
||||
});
|
||||
int itemId = ConfigHandler.loggingItem.getOrDefault(id, -1) + 1;
|
||||
ConfigHandler.loggingItem.put(id, itemId);
|
||||
return itemId;
|
||||
}
|
||||
|
||||
private static synchronized void addConsumer(int currentConsumer, Object[] data) {
|
||||
Consumer.consumer.get(currentConsumer).add(data);
|
||||
private static boolean queueStandardData(Object[] data, String[] user, Object object, boolean first, long reservation) {
|
||||
return queueStandardData(data, user, object, first, null, null, reservation);
|
||||
}
|
||||
|
||||
private static synchronized void queueStandardData(int consumerId, int currentConsumer, String[] user, Object object) {
|
||||
Consumer.consumerUsers.get(currentConsumer).put(consumerId, user);
|
||||
Consumer.consumerObjects.get(currentConsumer).put(consumerId, object);
|
||||
Consumer.consumer_id.put(currentConsumer, new Integer[] { Consumer.consumer_id.get(currentConsumer)[0], 0 });
|
||||
private static synchronized <T> boolean queueStandardData(Object[] data, String[] user, Object object, boolean first, Map<Integer, ? extends Map<Integer, T>> additionalMaps, T additionalData, long reservation) {
|
||||
boolean rollbackPublication = Process.isRollbackPublication((int) data[1], object);
|
||||
if (Consumer.isPersistenceHalted()) {
|
||||
Consumer.completeReservation(reservation, 1);
|
||||
if (rollbackPublication) {
|
||||
throw new IllegalStateException("Database persistence halted before rollback state could be queued");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
int currentConsumer = (int) (reservation >>> 32);
|
||||
int consumerId = (int) reservation;
|
||||
boolean published = false;
|
||||
if (rollbackPublication) {
|
||||
Consumer.registerRollbackPublications(1);
|
||||
}
|
||||
try {
|
||||
data[0] = consumerId;
|
||||
if (additionalMaps != null) {
|
||||
additionalMaps.get(currentConsumer).put(consumerId, additionalData);
|
||||
}
|
||||
Consumer.consumerUsers.get(currentConsumer).put(consumerId, user);
|
||||
Consumer.consumerObjects.get(currentConsumer).put(consumerId, object);
|
||||
if (first) {
|
||||
Consumer.consumer.get(currentConsumer).add(0, data);
|
||||
}
|
||||
else {
|
||||
Consumer.consumer.get(currentConsumer).add(data);
|
||||
}
|
||||
published = true;
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
if (!published) {
|
||||
Consumer.consumer.get(currentConsumer).remove(data);
|
||||
Consumer.consumerUsers.get(currentConsumer).remove(consumerId);
|
||||
Consumer.consumerObjects.get(currentConsumer).remove(consumerId);
|
||||
if (additionalMaps != null) {
|
||||
additionalMaps.get(currentConsumer).remove(consumerId);
|
||||
}
|
||||
if (rollbackPublication) {
|
||||
Consumer.completeRollbackPublications(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Consumer.completeReservation(reservation, 1);
|
||||
}
|
||||
}
|
||||
return published;
|
||||
}
|
||||
|
||||
private static Location getBlockLocation(Location location) {
|
||||
return new Location(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
}
|
||||
|
||||
protected static void queueAdvancedBreak(String user, BlockState block, Material type, String blockData, int data, Material breakType, int blockNumber) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.BLOCK_BREAK, type, data, breakType, 0, blockNumber, blockData });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queueStandardData(new Object[] { null, Process.BLOCK_BREAK, type, data, breakType, 0, blockNumber, blockData }, new String[] { user, null }, block, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueArtInsert(int id, String name) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.ART_INSERT, null, 0, null, 0, id, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { null, null }, name);
|
||||
queueStandardData(new Object[] { null, Process.ART_INSERT, null, 0, null, 0, id, null }, new String[] { null, null }, name, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueBlockBreak(String user, BlockState block, Material type, String blockData, int extraData) {
|
||||
@@ -89,12 +193,12 @@ public class Queue {
|
||||
protected static void queueBlockBreakValidate(final String user, final Block block, final BlockState blockState, final Material type, final String blockData, final int extraData, int ticks) {
|
||||
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> {
|
||||
try {
|
||||
if (!block.getType().equals(type)) {
|
||||
if (!Objects.equals(block.getType(), type)) {
|
||||
queueBlockBreak(user, blockState, type, blockData, null, extraData, 0);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}, block.getLocation(), ticks);
|
||||
}
|
||||
@@ -104,7 +208,7 @@ public class Queue {
|
||||
CreatureSpawner mobSpawner = (CreatureSpawner) block;
|
||||
extraData = EntityUtils.getSpawnerType(mobSpawner.getSpawnedType());
|
||||
}
|
||||
else if (type == Material.IRON_DOOR || BlockGroup.DOORS.contains(type) || type.equals(Material.SUNFLOWER) || type.equals(Material.LILAC) || type.equals(Material.TALL_GRASS) || type.equals(Material.LARGE_FERN) || type.equals(Material.ROSE_BUSH) || type.equals(Material.PEONY)) { // Double plant
|
||||
else if (type != null && BlockGroup.LOGGED_BY_LOWER_HALF.contains(type)) { // Double plant
|
||||
if (block.getBlockData() instanceof Bisected) {
|
||||
if (((Bisected) block.getBlockData()).getHalf().equals(Half.TOP)) {
|
||||
if (blockNumber == 5) {
|
||||
@@ -112,8 +216,8 @@ public class Queue {
|
||||
}
|
||||
|
||||
if (block.getY() > BukkitAdapter.ADAPTER.getMinHeight(block.getWorld())) {
|
||||
block = block.getWorld().getBlockAt(block.getX(), block.getY() - 1, block.getZ()).getState();
|
||||
if (type != block.getType()) {
|
||||
block = block instanceof WorldEditBlockState ? ((WorldEditBlockState) block).getLowerHalf() : block.getWorld().getBlockAt(block.getX(), block.getY() - 1, block.getZ()).getState();
|
||||
if (block == null || type != block.getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,16 +226,13 @@ public class Queue {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type.name().endsWith("_BED") && block.getBlockData() instanceof Bed) {
|
||||
else if (type != null && type.name().endsWith("_BED") && block.getBlockData() instanceof Bed) {
|
||||
if (((Bed) block.getBlockData()).getPart().equals(Part.HEAD)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.BLOCK_BREAK, type, extraData, breakType, 0, blockNumber, blockData });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queueStandardData(new Object[] { null, Process.BLOCK_BREAK, type, extraData, breakType, 0, blockNumber, blockData }, new String[] { user, null }, block, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueBlockPlace(String user, BlockState blockLocation, Material blockType, BlockState blockReplaced, Material forceType, int forceD, int forceData, String blockData) {
|
||||
@@ -151,7 +252,7 @@ public class Queue {
|
||||
replaceType = blockReplaced.getType();
|
||||
replaceData = 0;
|
||||
|
||||
if ((replaceType == Material.IRON_DOOR || BlockGroup.DOORS.contains(replaceType) || replaceType.equals(Material.SUNFLOWER) || replaceType.equals(Material.LILAC) || replaceType.equals(Material.TALL_GRASS) || replaceType.equals(Material.LARGE_FERN) || replaceType.equals(Material.ROSE_BUSH) || replaceType.equals(Material.PEONY)) && replaceData >= 8) { // Double plant top half
|
||||
if (replaceType != null && (replaceType == Material.IRON_DOOR || BlockGroup.DOORS.contains(replaceType) || replaceType.equals(Material.SUNFLOWER) || replaceType.equals(Material.LILAC) || replaceType.equals(Material.TALL_GRASS) || replaceType.equals(Material.LARGE_FERN) || replaceType.equals(Material.ROSE_BUSH) || replaceType.equals(Material.PEONY)) && replaceData >= 8) { // Double plant top half
|
||||
BlockState blockBelow = blockReplaced.getWorld().getBlockAt(blockReplaced.getX(), blockReplaced.getY() - 1, blockReplaced.getZ()).getState();
|
||||
Material belowType = blockBelow.getType();
|
||||
Queue.queueBlockBreak(user, blockBelow, belowType, blockBelow.getBlockData().getAsString(), 0);
|
||||
@@ -173,10 +274,7 @@ public class Queue {
|
||||
replacedBlockData = blockReplaced.getBlockData().getAsString();
|
||||
}
|
||||
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.BLOCK_PLACE, type, data, replaceType, replaceData, forceData, blockData, replacedBlockData });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, blockLocation);
|
||||
queueStandardData(new Object[] { null, Process.BLOCK_PLACE, type, data, replaceType, replaceData, forceData, blockData, replacedBlockData }, new String[] { user, null }, blockLocation, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueBlockPlaceDelayed(final String user, final Location placed, final Material type, final String blockData, final BlockState replaced, int ticks) {
|
||||
@@ -185,101 +283,210 @@ public class Queue {
|
||||
queueBlockPlace(user, placed.getBlock().getState(), type, replaced, null, -1, 0, blockData);
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}, placed, ticks);
|
||||
}
|
||||
|
||||
protected static void queueBlockPlaceValidate(final String user, final BlockState blockLocation, final Block block, final BlockState blockReplaced, final Material forceT, final int forceD, final int forceData, final String blockData, int ticks) {
|
||||
queueBlockPlaceValidate(user, blockLocation, block, blockReplaced, forceT, forceD, forceData, blockData, ticks, false);
|
||||
}
|
||||
|
||||
protected static void queueBlockPlaceValidate(final String user, final BlockState blockLocation, final Block block, final BlockState blockReplaced, final Material forceT, final int forceD, final int forceData, final String blockData, int ticks, boolean cacheLookup) {
|
||||
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> {
|
||||
try {
|
||||
Material blockType = block.getType();
|
||||
if (blockType.equals(forceT)) {
|
||||
BlockState replacedBlock = blockReplaced;
|
||||
boolean placed = Objects.equals(blockType, forceT);
|
||||
if (!placed && forceT == Material.WATER) {
|
||||
BlockData currentBlockData = block.getBlockData();
|
||||
if (currentBlockData instanceof Waterlogged) {
|
||||
Waterlogged waterlogged = (Waterlogged) currentBlockData;
|
||||
if (waterlogged.isWaterlogged()) {
|
||||
boolean wasWaterlogged = false;
|
||||
if (blockReplaced != null) {
|
||||
BlockData replacedBlockData = blockReplaced.getBlockData();
|
||||
if (replacedBlockData instanceof Waterlogged) {
|
||||
wasWaterlogged = ((Waterlogged) replacedBlockData).isWaterlogged();
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasWaterlogged) {
|
||||
placed = true;
|
||||
replacedBlock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (placed) {
|
||||
BlockState blockStateLocation = blockLocation;
|
||||
if (Config.getConfig(blockLocation.getWorld()).BLOCK_MOVEMENT) {
|
||||
if (blockType != null && Objects.equals(blockType, forceT) && Config.getConfig(blockLocation.getWorld()).BLOCK_MOVEMENT) {
|
||||
blockStateLocation = BlockUtil.gravityScan(blockLocation.getLocation(), blockType, user).getState();
|
||||
}
|
||||
|
||||
queueBlockPlace(user, blockStateLocation, blockType, blockReplaced, forceT, forceD, forceData, blockData);
|
||||
if (cacheLookup) {
|
||||
Location cacheLocation = blockStateLocation.getLocation();
|
||||
int worldId = WorldUtils.getWorldId(cacheLocation.getWorld().getName());
|
||||
int unixTimestamp = (int) (System.currentTimeMillis() / 1000L);
|
||||
CacheHandler.lookupCache.put(cacheLocation.getBlockX() + "." + cacheLocation.getBlockY() + "." + cacheLocation.getBlockZ() + "." + worldId, new Object[] { unixTimestamp, user, forceT });
|
||||
}
|
||||
|
||||
queueBlockPlace(user, blockStateLocation, blockType, replacedBlock, forceT, forceD, forceData, blockData);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}, blockLocation.getLocation(), ticks);
|
||||
}
|
||||
|
||||
protected static void queueBlockGravityValidate(final String user, final Location location, final Block block, final Material blockType, int ticks) {
|
||||
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> {
|
||||
try {
|
||||
Block placementBlock = BlockUtil.gravityScan(location, blockType, user);
|
||||
if (!block.equals(placementBlock)) {
|
||||
queueBlockPlace(user, placementBlock.getState(), blockType, null, blockType, -1, 0, null);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, location, ticks);
|
||||
}
|
||||
|
||||
protected static void queueContainerBreak(String user, Location location, Material type, ItemStack[] oldInventory) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.CONTAINER_BREAK, type, 0, null, 0, 0, null });
|
||||
Consumer.consumerContainers.get(currentConsumer).put(consumerId, oldInventory);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, location);
|
||||
queueStandardData(new Object[] { null, Process.CONTAINER_BREAK, type, 0, null, 0, 0, null }, new String[] { user, null }, location, false, Consumer.consumerContainers, oldInventory, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static synchronized void queueContainerTransaction(String user, Location location, Material type, Object inventory, int chestId) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.CONTAINER_TRANSACTION, type, 0, null, 0, chestId, null });
|
||||
Consumer.consumerInventories.get(currentConsumer).put(consumerId, inventory);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, location);
|
||||
queueStandardData(new Object[] { null, Process.CONTAINER_TRANSACTION, type, 0, null, 0, chestId, null }, new String[] { user, null }, location, false, Consumer.consumerInventories, inventory, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
public static void queueEntityContainerTransaction(String user, java.util.UUID entityUuid, Location currentLocation, ItemStack[] oldContents, ItemStack[] newContents) {
|
||||
queueEntityContainerTransaction(user, new EntityContainerTransaction(entityUuid, currentLocation, oldContents, newContents));
|
||||
}
|
||||
|
||||
public static void queueEntityContainerTransaction(String user, EntityContainerTransaction transaction) {
|
||||
if (user == null || user.isEmpty() || transaction == null) {
|
||||
return;
|
||||
}
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_CONTAINER_TRANSACTION, null, 0, null, 0, 0, null }, new String[] { user, null }, transaction, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
public static void queueEntityInteraction(String user, Entity entity, EntityInteractionAction action) {
|
||||
queueEntityInteraction(user, entity, action, null);
|
||||
}
|
||||
|
||||
public static void queueEntityInteraction(String user, Entity entity, EntityInteractionAction action, byte[] metadata) {
|
||||
if (user == null || user.trim().isEmpty() || entity == null || action == null) {
|
||||
return;
|
||||
}
|
||||
Location currentLocation = entity.getLocation();
|
||||
EntityInteractionOrigin origin = EntitySpawnTracking.getOrCreateInteractionOrigin(entity);
|
||||
if (origin == null || currentLocation.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
boolean promotion = EntitySpawnTracking.beginDatabaseIdentityPromotion(entity);
|
||||
EntityInteraction interaction = new EntityInteraction(entity.getUniqueId(), entity.getType(), origin, currentLocation, action, metadata).withIdentityPromotion(promotion);
|
||||
queueEntityInteraction(user, interaction);
|
||||
}
|
||||
|
||||
public static void queueEntityInteraction(String user, EntityInteraction interaction) {
|
||||
if (user == null || user.trim().isEmpty() || interaction == null) {
|
||||
if (interaction != null && interaction.hasIdentityPromotion()) {
|
||||
EntitySpawnTracking.cancelDatabaseIdentityPromotion(interaction.getEntityUuid(), interaction.getCurrentLocation());
|
||||
}
|
||||
return;
|
||||
}
|
||||
queueReservedEntityInteraction(user, interaction, reserveEntityInteractionQueue());
|
||||
}
|
||||
|
||||
protected static long reserveEntityInteractionQueue() {
|
||||
return Consumer.reserveConsumer();
|
||||
}
|
||||
|
||||
protected static void queueReservedEntityInteraction(String user, EntityInteraction interaction, long reservation) {
|
||||
boolean queued = false;
|
||||
try {
|
||||
if (user == null || user.trim().isEmpty() || interaction == null) {
|
||||
Consumer.completeReservation(reservation, 1);
|
||||
return;
|
||||
}
|
||||
queued = queueStandardData(new Object[] { null, Process.ENTITY_INTERACTION, null, 0, null, 0, 0, null }, new String[] { user, null }, interaction, false, reservation);
|
||||
}
|
||||
finally {
|
||||
if (!queued && interaction != null && interaction.hasIdentityPromotion()) {
|
||||
EntitySpawnTracking.cancelDatabaseIdentityPromotion(interaction.getEntityUuid(), interaction.getCurrentLocation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static void queueItemTransaction(String user, Location location, int time, int offset, int itemId) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.ITEM_TRANSACTION, null, offset, null, time, itemId, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, location);
|
||||
queueStandardData(new Object[] { null, Process.ITEM_TRANSACTION, null, offset, null, time, itemId, null }, new String[] { user, null }, location, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueEntityInsert(int id, String name) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.ENTITY_INSERT, null, 0, null, 0, id, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { null, null }, name);
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_INSERT, null, 0, null, 0, id, null }, new String[] { null, null }, name, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueEntityKill(String user, Location location, List<Object> data, EntityType type) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.ENTITY_KILL, null, 0, null, 0, 0 });
|
||||
Consumer.consumerObjectList.get(currentConsumer).put(consumerId, data);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, new Object[] { location.getBlock().getState(), type, null });
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_KILL, null, 0, null, 0, 0 }, new String[] { user, null }, new Object[] { getBlockLocation(location), type, null }, false, Consumer.consumerObjectList, data, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueEntitySpawn(String user, BlockState block, EntityType type, int data) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.ENTITY_SPAWN, null, 0, null, 0, data, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, new Object[] { block, type });
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_SPAWN, null, 0, null, 0, data, null }, new String[] { user, null }, new Object[] { block, type }, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnLog(String user, java.util.UUID uuid, EntityType type, Location location) {
|
||||
queueEntitySpawnLog(user, EntitySpawnData.log(uuid, type, location));
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnLog(String user, EntitySpawnData spawnData) {
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_SPAWN_LOG, null, 0, null, 0, 0, null }, new String[] { user, null }, spawnData, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnLocation(java.util.UUID uuid, Location location, long verificationEpoch) {
|
||||
queueEntitySpawnUpdate(EntitySpawnData.location(uuid, location, verificationEpoch));
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnRemoved(java.util.UUID uuid, Location location) {
|
||||
if (uuid == null || location == null || location.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
|
||||
if (worldId < 0) {
|
||||
queueEntitySpawnUpdate(EntitySpawnData.removed(uuid, location));
|
||||
return;
|
||||
}
|
||||
queueEntitySpawnRemoved(uuid, new EntityInteractionOrigin(worldId, location.getX(), location.getY(), location.getZ()), location);
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnRemoved(Entity entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
Location location = entity.getLocation();
|
||||
EntityInteractionOrigin origin = EntitySpawnTracking.getOrCreateInteractionOrigin(entity);
|
||||
if (location.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
if (origin == null) {
|
||||
queueEntitySpawnUpdate(EntitySpawnData.removed(entity.getUniqueId(), location));
|
||||
return;
|
||||
}
|
||||
queueEntitySpawnRemoved(entity.getUniqueId(), origin, location);
|
||||
}
|
||||
|
||||
private static void queueEntitySpawnRemoved(java.util.UUID uuid, EntityInteractionOrigin origin, Location location) {
|
||||
int time = (int) (System.currentTimeMillis() / 1000L);
|
||||
queueEntitySpawnUpdate(EntitySpawnData.removed(uuid, origin, location, time));
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnRevived(java.util.UUID previousUuid, java.util.UUID uuid, Location location) {
|
||||
queueEntitySpawnUpdate(EntitySpawnData.revived(previousUuid, uuid, location));
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnUpdate(EntitySpawnData update) {
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_SPAWN_UPDATE, null, 0, null, 0, 0, null }, new String[] { null, null }, update, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
public static void queueEntitySpawnUpdateFirst(EntitySpawnData update) {
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_SPAWN_UPDATE, null, 0, null, 0, 0, null }, new String[] { null, null }, update, true, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueMaterialInsert(int id, String name) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.MATERIAL_INSERT, null, 0, null, 0, id, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { null, null }, name);
|
||||
queueStandardData(new Object[] { null, Process.MATERIAL_INSERT, null, 0, null, 0, id, null }, new String[] { null, null }, name, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueBlockDataInsert(int id, String data) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.BLOCKDATA_INSERT, null, 0, null, 0, id, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { null, null }, data);
|
||||
queueStandardData(new Object[] { null, Process.BLOCKDATA_INSERT, null, 0, null, 0, id, null }, new String[] { null, null }, data, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueNaturalBlockBreak(String user, BlockState block, Block relative, Material type, String blockData, int data) {
|
||||
@@ -288,57 +495,36 @@ public class Queue {
|
||||
blockStates.add(relative.getState());
|
||||
}
|
||||
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.NATURAL_BLOCK_BREAK, type, data, null, 0, 0, blockData });
|
||||
Consumer.consumerBlockList.get(currentConsumer).put(consumerId, blockStates);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queueStandardData(new Object[] { null, Process.NATURAL_BLOCK_BREAK, type, data, null, 0, 0, blockData }, new String[] { user, null }, block, false, Consumer.consumerBlockList, blockStates, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queuePlayerChat(Player player, String message, long timestamp) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.PLAYER_CHAT, null, 0, null, 0, 0, null });
|
||||
Consumer.consumerStrings.get(currentConsumer).put(consumerId, message);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { player.getName(), null }, new Object[] { timestamp, player.getLocation().clone() });
|
||||
queueStandardData(new Object[] { null, Process.PLAYER_CHAT, null, 0, null, 0, 0, null }, new String[] { player.getName(), null }, new Object[] { timestamp, player.getLocation().clone() }, false, Consumer.consumerStrings, message, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queuePlayerCommand(Player player, String message, long timestamp) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.PLAYER_COMMAND, null, 0, null, 0, 0, null });
|
||||
Consumer.consumerStrings.get(currentConsumer).put(consumerId, message);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { player.getName(), null }, new Object[] { timestamp, player.getLocation().clone() });
|
||||
queueStandardData(new Object[] { null, Process.PLAYER_COMMAND, null, 0, null, 0, 0, null }, new String[] { player.getName(), null }, new Object[] { timestamp, player.getLocation().clone() }, false, Consumer.consumerStrings, message, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queuePlayerInteraction(String user, BlockState block, Material type) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.PLAYER_INTERACTION, type, 0, null, 0, 0, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queuePlayerInteraction(user, block.getLocation(), type, block.getBlockData().getAsString());
|
||||
}
|
||||
|
||||
protected static void queuePlayerInteraction(String user, Location location, Material type, String blockData) {
|
||||
queueStandardData(new Object[] { null, Process.PLAYER_INTERACTION, type, 0, null, 0, 0, blockData }, new String[] { user, null }, getBlockLocation(location), false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queuePlayerKill(String user, Location location, String player) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.PLAYER_KILL, null, 0, null, 0, 0, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, new Object[] { location.getBlock().getState(), player });
|
||||
queueStandardData(new Object[] { null, Process.PLAYER_KILL, null, 0, null, 0, 0, null }, new String[] { user, null }, new Object[] { getBlockLocation(location), player }, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queuePlayerLogin(Player player, int time, int configSessions, int configUsernames) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
String uuid = player.getUniqueId().toString();
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.PLAYER_LOGIN, null, configSessions, null, configUsernames, time, null });
|
||||
Consumer.consumerStrings.get(currentConsumer).put(consumerId, uuid);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { player.getName(), uuid }, player.getLocation().clone());
|
||||
queueStandardData(new Object[] { null, Process.PLAYER_LOGIN, null, configSessions, null, configUsernames, time, null }, new String[] { player.getName(), uuid }, player.getLocation().clone(), false, Consumer.consumerStrings, uuid, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queuePlayerQuit(Player player, int time) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.PLAYER_LOGOUT, null, 0, null, 0, time, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { player.getName(), null }, player.getLocation().clone());
|
||||
queueStandardData(new Object[] { null, Process.PLAYER_LOGOUT, null, 0, null, 0, time, null }, new String[] { player.getName(), null }, player.getLocation().clone(), false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueRollbackUpdate(String user, Location location, List<Object[]> list, int table, int action) {
|
||||
@@ -346,11 +532,130 @@ public class Queue {
|
||||
location = new Location(Bukkit.getServer().getWorlds().get(0), 0, 0, 0);
|
||||
}
|
||||
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, table, null, 0, null, 0, action, null });
|
||||
Consumer.consumerObjectArrayList.get(currentConsumer).put(consumerId, list);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, location);
|
||||
queueRollbackUpdates(user, location, list, table, action, false);
|
||||
}
|
||||
|
||||
public static void queueEntityContainerRollbackUpdate(String user, Location location, List<Object[]> rows, int rollbackType, boolean inventoryRollback) {
|
||||
if (location == null) {
|
||||
location = new Location(Bukkit.getServer().getWorlds().get(0), 0, 0, 0);
|
||||
}
|
||||
queueRollbackUpdates(user, location, rows, Process.ENTITY_CONTAINER_ROLLBACK_UPDATE, rollbackType, inventoryRollback);
|
||||
}
|
||||
|
||||
public static void queueEntityContainerRollbackUpdate(String user, EntitySpawnData transition, List<Object[]> rows, int rollbackType, boolean inventoryRollback) {
|
||||
EntityContainerRollbackUpdate update = new EntityContainerRollbackUpdate(user, transition, rows, rollbackType, inventoryRollback);
|
||||
queueStandardData(new Object[] { null, Process.ENTITY_CONTAINER_TRANSITION_UPDATE, null, 0, null, 0, 0, null }, new String[] { user, null }, update, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
public static synchronized void queueEntityRetriesFirst(List<EntityContainerRollbackUpdate> containerUpdates, List<EntitySpawnData> spawnUpdates) {
|
||||
if (Consumer.isPersistenceHalted()) {
|
||||
return;
|
||||
}
|
||||
List<EntityContainerRollbackUpdate> containerRetries = new ArrayList<>(containerUpdates);
|
||||
List<EntitySpawnData> spawnRetries = new ArrayList<>(spawnUpdates);
|
||||
int updateCount = containerRetries.size() + spawnRetries.size();
|
||||
if (updateCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Object[]> records = new ArrayList<>(updateCount);
|
||||
long reservation = Consumer.reserveConsumers(updateCount);
|
||||
int rollbackPublicationCount = containerRetries.size();
|
||||
for (EntitySpawnData update : spawnRetries) {
|
||||
if (Process.isRollbackPublication(Process.ENTITY_SPAWN_UPDATE, update)) {
|
||||
rollbackPublicationCount++;
|
||||
}
|
||||
}
|
||||
Consumer.registerRollbackPublications(rollbackPublicationCount);
|
||||
boolean published = false;
|
||||
try {
|
||||
int currentConsumer = (int) (reservation >>> 32);
|
||||
int firstConsumerId = (int) reservation;
|
||||
int updateIndex = 0;
|
||||
for (EntityContainerRollbackUpdate update : containerRetries) {
|
||||
int consumerId = firstConsumerId + updateIndex++;
|
||||
records.add(new Object[] { consumerId, Process.ENTITY_CONTAINER_TRANSITION_UPDATE, null, 0, null, 0, 0, null });
|
||||
Consumer.consumerUsers.get(currentConsumer).put(consumerId, new String[] { update.getUser(), null });
|
||||
Consumer.consumerObjects.get(currentConsumer).put(consumerId, update);
|
||||
}
|
||||
for (EntitySpawnData update : spawnRetries) {
|
||||
int consumerId = firstConsumerId + updateIndex++;
|
||||
records.add(new Object[] { consumerId, Process.ENTITY_SPAWN_UPDATE, null, 0, null, 0, 0, null });
|
||||
Consumer.consumerUsers.get(currentConsumer).put(consumerId, new String[] { null, null });
|
||||
Consumer.consumerObjects.get(currentConsumer).put(consumerId, update);
|
||||
}
|
||||
Consumer.consumer.get(currentConsumer).addAll(0, records);
|
||||
published = true;
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
if (!published) {
|
||||
int currentConsumer = (int) (reservation >>> 32);
|
||||
int firstConsumerId = (int) reservation;
|
||||
Consumer.consumer.get(currentConsumer).removeAll(records);
|
||||
for (int index = 0; index < updateCount; index++) {
|
||||
int consumerId = firstConsumerId + index;
|
||||
Consumer.consumerUsers.get(currentConsumer).remove(consumerId);
|
||||
Consumer.consumerObjects.get(currentConsumer).remove(consumerId);
|
||||
}
|
||||
Consumer.completeRollbackPublications(rollbackPublicationCount);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Consumer.completeReservation(reservation, updateCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void queueRollbackUpdates(String user, Location location, List<Object[]> rows, int table, int action, boolean inventoryRollback) {
|
||||
Objects.requireNonNull(rows, "rows");
|
||||
if (rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int recordCount = ((rows.size() - 1) / MAX_ROLLBACK_ROWS_PER_RECORD) + 1;
|
||||
long reservation = Consumer.reserveConsumers(recordCount);
|
||||
int currentConsumer = (int) (reservation >>> 32);
|
||||
int firstConsumerId = (int) reservation;
|
||||
List<Object[]> records = new ArrayList<>(recordCount);
|
||||
boolean registered = false;
|
||||
boolean published = false;
|
||||
try {
|
||||
if (Consumer.isPersistenceHalted()) {
|
||||
throw new IllegalStateException("Database persistence halted before rollback state could be queued");
|
||||
}
|
||||
Consumer.registerRollbackPublications(recordCount);
|
||||
registered = true;
|
||||
for (int recordIndex = 0; recordIndex < recordCount; recordIndex++) {
|
||||
int fromIndex = recordIndex * MAX_ROLLBACK_ROWS_PER_RECORD;
|
||||
int toIndex = Math.min(fromIndex + MAX_ROLLBACK_ROWS_PER_RECORD, rows.size());
|
||||
int consumerId = firstConsumerId + recordIndex;
|
||||
records.add(new Object[] { consumerId, table, null, inventoryRollback ? 1 : 0, null, 0, action, null });
|
||||
Consumer.consumerUsers.get(currentConsumer).put(consumerId, new String[] { user, null });
|
||||
Consumer.consumerObjects.get(currentConsumer).put(consumerId, location);
|
||||
Consumer.consumerObjectArrayList.get(currentConsumer).put(consumerId, new ArrayList<>(rows.subList(fromIndex, toIndex)));
|
||||
}
|
||||
Consumer.consumer.get(currentConsumer).addAll(records);
|
||||
published = true;
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
if (!published) {
|
||||
Consumer.consumer.get(currentConsumer).removeAll(records);
|
||||
for (int recordIndex = 0; recordIndex < recordCount; recordIndex++) {
|
||||
int consumerId = firstConsumerId + recordIndex;
|
||||
Consumer.consumerUsers.get(currentConsumer).remove(consumerId);
|
||||
Consumer.consumerObjects.get(currentConsumer).remove(consumerId);
|
||||
Consumer.consumerObjectArrayList.get(currentConsumer).remove(consumerId);
|
||||
}
|
||||
if (registered) {
|
||||
Consumer.completeRollbackPublications(recordCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Consumer.completeReservation(reservation, recordCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static void queueSignText(String user, Location location, int action, int color, int colorSecondary, boolean frontGlowing, boolean backGlowing, boolean isWaxed, boolean isFront, String line1, String line2, String line3, String line4, String line5, String line6, String line7, String line8, int offset) {
|
||||
@@ -359,39 +664,27 @@ public class Queue {
|
||||
return;
|
||||
}
|
||||
*/
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.SIGN_TEXT, null, color, null, action, offset, null });
|
||||
Consumer.consumerSigns.get(currentConsumer).put(consumerId, new Object[] { colorSecondary, BlockUtils.getSignData(frontGlowing, backGlowing), isWaxed, isFront, line1, line2, line3, line4, line5, line6, line7, line8 });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, location);
|
||||
Object[] signData = new Object[] { colorSecondary, BlockUtils.getSignData(frontGlowing, backGlowing), isWaxed, isFront, line1, line2, line3, line4, line5, line6, line7, line8 };
|
||||
queueStandardData(new Object[] { null, Process.SIGN_TEXT, null, color, null, action, offset, null }, new String[] { user, null }, location, false, Consumer.consumerSigns, signData, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueSignUpdate(String user, BlockState block, int action, int time) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.SIGN_UPDATE, null, action, null, 0, time, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queueStandardData(new Object[] { null, Process.SIGN_UPDATE, null, action, null, 0, time, null }, new String[] { user, null }, block, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueSkullUpdate(String user, BlockState block, int rowId) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.SKULL_UPDATE, null, 0, null, 0, rowId, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queueStandardData(new Object[] { null, Process.SKULL_UPDATE, null, 0, null, 0, rowId, null }, new String[] { user, null }, block, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueStructureGrow(String user, BlockState block, List<BlockState> blockList, int replacedListSize) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.STRUCTURE_GROWTH, null, 0, null, 0, replacedListSize, null });
|
||||
Consumer.consumerBlockList.get(currentConsumer).put(consumerId, blockList);
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { user, null }, block);
|
||||
queueStandardData(new Object[] { null, Process.STRUCTURE_GROWTH, null, 0, null, 0, replacedListSize, null }, new String[] { user, null }, block, false, Consumer.consumerBlockList, blockList, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static void queueWorldInsert(int id, String world) {
|
||||
int currentConsumer = Consumer.currentConsumer;
|
||||
int consumerId = Consumer.newConsumerId(currentConsumer);
|
||||
addConsumer(currentConsumer, new Object[] { consumerId, Process.WORLD_INSERT, null, 0, null, 0, id, null });
|
||||
queueStandardData(consumerId, currentConsumer, new String[] { null, null }, world);
|
||||
queueStandardData(new Object[] { null, Process.WORLD_INSERT, null, 0, null, 0, id, null }, new String[] { null, null }, world, false, Consumer.reserveConsumer());
|
||||
}
|
||||
|
||||
protected static boolean tryQueueIdentifierInsert(int action, int id, String value) {
|
||||
return queueStandardData(new Object[] { null, action, null, 0, null, 0, id, null }, new String[] { null, null }, value, false, Consumer.reserveConsumer());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.Statement;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import net.coreprotect.database.statement.MaterialStatement;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
@@ -11,12 +11,12 @@ import net.coreprotect.utility.Chat;
|
||||
|
||||
class ArtInsertProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, Statement statement, int batchCount, Object name, int materialId) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, Statement statement, int batchCount, Object name, int materialId) {
|
||||
if (name instanceof String) {
|
||||
String query = "SELECT id FROM " + ConfigHandler.prefix + "art_map WHERE id = '" + materialId + "' LIMIT 0, 1";
|
||||
boolean hasMaterial = MaterialStatement.hasMaterial(statement, query);
|
||||
String query = "SELECT id FROM " + ConfigHandler.prefix + "art_map WHERE id = " + materialId + " LIMIT 1 OFFSET 0";
|
||||
boolean hasMaterial = !ConfigHandler.databaseType.isClickHouse() && MaterialStatement.hasMaterial(statement, query);
|
||||
if (!hasMaterial) {
|
||||
MaterialStatement.insert(preparedStmt, batchCount, materialId, (String) name);
|
||||
MaterialStatement.insert(preparedStmt, ConsumerWriteBatch.ReferenceKind.ART, batchCount, materialId, (String) name);
|
||||
|
||||
// validate ID maps to ensure mapping wasn't reloaded from database prior to this insertion completing
|
||||
ConfigHandler.art.put((String) name, materialId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Material;
|
||||
@@ -14,15 +14,15 @@ import net.coreprotect.utility.MaterialUtils;
|
||||
|
||||
class BlockBreakProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, PreparedStatement preparedStmtSkulls, int batchCount, int processId, int id, Material blockType, int blockDataId, Material replaceType, int forceData, String user, Object object, String blockData) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch preparedStmtSkulls, int batchCount, int processId, int id, Material blockType, int blockDataId, Material replaceType, int forceData, String user, Object object, String blockData) {
|
||||
if (object instanceof BlockState) {
|
||||
BlockState block = (BlockState) object;
|
||||
List<Object> meta = BlockUtils.processMeta(block);
|
||||
if (block instanceof Skull) {
|
||||
if (block instanceof Skull && blockType != null && blockType.equals(block.getType())) {
|
||||
SkullBreakLogger.log(preparedStmt, preparedStmtSkulls, batchCount, user, block);
|
||||
}
|
||||
else {
|
||||
BlockBreakLogger.log(preparedStmt, batchCount, user, block.getLocation(), MaterialUtils.getBlockId(blockType), blockDataId, meta, block.getBlockData().getAsString(), blockData);
|
||||
BlockBreakLogger.log(preparedStmt, batchCount, user, block.getLocation(), MaterialUtils.getBlockId(blockType), blockDataId, meta, blockData, blockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.Statement;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import net.coreprotect.database.statement.MaterialStatement;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
@@ -11,12 +11,12 @@ import net.coreprotect.utility.Chat;
|
||||
|
||||
class BlockDataInsertProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, Statement statement, int batchCount, Object name, int materialId) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, Statement statement, int batchCount, Object name, int materialId) {
|
||||
if (name instanceof String) {
|
||||
String query = "SELECT id FROM " + ConfigHandler.prefix + "blockdata_map WHERE id = '" + materialId + "' LIMIT 0, 1";
|
||||
boolean hasMaterial = MaterialStatement.hasMaterial(statement, query);
|
||||
String query = "SELECT id FROM " + ConfigHandler.prefix + "blockdata_map WHERE id = " + materialId + " LIMIT 1 OFFSET 0";
|
||||
boolean hasMaterial = !ConfigHandler.databaseType.isClickHouse() && MaterialStatement.hasMaterial(statement, query);
|
||||
if (!hasMaterial) {
|
||||
MaterialStatement.insert(preparedStmt, batchCount, materialId, (String) name);
|
||||
MaterialStatement.insert(preparedStmt, ConsumerWriteBatch.ReferenceKind.BLOCK_DATA, batchCount, materialId, (String) name);
|
||||
|
||||
// validate ID maps to ensure mapping wasn't reloaded from database prior to this insertion completing
|
||||
ConfigHandler.blockdata.put((String) name, materialId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Material;
|
||||
@@ -13,11 +13,11 @@ import net.coreprotect.utility.MaterialUtils;
|
||||
|
||||
class BlockPlaceProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, PreparedStatement preparedStmtSkulls, int batchCount, Material blockType, int blockData, Material replaceType, int replaceData, int forceData, String user, Object object, String newBlockData, String replacedBlockData) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch preparedStmtSkulls, int batchCount, Material blockType, int blockData, Material replaceType, int replaceData, int forceData, String user, Object object, String newBlockData, String replacedBlockData) {
|
||||
if (object instanceof BlockState) {
|
||||
BlockState block = (BlockState) object;
|
||||
List<Object> meta = BlockUtils.processMeta(block);
|
||||
if (blockType.equals(Material.SKELETON_SKULL) || blockType.equals(Material.SKELETON_WALL_SKULL) || blockType.equals(Material.WITHER_SKELETON_SKULL) || blockType.equals(Material.WITHER_SKELETON_WALL_SKULL) || blockType.equals(Material.ZOMBIE_HEAD) || blockType.equals(Material.ZOMBIE_WALL_HEAD) || blockType.equals(Material.PLAYER_HEAD) || blockType.equals(Material.PLAYER_WALL_HEAD) || blockType.equals(Material.CREEPER_HEAD) || blockType.equals(Material.CREEPER_WALL_HEAD) || blockType.equals(Material.DRAGON_HEAD) || blockType.equals(Material.DRAGON_WALL_HEAD)) {
|
||||
if (blockType != null && (blockType.equals(Material.SKELETON_SKULL) || blockType.equals(Material.SKELETON_WALL_SKULL) || blockType.equals(Material.WITHER_SKELETON_SKULL) || blockType.equals(Material.WITHER_SKELETON_WALL_SKULL) || blockType.equals(Material.ZOMBIE_HEAD) || blockType.equals(Material.ZOMBIE_WALL_HEAD) || blockType.equals(Material.PLAYER_HEAD) || blockType.equals(Material.PLAYER_WALL_HEAD) || blockType.equals(Material.CREEPER_HEAD) || blockType.equals(Material.CREEPER_WALL_HEAD) || blockType.equals(Material.DRAGON_HEAD) || blockType.equals(Material.DRAGON_WALL_HEAD))) {
|
||||
SkullPlaceLogger.log(preparedStmt, preparedStmtSkulls, batchCount, user, block, MaterialUtils.getBlockId(replaceType), replaceData);
|
||||
}
|
||||
else if (forceData == 1) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Location;
|
||||
@@ -9,17 +9,18 @@ import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.database.logger.ContainerBreakLogger;
|
||||
import net.coreprotect.utility.HopperTransactionUtils;
|
||||
|
||||
class ContainerBreakProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, int batchCount, int processId, int id, Material type, String user, Object object) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, int batchCount, int processId, int id, Material type, String user, Object object) {
|
||||
if (object instanceof Location) {
|
||||
Location location = (Location) object;
|
||||
Map<Integer, ItemStack[]> containers = Consumer.consumerContainers.get(processId);
|
||||
if (containers.get(id) != null) {
|
||||
ItemStack[] container = containers.get(id);
|
||||
ContainerBreakLogger.log(preparedStmt, batchCount, user, location, type, container);
|
||||
containers.remove(id);
|
||||
String transactionId = HopperTransactionUtils.getTransactionId(location);
|
||||
HopperTransactionUtils.synchronizeTransaction(transactionId, () -> ContainerBreakLogger.log(preparedStmt, batchCount, user, location, type, container));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -17,75 +12,166 @@ import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.consumer.Queue;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import net.coreprotect.database.logger.ContainerLogger;
|
||||
import net.coreprotect.model.entity.EntityContainerTransaction;
|
||||
import net.coreprotect.model.entity.EntitySpawnIdentity;
|
||||
import net.coreprotect.utility.ErrorReporter;
|
||||
import net.coreprotect.utility.HopperTransactionUtils;
|
||||
|
||||
class ContainerTransactionProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmtContainer, PreparedStatement preparedStmtItems, int batchCount, int processId, int id, Material type, int forceData, String user, Object object) {
|
||||
if (object instanceof Location) {
|
||||
Location location = (Location) object;
|
||||
Map<Integer, Object> inventories = Consumer.consumerInventories.get(processId);
|
||||
if (inventories.get(id) != null) {
|
||||
Object inventory = inventories.get(id);
|
||||
String transactingChestId = location.getWorld().getUID().toString() + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ();
|
||||
String loggingChestId = user.toLowerCase(Locale.ROOT) + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ();
|
||||
if (ConfigHandler.loggingChest.get(loggingChestId) != null) {
|
||||
int current_chest = ConfigHandler.loggingChest.get(loggingChestId);
|
||||
if (ConfigHandler.oldContainer.get(loggingChestId) == null) {
|
||||
return;
|
||||
}
|
||||
int force_size = 0;
|
||||
if (ConfigHandler.forceContainer.get(loggingChestId) != null) {
|
||||
force_size = ConfigHandler.forceContainer.get(loggingChestId).size();
|
||||
}
|
||||
if (current_chest == forceData || force_size > 0) { // This prevents client side chest sorting mods from messing things up.
|
||||
ContainerLogger.log(preparedStmtContainer, preparedStmtItems, batchCount, user, type, inventory, location);
|
||||
List<ItemStack[]> old = ConfigHandler.oldContainer.get(loggingChestId);
|
||||
if (old.size() == 0) {
|
||||
ConfigHandler.oldContainer.remove(loggingChestId);
|
||||
ConfigHandler.loggingChest.remove(loggingChestId);
|
||||
ConfigHandler.transactingChest.remove(transactingChestId);
|
||||
}
|
||||
}
|
||||
else if (loggingChestId.startsWith("#hopper")) {
|
||||
List<Object> transactingChest = ConfigHandler.transactingChest.get(transactingChestId);
|
||||
if (force_size == 0 && ConfigHandler.oldContainer.getOrDefault(loggingChestId, Collections.synchronizedList(new ArrayList<>())).size() == 1 && transactingChest != null && transactingChest.isEmpty()) {
|
||||
int loopCount = ConfigHandler.loggingChest.getOrDefault(loggingChestId, 0);
|
||||
int maxInventorySize = (99 * 54);
|
||||
try {
|
||||
Inventory checkInventory = (Inventory) inventory;
|
||||
maxInventorySize = checkInventory.getSize() * checkInventory.getMaxStackSize();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// use default of 5,346
|
||||
}
|
||||
static boolean processEntity(ConsumerWriteBatch preparedStmtContainer, int batchCount, String user, Object object, EntitySpawnIdentity identity) throws Exception {
|
||||
if (!(object instanceof EntityContainerTransaction) || identity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (loopCount > maxInventorySize) {
|
||||
ItemStack[] destinationContents = null;
|
||||
ItemStack movedItem = null;
|
||||
if (ConfigHandler.databaseType.isColumnar()) {
|
||||
preparedStmtContainer.executeAtomically("entity_container_transaction", () -> ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, identity, (EntityContainerTransaction) object));
|
||||
}
|
||||
else {
|
||||
try {
|
||||
ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, identity, (EntityContainerTransaction) object);
|
||||
}
|
||||
catch (Exception e) {
|
||||
ErrorReporter.report(e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String hopperPush = "#hopper-push." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ();
|
||||
Object[] hopperPushData = ConfigHandler.hopperSuccess.remove(hopperPush);
|
||||
if (hopperPushData != null) {
|
||||
destinationContents = (ItemStack[]) hopperPushData[0];
|
||||
movedItem = (ItemStack) hopperPushData[1];
|
||||
}
|
||||
static void process(ConsumerWriteBatch preparedStmtContainer, ConsumerWriteBatch preparedStmtItems, int batchCount, int processId, int id, Material type, int forceData, String user, Object object) {
|
||||
if (object instanceof ContainerLogger.PreparedTransaction) {
|
||||
((ContainerLogger.PreparedTransaction) object).log(preparedStmtContainer, preparedStmtItems, batchCount, user);
|
||||
return;
|
||||
}
|
||||
if (!(object instanceof Location)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (destinationContents != null) {
|
||||
Set<ItemStack> movedItems = new HashSet<>();
|
||||
Object[] lastAbort = ConfigHandler.hopperAbort.get(hopperPush);
|
||||
if (lastAbort != null && Arrays.equals(destinationContents, (ItemStack[]) lastAbort[1])) {
|
||||
((Set<?>) lastAbort[0]).forEach(itemStack -> movedItems.add((ItemStack) itemStack));
|
||||
}
|
||||
movedItems.add(movedItem);
|
||||
ConfigHandler.hopperAbort.put(hopperPush, new Object[] { movedItems, destinationContents });
|
||||
}
|
||||
}
|
||||
}
|
||||
Location location = (Location) object;
|
||||
Map<Integer, Object> inventories = Consumer.consumerInventories.get(processId);
|
||||
Object inventory = inventories.get(id);
|
||||
if (inventory == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String transactingChestId = HopperTransactionUtils.getTransactionId(location);
|
||||
String loggingChestIdSuffix = HopperTransactionUtils.getLoggingIdSuffix(location);
|
||||
String loggingChestId = HopperTransactionUtils.getLoggingId(user, loggingChestIdSuffix);
|
||||
HopperTransactionUtils.synchronizeTransaction(transactingChestId,
|
||||
() -> processTransaction(preparedStmtContainer, preparedStmtItems, batchCount, processId, id, type, forceData, user, inventory, location, transactingChestId, loggingChestIdSuffix, loggingChestId));
|
||||
}
|
||||
|
||||
private static void processTransaction(ConsumerWriteBatch preparedStmtContainer, ConsumerWriteBatch preparedStmtItems, int batchCount, int processId, int id, Material type, int forceData, String user, Object inventory,
|
||||
Location location, String transactingChestId, String loggingChestIdSuffix, String loggingChestId) {
|
||||
if (ConfigHandler.loggingChest.get(loggingChestId) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int current_chest = ConfigHandler.loggingChest.get(loggingChestId);
|
||||
if (ConfigHandler.oldContainer.get(loggingChestId) == null) {
|
||||
clearContainerTransaction(transactingChestId, loggingChestIdSuffix, loggingChestId);
|
||||
return;
|
||||
}
|
||||
int force_size = Queue.getForceContainerSize(loggingChestId);
|
||||
if (current_chest == forceData || force_size > 0) { // This prevents client side chest sorting mods from messing things up.
|
||||
try {
|
||||
if (ConfigHandler.databaseType.isColumnar()) {
|
||||
ContainerLogger.log(preparedStmtContainer, preparedStmtItems, batchCount, user, type, inventory, location,
|
||||
prepared -> Consumer.consumerObjects.get(processId).put(id, prepared));
|
||||
}
|
||||
else {
|
||||
ContainerLogger.log(preparedStmtContainer, preparedStmtItems, batchCount, user, type, inventory, location);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
List<ItemStack[]> old = ConfigHandler.oldContainer.get(loggingChestId);
|
||||
if (old == null || old.isEmpty()) {
|
||||
clearContainerTransaction(transactingChestId, loggingChestIdSuffix, loggingChestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (loggingChestId.startsWith("#hopper")) {
|
||||
if (force_size == 0 && ConfigHandler.oldContainer.getOrDefault(loggingChestId, Collections.synchronizedList(new ArrayList<>())).size() == 1
|
||||
&& HopperTransactionUtils.pendingDeltaCount(transactingChestId) == 0) {
|
||||
int loopCount = ConfigHandler.loggingChest.getOrDefault(loggingChestId, 0);
|
||||
int maxInventorySize = (99 * 54);
|
||||
try {
|
||||
Inventory checkInventory = (Inventory) inventory;
|
||||
maxInventorySize = checkInventory.getSize() * checkInventory.getMaxStackSize();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// use default of 5,346
|
||||
}
|
||||
|
||||
if (loopCount > maxInventorySize) {
|
||||
ItemStack[] destinationContents = null;
|
||||
ItemStack movedItem = null;
|
||||
|
||||
String hopperPush = HopperTransactionUtils.getHopperPushId(location);
|
||||
Object[] hopperPushData = ConfigHandler.hopperSuccess.remove(hopperPush);
|
||||
if (hopperPushData != null) {
|
||||
destinationContents = (ItemStack[]) hopperPushData[0];
|
||||
movedItem = (ItemStack) hopperPushData[1];
|
||||
}
|
||||
|
||||
if (destinationContents != null) {
|
||||
Object[] lastAbort = ConfigHandler.hopperAbort.get(hopperPush);
|
||||
ConfigHandler.hopperAbort.put(hopperPush, HopperTransactionUtils.createAbortState(lastAbort, destinationContents, movedItem));
|
||||
}
|
||||
}
|
||||
inventories.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearContainerTransaction(String transactionId, String locationSuffix, String loggingId) {
|
||||
ConfigHandler.oldContainer.remove(loggingId);
|
||||
ConfigHandler.removeOldContainerViewer(locationSuffix, loggingId);
|
||||
ConfigHandler.loggingChest.remove(loggingId);
|
||||
Queue.removeForceContainer(loggingId);
|
||||
HopperTransactionUtils.removeOwner(transactionId, loggingId);
|
||||
}
|
||||
|
||||
static void discard(int processId, int failedIndex, int forceData, String user, Location location) {
|
||||
if (location.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
String transactionId = HopperTransactionUtils.getTransactionId(location);
|
||||
String locationSuffix = HopperTransactionUtils.getLoggingIdSuffix(location);
|
||||
String loggingId = HopperTransactionUtils.getLoggingId(user, locationSuffix);
|
||||
HopperTransactionUtils.synchronizeTransaction(transactionId, () -> {
|
||||
List<ItemStack[]> old = ConfigHandler.oldContainer.get(loggingId);
|
||||
if (old == null || old.isEmpty()) {
|
||||
clearContainerTransaction(transactionId, locationSuffix, loggingId);
|
||||
return;
|
||||
}
|
||||
Integer generation = ConfigHandler.loggingChest.get(loggingId);
|
||||
int forceSize = Queue.getForceContainerSize(loggingId);
|
||||
int snapshotIndex = 0;
|
||||
List<Object[]> queued = Consumer.consumer.get(processId);
|
||||
for (int index = 0; index < failedIndex; index++) {
|
||||
if (Process.inventoryTransactionGeneration(processId, queued.get(index), Process.CONTAINER_TRANSACTION, loggingId) != null) {
|
||||
snapshotIndex++;
|
||||
}
|
||||
}
|
||||
if (snapshotIndex >= forceSize) {
|
||||
if (generation == null || generation != forceData) {
|
||||
return;
|
||||
}
|
||||
snapshotIndex = forceSize;
|
||||
}
|
||||
if (snapshotIndex < old.size()) {
|
||||
old.remove(snapshotIndex);
|
||||
if (snapshotIndex < forceSize) {
|
||||
Queue.pollForceContainer(loggingId, snapshotIndex);
|
||||
}
|
||||
HopperTransactionUtils.consumeSnapshot(transactionId, loggingId, snapshotIndex);
|
||||
}
|
||||
if (old.isEmpty()) {
|
||||
clearContainerTransaction(transactionId, locationSuffix, loggingId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.Statement;
|
||||
|
||||
import net.coreprotect.config.ConfigHandler;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import net.coreprotect.database.statement.MaterialStatement;
|
||||
import net.coreprotect.language.Phrase;
|
||||
import net.coreprotect.language.Selector;
|
||||
@@ -11,12 +11,12 @@ import net.coreprotect.utility.Chat;
|
||||
|
||||
class EntityInsertProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, Statement statement, int batchCount, Object name, int materialId) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, Statement statement, int batchCount, Object name, int materialId) {
|
||||
if (name instanceof String) {
|
||||
String query = "SELECT id FROM " + ConfigHandler.prefix + "entity_map WHERE id = '" + materialId + "' LIMIT 0, 1";
|
||||
boolean hasMaterial = MaterialStatement.hasMaterial(statement, query);
|
||||
String query = "SELECT id FROM " + ConfigHandler.prefix + "entity_map WHERE id = " + materialId + " LIMIT 1 OFFSET 0";
|
||||
boolean hasMaterial = !ConfigHandler.databaseType.isClickHouse() && MaterialStatement.hasMaterial(statement, query);
|
||||
if (!hasMaterial) {
|
||||
MaterialStatement.insert(preparedStmt, batchCount, materialId, (String) name);
|
||||
MaterialStatement.insert(preparedStmt, ConsumerWriteBatch.ReferenceKind.ENTITY, batchCount, materialId, (String) name);
|
||||
|
||||
// validate ID maps to ensure mapping wasn't reloaded from database prior to this insertion completing
|
||||
ConfigHandler.entities.put((String) name, materialId);
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import net.coreprotect.consumer.Consumer;
|
||||
import net.coreprotect.database.logger.EntityKillLogger;
|
||||
import net.coreprotect.utility.EntityUtils;
|
||||
import net.coreprotect.utility.Util;
|
||||
|
||||
class EntityKillProcess {
|
||||
|
||||
static void process(PreparedStatement preparedStmt, PreparedStatement preparedStmtEntities, int batchCount, int processId, int id, Object object, String user) {
|
||||
static void process(ConsumerWriteBatch preparedStmt, ConsumerWriteBatch preparedStmtEntities, ConsumerWriteBatch preparedStmtEntityKillLinks, int batchCount, int processId, int id, Object object, String user) {
|
||||
if (object instanceof Object[]) {
|
||||
BlockState block = (BlockState) ((Object[]) object)[0];
|
||||
EntityType type = (EntityType) ((Object[]) object)[1];
|
||||
Object[] values = (Object[]) object;
|
||||
if (values.length <= 1 || !isLocationData(values[0]) || !(values[1] instanceof EntityType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = getLocation(values[0]);
|
||||
EntityType type = (EntityType) values[1];
|
||||
Map<Integer, List<Object>> objectLists = Consumer.consumerObjectList.get(processId);
|
||||
if (objectLists.get(id) != null) {
|
||||
if (objectLists != null && objectLists.get(id) != null) {
|
||||
List<Object> objectList = objectLists.get(id);
|
||||
int entityId = EntityUtils.getEntityId(type);
|
||||
EntityKillLogger.log(preparedStmt, preparedStmtEntities, batchCount, user, block, objectList, entityId);
|
||||
objectLists.remove(id);
|
||||
EntityKillLogger.log(preparedStmt, preparedStmtEntities, preparedStmtEntityKillLinks, batchCount, user, location, objectList, entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLocationData(Object value) {
|
||||
return value instanceof Location || value instanceof BlockState;
|
||||
}
|
||||
|
||||
private static Location getLocation(Object value) {
|
||||
if (value instanceof Location) {
|
||||
return ((Location) value).clone();
|
||||
}
|
||||
|
||||
BlockState block = (BlockState) value;
|
||||
return new Location(block.getWorld(), block.getX(), block.getY(), block.getZ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.coreprotect.consumer.process;
|
||||
|
||||
import net.coreprotect.database.ConsumerWriteBatch;
|
||||
|
||||
import net.coreprotect.database.logger.EntitySpawnLogger;
|
||||
import net.coreprotect.model.entity.EntitySpawnData;
|
||||
import net.coreprotect.model.entity.EntitySpawnIdentity;
|
||||
|
||||
final class EntitySpawnLogProcess {
|
||||
|
||||
private EntitySpawnLogProcess() {
|
||||
}
|
||||
|
||||
static EntitySpawnIdentity process(ConsumerWriteBatch batch, Object object, String user, EntitySpawnIdentity identity) throws Exception {
|
||||
if (object instanceof EntitySpawnData) {
|
||||
EntitySpawnData data = (EntitySpawnData) object;
|
||||
return EntitySpawnLogger.logIdentity(batch, user, data, identity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 383 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user