Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16cfe03908
|
||
|
|
87c9f7c4ec
|
||
|
|
3df8a6714a | ||
|
|
e715691b2b | ||
|
|
fab4fdcb0e | ||
|
|
4747f0cbaf | ||
|
|
9ce4964da2 | ||
|
|
7a37baeff3 | ||
|
|
31cb6cc430 | ||
|
|
c7fcba7e2b | ||
|
|
e7a04a1148 | ||
|
|
d97900d5ed | ||
|
|
f1792477a5 | ||
|
|
7daa7a484f | ||
|
|
e278b8eb88 | ||
|
|
20cde1715c | ||
|
|
cc50d7c2c9 | ||
|
|
36d452cdfe | ||
|
|
8cc3219a33 | ||
|
|
98b2b1982e | ||
|
|
fc1e14583c | ||
|
|
25eedbe0be | ||
|
|
33e7fdc7c2 | ||
|
|
1157ca1813 | ||
|
|
5a3894d6fe | ||
|
|
61e1f5aab3 | ||
|
|
05948afdba | ||
|
|
116e94c2f7 | ||
|
|
34366439bb | ||
|
|
00245cd8d1 | ||
|
|
aeedecd808 | ||
|
|
48096bbd78 | ||
|
|
105b4f41a1 | ||
|
|
861efce918 | ||
|
|
fa617744b8 | ||
|
|
2bd23ae498 | ||
|
|
5879d44242 | ||
|
|
cd430775f3 | ||
|
|
d035989df5 | ||
|
|
6c1ddfcf05 | ||
|
|
33543b9edb | ||
|
|
5379ef308e | ||
|
|
5ade6c9b24 | ||
|
|
c339a51723 | ||
|
|
3130ddf5cc | ||
|
|
2a7fe930aa | ||
|
|
d706da2f4b | ||
|
|
184b9749ab | ||
|
|
59f0ee33f5 | ||
|
|
a5a309e2d4 | ||
|
|
93c1911d60 | ||
|
|
e25eff4fac | ||
|
|
837a9a5b73 | ||
|
|
07ecafa7da | ||
|
|
fd7b071b5d | ||
|
|
1322dcdb91 | ||
|
|
c197158850 | ||
|
|
98f429d04c | ||
|
|
c6e04888c6 | ||
|
|
0cfb292ac3 | ||
|
|
7677eef07c | ||
|
|
49feef2999 | ||
|
|
0162cebc41 | ||
|
|
0295f9ef5a | ||
|
|
d64cbfccfc | ||
|
|
4a9ca90188 | ||
|
|
d18b0743db | ||
|
|
6d231e52c6 | ||
|
|
3ea1420304 | ||
|
|
2a1adee462 | ||
|
|
e8204c1fa9 | ||
|
|
c37a1fda8f | ||
|
|
a9b41c814c | ||
|
|
430a658e0e | ||
|
|
ff8f5febac | ||
|
|
fe7a551be8 | ||
|
|
2c2d719143 | ||
|
|
3cfdc8f507 | ||
|
|
8a89a33b42 | ||
|
|
6af8fd7b76 | ||
|
|
f412275b37 | ||
|
|
9b03eb6ea4 | ||
|
|
c40f614a61 | ||
|
|
683bbdcde0 | ||
|
|
0c2d8f7787 | ||
|
|
ea0563ff50 | ||
|
|
1b2f47f8db | ||
|
|
cdfc2b480b | ||
|
|
fcfe414ba2 | ||
|
|
e203b3b4d3 | ||
|
|
7e05d59387 | ||
|
|
4106f0bcd5 | ||
|
|
23d2113ca7 | ||
|
|
e1fe6dc81e | ||
|
|
708d94990e | ||
|
|
6120ee8fa8 | ||
|
|
fa3e26823f | ||
|
|
3cb325a200 | ||
|
|
b33c0f1eee | ||
|
|
11daa22cd5 | ||
|
|
42462cc9d2 | ||
|
|
1190040ccc | ||
|
|
8d5353c471 | ||
|
|
91b8e00dbf | ||
|
|
34f9a2c84b | ||
|
|
fa2a5d2f2d | ||
|
|
1c69e26e01 | ||
|
|
4990b53474 | ||
|
|
5212f0522e | ||
|
|
75267c1305 | ||
|
|
b8371a21bc | ||
|
|
085ce5222f | ||
|
|
306d57b0c7 | ||
|
|
9b8efe2295 | ||
|
|
e3cc812425 | ||
|
|
286ad3547e | ||
|
|
39e435f1e4 | ||
|
|
08c22adc1b | ||
|
|
f23b67a64c | ||
|
|
af8597354a | ||
|
|
82c81b77c1 | ||
|
|
b7403f9481 | ||
|
|
dbd0b8a660 | ||
|
|
520735e389 | ||
|
|
fafe4e1bb4 | ||
|
|
7118fcae81 | ||
|
|
2249c7568f | ||
|
|
ec29a575e6 | ||
|
|
a344a1159d | ||
|
|
5e1ccd588e | ||
|
|
7f0ad6a3a6 | ||
|
|
296d3d87fc | ||
|
|
a667937148 | ||
|
|
c25533e16c | ||
|
|
cffe0a02f9 | ||
|
|
c0cf0ea2b7 | ||
|
|
6e23bbebef | ||
|
|
4d3266d92a | ||
|
|
d7f9cf334a | ||
|
|
ee17be5afd | ||
|
|
c8054f72cb | ||
|
|
e27ac14a31 | ||
|
|
d6bb052d12 | ||
|
|
3e482b37b3 | ||
|
|
8f563d3820 | ||
|
|
e66fcde6b8 | ||
|
|
0e2245af2e | ||
|
|
2a25cbff2d | ||
|
|
3eac4d6828 | ||
|
|
2c50d8e382 | ||
|
|
c3308febb5 | ||
|
|
5f6cd8735c | ||
|
|
f077708ad1 | ||
|
|
0fad2018b5 | ||
|
|
b59e718a56 | ||
|
|
425c9bca20 | ||
|
|
b4afa61420 | ||
|
|
e11d136d14 | ||
|
|
54eaaa1e55 | ||
|
|
ecb2bac48b | ||
|
|
e74750c191 | ||
|
|
0cf28de45d | ||
|
|
c6a92dc7ce | ||
|
|
9e97576618 | ||
|
|
4b5b64a9a3 | ||
|
|
7da31597f0 | ||
|
|
9272225f46 | ||
|
|
2e89121489 | ||
|
|
98f32c78d5 | ||
|
|
a1c1f5f555 | ||
|
|
9fe0c2f6dc | ||
|
|
6c7fcf6117 | ||
|
|
8eb8fa865f | ||
|
|
1a38c9b078 | ||
|
|
3eb9401e8a | ||
|
|
42cfa1fd87 | ||
|
|
6e365e8bea | ||
|
|
013135bf9a | ||
|
|
21b5971b24 | ||
|
|
9fc17997b2 | ||
|
|
fa46d3a552 | ||
|
|
ad1520bb85 | ||
|
|
dd5a7e56e8 | ||
|
|
9ba60be63b | ||
|
|
ecb034bb64 | ||
|
|
a74a8d7252 | ||
|
|
8e1da37d1b | ||
|
|
6f1538bc0a | ||
|
|
bb0703d123 | ||
|
|
751b2ce5a2 | ||
|
|
94bb83692a | ||
|
|
7c959ab10f | ||
|
|
2c56682115 | ||
|
|
7f2fd61790 | ||
|
|
a90c557a1a | ||
|
|
13a7ff04a0 | ||
|
|
ae8383a9ef | ||
|
|
a186ccf707 | ||
|
|
ad02c8c006 | ||
|
|
baf6914f6d | ||
|
|
f3505bc28c | ||
|
|
c49fa30d1e | ||
|
|
5b0edb02e2 | ||
|
|
f2f525c37d | ||
|
|
92a698205f | ||
|
|
cdb7a52aac | ||
|
|
68fabd8b10 | ||
|
|
26c8250d53 | ||
|
|
879829c2c0 | ||
|
|
ce69b915a1 | ||
|
|
b5b8e40138 | ||
|
|
370ed67d8f | ||
|
|
adfc4e8f69 | ||
|
|
72ce0a236e | ||
|
|
ac8a977683 | ||
|
|
1ce29654a9 | ||
|
|
3e0fd9be94 | ||
|
|
e69dc2a16d | ||
|
|
a76f14e87b | ||
|
|
8343d79f3c | ||
|
|
7051155fa1 | ||
|
|
2a11c93fc9 | ||
|
|
628ab4123c | ||
|
|
10f0c15e7a | ||
|
|
4127f8b643 |
No files matched your search
@@ -0,0 +1 @@
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Gradle files
|
||||
.gradle
|
||||
build
|
||||
|
||||
# Eclipse files
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# Package Files #
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
target/
|
||||
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
.flattened-pom.xml
|
||||
|
||||
# Common working directory
|
||||
run/
|
||||
@@ -1,12 +1,68 @@
|
||||
[](https://ci.loohpjames.com/job/ImageFrame/)
|
||||
[](https://ci.loohpjames.com/job/ImageFrame/) [](https://crowdin.com/project/imageframe)
|
||||
# ImageFrame
|
||||
|
||||
https://www.spigotmc.org/resources/-1/
|
||||
https://www.spigotmc.org/resources/106031/<br>
|
||||
https://modrinth.com/plugin/imageframe<br>
|
||||
https://hangar.papermc.io/LOOHP/ImageFrame
|
||||
|
||||
Put images on maps and walls!
|
||||
|
||||
More information (screenshots, commands, permissions) about the plugin can be found on the Spigot page linked above.
|
||||
|
||||
## Creating Images In-Game
|
||||
|
||||
The main command is `/imageframe` (aliases: `/iframe`, `/if`, `/frame`). All creation commands are op-only by default.
|
||||
Sizes are measured in **maps** — one map fills one item frame block, so a `4 x 3` image needs a 4-wide by 3-tall grid of item frames.
|
||||
|
||||
Image URLs are **not accepted** — `create`, `overlay` and `refresh` reject any link. Images come from:
|
||||
- **The images folder** — drop image files (png, jpg, gif, webp, …) into `plugins/ImageFrame/images/` and pass the file name, e.g. `/imageframe create poster poster.png selection`. No restart needed; new files show up in tab completion straight away. File names can't contain spaces.
|
||||
- **The bundled maps** (below).
|
||||
- **The upload service**, when `UploadService.Enabled` is `true` in `config.yml` — pass `upload` instead of a file name, then open the link the plugin sends you to upload a file.
|
||||
|
||||
### Quick way — create straight onto a wall
|
||||
1. Place a grid of item frames on the wall (e.g. 4 wide × 3 tall).
|
||||
2. Run `/imageframe select`, then left-click one corner frame and right-click the opposite corner frame to select the whole grid.
|
||||
3. Run `/imageframe create <name> <file> selection`, where `<file>` is an image in `plugins/ImageFrame/images/`.
|
||||
|
||||
The image is sized to your selection and placed into the frames automatically.
|
||||
|
||||
### Manual way — create, then place yourself
|
||||
1. `/imageframe create <name> <file> <width> <height>` — creates the image map (`width`/`height` in maps).
|
||||
2. Put it on frames one of two ways:
|
||||
- `/imageframe get <name>` — gives you the map item(s); place each on the item frames in order (left→right, top→bottom).
|
||||
- or select a frame grid (as above) and run `/imageframe get <name> selection` to auto-fill it.
|
||||
|
||||
### Using bundled maps
|
||||
Bundled maps are canonical maps shipped inside the plugin jar, under `maps/`. They are re-rendered from the jar on every startup and never touch the plugin data folder, so they survive data wipes — their map ids are kept so existing item frames in the world keep pointing at the right maps, the same way LaunchPads-OG preserves pad locations in its config.
|
||||
|
||||
Bundled maps are owned by **Console**. Anyone with access can see them with `/imageframe list` and use them by name:
|
||||
- `/imageframe get <name> selection`, or reference them explicitly as `Console:<name>`.
|
||||
|
||||
#### Adding a bundled map
|
||||
It works like the images folder: drop the file in and it becomes a map named after the file.
|
||||
1. Optional, to try it out live first: put the image in `plugins/ImageFrame/images/` and run `imageframe create Console:<name> <file> <width> <height>` from the console, using the file name without its extension as `<name>`. Refresh or recreate it until it looks right, and place it on frames if you like.
|
||||
2. Copy the image into `common/src/main/resources/maps/`, rebuild the jar, deploy it and restart. No `maps.yml` entry is needed.
|
||||
|
||||
On first load the server sets the map up:
|
||||
- If you did step 1, the bundled map takes over that map's size, dithering and map ids, and the copy in the data folder is removed. Frames you already placed keep working.
|
||||
- Otherwise the size comes from the image at 128 pixels per map (a 512×384 image becomes 4×3), and the server assigns new map ids. Images that would go over `MaxSize` are skipped with a console message.
|
||||
|
||||
The server saves what it decided to `imageframe-bundled-maps.yml` in the main world folder. It stays the same on every restart and survives plugin data folder wipes, so there is nothing to copy back. Keep that file with the world.
|
||||
|
||||
Replacing a bundled image with a new file of the same name keeps its size and map ids, and frames update after the restart. Animated GIFs are supported. File names can't contain spaces or colons.
|
||||
|
||||
To override the name, size or dithering, or to pin exact map ids, add an entry to `common/src/main/resources/maps.yml` (see the comments at the top of that file). Never change the `MapIds` of an existing map — item frames in the world reference those ids.
|
||||
|
||||
### Useful extras
|
||||
- `/imageframe info <name>` — shows a map's size (handy to build a matching frame grid).
|
||||
- `/imageframe refresh <name> [file]` — re-renders an existing map. Maps made from the images folder re-read their file, so edit the image in place and refresh to update it live. Pass a different file name (or `upload`) to swap the image. Bundled maps re-render from the jar. Maps whose stored source is a web URL can no longer be refreshed.
|
||||
- Optional flags on `create`/`get`: a dithering type, and `combined`/`separated`.
|
||||
|
||||
### Behaviour of this fork
|
||||
- Image URLs are not accepted anywhere; the old `RestrictImageUrl` whitelist is gone.
|
||||
- Images are **stretched to fill** the whole frame grid (edge to edge) instead of being letterboxed with a transparent border. Match your frame grid's width:height to the image's aspect ratio to avoid distortion.
|
||||
- Item frames filled by the plugin are automatically made **invisible**, so neighbouring tiles join seamlessly with no thin border between them.
|
||||
|
||||
## Built against Spigot
|
||||
Built against [Spigot's API](https://www.spigotmc.org/wiki/buildtools/) (required mc versions are listed on the spigot page above).
|
||||
Plugins built against Spigot usually also work with [Paper](https://papermc.io/).
|
||||
@@ -32,6 +88,10 @@ Plugins built against Spigot usually also work with [Paper](https://papermc.io/)
|
||||
```
|
||||
Replace `VERSION` with the version number.
|
||||
|
||||
## Official Addons
|
||||
|
||||
- [ImageFrameClient](https://github.com/LOOHP/ImageFrameClient) (Complementary client mod for servers with ImageFrame to display HD and full color images)
|
||||
|
||||
## Partnerships
|
||||
|
||||
### Server Hosting
|
||||
@@ -42,4 +102,4 @@ It also **supports my development**, take it as an alternative way to donate whi
|
||||
|
||||
**https://www.bisecthosting.com/loohp**
|
||||
|
||||

|
||||
[](https://www.bisecthosting.com/loohp)
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.minecraft.server.v1_16_R1.ChunkProviderServer;
|
||||
import net.minecraft.server.v1_16_R1.DataWatcher;
|
||||
import net.minecraft.server.v1_16_R1.DataWatcherObject;
|
||||
import net.minecraft.server.v1_16_R1.EntityHuman;
|
||||
import net.minecraft.server.v1_16_R1.EntityItemFrame;
|
||||
import net.minecraft.server.v1_16_R1.EntityPlayer;
|
||||
import net.minecraft.server.v1_16_R1.IChatBaseComponent;
|
||||
import net.minecraft.server.v1_16_R1.ItemWorldMap;
|
||||
import net.minecraft.server.v1_16_R1.MapIcon;
|
||||
import net.minecraft.server.v1_16_R1.MinecraftKey;
|
||||
import net.minecraft.server.v1_16_R1.NBTTagCompound;
|
||||
import net.minecraft.server.v1_16_R1.NBTTagList;
|
||||
import net.minecraft.server.v1_16_R1.NBTTagString;
|
||||
import net.minecraft.server.v1_16_R1.Packet;
|
||||
import net.minecraft.server.v1_16_R1.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.server.v1_16_R1.PacketPlayOutMap;
|
||||
import net.minecraft.server.v1_16_R1.PersistentIdCounts;
|
||||
import net.minecraft.server.v1_16_R1.PlayerChunkMap;
|
||||
import net.minecraft.server.v1_16_R1.PlayerInventory;
|
||||
import net.minecraft.server.v1_16_R1.ResourceKey;
|
||||
import net.minecraft.server.v1_16_R1.WorldMap;
|
||||
import net.minecraft.server.v1_16_R1.WorldServer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_16_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_16 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field nmsMapIconTypeDisplayField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_16() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.server.v1_16_R1.Entity.class.getDeclaredField("T");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsMapIconTypeDisplayField = MapIcon.Type.class.getDeclaredField("C");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ITEM");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("a");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.colors = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
try {
|
||||
nmsMapIconTypeDisplayField.setAccessible(true);
|
||||
return nmsMapIconTypeDisplayField.getBoolean(mapIconType);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "resource"})
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().D().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked", "resource"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.server.v1_16_R1.World> worldTypeKey = worldServer.getDimensionKey();
|
||||
String mapId = ItemWorldMap.a(id);
|
||||
WorldMap worldMap = new WorldMap(mapId);
|
||||
worldMap.a(spawnLocation.getBlockX(), spawnLocation.getBlockZ(), 3, false, false, worldTypeKey);
|
||||
worldServer.a(worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().D().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.playerChunkMap;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.trackedEntities;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (EntityPlayer player : entityTracker.trackedPlayers) {
|
||||
players.add(player.getBukkitEntity());
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? Collections.emptyList() : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
byte[] colors0;
|
||||
int size;
|
||||
if (colors == null) {
|
||||
colors0 = EMPTY_BYTE_ARRAY;
|
||||
size = 0;
|
||||
} else {
|
||||
colors0 = colors;
|
||||
size = 128;
|
||||
}
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, false, mapIcons, colors0, 0, 0, size, size);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.server.v1_16_R1.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.server.v1_16_R1.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata();
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.server.v1_16_R1.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.getDataWatcher();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.get(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().playerConnection.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
|
||||
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == 9) {
|
||||
NBTTagList loreLineTagList = displayTag.getList("Lore", 8);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.getString(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
|
||||
translatableComponent.setColor(ChatColor.GRAY);
|
||||
translatableComponent.setItalic(false);
|
||||
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.set("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.inventory;
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.pickup(nmsItemStack);
|
||||
if (!added || !nmsItemStack.isEmpty()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.activeContainer.c();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
|
||||
MinecraftKey key = nmsWorld.getTypeKey().a();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.minecraft.server.v1_16_R2.ChunkProviderServer;
|
||||
import net.minecraft.server.v1_16_R2.DataWatcher;
|
||||
import net.minecraft.server.v1_16_R2.DataWatcherObject;
|
||||
import net.minecraft.server.v1_16_R2.EntityHuman;
|
||||
import net.minecraft.server.v1_16_R2.EntityItemFrame;
|
||||
import net.minecraft.server.v1_16_R2.EntityPlayer;
|
||||
import net.minecraft.server.v1_16_R2.IChatBaseComponent;
|
||||
import net.minecraft.server.v1_16_R2.ItemWorldMap;
|
||||
import net.minecraft.server.v1_16_R2.MapIcon;
|
||||
import net.minecraft.server.v1_16_R2.MinecraftKey;
|
||||
import net.minecraft.server.v1_16_R2.NBTTagCompound;
|
||||
import net.minecraft.server.v1_16_R2.NBTTagList;
|
||||
import net.minecraft.server.v1_16_R2.NBTTagString;
|
||||
import net.minecraft.server.v1_16_R2.Packet;
|
||||
import net.minecraft.server.v1_16_R2.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.server.v1_16_R2.PacketPlayOutMap;
|
||||
import net.minecraft.server.v1_16_R2.PersistentIdCounts;
|
||||
import net.minecraft.server.v1_16_R2.PlayerChunkMap;
|
||||
import net.minecraft.server.v1_16_R2.PlayerInventory;
|
||||
import net.minecraft.server.v1_16_R2.ResourceKey;
|
||||
import net.minecraft.server.v1_16_R2.WorldMap;
|
||||
import net.minecraft.server.v1_16_R2.WorldServer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_16_R2.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_16_2 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field nmsMapIconTypeDisplayField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_16_2() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.server.v1_16_R2.Entity.class.getDeclaredField("S");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsMapIconTypeDisplayField = MapIcon.Type.class.getDeclaredField("C");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ITEM");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("a");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.colors = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
try {
|
||||
nmsMapIconTypeDisplayField.setAccessible(true);
|
||||
return nmsMapIconTypeDisplayField.getBoolean(mapIconType);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "resource"})
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked", "resource"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.server.v1_16_R2.World> worldTypeKey = worldServer.getDimensionKey();
|
||||
String mapId = ItemWorldMap.a(id);
|
||||
WorldMap worldMap = new WorldMap(mapId);
|
||||
worldMap.a(spawnLocation.getBlockX(), spawnLocation.getBlockZ(), 3, false, false, worldTypeKey);
|
||||
worldServer.a(worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.playerChunkMap;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.trackedEntities;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (EntityPlayer player : entityTracker.trackedPlayers) {
|
||||
players.add(player.getBukkitEntity());
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? Collections.emptyList() : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
byte[] colors0;
|
||||
int size;
|
||||
if (colors == null) {
|
||||
colors0 = EMPTY_BYTE_ARRAY;
|
||||
size = 0;
|
||||
} else {
|
||||
colors0 = colors;
|
||||
size = 128;
|
||||
}
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, false, mapIcons, colors0, 0, 0, size, size);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.server.v1_16_R2.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.server.v1_16_R2.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata();
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.server.v1_16_R2.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.getDataWatcher();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.get(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().playerConnection.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
|
||||
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == 9) {
|
||||
NBTTagList loreLineTagList = displayTag.getList("Lore", 8);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.getString(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
|
||||
translatableComponent.setColor(ChatColor.GRAY);
|
||||
translatableComponent.setItalic(false);
|
||||
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.set("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.inventory;
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.pickup(nmsItemStack);
|
||||
if (!added || !nmsItemStack.isEmpty()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.activeContainer.c();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
|
||||
MinecraftKey key = nmsWorld.getTypeKey().a();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.minecraft.server.v1_16_R3.ChunkProviderServer;
|
||||
import net.minecraft.server.v1_16_R3.DataWatcher;
|
||||
import net.minecraft.server.v1_16_R3.DataWatcherObject;
|
||||
import net.minecraft.server.v1_16_R3.EntityHuman;
|
||||
import net.minecraft.server.v1_16_R3.EntityItemFrame;
|
||||
import net.minecraft.server.v1_16_R3.EntityPlayer;
|
||||
import net.minecraft.server.v1_16_R3.IChatBaseComponent;
|
||||
import net.minecraft.server.v1_16_R3.ItemWorldMap;
|
||||
import net.minecraft.server.v1_16_R3.MapIcon;
|
||||
import net.minecraft.server.v1_16_R3.MinecraftKey;
|
||||
import net.minecraft.server.v1_16_R3.NBTTagCompound;
|
||||
import net.minecraft.server.v1_16_R3.NBTTagList;
|
||||
import net.minecraft.server.v1_16_R3.NBTTagString;
|
||||
import net.minecraft.server.v1_16_R3.Packet;
|
||||
import net.minecraft.server.v1_16_R3.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.server.v1_16_R3.PacketPlayOutMap;
|
||||
import net.minecraft.server.v1_16_R3.PersistentIdCounts;
|
||||
import net.minecraft.server.v1_16_R3.PlayerChunkMap;
|
||||
import net.minecraft.server.v1_16_R3.PlayerInventory;
|
||||
import net.minecraft.server.v1_16_R3.ResourceKey;
|
||||
import net.minecraft.server.v1_16_R3.WorldMap;
|
||||
import net.minecraft.server.v1_16_R3.WorldServer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_16_R3.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_16_4 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field nmsMapIconTypeDisplayField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_16_4() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.server.v1_16_R3.Entity.class.getDeclaredField("S");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsMapIconTypeDisplayField = MapIcon.Type.class.getDeclaredField("C");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ITEM");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("a");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.colors = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
try {
|
||||
nmsMapIconTypeDisplayField.setAccessible(true);
|
||||
return nmsMapIconTypeDisplayField.getBoolean(mapIconType);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "resource"})
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked", "resource"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.server.v1_16_R3.World> worldTypeKey = worldServer.getDimensionKey();
|
||||
String mapId = ItemWorldMap.a(id);
|
||||
WorldMap worldMap = new WorldMap(mapId);
|
||||
worldMap.a(spawnLocation.getBlockX(), spawnLocation.getBlockZ(), 3, false, false, worldTypeKey);
|
||||
worldServer.a(worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.playerChunkMap;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.trackedEntities;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (EntityPlayer player : entityTracker.trackedPlayers) {
|
||||
players.add(player.getBukkitEntity());
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? Collections.emptyList() : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
byte[] colors0;
|
||||
int size;
|
||||
if (colors == null) {
|
||||
colors0 = EMPTY_BYTE_ARRAY;
|
||||
size = 0;
|
||||
} else {
|
||||
colors0 = colors;
|
||||
size = 128;
|
||||
}
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, false, mapIcons, colors0, 0, 0, size, size);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.server.v1_16_R3.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.server.v1_16_R3.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata();
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.server.v1_16_R3.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.getDataWatcher();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.get(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().playerConnection.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
|
||||
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == 9) {
|
||||
NBTTagList loreLineTagList = displayTag.getList("Lore", 8);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.getString(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
|
||||
translatableComponent.setColor(ChatColor.GRAY);
|
||||
translatableComponent.setItalic(false);
|
||||
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.set("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.inventory;
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.pickup(nmsItemStack);
|
||||
if (!added || !nmsItemStack.isEmpty()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.activeContainer.c();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
|
||||
MinecraftKey key = nmsWorld.getTypeKey().a();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.MinecraftKey;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_17_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_17 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_17() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ao");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked", "resource"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.getDimensionKey();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.G;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.getDataWatcher();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.get(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().b.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
|
||||
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getTag();
|
||||
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
|
||||
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.getList("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.getString(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
|
||||
translatableComponent.setColor(ChatColor.GRAY);
|
||||
translatableComponent.setItalic(false);
|
||||
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.set("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.getInventory();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.pickup(nmsItemStack);
|
||||
if (!added || !nmsItemStack.isEmpty()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bU.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
|
||||
MinecraftKey key = nmsWorld.getTypeKey().a();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.MinecraftKey;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_18_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_18 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_18() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("aa");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ap");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().u().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aa();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().u().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.I;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ai();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.s();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.t();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.s();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.t();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
|
||||
translatableComponent.setColor(ChatColor.GRAY);
|
||||
translatableComponent.setItalic(false);
|
||||
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fq();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bW.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
|
||||
MinecraftKey key = nmsWorld.getTypeKey().a();
|
||||
return Key.key(key.b(), key.a());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_18_R2.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_18_2 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_18_2() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ao");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aa();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.J;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ai();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.t();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.t();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
|
||||
translatableComponent.setColor(ChatColor.GRAY);
|
||||
translatableComponent.setItalic(false);
|
||||
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fr();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bU.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import com.loohp.imageframe.utils.UnsafeAccessor;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_19_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_19 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field[] nmsPacketPlayOutEntityMetadataFields;
|
||||
private final Field nmsItemFrameItemStackDataWatcherField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_19() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
|
||||
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ao");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ab();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.K;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
try {
|
||||
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
|
||||
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ai();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
|
||||
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
|
||||
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
|
||||
return packet;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fA();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bU.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_19_R2.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_19_3 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_19_3() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ac();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.L;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.ao, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.al();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fE();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bU.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_19_R3.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_19_4 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_19_4() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("an");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ab();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.L;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.aj();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.u();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fJ();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bP.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_20_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_20 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_20() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("an");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.b();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ac();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.b();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.K;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.aj();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.b(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.a((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.w();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.w();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fN();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bR.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ChunkProviderServer;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.PlayerChunkMap;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.server.network.PlayerConnection;
|
||||
import net.minecraft.server.network.ServerPlayerConnection;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_20_2 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_20_2() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("ao");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.d();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ac();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
|
||||
ChunkProviderServer chunkProviderServer = worldServer.k();
|
||||
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
|
||||
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.K;
|
||||
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
|
||||
if (entityTracker == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (ServerPlayerConnection connection : entityTracker.f) {
|
||||
if (connection instanceof PlayerConnection) {
|
||||
players.add(((PlayerConnection) connection).getCraftPlayer());
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.al();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.b(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.w();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.w();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fR();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bS.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.ItemWorldMap;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_20_R3.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_20_3 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_20_3() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("ao");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
|
||||
return MapIcon.Type.a(type.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
MapIcon.Type mapIconType = toNMSMapIconType(type);
|
||||
return mapIconType.d();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().F().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ae();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
worldServer.a(ItemWorldMap.a(id), worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().F().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.an();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.b(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.w();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.v();
|
||||
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound tag = nmsItemStack.w();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
NBTTagCompound displayTag = nmsItemStack.a("display");
|
||||
List<String> loreLines;
|
||||
if (displayTag.d("Lore") == NBTBase.q) {
|
||||
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
|
||||
loreLines = new ArrayList<>(loreLineTagList.size());
|
||||
for (int i = 0; i < loreLineTagList.size(); i++) {
|
||||
loreLines.add(loreLineTagList.j(i));
|
||||
}
|
||||
} else {
|
||||
loreLines = new ArrayList<>(1);
|
||||
}
|
||||
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
|
||||
NBTTagList loreLineTagList = new NBTTagList();
|
||||
for (int i = 0; i < loreLines.size(); i++) {
|
||||
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
|
||||
}
|
||||
displayTag.a("Lore", loreLineTagList);
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fS();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.e(nmsItemStack);
|
||||
if (!added || !nmsItemStack.b()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bS.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_20_5 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_20_5() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ap();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gc();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.e()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cb.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_20_R4.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_20_6 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_20_6() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ap();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gc();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.e()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cb.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_21() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.f, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ar();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fY();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.e()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cd.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R1.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_1 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_21_1() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.f, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ar();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.fY();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.e()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cd.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_10 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_10() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.al();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.t();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.aC();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.b();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.b();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.b();
|
||||
if (!tag.b(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.b();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gB();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cl.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R7.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_11 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_11() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.s().N().A().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "DataFlowIssue"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aq();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.s().N().A().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.u();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.aD();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.b();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.b();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.b();
|
||||
if (!tag.b(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.b();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.m, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.m, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gK();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cn.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_2 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_21_2() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ah();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.au();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gi();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cd.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R2.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_3 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
|
||||
public V1_21_3() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ah();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, renderData.cursors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.au();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.c();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.c();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gi();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cd.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R3.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_4 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsUsedAuxIdsField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_4() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.g = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
return usedAuxIds.getInt("map") + 1;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsUsedAuxIdsField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ai();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.a(), PersistentIdCounts.a);
|
||||
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
|
||||
int freeAuxValue = usedAuxIds.getInt("map");
|
||||
if (freeAuxValue < id) {
|
||||
usedAuxIds.put("map", id);
|
||||
persistentIdCounts.c();
|
||||
}
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.au();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.e(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
|
||||
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.e(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
|
||||
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gi();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.f(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cd.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R4.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_5 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_5() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.f();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.ar();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gj();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.bR.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_6 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_6() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "az");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.e();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.d, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.au();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().g.sendPacket((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gs();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cn.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_7 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_7() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "az");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.e();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.d, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.au();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gs();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cn.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R5.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_8 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_8() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "az");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.e();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.d, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.au();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.d();
|
||||
if (!tag.b(FilledMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.d();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gs();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cn.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import com.loohp.imageframe.utils.ReflectionUtils;
|
||||
import com.loohp.imageframe.utils.UUIDUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.EnumChatFormat;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.chat.ChatModifier;
|
||||
import net.minecraft.network.chat.IChatBaseComponent;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
|
||||
import net.minecraft.network.protocol.game.PacketPlayOutMap;
|
||||
import net.minecraft.network.syncher.DataWatcher;
|
||||
import net.minecraft.network.syncher.DataWatcherObject;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.EntityPlayer;
|
||||
import net.minecraft.server.level.WorldServer;
|
||||
import net.minecraft.world.entity.decoration.EntityItemFrame;
|
||||
import net.minecraft.world.entity.player.EntityHuman;
|
||||
import net.minecraft.world.entity.player.PlayerInventory;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
|
||||
import net.minecraft.world.level.saveddata.maps.MapIcon;
|
||||
import net.minecraft.world.level.saveddata.maps.MapId;
|
||||
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
|
||||
import net.minecraft.world.level.saveddata.maps.WorldMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapCursor;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapView;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.map.RenderData;
|
||||
import org.bukkit.craftbukkit.v1_21_R6.util.CraftChatMessage;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class V1_21_9 extends NMSWrapper {
|
||||
|
||||
private final Field nmsEntityByteDataWatcherField;
|
||||
private final Field craftMapViewWorldMapField;
|
||||
private final Field persistentIdCountsLastMapIdField;
|
||||
private final Field renderDataCursorsField;
|
||||
|
||||
public V1_21_9() {
|
||||
try {
|
||||
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA");
|
||||
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
|
||||
Field persistentIdCountsLastMapIdField0;
|
||||
try {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
|
||||
} catch (NoSuchFieldException e) {
|
||||
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
|
||||
}
|
||||
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
|
||||
renderDataCursorsField = RenderData.class.getField("cursors");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMap getWorldMap(MapView mapView) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
craftMapViewWorldMapField.setAccessible(true);
|
||||
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColors(MapView mapView, byte[] colors) {
|
||||
if (colors.length != COLOR_ARRAY_LENGTH) {
|
||||
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
|
||||
}
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
nmsWorldMap.h = colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasViewers(MapView mapView) {
|
||||
WorldMap nmsWorldMap = getWorldMap(mapView);
|
||||
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
|
||||
return !humansMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
|
||||
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
|
||||
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
|
||||
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRenderOnFrame(MapCursor.Type type) {
|
||||
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
|
||||
return decorationTypeHolder.a().c();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextAvailableMapId(World world) {
|
||||
try {
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
return atomicInteger.get() + 1;
|
||||
} else {
|
||||
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public MapView getMapOrCreateMissing(World world, int id) {
|
||||
try {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView != null) {
|
||||
return mapView;
|
||||
}
|
||||
persistentIdCountsLastMapIdField.setAccessible(true);
|
||||
Location spawnLocation = world.getSpawnLocation();
|
||||
WorldServer worldServer = ((CraftWorld) world).getHandle();
|
||||
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.al();
|
||||
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
|
||||
MapId mapId = new MapId(id);
|
||||
worldServer.a(mapId, worldMap);
|
||||
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
|
||||
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
|
||||
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
|
||||
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
|
||||
} else {
|
||||
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
|
||||
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
|
||||
}
|
||||
persistentIdCounts.t();
|
||||
return Bukkit.getMap(id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
|
||||
try {
|
||||
CraftMapView craftMapView = (CraftMapView) mapView;
|
||||
CraftPlayer craftPlayer = (CraftPlayer) player;
|
||||
RenderData renderData = craftMapView.render(craftPlayer);
|
||||
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Player> getEntityTrackers(Entity entity) {
|
||||
return new HashSet<>(entity.getTrackedBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
|
||||
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
|
||||
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
|
||||
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack)));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
|
||||
try {
|
||||
int entityId = entity.getEntityId();
|
||||
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
|
||||
|
||||
nmsEntityByteDataWatcherField.setAccessible(true);
|
||||
|
||||
DataWatcher watcher = nmsEntity.aC();
|
||||
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
|
||||
byte value = watcher.a(byteField);
|
||||
|
||||
if (invisible != null) {
|
||||
if (invisible) {
|
||||
value = (byte) (value | 0x20);
|
||||
} else {
|
||||
value = (byte) (value & ~0x20);
|
||||
}
|
||||
}
|
||||
if (glowing != null) {
|
||||
if (glowing) {
|
||||
value = (byte) (value | 0x40);
|
||||
} else {
|
||||
value = (byte) (value & ~0x40);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
|
||||
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(Player player, Object packet) {
|
||||
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Override
|
||||
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.b();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
|
||||
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
|
||||
return new CombinedMapItemInfo(imageMapIndex);
|
||||
}
|
||||
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
|
||||
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
|
||||
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.b();
|
||||
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
|
||||
if (combinedMapItemInfo.hasPlacement()) {
|
||||
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
|
||||
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
|
||||
}
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b);
|
||||
if (customData == null) {
|
||||
return null;
|
||||
}
|
||||
NBTTagCompound tag = customData.b();
|
||||
if (!tag.b(CombinedMapItemInfo.KEY)) {
|
||||
return null;
|
||||
}
|
||||
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
|
||||
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
|
||||
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
|
||||
NBTTagCompound tag = customData.b();
|
||||
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
|
||||
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getImageMapIndex());
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
|
||||
return CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
|
||||
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
|
||||
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
|
||||
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
|
||||
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
|
||||
ItemMeta itemMeta = modified.getItemMeta();
|
||||
if (itemMeta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
modified.setItemMeta(itemMeta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
|
||||
List<ItemStack> leftovers = new ArrayList<>();
|
||||
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
|
||||
PlayerInventory inventory = nmsPlayer.gB();
|
||||
for (ItemStack itemStack : itemStacks) {
|
||||
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
|
||||
boolean added = inventory.g(nmsItemStack);
|
||||
if (!added || !nmsItemStack.f()) {
|
||||
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
|
||||
}
|
||||
}
|
||||
nmsPlayer.cl.d();
|
||||
return leftovers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public Key getWorldNamespacedKey(World world) {
|
||||
NamespacedKey key = world.getKey();
|
||||
return Key.key(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
|
||||
import com.loohp.imageframe.objectholders.MutablePair;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class NMSWrapper {
|
||||
|
||||
public static final int COLOR_ARRAY_LENGTH = 16384;
|
||||
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
|
||||
public abstract void setColors(MapView mapView, byte[] colors);
|
||||
|
||||
public abstract Collection<Player> getViewers(MapView mapView);
|
||||
|
||||
public abstract boolean hasViewers(MapView mapView);
|
||||
|
||||
public abstract Object toNMSMapIcon(MapCursor mapCursor);
|
||||
|
||||
public abstract Object toNMSMapIconType(MapCursor.Type type);
|
||||
|
||||
public abstract boolean isRenderOnFrame(MapCursor.Type type);
|
||||
|
||||
public abstract int getNextAvailableMapId(World world);
|
||||
|
||||
public abstract MapView getMapOrCreateMissing(World world, int id);
|
||||
|
||||
public abstract MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player);
|
||||
|
||||
public abstract Set<Player> getEntityTrackers(Entity entity);
|
||||
|
||||
public abstract Object createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors);
|
||||
|
||||
public abstract Object createItemFrameItemChangePacket(int entityId, ItemStack itemStack);
|
||||
|
||||
public abstract Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing);
|
||||
|
||||
public abstract void sendPacket(Player player, Object packet);
|
||||
|
||||
public abstract CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack);
|
||||
|
||||
public abstract ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo);
|
||||
|
||||
public abstract FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack);
|
||||
|
||||
public abstract ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo);
|
||||
|
||||
public abstract ItemStack withInvisibleItemFrameMeta(ItemStack itemStack);
|
||||
|
||||
public abstract List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks);
|
||||
|
||||
public abstract Key getWorldNamespacedKey(World world);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class CombinedMapItemInfo {
|
||||
|
||||
public static final String KEY = "CombinedImageMap";
|
||||
public static final String PLACEMENT_YAW_KEY = "CombinedImageMapPlacementYaw";
|
||||
public static final String PLACEMENT_UUID_KEY = "CombinedImageMapPlacementUUID";
|
||||
|
||||
private final int imageMapIndex;
|
||||
private final PlacementInfo placementInfo;
|
||||
|
||||
public CombinedMapItemInfo(int imageMapIndex, PlacementInfo placementInfo) {
|
||||
this.imageMapIndex = imageMapIndex;
|
||||
this.placementInfo = placementInfo;
|
||||
}
|
||||
|
||||
public CombinedMapItemInfo(int imageMapIndex) {
|
||||
this(imageMapIndex, null);
|
||||
}
|
||||
|
||||
public int getImageMapIndex() {
|
||||
return imageMapIndex;
|
||||
}
|
||||
|
||||
public boolean hasPlacement() {
|
||||
return placementInfo != null;
|
||||
}
|
||||
|
||||
public PlacementInfo getPlacement() {
|
||||
return placementInfo;
|
||||
}
|
||||
|
||||
public static class PlacementInfo {
|
||||
|
||||
private final float yaw;
|
||||
private final UUID uuid;
|
||||
|
||||
public PlacementInfo(float yaw, UUID uuid) {
|
||||
this.yaw = yaw;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public float getYaw() {
|
||||
return yaw;
|
||||
}
|
||||
|
||||
public UUID getUniqueId() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
public class FilledMapItemInfo {
|
||||
|
||||
public static final String KEY = "ImageMap";
|
||||
public static final String INDEX_KEY = "ImageMapPart";
|
||||
|
||||
private final int imageMapIndex;
|
||||
private final int mapPartIndex;
|
||||
|
||||
public FilledMapItemInfo(int imageMapIndex, int mapPartIndex) {
|
||||
this.imageMapIndex = imageMapIndex;
|
||||
this.mapPartIndex = mapPartIndex;
|
||||
}
|
||||
|
||||
public int getImageMapIndex() {
|
||||
return imageMapIndex;
|
||||
}
|
||||
|
||||
public int getMapPartIndex() {
|
||||
return mapPartIndex;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class MutablePair<F, S> {
|
||||
|
||||
private F first;
|
||||
private S second;
|
||||
|
||||
public MutablePair(F first, S second) {
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
public void setFirst(F first) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
public void setSecond(S second) {
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
public F getFirst() {
|
||||
return first;
|
||||
}
|
||||
|
||||
public S getSecond() {
|
||||
return second;
|
||||
}
|
||||
|
||||
public <T> T product(BiFunction<F, S, T> function) {
|
||||
return function.apply(first, second);
|
||||
}
|
||||
|
||||
public void consume(BiConsumer<F, S> consumer) {
|
||||
consumer.accept(first, second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MutablePair<?, ?> that = (MutablePair<?, ?>) o;
|
||||
return Objects.equals(first, that.first) && Objects.equals(second, that.second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(first, second);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.utils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ReflectionUtils {
|
||||
|
||||
public static Field findDeclaredField(Class<?> clazz, Class<?> fieldType, String... names) throws NoSuchFieldException {
|
||||
NoSuchFieldException exception = null;
|
||||
for (String name : names) {
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(name);
|
||||
if (field.getType().equals(fieldType)) {
|
||||
return field;
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
if (exception == null) {
|
||||
throw new NoSuchFieldException();
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
public static Method findDeclaredMethod(Class<?> clazz, Class<?>[] parameters, String... names) throws NoSuchMethodException {
|
||||
NoSuchMethodException exception = null;
|
||||
for (String name : names) {
|
||||
try {
|
||||
return clazz.getDeclaredMethod(name, parameters);
|
||||
} catch (NoSuchMethodException e) {
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
if (exception == null) {
|
||||
throw new NoSuchMethodException();
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.utils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class UUIDUtils {
|
||||
|
||||
public static UUID fromIntArray(int[] array) {
|
||||
if (array.length != 4) {
|
||||
throw new IllegalArgumentException("array length is not 4");
|
||||
}
|
||||
return new UUID((long) array[0] << 32 | (long) array[1] & 4294967295L, (long) array[2] << 32 | (long) array[3] & 4294967295L);
|
||||
}
|
||||
|
||||
public static int[] toIntArray(UUID uuid) {
|
||||
long high = uuid.getMostSignificantBits();
|
||||
long low = uuid.getLeastSignificantBits();
|
||||
return new int[] {(int) (high >>> 32), (int) high, (int) (high >>> low), (int) low};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.utils;
|
||||
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public class UnsafeAccessor {
|
||||
|
||||
private static final Field unsafeField;
|
||||
private static Unsafe unsafe;
|
||||
|
||||
static {
|
||||
try {
|
||||
unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Unsafe getUnsafe() {
|
||||
if (unsafe != null) {
|
||||
return unsafe;
|
||||
}
|
||||
try {
|
||||
unsafeField.setAccessible(true);
|
||||
return unsafe = (Unsafe) unsafeField.get(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion
|
||||
import org.gradle.api.tasks.Copy
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
import org.gradle.jvm.toolchain.JvmVendorSpec
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import java.io.File
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("com.gradleup.shadow") version "8.3.9" apply false
|
||||
eclipse
|
||||
}
|
||||
|
||||
group = "com.loohp"
|
||||
version = "1.9.0.3"
|
||||
|
||||
val adventureVersion = "4.25.0"
|
||||
val moduleMinecraftVersions =
|
||||
mapOf(
|
||||
"V1_19_4" to "1.19.4-R0.1-SNAPSHOT",
|
||||
)
|
||||
|
||||
fun localMavenRepoDirs(project: Project): List<File> {
|
||||
val repos = mutableListOf(
|
||||
File(System.getProperty("user.home"), ".m2/repository"),
|
||||
)
|
||||
System.getProperty("SELF_MAVEN_LOCAL_REPO")?.let { path ->
|
||||
repos.add(project.file(path))
|
||||
}
|
||||
return repos.distinct()
|
||||
}
|
||||
|
||||
fun artifactExists(repoDir: File, group: String, artifact: String, version: String): Boolean {
|
||||
val artifactDir = File(repoDir, "${group.replace('.', '/')}/$artifact/$version")
|
||||
if (!artifactDir.isDirectory) {
|
||||
return false
|
||||
}
|
||||
val exactJar = File(artifactDir, "$artifact-$version.jar")
|
||||
if (exactJar.isFile) {
|
||||
return true
|
||||
}
|
||||
return artifactDir.listFiles()?.any { file ->
|
||||
file.isFile && file.extension == "jar" && file.name.startsWith("$artifact-$version")
|
||||
} == true
|
||||
}
|
||||
|
||||
fun needsLocalCraftBukkitBootstrap(project: Project, version: String): Boolean {
|
||||
return localMavenRepoDirs(project).none { repoDir ->
|
||||
artifactExists(repoDir, "org.bukkit", "craftbukkit", version)
|
||||
}
|
||||
}
|
||||
|
||||
fun fallbackRepoMinecraftVersion(project: Project, moduleName: String, exactVersion: String): String {
|
||||
if (!needsLocalCraftBukkitBootstrap(project, exactVersion)) {
|
||||
return exactVersion
|
||||
}
|
||||
|
||||
return when (moduleName) {
|
||||
"V1_20_3" -> "1.20.4-R0.1-SNAPSHOT"
|
||||
"V1_21_2" -> "1.21.3-R0.1-SNAPSHOT"
|
||||
else -> exactVersion
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = rootProject.group
|
||||
version = rootProject.version
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java-library")
|
||||
apply(plugin = "eclipse")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
mavenLocal()
|
||||
maven { url = uri("file://${System.getProperty("user.home")}/.m2/repository") }
|
||||
System.getProperty("SELF_MAVEN_LOCAL_REPO")?.let { path ->
|
||||
val dir = file(path)
|
||||
if (dir.isDirectory) {
|
||||
maven { url = uri("file://${dir.absolutePath}") }
|
||||
} else {
|
||||
logger.warn("SELF_MAVEN_LOCAL_REPO is not a directory: $path")
|
||||
}
|
||||
}
|
||||
maven("https://libraries.minecraft.net/") {
|
||||
content {
|
||||
includeGroup("com.mojang")
|
||||
}
|
||||
}
|
||||
maven("https://repo.loohpjames.com/repository") {
|
||||
// Prefer the mirror first so Gradle does not pin snapshot metadata from Spigot that
|
||||
// points at missing timestamped artifacts for legacy Bukkit/CraftBukkit coordinates.
|
||||
metadataSources {
|
||||
mavenPom()
|
||||
artifact()
|
||||
ignoreGradleMetadataRedirection()
|
||||
}
|
||||
content {
|
||||
includeGroup("com.loohp")
|
||||
includeGroup("org.bukkit")
|
||||
includeGroup("org.spigotmc")
|
||||
}
|
||||
}
|
||||
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") {
|
||||
content {
|
||||
includeGroup("org.spigotmc")
|
||||
includeGroup("org.bukkit")
|
||||
}
|
||||
}
|
||||
maven("https://repo.papermc.io/repository/maven-public/") {
|
||||
content {
|
||||
includeGroup("dev.folia")
|
||||
includeGroup("io.papermc.paper")
|
||||
}
|
||||
}
|
||||
maven("https://repository.liferay.com/nexus/content/repositories/public/") {
|
||||
content {
|
||||
includeGroup("me.carleslc.Simple-YAML")
|
||||
}
|
||||
}
|
||||
maven("https://repo.viaversion.com") {
|
||||
content {
|
||||
includeGroup("com.viaversion")
|
||||
}
|
||||
}
|
||||
maven("https://repo.extendedclip.com/releases/") {
|
||||
content {
|
||||
includeGroup("me.clip")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
if (isCanBeResolved) {
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
vendor.set(JvmVendorSpec.GRAAL_VM)
|
||||
}
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
options.isFork = true
|
||||
options.compilerArgs.add("-Xlint:deprecation")
|
||||
sourceCompatibility = "1.8"
|
||||
targetCompatibility = "1.8"
|
||||
}
|
||||
|
||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||
isPreserveFileTimestamps = false
|
||||
isReproducibleFileOrder = true
|
||||
}
|
||||
|
||||
tasks.withType<Jar>().configureEach {
|
||||
archiveBaseName.set(rootProject.name)
|
||||
}
|
||||
}
|
||||
|
||||
project(":abstraction") {
|
||||
dependencies {
|
||||
compileOnly("org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT")
|
||||
compileOnly("org.bukkit:craftbukkit:1.19.4-R0.1-SNAPSHOT")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:$adventureVersion")
|
||||
compileOnly("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
|
||||
compileOnly("net.kyori:adventure-text-serializer-plain:$adventureVersion")
|
||||
compileOnly("net.kyori:adventure-api:$adventureVersion")
|
||||
}
|
||||
}
|
||||
|
||||
moduleMinecraftVersions.forEach { (moduleName, minecraftVersion) ->
|
||||
project(":$moduleName") {
|
||||
val resolvedMinecraftVersion = fallbackRepoMinecraftVersion(this, moduleName, minecraftVersion)
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.spigotmc:spigot-api:$resolvedMinecraftVersion")
|
||||
compileOnly("org.bukkit:craftbukkit:$resolvedMinecraftVersion")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:$adventureVersion")
|
||||
compileOnly("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
|
||||
compileOnly("net.kyori:adventure-text-serializer-plain:$adventureVersion")
|
||||
compileOnly("net.kyori:adventure-api:$adventureVersion")
|
||||
compileOnly(project(":abstraction"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project(":common") {
|
||||
apply(plugin = "com.gradleup.shadow")
|
||||
|
||||
dependencies {
|
||||
implementation("com.zaxxer:HikariCP:7.0.2")
|
||||
implementation("com.loohp:PlatformScheduler:1.0.0")
|
||||
compileOnly("com.viaversion:viaversion-api:4.5.1")
|
||||
implementation("me.carleslc.Simple-YAML:Simple-Yaml:1.8.1")
|
||||
implementation("commons-fileupload:commons-fileupload:1.4")
|
||||
compileOnly("me.clip:placeholderapi:2.11.6")
|
||||
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
|
||||
implementation("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
|
||||
implementation("net.kyori:adventure-text-serializer-plain:$adventureVersion")
|
||||
implementation("net.kyori:adventure-api:$adventureVersion")
|
||||
implementation("com.madgag:animated-gif-lib:1.4")
|
||||
compileOnly("org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT")
|
||||
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.1")
|
||||
|
||||
implementation(project(":abstraction"))
|
||||
moduleMinecraftVersions.keys.forEach { implementation(project(":$it")) }
|
||||
}
|
||||
|
||||
tasks.named<ProcessResources>("processResources") {
|
||||
filteringCharset = "UTF-8"
|
||||
filesMatching("plugin.yml") {
|
||||
filter { line -> line.replace("\${project.version}", project.version.toString()) }
|
||||
}
|
||||
from(rootProject.file("LICENSE")) {
|
||||
into("/")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
archiveClassifier.set("part")
|
||||
}
|
||||
|
||||
tasks.named<ShadowJar>("shadowJar") {
|
||||
archiveClassifier.set("")
|
||||
relocate("com.loohp.platformscheduler", "com.loohp.imageframe.libs.com.loohp.platformscheduler")
|
||||
relocate("com.zaxxer.hikari", "com.loohp.imageframe.libs.com.zaxxer.hikari")
|
||||
relocate("org.simpleyaml", "com.loohp.imageframe.libs.org.simpleyaml")
|
||||
relocate("org.yaml.snakeyaml", "com.loohp.imageframe.libs.org.yaml.snakeyaml")
|
||||
relocate("com.madgag.gif", "com.loohp.imageframe.libs.com.madgag.gif")
|
||||
relocate("net.kyori", "com.loohp.imageframe.libs.net.kyori")
|
||||
relocate("org.apache.commons", "com.loohp.imageframe.libs.org.apache.commons")
|
||||
}
|
||||
|
||||
tasks.named("build") {
|
||||
dependsOn(tasks.named("shadowJar"))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
tasks.register<Copy>("collectPluginJar") {
|
||||
dependsOn(":common:shadowJar")
|
||||
from(project(":common").tasks.named<ShadowJar>("shadowJar").flatMap { it.archiveFile })
|
||||
into(layout.buildDirectory.dir("libs"))
|
||||
}
|
||||
|
||||
tasks.named("assemble") {
|
||||
dependsOn("collectPluginJar")
|
||||
}
|
||||
|
||||
tasks.named("build") {
|
||||
dependsOn("collectPluginJar")
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe;
|
||||
|
||||
import com.loohp.imageframe.config.Config;
|
||||
import com.loohp.imageframe.debug.Debug;
|
||||
import com.loohp.imageframe.invisibleframe.InvisibleFrameManager;
|
||||
import com.loohp.imageframe.language.LanguageManager;
|
||||
import com.loohp.imageframe.listeners.Events;
|
||||
import com.loohp.imageframe.metrics.Charts;
|
||||
import com.loohp.imageframe.metrics.Metrics;
|
||||
import com.loohp.imageframe.objectholders.AnimatedFakeMapManager;
|
||||
import com.loohp.imageframe.objectholders.BundledMaps;
|
||||
import com.loohp.imageframe.objectholders.CombinedMapItemHandler;
|
||||
import com.loohp.imageframe.objectholders.CustomClientNetworkManager;
|
||||
import com.loohp.imageframe.objectholders.IFPlayerManager;
|
||||
import com.loohp.imageframe.objectholders.IFPlayerPreference;
|
||||
import com.loohp.imageframe.objectholders.ImageMap;
|
||||
import com.loohp.imageframe.objectholders.ImageMapAccessPermissionType;
|
||||
import com.loohp.imageframe.objectholders.ImageMapCacheControlMode;
|
||||
import com.loohp.imageframe.objectholders.ImageMapCreationTaskManager;
|
||||
import com.loohp.imageframe.objectholders.ImageMapLoaders;
|
||||
import com.loohp.imageframe.objectholders.ImageMapManager;
|
||||
import com.loohp.imageframe.objectholders.IntRange;
|
||||
import com.loohp.imageframe.objectholders.IntRangeList;
|
||||
import com.loohp.imageframe.objectholders.ItemFrameSelectionManager;
|
||||
import com.loohp.imageframe.objectholders.MapMarkerEditManager;
|
||||
import com.loohp.imageframe.objectholders.RateLimitedPacketSendingManager;
|
||||
import com.loohp.imageframe.objectholders.UnsetState;
|
||||
import com.loohp.imageframe.placeholderapi.Placeholders;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorageLoaders;
|
||||
import com.loohp.imageframe.updater.Updater;
|
||||
import com.loohp.imageframe.upload.ImageUploadManager;
|
||||
import com.loohp.imageframe.upload.LocalImageFolder;
|
||||
import com.loohp.imageframe.utils.ChatColorUtils;
|
||||
import com.loohp.imageframe.utils.KeyUtils;
|
||||
import com.loohp.imageframe.utils.MCVersion;
|
||||
import com.loohp.imageframe.utils.ModernEventsUtils;
|
||||
import com.loohp.platformscheduler.ScheduledTask;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ImageFrame extends JavaPlugin {
|
||||
|
||||
public static final int BSTATS_PLUGIN_ID = 16773;
|
||||
public static final String CONFIG_ID = "config";
|
||||
|
||||
public static ImageFrame plugin;
|
||||
|
||||
public static MCVersion version;
|
||||
|
||||
public Metrics metrics;
|
||||
|
||||
public static boolean viaHook = false;
|
||||
public static boolean viaDisableSmoothAnimationForLegacyPlayers = false;
|
||||
|
||||
public static boolean debugLogging;
|
||||
|
||||
public static boolean updaterEnabled;
|
||||
public static ScheduledTask updaterTask = null;
|
||||
|
||||
public static String language;
|
||||
|
||||
public static SimpleDateFormat dateFormat;
|
||||
|
||||
public static String mapItemFormat;
|
||||
public static boolean requireEmptyMaps;
|
||||
public static int mapMaxSize;
|
||||
public static Map<String, Integer> playerCreationLimit;
|
||||
public static int mapMarkerLimit;
|
||||
public static long maxImageFileSize;
|
||||
public static int maxProcessingTime;
|
||||
public static int parallelProcessingLimit;
|
||||
|
||||
public static int rateLimit;
|
||||
|
||||
public static IntRangeList exemptMapIdsFromDeletion;
|
||||
|
||||
public static boolean mapRenderersContextual;
|
||||
public static boolean handleAnimatedMapsOnMainThread;
|
||||
public static boolean sendAnimatedMapsOnMainThread;
|
||||
|
||||
public static ImageMapCacheControlMode<?> cacheControlMode;
|
||||
public static boolean tryDeleteBlankMapFiles;
|
||||
|
||||
public static boolean combinedByDefault;
|
||||
|
||||
public static boolean uploadServiceEnabled;
|
||||
public static String uploadServiceDisplayURL;
|
||||
public static String uploadServiceServerAddress;
|
||||
public static int uploadServiceServerPort;
|
||||
|
||||
public static boolean imageFrameClientEnabled;
|
||||
|
||||
public static int invisibleFrameMaxConversionsPerSplash;
|
||||
public static boolean invisibleFrameGlowEmptyFrames;
|
||||
|
||||
public static Key storageType;
|
||||
public static Map<String, String> storageOptions;
|
||||
|
||||
public static LanguageManager languageManager;
|
||||
public static ImageFrameStorage imageFrameStorage;
|
||||
public static ImageMapManager imageMapManager;
|
||||
public static IFPlayerManager ifPlayerManager;
|
||||
public static ItemFrameSelectionManager itemFrameSelectionManager;
|
||||
public static MapMarkerEditManager mapMarkerEditManager;
|
||||
public static CombinedMapItemHandler combinedMapItemHandler;
|
||||
public static AnimatedFakeMapManager animatedFakeMapManager;
|
||||
public static RateLimitedPacketSendingManager rateLimitedPacketSendingManager;
|
||||
public static InvisibleFrameManager invisibleFrameManager;
|
||||
public static ImageMapCreationTaskManager imageMapCreationTaskManager;
|
||||
public static ImageUploadManager imageUploadManager;
|
||||
public static LocalImageFolder localImageFolder;
|
||||
public static CustomClientNetworkManager customClientNetworkManager;
|
||||
|
||||
// Image URLs are not accepted, only images from the local images folder or files sent through the upload service
|
||||
public static boolean isURLAllowed(String link) {
|
||||
if (link == null) {
|
||||
return false;
|
||||
}
|
||||
if (localImageFolder.isImageFileUrl(link)) {
|
||||
return true;
|
||||
}
|
||||
if (!imageUploadManager.isOperational()) {
|
||||
return false;
|
||||
}
|
||||
return "upload".equalsIgnoreCase(link) || imageUploadManager.wasUploaded(link);
|
||||
}
|
||||
|
||||
public static <T extends UnsetState> T getPreferenceUnsetValue(Player player, IFPlayerPreference<T> preference) {
|
||||
String prefix = "imageframe.preference.unsetdefault." + preference.name().toLowerCase() + ".";
|
||||
for (Map.Entry<String, T> entry : preference.getSuggestedValues().entrySet()) {
|
||||
T value = entry.getValue();
|
||||
if (!value.isUnset() && player.hasPermission(prefix + entry.getKey().toLowerCase())) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return preference.getDefaultValue(ifPlayerManager.getIFPlayer(player.getUniqueId()));
|
||||
}
|
||||
|
||||
public static int getPlayerCreationLimit(Player player) {
|
||||
if (player.hasPermission("imageframe.createlimit.unlimited")) {
|
||||
return -1;
|
||||
}
|
||||
int limit = Integer.MIN_VALUE;
|
||||
for (Map.Entry<String, Integer> entry : playerCreationLimit.entrySet()) {
|
||||
if (player.hasPermission("imageframe.createlimit." + entry.getKey())) {
|
||||
int value = entry.getValue();
|
||||
if (value < 0) {
|
||||
return -1;
|
||||
} else if (value > limit) {
|
||||
limit = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (limit == Integer.MIN_VALUE) {
|
||||
return playerCreationLimit.getOrDefault("default", -1);
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
public static boolean hasImageMapPermission(ImageMap imageMap, CommandSender sender, ImageMapAccessPermissionType permissionType) {
|
||||
if (permissionType == null) {
|
||||
return true;
|
||||
}
|
||||
if (sender.hasPermission("imageframe.adminbypass")) {
|
||||
return true;
|
||||
}
|
||||
// Console-created (bundled) maps have no player owner, so the access-control layer would
|
||||
// otherwise deny everyone. Grant access here and let the per-command permissions
|
||||
// (imageframe.get / imageframe.refresh / imageframe.delete, all op by default) gate who can
|
||||
// actually use or modify them. This lets admins refresh/re-render bundled maps.
|
||||
if (imageMap.getCreator().equals(ImageMap.CONSOLE_CREATOR)) {
|
||||
return true;
|
||||
}
|
||||
if (!(sender instanceof Player)) {
|
||||
return false;
|
||||
}
|
||||
return imageMap.getAccessControl().hasPermission(((Player) sender).getUniqueId(), permissionType);
|
||||
}
|
||||
|
||||
public static boolean isPluginEnabled(String name) {
|
||||
return isPluginEnabled(name, true);
|
||||
}
|
||||
|
||||
public static boolean isPluginEnabled(String name, boolean checkRunning) {
|
||||
Plugin plugin = Bukkit.getPluginManager().getPlugin(name);
|
||||
return plugin != null && (!checkRunning || plugin.isEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
plugin = this;
|
||||
|
||||
version = MCVersion.resolve();
|
||||
|
||||
if (!version.isSupported()) {
|
||||
getServer().getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] This version of minecraft is unsupported! (" + version.toString() + ")");
|
||||
}
|
||||
|
||||
metrics = new Metrics(this, BSTATS_PLUGIN_ID);
|
||||
Charts.setup(metrics);
|
||||
|
||||
getDataFolder().mkdirs();
|
||||
|
||||
if (!ImageIO.getImageReadersByFormatName("webp").hasNext()) {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new WebPImageReaderSpi());
|
||||
}
|
||||
|
||||
try {
|
||||
Config.loadConfig(CONFIG_ID, new File(getDataFolder(), "config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), true);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
reloadConfig();
|
||||
|
||||
getCommand("imageframe").setExecutor(new Commands());
|
||||
|
||||
if (isPluginEnabled("ViaVersion")) {
|
||||
getServer().getConsoleSender().sendMessage(ChatColor.AQUA + "[ImageFrame] ImageFrame has hooked into ViaVersion!");
|
||||
viaHook = true;
|
||||
}
|
||||
|
||||
getServer().getPluginManager().registerEvents(new Debug(), this);
|
||||
getServer().getPluginManager().registerEvents(new Updater(), this);
|
||||
getServer().getPluginManager().registerEvents(new Events(), this);
|
||||
if (ModernEventsUtils.modernEventsExists()) {
|
||||
getServer().getPluginManager().registerEvents(new Events.ModernEvents(), this);
|
||||
}
|
||||
|
||||
languageManager = new LanguageManager();
|
||||
imageFrameStorage = ImageFrameStorageLoaders.create(storageType, getDataFolder(), storageOptions);
|
||||
imageMapManager = new ImageMapManager(imageFrameStorage);
|
||||
ifPlayerManager = new IFPlayerManager(imageFrameStorage);
|
||||
itemFrameSelectionManager = new ItemFrameSelectionManager();
|
||||
mapMarkerEditManager = new MapMarkerEditManager();
|
||||
combinedMapItemHandler = new CombinedMapItemHandler();
|
||||
animatedFakeMapManager = new AnimatedFakeMapManager();
|
||||
rateLimitedPacketSendingManager = new RateLimitedPacketSendingManager();
|
||||
invisibleFrameManager = new InvisibleFrameManager();
|
||||
imageMapCreationTaskManager = new ImageMapCreationTaskManager(ImageFrame.parallelProcessingLimit);
|
||||
imageUploadManager = new ImageUploadManager(uploadServiceEnabled, uploadServiceServerAddress, uploadServiceServerPort);
|
||||
localImageFolder = new LocalImageFolder(new File(getDataFolder(), "images"));
|
||||
customClientNetworkManager = new CustomClientNetworkManager(imageFrameClientEnabled);
|
||||
|
||||
if (isPluginEnabled("PlaceholderAPI")) {
|
||||
new Placeholders().register();
|
||||
}
|
||||
|
||||
ImageMapLoaders.init();
|
||||
|
||||
Scheduler.runTaskAsynchronously(this, () -> imageMapManager.loadMaps(ifPlayerManager, () -> BundledMaps.load(imageMapManager, getFile())));
|
||||
|
||||
getServer().getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] ImageFrame has been Enabled!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (mapMarkerEditManager != null) {
|
||||
mapMarkerEditManager.close();
|
||||
}
|
||||
if (itemFrameSelectionManager != null) {
|
||||
itemFrameSelectionManager.close();
|
||||
}
|
||||
if (imageMapManager != null) {
|
||||
imageMapManager.close();
|
||||
}
|
||||
if (combinedMapItemHandler != null) {
|
||||
combinedMapItemHandler.close();
|
||||
}
|
||||
if (imageUploadManager != null) {
|
||||
imageUploadManager.close();
|
||||
}
|
||||
if (imageFrameStorage != null) {
|
||||
imageFrameStorage.close();
|
||||
}
|
||||
getServer().getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageFrame has been Disabled!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
Config config = Config.getConfig(CONFIG_ID);
|
||||
config.reload();
|
||||
|
||||
viaDisableSmoothAnimationForLegacyPlayers = config.getConfiguration().getBoolean("Hooks.ViaVersion.DisableSmoothAnimationForLegacyPlayers");
|
||||
|
||||
language = config.getConfiguration().getString("Settings.Language");
|
||||
|
||||
dateFormat = new SimpleDateFormat(config.getConfiguration().getString("Settings.DateFormat"));
|
||||
|
||||
mapItemFormat = ChatColorUtils.translateAlternateColorCodes('&', config.getConfiguration().getString("Settings.MapItemFormat"));
|
||||
requireEmptyMaps = config.getConfiguration().getBoolean("Settings.RequireEmptyMaps");
|
||||
mapMaxSize = config.getConfiguration().getInt("Settings.MaxSize");
|
||||
playerCreationLimit = new HashMap<>();
|
||||
for (String group : config.getConfiguration().getConfigurationSection("Settings.PlayerCreationLimit").getKeys(false)) {
|
||||
playerCreationLimit.put(group, config.getConfiguration().getInt("Settings.PlayerCreationLimit." + group));
|
||||
}
|
||||
mapMarkerLimit = config.getConfiguration().getInt("Settings.MapMarkerLimit");
|
||||
maxImageFileSize = config.getConfiguration().getLong("Settings.MaxImageFileSize");
|
||||
maxProcessingTime = config.getConfiguration().getInt("Settings.MaxProcessingTime");
|
||||
parallelProcessingLimit = config.getConfiguration().getInt("Settings.ParallelProcessingLimit");
|
||||
|
||||
exemptMapIdsFromDeletion = config.getConfiguration().getList("Settings.ExemptMapIdsFromDeletion").stream().map(v -> {
|
||||
try {
|
||||
if (v instanceof Number) {
|
||||
return IntRange.of(((Number) v).intValue());
|
||||
} else {
|
||||
return IntRange.of(v.toString());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}).filter(v -> v != null).collect(Collectors.toCollection(IntRangeList::new));
|
||||
|
||||
rateLimit = config.getConfiguration().getInt("Settings.MapPacketSendingRateLimit");
|
||||
|
||||
mapRenderersContextual = config.getConfiguration().getBoolean("Settings.MapRenderersContextual");
|
||||
handleAnimatedMapsOnMainThread = config.getConfiguration().getBoolean("Settings.HandleAnimatedMapsOnMainThread");
|
||||
sendAnimatedMapsOnMainThread = config.getConfiguration().getBoolean("Settings.SendAnimatedMapsOnMainThread");
|
||||
|
||||
cacheControlMode = ImageMapCacheControlMode.valueOf(config.getConfiguration().getString("Settings.CacheControlMode"));
|
||||
tryDeleteBlankMapFiles = config.getConfiguration().getBoolean("Settings.TryDeleteBlankMapFiles");
|
||||
|
||||
combinedByDefault = config.getConfiguration().getBoolean("Settings.CombinedByDefault");
|
||||
|
||||
uploadServiceEnabled = config.getConfiguration().getBoolean("UploadService.Enabled");
|
||||
uploadServiceDisplayURL = config.getConfiguration().getString("UploadService.DisplayURL");
|
||||
uploadServiceServerAddress = config.getConfiguration().getString("UploadService.WebServer.Host");
|
||||
uploadServiceServerPort = config.getConfiguration().getInt("UploadService.WebServer.Port");
|
||||
|
||||
imageFrameClientEnabled = config.getConfiguration().getBoolean("ImageFrameClient.Enabled");
|
||||
|
||||
invisibleFrameMaxConversionsPerSplash = config.getConfiguration().getInt("InvisibleFrame.MaxConversionsPerSplash");
|
||||
invisibleFrameGlowEmptyFrames = config.getConfiguration().getBoolean("InvisibleFrame.GlowEmptyFrames");
|
||||
|
||||
storageType = KeyUtils.imageFrameKey(config.getConfiguration().getString("Storage.Type"));
|
||||
storageOptions = config.getConfiguration().getConfigurationSection("Storage.Options").getValues(true).entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString()));
|
||||
|
||||
debugLogging = config.getConfiguration().getBoolean("DebugLogging");
|
||||
|
||||
if (updaterTask != null) {
|
||||
updaterTask.cancel();
|
||||
}
|
||||
updaterEnabled = config.getConfiguration().getBoolean("Updater");
|
||||
if (updaterEnabled) {
|
||||
Bukkit.getPluginManager().registerEvents(new Updater(), this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.api.events;
|
||||
|
||||
import com.loohp.imageframe.objectholders.ImageMap;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static com.loohp.imageframe.utils.ImageUtils.copy;
|
||||
|
||||
public class HDMapPreRespondEvent extends Event {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
private final Player player;
|
||||
private final int mapId;
|
||||
private final ImageMap imageMap;
|
||||
private final MapView mapView;
|
||||
|
||||
private boolean requestAccepted;
|
||||
private BufferedImage image;
|
||||
|
||||
public HDMapPreRespondEvent(Player player, int mapId, ImageMap imageMap, MapView mapView, boolean requestAccepted, BufferedImage image) {
|
||||
super(!Scheduler.isPrimaryThread());
|
||||
this.player = player;
|
||||
this.mapId = mapId;
|
||||
this.imageMap = imageMap;
|
||||
this.mapView = mapView;
|
||||
this.requestAccepted = requestAccepted;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public int getMapId() {
|
||||
return mapId;
|
||||
}
|
||||
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
public boolean isRequestAccepted() {
|
||||
return requestAccepted;
|
||||
}
|
||||
|
||||
public void setRequestAccepted(boolean requestAccepted) {
|
||||
this.requestAccepted = requestAccepted;
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
return image == null ? null : copy(image);
|
||||
}
|
||||
|
||||
public void setImage(BufferedImage image) {
|
||||
this.image = image == null ? null : copy(image);
|
||||
}
|
||||
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.api.events;
|
||||
|
||||
import com.loohp.imageframe.objectholders.ImageMap;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
public class ImageMapAddedEvent extends Event {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
private final ImageMap imageMap;
|
||||
|
||||
public ImageMapAddedEvent(ImageMap imageMap) {
|
||||
super(!Scheduler.isPrimaryThread());
|
||||
this.imageMap = imageMap;
|
||||
}
|
||||
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.api.events;
|
||||
|
||||
import com.loohp.imageframe.objectholders.ImageMap;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
public class ImageMapDeletedEvent extends Event {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
private final ImageMap imageMap;
|
||||
|
||||
public ImageMapDeletedEvent(ImageMap imageMap) {
|
||||
super(!Scheduler.isPrimaryThread());
|
||||
this.imageMap = imageMap;
|
||||
}
|
||||
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.api.events;
|
||||
|
||||
import com.loohp.imageframe.objectholders.ImageMap;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
public class ImageMapUpdatedEvent extends Event {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
private final ImageMap imageMap;
|
||||
|
||||
public ImageMapUpdatedEvent(ImageMap imageMap) {
|
||||
super(!Scheduler.isPrimaryThread());
|
||||
this.imageMap = imageMap;
|
||||
}
|
||||
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2022. Contributors
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
+10
-3
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* This file is part of InteractiveChatDiscordSrvAddon.
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2022. Contributors
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.loohp.imageframe.debug;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
@@ -28,6 +29,12 @@ import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
public class Debug implements Listener {
|
||||
|
||||
public static void debug(String message) {
|
||||
if (ImageFrame.debugLogging) {
|
||||
Bukkit.getConsoleSender().sendMessage("[IMAGEFRAME|DEBUG] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoinPluginActive(PlayerJoinEvent event) {
|
||||
if (event.getPlayer().getName().equals("LOOHP") || event.getPlayer().getName().equals("AppLEshakE")) {
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.hooks.viaversion;
|
||||
|
||||
import com.viaversion.viaversion.api.Via;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class ViaHook {
|
||||
|
||||
public static final int MAP_ID_EXPANSION_VERSION = 346;
|
||||
|
||||
public static boolean isPlayerLegacy(Player player) {
|
||||
int version = Via.getAPI().getPlayerVersion(player.getUniqueId());
|
||||
return version >= 0 && version < MAP_ID_EXPANSION_VERSION;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.invisibleframe;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.nms.NMS;
|
||||
import com.loohp.imageframe.utils.MCVersion;
|
||||
import com.loohp.platformscheduler.ScheduledTask;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.SoundCategory;
|
||||
import org.bukkit.entity.AreaEffectCloud;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.ThrownPotion;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.AreaEffectCloudApplyEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntitySpawnEvent;
|
||||
import org.bukkit.event.entity.ItemSpawnEvent;
|
||||
import org.bukkit.event.entity.LingeringPotionSplashEvent;
|
||||
import org.bukkit.event.entity.PotionSplashEvent;
|
||||
import org.bukkit.event.hanging.HangingBreakEvent;
|
||||
import org.bukkit.event.hanging.HangingPlaceEvent;
|
||||
import org.bukkit.event.inventory.PrepareGrindstoneEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.RecipeChoice;
|
||||
import org.bukkit.inventory.ShapelessRecipe;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.bukkit.potion.PotionType;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class InvisibleFrameManager implements Listener {
|
||||
|
||||
public static final NamespacedKey INVISIBLE_GLOW_ITEM_FRAME_CRAFTING_KEY = new NamespacedKey(ImageFrame.plugin, "invisible_glow_item_frame");
|
||||
public static final NamespacedKey INVISIBLE_KEY = new NamespacedKey(ImageFrame.plugin, "invisible");
|
||||
|
||||
private final Map<UUID, Location> removedInvisibleFrames;
|
||||
private final Set<UUID> knownAreaEffectClouds;
|
||||
|
||||
private final AtomicLong itemFramesMadeInvisible;
|
||||
private final AtomicLong invisibleItemFramesPlaced;
|
||||
|
||||
public InvisibleFrameManager() {
|
||||
this.removedInvisibleFrames = new ConcurrentHashMap<>();
|
||||
this.knownAreaEffectClouds = ConcurrentHashMap.newKeySet();
|
||||
|
||||
this.itemFramesMadeInvisible = new AtomicLong(0);
|
||||
this.invisibleItemFramesPlaced = new AtomicLong(0);
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
|
||||
|
||||
if (ImageFrame.version.isNewerOrEqualTo(MCVersion.V1_17)) {
|
||||
addRecipes();
|
||||
}
|
||||
}
|
||||
|
||||
public AtomicLong getItemFramesMadeInvisible() {
|
||||
return itemFramesMadeInvisible;
|
||||
}
|
||||
|
||||
public AtomicLong getInvisibleItemFramesPlaced() {
|
||||
return invisibleItemFramesPlaced;
|
||||
}
|
||||
|
||||
private void addRecipes() {
|
||||
ItemStack result = new ItemStack(Material.valueOf("GLOW_ITEM_FRAME"));
|
||||
ShapelessRecipe recipe = new ShapelessRecipe(INVISIBLE_GLOW_ITEM_FRAME_CRAFTING_KEY, withInvisibleItemFrameData(result))
|
||||
.addIngredient(Material.valueOf("GLOW_INK_SAC"))
|
||||
.addIngredient(new RecipeChoice.ExactChoice(withInvisibleItemFrameData(new ItemStack(Material.ITEM_FRAME))));
|
||||
Bukkit.addRecipe(recipe);
|
||||
}
|
||||
|
||||
public ItemStack withInvisibleItemFrameData(ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
return itemStack;
|
||||
}
|
||||
if (isInvisibleItemFrame(itemStack)) {
|
||||
return itemStack;
|
||||
}
|
||||
ItemStack modified = NMS.getInstance().withInvisibleItemFrameMeta(itemStack);
|
||||
ItemMeta meta = modified.getItemMeta();
|
||||
if (meta == null) {
|
||||
return itemStack;
|
||||
}
|
||||
meta.getPersistentDataContainer().set(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 1);
|
||||
modified.setItemMeta(meta);
|
||||
return modified;
|
||||
}
|
||||
|
||||
public boolean isInvisibleItemFrame(ItemStack itemStack) {
|
||||
if (itemStack == null || !itemStack.hasItemMeta()) {
|
||||
return false;
|
||||
}
|
||||
ItemMeta meta = itemStack.getItemMeta();
|
||||
if (meta == null) {
|
||||
return false;
|
||||
}
|
||||
return meta.getPersistentDataContainer().getOrDefault(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 0) > 0;
|
||||
}
|
||||
|
||||
public void setInvisibleItemFrameData(Entity entity) {
|
||||
if (entity instanceof ItemFrame) {
|
||||
entity.getPersistentDataContainer().set(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 1);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInvisibleItemFrame(Entity entity) {
|
||||
return entity instanceof ItemFrame && entity.getPersistentDataContainer().getOrDefault(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 0) > 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantValue")
|
||||
public void updateInvisibleItemFrame(ItemFrame itemFrame) {
|
||||
if (!itemFrame.isValid()) {
|
||||
return;
|
||||
}
|
||||
ItemStack itemStack = itemFrame.getItem();
|
||||
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
|
||||
if (ImageFrame.invisibleFrameGlowEmptyFrames) {
|
||||
itemFrame.setGlowing(true);
|
||||
}
|
||||
itemFrame.setVisible(true);
|
||||
} else {
|
||||
itemFrame.setGlowing(false);
|
||||
itemFrame.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onGrindstone(PrepareGrindstoneEvent event) {
|
||||
Inventory inventory = event.getInventory();
|
||||
if (isInvisibleItemFrame(inventory.getItem(0)) || isInvisibleItemFrame(inventory.getItem(1))) {
|
||||
event.setResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlace(HangingPlaceEvent event) {
|
||||
if (!isInvisibleItemFrame(event.getItemStack())) {
|
||||
return;
|
||||
}
|
||||
Entity entity = event.getEntity();
|
||||
Location location = entity.getLocation();
|
||||
setInvisibleItemFrameData(entity);
|
||||
invisibleItemFramesPlaced.incrementAndGet();
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> updateInvisibleItemFrame((ItemFrame) entity), 1, location);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBreak(HangingBreakEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (!isInvisibleItemFrame(entity)) {
|
||||
return;
|
||||
}
|
||||
UUID uuid = entity.getUniqueId();
|
||||
Location location = entity.getLocation();
|
||||
removedInvisibleFrames.put(uuid, entity.getLocation());
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> removedInvisibleFrames.remove(uuid), 1, location);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onItemSpawn(ItemSpawnEvent event) {
|
||||
Item item = event.getEntity();
|
||||
ItemStack itemStack = item.getItemStack();
|
||||
String type = itemStack.getType().name();
|
||||
if (!type.equals("ITEM_FRAME") && !type.equals("GLOW_ITEM_FRAME")) {
|
||||
return;
|
||||
}
|
||||
if (isInvisibleItemFrame(itemStack)) {
|
||||
return;
|
||||
}
|
||||
for (Iterator<Map.Entry<UUID, Location>> itr = removedInvisibleFrames.entrySet().iterator(); itr.hasNext();) {
|
||||
Map.Entry<UUID, Location> entry = itr.next();
|
||||
Location location = entry.getValue();
|
||||
if (location.getWorld().equals(item.getWorld())) {
|
||||
BoundingBox boundingBox = BoundingBox.of(location, 1, 1, 1);
|
||||
if (boundingBox.contains(item.getLocation().toVector())) {
|
||||
ItemStack modified = withInvisibleItemFrameData(itemStack);
|
||||
item.setItemStack(modified);
|
||||
itr.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlaceItem(PlayerInteractEntityEvent event) {
|
||||
Entity entity = event.getRightClicked();
|
||||
if (!isInvisibleItemFrame(entity)) {
|
||||
return;
|
||||
}
|
||||
Location location = entity.getLocation();
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> updateInvisibleItemFrame((ItemFrame) entity), 1, location);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onRemoveItem(EntityDamageByEntityEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (!isInvisibleItemFrame(entity)) {
|
||||
return;
|
||||
}
|
||||
Location location = entity.getLocation();
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> updateInvisibleItemFrame((ItemFrame) entity), 1, location);
|
||||
}
|
||||
|
||||
public ItemStack splitItemStacks(ItemStack itemStack, int maxSize) {
|
||||
if (maxSize < 0) {
|
||||
return null;
|
||||
}
|
||||
int amount = itemStack.getAmount();
|
||||
if (amount <= maxSize) {
|
||||
return null;
|
||||
}
|
||||
itemStack.setAmount(maxSize);
|
||||
ItemStack splitStack = itemStack.clone();
|
||||
splitStack.setAmount(amount - maxSize);
|
||||
return splitStack;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPotionSplash(PotionSplashEvent event) {
|
||||
ThrownPotion thrownPotion = event.getEntity();
|
||||
if (thrownPotion.getEffects().stream().noneMatch(e -> e.getType().equals(PotionEffectType.INVISIBILITY))) {
|
||||
return;
|
||||
}
|
||||
int conversionsRemaining = ImageFrame.invisibleFrameMaxConversionsPerSplash;
|
||||
BoundingBox boundingBox = thrownPotion.getBoundingBox().expand(4.0, 2.0, 4.0);
|
||||
for (Entity entity : thrownPotion.getWorld().getNearbyEntities(boundingBox)) {
|
||||
if (conversionsRemaining == 0) {
|
||||
break;
|
||||
}
|
||||
if (!(entity instanceof Item)) {
|
||||
continue;
|
||||
}
|
||||
Item item = (Item) entity;
|
||||
ItemStack itemStack = item.getItemStack();
|
||||
String type = itemStack.getType().name();
|
||||
if (!type.equals("ITEM_FRAME") && !type.equals("GLOW_ITEM_FRAME")) {
|
||||
continue;
|
||||
}
|
||||
if (isInvisibleItemFrame(itemStack)) {
|
||||
continue;
|
||||
}
|
||||
if (entity.getLocation().distanceSquared(thrownPotion.getLocation()) < 16.0) {
|
||||
ItemStack splitStack = splitItemStacks(itemStack, conversionsRemaining);
|
||||
Location location = item.getLocation();
|
||||
if (itemStack.getAmount() > 0) {
|
||||
itemFramesMadeInvisible.addAndGet(itemStack.getAmount());
|
||||
item.setItemStack(withInvisibleItemFrameData(itemStack));
|
||||
item.getWorld().playSound(item.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, SoundCategory.BLOCKS, 1F, 1F);
|
||||
} else {
|
||||
item.remove();
|
||||
}
|
||||
Vector velocity = null;
|
||||
if (splitStack != null) {
|
||||
Item splitItem = thrownPotion.getWorld().dropItem(location, splitStack);
|
||||
velocity = splitItem.getVelocity();
|
||||
splitItem.setVelocity(new Vector(0, 0, 0));
|
||||
}
|
||||
if (velocity != null) {
|
||||
item.setVelocity(velocity);
|
||||
}
|
||||
if (conversionsRemaining >= 0) {
|
||||
conversionsRemaining = Math.max(0, conversionsRemaining - itemStack.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasInvisibilityEffect(AreaEffectCloud areaEffectCloud) {
|
||||
PotionType baseType = areaEffectCloud.getBasePotionData().getType();
|
||||
if (baseType != null) {
|
||||
if (baseType.equals(PotionType.INVISIBILITY)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return areaEffectCloud.hasCustomEffect(PotionEffectType.INVISIBILITY);
|
||||
}
|
||||
|
||||
private void handleAreaEffectCloud(AreaEffectCloud areaEffectCloud) {
|
||||
UUID uuid = areaEffectCloud.getUniqueId();
|
||||
if (!knownAreaEffectClouds.add(uuid)) {
|
||||
return;
|
||||
}
|
||||
AtomicReference<Runnable> runnableReference = new AtomicReference<>(null);
|
||||
AtomicReference<ScheduledTask> taskReference = new AtomicReference<>(null);
|
||||
runnableReference.set(new Runnable() {
|
||||
int conversionsRemaining = ImageFrame.invisibleFrameMaxConversionsPerSplash;
|
||||
boolean firstTick = true;
|
||||
@Override
|
||||
public void run() {
|
||||
if (firstTick) {
|
||||
firstTick = false;
|
||||
} else if (!areaEffectCloud.isValid()) {
|
||||
ScheduledTask task = taskReference.get();
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
}
|
||||
knownAreaEffectClouds.remove(uuid);
|
||||
return;
|
||||
}
|
||||
taskReference.set(Scheduler.runTaskLater(ImageFrame.plugin, runnableReference.get(), 1, areaEffectCloud.getLocation()));
|
||||
if (!hasInvisibilityEffect(areaEffectCloud)) {
|
||||
return;
|
||||
}
|
||||
if (areaEffectCloud.getTicksLived() % 5 != 0) {
|
||||
return;
|
||||
}
|
||||
BoundingBox boundingBox = areaEffectCloud.getBoundingBox();
|
||||
for (Entity entity : areaEffectCloud.getWorld().getNearbyEntities(boundingBox)) {
|
||||
if (conversionsRemaining == 0) {
|
||||
break;
|
||||
}
|
||||
if (!(entity instanceof Item)) {
|
||||
continue;
|
||||
}
|
||||
Item item = (Item) entity;
|
||||
ItemStack itemStack = item.getItemStack();
|
||||
String type = itemStack.getType().name();
|
||||
if (!type.equals("ITEM_FRAME") && !type.equals("GLOW_ITEM_FRAME")) {
|
||||
continue;
|
||||
}
|
||||
if (isInvisibleItemFrame(itemStack)) {
|
||||
continue;
|
||||
}
|
||||
ItemStack splitStack = splitItemStacks(itemStack, conversionsRemaining);
|
||||
Location location = item.getLocation();
|
||||
if (itemStack.getAmount() > 0) {
|
||||
itemFramesMadeInvisible.addAndGet(itemStack.getAmount());
|
||||
item.setItemStack(withInvisibleItemFrameData(itemStack));
|
||||
item.getWorld().playSound(item.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, SoundCategory.BLOCKS, 1F, 1F);
|
||||
} else {
|
||||
item.remove();
|
||||
}
|
||||
Vector velocity = null;
|
||||
if (splitStack != null) {
|
||||
Item splitItem = areaEffectCloud.getWorld().dropItem(location, splitStack);
|
||||
velocity = splitItem.getVelocity();
|
||||
splitItem.setVelocity(new Vector(0, 0, 0));
|
||||
}
|
||||
if (velocity != null) {
|
||||
item.setVelocity(velocity);
|
||||
}
|
||||
if (conversionsRemaining >= 0) {
|
||||
conversionsRemaining = Math.max(0, conversionsRemaining - itemStack.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
runnableReference.get().run();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPotionLinger(EntitySpawnEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (entity instanceof AreaEffectCloud) {
|
||||
handleAreaEffectCloud((AreaEffectCloud) entity);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPotionLinger(LingeringPotionSplashEvent event) {
|
||||
handleAreaEffectCloud(event.getAreaEffectCloud());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onAreaEffectCloudApply(AreaEffectCloudApplyEvent event) {
|
||||
handleAreaEffectCloud(event.getEntity());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.language;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.debug.Debug;
|
||||
import com.loohp.imageframe.utils.HTTPRequestUtils;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.TranslationArgument;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class LanguageManager {
|
||||
|
||||
public static final String LANGUAGE_META_URL = "https://api.loohpjames.com/spigot/plugins/imageframe/language";
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?s");
|
||||
|
||||
private final File languageFolder;
|
||||
private final Map<String, Map<String, String>> translations;
|
||||
|
||||
public LanguageManager() {
|
||||
this.languageFolder = new File(ImageFrame.plugin.getDataFolder(), "language");
|
||||
this.languageFolder.mkdirs();
|
||||
this.translations = new ConcurrentHashMap<>();
|
||||
|
||||
reloadLanguages();
|
||||
}
|
||||
|
||||
public void reloadLanguages() {
|
||||
downloadTranslations();
|
||||
this.translations.clear();
|
||||
loadTranslations();
|
||||
}
|
||||
|
||||
public Set<String> getLoadedLanguages() {
|
||||
return Collections.unmodifiableSet(translations.keySet());
|
||||
}
|
||||
|
||||
public void downloadTranslations() {
|
||||
try {
|
||||
File languageHashFile = new File(ImageFrame.plugin.getDataFolder(), "language_hashes.json");
|
||||
JsonObject languageHashes;
|
||||
if (languageHashFile.exists()) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(languageHashFile.toPath()), StandardCharsets.UTF_8))) {
|
||||
languageHashes = GSON.fromJson(reader, JsonObject.class);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
languageHashes = new JsonObject();
|
||||
}
|
||||
} else {
|
||||
languageHashes = new JsonObject();
|
||||
}
|
||||
|
||||
JsonObject json;
|
||||
try {
|
||||
Debug.debug("Downloading language meta...");
|
||||
json = HTTPRequestUtils.getJsonResponse(LANGUAGE_META_URL);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to fetch language meta from \"api.loohpjames.com\". This could be an internet issue or \"api.loohpjames.com\" is down. If the plugin functions correctly after this, this error can be ignored.", e);
|
||||
}
|
||||
for (JsonElement element : json.get("languages").getAsJsonArray()) {
|
||||
JsonObject languageObj = element.getAsJsonObject();
|
||||
String language = languageObj.get("language").getAsString();
|
||||
String url = languageObj.get("url").getAsString();
|
||||
String hash = languageObj.get("hash").getAsString();
|
||||
|
||||
String existingHash = languageHashes.has(language) ? languageHashes.get(language).getAsString() : null;
|
||||
if (!hash.equalsIgnoreCase(existingHash)) {
|
||||
Debug.debug("Downloading language " + language + "...");
|
||||
try (
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(HTTPRequestUtils.getInputStream(url), StandardCharsets.UTF_8));
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(new File(languageFolder, language + ".json").toPath()), StandardCharsets.UTF_8));
|
||||
) {
|
||||
reader.lines().forEachOrdered(l -> writer.println(l));
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
new IOException("Unable to download language " + language + " from " + url, e).printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
languageHashes.addProperty(language, hash);
|
||||
}
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(languageHashFile.toPath()), StandardCharsets.UTF_8))) {
|
||||
writer.println(GSON.toJson(languageHashes));
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
new IOException("Unable to save language hashes to " + languageHashFile.getAbsolutePath(), e).printStackTrace();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadTranslations() {
|
||||
for (File file : languageFolder.listFiles()) {
|
||||
String name = file.getName();
|
||||
if (name.endsWith(".json")) {
|
||||
String language = name.substring(0, name.indexOf("."));
|
||||
Map<String, String> translations = new ConcurrentHashMap<>();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), StandardCharsets.UTF_8))) {
|
||||
JsonObject jsonObject = GSON.fromJson(reader, JsonObject.class);
|
||||
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
|
||||
translations.put(entry.getKey(), entry.getValue().getAsString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
new IOException("Unable to load language file " + file.getAbsolutePath(), e).printStackTrace();
|
||||
}
|
||||
this.translations.put(language, translations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getTranslation(String key, String language) {
|
||||
String translation = translations.getOrDefault(language.toLowerCase(), Collections.emptyMap()).get(key);
|
||||
if (translation != null) {
|
||||
return translation;
|
||||
}
|
||||
if (ImageFrame.language.equalsIgnoreCase(language)) {
|
||||
return null;
|
||||
}
|
||||
return getTranslation(key, ImageFrame.language);
|
||||
}
|
||||
|
||||
public List<String> getListTranslation(String key, String language) {
|
||||
return getListTranslation(key, language, Byte.MAX_VALUE);
|
||||
}
|
||||
|
||||
public List<String> getListTranslation(String key, String language, int maxSize) {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (int i = 0; i < maxSize; i++) {
|
||||
String translated = getTranslation(key + "." + i, language);
|
||||
if (translated == null) {
|
||||
return result;
|
||||
}
|
||||
result.add(translated);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Component resolve(Component component, String language) {
|
||||
if (component instanceof TranslatableComponent) {
|
||||
component = convertSingleTranslatable((TranslatableComponent) component, language);
|
||||
}
|
||||
List<Component> children = new ArrayList<>(component.children());
|
||||
children.replaceAll(child -> resolve(child, language));
|
||||
return component.children(children);
|
||||
}
|
||||
|
||||
private Component convertSingleTranslatable(TranslatableComponent component, String language) {
|
||||
String translation = getTranslation(component.key(), language);
|
||||
if (translation == null) {
|
||||
return component;
|
||||
}
|
||||
|
||||
List<TranslationArgument> arguments = component.arguments();
|
||||
Matcher matcher = FORMAT_PATTERN.matcher(translation);
|
||||
|
||||
int lastEnd = 0;
|
||||
int nextUnindexed = 0;
|
||||
List<Component> parts = new ArrayList<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
if (matcher.start() > lastEnd) {
|
||||
String literal = translation.substring(lastEnd, matcher.start());
|
||||
if (!literal.isEmpty()) {
|
||||
parts.add(Component.text(literal));
|
||||
}
|
||||
}
|
||||
String indexGroup = matcher.group(1);
|
||||
int argIndex;
|
||||
if (indexGroup != null) {
|
||||
argIndex = Integer.parseInt(indexGroup) - 1;
|
||||
} else {
|
||||
argIndex = nextUnindexed++;
|
||||
}
|
||||
if (argIndex >= 0 && argIndex < arguments.size()) {
|
||||
parts.add(arguments.get(argIndex).asComponent());
|
||||
} else {
|
||||
parts.add(Component.text(matcher.group()));
|
||||
}
|
||||
lastEnd = matcher.end();
|
||||
}
|
||||
if (lastEnd < translation.length()) {
|
||||
String tail = translation.substring(lastEnd);
|
||||
if (!tail.isEmpty()) {
|
||||
parts.add(Component.text(tail));
|
||||
}
|
||||
}
|
||||
Component result = Component.empty().style(component.style());
|
||||
for (Component part : parts) {
|
||||
result = result.append(part);
|
||||
}
|
||||
for (Component child : component.children()) {
|
||||
result = result.append(child);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.language;
|
||||
|
||||
import com.loohp.imageframe.objectholders.IFPlayerPreference;
|
||||
import com.loohp.imageframe.objectholders.ImageMapAccessPermissionType;
|
||||
import com.loohp.imageframe.objectholders.PreferenceState;
|
||||
|
||||
public class TranslationKey {
|
||||
|
||||
public static final String RELOADED = "imageframe.messages.reloaded";
|
||||
public static final String RESYNC = "imageframe.messages.resync";
|
||||
public static final String STORAGE_MIGRATION = "imageframe.messages.storage_migration";
|
||||
public static final String IMAGE_MAP_PROCESSING = "imageframe.messages.image_map_processing";
|
||||
public static final String IMAGE_MAP_PROCESSING_ACTION_BAR = "imageframe.messages.image_map_processing_action_bar";
|
||||
public static final String IMAGE_MAP_QUEUED_ACTION_BAR = "imageframe.messages.image_map_queued_action_bar";
|
||||
public static final String IMAGE_MAP_CREATED = "imageframe.messages.image_map_created";
|
||||
public static final String IMAGE_MAP_REFRESHED = "imageframe.messages.image_map_refreshed";
|
||||
public static final String IMAGE_MAP_DELETED = "imageframe.messages.image_map_deleted";
|
||||
public static final String IMAGE_MAP_RENAMED = "imageframe.messages.image_map_renamed";
|
||||
public static final String IMAGE_MAP_TOGGLE_PAUSED = "imageframe.messages.image_map_toggle_paused";
|
||||
public static final String IMAGE_MAP_PLAYBACK_JUMP_TO = "imageframe.messages.image_map_playback_jump_to";
|
||||
public static final String IMAGE_MAP_PLAYER_PURGE = "imageframe.messages.image_map_player_purge";
|
||||
public static final String SET_CREATOR = "imageframe.messages.set_creator";
|
||||
public static final String INVALID_OVERLAY_MAP = "imageframe.messages.invalid_overlay_map";
|
||||
public static final String IMAGE_MAP_ALREADY_QUEUED = "imageframe.messages.image_map_already_queued";
|
||||
public static final String UNABLE_TO_LOAD_MAP = "imageframe.messages.unable_to_load_map";
|
||||
public static final String UNABLE_TO_CHANGE_IMAGE_TYPE = "imageframe.messages.unable_to_change_image_type";
|
||||
public static final String UNKNOWN_ERROR = "imageframe.messages.unknown_error";
|
||||
public static final String IMAGE_OVER_MAX_FILE_SIZE = "imageframe.messages.image_over_max_file_size";
|
||||
public static final String NOT_AN_IMAGE_MAP = "imageframe.messages.not_an_image_map";
|
||||
public static final String UPLOAD_LINK = "imageframe.messages.upload_link";
|
||||
public static final String UPLOAD_EXPIRED = "imageframe.messages.upload_expired";
|
||||
|
||||
public static final String URL_IMAGE_MAP_INFO_1 = "imageframe.messages.url_image_map_info.1";
|
||||
public static final String URL_IMAGE_MAP_INFO_2 = "imageframe.messages.url_image_map_info.2";
|
||||
public static final String URL_IMAGE_MAP_INFO_3 = "imageframe.messages.url_image_map_info.3";
|
||||
public static final String URL_IMAGE_MAP_INFO_4 = "imageframe.messages.url_image_map_info.4";
|
||||
public static final String URL_IMAGE_MAP_INFO_5 = "imageframe.messages.url_image_map_info.5";
|
||||
public static final String URL_IMAGE_MAP_INFO_6 = "imageframe.messages.url_image_map_info.6";
|
||||
public static final String URL_IMAGE_MAP_INFO_7 = "imageframe.messages.url_image_map_info.7";
|
||||
public static final String URL_IMAGE_MAP_INFO_8 = "imageframe.messages.url_image_map_info.8";
|
||||
public static final String URL_IMAGE_MAP_INFO_9 = "imageframe.messages.url_image_map_info.9";
|
||||
|
||||
public static final String NO_PERMISSION = "imageframe.messages.no_permission";
|
||||
public static final String NO_CONSOLE = "imageframe.messages.no_console";
|
||||
public static final String PLAYER_NOT_FOUND = "imageframe.messages.player_not_found";
|
||||
public static final String INVALID_USAGE = "imageframe.messages.invalid_usage";
|
||||
public static final String NOT_ENOUGH_MAPS = "imageframe.messages.not_enough_maps";
|
||||
public static final String OVERSIZE = "imageframe.messages.oversize";
|
||||
public static final String URL_RESTRICTED = "imageframe.messages.url_restricted";
|
||||
public static final String PLAYER_CREATION_LIMIT_REACHED = "imageframe.messages.player_creation_limit_reached";
|
||||
public static final String DUPLICATE_MAP_NAME = "imageframe.messages.duplicate_map_name";
|
||||
public static final String MAP_LOOKUP = "imageframe.messages.map_lookup";
|
||||
public static final String ITEM_FRAME_OCCUPIED = "imageframe.messages.item_frame_occupied";
|
||||
public static final String NOT_ENOUGH_SPACE = "imageframe.messages.not_enough_space";
|
||||
public static final String INVALID_IMAGE_MAP = "imageframe.messages.invalid_image_map";
|
||||
public static final String GIVEN_INVISIBLE_FRAME = "imageframe.messages.given_invisible_frame";
|
||||
|
||||
public static final String ACCESS_UPDATED = "imageframe.messages.access_permission.updated";
|
||||
public static String ACCESS_TYPE(ImageMapAccessPermissionType type) {
|
||||
String key = type == null ? "none" : type.name().toLowerCase();
|
||||
return "imageframe.messages.access_permission.types." + key;
|
||||
}
|
||||
|
||||
public static final String SELECTION_BEGIN = "imageframe.messages.selection.begin";
|
||||
public static final String SELECTION_CLEAR = "imageframe.messages.selection.clear";
|
||||
public static final String SELECTION_CORNER1 = "imageframe.messages.selection.corner1";
|
||||
public static final String SELECTION_CORNER2 = "imageframe.messages.selection.corner2";
|
||||
public static final String SELECTION_INVALID = "imageframe.messages.selection.invalid";
|
||||
public static final String SELECTION_OVERSIZE = "imageframe.messages.selection.oversize";
|
||||
public static final String SELECTION_SUCCESS = "imageframe.messages.selection.success";
|
||||
public static final String SELECTION_NO_SELECTION = "imageframe.messages.selection.no_selection";
|
||||
public static final String SELECTION_INCORRECT_SIZE = "imageframe.messages.selection.incorrect_size";
|
||||
|
||||
public static final String MARKERS_ADD_BEGIN = "imageframe.messages.markers.add_begin";
|
||||
public static final String MARKERS_ADD_CONFIRM = "imageframe.messages.markers.add_confirm";
|
||||
public static final String MARKERS_REMOVE = "imageframe.messages.markers.remove";
|
||||
public static final String MARKERS_CLEAR = "imageframe.messages.markers.clear";
|
||||
public static final String MARKERS_CANCEL = "imageframe.messages.markers.cancel";
|
||||
public static final String MARKERS_DUPLICATE_NAME = "imageframe.messages.markers.duplicate_name";
|
||||
public static final String MARKERS_NOT_A_MARKER = "imageframe.messages.markers.not_a_marker";
|
||||
public static final String MARKERS_NOT_RENDER_WARNING = "imageframe.messages.markers.not_render_on_frame_warning";
|
||||
public static final String MARKERS_LIMIT_REACHED = "imageframe.messages.markers.limit_reached";
|
||||
|
||||
public static final String PREFERENCES_UPDATE = "imageframe.messages.preferences.update_message";
|
||||
public static String PREFERENCES_TYPE(IFPlayerPreference<?> type) {
|
||||
return "imageframe.messages.preferences.keys." + type.name().toLowerCase();
|
||||
}
|
||||
public static String PREFERENCES_VALUE(PreferenceState state) {
|
||||
return "imageframe.messages.preferences.values." + state.name().toLowerCase();
|
||||
}
|
||||
|
||||
public static final String COMBINED_ITEM_NAME = "imageframe.settings.combined_map_item.name";
|
||||
public static final String COMBINED_ITEM_LORE_1 = "imageframe.settings.combined_map_item.lore.1";
|
||||
public static final String COMBINED_ITEM_LORE_2 = "imageframe.settings.combined_map_item.lore.2";
|
||||
public static final String COMBINED_ITEM_LORE_3 = "imageframe.settings.combined_map_item.lore.3";
|
||||
public static final String COMBINED_ITEM_LORE_4 = "imageframe.settings.combined_map_item.lore.4";
|
||||
public static final String COMBINED_ITEM_LORE_5 = "imageframe.settings.combined_map_item.lore.5";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.listeners;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import com.loohp.imageframe.utils.ModernEventsUtils;
|
||||
import com.loohp.imageframe.utils.SlotAccessor;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerItemHeldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.EntitiesLoadEvent;
|
||||
import org.bukkit.inventory.EntityEquipment;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import static com.loohp.imageframe.objectholders.CombinedMapItemHandler.containsCombinedMaps;
|
||||
import static com.loohp.imageframe.objectholders.CombinedMapItemHandler.isCombinedMaps;
|
||||
import static com.loohp.imageframe.utils.ImageFilledMapUtils.processImageFilledMap;
|
||||
import static com.loohp.imageframe.utils.ImageFilledMapUtils.processImageFilledMaps;
|
||||
|
||||
public class Events implements Listener {
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onClick(InventoryClickEvent event) {
|
||||
ItemStack currentItem = event.getCurrentItem();
|
||||
MapView currentMapView = MapUtils.getItemMapView(currentItem);
|
||||
if (currentMapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
|
||||
event.setCurrentItem(new ItemStack(Material.MAP, currentItem.getAmount()));
|
||||
}
|
||||
}
|
||||
ItemStack replacement = processImageFilledMap(event.getCurrentItem());
|
||||
if (replacement != null) {
|
||||
event.setCurrentItem(replacement);
|
||||
}
|
||||
|
||||
boolean isClickingTop = event.getView().getTopInventory().equals(event.getClickedInventory());
|
||||
boolean isClickingBottom = event.getView().getBottomInventory().equals(event.getClickedInventory());
|
||||
|
||||
Inventory inventory = event.getView().getTopInventory();
|
||||
switch (inventory.getType()) {
|
||||
case CRAFTING: {
|
||||
if (event.getClickedInventory() != null && InventoryType.CRAFTING.equals(event.getClickedInventory().getType())) {
|
||||
if (isCombinedMaps(event.getCursor())
|
||||
|| (isClickingBottom && event.isShiftClick() && isCombinedMaps(event.getCurrentItem()))
|
||||
|| (isClickingTop && event.getHotbarButton() != -1 && isCombinedMaps(event.getWhoClicked().getInventory().getItem(event.getHotbarButton())))
|
||||
|| (isClickingTop && event.getClick().equals(ClickType.SWAP_OFFHAND) && isCombinedMaps(event.getWhoClicked().getEquipment().getItemInOffHand()))
|
||||
|| containsCombinedMaps(i -> event.getView().getItem(i), 5)) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WORKBENCH: {
|
||||
if ((isClickingTop && isCombinedMaps(event.getCursor()))
|
||||
|| (isClickingBottom && event.isShiftClick() && isCombinedMaps(event.getCurrentItem()))
|
||||
|| (isClickingTop && event.getHotbarButton() != -1 && isCombinedMaps(event.getWhoClicked().getInventory().getItem(event.getHotbarButton())))
|
||||
|| (isClickingTop && event.getClick().equals(ClickType.SWAP_OFFHAND) && isCombinedMaps(event.getWhoClicked().getEquipment().getItemInOffHand()))
|
||||
|| containsCombinedMaps(i -> event.getView().getItem(i), 10)) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
} else if (event.getRawSlot() == 0) {
|
||||
ItemStack map = event.getView().getItem(5);
|
||||
MapView mapView = MapUtils.getItemMapView(map);
|
||||
if (mapView == null) {
|
||||
return;
|
||||
}
|
||||
if (ImageFrame.imageMapManager.getFromMapView(mapView) == null) {
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
for (int i = 1; i <= 9; i++) {
|
||||
if (i == 5) {
|
||||
continue;
|
||||
}
|
||||
ItemStack itemStack = event.getView().getItem(i);
|
||||
if (itemStack != null && itemStack.getType().equals(Material.PAPER)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count >= 8) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CARTOGRAPHY: {
|
||||
if ((isClickingTop && isCombinedMaps(event.getCursor()))
|
||||
|| (isClickingBottom && event.isShiftClick() && isCombinedMaps(event.getCurrentItem()))
|
||||
|| (isClickingTop && event.getHotbarButton() != -1 && isCombinedMaps(event.getWhoClicked().getInventory().getItem(event.getHotbarButton())))
|
||||
|| (isClickingTop && event.getClick().equals(ClickType.SWAP_OFFHAND) && isCombinedMaps(event.getWhoClicked().getEquipment().getItemInOffHand()))
|
||||
|| containsCombinedMaps(i -> event.getView().getItem(i), 3)) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
} else if (event.getRawSlot() == 2) {
|
||||
ItemStack map = event.getView().getItem(0);
|
||||
MapView mapView = MapUtils.getItemMapView(map);
|
||||
if (mapView == null) {
|
||||
return;
|
||||
}
|
||||
if (ImageFrame.imageMapManager.getFromMapView(mapView) == null) {
|
||||
return;
|
||||
}
|
||||
ItemStack item = event.getView().getItem(1);
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
if (item.getType().equals(Material.PAPER) || item.getType().equals(Material.GLASS_PANE)) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDrag(InventoryDragEvent event) {
|
||||
Inventory inventory = event.getView().getTopInventory();
|
||||
int size = inventory.getSize();
|
||||
if (inventory.getType().equals(InventoryType.WORKBENCH) || inventory.getType().equals(InventoryType.CARTOGRAPHY)) {
|
||||
if (containsCombinedMaps(event.getNewItems().values()) && event.getNewItems().keySet().stream().anyMatch(i -> i < size)) {
|
||||
event.setResult(Event.Result.DENY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerSwitchSlot(PlayerItemHeldEvent event) {
|
||||
Inventory inventory = event.getPlayer().getInventory();
|
||||
int slot = event.getNewSlot();
|
||||
ItemStack currentItem = inventory.getItem(slot);
|
||||
MapView currentMapView = MapUtils.getItemMapView(currentItem);
|
||||
if (currentMapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
|
||||
inventory.setItem(slot, new ItemStack(Material.MAP, currentItem.getAmount()));
|
||||
}
|
||||
}
|
||||
ItemStack replacement = processImageFilledMap(inventory.getItem(slot));
|
||||
if (replacement != null) {
|
||||
inventory.setItem(slot, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (event.getAction().equals(Action.PHYSICAL)) {
|
||||
return;
|
||||
}
|
||||
EntityEquipment equipment = event.getPlayer().getEquipment();
|
||||
EquipmentSlot hand = event.getHand();
|
||||
if (hand == null) {
|
||||
return;
|
||||
}
|
||||
ItemStack currentItem = equipment.getItem(hand);
|
||||
MapView currentMapView = MapUtils.getItemMapView(currentItem);
|
||||
if (currentMapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
|
||||
equipment.setItem(hand, new ItemStack(Material.MAP, currentItem.getAmount()));
|
||||
}
|
||||
}
|
||||
ItemStack replacement = processImageFilledMap(equipment.getItem(hand));
|
||||
if (replacement != null) {
|
||||
equipment.setItem(hand, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
|
||||
EntityEquipment equipment = event.getPlayer().getEquipment();
|
||||
EquipmentSlot hand = event.getHand();
|
||||
ItemStack currentItem = equipment.getItem(hand);
|
||||
MapView currentMapView = MapUtils.getItemMapView(currentItem);
|
||||
if (currentMapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
|
||||
equipment.setItem(hand, new ItemStack(Material.MAP, currentItem.getAmount()));
|
||||
}
|
||||
}
|
||||
ItemStack replacement = processImageFilledMap(equipment.getItem(hand));
|
||||
if (replacement != null) {
|
||||
equipment.setItem(hand, replacement);
|
||||
}
|
||||
|
||||
Entity entity = event.getRightClicked();
|
||||
if (entity instanceof ItemFrame) {
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
ItemStack itemStack = itemFrame.getItem();
|
||||
MapView mapView = MapUtils.getItemMapView(itemStack);
|
||||
if (mapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
|
||||
itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onEntityDamage(EntityDamageEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (entity instanceof ItemFrame) {
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
ItemStack itemStack = itemFrame.getItem();
|
||||
MapView mapView = MapUtils.getItemMapView(itemStack);
|
||||
if (mapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
|
||||
itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onEntityPickupItem(EntityPickupItemEvent event) {
|
||||
Item item = event.getItem();
|
||||
ItemStack currentItem = item.getItemStack();
|
||||
MapView currentMapView = MapUtils.getItemMapView(currentItem);
|
||||
if (currentMapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
|
||||
item.setItemStack(new ItemStack(Material.MAP, currentItem.getAmount()));
|
||||
}
|
||||
}
|
||||
ItemStack replacement = processImageFilledMap(item.getItemStack());
|
||||
if (replacement != null) {
|
||||
item.setItemStack(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void onChunkLoad(ChunkLoadEvent event) {
|
||||
if (ModernEventsUtils.modernEventsExists()) {
|
||||
return;
|
||||
}
|
||||
Chunk chunk = event.getChunk();
|
||||
Scheduler.executeOrScheduleSync(ImageFrame.plugin, () -> {
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof ItemFrame) {
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
ItemStack itemStack = itemFrame.getItem();
|
||||
MapView mapView = MapUtils.getItemMapView(itemStack);
|
||||
if (mapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
|
||||
Scheduler.runTask(ImageFrame.plugin, () -> itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false), itemFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, chunk);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
Inventory inventory = event.getPlayer().getInventory();
|
||||
processImageFilledMaps(SlotAccessor.of(i -> inventory.getItem(i), (i, s) -> inventory.setItem(i, s)), inventory.getSize());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
Inventory inventory = event.getPlayer().getInventory();
|
||||
processImageFilledMaps(SlotAccessor.of(i -> inventory.getItem(i), (i, s) -> inventory.setItem(i, s)), inventory.getSize());
|
||||
}
|
||||
|
||||
public static class ModernEvents implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void onEntityLoad(EntitiesLoadEvent event) {
|
||||
for (Entity entity : event.getEntities()) {
|
||||
if (entity instanceof ItemFrame) {
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
ItemStack itemStack = itemFrame.getItem();
|
||||
MapView mapView = MapUtils.getItemMapView(itemStack);
|
||||
if (mapView != null) {
|
||||
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
|
||||
Scheduler.runTask(ImageFrame.plugin, () -> itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false), itemFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.media;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.utils.GifReader;
|
||||
import com.loohp.imageframe.utils.HTTPRequestUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class GifReaderMediaLoader implements MediaLoader {
|
||||
|
||||
private static final Key IDENTIFIER = Key.key("imageframe", "gif_reader");
|
||||
|
||||
@Override
|
||||
public Key getIdentifier() {
|
||||
return IDENTIFIER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldTryRead(String url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<MediaFrame> tryLoad(String url) throws Exception {
|
||||
List<GifReader.ImageFrame> frames = GifReader.readGif(HTTPRequestUtils.getInputStream(url), ImageFrame.maxImageFileSize).get();
|
||||
return frames.stream().map(f -> MediaFrame.animatedFrame(f.getImage(), f.getDelay())).iterator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.media;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.utils.HTTPRequestUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class ImageIOMediaLoader implements MediaLoader {
|
||||
|
||||
private static final Key IDENTIFIER = Key.key("imageframe", "imageio");
|
||||
|
||||
@Override
|
||||
public Key getIdentifier() {
|
||||
return IDENTIFIER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldTryRead(String url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<MediaFrame> tryLoad(String url) throws Exception {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(HTTPRequestUtils.download(url, ImageFrame.maxImageFileSize)));
|
||||
if (image == null) {
|
||||
throw new IOException("ImageIO cannot read media type in " + url);
|
||||
}
|
||||
return MediaLoader.staticImage(image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.media;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class MediaFrame {
|
||||
|
||||
public static MediaFrame staticFrame(BufferedImage image) {
|
||||
return new MediaFrame(image, -1);
|
||||
}
|
||||
|
||||
public static MediaFrame animatedFrame(BufferedImage image, int durationMs) {
|
||||
return new MediaFrame(image, durationMs);
|
||||
}
|
||||
|
||||
private final BufferedImage image;
|
||||
private final int durationMs;
|
||||
|
||||
private MediaFrame(BufferedImage image, int durationMs) {
|
||||
this.image = image;
|
||||
this.durationMs = durationMs;
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public int getDurationMs() {
|
||||
return durationMs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.media;
|
||||
|
||||
import net.kyori.adventure.key.Key;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
public interface MediaLoader {
|
||||
|
||||
static Iterator<MediaFrame> staticImage(BufferedImage image) {
|
||||
return Collections.singletonList(MediaFrame.staticFrame(image)).iterator();
|
||||
}
|
||||
|
||||
Key getIdentifier();
|
||||
|
||||
boolean shouldTryRead(String url);
|
||||
|
||||
Iterator<MediaFrame> tryLoad(String url) throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.media;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public class TimedMediaFrameIterator implements Iterator<BufferedImage> {
|
||||
|
||||
private final Iterator<MediaFrame> backing;
|
||||
private final int stepMs;
|
||||
|
||||
private MediaFrame current;
|
||||
private int elapsedInFrame = 0;
|
||||
private boolean finished = false;
|
||||
|
||||
public TimedMediaFrameIterator(Iterator<MediaFrame> backing, int stepMs) {
|
||||
this.backing = backing;
|
||||
this.stepMs = stepMs;
|
||||
|
||||
if (backing.hasNext()) {
|
||||
this.current = backing.next();
|
||||
} else {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return !finished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
MediaFrame result = current;
|
||||
if (current.getDurationMs() == -1) {
|
||||
return result.getImage();
|
||||
}
|
||||
elapsedInFrame += stepMs;
|
||||
while (current.getDurationMs() != -1 && elapsedInFrame >= current.getDurationMs()) {
|
||||
elapsedInFrame -= current.getDurationMs();
|
||||
if (backing.hasNext()) {
|
||||
current = backing.next();
|
||||
} else {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result.getImage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.metrics;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.objectholders.ImageMap;
|
||||
import com.loohp.imageframe.utils.PlayerUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapCursor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
@SuppressWarnings("Convert2Lambda")
|
||||
public class Charts {
|
||||
|
||||
public static void setup(Metrics metrics) {
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("total_images_created", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
return ImageFrame.imageMapManager.getMaps().size();
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("total_maps_created", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
return ImageFrame.imageMapManager.getMaps().stream().mapToInt(each -> each.getMapViews().size()).sum();
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.AdvancedPie("images_created_by_type_id", new Callable<Map<String, Integer>>() {
|
||||
@Override
|
||||
public Map<String, Integer> call() {
|
||||
Map<String, Integer> valueMap = new HashMap<>();
|
||||
for (ImageMap imageMap : ImageFrame.imageMapManager.getMaps()) {
|
||||
String type = imageMap.getType().asString();
|
||||
valueMap.merge(type, 1, Integer::sum);
|
||||
}
|
||||
return valueMap;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.AdvancedPie("images_created_by_type", new Callable<Map<String, Integer>>() {
|
||||
@Override
|
||||
public Map<String, Integer> call() {
|
||||
Map<String, Integer> valueMap = new HashMap<>();
|
||||
for (ImageMap imageMap : ImageFrame.imageMapManager.getMaps()) {
|
||||
String type = imageMap.getLegacyType();
|
||||
if (type != null) {
|
||||
valueMap.merge(type, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("total_markers_created", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
return ImageFrame.imageMapManager.getMaps().stream().flatMap(each -> each.getMapMarkers().stream()).mapToInt(each -> each.size()).sum();
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.AdvancedPie("markers_created_by_type", new Callable<Map<String, Integer>>() {
|
||||
@Override
|
||||
public Map<String, Integer> call() {
|
||||
Map<String, Integer> valueMap = new HashMap<>();
|
||||
for (ImageMap imageMap : ImageFrame.imageMapManager.getMaps()) {
|
||||
for (Map<String, MapCursor> map : imageMap.getMapMarkers()) {
|
||||
for (MapCursor mapCursor : map.values()) {
|
||||
String type = mapCursor.getType().name().toLowerCase();
|
||||
valueMap.merge(type, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("item_frames_made_invisible_in_last_interval", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
long value = ImageFrame.invisibleFrameManager.getItemFramesMadeInvisible().getAndSet(0);
|
||||
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("invisible_item_frames_placed_in_last_interval", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
long value = ImageFrame.invisibleFrameManager.getInvisibleItemFramesPlaced().getAndSet(0);
|
||||
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("embedded_service_image_uploaded_in_last_interval", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
long value = ImageFrame.imageUploadManager.getImagesUploadedCounter().getAndSet(0);
|
||||
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SimplePie("storage_type", new Callable<String>() {
|
||||
@Override
|
||||
public String call() {
|
||||
return ImageFrame.imageFrameStorage.getLoader().getIdentifier().asString();
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SingleLineChart("players_with_imageframe_client", new Callable<Integer>() {
|
||||
@Override
|
||||
public Integer call() {
|
||||
return ImageFrame.customClientNetworkManager.getPlayers().size();
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.SimplePie("imageframe_language_by_server", new Callable<String>() {
|
||||
@Override
|
||||
public String call() {
|
||||
return ImageFrame.language;
|
||||
}
|
||||
}));
|
||||
|
||||
metrics.addCustomChart(new Metrics.AdvancedPie("imageframe_language_by_players", new Callable<Map<String, Integer>>() {
|
||||
@Override
|
||||
public Map<String, Integer> call() {
|
||||
Map<String, Integer> valueMap = new HashMap<>();
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
String language = PlayerUtils.getPlayerLanguage(player);
|
||||
valueMap.merge(language, 1, Integer::sum);
|
||||
}
|
||||
return valueMap;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
+166
-122
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2022. Contributors
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -34,6 +34,13 @@
|
||||
*/
|
||||
package com.loohp.imageframe.metrics;
|
||||
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
@@ -51,8 +58,8 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
@@ -60,12 +67,6 @@ import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class Metrics {
|
||||
|
||||
@@ -80,7 +81,7 @@ public class Metrics {
|
||||
* @param serviceId The id of the service. It can be found at <a
|
||||
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
|
||||
*/
|
||||
public Metrics(JavaPlugin plugin, int serviceId) {
|
||||
public Metrics(Plugin plugin, int serviceId) {
|
||||
this.plugin = plugin;
|
||||
// Get the config file
|
||||
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
|
||||
@@ -113,21 +114,41 @@ public class Metrics {
|
||||
boolean logErrors = config.getBoolean("logFailedRequests", false);
|
||||
boolean logSentData = config.getBoolean("logSentData", false);
|
||||
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
|
||||
boolean isFolia = false;
|
||||
try {
|
||||
isFolia = Class.forName("io.papermc.paper.threadedregions.RegionizedServer") != null;
|
||||
} catch (Exception e) {
|
||||
}
|
||||
metricsBase =
|
||||
new MetricsBase(
|
||||
new // See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
MetricsBase(
|
||||
"bukkit",
|
||||
serverUUID,
|
||||
serviceId,
|
||||
enabled,
|
||||
this::appendPlatformData,
|
||||
this::appendServiceData,
|
||||
submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
|
||||
isFolia
|
||||
? null
|
||||
: submitDataTask -> Scheduler.runTask(plugin, submitDataTask),
|
||||
plugin::isEnabled,
|
||||
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
|
||||
(message) -> this.plugin.getLogger().log(Level.INFO, message),
|
||||
logErrors,
|
||||
logSentData,
|
||||
logResponseStatusText);
|
||||
logResponseStatusText,
|
||||
false);
|
||||
}
|
||||
|
||||
/** Shuts down the underlying scheduler service. */
|
||||
public void shutdown() {
|
||||
metricsBase.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,13 +194,12 @@ public class Metrics {
|
||||
public static class MetricsBase {
|
||||
|
||||
/** The version of the Metrics class. */
|
||||
public static final String METRICS_VERSION = "3.0.0";
|
||||
|
||||
private static final ScheduledExecutorService scheduler =
|
||||
Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
|
||||
public static final String METRICS_VERSION = "3.1.0";
|
||||
|
||||
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private final String platform;
|
||||
|
||||
private final String serverUuid;
|
||||
@@ -228,6 +248,7 @@ public class Metrics {
|
||||
* @param logErrors Whether or not errors should be logged.
|
||||
* @param logSentData Whether or not the sent data should be logged.
|
||||
* @param logResponseStatusText Whether or not the response status text should be logged.
|
||||
* @param skipRelocateCheck Whether or not the relocate check should be skipped.
|
||||
*/
|
||||
public MetricsBase(
|
||||
String platform,
|
||||
@@ -242,7 +263,22 @@ public class Metrics {
|
||||
Consumer<String> infoLogger,
|
||||
boolean logErrors,
|
||||
boolean logSentData,
|
||||
boolean logResponseStatusText) {
|
||||
boolean logResponseStatusText,
|
||||
boolean skipRelocateCheck) {
|
||||
ScheduledThreadPoolExecutor scheduler =
|
||||
new ScheduledThreadPoolExecutor(
|
||||
1,
|
||||
task -> {
|
||||
Thread thread = new Thread(task, "bStats-Metrics");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
// We want delayed tasks (non-periodic) that will execute in the future to be
|
||||
// cancelled when the scheduler is shutdown.
|
||||
// Otherwise, we risk preventing the server from shutting down even when
|
||||
// MetricsBase#shutdown() is called
|
||||
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
this.scheduler = scheduler;
|
||||
this.platform = platform;
|
||||
this.serverUuid = serverUuid;
|
||||
this.serviceId = serviceId;
|
||||
@@ -256,9 +292,12 @@ public class Metrics {
|
||||
this.logErrors = logErrors;
|
||||
this.logSentData = logSentData;
|
||||
this.logResponseStatusText = logResponseStatusText;
|
||||
checkRelocation();
|
||||
if (!skipRelocateCheck) {
|
||||
checkRelocation();
|
||||
}
|
||||
if (enabled) {
|
||||
// WARNING: Removing the option to opt-out will get your plugin banned from bStats
|
||||
// WARNING: Removing the option to opt-out will get your plugin banned from
|
||||
// bStats
|
||||
startSubmitting();
|
||||
}
|
||||
}
|
||||
@@ -267,6 +306,10 @@ public class Metrics {
|
||||
this.customCharts.add(chart);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
private void startSubmitting() {
|
||||
final Runnable submitTask =
|
||||
() -> {
|
||||
@@ -281,13 +324,14 @@ public class Metrics {
|
||||
this.submitData();
|
||||
}
|
||||
};
|
||||
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
|
||||
// of requests on the
|
||||
// bStats backend. To circumvent this problem, we introduce some randomness into the initial
|
||||
// and second delay.
|
||||
// WARNING: You must not modify and part of this Metrics class, including the submit delay or
|
||||
// frequency!
|
||||
// WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
|
||||
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven
|
||||
// distribution of requests on the
|
||||
// bStats backend. To circumvent this problem, we introduce some randomness into
|
||||
// the initial and second delay.
|
||||
// WARNING: You must not modify and part of this Metrics class, including the
|
||||
// submit delay or frequency!
|
||||
// WARNING: Modifying this code will get your plugin banned on bStats. Just
|
||||
// don't do it!
|
||||
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
|
||||
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
|
||||
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
|
||||
@@ -362,14 +406,14 @@ public class Metrics {
|
||||
// You can use the property to disable the check in your test environment
|
||||
if (System.getProperty("bstats.relocatecheck") == null
|
||||
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
|
||||
// Maven's Relocate is clever and changes strings, too. So we have to use this little
|
||||
// "trick" ... :D
|
||||
// Maven's Relocate is clever and changes strings, too. So we have to use this
|
||||
// little "trick" ... :D
|
||||
final String defaultPackage =
|
||||
new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
|
||||
final String examplePackage =
|
||||
new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
|
||||
// We want to make sure no one just copy & pastes the example and uses the wrong package
|
||||
// names
|
||||
// We want to make sure no one just copy & pastes the example and uses the wrong
|
||||
// package names
|
||||
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|
||||
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
|
||||
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
|
||||
@@ -395,6 +439,72 @@ public class Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdvancedBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, int[]>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, int[]> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
||||
if (entry.getValue().length == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SimplePie extends CustomChart {
|
||||
|
||||
private final Callable<String> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimplePie(String chartId, Callable<String> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
String value = callable.call();
|
||||
if (value == null || value.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DrilldownPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Map<String, Integer>>> callable;
|
||||
@@ -439,9 +549,9 @@ public class Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdvancedPie extends CustomChart {
|
||||
public static class SingleLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
private final Callable<Integer> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
@@ -449,33 +559,19 @@ public class Metrics {
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
int value = callable.call();
|
||||
if (value == 0) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +615,7 @@ public class Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
public static class SimpleBarChart extends CustomChart {
|
||||
public static class AdvancedPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
@@ -529,7 +625,7 @@ public class Metrics {
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
@@ -542,8 +638,18 @@ public class Metrics {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
|
||||
if (entry.getValue() == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
@@ -583,9 +689,9 @@ public class Metrics {
|
||||
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
|
||||
}
|
||||
|
||||
public static class SimplePie extends CustomChart {
|
||||
public static class SimpleBarChart extends CustomChart {
|
||||
|
||||
private final Callable<String> callable;
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
@@ -593,33 +699,7 @@ public class Metrics {
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimplePie(String chartId, Callable<String> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
String value = callable.call();
|
||||
if (value == null || value.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdvancedBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, int[]>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
||||
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
@@ -627,54 +707,18 @@ public class Metrics {
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, int[]> map = callable.call();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
||||
if (entry.getValue().length == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SingleLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Integer> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
int value = callable.call();
|
||||
if (value == 0) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An extremely simple JSON builder.
|
||||
*
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.migration;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.objectholders.DitheringType;
|
||||
import com.loohp.imageframe.objectholders.ImageMapLoaders;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableImageMapCreateInfo;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMap;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMapLoader;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DrMapMigration implements ExternalPluginMigration {
|
||||
|
||||
public static final String PLUGIN_NAME = "DrMap";
|
||||
|
||||
@Override
|
||||
public String externalPluginName() {
|
||||
return PLUGIN_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requirePlayer() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrate(UUID owner) {
|
||||
if (Bukkit.getPluginManager().isPluginEnabled(PLUGIN_NAME)) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] DrMap must be disabled for migration to begin");
|
||||
return;
|
||||
}
|
||||
File migrationMarker = new File(ImageFrame.plugin.getDataFolder().getParent() + "/DrMap/imageframe-migrated.bin");
|
||||
if (migrationMarker.exists()) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.YELLOW + "[ImageFrame] DrMap data already marked as migrated");
|
||||
return;
|
||||
}
|
||||
File folder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/DrMap/images");
|
||||
if (!folder.exists() || !folder.isDirectory()) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] DrMap plugin data folder not found");
|
||||
return;
|
||||
}
|
||||
NonUpdatableStaticImageMapLoader loader = ImageMapLoaders.getLoader(NonUpdatableStaticImageMapLoader.class);
|
||||
for (File file : folder.listFiles()) {
|
||||
try {
|
||||
BufferedImage[] images = new BufferedImage[] {ImageIO.read(file)};
|
||||
int mapId = Integer.parseInt(file.getName().substring(0, file.getName().lastIndexOf(".")));
|
||||
String name = "DrMap_" + mapId;
|
||||
NonUpdatableStaticImageMap imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, name, images, Collections.singletonList(mapId), 1, 1, DitheringType.NEAREST_COLOR, owner)).get();
|
||||
ImageFrame.imageMapManager.addMap(imageMap);
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Migrated DrMap " + file.getName() + " to " + name);
|
||||
} catch (Exception e) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate DrMap " + file.getAbsolutePath());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
try {
|
||||
migrationMarker.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] DrMap migration complete!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.migration;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ExternalPluginMigration {
|
||||
|
||||
String externalPluginName();
|
||||
|
||||
boolean requirePlayer();
|
||||
|
||||
default void migrate() {
|
||||
if (requirePlayer()) {
|
||||
throw new IllegalStateException("Player required!");
|
||||
}
|
||||
migrate(null);
|
||||
}
|
||||
|
||||
void migrate(UUID player);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.migration;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.objectholders.DitheringType;
|
||||
import com.loohp.imageframe.objectholders.ImageMapLoaders;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableImageMapCreateInfo;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMap;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMapLoader;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.simpleyaml.configuration.file.YamlConfiguration;
|
||||
import org.simpleyaml.configuration.serialization.ConfigurationSerializable;
|
||||
import org.simpleyaml.configuration.serialization.ConfigurationSerialization;
|
||||
import org.simpleyaml.configuration.serialization.SerializableAs;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ImageMapMigration implements ExternalPluginMigration {
|
||||
|
||||
public static final String PLUGIN_NAME = "ImageMaps";
|
||||
|
||||
@Override
|
||||
public String externalPluginName() {
|
||||
return PLUGIN_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requirePlayer() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrate(UUID owner) {
|
||||
if (Bukkit.getPluginManager().isPluginEnabled(PLUGIN_NAME)) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageMaps must be disabled for migration to begin");
|
||||
return;
|
||||
}
|
||||
File migrationMarker = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageMaps/imageframe-migrated.bin");
|
||||
if (migrationMarker.exists()) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.YELLOW + "[ImageFrame] ImageMaps data already marked as migrated");
|
||||
return;
|
||||
}
|
||||
File folder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageMaps/images");
|
||||
if (!folder.exists() || !folder.isDirectory()) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageMaps plugin data folder not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
NonUpdatableStaticImageMapLoader loader = ImageMapLoaders.getLoader(NonUpdatableStaticImageMapLoader.class);
|
||||
ConfigurationSerialization.registerClass(ImageMap.class);
|
||||
YamlConfiguration mapConfig = YamlConfiguration.loadConfiguration(new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageMaps/maps.yml"));
|
||||
for (Map.Entry<String, Object> entry : mapConfig.getConfigurationSection("maps").getValues(false).entrySet()) {
|
||||
String key = entry.getKey();
|
||||
try {
|
||||
int mapId = Integer.parseInt(key);
|
||||
ImageMap data = (ImageMap) entry.getValue();
|
||||
String fileName = data.getFilename();
|
||||
BufferedImage[] images = new BufferedImage[] {MapUtils.getSubImage(ImageIO.read(new File(folder, fileName)), data.getX(), data.getY())};
|
||||
String name = "ImageMaps_" + mapId;
|
||||
NonUpdatableStaticImageMap imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, name, images, Collections.singletonList(mapId), 1, 1, DitheringType.NEAREST_COLOR, owner)).get();
|
||||
ImageFrame.imageMapManager.addMap(imageMap);
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Migrated ImageMaps " + key + " to " + name);
|
||||
} catch (Exception e) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate ImageMaps " + key);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
migrationMarker.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] ImageMaps migration complete!");
|
||||
}
|
||||
|
||||
@SerializableAs("ImageMaps.Map")
|
||||
public static class ImageMap implements ConfigurationSerializable {
|
||||
|
||||
private final String filename;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final double scale;
|
||||
|
||||
public ImageMap(String filename, int x, int y, double scale) {
|
||||
this.filename = filename;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public ImageMap(Map<?, ?> map) {
|
||||
this.filename = map.get("image").toString();
|
||||
this.x = (Integer) map.get("x");
|
||||
this.y = (Integer) map.get("y");
|
||||
this.scale = (Double) map.get("scale");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> serialize() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("image", filename);
|
||||
map.put("x", x);
|
||||
map.put("y", y);
|
||||
map.put("scale", scale);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public double getScale() {
|
||||
return scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((filename == null) ? 0 : filename.hashCode());
|
||||
long temp;
|
||||
temp = Double.doubleToLongBits(scale);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
result = prime * result + x;
|
||||
result = prime * result + y;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof ImageMap))
|
||||
return false;
|
||||
ImageMap other = (ImageMap) obj;
|
||||
if (filename == null) {
|
||||
if (other.filename != null)
|
||||
return false;
|
||||
} else if (!filename.equals(other.filename))
|
||||
return false;
|
||||
if (Double.doubleToLongBits(scale) != Double.doubleToLongBits(other.scale))
|
||||
return false;
|
||||
if (x != other.x)
|
||||
return false;
|
||||
if (y != other.y)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.migration;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.objectholders.DitheringType;
|
||||
import com.loohp.imageframe.objectholders.ImageMapLoaders;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableImageMapCreateInfo;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMap;
|
||||
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMapLoader;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ImageOnMapMigration implements ExternalPluginMigration {
|
||||
|
||||
public static final String PLUGIN_NAME = "ImageOnMap";
|
||||
|
||||
@Override
|
||||
public String externalPluginName() {
|
||||
return PLUGIN_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requirePlayer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void migrate(UUID unused) {
|
||||
if (Bukkit.getPluginManager().isPluginEnabled(PLUGIN_NAME)) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageOnMap must be disabled for migration to begin");
|
||||
return;
|
||||
}
|
||||
File migrationMarker = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageOnMap/imageframe-migrated.bin");
|
||||
if (migrationMarker.exists()) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.YELLOW + "[ImageFrame] ImageOnMap data already marked as migrated");
|
||||
return;
|
||||
}
|
||||
File userFolder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageOnMap/maps");
|
||||
if (!userFolder.exists() || !userFolder.isDirectory()) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageOnMap plugin data folder not found");
|
||||
return;
|
||||
}
|
||||
NonUpdatableStaticImageMapLoader loader = ImageMapLoaders.getLoader(NonUpdatableStaticImageMapLoader.class);
|
||||
File imageFolder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageOnMap/images");
|
||||
for (File file : userFolder.listFiles()) {
|
||||
String fileNameWithoutExtension = file.getName().substring(0, file.getName().lastIndexOf("."));
|
||||
UUID owner;
|
||||
try {
|
||||
owner = UUID.fromString(fileNameWithoutExtension);
|
||||
} catch (IllegalArgumentException e) {
|
||||
owner = Bukkit.getOfflinePlayer(fileNameWithoutExtension).getUniqueId();
|
||||
}
|
||||
try {
|
||||
YamlFile yaml = YamlFile.loadConfiguration(file);
|
||||
List<Map<String, Object>> mapList = (List<Map<String, Object>>) yaml.getList("PlayerMapStore.mapList");
|
||||
if (mapList == null) {
|
||||
continue;
|
||||
}
|
||||
int index = 0;
|
||||
for (Map<String, Object> section : mapList) {
|
||||
try {
|
||||
String name = ((String) section.get("name")).replace(" ", "_");
|
||||
String iomId = (String) section.get("id");
|
||||
String type = (String) section.get("type");
|
||||
BufferedImage[] images;
|
||||
List<Integer> mapIds;
|
||||
int width;
|
||||
int height;
|
||||
if (type.equalsIgnoreCase("SINGLE")) {
|
||||
int mapId = (int) section.get("mapID");
|
||||
mapIds = Collections.singletonList(mapId);
|
||||
images = new BufferedImage[] {ImageIO.read(new File(imageFolder, "map" + mapId + ".png"))};
|
||||
width = 1;
|
||||
height = 1;
|
||||
} else if (type.equalsIgnoreCase("POSTER")) {
|
||||
mapIds = new ArrayList<>((List<Integer>) section.get("mapsIDs"));
|
||||
images = new BufferedImage[mapIds.size()];
|
||||
width = (int) section.get("columns");
|
||||
height = (int) section.get("rows");
|
||||
for (int i = 0; i < images.length; i++) {
|
||||
images[i] = ImageIO.read(new File(imageFolder, "map" + mapIds.get(i) + ".png"));
|
||||
}
|
||||
} else {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate " + name + " ImageOnMap user file " + file.getName() + ": Unknown type " + type);
|
||||
continue;
|
||||
}
|
||||
NonUpdatableStaticImageMap imageMap;
|
||||
if (ImageFrame.imageMapManager.getFromCreator(owner, name) == null) {
|
||||
imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, name, images, mapIds, width, height, DitheringType.NEAREST_COLOR, owner)).get();
|
||||
} else if (ImageFrame.imageMapManager.getFromCreator(owner, iomId) == null) {
|
||||
imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, iomId, images, mapIds, width, height, DitheringType.NEAREST_COLOR, owner)).get();
|
||||
} else {
|
||||
imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, "ImageOnMap-" + iomId, images, mapIds, width, height, DitheringType.NEAREST_COLOR, owner)).get();
|
||||
}
|
||||
ImageFrame.imageMapManager.addMap(imageMap);
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Migrated ImageOnMap " + file.getName() + " to " + name + " of " + owner);
|
||||
} catch (Exception e) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate ImageOnMap " + file.getName() + " of index " + index);
|
||||
e.printStackTrace();
|
||||
}
|
||||
index++;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate ImageOnMap user file " + file.getAbsolutePath());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
try {
|
||||
migrationMarker.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] ImageOnMap migration complete!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.migration;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PluginMigrationRegistry {
|
||||
|
||||
private static final Map<String, ExternalPluginMigration> REGISTERED_MIGRATIONS = new ConcurrentHashMap<>();
|
||||
|
||||
public static final DrMapMigration DR_MAP_MIGRATION = register(new DrMapMigration());
|
||||
public static final ImageOnMapMigration IMAGE_ON_MAP_MIGRATION = register(new ImageOnMapMigration());
|
||||
public static final ImageMapMigration IMAGE_MAP_MIGRATION = register(new ImageMapMigration());
|
||||
|
||||
public static Set<String> getPluginNames() {
|
||||
return REGISTERED_MIGRATIONS.keySet();
|
||||
}
|
||||
|
||||
public static ExternalPluginMigration getMigration(String plugin) {
|
||||
ExternalPluginMigration migration = REGISTERED_MIGRATIONS.get(plugin);
|
||||
if (migration != null) {
|
||||
return migration;
|
||||
}
|
||||
return REGISTERED_MIGRATIONS.entrySet().stream()
|
||||
.filter(e -> e.getKey().equalsIgnoreCase(plugin))
|
||||
.findFirst()
|
||||
.map(e -> e.getValue())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static <T extends ExternalPluginMigration> T register(T migration) {
|
||||
REGISTERED_MIGRATIONS.put(migration.externalPluginName(), migration);
|
||||
return migration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.nms;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class NMS {
|
||||
|
||||
private static NMSWrapper instance;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public synchronized static NMSWrapper getInstance() {
|
||||
if (instance != null) {
|
||||
return instance;
|
||||
}
|
||||
try {
|
||||
Class<NMSWrapper> nmsImplClass = (Class<NMSWrapper>) Class.forName("com.loohp.imageframe.nms." + ImageFrame.version.name());
|
||||
return instance = nmsImplClass.getConstructor().newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
|
||||
if (ImageFrame.version.isSupported()) {
|
||||
throw new RuntimeException("Missing NMSWrapper implementation for version " + ImageFrame.version.name(), e);
|
||||
} else {
|
||||
throw new RuntimeException("No NMSWrapper implementation for UNSUPPORTED version " + ImageFrame.version.name(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
|
||||
import com.loohp.imageframe.hooks.viaversion.ViaHook;
|
||||
import com.loohp.imageframe.nms.NMS;
|
||||
import com.loohp.imageframe.utils.FakeItemUtils;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import com.loohp.imageframe.utils.ModernEventsUtils;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import com.loohp.platformscheduler.platform.folia.FoliaScheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityTeleportEvent;
|
||||
import org.bukkit.event.hanging.HangingPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.EntitiesLoadEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class AnimatedFakeMapManager implements Listener, Runnable {
|
||||
|
||||
private final Map<UUID, TrackedItemFrameData> itemFrames;
|
||||
private final Map<Player, Set<Integer>> knownMapIds;
|
||||
private final Map<Player, Set<Integer>> pendingKnownMapIds;
|
||||
|
||||
public AnimatedFakeMapManager() {
|
||||
this.itemFrames = new ConcurrentHashMap<>();
|
||||
this.knownMapIds = new ConcurrentHashMap<>();
|
||||
this.pendingKnownMapIds = new ConcurrentHashMap<>();
|
||||
Scheduler.runTaskTimerAsynchronously(ImageFrame.plugin, this, 0, 1);
|
||||
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
|
||||
if (ModernEventsUtils.modernEventsExists()) {
|
||||
Bukkit.getPluginManager().registerEvents(new ModernEvents(), ImageFrame.plugin);
|
||||
}
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
for (Entity entity : world.getEntities()) {
|
||||
Scheduler.executeOrScheduleSync(ImageFrame.plugin, () -> handleEntity(entity), entity);
|
||||
}
|
||||
}
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
knownMapIds.put(player, ConcurrentHashMap.newKeySet());
|
||||
pendingKnownMapIds.put(player, ConcurrentHashMap.newKeySet());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<UUID, CompletableFuture<ItemFrameInfo>> collectItemFramesInfo(boolean async) {
|
||||
boolean isFolia = Scheduler.getPlatform() instanceof FoliaScheduler;
|
||||
Map<UUID, CompletableFuture<ItemFrameInfo>> futures = new HashMap<>();
|
||||
for (Map.Entry<UUID, TrackedItemFrameData> entry : itemFrames.entrySet()) {
|
||||
UUID uuid = entry.getKey();
|
||||
ItemFrame itemFrame = entry.getValue().getItemFrame();
|
||||
CompletableFuture<ItemFrameInfo> future = new CompletableFuture<>();
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
if (itemFrame.isValid()) {
|
||||
Set<Player> trackedPlayers;
|
||||
trackedPlayers = NMS.getInstance().getEntityTrackers(itemFrame);
|
||||
future.complete(new ItemFrameInfo(itemFrame.getEntityId(), trackedPlayers, itemFrame.getItem()));
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
};
|
||||
if (async && !isFolia) {
|
||||
task.run();
|
||||
} else {
|
||||
Scheduler.executeOrScheduleSync(ImageFrame.plugin, task, itemFrame);
|
||||
}
|
||||
futures.put(uuid, future);
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Map<UUID, CompletableFuture<ItemFrameInfo>> entityTrackers = collectItemFramesInfo(!ImageFrame.handleAnimatedMapsOnMainThread);
|
||||
Map<Player, List<FakeItemUtils.ItemFrameUpdateData>> updateData = new HashMap<>();
|
||||
long deadline = System.currentTimeMillis() + 2000;
|
||||
for (Map.Entry<UUID, CompletableFuture<ItemFrameInfo>> entry : entityTrackers.entrySet()) {
|
||||
UUID uuid = entry.getKey();
|
||||
ItemFrameInfo frameInfo;
|
||||
try {
|
||||
frameInfo = entry.getValue().get(Math.max(0, deadline - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
frameInfo = null;
|
||||
}
|
||||
if (frameInfo == null) {
|
||||
itemFrames.remove(uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int entityId = frameInfo.getEntityId();
|
||||
Set<Player> players = frameInfo.getTrackedPlayers();
|
||||
ItemStack itemStack = frameInfo.getItemStack();
|
||||
|
||||
TrackedItemFrameData data = itemFrames.get(uuid);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AnimationData animationData = data.getAnimationData();
|
||||
MapView mapView = MapUtils.getItemMapView(itemStack);
|
||||
|
||||
if (mapView == null) {
|
||||
data.setAnimationData(AnimationData.EMPTY);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (animationData.isEmpty() || !animationData.getMapView().equals(mapView)) {
|
||||
ImageMap map = ImageFrame.imageMapManager.getFromMapView(mapView);
|
||||
if (map == null || !map.requiresAnimationService()) {
|
||||
if (!animationData.isEmpty()) {
|
||||
data.setAnimationData(AnimationData.EMPTY);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
data.setAnimationData(animationData = new AnimationData(map, mapView, map.getMapViews().indexOf(mapView)));
|
||||
} else if (!animationData.isEmpty()) {
|
||||
if (!animationData.getImageMap().isValid()) {
|
||||
for (Player player : players) {
|
||||
FakeItemUtils.sendFakeItemChange(player, entityId, itemStack);
|
||||
}
|
||||
data.setAnimationData(AnimationData.EMPTY);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ImageMap imageMap = animationData.getImageMap();
|
||||
|
||||
if (!imageMap.requiresAnimationService()) {
|
||||
data.setAnimationData(AnimationData.EMPTY);
|
||||
continue;
|
||||
}
|
||||
int index = animationData.getIndex();
|
||||
int currentPosition = imageMap.getCurrentPositionInSequenceWithOffset();
|
||||
int mapId = imageMap.getAnimationFakeMapId(currentPosition, index, imageMap.isAnimationPaused());
|
||||
if (mapId < 0) {
|
||||
continue;
|
||||
}
|
||||
Set<Player> requiresSending = new HashSet<>();
|
||||
Set<Player> needReset = new HashSet<>();
|
||||
for (Iterator<Player> itr = players.iterator(); itr.hasNext();) {
|
||||
Player player = itr.next();
|
||||
MapMarkerEditManager.MapMarkerEditData edit = ImageFrame.mapMarkerEditManager.getActiveEditing(player);
|
||||
if (edit != null && Objects.equals(edit.getImageMap(), imageMap)) {
|
||||
needReset.add(player);
|
||||
itr.remove();
|
||||
continue;
|
||||
}
|
||||
Set<Integer> knownIds = knownMapIds.get(player);
|
||||
Set<Integer> pendingKnownIds = pendingKnownMapIds.get(player);
|
||||
if (knownIds != null && !knownIds.contains(mapId)) {
|
||||
if (pendingKnownIds != null && !pendingKnownIds.contains(mapId)) {
|
||||
Set<Integer> fakeMapIds = imageMap.getFakeMapIds();
|
||||
if (fakeMapIds != null) {
|
||||
pendingKnownIds.addAll(fakeMapIds);
|
||||
requiresSending.add(player);
|
||||
}
|
||||
}
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
if (!requiresSending.isEmpty()) {
|
||||
imageMap.sendAnimationFakeMaps(requiresSending, (p, i, r) -> {
|
||||
Set<Integer> pendingKnownIds = pendingKnownMapIds.get(p);
|
||||
if (pendingKnownIds != null && pendingKnownIds.remove(i) && r) {
|
||||
Set<Integer> knownIds = knownMapIds.get(p);
|
||||
if (knownIds != null) {
|
||||
knownIds.add(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!needReset.isEmpty()) {
|
||||
FakeItemUtils.ItemFrameUpdateData itemFrameUpdateData = new FakeItemUtils.ItemFrameUpdateData(entityId, itemStack, mapView.getId(), mapView, currentPosition);
|
||||
needReset.forEach(p -> updateData.computeIfAbsent(p, k -> new ArrayList<>()).add(itemFrameUpdateData));
|
||||
}
|
||||
FakeItemUtils.ItemFrameUpdateData itemFrameUpdateData = new FakeItemUtils.ItemFrameUpdateData(entityId, getMapItem(mapId), mapView.getId(), mapView, currentPosition);
|
||||
players.forEach(p -> updateData.computeIfAbsent(p, k -> new ArrayList<>()).add(itemFrameUpdateData));
|
||||
}
|
||||
Map<Player, List<Runnable>> sendingTasks = new HashMap<>();
|
||||
for (Map.Entry<Player, List<FakeItemUtils.ItemFrameUpdateData>> entry : updateData.entrySet()) {
|
||||
Player player = entry.getKey();
|
||||
if (ImageFrame.ifPlayerManager.getIFPlayer(player.getUniqueId()).getPreference(IFPlayerPreference.VIEW_ANIMATED_MAPS, BooleanState.class).getCalculatedValue(() -> ImageFrame.getPreferenceUnsetValue(player, IFPlayerPreference.VIEW_ANIMATED_MAPS).getRawValue(true))) {
|
||||
if (ImageFrame.viaHook && ViaHook.isPlayerLegacy(player)) {
|
||||
if (!ImageFrame.viaDisableSmoothAnimationForLegacyPlayers) {
|
||||
List<FakeItemUtils.ItemFrameUpdateData> list = entry.getValue();
|
||||
for (FakeItemUtils.ItemFrameUpdateData data : list) {
|
||||
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> MapUtils.sendImageMap(data.getRealMapId(), data.getMapView(), data.getCurrentPosition(), Collections.singleton(player), true));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> FakeItemUtils.sendFakeItemChange(player, entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (ImageFrame.ifPlayerManager.getIFPlayer(player.getUniqueId()).getPreference(IFPlayerPreference.VIEW_ANIMATED_MAPS, BooleanState.class).getCalculatedValue(() -> ImageFrame.getPreferenceUnsetValue(player, IFPlayerPreference.VIEW_ANIMATED_MAPS).getRawValue(true))) {
|
||||
ItemStack mainhand = player.getEquipment().getItemInMainHand();
|
||||
ItemStack offhand = player.getEquipment().getItemInOffHand();
|
||||
MapView mainHandView = MapUtils.getItemMapView(mainhand);
|
||||
MapView offhandView = MapUtils.getItemMapView(offhand);
|
||||
if (mainHandView != null) {
|
||||
ImageMap mainHandMap = ImageFrame.imageMapManager.getFromMapView(mainHandView);
|
||||
if (mainHandMap != null && mainHandMap.requiresAnimationService()) {
|
||||
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> mainHandMap.send(player));
|
||||
}
|
||||
}
|
||||
if (offhandView != null && !offhandView.equals(mainHandView)) {
|
||||
ImageMap offHandMap = ImageFrame.imageMapManager.getFromMapView(offhandView);
|
||||
if (offHandMap != null && offHandMap.requiresAnimationService()) {
|
||||
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> offHandMap.send(player));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImageFrame.sendAnimatedMapsOnMainThread) {
|
||||
for (Map.Entry<Player, List<Runnable>> entry : sendingTasks.entrySet()) {
|
||||
Scheduler.runTask(ImageFrame.plugin, () -> entry.getValue().forEach(Runnable::run), entry.getKey());
|
||||
}
|
||||
} else {
|
||||
sendingTasks.values().forEach(l -> l.forEach(Runnable::run));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private ItemStack getMapItem(int mapId) {
|
||||
ItemStack itemStack = new ItemStack(Material.FILLED_MAP);
|
||||
MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
|
||||
mapMeta.setMapId(mapId);
|
||||
itemStack.setItemMeta(mapMeta);
|
||||
return itemStack;
|
||||
}
|
||||
|
||||
private void handleEntity(Entity entity) {
|
||||
if (!(entity instanceof ItemFrame)) {
|
||||
return;
|
||||
}
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
UUID uuid = itemFrame.getUniqueId();
|
||||
if (itemFrames.containsKey(uuid)) {
|
||||
return;
|
||||
}
|
||||
MapView mapView = MapUtils.getItemMapView(itemFrame.getItem());
|
||||
if (mapView == null) {
|
||||
itemFrames.put(uuid, new TrackedItemFrameData(itemFrame, AnimationData.EMPTY));
|
||||
return;
|
||||
}
|
||||
ImageMap map = ImageFrame.imageMapManager.getFromMapView(mapView);
|
||||
if (map == null || !map.requiresAnimationService()) {
|
||||
itemFrames.put(uuid, new TrackedItemFrameData(itemFrame, AnimationData.EMPTY));
|
||||
return;
|
||||
}
|
||||
itemFrames.put(uuid, new TrackedItemFrameData(itemFrame, new AnimationData(map, mapView, map.getMapViews().indexOf(mapView))));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onChunkLoad(ChunkLoadEvent event) {
|
||||
Chunk chunk = event.getChunk();
|
||||
Scheduler.executeOrScheduleSync(ImageFrame.plugin, () -> {
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
handleEntity(entity);
|
||||
}
|
||||
}, chunk);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onHangingPlace(HangingPlaceEvent event) {
|
||||
handleEntity(event.getEntity());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerInteract(PlayerInteractEntityEvent event) {
|
||||
handleEntity(event.getRightClicked());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
knownMapIds.put(player, ConcurrentHashMap.newKeySet());
|
||||
pendingKnownMapIds.put(player, ConcurrentHashMap.newKeySet());
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
knownMapIds.remove(player);
|
||||
pendingKnownMapIds.remove(player);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onEntityTeleport(EntityTeleportEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (entity instanceof ItemFrame) {
|
||||
UUID uuid = entity.getUniqueId();
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
TrackedItemFrameData data = itemFrames.remove(uuid);
|
||||
if (data != null) {
|
||||
itemFrames.put(uuid, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onImageMapUpdate(ImageMapUpdatedEvent event) {
|
||||
ImageMap imageMap = event.getImageMap();
|
||||
if (!imageMap.requiresAnimationService()) {
|
||||
return;
|
||||
}
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
Set<Integer> ids = imageMap.getFakeMapIds();
|
||||
if (ids != null) {
|
||||
for (Set<Integer> knownIds : knownMapIds.values()) {
|
||||
knownIds.removeAll(ids);
|
||||
}
|
||||
for (Set<Integer> pendingKnownIds : pendingKnownMapIds.values()) {
|
||||
pendingKnownIds.removeAll(ids);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public class ModernEvents implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onEntityLoad(EntitiesLoadEvent event) {
|
||||
for (Entity entity : event.getEntities()) {
|
||||
handleEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TrackedItemFrameData {
|
||||
|
||||
private final ItemFrame itemFrame;
|
||||
private AnimationData animationData;
|
||||
|
||||
public TrackedItemFrameData(ItemFrame itemFrame, AnimationData animationData) {
|
||||
this.itemFrame = itemFrame;
|
||||
this.animationData = animationData;
|
||||
}
|
||||
|
||||
public ItemFrame getItemFrame() {
|
||||
return itemFrame;
|
||||
}
|
||||
|
||||
public AnimationData getAnimationData() {
|
||||
return animationData;
|
||||
}
|
||||
|
||||
public void setAnimationData(AnimationData animationData) {
|
||||
this.animationData = animationData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class AnimationData {
|
||||
|
||||
public static final AnimationData EMPTY = new AnimationData(null, null, -1);
|
||||
|
||||
private final ImageMap imageMap;
|
||||
private final MapView mapView;
|
||||
private final int index;
|
||||
|
||||
public AnimationData(ImageMap imageMap, MapView mapView, int index) {
|
||||
this.imageMap = imageMap;
|
||||
this.mapView = mapView;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return imageMap == null;
|
||||
}
|
||||
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ItemFrameInfo {
|
||||
|
||||
private final int entityId;
|
||||
private final Set<Player> trackedPlayers;
|
||||
private final ItemStack itemStack;
|
||||
|
||||
public ItemFrameInfo(int entityId, Set<Player> trackedPlayers, ItemStack itemStack) {
|
||||
this.entityId = entityId;
|
||||
this.trackedPlayers = trackedPlayers;
|
||||
this.itemStack = itemStack;
|
||||
}
|
||||
|
||||
public int getEntityId() {
|
||||
return entityId;
|
||||
}
|
||||
|
||||
public Set<Player> getTrackedPlayers() {
|
||||
return trackedPlayers;
|
||||
}
|
||||
|
||||
public ItemStack getItemStack() {
|
||||
return itemStack;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class BlockPosition {
|
||||
|
||||
private final WeakReference<World> world;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
|
||||
public BlockPosition(World world, int x, int y, int z) {
|
||||
this.world = new WeakReference<>(world);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public boolean isWorldLoaded() {
|
||||
World world = this.world.get();
|
||||
return world != null && Bukkit.getWorld(world.getUID()) != null;
|
||||
}
|
||||
|
||||
public World getWorld() {
|
||||
return world.get();
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public Block getBlock() {
|
||||
return getWorld().getBlockAt(x, y, z);
|
||||
}
|
||||
|
||||
public Location toLocation() {
|
||||
return new Location(getWorld(), x, y, z);
|
||||
}
|
||||
|
||||
public Vector toVector() {
|
||||
return new Vector(x, y, z);
|
||||
}
|
||||
|
||||
public BoundingBox toBoundingBox() {
|
||||
return new BoundingBox(x, y, z, x + 1, y + 1, z + 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum BooleanState implements UnsetState, PreferenceState {
|
||||
|
||||
TRUE(new JsonPrimitive(true), true, NamedTextColor.GREEN),
|
||||
FALSE(new JsonPrimitive(false), false, NamedTextColor.RED),
|
||||
UNSET(UNSET_JSON, null, NamedTextColor.GRAY);
|
||||
|
||||
public static final Map<String, BooleanState> STRING_VALUES_MAP = Collections.unmodifiableMap(Arrays.stream(values()).collect(Collectors.toMap(e -> e.toString(), e -> e, (x, y) -> y, LinkedHashMap::new)));
|
||||
public static final Map<Boolean, BooleanState> BOOLEAN_VALUES_MAP = Collections.unmodifiableMap(Arrays.stream(values()).collect(Collectors.toMap(e -> e.booleanValue, e -> e, (x, y) -> y, LinkedHashMap::new)));
|
||||
|
||||
private final JsonElement jsonValue;
|
||||
private final Boolean booleanValue;
|
||||
private final TextColor displayColor;
|
||||
|
||||
BooleanState(JsonElement jsonValue, Boolean booleanValue, TextColor displayColor) {
|
||||
this.jsonValue = jsonValue;
|
||||
this.booleanValue = booleanValue;
|
||||
this.displayColor = displayColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnset() {
|
||||
return this.equals(UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement getJsonValue() {
|
||||
return jsonValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextColor getDisplayColor() {
|
||||
return displayColor;
|
||||
}
|
||||
|
||||
public Boolean getRawValue() {
|
||||
return booleanValue;
|
||||
}
|
||||
|
||||
public boolean getRawValue(boolean unsetIsTrue) {
|
||||
return booleanValue == null ? unsetIsTrue : booleanValue;
|
||||
}
|
||||
|
||||
public boolean getCalculatedValue(BooleanSupplier unsetValueFunction) {
|
||||
return booleanValue == null ? unsetValueFunction.getAsBoolean() : booleanValue;
|
||||
}
|
||||
|
||||
public static BooleanState fromBoolean(boolean value) {
|
||||
return fromBoolean((Boolean) value);
|
||||
}
|
||||
|
||||
public static BooleanState fromBoolean(Boolean value) {
|
||||
return BOOLEAN_VALUES_MAP.getOrDefault(value, UNSET);
|
||||
}
|
||||
|
||||
public static BooleanState fromString(String value) {
|
||||
return STRING_VALUES_MAP.getOrDefault(value.toLowerCase(), UNSET);
|
||||
}
|
||||
|
||||
public static BooleanState fromJsonValue(JsonElement jsonValue) {
|
||||
if (jsonValue.isJsonPrimitive()) {
|
||||
JsonPrimitive jsonPrimitive = jsonValue.getAsJsonPrimitive();
|
||||
if (jsonPrimitive.isBoolean()) {
|
||||
return fromBoolean(jsonPrimitive.getAsBoolean());
|
||||
} else if (jsonPrimitive.isNumber()) {
|
||||
return fromBoolean(jsonPrimitive.getAsNumber().intValue() != 0);
|
||||
} else if (jsonPrimitive.isString()) {
|
||||
return fromString(jsonPrimitive.getAsString());
|
||||
}
|
||||
}
|
||||
return UNSET;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.media.MediaFrame;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.imageframe.utils.GifReader;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Animated (GIF) counterpart of {@link BundledStaticImageMap}: the GIF is a
|
||||
* jar resource and the Minecraft map ids are pinned in maps.yml.
|
||||
*/
|
||||
public class BundledAnimatedImageMap extends URLAnimatedImageMap implements BundledImageMap {
|
||||
|
||||
protected BundledAnimatedImageMap(ImageMapManager manager, ImageMapLoader<?, ?> loader, String name, String resourcePath, LazyMappedBufferedImage[][] cachedImages, List<MapView> mapViews, List<Integer> mapIds, List<Map<String, MapCursor>> mapMarkers, int width, int height, DitheringType ditheringType, long creationTime) {
|
||||
super(manager, loader, -1, name, resourcePath, cachedImages, mapViews, mapIds, mapMarkers, width, height, ditheringType, ImageMap.CONSOLE_CREATOR, Collections.emptyMap(), creationTime, -1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUrl(String url) {
|
||||
//bundled maps always render from their jar resource, the path cannot change at runtime
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Iterator<MediaFrame> loadMedia() throws Exception {
|
||||
List<GifReader.ImageFrame> frames = GifReader.readGif(BundledStaticImageMap.openResource(getResourcePath()), Long.MAX_VALUE).get();
|
||||
return frames.stream().map(f -> MediaFrame.animatedFrame(f.getImage(), f.getDelay())).iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ImageFrameStorage storage, boolean saveAsCopy) {
|
||||
//the jar is the canonical storage, nothing to save
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageMap deepClone(String name, UUID creator) {
|
||||
throw new UnsupportedOperationException("bundled maps cannot be cloned");
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.utils.FutureUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapRenderer;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class BundledAnimatedImageMapLoader extends ImageMapLoader<BundledAnimatedImageMap, BundledMapCreateInfo> {
|
||||
|
||||
private static final Key IDENTIFIER = Key.key("imageframe", "bundled_animated");
|
||||
|
||||
@Override
|
||||
public Key getIdentifier() {
|
||||
return IDENTIFIER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<BundledAnimatedImageMap> getImageMapClass() {
|
||||
return BundledAnimatedImageMap.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<BundledMapCreateInfo> getImageMapCreateInfoClass() {
|
||||
return BundledMapCreateInfo.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getExtraPermissions() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(String imageType) {
|
||||
//bundled maps are only created from maps.yml in the jar, never through commands
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<BundledAnimatedImageMap> create(BundledMapCreateInfo createInfo) throws Exception {
|
||||
int mapsCount = createInfo.getWidth() * createInfo.getHeight();
|
||||
List<MapView> mapViews = BundledStaticImageMapLoader.getPinnedMapViews(createInfo);
|
||||
List<Integer> mapIds = new ArrayList<>(mapsCount);
|
||||
List<Map<String, MapCursor>> markers = new ArrayList<>(mapsCount);
|
||||
for (MapView mapView : mapViews) {
|
||||
mapIds.add(mapView.getId());
|
||||
markers.add(new ConcurrentHashMap<>());
|
||||
}
|
||||
BundledAnimatedImageMap map = new BundledAnimatedImageMap(createInfo.getManager(), this, createInfo.getName(), createInfo.getResourcePath(), new LazyMappedBufferedImage[mapsCount][], mapViews, mapIds, markers, createInfo.getWidth(), createInfo.getHeight(), createInfo.getDitheringType(), createInfo.getCreationTime());
|
||||
return FutureUtils.callAsyncMethod(() -> {
|
||||
FutureUtils.callSyncMethod(() -> {
|
||||
for (int i = 0; i < mapViews.size(); i++) {
|
||||
MapView mapView = mapViews.get(i);
|
||||
for (MapRenderer renderer : mapView.getRenderers()) {
|
||||
mapView.removeRenderer(renderer);
|
||||
}
|
||||
mapView.addRenderer(new URLAnimatedImageMap.URLAnimatedImageMapRenderer(map, i));
|
||||
}
|
||||
}).get();
|
||||
map.update(false);
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<BundledAnimatedImageMap> load(ImageMapManager manager, JsonObject json) {
|
||||
throw new UnsupportedOperationException("bundled maps are loaded from maps.yml in the plugin jar, not from storage");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
/**
|
||||
* An image map whose canonical definition lives inside the plugin jar
|
||||
* (maps.yml + an image under maps/). Bundled maps are rebuilt from the jar
|
||||
* on every startup and are never persisted to the plugin data folder.
|
||||
*/
|
||||
public interface BundledImageMap {
|
||||
|
||||
String getResourcePath();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class BundledMapCreateInfo extends ImageMapCreateInfo {
|
||||
|
||||
private final String resourcePath;
|
||||
private final List<Integer> mapIds;
|
||||
private final long creationTime;
|
||||
|
||||
public BundledMapCreateInfo(ImageMapManager manager, String name, String resourcePath, int width, int height, DitheringType ditheringType, List<Integer> mapIds, long creationTime) {
|
||||
super(manager, name, width, height, ditheringType, ImageMap.CONSOLE_CREATOR);
|
||||
this.resourcePath = resourcePath;
|
||||
this.mapIds = Collections.unmodifiableList(mapIds);
|
||||
this.creationTime = creationTime;
|
||||
}
|
||||
|
||||
public String getResourcePath() {
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
public List<Integer> getMapIds() {
|
||||
return mapIds;
|
||||
}
|
||||
|
||||
public long getCreationTime() {
|
||||
return creationTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* Loads the maps whose canonical definition is bundled inside the plugin jar.
|
||||
* <p>
|
||||
* Every image directly under maps/ in the jar becomes a Console owned map
|
||||
* named after the file (without its extension); a .gif becomes an animated
|
||||
* map. maps.yml can optionally add an entry per map to override its name,
|
||||
* size, dithering or map ids. Anything not set there is decided once, on the
|
||||
* first load:
|
||||
* <ul>
|
||||
* <li>If a stored Console map with the same name exists (for example one
|
||||
* previewed from plugins/ImageFrame/images/), the bundled map takes over
|
||||
* its size, dithering and map ids, and the stored copy is deleted.</li>
|
||||
* <li>Otherwise the size is the image size divided by 128 pixels per map,
|
||||
* and the server assigns new map ids.</li>
|
||||
* </ul>
|
||||
* Those decisions are remembered in the main world folder, so the maps keep
|
||||
* their map ids even if the plugin data folder is wiped. MapIds pinned in
|
||||
* maps.yml always win.
|
||||
*/
|
||||
public class BundledMaps {
|
||||
|
||||
public static final String MAPS_YML = "maps.yml";
|
||||
public static final String MAPS_FOLDER = "maps/";
|
||||
public static final String ASSIGNED_MAPS_FILE_NAME = "imageframe-bundled-maps.yml";
|
||||
public static final Set<String> IMAGE_EXTENSIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("png", "jpg", "jpeg", "gif", "bmp", "webp")));
|
||||
|
||||
public static int load(ImageMapManager manager, File pluginJar) {
|
||||
return new BundledMaps(manager, pluginJar).load();
|
||||
}
|
||||
|
||||
private final ImageMapManager manager;
|
||||
private final File pluginJar;
|
||||
private final CommandSender console;
|
||||
|
||||
private BundledMaps(ImageMapManager manager, File pluginJar) {
|
||||
this.manager = manager;
|
||||
this.pluginJar = pluginJar;
|
||||
this.console = Bukkit.getConsoleSender();
|
||||
}
|
||||
|
||||
private int load() {
|
||||
List<Definition> definitions = new ArrayList<>(readMapsYml());
|
||||
Set<String> definedImages = new HashSet<>();
|
||||
for (Definition definition : definitions) {
|
||||
definedImages.add(definition.image);
|
||||
}
|
||||
for (String image : listBundledImages()) {
|
||||
if (!definedImages.contains(image)) {
|
||||
definitions.add(new Definition(getFileNameWithoutExtension(image), image));
|
||||
}
|
||||
}
|
||||
// Pinned maps load first, so map ids the server assigns to other maps never collide with pinned ones
|
||||
definitions.sort(Comparator.comparing(definition -> definition.pinnedMapIds == null));
|
||||
|
||||
World world = MapUtils.getMainWorld();
|
||||
File assignedMapsFile = new File(world.getWorldFolder(), ASSIGNED_MAPS_FILE_NAME);
|
||||
Map<String, Assigned> assignedMaps = readAssignedMaps(assignedMapsFile);
|
||||
boolean assignedMapsChanged = false;
|
||||
int count = 0;
|
||||
for (Definition definition : definitions) {
|
||||
String name = definition.name;
|
||||
try {
|
||||
if (name.isEmpty() || name.indexOf(':') >= 0 || name.chars().anyMatch(Character::isWhitespace)) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping bundled image \"" + definition.image + "\": map names cannot contain spaces or colons, rename the file or set a Name in maps.yml");
|
||||
continue;
|
||||
}
|
||||
// Validate the bundled image is present and decodable before touching
|
||||
// any stored copy of this map, so a bad jar cannot destroy working maps.
|
||||
BufferedImage image = BundledStaticImageMap.readResourceImage(definition.image);
|
||||
ImageMap existing = manager.getFromCreator(ImageMap.CONSOLE_CREATOR, name);
|
||||
if (existing instanceof BundledImageMap) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\" from " + definition.image + ": " + ((BundledImageMap) existing).getResourcePath() + " already uses this name");
|
||||
continue;
|
||||
}
|
||||
Assigned assigned = definition.pinnedMapIds == null ? assignedMaps.get(name) : null;
|
||||
|
||||
int width;
|
||||
int height;
|
||||
if (definition.width != null && definition.height != null) {
|
||||
width = definition.width;
|
||||
height = definition.height;
|
||||
} else if (existing != null) {
|
||||
width = existing.getWidth();
|
||||
height = existing.getHeight();
|
||||
} else if (assigned != null) {
|
||||
width = assigned.width;
|
||||
height = assigned.height;
|
||||
} else {
|
||||
width = Math.max(1, Math.round(image.getWidth() / (float) MapUtils.MAP_WIDTH));
|
||||
height = Math.max(1, Math.round(image.getHeight() / (float) MapUtils.MAP_WIDTH));
|
||||
if (width * height > ImageFrame.mapMaxSize) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping bundled image \"" + definition.image + "\": at " + MapUtils.MAP_WIDTH + " pixels per map it would be " + width + "x" + height + " maps, over the MaxSize of " + ImageFrame.mapMaxSize + ". Shrink the image or set Width and Height in maps.yml");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\": Width and Height must be positive (got " + width + "x" + height + ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
DitheringType ditheringType;
|
||||
if (definition.ditheringType != null) {
|
||||
ditheringType = definition.ditheringType;
|
||||
} else if (existing != null) {
|
||||
ditheringType = existing.getDitheringType();
|
||||
} else if (assigned != null) {
|
||||
ditheringType = DitheringType.fromName(assigned.ditheringType);
|
||||
} else {
|
||||
ditheringType = DitheringType.fromName(null);
|
||||
}
|
||||
|
||||
long creationTime;
|
||||
if (definition.creationTime != null) {
|
||||
creationTime = definition.creationTime;
|
||||
} else if (existing != null) {
|
||||
creationTime = existing.getCreationTime();
|
||||
} else if (assigned != null) {
|
||||
creationTime = assigned.creationTime;
|
||||
} else {
|
||||
creationTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
List<Integer> mapIds;
|
||||
if (definition.pinnedMapIds != null) {
|
||||
mapIds = definition.pinnedMapIds;
|
||||
if (mapIds.size() != width * height) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\": MapIds must contain exactly Width * Height (" + (width * height) + ") map ids");
|
||||
continue;
|
||||
}
|
||||
} else if (existing != null) {
|
||||
if (existing.getMapIds().size() != width * height) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Bundled ImageMap \"" + name + "\" is set to " + width + "x" + height + " in maps.yml but the copy in storage is " + existing.getWidth() + "x" + existing.getHeight() + ", keeping the stored copy. Remove Width and Height from maps.yml to migrate it.");
|
||||
continue;
|
||||
}
|
||||
mapIds = new ArrayList<>(existing.getMapIds());
|
||||
} else if (assigned != null && assigned.mapIds.size() == width * height) {
|
||||
mapIds = assigned.mapIds;
|
||||
} else {
|
||||
mapIds = new ArrayList<>(width * height);
|
||||
for (int i = 0; i < width * height; i++) {
|
||||
mapIds.add(MapUtils.createMap(world).get().getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (existing != null) {
|
||||
if (!existing.getMapIds().equals(mapIds)) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Bundled ImageMap \"" + name + "\" has different map ids than the copy in storage, keeping the stored copy. Remove MapIds from maps.yml, or set them to " + existing.getMapIds() + ", to migrate it.");
|
||||
continue;
|
||||
}
|
||||
// Same name and same map ids: the jar is now the canonical
|
||||
// definition, so drop the copy from the data folder.
|
||||
manager.deleteMap(existing.getImageIndex());
|
||||
console.sendMessage(ChatColor.YELLOW + "[ImageFrame] Migrated ImageMap \"" + name + "\" to the bundled definition in the plugin jar, its data folder copy has been removed");
|
||||
}
|
||||
|
||||
if (definition.pinnedMapIds != null) {
|
||||
if (assignedMaps.remove(name) != null) {
|
||||
assignedMapsChanged = true;
|
||||
}
|
||||
} else {
|
||||
Assigned decided = new Assigned(width, height, ditheringType.getName(), mapIds, creationTime);
|
||||
if (!decided.equals(assigned)) {
|
||||
assignedMaps.put(name, decided);
|
||||
assignedMapsChanged = true;
|
||||
console.sendMessage(ChatColor.YELLOW + "[ImageFrame] Bundled ImageMap \"" + name + "\" uses map ids " + mapIds + ", saved to " + assignedMapsFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
BundledMapCreateInfo createInfo = new BundledMapCreateInfo(manager, name, definition.image, width, height, ditheringType, mapIds, creationTime);
|
||||
boolean animated = definition.image.toLowerCase().endsWith(".gif");
|
||||
ImageMap imageMap;
|
||||
if (animated) {
|
||||
imageMap = ImageMapLoaders.BUNDLED_ANIMATED_IMAGE_MAP_LOADER.create(createInfo).get();
|
||||
} else {
|
||||
imageMap = ImageMapLoaders.BUNDLED_STATIC_IMAGE_MAP_LOADER.create(createInfo).get();
|
||||
}
|
||||
manager.addBundledMap(imageMap);
|
||||
count++;
|
||||
console.sendMessage(ChatColor.GREEN + "[ImageFrame] Loaded bundled " + (animated ? "animated " : "") + "ImageMap \"" + name + "\" (" + width + "x" + height + ", map ids " + mapIds.get(0) + "-" + mapIds.get(mapIds.size() - 1) + ") from the plugin jar");
|
||||
} catch (Exception e) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\" from the plugin jar");
|
||||
new IOException("Unable to load bundled ImageMap \"" + name + "\" from the plugin jar", e).printStackTrace();
|
||||
}
|
||||
}
|
||||
if (assignedMapsChanged) {
|
||||
saveAssignedMaps(assignedMapsFile, assignedMaps);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private List<Definition> readMapsYml() {
|
||||
InputStream stream = ImageFrame.plugin.getClass().getClassLoader().getResourceAsStream(MAPS_YML);
|
||||
if (stream == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
YamlConfiguration mapsYml;
|
||||
try (Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
|
||||
mapsYml = YamlConfiguration.loadConfiguration(reader);
|
||||
} catch (IOException e) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to read bundled maps.yml from the plugin jar");
|
||||
e.printStackTrace();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Definition> definitions = new ArrayList<>();
|
||||
for (Map<?, ?> entry : mapsYml.getMapList("Maps")) {
|
||||
Object image = entry.get("Image");
|
||||
if (image == null) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping invalid maps.yml entry (missing Image): " + entry);
|
||||
continue;
|
||||
}
|
||||
String imagePath = image.toString();
|
||||
Definition definition = new Definition(entry.get("Name") == null ? getFileNameWithoutExtension(imagePath) : entry.get("Name").toString(), imagePath);
|
||||
definition.width = entry.get("Width") instanceof Number ? ((Number) entry.get("Width")).intValue() : null;
|
||||
definition.height = entry.get("Height") instanceof Number ? ((Number) entry.get("Height")).intValue() : null;
|
||||
if ((definition.width == null) != (definition.height == null)) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping maps.yml entry for " + imagePath + ": set both Width and Height, or neither");
|
||||
continue;
|
||||
}
|
||||
definition.ditheringType = entry.get("Dithering") == null ? null : DitheringType.fromName(entry.get("Dithering").toString());
|
||||
definition.pinnedMapIds = entry.get("MapIds") instanceof List ? parseMapIds(entry.get("MapIds")) : null;
|
||||
definition.creationTime = entry.get("CreationTime") instanceof Number ? ((Number) entry.get("CreationTime")).longValue() : null;
|
||||
definitions.add(definition);
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private List<String> listBundledImages() {
|
||||
List<String> images = new ArrayList<>();
|
||||
if (pluginJar == null || !pluginJar.isFile()) {
|
||||
return images;
|
||||
}
|
||||
try (ZipFile zip = new ZipFile(pluginJar)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String path = entry.getName();
|
||||
if (entry.isDirectory() || !path.startsWith(MAPS_FOLDER) || path.indexOf('/', MAPS_FOLDER.length()) >= 0) {
|
||||
continue;
|
||||
}
|
||||
int dot = path.lastIndexOf('.');
|
||||
if (dot > MAPS_FOLDER.length() && IMAGE_EXTENSIONS.contains(path.substring(dot + 1).toLowerCase())) {
|
||||
images.add(path);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to list bundled images in the plugin jar");
|
||||
e.printStackTrace();
|
||||
}
|
||||
// Sorted so the server assigns map ids in a predictable order
|
||||
Collections.sort(images);
|
||||
return images;
|
||||
}
|
||||
|
||||
private Map<String, Assigned> readAssignedMaps(File file) {
|
||||
Map<String, Assigned> assignedMaps = new LinkedHashMap<>();
|
||||
if (!file.exists()) {
|
||||
return assignedMaps;
|
||||
}
|
||||
for (Map<?, ?> entry : YamlConfiguration.loadConfiguration(file).getMapList("Maps")) {
|
||||
if (entry.get("Name") == null || !(entry.get("Width") instanceof Number) || !(entry.get("Height") instanceof Number)) {
|
||||
continue;
|
||||
}
|
||||
int width = ((Number) entry.get("Width")).intValue();
|
||||
int height = ((Number) entry.get("Height")).intValue();
|
||||
String ditheringType = entry.get("Dithering") == null ? null : entry.get("Dithering").toString();
|
||||
long creationTime = entry.get("CreationTime") instanceof Number ? ((Number) entry.get("CreationTime")).longValue() : System.currentTimeMillis();
|
||||
assignedMaps.put(entry.get("Name").toString(), new Assigned(width, height, ditheringType, parseMapIds(entry.get("MapIds")), creationTime));
|
||||
}
|
||||
return assignedMaps;
|
||||
}
|
||||
|
||||
private void saveAssignedMaps(File file, Map<String, Assigned> assignedMaps) {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
List<Map<String, Object>> entries = new ArrayList<>(assignedMaps.size());
|
||||
for (Map.Entry<String, Assigned> assigned : assignedMaps.entrySet()) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("Name", assigned.getKey());
|
||||
entry.put("Width", assigned.getValue().width);
|
||||
entry.put("Height", assigned.getValue().height);
|
||||
entry.put("Dithering", assigned.getValue().ditheringType);
|
||||
entry.put("MapIds", assigned.getValue().mapIds);
|
||||
entry.put("CreationTime", assigned.getValue().creationTime);
|
||||
entries.add(entry);
|
||||
}
|
||||
yaml.set("Maps", entries);
|
||||
String header = "# Written by ImageFrame. Size, dithering and map ids of the bundled maps that do not\n" +
|
||||
"# pin MapIds in maps.yml, decided the first time each map loaded. Item frames in this\n" +
|
||||
"# world show these map ids, so keep this file with the world and do not edit the ids.\n";
|
||||
try {
|
||||
Files.write(file.toPath(), (header + yaml.saveToString()).getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to save " + file.getPath());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Integer> parseMapIds(Object value) {
|
||||
List<Integer> mapIds = new ArrayList<>();
|
||||
if (value instanceof List) {
|
||||
for (Object id : (List<?>) value) {
|
||||
if (id instanceof Number) {
|
||||
mapIds.add(((Number) id).intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return mapIds;
|
||||
}
|
||||
|
||||
private static String getFileNameWithoutExtension(String path) {
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
int dot = fileName.lastIndexOf('.');
|
||||
return dot > 0 ? fileName.substring(0, dot) : fileName;
|
||||
}
|
||||
|
||||
private static class Definition {
|
||||
|
||||
private final String name;
|
||||
private final String image;
|
||||
private Integer width;
|
||||
private Integer height;
|
||||
private DitheringType ditheringType;
|
||||
private List<Integer> pinnedMapIds;
|
||||
private Long creationTime;
|
||||
|
||||
private Definition(String name, String image) {
|
||||
this.name = name;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Assigned {
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final String ditheringType;
|
||||
private final List<Integer> mapIds;
|
||||
private final long creationTime;
|
||||
|
||||
private Assigned(int width, int height, String ditheringType, List<Integer> mapIds, long creationTime) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.ditheringType = ditheringType;
|
||||
this.mapIds = mapIds;
|
||||
this.creationTime = creationTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Assigned)) {
|
||||
return false;
|
||||
}
|
||||
Assigned assigned = (Assigned) o;
|
||||
return width == assigned.width && height == assigned.height && creationTime == assigned.creationTime && Objects.equals(ditheringType, assigned.ditheringType) && mapIds.equals(assigned.mapIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height, ditheringType, mapIds, creationTime);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An image map whose canonical definition lives inside the plugin jar:
|
||||
* the image is a jar resource and the Minecraft map ids are pinned in
|
||||
* maps.yml. Bundled maps are re-rendered from the jar on every startup
|
||||
* and are never persisted to the plugin data folder.
|
||||
*/
|
||||
public class BundledStaticImageMap extends URLStaticImageMap implements BundledImageMap {
|
||||
|
||||
public static InputStream openResource(String resourcePath) throws IOException {
|
||||
InputStream stream = ImageFrame.plugin.getClass().getClassLoader().getResourceAsStream(resourcePath);
|
||||
if (stream == null) {
|
||||
throw new IOException("Missing bundled image resource in jar: " + resourcePath);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
public static BufferedImage readResourceImage(String resourcePath) throws IOException {
|
||||
try (InputStream stream = openResource(resourcePath)) {
|
||||
BufferedImage image = ImageIO.read(stream);
|
||||
if (image == null) {
|
||||
throw new IOException("Unable to decode bundled image resource in jar: " + resourcePath);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
protected BundledStaticImageMap(ImageMapManager manager, ImageMapLoader<?, ?> loader, String name, String resourcePath, LazyMappedBufferedImage[] cachedImages, List<MapView> mapViews, List<Integer> mapIds, List<Map<String, MapCursor>> mapMarkers, int width, int height, DitheringType ditheringType, long creationTime) {
|
||||
super(manager, loader, -1, name, resourcePath, cachedImages, mapViews, mapIds, mapMarkers, width, height, ditheringType, ImageMap.CONSOLE_CREATOR, Collections.emptyMap(), creationTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUrl(String url) {
|
||||
//bundled maps always render from their jar resource, the path cannot change at runtime
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(boolean save) throws Exception {
|
||||
BufferedImage image = readResourceImage(getResourcePath());
|
||||
int hdMapWidth = image.getWidth() / width;
|
||||
image = MapUtils.resize(image, width, height, hdMapWidth);
|
||||
int i = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
cachedImages[i++] = StandardLazyMappedBufferedImage.fromImage(MapUtils.getSubImage(image, x, y, hdMapWidth));
|
||||
}
|
||||
}
|
||||
reloadColorCache();
|
||||
Bukkit.getPluginManager().callEvent(new ImageMapUpdatedEvent(this));
|
||||
send(getViewers());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ImageFrameStorage storage, boolean saveAsCopy) {
|
||||
//the jar is the canonical storage, nothing to save
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageMap deepClone(String name, UUID creator) {
|
||||
throw new UnsupportedOperationException("bundled maps cannot be cloned");
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.utils.FutureUtils;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapRenderer;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class BundledStaticImageMapLoader extends ImageMapLoader<BundledStaticImageMap, BundledMapCreateInfo> {
|
||||
|
||||
private static final Key IDENTIFIER = Key.key("imageframe", "bundled_static");
|
||||
|
||||
@Override
|
||||
public Key getIdentifier() {
|
||||
return IDENTIFIER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<BundledStaticImageMap> getImageMapClass() {
|
||||
return BundledStaticImageMap.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<BundledMapCreateInfo> getImageMapCreateInfoClass() {
|
||||
return BundledMapCreateInfo.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getExtraPermissions() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(String imageType) {
|
||||
//bundled maps are only created from maps.yml in the jar, never through commands
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the pinned map ids of a bundled map to their map views,
|
||||
* creating any map id that does not exist in the world yet.
|
||||
*/
|
||||
public static List<MapView> getPinnedMapViews(BundledMapCreateInfo createInfo) throws Exception {
|
||||
int mapsCount = createInfo.getWidth() * createInfo.getHeight();
|
||||
List<Integer> pinnedMapIds = createInfo.getMapIds();
|
||||
if (pinnedMapIds.size() != mapsCount) {
|
||||
throw new IllegalArgumentException("MapIds size " + pinnedMapIds.size() + " does not equal Width * Height (" + mapsCount + ")");
|
||||
}
|
||||
World world = MapUtils.getMainWorld();
|
||||
List<Future<MapView>> mapViewsFuture = new ArrayList<>(mapsCount);
|
||||
for (int mapId : pinnedMapIds) {
|
||||
mapViewsFuture.add(MapUtils.getMapOrCreateMissing(world, mapId));
|
||||
}
|
||||
List<MapView> mapViews = new ArrayList<>(mapsCount);
|
||||
for (Future<MapView> future : mapViewsFuture) {
|
||||
mapViews.add(future.get());
|
||||
}
|
||||
return mapViews;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<BundledStaticImageMap> create(BundledMapCreateInfo createInfo) throws Exception {
|
||||
int mapsCount = createInfo.getWidth() * createInfo.getHeight();
|
||||
List<MapView> mapViews = getPinnedMapViews(createInfo);
|
||||
List<Integer> mapIds = new ArrayList<>(mapsCount);
|
||||
List<Map<String, MapCursor>> markers = new ArrayList<>(mapsCount);
|
||||
for (MapView mapView : mapViews) {
|
||||
mapIds.add(mapView.getId());
|
||||
markers.add(new ConcurrentHashMap<>());
|
||||
}
|
||||
BundledStaticImageMap map = new BundledStaticImageMap(createInfo.getManager(), this, createInfo.getName(), createInfo.getResourcePath(), new LazyMappedBufferedImage[mapsCount], mapViews, mapIds, markers, createInfo.getWidth(), createInfo.getHeight(), createInfo.getDitheringType(), createInfo.getCreationTime());
|
||||
return FutureUtils.callAsyncMethod(() -> {
|
||||
FutureUtils.callSyncMethod(() -> {
|
||||
for (int i = 0; i < mapViews.size(); i++) {
|
||||
MapView mapView = mapViews.get(i);
|
||||
for (MapRenderer renderer : mapView.getRenderers()) {
|
||||
mapView.removeRenderer(renderer);
|
||||
}
|
||||
mapView.addRenderer(new URLStaticImageMap.URLStaticImageMapRenderer(map, i));
|
||||
}
|
||||
}).get();
|
||||
map.update(false);
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<BundledStaticImageMap> load(ImageMapManager manager, JsonObject json) {
|
||||
throw new UnsupportedOperationException("bundled maps are loaded from maps.yml in the plugin jar, not from storage");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.language.TranslationKey;
|
||||
import com.loohp.imageframe.nms.NMS;
|
||||
import com.loohp.imageframe.utils.CommandSenderUtils;
|
||||
import com.loohp.imageframe.utils.ComponentUtils;
|
||||
import com.loohp.imageframe.utils.ItemFrameUtils;
|
||||
import com.loohp.imageframe.utils.PlayerUtils;
|
||||
import com.loohp.imageframe.utils.SlotAccessor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.hanging.HangingBreakEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CombinedMapItemHandler implements Listener, AutoCloseable {
|
||||
|
||||
public static boolean containsCombinedMaps(SlotAccessor slotAccess, int size) {
|
||||
return containsCombinedMaps(slotAccess, 0, size);
|
||||
}
|
||||
|
||||
public static boolean containsCombinedMaps(SlotAccessor slotAccess, int begin, int size) {
|
||||
for (int i = begin; i < size; i++) {
|
||||
ItemStack itemStack = slotAccess.apply(i);
|
||||
if (isCombinedMaps(itemStack)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean containsCombinedMaps(Collection<ItemStack> itemStacks) {
|
||||
return itemStacks.stream().anyMatch(itemStack -> isCombinedMaps(itemStack));
|
||||
}
|
||||
|
||||
public static boolean isCombinedMaps(ItemStack itemStack) {
|
||||
if (itemStack != null && itemStack.getType().equals(Material.PAPER)) {
|
||||
return NMS.getInstance().getCombinedMapItemInfo(itemStack) != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private final Set<Player> entityInteractionChecking;
|
||||
private final Set<Player> entityDamageChecking;
|
||||
private final Set<ItemFrame> itemFrameProcessing;
|
||||
|
||||
public CombinedMapItemHandler() {
|
||||
this.entityInteractionChecking = new HashSet<>();
|
||||
this.entityDamageChecking = new HashSet<>();
|
||||
this.itemFrameProcessing = new HashSet<>();
|
||||
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
|
||||
}
|
||||
|
||||
public ItemStack getCombinedMap(ImageMap imageMap) {
|
||||
ItemStack itemStack = new ItemStack(Material.PAPER);
|
||||
ItemMeta meta = itemStack.getItemMeta();
|
||||
|
||||
Component name = ComponentUtils.translatable(
|
||||
TranslationKey.COMBINED_ITEM_NAME,
|
||||
Component.text(imageMap.getName()).color(NamedTextColor.YELLOW),
|
||||
Component.text(imageMap.getWidth()).color(NamedTextColor.WHITE),
|
||||
Component.text(imageMap.getHeight()).color(NamedTextColor.WHITE)
|
||||
).color(NamedTextColor.AQUA);
|
||||
meta.setDisplayName(LegacyComponentSerializer.legacySection().serialize(ImageFrame.languageManager.resolve(name, ImageFrame.language)));
|
||||
|
||||
Component lore1 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_1, imageMap.getWidth(), imageMap.getHeight()).color(NamedTextColor.GREEN);
|
||||
Component lore2 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_2).color(NamedTextColor.GRAY);
|
||||
Component lore3 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_3, imageMap.getImageIndex()).color(NamedTextColor.GRAY);
|
||||
Component lore4 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_4, imageMap.getCreatorName(), imageMap.getCreator()).color(NamedTextColor.GRAY);
|
||||
Component lore5 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_5, ImageFrame.dateFormat.format(new Date(imageMap.getCreationTime()))).color(NamedTextColor.GRAY);
|
||||
meta.setLore(Stream.of(lore1, lore2, lore3, lore4, lore5).map(c -> LegacyComponentSerializer.legacySection().serialize(ImageFrame.languageManager.resolve(c, ImageFrame.language))).collect(Collectors.toList()));
|
||||
|
||||
itemStack.setItemMeta(meta);
|
||||
return NMS.getInstance().withCombinedMapItemInfo(itemStack, new CombinedMapItemInfo(imageMap.getImageIndex()));
|
||||
}
|
||||
|
||||
public void giveCombinedMap(ImageMap imageMap, Player player) {
|
||||
giveCombinedMap(imageMap, Collections.singleton(player));
|
||||
}
|
||||
|
||||
public void giveCombinedMap(ImageMap imageMap, Collection<? extends Player> players) {
|
||||
ItemStack map = getCombinedMap(imageMap);
|
||||
for (Player player : players) {
|
||||
PlayerUtils.giveItem(player, map.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
public ItemFrameSelectionManager.SelectedItemFrameResult findItemFrames(ItemFrame itemFrame, float yaw, int width, int height, Predicate<ItemStack> itemCheck) {
|
||||
Vector up;
|
||||
Vector left;
|
||||
BlockFace facing = itemFrame.getFacing();
|
||||
if (ItemFrameUtils.isOnWalls(itemFrame)) {
|
||||
up = new Vector(0, 1, 0);
|
||||
left = facing.getDirection().rotateAroundY(Math.toRadians(-90));
|
||||
} else if (ItemFrameUtils.isOnCeiling(itemFrame)) {
|
||||
up = ItemFrameUtils.getClosestCardinalDirection(yaw + 180F);
|
||||
left = up.clone().rotateAroundY(Math.toRadians(-90));
|
||||
} else {
|
||||
up = ItemFrameUtils.getClosestCardinalDirection(yaw);
|
||||
left = up.clone().rotateAroundY(Math.toRadians(90));
|
||||
}
|
||||
List<Location> checkingLocations = getCheckingLocations(itemFrame.getLocation(), up, left, width, height);
|
||||
outer: for (Location location : checkingLocations) {
|
||||
itemFrame = getItemFrame(location, facing);
|
||||
if (itemFrame == null) {
|
||||
continue;
|
||||
}
|
||||
ItemFrame opposite = getOppositeItemFrameCorner(itemFrame, up, left, width, height);
|
||||
if (opposite == null) {
|
||||
continue;
|
||||
}
|
||||
ItemFrameSelectionManager.SelectedItemFrameResult result = ImageFrame.itemFrameSelectionManager.getSelectedItemFrames(yaw, itemFrame, opposite);
|
||||
if (result == null) {
|
||||
continue;
|
||||
}
|
||||
List<ItemFrame> selectedFrames = result.getItemFrames();
|
||||
if (selectedFrames.size() != width * height) {
|
||||
continue;
|
||||
}
|
||||
for (ItemFrame frame : selectedFrames) {
|
||||
ItemStack item = frame.getItem();
|
||||
if (!itemCheck.test(item)) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ItemFrame getOppositeItemFrameCorner(ItemFrame itemFrame, Vector up, Vector left, int width, int height) {
|
||||
Location location = itemFrame.getLocation().add(left.clone().multiply(-(width - 1))).add(up.clone().multiply(-(height - 1)));
|
||||
BoundingBox boundingBox = BoundingBox.of(location.getBlock(), location.getBlock());
|
||||
Collection<Entity> entities = location.getWorld().getNearbyEntities(boundingBox, e -> e instanceof ItemFrame);
|
||||
for (Entity entity : entities) {
|
||||
if (itemFrame.getFacing().equals(entity.getFacing())) {
|
||||
return (ItemFrame) entity;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ItemFrame getItemFrame(Location location, BlockFace facing) {
|
||||
BoundingBox boundingBox = BoundingBox.of(location.getBlock(), location.getBlock());
|
||||
Collection<Entity> entities = location.getWorld().getNearbyEntities(boundingBox, e -> e instanceof ItemFrame);
|
||||
for (Entity entity : entities) {
|
||||
if (facing.equals(entity.getFacing())) {
|
||||
return (ItemFrame) entity;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Location> getCheckingLocations(Location origin, Vector up, Vector left, int width, int height) {
|
||||
List<Location> locations = new ArrayList<>(width * height);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
locations.add(origin.clone().add(up.clone().multiply(y)).add(left.clone().multiply(x)));
|
||||
}
|
||||
}
|
||||
locations.sort(Comparator.comparing(each -> each.distanceSquared(origin)));
|
||||
return locations;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInteractEntity(PlayerInteractEntityEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (!entityInteractionChecking.add(player)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
EquipmentSlot slot = event.getHand();
|
||||
ItemStack itemStack = player.getEquipment().getItem(slot);
|
||||
if (itemStack == null || !itemStack.getType().equals(Material.PAPER)) {
|
||||
return;
|
||||
}
|
||||
CombinedMapItemInfo mapItemInfo = NMS.getInstance().getCombinedMapItemInfo(itemStack);
|
||||
if (mapItemInfo == null) {
|
||||
return;
|
||||
}
|
||||
Entity entity = event.getRightClicked();
|
||||
if (!(entity instanceof ItemFrame)) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
int id = mapItemInfo.getImageMapIndex();
|
||||
ImageMap imageMap = ImageFrame.imageMapManager.getFromImageId(id);
|
||||
if (imageMap == null) {
|
||||
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.INVALID_IMAGE_MAP).color(NamedTextColor.RED));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
float yaw = player.getLocation().getYaw();
|
||||
ItemFrameSelectionManager.SelectedItemFrameResult selection = findItemFrames(itemFrame, yaw, imageMap.getWidth(), imageMap.getHeight(), item -> item == null || item.getType().equals(Material.AIR));
|
||||
if (selection == null) {
|
||||
event.setCancelled(true);
|
||||
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.NOT_ENOUGH_SPACE).color(NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
List<ItemFrame> selectedFrames = selection.getItemFrames();
|
||||
for (ItemFrame frame : selectedFrames) {
|
||||
ItemStack item = frame.getItem();
|
||||
if ((item != null && !item.getType().equals(Material.AIR)) || !PlayerUtils.isInteractionAllowed(player, frame)) {
|
||||
event.setCancelled(true);
|
||||
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.ITEM_FRAME_OCCUPIED).color(NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
itemFrameProcessing.addAll(selectedFrames);
|
||||
UUID uuid = UUID.randomUUID();
|
||||
imageMap.fillItemFrames(selectedFrames, selection.getRotation(), (frame, item) -> true, (frame, item) -> {}, ImageFrame.mapItemFormat, item -> {
|
||||
CombinedMapItemInfo newInfo = new CombinedMapItemInfo(imageMap.getImageIndex(), new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
|
||||
return NMS.getInstance().withCombinedMapItemInfo(item, newInfo);
|
||||
}, () -> selectedFrames.forEach(f -> itemFrameProcessing.remove(f)));
|
||||
} finally {
|
||||
entityInteractionChecking.remove(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDamageHanging(EntityDamageEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (!(entity instanceof ItemFrame)) {
|
||||
return;
|
||||
}
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
if (itemFrameProcessing.contains(itemFrame)) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
Player player = null;
|
||||
if (event instanceof EntityDamageByEntityEvent) {
|
||||
EntityDamageByEntityEvent entityDamageByEntityEvent = (EntityDamageByEntityEvent) event;
|
||||
Entity damager = entityDamageByEntityEvent.getDamager();
|
||||
if (damager instanceof Player) {
|
||||
player = (Player) damager;
|
||||
}
|
||||
}
|
||||
handleItemFrameBreak(player, itemFrame);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onRemoveHanging(HangingBreakEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (!(entity instanceof ItemFrame)) {
|
||||
return;
|
||||
}
|
||||
ItemFrame itemFrame = (ItemFrame) entity;
|
||||
if (itemFrameProcessing.contains(itemFrame)) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
handleItemFrameBreak(null, itemFrame);
|
||||
}
|
||||
|
||||
public void handleItemFrameBreak(Player player, ItemFrame itemFrame) {
|
||||
if (player != null && !entityDamageChecking.add(player)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ItemStack itemStack = itemFrame.getItem();
|
||||
if (itemStack == null || !itemStack.getType().equals(Material.FILLED_MAP)) {
|
||||
return;
|
||||
}
|
||||
CombinedMapItemInfo mapItemInfo = NMS.getInstance().getCombinedMapItemInfo(itemStack);
|
||||
if (mapItemInfo == null || !mapItemInfo.hasPlacement()) {
|
||||
return;
|
||||
}
|
||||
CombinedMapItemInfo.PlacementInfo placement = mapItemInfo.getPlacement();
|
||||
int id = mapItemInfo.getImageMapIndex();
|
||||
float yaw = placement.getYaw();
|
||||
UUID uuid = placement.getUniqueId();
|
||||
ImageMap imageMap = ImageFrame.imageMapManager.getFromImageId(id);
|
||||
if (imageMap == null) {
|
||||
return;
|
||||
}
|
||||
ItemFrameSelectionManager.SelectedItemFrameResult selection = findItemFrames(itemFrame, yaw, imageMap.getWidth(), imageMap.getHeight(), item -> {
|
||||
if (item == null || !item.getType().equals(Material.FILLED_MAP)) {
|
||||
return false;
|
||||
}
|
||||
CombinedMapItemInfo info = NMS.getInstance().getCombinedMapItemInfo(item);
|
||||
if (info == null || !info.hasPlacement()) {
|
||||
return false;
|
||||
}
|
||||
return info.getImageMapIndex() == id && info.getPlacement().getUniqueId().equals(uuid);
|
||||
});
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
List<ItemFrame> itemFrames = selection.getItemFrames();
|
||||
if (player == null || itemFrames.stream().allMatch(each -> PlayerUtils.isDamageAllowed(player, each))) {
|
||||
itemFrames.forEach(each -> each.setItem(null, false));
|
||||
itemFrame.setItem(getCombinedMap(imageMap), false);
|
||||
} else {
|
||||
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.ITEM_FRAME_OCCUPIED).color(NamedTextColor.RED));
|
||||
}
|
||||
} finally {
|
||||
entityDamageChecking.remove(player);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.utils.ComponentUtils;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
|
||||
public class ComponentTemplate {
|
||||
|
||||
public static ComponentTemplate template(String key, Object... arguments) {
|
||||
return new ComponentTemplate(key, arguments);
|
||||
}
|
||||
|
||||
private final String key;
|
||||
private final Object[] arguments;
|
||||
|
||||
private ComponentTemplate(String key, Object... arguments) {
|
||||
this.key = key;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public TranslatableComponent build(Object... extraArguments) {
|
||||
Object[] args = new Object[arguments.length + extraArguments.length];
|
||||
System.arraycopy(arguments, 0, args, 0, arguments.length);
|
||||
System.arraycopy(extraArguments, 0, args, arguments.length, extraArguments.length);
|
||||
return ComponentUtils.translatable(key, args);
|
||||
}
|
||||
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.api.events.HDMapPreRespondEvent;
|
||||
import com.loohp.imageframe.api.events.ImageMapDeletedEvent;
|
||||
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class CustomClientNetworkManager implements PluginMessageListener, Listener {
|
||||
|
||||
public static final String CLIENTBOUND_ACKNOWLEDGEMENT = "imageframe:clientbound_ack";
|
||||
public static final String SERVERBOUND_ACKNOWLEDGEMENT = "imageframe:serverbound_ack";
|
||||
|
||||
public static final String SERVERBOUND_HD_IMAGE_REQUEST = "imageframe:serverbound_hd_image";
|
||||
public static final String CLIENTBOUND_HD_IMAGE_RESPONSE = "imageframe:clientbound_hd_image";
|
||||
public static final String CLIENTBOUND_HD_IMAGE_MULTIPART_RESPONSE = "imageframe:clientbound_hd_image_multi";
|
||||
|
||||
public static final String CLIENTBOUND_HD_UPDATE_SIGNAL = "imageframe:clientbound_update";
|
||||
|
||||
public static final String SERVERBOUND_IMAGEMAP_DETAILS_REQUEST = "imageframe:serverbound_imagemap_details";
|
||||
public static final String CLIENTBOUND_IMAGEMAP_DETAILS_RESPONSE = "imageframe:clientbound_imagemap_details";
|
||||
|
||||
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
|
||||
private final Map<UUID, Long> acknowledgements;
|
||||
private final Set<UUID> acknowledged;
|
||||
|
||||
public CustomClientNetworkManager(boolean enabled) {
|
||||
this.acknowledgements = new ConcurrentHashMap<>();
|
||||
this.acknowledged = ConcurrentHashMap.newKeySet();
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_ACKNOWLEDGEMENT);
|
||||
Bukkit.getServer().getMessenger().registerIncomingPluginChannel(ImageFrame.plugin, SERVERBOUND_ACKNOWLEDGEMENT, this);
|
||||
|
||||
Bukkit.getServer().getMessenger().registerIncomingPluginChannel(ImageFrame.plugin, SERVERBOUND_HD_IMAGE_REQUEST, this);
|
||||
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_RESPONSE);
|
||||
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_MULTIPART_RESPONSE);
|
||||
|
||||
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_HD_UPDATE_SIGNAL);
|
||||
|
||||
Bukkit.getServer().getMessenger().registerIncomingPluginChannel(ImageFrame.plugin, SERVERBOUND_IMAGEMAP_DETAILS_REQUEST, this);
|
||||
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_IMAGEMAP_DETAILS_RESPONSE);
|
||||
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, ImageFrame.plugin);
|
||||
}
|
||||
|
||||
public Set<UUID> getPlayers() {
|
||||
return Collections.unmodifiableSet(acknowledged);
|
||||
}
|
||||
|
||||
public boolean hasPlayer(Player player) {
|
||||
return hasPlayer(player.getUniqueId());
|
||||
}
|
||||
|
||||
public boolean hasPlayer(UUID uuid) {
|
||||
return acknowledged.contains(uuid);
|
||||
}
|
||||
|
||||
public void sendAcknowledgement(Player player) {
|
||||
long id = ThreadLocalRandom.current().nextLong();
|
||||
acknowledgements.put(player.getUniqueId(), id);
|
||||
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeLong(id);
|
||||
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_ACKNOWLEDGEMENT, out.toByteArray());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
sendAcknowledgement(player);
|
||||
}
|
||||
}, 40);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
UUID uuid = event.getPlayer().getUniqueId();
|
||||
acknowledgements.remove(uuid);
|
||||
acknowledged.remove(uuid);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onUpdate(ImageMapUpdatedEvent event) {
|
||||
ImageMap imageMap = event.getImageMap();
|
||||
notifyHdMapUpdated(acknowledged, Collections.singletonList(imageMap), imageMap.getMapIds());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDelete(ImageMapDeletedEvent event) {
|
||||
ImageMap imageMap = event.getImageMap();
|
||||
notifyHdMapUpdated(acknowledged, Collections.singletonList(imageMap), imageMap.getMapIds());
|
||||
}
|
||||
|
||||
public void notifyHdMapUpdated(Collection<UUID> players, Collection<ImageMap> imageMaps, Collection<Integer> mapIds) {
|
||||
handle(() -> {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
writeVarInt(out, imageMaps.size());
|
||||
for (ImageMap imageMap : imageMaps) {
|
||||
out.writeInt(imageMap.getImageIndex());
|
||||
}
|
||||
writeVarInt(out, mapIds.size());
|
||||
for (int mapId : mapIds) {
|
||||
out.writeInt(mapId);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}, () -> true, out -> {
|
||||
for (UUID uuid : players) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if (player != null) {
|
||||
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_HD_UPDATE_SIGNAL, out);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player player, byte[] data) {
|
||||
try {
|
||||
switch (channel) {
|
||||
case SERVERBOUND_ACKNOWLEDGEMENT: {
|
||||
ByteArrayDataInput in = ByteStreams.newDataInput(data);
|
||||
long id = in.readLong();
|
||||
UUID uuid = player.getUniqueId();
|
||||
if (acknowledgements.get(uuid) == id) {
|
||||
acknowledgements.remove(uuid);
|
||||
acknowledged.add(uuid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SERVERBOUND_HD_IMAGE_REQUEST: {
|
||||
ByteArrayDataInput in = ByteStreams.newDataInput(data);
|
||||
int mapId = in.readInt();
|
||||
ImageMap imageMap = ImageFrame.imageMapManager.getFromMapId(mapId);
|
||||
handle(() -> {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeInt(mapId);
|
||||
try {
|
||||
MapView mapView = imageMap == null ? null : imageMap.getMapViewFromMapId(mapId);
|
||||
boolean requestAccepted = imageMap == null || MapUtils.canViewMap(player, mapView).get();
|
||||
BufferedImage image = imageMap == null ? null : imageMap.getOriginalImage(mapId);
|
||||
HDMapPreRespondEvent event = new HDMapPreRespondEvent(player, mapId, imageMap, mapView, requestAccepted, image);
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
requestAccepted = event.isRequestAccepted();
|
||||
image = event.getImage();
|
||||
out.writeBoolean(requestAccepted);
|
||||
if (image == null) {
|
||||
writeVarInt(out, 0);
|
||||
out.writeBoolean(false);
|
||||
} else {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
byte[] array;
|
||||
try {
|
||||
ImageIO.write(image, "png", byteArrayOutputStream);
|
||||
array = byteArrayOutputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
array = EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
if (array.length == 0) {
|
||||
writeVarInt(out, 0);
|
||||
out.writeBoolean(false);
|
||||
} else {
|
||||
List<byte[]> chunked = chunked(array, 32000);
|
||||
writeVarInt(out, chunked.get(0).length);
|
||||
out.write(chunked.get(0));
|
||||
if (chunked.size() <= 1) {
|
||||
out.writeBoolean(false);
|
||||
} else {
|
||||
int multipartId = ThreadLocalRandom.current().nextInt();
|
||||
out.writeBoolean(true);
|
||||
out.writeInt(multipartId);
|
||||
for (int i = 1; i < chunked.size(); i++) {
|
||||
ByteArrayDataOutput outMulti = ByteStreams.newDataOutput();
|
||||
outMulti.writeInt(mapId);
|
||||
outMulti.writeInt(multipartId);
|
||||
outMulti.writeInt(i);
|
||||
writeVarInt(outMulti, chunked.get(i).length);
|
||||
outMulti.write(chunked.get(i));
|
||||
outMulti.writeBoolean(i + 1 >= chunked.size());
|
||||
list.add(outMulti.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
list.add(0, out.toByteArray());
|
||||
return list;
|
||||
}, () -> player.isOnline(), out -> {
|
||||
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_RESPONSE, out.get(0));
|
||||
for (int i = 1; i < out.size(); i++) {
|
||||
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_MULTIPART_RESPONSE, out.get(i));
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SERVERBOUND_IMAGEMAP_DETAILS_REQUEST: {
|
||||
ByteArrayDataInput in = ByteStreams.newDataInput(data);
|
||||
int index = in.readInt();
|
||||
ImageMap imageMap = ImageFrame.imageMapManager.getFromImageId(index);
|
||||
handle(() -> {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeInt(index);
|
||||
if (imageMap == null) {
|
||||
out.writeInt(0);
|
||||
out.writeInt(0);
|
||||
writeVarInt(out, 0);
|
||||
} else {
|
||||
out.writeInt(imageMap.getWidth());
|
||||
out.writeInt(imageMap.getHeight());
|
||||
writeVarInt(out, imageMap.getMapIds().size());
|
||||
for (int mapId : imageMap.getMapIds()) {
|
||||
out.writeInt(mapId);
|
||||
}
|
||||
}
|
||||
return out.toByteArray();
|
||||
}, () -> player.isOnline(), out -> {
|
||||
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_IMAGEMAP_DETAILS_RESPONSE, out);
|
||||
if (imageMap != null) {
|
||||
imageMap.send(player);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
new RuntimeException("Illegal payload received from " + player.getName() + " on channel " + channel, e).printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeVarInt(ByteArrayDataOutput out, int i) {
|
||||
while ((i & -128) != 0) {
|
||||
out.writeByte(i & 127 | 128);
|
||||
i >>>= 7;
|
||||
}
|
||||
out.writeByte(i);
|
||||
}
|
||||
|
||||
private static <T> void handle(Supplier<T> handleAsync, BooleanSupplier syncPredicate, Consumer<T> completeSync) {
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
T value = handleAsync.get();
|
||||
if (syncPredicate.getAsBoolean()) {
|
||||
Scheduler.runTask(ImageFrame.plugin, () -> completeSync.accept(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static List<byte[]> chunked(byte[] data, int chunkSize) {
|
||||
if (data.length <= chunkSize) {
|
||||
return Collections.singletonList(data);
|
||||
}
|
||||
List<byte[]> chunks = new ArrayList<>();
|
||||
for (int i = 0; i < data.length; i += chunkSize) {
|
||||
int end = Math.min(data.length, i + chunkSize);
|
||||
byte[] chunk = Arrays.copyOfRange(data, i, end);
|
||||
chunks.add(chunk);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.utils.dithering.FloydSteinbergDithering;
|
||||
import org.bukkit.map.MapPalette;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class DitheringType {
|
||||
|
||||
private static final Map<String, DitheringType> REGISTERED_TYPES = new LinkedHashMap<>();
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public static final DitheringType NEAREST_COLOR = register(new DitheringType("nearest-color", image -> MapPalette.imageToBytes(image)));
|
||||
public static final DitheringType FLOYD_STEINBERG = register(new DitheringType("floyd-steinberg", image -> FloydSteinbergDithering.floydSteinbergDithering(image)));
|
||||
|
||||
public static DitheringType register(DitheringType ditheringType) {
|
||||
REGISTERED_TYPES.put(ditheringType.getName(), ditheringType);
|
||||
return ditheringType;
|
||||
}
|
||||
|
||||
public static Map<String, DitheringType> values() {
|
||||
return Collections.unmodifiableMap(REGISTERED_TYPES);
|
||||
}
|
||||
|
||||
public static DitheringType fromName(String name) {
|
||||
return REGISTERED_TYPES.getOrDefault(name, NEAREST_COLOR);
|
||||
}
|
||||
|
||||
public static DitheringType fromNameOrNull(String name) {
|
||||
return REGISTERED_TYPES.get(name);
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final Function<BufferedImage, byte[]> applyDithering;
|
||||
|
||||
public DitheringType(String name, Function<BufferedImage, byte[]> applyDithering) {
|
||||
this.name = name;
|
||||
this.applyDithering = applyDithering;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public byte[] applyDithering(BufferedImage image) {
|
||||
return applyDithering.apply(image);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class IFPlayer {
|
||||
|
||||
public static IFPlayer create(IFPlayerManager manager, UUID player) throws Exception {
|
||||
IFPlayer ifPlayer = new IFPlayer(manager, player, Collections.emptyMap());
|
||||
ifPlayer.save();
|
||||
return ifPlayer;
|
||||
}
|
||||
|
||||
public static IFPlayer load(IFPlayerManager manager, JsonObject json) {
|
||||
UUID uuid = UUID.fromString(json.get("uuid").getAsString());
|
||||
Map<IFPlayerPreference<?>, PreferenceState> preferences = new HashMap<>();
|
||||
JsonObject preferenceJson = json.get("preferences").getAsJsonObject();
|
||||
for (IFPlayerPreference<?> preference : IFPlayerPreference.values()) {
|
||||
JsonElement element = preferenceJson.get(preference.getJsonName());
|
||||
if (element != null) {
|
||||
preferences.put(preference, preference.getDeserializer().apply(element));
|
||||
}
|
||||
}
|
||||
return new IFPlayer(manager, uuid, preferences);
|
||||
}
|
||||
|
||||
private final IFPlayerManager manager;
|
||||
|
||||
private final UUID uuid;
|
||||
private final Map<IFPlayerPreference<?>, PreferenceState> preferences;
|
||||
|
||||
private IFPlayer(IFPlayerManager manager, UUID uuid, Map<IFPlayerPreference<?>, PreferenceState> preferences) {
|
||||
this.manager = manager;
|
||||
this.uuid = uuid;
|
||||
this.preferences = new ConcurrentHashMap<>(preferences);
|
||||
}
|
||||
|
||||
public void applyUpdate(JsonObject json) {
|
||||
Map<IFPlayerPreference<?>, PreferenceState> preferences = new HashMap<>();
|
||||
JsonObject preferenceJson = json.get("preferences").getAsJsonObject();
|
||||
for (IFPlayerPreference<?> preference : IFPlayerPreference.values()) {
|
||||
JsonElement element = preferenceJson.get(preference.getJsonName());
|
||||
if (element != null) {
|
||||
preferences.put(preference, preference.getDeserializer().apply(element));
|
||||
}
|
||||
}
|
||||
this.preferences.clear();
|
||||
this.preferences.putAll(preferences);
|
||||
}
|
||||
|
||||
public OfflinePlayer getLocalPlayer() {
|
||||
return Bukkit.getOfflinePlayer(uuid);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return getLocalPlayer().getName();
|
||||
}
|
||||
|
||||
public UUID getUniqueId() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public PreferenceState getPreference(IFPlayerPreference<?> preference) {
|
||||
return preferences.computeIfAbsent(preference, k -> preference.getDefaultValue(this));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends PreferenceState> T getPreference(IFPlayerPreference<?> preference, Class<T> type) {
|
||||
return (T) preferences.computeIfAbsent(preference, k -> preference.getDefaultValue(this, type));
|
||||
}
|
||||
|
||||
public void setPreference(IFPlayerPreference<?> preference, PreferenceState value) {
|
||||
preferences.put(preference, value);
|
||||
saveInternal();
|
||||
}
|
||||
|
||||
private void saveInternal() {
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
try {
|
||||
save();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void save() throws Exception {
|
||||
save(manager.getStorage());
|
||||
}
|
||||
|
||||
public void save(ImageFrameStorage storage) throws Exception {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("uuid", uuid.toString());
|
||||
JsonObject preferenceJson = new JsonObject();
|
||||
for (Map.Entry<IFPlayerPreference<?>, PreferenceState> entry : preferences.entrySet()) {
|
||||
IFPlayerPreference<?> preference = entry.getKey();
|
||||
preferenceJson.add(preference.getJsonName(), preference.getSerializer(PreferenceState.class).apply(entry.getValue()));
|
||||
}
|
||||
json.add("preferences", preferenceJson);
|
||||
storage.savePlayerData(uuid, json);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class IFPlayerManager implements AutoCloseable, Listener {
|
||||
|
||||
private final ImageFrameStorage imageFrameStorage;
|
||||
private final Map<UUID, IFPlayer> loadedPlayers;
|
||||
private final Set<IFPlayer> persistentLoadedPlayers;
|
||||
|
||||
public IFPlayerManager(ImageFrameStorage imageFrameStorage) {
|
||||
this.imageFrameStorage = imageFrameStorage;
|
||||
Cache<UUID, IFPlayer> playersCache = CacheBuilder.newBuilder().weakValues().build();
|
||||
this.loadedPlayers = playersCache.asMap();
|
||||
this.persistentLoadedPlayers = ConcurrentHashMap.newKeySet();
|
||||
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
|
||||
}
|
||||
|
||||
public ImageFrameStorage getStorage() {
|
||||
return imageFrameStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> persistentLoadedPlayers.add(getIFPlayer(event.getPlayer().getUniqueId())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
IFPlayer ifPlayer = loadedPlayers.get(event.getPlayer().getUniqueId());
|
||||
if (ifPlayer != null) {
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
try {
|
||||
ifPlayer.save();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
persistentLoadedPlayers.remove(ifPlayer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public IFPlayer getIFPlayer(UUID uuid) {
|
||||
return loadedPlayers.computeIfAbsent(uuid, id -> {
|
||||
JsonObject json = imageFrameStorage.loadPlayerData(this, id);
|
||||
if (json != null) {
|
||||
return IFPlayer.load(this, json);
|
||||
}
|
||||
return createNewIfPlayer(id);
|
||||
});
|
||||
}
|
||||
|
||||
public IFPlayer getIFPlayerIfLoaded(UUID uuid) {
|
||||
return loadedPlayers.get(uuid);
|
||||
}
|
||||
|
||||
private IFPlayer createNewIfPlayer(UUID uuid) {
|
||||
try {
|
||||
return IFPlayer.create(this, uuid);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to create ImageFrame player data for " + uuid, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class IFPlayerPreference<T extends PreferenceState> {
|
||||
|
||||
private static final Map<String, IFPlayerPreference<?>> VALUES = new ConcurrentHashMap<>();
|
||||
|
||||
public static final IFPlayerPreference<BooleanState> VIEW_ANIMATED_MAPS = register(new IFPlayerPreference<>("VIEW_ANIMATED_MAPS", "viewAnimatedMaps", BooleanState.class, () -> BooleanState.STRING_VALUES_MAP, i -> BooleanState.UNSET, v -> v.getJsonValue(), j -> BooleanState.fromJsonValue(j), s -> StringDeserializerResult.accepted(BooleanState.fromString(s))));
|
||||
public static final IFPlayerPreference<LanguageState> LANGUAGE = register(new IFPlayerPreference<>("LANGUAGE", "language", LanguageState.class, LanguageState.VALUES_MAP_SUPPLIER, i -> LanguageState.UNSET, v -> v.getJsonValue(), j -> LanguageState.fromJsonValue(j), s -> StringDeserializerResult.accepted(LanguageState.fromString(s))));
|
||||
|
||||
public static <T extends IFPlayerPreference<?>> T register(T preference) {
|
||||
VALUES.put(preference.name(), preference);
|
||||
return preference;
|
||||
}
|
||||
|
||||
public static Collection<IFPlayerPreference<?>> values() {
|
||||
return Collections.unmodifiableCollection(VALUES.values());
|
||||
}
|
||||
|
||||
public static IFPlayerPreference<?> valueOf(String name) {
|
||||
return VALUES.get(name.toUpperCase());
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final String jsonName;
|
||||
private final Class<T> type;
|
||||
private final Supplier<Map<String, T>> suggestedValues;
|
||||
private final Function<IFPlayer, T> defaultValue;
|
||||
private final Function<T, JsonElement> serializer;
|
||||
private final Function<JsonElement, T> deserializer;
|
||||
private final Function<String, StringDeserializerResult<T>> stringDeserializer;
|
||||
|
||||
IFPlayerPreference(String name, String jsonName, Class<T> type, Supplier<Map<String, T>> suggestedValues, Function<IFPlayer, T> defaultValue, Function<T, JsonElement> serializer, Function<JsonElement, T> deserializer, Function<String, StringDeserializerResult<T>> stringDeserializer) {
|
||||
this.name = name;
|
||||
this.jsonName = jsonName;
|
||||
this.type = type;
|
||||
this.suggestedValues = suggestedValues;
|
||||
this.defaultValue = defaultValue;
|
||||
this.serializer = serializer;
|
||||
this.deserializer = deserializer;
|
||||
this.stringDeserializer = stringDeserializer;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getJsonName() {
|
||||
return jsonName;
|
||||
}
|
||||
|
||||
public Class<T> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Map<String, T> getSuggestedValues() {
|
||||
return suggestedValues.get();
|
||||
}
|
||||
|
||||
public Function<T, JsonElement> getSerializer() {
|
||||
return serializer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends PreferenceState> Function<E, JsonElement> getSerializer(Class<E> type) {
|
||||
return (Function<E, JsonElement>) serializer;
|
||||
}
|
||||
|
||||
public Function<JsonElement, T> getDeserializer() {
|
||||
return deserializer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends PreferenceState> Function<JsonElement, E> getDeserializer(Class<E> type) {
|
||||
return (Function<JsonElement, E>) deserializer;
|
||||
}
|
||||
|
||||
public Function<String, StringDeserializerResult<T>> getStringDeserializer() {
|
||||
return stringDeserializer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends PreferenceState> Function<String, StringDeserializerResult<E>> getStringDeserializer(Class<E> type) {
|
||||
return (Function<String, StringDeserializerResult<E>>) (Function<String, ?>) stringDeserializer;
|
||||
}
|
||||
|
||||
public T getDefaultValue(IFPlayer ifPlayer) {
|
||||
return defaultValue.apply(ifPlayer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends PreferenceState> E getDefaultValue(IFPlayer ifPlayer, Class<E> type) {
|
||||
return (E) defaultValue.apply(ifPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IFPlayerPreference<?> that = (IFPlayerPreference<?>) o;
|
||||
return Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name);
|
||||
}
|
||||
|
||||
public static class StringDeserializerResult<T extends PreferenceState> {
|
||||
|
||||
public static StringDeserializerResult<?> REJECTED_INSTANCE = new StringDeserializerResult<>(false, null);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends PreferenceState> StringDeserializerResult<T> rejected() {
|
||||
return (StringDeserializerResult<T>) REJECTED_INSTANCE;
|
||||
}
|
||||
|
||||
public static <T extends PreferenceState> StringDeserializerResult<T> accepted(T result) {
|
||||
return new StringDeserializerResult<>(true, result);
|
||||
}
|
||||
|
||||
private final boolean accepted;
|
||||
private final T value;
|
||||
|
||||
private StringDeserializerResult(boolean accepted, T value) {
|
||||
this.accepted = accepted;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean isAccepted() {
|
||||
return accepted;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.nms.NMS;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import com.loohp.imageframe.utils.PlayerUtils;
|
||||
import com.loohp.imageframe.utils.StringUtils;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Rotation;
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapCanvas;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapRenderer;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class ImageMap {
|
||||
|
||||
public static final UUID CONSOLE_CREATOR = new UUID(0, 0);
|
||||
public static final String CONSOLE_CREATOR_NAME = "Console";
|
||||
public static final String UNKNOWN_CREATOR_NAME = "???";
|
||||
|
||||
protected final ImageMapManager manager;
|
||||
protected final ImageMapLoader<?, ?> loader;
|
||||
|
||||
protected int imageIndex;
|
||||
protected String name;
|
||||
protected final List<MapView> mapViews;
|
||||
protected final List<Integer> mapIds;
|
||||
protected final List<Map<String, MapCursor>> mapMarkers;
|
||||
protected final int width;
|
||||
protected final int height;
|
||||
protected DitheringType ditheringType;
|
||||
protected UUID creator;
|
||||
protected ImageMapAccessControl accessControl;
|
||||
protected final long creationTime;
|
||||
|
||||
protected final ImageMapCacheControlTask cacheControlTask;
|
||||
private boolean isValid;
|
||||
|
||||
public ImageMap(ImageMapManager manager, ImageMapLoader<?, ?> loader, int imageIndex, String name, List<MapView> mapViews, List<Integer> mapIds, List<Map<String, MapCursor>> mapMarkers, int width, int height, DitheringType ditheringType, UUID creator, Map<UUID, ImageMapAccessPermissionType> hasAccess, long creationTime) {
|
||||
if (mapViews.size() != width * height) {
|
||||
throw new IllegalArgumentException("mapViews size does not equal width * height");
|
||||
}
|
||||
if (mapViews.size() != mapMarkers.size()) {
|
||||
throw new IllegalArgumentException("mapViews size does not equal mapMarkers size");
|
||||
}
|
||||
if (mapViews.size() != mapIds.size()) {
|
||||
throw new IllegalArgumentException("mapViews size does not equal mapIds size");
|
||||
}
|
||||
this.manager = manager;
|
||||
this.loader = loader;
|
||||
this.imageIndex = imageIndex;
|
||||
this.name = StringUtils.sanitize(name);
|
||||
this.mapViews = Collections.unmodifiableList(mapViews);
|
||||
this.mapIds = Collections.unmodifiableList(mapIds);
|
||||
this.mapMarkers = Collections.unmodifiableList(mapMarkers);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.ditheringType = ditheringType;
|
||||
this.creator = creator;
|
||||
this.accessControl = new ImageMapAccessControl(this, hasAccess);
|
||||
this.creationTime = creationTime;
|
||||
|
||||
this.cacheControlTask = ImageFrame.cacheControlMode.newInstance(this);
|
||||
this.isValid = true;
|
||||
|
||||
this.accessControl.setPermissionWithoutSave(creator, null);
|
||||
}
|
||||
|
||||
public ImageMapManager getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
public Key getType() {
|
||||
return loader.getIdentifier();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public String getLegacyType() {
|
||||
return loader.getLegacyType();
|
||||
}
|
||||
|
||||
public boolean applyUpdate(JsonObject json) {
|
||||
this.name = json.has("name") ? json.get("name").getAsString() : "Unnamed";
|
||||
this.creator = UUID.fromString(json.get("creator").getAsString());
|
||||
DitheringType previousDitheringType = ditheringType;
|
||||
this.ditheringType = DitheringType.fromName(json.has("ditheringType") ? json.get("ditheringType").getAsString() : null);
|
||||
|
||||
if (json.has("hasAccess")) {
|
||||
JsonObject accessJson = json.get("hasAccess").getAsJsonObject();
|
||||
Map<UUID, ImageMapAccessPermissionType> hasAccess = new HashMap<>(accessJson.size());
|
||||
for (Map.Entry<String, JsonElement> entry : accessJson.entrySet()) {
|
||||
hasAccess.put(UUID.fromString(entry.getKey()), ImageMapAccessPermissionType.valueOf(entry.getValue().getAsString().toUpperCase()));
|
||||
}
|
||||
this.accessControl.reapply(hasAccess);
|
||||
}
|
||||
|
||||
JsonArray mapDataJson = json.get("mapdata").getAsJsonArray();
|
||||
int i = 0;
|
||||
for (JsonElement dataJson : mapDataJson) {
|
||||
JsonObject jsonObject = dataJson.getAsJsonObject();
|
||||
Map<String, MapCursor> mapCursors = new ConcurrentHashMap<>();
|
||||
if (jsonObject.has("markers")) {
|
||||
JsonArray markerArray = jsonObject.get("markers").getAsJsonArray();
|
||||
for (JsonElement element : markerArray) {
|
||||
JsonObject markerData = element.getAsJsonObject();
|
||||
String markerName = markerData.get("name").getAsString();
|
||||
byte x = markerData.get("x").getAsByte();
|
||||
byte y = markerData.get("y").getAsByte();
|
||||
MapCursor.Type type = MapCursor.Type.valueOf(markerData.get("type").getAsString().toUpperCase());
|
||||
byte direction = markerData.get("direction").getAsByte();
|
||||
boolean visible = markerData.get("visible").getAsBoolean();
|
||||
JsonElement caption = markerData.get("caption");
|
||||
mapCursors.put(markerName, new MapCursor(x, y, direction, type, visible, caption.isJsonNull() ? null : caption.getAsString()));
|
||||
}
|
||||
}
|
||||
Map<String, MapCursor> markers = this.mapMarkers.get(i++);
|
||||
markers.clear();
|
||||
markers.putAll(mapCursors);
|
||||
}
|
||||
|
||||
return previousDitheringType != ditheringType;
|
||||
}
|
||||
|
||||
protected abstract void loadColorCache();
|
||||
|
||||
protected void reloadColorCache() {
|
||||
if (hasColorCached()) {
|
||||
loadColorCache();
|
||||
} else {
|
||||
cacheControlTask.loadCacheIfManual();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract boolean hasColorCached();
|
||||
|
||||
protected abstract void unloadColorCache();
|
||||
|
||||
public BufferedImage getOriginalImage(int mapId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getImageIndex() {
|
||||
return imageIndex;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void rename(String name) throws Exception {
|
||||
this.name = StringUtils.sanitize(name);
|
||||
save();
|
||||
}
|
||||
|
||||
public List<Integer> getMapIds() {
|
||||
return mapIds;
|
||||
}
|
||||
|
||||
protected void markInvalid() {
|
||||
this.isValid = false;
|
||||
this.cacheControlTask.close();
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
for (MapView mapView : mapViews) {
|
||||
for (MapRenderer mapRenderer : mapView.getRenderers()) {
|
||||
mapView.removeRenderer(mapRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requiresAnimationService() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getCurrentPositionInSequenceWithOffset() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean isAnimationPaused() {
|
||||
throw new UnsupportedOperationException("this map does not require animation");
|
||||
}
|
||||
|
||||
public void setAnimationPause(boolean pause) throws Exception {
|
||||
throw new UnsupportedOperationException("this map does not require animation");
|
||||
}
|
||||
|
||||
public void setCurrentPositionInSequence(int position) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
public void setAnimationPlaybackTime(double seconds) throws Exception {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
public int getCurrentPositionInSequence() {
|
||||
return getCurrentPositionInSequenceWithOffset() % getSequenceLength();
|
||||
}
|
||||
|
||||
public int getSequenceLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public byte[] getRawAnimationColors(int currentTick, int index) {
|
||||
throw new UnsupportedOperationException("this map does not require animation");
|
||||
}
|
||||
|
||||
public int getAnimationFakeMapId(int currentTick, int index, boolean lookbehind) {
|
||||
throw new UnsupportedOperationException("this map does not require animation");
|
||||
}
|
||||
|
||||
public void sendAnimationFakeMaps(Collection<? extends Player> players, MapPacketSentCallback completionCallback) {
|
||||
throw new UnsupportedOperationException("this map does not require animation");
|
||||
}
|
||||
|
||||
public Set<Integer> getFakeMapIds() {
|
||||
throw new UnsupportedOperationException("this map does not require animation");
|
||||
}
|
||||
|
||||
public boolean trackDeletedMaps() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public DitheringType getDitheringType() {
|
||||
return ditheringType == null ? DitheringType.NEAREST_COLOR : ditheringType;
|
||||
}
|
||||
|
||||
public void setDitheringType(DitheringType ditheringType) throws Exception {
|
||||
this.ditheringType = ditheringType;
|
||||
save();
|
||||
}
|
||||
|
||||
public abstract ImageMap deepClone(String name, UUID creator) throws Exception;
|
||||
|
||||
public abstract void update(boolean save) throws Exception;
|
||||
|
||||
public void update() throws Exception {
|
||||
update(true);
|
||||
}
|
||||
|
||||
public void send(Player player) {
|
||||
send(Collections.singleton(player));
|
||||
}
|
||||
|
||||
public void send(Collection<? extends Player> players) {
|
||||
for (MapView mapView : mapViews) {
|
||||
MapUtils.sendImageMap(mapView, players);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws Exception {
|
||||
save(manager.getStorage(), false);
|
||||
}
|
||||
|
||||
public abstract void save(ImageFrameStorage storage, boolean saveAsCopy) throws Exception;
|
||||
|
||||
public ItemStack getMap(int x, int y, String mapNameFormat) {
|
||||
return getMap(x, y, mapNameFormat, itemStack -> itemStack);
|
||||
}
|
||||
|
||||
public ItemStack getMap(int x, int y, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
|
||||
if (x >= width || y >= height) {
|
||||
throw new IndexOutOfBoundsException("x, y position out of image map size");
|
||||
}
|
||||
MapView mapView = mapViews.get(y * width + x);
|
||||
ItemStack itemStack = new ItemStack(Material.FILLED_MAP);
|
||||
MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
|
||||
mapMeta.setMapView(mapView);
|
||||
mapMeta.setLore(Collections.singletonList(mapNameFormat
|
||||
.replace("{ImageID}", getImageIndex() + "")
|
||||
.replace("{X}", x + "")
|
||||
.replace("{Y}", y + "")
|
||||
.replace("{Name}", getName())
|
||||
.replace("{Width}", getWidth() + "")
|
||||
.replace("{Height}", getHeight() + "")
|
||||
.replace("{DitheringType}", getDitheringType().getName())
|
||||
.replace("{CreatorName}", getCreatorName())
|
||||
.replace("{CreatorUUID}", getCreator().toString())
|
||||
.replace("{TimeCreated}", ImageFrame.dateFormat.format(new Date(getCreationTime())))));
|
||||
itemStack.setItemMeta(mapMeta);
|
||||
return postCreationFunction.apply(itemStack);
|
||||
}
|
||||
|
||||
public List<ItemStack> getMaps(String mapNameFormat) {
|
||||
return getMaps(mapNameFormat, itemStack -> itemStack);
|
||||
}
|
||||
|
||||
public List<ItemStack> getMaps(String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
|
||||
List<ItemStack> maps = new ArrayList<>(mapViews.size());
|
||||
int i = 0;
|
||||
for (MapView mapView : mapViews) {
|
||||
ItemStack itemStack = new ItemStack(Material.FILLED_MAP);
|
||||
MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
|
||||
mapMeta.setMapView(mapView);
|
||||
mapMeta.setLore(Collections.singletonList(mapNameFormat
|
||||
.replace("{ImageID}", getImageIndex() + "")
|
||||
.replace("{X}", (i % width) + "")
|
||||
.replace("{Y}", (i / width) + "")
|
||||
.replace("{Name}", getName())
|
||||
.replace("{Width}", getWidth() + "")
|
||||
.replace("{Height}", getHeight() + "")
|
||||
.replace("{DitheringType}", getDitheringType().getName())
|
||||
.replace("{CreatorName}", getCreatorName())
|
||||
.replace("{CreatorUUID}", getCreator().toString())
|
||||
.replace("{TimeCreated}", ImageFrame.dateFormat.format(new Date(getCreationTime())))));
|
||||
itemStack.setItemMeta(mapMeta);
|
||||
maps.add(postCreationFunction.apply(itemStack));
|
||||
i++;
|
||||
}
|
||||
return maps;
|
||||
}
|
||||
|
||||
public void giveMap(Player player, int x, int y, String mapNameFormat) {
|
||||
giveMap(Collections.singleton(player), x, y, mapNameFormat, itemStack -> itemStack);
|
||||
}
|
||||
|
||||
public void giveMap(Player player, int x, int y, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
|
||||
giveMap(Collections.singleton(player), x, y, mapNameFormat, postCreationFunction);
|
||||
}
|
||||
|
||||
public void giveMap(Collection<? extends Player> players, int x, int y, String mapNameFormat) {
|
||||
giveMap(players, x, y, mapNameFormat, itemStack -> itemStack);
|
||||
}
|
||||
|
||||
public void giveMap(Collection<? extends Player> players, int x, int y, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
|
||||
ItemStack map = getMap(x, y, mapNameFormat, postCreationFunction);
|
||||
for (Player player : players) {
|
||||
PlayerUtils.giveItem(player, map.clone());
|
||||
}
|
||||
}
|
||||
|
||||
public void giveMaps(Player player, String mapNameFormat) {
|
||||
giveMaps(Collections.singleton(player), mapNameFormat, itemStack -> itemStack);
|
||||
}
|
||||
|
||||
public void giveMaps(Player player, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
|
||||
giveMaps(Collections.singleton(player), mapNameFormat, postCreationFunction);
|
||||
}
|
||||
|
||||
public void giveMaps(Collection<? extends Player> players, String mapNameFormat) {
|
||||
giveMaps(players, mapNameFormat, itemStack -> itemStack);
|
||||
}
|
||||
|
||||
public void giveMaps(Collection<? extends Player> players, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
|
||||
List<ItemStack> maps = getMaps(mapNameFormat, postCreationFunction);
|
||||
for (ItemStack map : maps) {
|
||||
for (Player player : players) {
|
||||
PlayerUtils.giveItem(player, map.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void fillItemFrames(List<ItemFrame> itemFrames, Rotation rotation, BiPredicate<ItemFrame, ItemStack> prePlaceCheck, BiConsumer<ItemFrame, ItemStack> unableToPlaceAction, String mapNameFormat) {
|
||||
fillItemFrames(itemFrames, rotation, prePlaceCheck, unableToPlaceAction, mapNameFormat, itemStack -> itemStack, null);
|
||||
}
|
||||
|
||||
public void fillItemFrames(List<ItemFrame> itemFrames, Rotation rotation, BiPredicate<ItemFrame, ItemStack> prePlaceCheck, BiConsumer<ItemFrame, ItemStack> unableToPlaceAction, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction, Runnable onComplete) {
|
||||
if (itemFrames.size() != mapViews.size()) {
|
||||
throw new IllegalArgumentException("itemFrames size does not equal to mapView size");
|
||||
}
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
List<ItemStack> items = getMaps(mapNameFormat, postCreationFunction);
|
||||
Iterator<ItemFrame> itr0 = itemFrames.iterator();
|
||||
Iterator<ItemStack> itr1 = items.iterator();
|
||||
while (itr0.hasNext() && itr1.hasNext()) {
|
||||
ItemFrame frame = itr0.next();
|
||||
ItemStack item = itr1.next();
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
futures.add(future);
|
||||
Scheduler.runTask(ImageFrame.plugin, () -> {
|
||||
try {
|
||||
if (frame.isValid()) {
|
||||
if (prePlaceCheck.test(frame, item)) {
|
||||
frame.setItem(item, false);
|
||||
frame.setRotation(rotation);
|
||||
// Hide the item frame's own border so adjacent map tiles tile
|
||||
// seamlessly with no thin pixel border between them.
|
||||
frame.setVisible(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
unableToPlaceAction.accept(frame, item);
|
||||
} finally {
|
||||
future.complete(null);
|
||||
}
|
||||
}, frame);
|
||||
}
|
||||
if (onComplete != null) {
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
Scheduler.runTask(ImageFrame.plugin, onComplete);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Player> getViewers() {
|
||||
return mapViews.stream().flatMap(m -> NMS.getInstance().getViewers(m).stream()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public boolean hasViewers() {
|
||||
return mapViews.stream().anyMatch(m -> NMS.getInstance().hasViewers(m));
|
||||
}
|
||||
|
||||
public UUID getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
public void changeCreator(UUID creator) throws Exception {
|
||||
this.creator = creator;
|
||||
this.accessControl.setPermissionWithoutSave(creator, null);
|
||||
save();
|
||||
}
|
||||
|
||||
public ImageMapAccessControl getAccessControl() {
|
||||
return accessControl;
|
||||
}
|
||||
|
||||
public String getCreatorName() {
|
||||
if (creator.equals(CONSOLE_CREATOR)) {
|
||||
return CONSOLE_CREATOR_NAME;
|
||||
}
|
||||
String name = Bukkit.getOfflinePlayer(creator).getName();
|
||||
return name == null ? UNKNOWN_CREATOR_NAME : name;
|
||||
}
|
||||
|
||||
public long getCreationTime() {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
public List<MapView> getMapViews() {
|
||||
return mapViews;
|
||||
}
|
||||
|
||||
public MapView getMapViewFromMapId(int mapId) {
|
||||
return mapViews.get(mapIds.indexOf(mapId));
|
||||
}
|
||||
|
||||
public List<Map<String, MapCursor>> getMapMarkers() {
|
||||
return mapMarkers;
|
||||
}
|
||||
|
||||
public Map<String, MapCursor> getMapMarkers(MapView mapView) {
|
||||
return mapMarkers.get(mapViews.indexOf(mapView));
|
||||
}
|
||||
|
||||
public MapCursor getMapMarker(String name) {
|
||||
return mapMarkers.stream().flatMap(each -> each.entrySet().stream()).filter(each -> each.getKey().equalsIgnoreCase(name)).findFirst().map(each -> each.getValue()).orElse(null);
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public static abstract class ImageMapRenderer extends MapRenderer {
|
||||
|
||||
protected final ImageMapManager manager;
|
||||
protected final ImageMap imageMap;
|
||||
protected final int index;
|
||||
|
||||
public ImageMapRenderer(ImageMapManager manager, ImageMap imageMap, int index) {
|
||||
super(ImageFrame.mapRenderersContextual);
|
||||
this.manager = manager;
|
||||
this.imageMap = imageMap;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MapView mapView, MapCanvas canvas, Player player) {
|
||||
MutablePair<byte[], Collection<MapCursor>> renderData = renderMap(mapView, 0, player);
|
||||
manager.callRenderEventListener(manager, imageMap, mapView, player, renderData);
|
||||
byte[] colors = renderData.getFirst();
|
||||
if (colors != null) {
|
||||
for (int i = 0; i < colors.length; i++) {
|
||||
canvas.setPixel(i % MapUtils.MAP_WIDTH, i / MapUtils.MAP_WIDTH, colors[i]);
|
||||
}
|
||||
}
|
||||
canvas.setCursors(MapUtils.toMapCursorCollection(renderData.getSecond()));
|
||||
}
|
||||
|
||||
public MutablePair<byte[], Collection<MapCursor>> renderPacketData(MapView mapView, int currentTick, Player player) {
|
||||
MutablePair<byte[], Collection<MapCursor>> renderData = renderMap(mapView, currentTick, player);
|
||||
manager.callRenderEventListener(manager, imageMap, mapView, player, renderData);
|
||||
return renderData;
|
||||
}
|
||||
|
||||
public MutablePair<byte[], Collection<MapCursor>> renderPacketData(MapView mapView, Player player) {
|
||||
MutablePair<byte[], Collection<MapCursor>> renderData = renderMap(mapView, player);
|
||||
manager.callRenderEventListener(manager, imageMap, mapView, player, renderData);
|
||||
return renderData;
|
||||
}
|
||||
|
||||
public MutablePair<byte[], Collection<MapCursor>> renderMap(MapView mapView, int currentTick, Player player) {
|
||||
return renderMap(mapView, player);
|
||||
}
|
||||
|
||||
public abstract MutablePair<byte[], Collection<MapCursor>> renderMap(MapView mapView, Player player);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ImageMapAccessControl {
|
||||
|
||||
public static final UUID EVERYONE = new UUID(-1, -1);
|
||||
|
||||
private final ImageMap imageMap;
|
||||
private final Map<UUID, ImageMapAccessPermissionType> permissions;
|
||||
|
||||
public ImageMapAccessControl(ImageMap imageMap, Map<UUID, ImageMapAccessPermissionType> hasAccess) {
|
||||
this.imageMap = imageMap;
|
||||
this.permissions = new ConcurrentHashMap<>(hasAccess);
|
||||
}
|
||||
|
||||
public ImageMapAccessPermissionType getPermission(UUID player) {
|
||||
if (player.equals(imageMap.getCreator())) {
|
||||
return ImageMapAccessPermissionType.ALL;
|
||||
}
|
||||
return permissions.get(player);
|
||||
}
|
||||
|
||||
public ImageMapAccessPermissionType getPermissionForEveryone() {
|
||||
return getPermission(EVERYONE);
|
||||
}
|
||||
|
||||
public Map<UUID, ImageMapAccessPermissionType> getPermissions() {
|
||||
return Collections.unmodifiableMap(permissions);
|
||||
}
|
||||
|
||||
public boolean hasPermission(UUID player, ImageMapAccessPermissionType permissionType) {
|
||||
if (permissionType == null) {
|
||||
return true;
|
||||
}
|
||||
if (player.equals(imageMap.getCreator())) {
|
||||
return true;
|
||||
}
|
||||
if (!player.equals(EVERYONE) && hasPermissionForEveryone(permissionType)) {
|
||||
return true;
|
||||
}
|
||||
ImageMapAccessPermissionType type = permissions.get(player);
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
return type.containsPermission(permissionType);
|
||||
}
|
||||
|
||||
public boolean hasPermissionForEveryone(ImageMapAccessPermissionType permissionType) {
|
||||
return hasPermission(EVERYONE, permissionType);
|
||||
}
|
||||
|
||||
public void setPermissionForEveryone(ImageMapAccessPermissionType permissionType) throws Exception {
|
||||
setPermission(EVERYONE, permissionType);
|
||||
}
|
||||
|
||||
protected void setPermissionForEveryoneWithoutSave(ImageMapAccessPermissionType permissionType) {
|
||||
setPermissionWithoutSave(EVERYONE, permissionType);
|
||||
}
|
||||
|
||||
public void setPermission(UUID player, ImageMapAccessPermissionType permissionType) throws Exception {
|
||||
setPermission(player, permissionType, true);
|
||||
}
|
||||
|
||||
protected void setPermissionWithoutSave(UUID player, ImageMapAccessPermissionType permissionType) {
|
||||
try {
|
||||
setPermission(player, permissionType, false);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void reapply(Map<UUID, ImageMapAccessPermissionType> hasAccess) {
|
||||
this.permissions.clear();
|
||||
this.permissions.putAll(hasAccess);
|
||||
}
|
||||
|
||||
private void setPermission(UUID player, ImageMapAccessPermissionType permissionType, boolean save) throws Exception {
|
||||
if (player.equals(imageMap.getCreator())) {
|
||||
return;
|
||||
}
|
||||
if (permissionType == null) {
|
||||
permissions.remove(player);
|
||||
} else {
|
||||
permissions.put(player, permissionType);
|
||||
}
|
||||
if (save) {
|
||||
imageMap.save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ImageMapAccessPermissionType implements Comparable<ImageMapAccessPermissionType> {
|
||||
|
||||
private static final Map<String, ImageMapAccessPermissionType> TYPES = new ConcurrentHashMap<>();
|
||||
|
||||
public static final ImageMapAccessPermissionType GET = register(new ImageMapAccessPermissionType("GET"));
|
||||
public static final ImageMapAccessPermissionType ADJUST_PLAYBACK = register(new ImageMapAccessPermissionType("ADJUST_PLAYBACK", GET));
|
||||
public static final ImageMapAccessPermissionType MARKER = register(new ImageMapAccessPermissionType("MARKER", GET));
|
||||
public static final ImageMapAccessPermissionType EDIT = register(new ImageMapAccessPermissionType("EDIT", MARKER));
|
||||
public static final ImageMapAccessPermissionType EDIT_CLONE = register(new ImageMapAccessPermissionType("EDIT_CLONE", EDIT));
|
||||
public static final ImageMapAccessPermissionType ALL = register(new ImageMapAccessPermissionType("ALL", EDIT_CLONE));
|
||||
|
||||
/**
|
||||
* Special case type: All registered ImageMapAccessPermissionTypes inherits this
|
||||
*/
|
||||
public static final ImageMapAccessPermissionType BASE = new ImageMapAccessPermissionType("BASE") {
|
||||
@Override
|
||||
public boolean containsPermission(ImageMapAccessPermissionType type) {
|
||||
return TYPES.containsKey(type.name());
|
||||
}
|
||||
};
|
||||
|
||||
public static ImageMapAccessPermissionType register(ImageMapAccessPermissionType type) {
|
||||
TYPES.put(type.name(), type);
|
||||
return type;
|
||||
}
|
||||
|
||||
public static ImageMapAccessPermissionType valueOf(String name) {
|
||||
ImageMapAccessPermissionType type = TYPES.get(name);
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
throw new IllegalArgumentException(name + " is not a registered ImageMapAccessPermissionType");
|
||||
}
|
||||
|
||||
public static Map<String, ImageMapAccessPermissionType> values() {
|
||||
return Collections.unmodifiableMap(TYPES);
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final Set<ImageMapAccessPermissionType> inheritance;
|
||||
|
||||
public ImageMapAccessPermissionType(String name, ImageMapAccessPermissionType... inheritance) {
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("name cannot be null");
|
||||
}
|
||||
this.name = name;
|
||||
this.inheritance = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(inheritance)));
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean containsPermission(ImageMapAccessPermissionType type) {
|
||||
if (this.equals(type)) {
|
||||
return true;
|
||||
}
|
||||
for (ImageMapAccessPermissionType inheritedType : inheritance) {
|
||||
if (!inheritedType.equals(this) && inheritedType.containsPermission(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ImageMapAccessPermissionType type = (ImageMapAccessPermissionType) o;
|
||||
return name.equals(type.name) && inheritance.equals(type.inheritance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, inheritance);
|
||||
}
|
||||
|
||||
private int calculateDepth() {
|
||||
if (this == BASE) {
|
||||
return 0;
|
||||
}
|
||||
return 1 + inheritance.stream().mapToInt(ImageMapAccessPermissionType::calculateDepth).max().orElse(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ImageMapAccessPermissionType o) {
|
||||
if (this == BASE && o == BASE) {
|
||||
return 0;
|
||||
}
|
||||
if (this == BASE) {
|
||||
return -1;
|
||||
}
|
||||
if (o == BASE) {
|
||||
return 1;
|
||||
}
|
||||
int depthThis = this.calculateDepth();
|
||||
int depthOther = o.calculateDepth();
|
||||
int diff = Integer.compare(depthThis, depthOther);
|
||||
if (diff != 0) {
|
||||
return diff;
|
||||
}
|
||||
return this.name.compareTo(o.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ImageMapCacheControlMode<T extends ImageMapCacheControlTask> {
|
||||
|
||||
private static final Map<String, ImageMapCacheControlMode<?>> MODES = new HashMap<>();
|
||||
|
||||
public static final ImageMapCacheControlMode<ImageMapDynamicCacheControlTask> DYNAMIC = register(new ImageMapCacheControlMode<>("DYNAMIC", ImageMapDynamicCacheControlTask.class, ImageMapDynamicCacheControlTask::new));
|
||||
public static final ImageMapCacheControlMode<ImageMapManualPersistentCacheControlTask> MANUAL_PERSISTENT = register(new ImageMapCacheControlMode<>("MANUAL_PERSISTENT", ImageMapManualPersistentCacheControlTask.class, ImageMapManualPersistentCacheControlTask::new));
|
||||
|
||||
public static <T extends ImageMapCacheControlTask> ImageMapCacheControlMode<T> register(ImageMapCacheControlMode<T> cacheControlTasks) {
|
||||
MODES.put(cacheControlTasks.getIdentifier(), cacheControlTasks);
|
||||
return cacheControlTasks;
|
||||
}
|
||||
|
||||
public static Map<String, ImageMapCacheControlMode<?>> values() {
|
||||
return Collections.unmodifiableMap(MODES);
|
||||
}
|
||||
|
||||
public static ImageMapCacheControlMode<?> valueOf(String identifier) {
|
||||
return MODES.get(identifier);
|
||||
}
|
||||
|
||||
private final String identifier;
|
||||
private final Class<T> clazz;
|
||||
private final Function<ImageMap, T> constructor;
|
||||
|
||||
public ImageMapCacheControlMode(String identifier, Class<T> clazz, Function<ImageMap, T> constructor) {
|
||||
this.identifier = identifier;
|
||||
this.clazz = clazz;
|
||||
this.constructor = constructor;
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public Class<T> getClazz() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public T newInstance(ImageMap imageMap) {
|
||||
return constructor.apply(imageMap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
public interface ImageMapCacheControlTask extends AutoCloseable {
|
||||
|
||||
ImageMap getImageMap();
|
||||
|
||||
void loadCacheIfManual();
|
||||
|
||||
boolean isClosed();
|
||||
|
||||
@Override
|
||||
void close();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ImageMapCreateInfo {
|
||||
|
||||
private final ImageMapManager manager;
|
||||
private final String name;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final DitheringType ditheringType;
|
||||
private final UUID creator;
|
||||
|
||||
public ImageMapCreateInfo(ImageMapManager manager, String name, int width, int height, DitheringType ditheringType, UUID creator) {
|
||||
this.manager = manager;
|
||||
this.name = name;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.ditheringType = ditheringType;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
public ImageMapManager getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public DitheringType getDitheringType() {
|
||||
return ditheringType;
|
||||
}
|
||||
|
||||
public UUID getCreator() {
|
||||
return creator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.language.TranslationKey;
|
||||
import com.loohp.imageframe.utils.CommandSenderUtils;
|
||||
import com.loohp.imageframe.utils.ThrowingSupplier;
|
||||
import com.loohp.platformscheduler.ScheduledTask;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class ImageMapCreationTask<T> extends CompletableFuture<T> implements Runnable {
|
||||
|
||||
private final ImageMapCreationTaskManager manager;
|
||||
private final UUID creationTaskId;
|
||||
private final UUID creator;
|
||||
private final ThrowingSupplier<T> creationTask;
|
||||
|
||||
private final ScheduledTask monitorTask;
|
||||
private final ComponentTemplate queuingMessageTemplate;
|
||||
private final ComponentTemplate processingMessageTemplate;
|
||||
|
||||
private final AtomicBoolean queuing;
|
||||
private final AtomicInteger tick;
|
||||
private final AtomicInteger processingStart;
|
||||
|
||||
private final Future<?> submission;
|
||||
|
||||
public ImageMapCreationTask(ImageMapCreationTaskManager manager, UUID creator, ThrowingSupplier<T> creationTask, ExecutorService executor, String imageMapName) {
|
||||
this.manager = manager;
|
||||
this.creationTaskId = UUID.randomUUID();
|
||||
this.creator = creator;
|
||||
this.creationTask = creationTask;
|
||||
|
||||
this.manager.getTaskInQueue().add(this);
|
||||
|
||||
this.queuingMessageTemplate = ComponentTemplate.template(TranslationKey.IMAGE_MAP_QUEUED_ACTION_BAR, imageMapName);
|
||||
this.processingMessageTemplate = ComponentTemplate.template(TranslationKey.IMAGE_MAP_PROCESSING_ACTION_BAR, imageMapName);
|
||||
this.monitorTask = Scheduler.runTaskTimerAsynchronously(ImageFrame.plugin, new MonitorTask(), 0, 10);
|
||||
|
||||
this.tick = new AtomicInteger(0);
|
||||
this.processingStart = new AtomicInteger(-1);
|
||||
this.queuing = new AtomicBoolean(true);
|
||||
|
||||
this.submission = executor.submit(this);
|
||||
}
|
||||
|
||||
public ImageMapCreationTaskManager getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
public UUID getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
public UUID getCreationTaskId() {
|
||||
return creationTaskId;
|
||||
}
|
||||
|
||||
public boolean isQueuing() {
|
||||
return queuing.get();
|
||||
}
|
||||
|
||||
public int getPositionInQueue() {
|
||||
return manager.getPositionInQueue(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.manager.getTaskInQueue().remove(this);
|
||||
this.queuing.set(false);
|
||||
try {
|
||||
complete(creationTask.get());
|
||||
} catch (Throwable e) {
|
||||
completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return monitorTask.isCancelled();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public void complete(Component message) {
|
||||
if (!isCompleted()) {
|
||||
monitorTask.cancel();
|
||||
manager.getCreatorsInQueue().remove(creator);
|
||||
Player player = Bukkit.getPlayer(creator);
|
||||
if (player != null) {
|
||||
CommandSenderUtils.sendMessage(player, ChatMessageType.ACTION_BAR, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MonitorTask implements Runnable {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void run() {
|
||||
processingStart.updateAndGet(currentProcessingStart -> {
|
||||
int currentTick = tick.get();
|
||||
if (currentProcessingStart < 0) {
|
||||
if (!queuing.get()) {
|
||||
return currentTick;
|
||||
}
|
||||
} else {
|
||||
if (currentTick - currentProcessingStart > (ImageFrame.maxProcessingTime * 20)) {
|
||||
submission.cancel(true);
|
||||
}
|
||||
}
|
||||
return currentProcessingStart;
|
||||
});
|
||||
Player player = Bukkit.getPlayer(creator);
|
||||
if (player != null) {
|
||||
TranslatableComponent message;
|
||||
if (queuing.get()) {
|
||||
int position = getPositionInQueue() + 1;
|
||||
message = queuingMessageTemplate.build(position);
|
||||
} else {
|
||||
int dots = tick.getAndIncrement() % 4;
|
||||
message = processingMessageTemplate.build(Strings.repeat(".", dots));
|
||||
}
|
||||
if (!message.key().isEmpty()) {
|
||||
CommandSenderUtils.sendMessage(player, ChatMessageType.ACTION_BAR, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.loohp.imageframe.utils.ThrowingSupplier;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ImageMapCreationTaskManager {
|
||||
|
||||
private final ThreadPoolExecutor executor;
|
||||
private final Set<UUID> creatorsInQueue;
|
||||
private final Queue<ImageMapCreationTask<?>> taskInQueue;
|
||||
|
||||
public ImageMapCreationTaskManager(int parallelCount) {
|
||||
ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("ImageFrame Image Map Creation Thread #%d").build();
|
||||
this.executor = new ThreadPoolExecutor(parallelCount, parallelCount, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), factory);
|
||||
this.creatorsInQueue = ConcurrentHashMap.newKeySet();
|
||||
this.taskInQueue = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
|
||||
public <T> ImageMapCreationTask<T> enqueue(UUID creator, String imageMapName, ThrowingSupplier<T> creationTask) throws EnqueueRejectedException {
|
||||
if (!creatorsInQueue.add(creator)) {
|
||||
throw new EnqueueRejectedException("Creator " + creator + " already in queue");
|
||||
}
|
||||
return new ImageMapCreationTask<>(this, creator, creationTask, executor, imageMapName);
|
||||
}
|
||||
|
||||
public boolean isCreatorInQueue(UUID creator) {
|
||||
return creatorsInQueue.contains(creator);
|
||||
}
|
||||
|
||||
protected Set<UUID> getCreatorsInQueue() {
|
||||
return creatorsInQueue;
|
||||
}
|
||||
|
||||
protected Queue<ImageMapCreationTask<?>> getTaskInQueue() {
|
||||
return taskInQueue;
|
||||
}
|
||||
|
||||
public int getPositionInQueue(ImageMapCreationTask<?> creationTask) {
|
||||
int index = 0;
|
||||
for (ImageMapCreationTask<?> queuedTask : taskInQueue) {
|
||||
if (creationTask.getCreationTaskId().equals(queuedTask.getCreationTaskId())) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int getPositionInQueue(UUID creator) {
|
||||
int index = 0;
|
||||
for (ImageMapCreationTask<?> queuedTask : taskInQueue) {
|
||||
if (creator.equals(queuedTask.getCreator())) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return executor.getQueue().size();
|
||||
}
|
||||
|
||||
public static class EnqueueRejectedException extends Exception {
|
||||
|
||||
public EnqueueRejectedException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EnqueueRejectedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EnqueueRejectedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public EnqueueRejectedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.platformscheduler.ScheduledTask;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class ImageMapDynamicCacheControlTask implements ImageMapCacheControlTask, Runnable {
|
||||
|
||||
private final ImageMap imageMap;
|
||||
|
||||
private final AtomicBoolean locked;
|
||||
private final AtomicBoolean closed;
|
||||
|
||||
private final AtomicReference<ScheduledTask> task;
|
||||
private final AtomicInteger noViewerCounts;
|
||||
|
||||
public ImageMapDynamicCacheControlTask(ImageMap imageMap) {
|
||||
this.imageMap = imageMap;
|
||||
this.noViewerCounts = new AtomicInteger(0);
|
||||
this.locked = new AtomicBoolean(false);
|
||||
this.closed = new AtomicBoolean(false);
|
||||
this.task = new AtomicReference<>(Scheduler.runTaskLaterAsynchronously(ImageFrame.plugin, this, 5));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadCacheIfManual() {
|
||||
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return locked.get();
|
||||
}
|
||||
|
||||
public void setLocked(boolean locked) {
|
||||
this.locked.set(locked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
if (!locked.get()) {
|
||||
if (imageMap.hasViewers()) {
|
||||
noViewerCounts.set(0);
|
||||
if (!imageMap.hasColorCached()) {
|
||||
imageMap.loadColorCache();
|
||||
}
|
||||
} else {
|
||||
if (noViewerCounts.getAndIncrement() > 200 && imageMap.hasColorCached()) {
|
||||
imageMap.unloadColorCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.task.set(Scheduler.runTaskLaterAsynchronously(ImageFrame.plugin, this, 5));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed.set(true);
|
||||
task.updateAndGet(t -> {
|
||||
if (t != null) {
|
||||
t.cancel();
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public class ImageMapHitTargetResult {
|
||||
|
||||
private final ItemFrame itemFrame;
|
||||
private final ImageMap imageMap;
|
||||
private final IntPosition localTargetPixel;
|
||||
private final IntPosition targetPixel;
|
||||
private final Vector worldTargetPosition;
|
||||
|
||||
public ImageMapHitTargetResult(ItemFrame itemFrame, ImageMap imageMap, IntPosition localTargetPixel, IntPosition targetPixel, Vector worldTargetPosition) {
|
||||
this.itemFrame = itemFrame;
|
||||
this.imageMap = imageMap;
|
||||
this.localTargetPixel = localTargetPixel;
|
||||
this.targetPixel = targetPixel;
|
||||
this.worldTargetPosition = worldTargetPosition;
|
||||
}
|
||||
|
||||
public ItemFrame getItemFrame() {
|
||||
return itemFrame;
|
||||
}
|
||||
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
public IntPosition getLocalTargetPixel() {
|
||||
return localTargetPixel;
|
||||
}
|
||||
|
||||
public IntPosition getTargetPixel() {
|
||||
return targetPixel;
|
||||
}
|
||||
|
||||
public Vector getWorldTargetPosition() {
|
||||
return worldTargetPosition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.media.MediaFrame;
|
||||
import com.loohp.imageframe.media.MediaLoader;
|
||||
import net.kyori.adventure.key.Key;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public abstract class ImageMapLoader<T extends ImageMap, C extends ImageMapCreateInfo> {
|
||||
|
||||
protected final List<MediaLoader> mediaLoaders;
|
||||
|
||||
protected ImageMapLoader() {
|
||||
this.mediaLoaders = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
public abstract Key getIdentifier();
|
||||
|
||||
public String getLegacyType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract Class<T> getImageMapClass();
|
||||
|
||||
public abstract Class<C> getImageMapCreateInfoClass();
|
||||
|
||||
public abstract List<String> getExtraPermissions();
|
||||
|
||||
public abstract boolean isSupported(String imageType);
|
||||
|
||||
public ImageMapLoaderPriority getPriority(String imageType) {
|
||||
return ImageMapLoaderPriority.NORMAL;
|
||||
}
|
||||
|
||||
public void registerMediaLoaderFirst(MediaLoader mediaLoader) {
|
||||
mediaLoaders.add(0, mediaLoader);
|
||||
}
|
||||
|
||||
public void registerMediaLoaderLast(MediaLoader mediaLoader) {
|
||||
mediaLoaders.add(mediaLoader);
|
||||
}
|
||||
|
||||
public void registerMediaLoaderBefore(Key identifier, MediaLoader mediaLoader) {
|
||||
int index = indexOfMediaLoader(identifier);
|
||||
if (index < 0) {
|
||||
registerMediaLoaderFirst(mediaLoader);
|
||||
} else {
|
||||
mediaLoaders.add(index, mediaLoader);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerMediaLoaderAfter(Key identifier, MediaLoader mediaLoader) {
|
||||
int index = indexOfMediaLoader(identifier);
|
||||
if (index < 0) {
|
||||
registerMediaLoaderLast(mediaLoader);
|
||||
} else {
|
||||
mediaLoaders.add(index + 1, mediaLoader);
|
||||
}
|
||||
}
|
||||
|
||||
protected int indexOfMediaLoader(Key identifier) {
|
||||
for (int i = 0; i < mediaLoaders.size(); i++) {
|
||||
if (mediaLoaders.get(i).getIdentifier().equals(identifier)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Iterator<MediaFrame> tryLoadMedia(String url) throws IOException {
|
||||
List<IOException> exceptions = new ArrayList<>();
|
||||
for (MediaLoader mediaLoader : mediaLoaders) {
|
||||
try {
|
||||
if (mediaLoader.shouldTryRead(url)) {
|
||||
return mediaLoader.tryLoad(url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exceptions.add(new IOException("Unable to read or download media with MediaLoader " + mediaLoader.getIdentifier(), e));
|
||||
}
|
||||
}
|
||||
IOException e = new IOException("Unable to read or download media with ImageMapLoader " + getIdentifier().asString() + ", does this url directly links to the image? (" + url + ")");
|
||||
for (IOException ex : exceptions) {
|
||||
e.addSuppressed(ex);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
public abstract Future<T> create(C createInfo) throws Exception;
|
||||
|
||||
public abstract Future<T> load(ImageMapManager manager, JsonObject json) throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
public enum ImageMapLoaderPriority {
|
||||
|
||||
LOWEST, LOW, NORMAL, HIGH, HIGHEST;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.media.GifReaderMediaLoader;
|
||||
import com.loohp.imageframe.media.ImageIOMediaLoader;
|
||||
import com.loohp.imageframe.media.MediaLoader;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class ImageMapLoaders {
|
||||
|
||||
private static final List<ImageMapLoader<?, ?>> LOADERS_ORDERED = new CopyOnWriteArrayList<>();
|
||||
private static final Map<Key, ImageMapLoader<?, ?>> LOADERS_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
public static final URLStaticImageMapLoader URL_STATIC_IMAGE_MAP_LOADER = registerLoader(new URLStaticImageMapLoader());
|
||||
public static final URLAnimatedImageMapLoader URL_ANIMATED_IMAGE_MAP_LOADER = registerLoader(new URLAnimatedImageMapLoader());
|
||||
public static final MinecraftURLOverlayImageMapLoader MINECRAFT_URL_OVERLAY_IMAGE_MAP_LOADER = registerLoader(new MinecraftURLOverlayImageMapLoader());
|
||||
public static final NonUpdatableStaticImageMapLoader NON_UPDATABLE_IMAGE_MAP_LOADER = registerLoader(new NonUpdatableStaticImageMapLoader());
|
||||
public static final BundledStaticImageMapLoader BUNDLED_STATIC_IMAGE_MAP_LOADER = registerLoader(new BundledStaticImageMapLoader());
|
||||
public static final BundledAnimatedImageMapLoader BUNDLED_ANIMATED_IMAGE_MAP_LOADER = registerLoader(new BundledAnimatedImageMapLoader());
|
||||
|
||||
public static final GifReaderMediaLoader GIF_READER_MEDIA_LOADER = registerLoader(new GifReaderMediaLoader(), URL_ANIMATED_IMAGE_MAP_LOADER);
|
||||
public static final ImageIOMediaLoader IMAGE_IO_MEDIA_LOADER = registerLoader(new ImageIOMediaLoader(), URL_STATIC_IMAGE_MAP_LOADER, MINECRAFT_URL_OVERLAY_IMAGE_MAP_LOADER);
|
||||
|
||||
public static void init() {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
public static <T extends ImageMapLoader<?, ?>> T registerLoader(T loader) {
|
||||
LOADERS_ORDERED.add(loader);
|
||||
LOADERS_MAP.put(loader.getIdentifier(), loader);
|
||||
Bukkit.getServer().getConsoleSender().sendMessage(ChatColor.GRAY + "[ImageFrame] Registered ImageMapLoader " + loader.getIdentifier().asString());
|
||||
return loader;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends ImageMapLoader<?, ?>> T getLoader(Class<T> loaderClass) {
|
||||
return (T) LOADERS_ORDERED.stream()
|
||||
.filter(l -> loaderClass.equals(l.getClass()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends ImageMap, C extends ImageMapCreateInfo> ImageMapLoader<? extends T, C> getLoader(Class<T> imageMapClass, Class<C> createInfoClass, String imageType, CommandSender sender) {
|
||||
return (ImageMapLoader<? extends T, C>) LOADERS_ORDERED.stream()
|
||||
.filter(l -> imageMapClass.isAssignableFrom(l.getImageMapClass()))
|
||||
.filter(l -> createInfoClass.equals(l.getImageMapCreateInfoClass()))
|
||||
.filter(l -> l.isSupported(imageType))
|
||||
.filter(l -> l.getExtraPermissions().stream().allMatch(p -> sender.hasPermission(p)))
|
||||
.max(Comparator.comparing((ImageMapLoader<?, ?> l) -> l.getPriority(imageType)))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static <T extends ImageMap> ImageMapLoader<?, ?> getLoader(Key identifier) {
|
||||
return LOADERS_MAP.get(identifier);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternValidation")
|
||||
public static <T extends ImageMap> ImageMapLoader<?, ?> getLoader(String identifier) {
|
||||
if (identifier.contains(":")) {
|
||||
return getLoader(Key.key(identifier));
|
||||
}
|
||||
return LOADERS_ORDERED.stream()
|
||||
.filter(l -> identifier.equalsIgnoreCase(l.getLegacyType()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static Future<? extends ImageMap> load(ImageMapManager manager, JsonObject json) throws Exception {
|
||||
String type = json.get("type").getAsString();
|
||||
ImageMapLoader<?, ?> loader = getLoader(type);
|
||||
if (loader == null) {
|
||||
throw new IllegalStateException("Unable to find ImageMapLoader " + type);
|
||||
}
|
||||
return loader.load(manager, json);
|
||||
}
|
||||
|
||||
public static <T extends MediaLoader> T registerLoader(T loader, ImageMapLoader<?, ?>... imageMapLoaders) {
|
||||
for (ImageMapLoader<?, ?> imageMapLoader : imageMapLoaders) {
|
||||
imageMapLoader.registerMediaLoaderLast(loader);
|
||||
Bukkit.getServer().getConsoleSender().sendMessage(ChatColor.GRAY + "[ImageFrame] Registered MediaLoader " + loader.getIdentifier().asString() + " to ImageMapLoader " + imageMapLoader.getIdentifier().asString());
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.loohp.imageframe.ImageFrame;
|
||||
import com.loohp.imageframe.api.events.ImageMapAddedEvent;
|
||||
import com.loohp.imageframe.api.events.ImageMapDeletedEvent;
|
||||
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
|
||||
import com.loohp.imageframe.storage.ImageFrameStorage;
|
||||
import com.loohp.imageframe.utils.MapUtils;
|
||||
import com.loohp.platformscheduler.Scheduler;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapCanvas;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapRenderer;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ImageMapManager implements AutoCloseable {
|
||||
|
||||
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
|
||||
public static final int FAKE_MAP_ID_START_RANGE = Integer.MAX_VALUE / 4 * 3;
|
||||
public static final int BUNDLED_IMAGE_INDEX_START_RANGE = 1000000000;
|
||||
|
||||
private static final AtomicInteger FAKE_MAP_ID_COUNTER = new AtomicInteger(FAKE_MAP_ID_START_RANGE);
|
||||
|
||||
public static int getNextFakeMapId() {
|
||||
return FAKE_MAP_ID_COUNTER.getAndUpdate(i -> i < FAKE_MAP_ID_START_RANGE ? FAKE_MAP_ID_START_RANGE : i + 1);
|
||||
}
|
||||
|
||||
public static boolean isBundledImageIndex(int imageIndex) {
|
||||
return imageIndex >= BUNDLED_IMAGE_INDEX_START_RANGE;
|
||||
}
|
||||
|
||||
private final ImageFrameStorage imageFrameStorage;
|
||||
private final Map<Integer, ImageMap> maps;
|
||||
private final Map<MapView, ImageMap> mapsByView;
|
||||
private final List<ImageMapRenderEventListener> renderEventListeners;
|
||||
private final Set<Integer> deletedMapIds;
|
||||
private final AtomicInteger bundledImageIndexCounter;
|
||||
|
||||
public ImageMapManager(ImageFrameStorage imageFrameStorage) {
|
||||
this.maps = new ConcurrentHashMap<>();
|
||||
this.mapsByView = new ConcurrentHashMap<>();
|
||||
this.imageFrameStorage = imageFrameStorage;
|
||||
this.renderEventListeners = new CopyOnWriteArrayList<>();
|
||||
this.deletedMapIds = ConcurrentHashMap.newKeySet();
|
||||
this.bundledImageIndexCounter = new AtomicInteger(BUNDLED_IMAGE_INDEX_START_RANGE);
|
||||
}
|
||||
|
||||
public ImageFrameStorage getStorage() {
|
||||
return imageFrameStorage;
|
||||
}
|
||||
|
||||
protected long getCurrentAnimationTick() {
|
||||
return System.currentTimeMillis() / 50;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
saveDeletedMaps();
|
||||
}
|
||||
|
||||
public void appendRenderEventListener(ImageMapRenderEventListener listener) {
|
||||
renderEventListeners.add(listener);
|
||||
}
|
||||
|
||||
public void prependRenderEventListener(ImageMapRenderEventListener listener) {
|
||||
renderEventListeners.add(0, listener);
|
||||
}
|
||||
|
||||
public void removeRenderEventListener(ImageMapRenderEventListener listener) {
|
||||
renderEventListeners.remove(listener);
|
||||
}
|
||||
|
||||
protected void callRenderEventListener(ImageMapManager manager, ImageMap imageMap, MapView map, Player player, MutablePair<byte[], Collection<MapCursor>> renderData) {
|
||||
renderEventListeners.forEach(each -> each.accept(manager, imageMap, map, player, renderData));
|
||||
}
|
||||
|
||||
public synchronized void addMap(ImageMap map) throws Exception {
|
||||
if (map.getManager() != this) {
|
||||
throw new IllegalArgumentException("ImageMap's manager is not set to this");
|
||||
}
|
||||
if (getFromCreator(map.getCreator(), map.getName()) != null) {
|
||||
throw new IllegalArgumentException("Duplicated map name for this creator");
|
||||
}
|
||||
int originalImageIndex = map.getImageIndex();
|
||||
imageFrameStorage.prepareImageIndex(map, i -> map.imageIndex = i);
|
||||
maps.put(map.getImageIndex(), map);
|
||||
for (MapView mapView : map.getMapViews()) {
|
||||
mapsByView.put(mapView, map);
|
||||
deletedMapIds.remove(mapView.getId());
|
||||
}
|
||||
try {
|
||||
map.save();
|
||||
Bukkit.getPluginManager().callEvent(new ImageMapAddedEvent(map));
|
||||
} catch (Throwable e) {
|
||||
maps.remove(originalImageIndex);
|
||||
for (MapView mapView : map.getMapViews()) {
|
||||
mapsByView.remove(mapView);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
saveDeletedMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a map whose canonical definition lives inside the plugin jar
|
||||
* (maps.yml + bundled image). Unlike {@link #addMap}, nothing is written
|
||||
* to storage: bundled maps occupy a reserved image index range and are
|
||||
* rebuilt from the jar on every startup.
|
||||
*/
|
||||
public synchronized void addBundledMap(ImageMap map) {
|
||||
if (map.getManager() != this) {
|
||||
throw new IllegalArgumentException("ImageMap's manager is not set to this");
|
||||
}
|
||||
if (getFromCreator(map.getCreator(), map.getName()) != null) {
|
||||
throw new IllegalArgumentException("Duplicated map name for this creator");
|
||||
}
|
||||
if (map.getImageIndex() < 0) {
|
||||
map.imageIndex = bundledImageIndexCounter.getAndIncrement();
|
||||
} else if (!isBundledImageIndex(map.getImageIndex())) {
|
||||
throw new IllegalArgumentException("Bundled ImageMap image index must be in the bundled range");
|
||||
}
|
||||
maps.put(map.getImageIndex(), map);
|
||||
for (MapView mapView : map.getMapViews()) {
|
||||
mapsByView.put(mapView, map);
|
||||
deletedMapIds.remove(mapView.getId());
|
||||
}
|
||||
Bukkit.getPluginManager().callEvent(new ImageMapAddedEvent(map));
|
||||
saveDeletedMaps();
|
||||
}
|
||||
|
||||
public boolean hasMap(int imageIndex) {
|
||||
return maps.containsKey(imageIndex);
|
||||
}
|
||||
|
||||
public Collection<ImageMap> getMaps() {
|
||||
return Collections.unmodifiableCollection(maps.values());
|
||||
}
|
||||
|
||||
public ImageMap getFromMapId(int id) {
|
||||
MapView mapView = Bukkit.getMap(id);
|
||||
if (mapView == null) {
|
||||
return null;
|
||||
}
|
||||
return getFromMapView(Bukkit.getMap(id));
|
||||
}
|
||||
|
||||
public ImageMap getFromImageId(int imageId) {
|
||||
return maps.get(imageId);
|
||||
}
|
||||
|
||||
public ImageMap getFromMapView(MapView mapView) {
|
||||
return mapsByView.get(mapView);
|
||||
}
|
||||
|
||||
public Set<ImageMap> getFromCreator(UUID uuid) {
|
||||
return maps.values().stream().filter(each -> each.getCreator().equals(uuid)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public List<ImageMap> getFromCreator(UUID uuid, Comparator<ImageMap> order) {
|
||||
return maps.values().stream().filter(each -> each.getCreator().equals(uuid)).sorted(order).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public ImageMap getFromCreator(UUID uuid, String name) {
|
||||
return maps.values().stream().filter(each -> each.getCreator().equals(uuid) && each.getName().equalsIgnoreCase(name)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public Set<UUID> getCreators() {
|
||||
return maps.values().stream().map(each -> each.getCreator()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public ImageMap getFromFakeMapId(int fakeMapId) {
|
||||
return maps.values().stream().filter(each -> each.requiresAnimationService() && each.getFakeMapIds().contains(fakeMapId)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public boolean deleteMap(int imageIndex) {
|
||||
ImageMap imageMap = maps.remove(imageIndex);
|
||||
if (imageMap == null) {
|
||||
return false;
|
||||
}
|
||||
List<MapView> mapViews = imageMap.getMapViews();
|
||||
for (MapView mapView : mapViews) {
|
||||
mapsByView.remove(mapView);
|
||||
}
|
||||
if (imageMap.trackDeletedMaps()) {
|
||||
mapViews.forEach(each -> deletedMapIds.add(each.getId()));
|
||||
}
|
||||
imageMap.markInvalid();
|
||||
if (!isBundledImageIndex(imageIndex)) {
|
||||
imageFrameStorage.deleteMap(imageIndex);
|
||||
}
|
||||
imageMap.stop();
|
||||
saveDeletedMaps();
|
||||
Bukkit.getPluginManager().callEvent(new ImageMapDeletedEvent(imageMap));
|
||||
Scheduler.runTask(ImageFrame.plugin, () -> {
|
||||
mapViews.forEach(each -> {
|
||||
if (each.getRenderers().isEmpty()) {
|
||||
each.addRenderer(DeletedMapRenderer.INSTANCE);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized void updateMap(int imageIndex, boolean exist) {
|
||||
ImageMap imageMap = maps.get(imageIndex);
|
||||
try {
|
||||
if (imageMap == null) {
|
||||
if (exist) {
|
||||
JsonObject json = imageFrameStorage.loadImageMapData(imageIndex);
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
try {
|
||||
addMap(ImageMapLoaders.load(this, json).get());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to update map " + imageIndex + " from source", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (exist) {
|
||||
JsonObject json = imageFrameStorage.loadImageMapData(imageIndex);
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
if (imageMap.applyUpdate(json)) {
|
||||
imageMap.reloadColorCache();
|
||||
Bukkit.getPluginManager().callEvent(new ImageMapUpdatedEvent(imageMap));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deleteMap(imageIndex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to update map " + imageIndex + " from source", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Integer> getDeletedMapIds() {
|
||||
return Collections.unmodifiableSet(deletedMapIds);
|
||||
}
|
||||
|
||||
public boolean isMapDeleted(int mapId) {
|
||||
return deletedMapIds.contains(mapId);
|
||||
}
|
||||
|
||||
public boolean isMapDeleted(MapView mapView) {
|
||||
return isMapDeleted(mapView.getId());
|
||||
}
|
||||
|
||||
public synchronized void loadMaps(IFPlayerManager ifPlayerManager) {
|
||||
loadMaps(ifPlayerManager, null);
|
||||
}
|
||||
|
||||
public synchronized void loadMaps(IFPlayerManager ifPlayerManager, IntSupplier completionCallback) {
|
||||
maps.clear();
|
||||
mapsByView.clear();
|
||||
List<MutablePair<String, Future<? extends ImageMap>>> futures = imageFrameStorage.loadMaps(this, deletedMapIds, ifPlayerManager);
|
||||
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
|
||||
int count = 0;
|
||||
for (MutablePair<String, Future<? extends ImageMap>> pair : futures) {
|
||||
try {
|
||||
addMap(pair.getSecond().get());
|
||||
count++;
|
||||
} catch (Throwable e) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to load ImageMap data in " + pair.getFirst());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (completionCallback != null) {
|
||||
count += completionCallback.getAsInt();
|
||||
}
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Data loading completed! Loaded " + count + " ImageMaps!");
|
||||
});
|
||||
}
|
||||
|
||||
public void syncMaps() {
|
||||
syncMaps(false);
|
||||
}
|
||||
|
||||
public synchronized void syncMaps(boolean verbose) {
|
||||
Set<Integer> indexesFromStorage = imageFrameStorage.getAllImageIndexes();
|
||||
Set<Integer> indexesFromLocal = maps.keySet().stream().filter(i -> !isBundledImageIndex(i)).collect(Collectors.toSet());
|
||||
int added = 0;
|
||||
int deleted = 0;
|
||||
for (int index : Sets.symmetricDifference(indexesFromStorage, indexesFromLocal)) {
|
||||
try {
|
||||
boolean exist = indexesFromStorage.contains(index);
|
||||
updateMap(index, exist);
|
||||
if (exist) {
|
||||
added++;
|
||||
} else {
|
||||
deleted++;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (verbose) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to sync ImageMap data for index " + index);
|
||||
}
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (verbose) {
|
||||
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Data sync completed! Newly loaded " + added + " and deleted " + deleted + " ImageMaps!");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void saveDeletedMaps() {
|
||||
imageFrameStorage.saveDeletedMaps(deletedMapIds);
|
||||
}
|
||||
|
||||
public void sendAllMaps(Collection<? extends Player> players) {
|
||||
maps.values().forEach(m -> m.send(players));
|
||||
}
|
||||
|
||||
public static class DeletedMapRenderer extends MapRenderer {
|
||||
|
||||
public static final DeletedMapRenderer INSTANCE = new DeletedMapRenderer();
|
||||
|
||||
private DeletedMapRenderer() {}
|
||||
|
||||
@Override
|
||||
public void render(MapView map, MapCanvas canvas, Player player) {
|
||||
List<MapRenderer> mapRenderers = map.getRenderers();
|
||||
if (mapRenderers.size() != 1 || mapRenderers.get(0) != this) {
|
||||
Scheduler.runTaskLater(ImageFrame.plugin, () -> map.removeRenderer(this), 1);
|
||||
return;
|
||||
}
|
||||
Random random = new Random(map.getId());
|
||||
byte[] colors = MapUtils.PALETTE_GRAYSCALE;
|
||||
for (int y = 0; y < MapUtils.MAP_WIDTH; y++) {
|
||||
for (int x = 0; x < MapUtils.MAP_WIDTH; x++) {
|
||||
canvas.setPixel(x, y, colors[random.nextInt(colors.length)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ImageMapManualPersistentCacheControlTask implements ImageMapCacheControlTask {
|
||||
|
||||
private final ImageMap imageMap;
|
||||
private final AtomicBoolean closed;
|
||||
|
||||
public ImageMapManualPersistentCacheControlTask(ImageMap imageMap) {
|
||||
this.imageMap = imageMap;
|
||||
this.closed = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageMap getImageMap() {
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadCacheIfManual() {
|
||||
imageMap.loadColorCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed.set(true);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapCursor;
|
||||
import org.bukkit.map.MapView;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ImageMapRenderEventListener {
|
||||
|
||||
void accept(ImageMapManager manager, ImageMap imageMap, MapView map, Player player, MutablePair<byte[], Collection<MapCursor>> renderData);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class IntPosition {
|
||||
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
public IntPosition(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IntPosition intPosition = (IntPosition) o;
|
||||
return x == intPosition.x && y == intPosition.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class IntRange {
|
||||
|
||||
public static IntRange of(String input) {
|
||||
String[] parts = Arrays.stream(input.split("-")).map(s -> s.trim()).filter(s -> !s.isEmpty()).toArray(String[]::new);
|
||||
if (parts.length == 1) {
|
||||
return of(Integer.parseInt(parts[0]));
|
||||
} else if (parts.length == 2) {
|
||||
return of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid range format: " + input);
|
||||
}
|
||||
|
||||
public static IntRange of(int value) {
|
||||
return new IntRange(value, value);
|
||||
}
|
||||
|
||||
public static IntRange of(int start, int end) {
|
||||
return start <= end ? new IntRange(start, end) : new IntRange(end, start);
|
||||
}
|
||||
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
private IntRange(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public boolean satisfies(int value) {
|
||||
return start <= value && value <= end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IntRange intRange = (IntRange) o;
|
||||
return start == intRange.start && end == intRange.end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(start, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of ImageFrame.
|
||||
*
|
||||
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
|
||||
* Copyright (C) 2025. Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.loohp.imageframe.objectholders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class IntRangeList extends ArrayList<IntRange> {
|
||||
|
||||
public boolean satisfies(int value) {
|
||||
return stream().anyMatch(r -> r.satisfies(value));
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 194 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user