Compare commits

...
232 Commits
Author SHA1 Message Date
LOOHP 946840c14a Update copyright and version 2026-10-29 22:47:54 +00:00
LOOHP 46a17d9caa Fixed GIFs not animated via upload server 2026-10-29 22:41:45 +00:00
LOOHP a97768028d Improved the gif loading process
#155
2026-10-10 22:03:40 +01:00
LOOHP bc0a073813 Extend GeyserHook to Floodgate as well 2026-10-08 22:28:32 +01:00
LOOHP a7369c5fe7 Fixed #182 2026-10-05 09:47:21 +01:00
LOOHP 7edeaa29fd Added Config Option DisableSmoothAnimationForBedrockPlayers
Helps with https://github.com/GeyserMC/Geyser/issues/6569
2026-09-29 17:30:27 +01:00
LOOHP c03d6c44a7 Ignore Fixed ItemFrames
Fixes #181
2026-09-28 11:06:58 +01:00
LOOHP a41bfa7fa8 Minecraft 26.3 2026-09-18 18:14:52 +01:00
LOOHP d5c20ec243 Try more times 2026-09-13 01:24:18 +01:00
LOOHP bbcdff0373 Suppress Upload Server HTTP Errors 2026-09-10 20:43:12 +01:00
LOOHP 89da42a483 Fixed refresh command on gifs with no url 2026-08-02 15:08:27 +01:00
LOOHP a33c6f4b1f Async language reload during command reload
Fixes #173
2026-07-07 21:29:33 +01:00
LOOHP fed3b4c3c8 Update config.yml 2026-06-29 20:53:17 +01:00
LOOHP c7620ce1fe Fixed #170 & #171 2026-06-29 20:43:16 +01:00
LOOHP 2e2146f1fc More dithering algorithms 2026-06-19 22:36:58 +01:00
LOOHP ff81317be1 Minecraft 26.2 2026-06-16 23:01:55 +01:00
LOOHP 2d7e08b79f Attempt to find direct image url from indirect urls 2026-05-28 00:44:15 +01:00
LOOHP 3527a881e4 Fixed issue when getting new map id on 26.1 when the world has no maps
Fixes #164
2026-05-06 21:15:15 +01:00
LOOHP 1cdb73099e Upload webpage now also translatable 2026-04-25 23:54:20 +01:00
LOOHP aa1a403b95 Minecraft 26.1.2 2026-04-09 19:25:50 +01:00
LOOHP 809ea6381b Fixed issue when getting new map id on 26.1 2026-04-09 18:34:12 +01:00
LOOHP f9f88e16ce Fixed wrong method used 2026-04-04 17:09:53 +01:00
LOOHP b766d2f9d5 Minecraft 26.1.1 2026-04-04 15:51:08 +01:00
LOOHP cc50d7c2c9 Fixed #161 2026-03-09 15:36:51 +08:00
LOOHP 36d452cdfe Added timeout to network requests 2026-01-12 16:26:23 +00:00
LOOHP 8cc3219a33 Added debug logging for language download 2026-01-11 13:55:40 +00:00
LOOHP 98b2b1982e Check hasMapView first 2026-01-11 13:02:18 +00:00
LOOHP fc1e14583c Explicitly add click event to upload URL
Fixes #142
2025-12-31 17:25:52 +00:00
LOOHP 25eedbe0be Minecraft 1.21.11 2025-12-24 00:19:37 +00:00
LOOHP 33e7fdc7c2 Change image link to markdown format
Updated image link format in README.
2025-12-06 10:21:46 +00:00
LOOHP 1157ca1813 Change banner image in README
Updated the banner image link in the README.
2025-12-06 09:39:28 +00:00
LOOHP 5a3894d6fe Fixed method not there on older versions of Minecraft 2025-12-02 23:54:34 +00:00
LOOHP 61e1f5aab3 Add Crowdin badge to README
Added Crowdin badge to README for localization status.
2025-12-02 00:01:58 +00:00
LOOHP 05948afdba Added link for languages 2025-12-01 23:05:09 +00:00
LOOHP 116e94c2f7 Better error message 2025-12-01 22:07:57 +00:00
LOOHP 34366439bb Internationalization and CombinedByDefault option 2025-12-01 22:04:13 +00:00
LOOHP 00245cd8d1 Refactor 2025-11-29 13:54:25 +00:00
LOOHP aeedecd808 Fixed first time initialization error 2025-11-26 07:46:56 +00:00
LOOHP 48096bbd78 Hot Fix 2025-11-24 20:57:42 +00:00
LOOHP 105b4f41a1 Block invisible item frames from grindstone 2025-11-24 20:32:33 +00:00
LOOHP 861efce918 Added ImageMapAddedEvent 2025-11-24 13:40:31 +00:00
LOOHP fa617744b8 Add GNU GPL v3 license
Added the GNU General Public License version 3 to the project.
2025-11-23 19:54:50 +00:00
LOOHP 2bd23ae498 MySQL Storage Support 2025-11-23 18:18:01 +00:00
LOOHP 5879d44242 MySQL Storage Support 2025-11-23 01:42:00 +00:00
LOOHP cd430775f3 Iterated design 2025-11-21 22:55:29 +00:00
LOOHP d035989df5 Improved design of media loading 2025-11-16 16:29:06 +00:00
LOOHP 6c1ddfcf05 Merge pull request #138 from virustotalop/main
Fix platform scheduler relocation
2025-11-14 07:21:25 +00:00
virustotalop 33543b9edb Fix platform scheduler relocation
Fixes platform scheduler relocation location, before this seemed to break the plugin on 1.21.7 paper with InteractiveChat installed related to https://github.com/LOOHP/ImageFrame/issues/113
2025-11-13 20:38:55 -08:00
LOOHP 5379ef308e Extract notifyHdMapUpdated 2025-11-11 22:28:50 +00:00
LOOHP 5ade6c9b24 Call event when hdmap is requested 2025-11-11 22:18:54 +00:00
LOOHP c339a51723 Wait a bit longer before notifying client 2025-11-11 17:49:00 +00:00
LOOHP 3130ddf5cc Update README.md 2025-11-10 22:24:47 +00:00
LOOHP 2a7fe930aa Added metrics 2025-11-10 21:11:21 +00:00
LOOHP d706da2f4b ImageFrame Client 2025-11-10 20:35:50 +00:00
LOOHP 184b9749ab Minecraft 1.21.10 2025-10-08 18:43:59 +01:00
LOOHP 59f0ee33f5 Minecraft 1.21.9 2025-10-06 22:46:52 +01:00
LOOHP a5a309e2d4 Force main thread collect item frames info on Folia
Closes #126
Closes #127
2025-09-06 10:06:23 +01:00
LOOHP 93c1911d60 Minecraft 1.21.8 2025-07-17 22:51:48 +01:00
LOOHP e25eff4fac Send messages to BlockCommandSender on owning thread
Fixes #124
2025-07-16 13:17:48 +01:00
LOOHP 837a9a5b73 Minecraft 1.21.7
Closes #121
Closes #122
2025-07-05 20:10:34 +01:00
LOOHP 07ecafa7da Minecraft 1.21.6
Fixes #119
Closes #117
2025-06-20 22:38:09 +01:00
LOOHP fd7b071b5d Fixed persistentIdCountsLastMapIdField0 for 1.21.5 2025-05-31 17:25:48 +01:00
LOOHP 1322dcdb91 Fixed a bug where images only appear after restart
Fixes #108
Fixes #110
2025-05-01 12:21:28 +01:00
LOOHP c197158850 Fixed recipe methods doesn't exist on old versions
Fixes #107
2025-04-27 23:34:35 +01:00
LOOHP 98f429d04c Fixed fakeMapIdsSet could be null
Fixes #106
2025-04-27 11:32:19 +01:00
LOOHP c6e04888c6 Merge pull request #104 from LOOHP/temp
Merge
2025-04-25 17:51:09 +01:00
LOOHP 0cfb292ac3 Bump version
Closes #101
2025-04-25 17:47:18 +01:00
LOOHP 7677eef07c Merge branch 'main' of https://github.com/LOOHP/ImageFrame 2025-04-25 17:39:52 +01:00
LOOHP 49feef2999 Bump version
Closes #101
2025-04-25 17:39:49 +01:00
LOOHP 0162cebc41 Bump version 2025-04-25 17:38:14 +01:00
LOOHP 0295f9ef5a Added "CacheControlMode" and "TryDeleteBlankMapFiles" config options
Fixes #103
2025-04-24 11:54:04 +01:00
LOOHP d64cbfccfc Fixed #95 2025-04-16 15:34:01 +01:00
LOOHP 4a9ca90188 Updated copyright year 2025-04-16 08:37:08 +01:00
LOOHP d18b0743db Minecraft 1.21.5 2025-04-16 08:33:17 +01:00
LOOHP 6d231e52c6 Set executor and properties for http server 2025-03-20 21:40:58 +00:00
LOOHP 3ea1420304 Make sure exchanges are closed 2025-03-18 12:17:59 +00:00
LOOHP 2a1adee462 Add to lore list instead of set on legacy versions 2025-03-17 19:09:42 +00:00
LOOHP e8204c1fa9 Make sure each cycle for cache control is finished before starting the next 2025-03-02 20:16:52 +00:00
LOOHP c37a1fda8f Only cache image colors when imagemap is active 2025-03-01 14:24:12 +00:00
LOOHP a9b41c814c Fixed a content max size bypass method 2025-02-28 19:44:15 +00:00
LOOHP 430a658e0e Relocate apache commons 2025-02-20 13:18:02 +00:00
LOOHP ff8f5febac Do not update animated internal pixels on the server 2025-02-19 16:11:36 +00:00
LOOHP fe7a551be8 Change most default permissions to op
Fixes #62
Fixes #86
2025-02-17 22:24:34 +00:00
LOOHP 2c2d719143 Remove paper method 2025-02-15 02:52:28 +00:00
LOOHP 3cfdc8f507 Fixed taken maps not returned if upload fails 2025-02-12 01:56:12 +00:00
LOOHP 8a89a33b42 Added embedded upload service feature 2025-02-11 16:17:07 +00:00
LOOHP 6af8fd7b76 Clear 0 length map data files to prevent error log 2025-02-09 15:30:51 +00:00
LOOHP f412275b37 Improved image map creation queue 2025-02-08 21:10:30 +00:00
LOOHP 9b03eb6ea4 Added playback papi placeholders & Hot Fixes 2025-02-05 00:10:44 +00:00
LOOHP c40f614a61 Merge branch 'main' of https://github.com/LOOHP/ImageFrame 2025-02-04 02:03:33 +00:00
LOOHP 683bbdcde0 Added survival friendly way to obtain invisible item frames 2025-02-04 02:02:29 +00:00
LOOHP 0c2d8f7787 Added survival friendly way to obtain invisible item frames 2025-02-04 01:51:25 +00:00
LOOHP ea0563ff50 Update README.md 2025-01-28 06:37:33 +08:00
LOOHP 1b2f47f8db Fixed an error in certain versions on chunk load 2025-01-18 14:21:16 +00:00
LOOHP cdfc2b480b Check IF & world folder map ids too when creating new maps
Fixes #77
2025-01-16 13:51:26 +00:00
LOOHP fcfe414ba2 Fixed jumpTo while animated map is paused 2025-01-01 13:57:28 +00:00
LOOHP e203b3b4d3 Fixed combined map items jittering in the player inventory 2024-12-29 15:48:28 +00:00
LOOHP 7e05d59387 Minecraft 1.21.4 2024-12-13 16:02:36 +00:00
LOOHP 4106f0bcd5 Minecraft 1.21.3 2024-11-01 18:05:41 +00:00
LOOHP 23d2113ca7 Fixed AnimatedFakeMapManager keeping Player instances after quit
Fixes #73, Fixes #58
2024-08-21 17:15:31 +01:00
LOOHP e1fe6dc81e Minecraft 1.21.1 2024-08-09 14:23:47 +01:00
LOOHP 708d94990e Fixed combined maps being used for crafting
Fixed #66
2024-08-09 14:23:37 +01:00
LOOHP 6120ee8fa8 Added convenience method "MapUtils.rayTraceTargetImageMap" 2024-07-06 00:12:35 +01:00
LOOHP fa3e26823f Fix backwards compatibility 2024-06-22 21:13:23 +01:00
LOOHP 3cb325a200 Minecraft 1.21 & Fixes #59 2024-06-16 23:19:36 +01:00
LOOHP b33c0f1eee Bump version 2024-06-12 14:55:28 +01:00
LOOHP 11daa22cd5 Merge pull request #55 from TechnicallyCoded/fix/hash-collisions
Fix hash collisions producing thread checks on Folia
2024-06-12 14:51:52 +01:00
LOOHP 42462cc9d2 Removed unused air item 2024-06-12 14:50:54 +01:00
LOOHP 1190040ccc Move things around 2024-06-12 14:49:19 +01:00
TechnicallyCoded 8d5353c471 Use synchronized identity hashmap for holding "itemFrames" 2024-06-11 21:43:25 +02:00
LOOHP 91b8e00dbf Experimental optional Floyd-Steinberg Dithering 2024-05-28 19:31:14 +01:00
LOOHP 34f9a2c84b Fixed #56 2024-05-21 14:34:41 +01:00
LOOHP fa2a5d2f2d Don't hardcode color array length error message 2024-05-16 12:30:41 +01:00
LOOHP 1c69e26e01 Centralize usage of MapPalette to MapUtils 2024-05-16 12:28:08 +01:00
LOOHP 4990b53474 Better method names in GifReader 2024-05-13 02:32:29 +01:00
LOOHP 5212f0522e Fixed bug on 1.16-1.17
Fixed #53
2024-05-12 19:26:54 +01:00
LOOHP 75267c1305 Fixed vanilla maps overwritting image maps 2024-05-12 16:44:29 +01:00
LOOHP b8371a21bc Added action bar processing message & Improved animated map storage
Fixes #50
2024-05-12 15:03:49 +01:00
LOOHP 085ce5222f Update pom.xml 2024-05-11 22:15:14 +01:00
LOOHP 306d57b0c7 Fixed CombinedMaps on 1.19.6 2024-05-11 22:13:42 +01:00
TechnicallyCoded 9b8efe2295 Fix hash collisions producing thread checks 2024-05-07 01:07:07 +02:00
LOOHP e3cc812425 Update .gitignore 2024-05-04 23:02:28 +01:00
LOOHP 286ad3547e Maintain getEntityTrackers returning mutable sets 2024-05-04 19:42:19 +01:00
LOOHP 39e435f1e4 Update AnimatedFakeMapManager.java 2024-05-04 16:14:44 +01:00
LOOHP 08c22adc1b Optimize Imports 2024-05-04 14:14:36 +01:00
LOOHP f23b67a64c Update UnsafeAccessor.java 2024-05-04 14:03:07 +01:00
LOOHP af8597354a Fix problems from last release 2024-05-04 13:51:20 +01:00
LOOHP 82c81b77c1 Update header 2024-05-04 02:33:08 +01:00
LOOHP b7403f9481 Fixed NMS DataWatcher Impl 2024-05-04 01:16:39 +01:00
LOOHP dbd0b8a660 Update pom.xml 2024-05-03 01:56:08 +01:00
LOOHP 520735e389 Minecraft 1.20.6 2024-05-03 01:50:45 +01:00
LOOHP fafe4e1bb4 Update plugin.yml
Fixes #54
2024-05-03 00:33:56 +01:00
LOOHP 7118fcae81 Fixes restrict image url potential security vulnerability 2024-04-05 22:13:04 +01:00
LOOHP 2249c7568f Catch mapMeta with no map view
Fixes #39
2024-01-26 15:20:14 +00:00
LOOHP ec29a575e6 Merge pull request #37 from TechnicallyCoded/folia-compat
Folia compat updates
2024-01-25 23:43:12 +00:00
LOOHP a344a1159d A bit of clean up 2024-01-25 23:41:59 +00:00
TechnicallyCoded 5e1ccd588e Try to fix folia issues 2024-01-06 15:20:43 +01:00
TechnicallyCoded 7f0ad6a3a6 Add debug logs 2024-01-05 00:47:32 +01:00
hsgamer 296d3d87fc try using IdentityHashMap 2024-01-01 01:17:19 +07:00
LOOHP a667937148 Handle blank player names
Fixes #32
2023-12-17 01:33:30 +00:00
LOOHP c25533e16c Minecraft 1.20.4 2023-12-14 18:44:14 +00:00
LOOHP cffe0a02f9 Minecraft 1.20.4 & Folia Fix 2023-12-14 18:28:38 +00:00
LOOHP c0cf0ea2b7 Catch IllegalArgumentException 2023-11-22 14:47:41 +00:00
LOOHP 6e23bbebef Added HandleAnimatedMapsOnMainThread 2023-11-15 20:56:18 +00:00
LOOHP 4d3266d92a Method is different on 1.16 2023-10-03 18:22:31 +01:00
LOOHP d7f9cf334a Minecraft 1.20.2 2023-09-23 17:13:55 +01:00
LOOHP ee17be5afd Wait 0.5 seconds before sending packets after player join 2023-09-13 23:52:27 +01:00
LOOHP c8054f72cb Better Map Item Check 2023-09-09 19:20:38 +01:00
LOOHP e27ac14a31 Added SendAnimatedMapsOnMainThread 2023-09-08 22:27:29 +01:00
LOOHP d6bb052d12 VIEW_ANIMATED_MAPS preference can now also be "unset" 2023-08-30 13:41:37 +01:00
LOOHP 3e482b37b3 Added preference command 2023-08-30 00:10:47 +01:00
LOOHP 8f563d3820 Create missing map views 2023-08-27 20:01:06 +01:00
LOOHP e66fcde6b8 Added mapRenderersContextual option 2023-08-25 16:41:05 +01:00
LOOHP 0e2245af2e Added purge command 2023-08-25 15:38:09 +01:00
LOOHP 2a25cbff2d Fake item reset instead of setting item for real 2023-08-24 19:05:59 +01:00
LOOHP 3eac4d6828 1.7.5 Release 2023-08-24 16:27:22 +01:00
LOOHP 2c50d8e382 More informative error 2023-08-23 21:23:51 +01:00
LOOHP c3308febb5 Update config.yml 2023-08-23 20:54:41 +01:00
LOOHP 5f6cd8735c Catch player tracker missing 2023-08-22 11:26:02 +01:00
LOOHP f077708ad1 Added ExemptMapIdsFromDeletion option 2023-08-21 19:07:50 +01:00
LOOHP 0fad2018b5 Bug Fixes 2023-08-20 15:01:23 +01:00
LOOHP b59e718a56 Direct selection command usable also in the console /if select world x1 y1 z1 x2 y2 z2
Closes #23
2023-08-19 23:37:58 +01:00
LOOHP 425c9bca20 Info and Refresh Commands are no longer restricted to players 2023-08-19 22:29:39 +01:00
LOOHP b4afa61420 Improve RAM usage 2023-08-19 18:04:08 +01:00
LOOHP e11d136d14 Internal improvements 2023-08-19 00:49:14 +01:00
LOOHP 54eaaa1e55 Replaced pause command with playback command 2023-08-03 15:40:20 +01:00
LOOHP ecb2bac48b Allow refresh command to be ran by the console
Closes #16
2023-07-02 13:25:47 +08:00
LOOHP e74750c191 Access ItemFrame item on owning thread 2023-06-29 15:25:43 +08:00
LOOHP 0cf28de45d Initial Folia Support 2023-06-26 16:55:37 +08:00
LOOHP c6a92dc7ce Minecraft 1.20 2023-06-08 17:35:00 +01:00
LOOHP 9e97576618 Fixed completionCallback NPE
Fixes #12
2023-05-31 01:25:31 +01:00
LOOHP 4b5b64a9a3 Added pause command 2023-05-01 22:44:35 +01:00
LOOHP 7da31597f0 Added ImageMapProcessing message 2023-04-30 22:40:59 +01:00
LOOHP 9272225f46 Check if sender is console 2023-03-27 23:30:18 +01:00
LOOHP 2e89121489 ImageMap Migration, Map creation for other players 2023-03-27 22:56:37 +01:00
LOOHP 98f32c78d5 Fake Item Change Fix 2023-03-20 14:50:51 +00:00
LOOHP a1c1f5f555 Minecraft 1.19.4 2023-03-16 03:10:32 +00:00
LOOHP 9fe0c2f6dc Fixed /if get selection 2023-03-11 17:33:48 +00:00
LOOHP 6c7fcf6117 Better handle ViaVersion legacy players 2023-03-08 02:04:51 +00:00
LOOHP 8eb8fa865f Fixed event registration on MC versions with no EntitiesLoadEvent 2023-03-03 13:57:20 +00:00
LOOHP 1a38c9b078 Extended /if get to allow specifying which player
Closes #8
2023-03-01 23:49:43 +00:00
LOOHP 3eb9401e8a Fixed denying combined maps into tables 2023-02-20 21:04:58 +00:00
LOOHP 42cfa1fd87 Drastically decrease network usage for animated images 2023-02-16 23:15:22 +00:00
LOOHP 6e365e8bea Fixed animations where all frames are the same 2023-01-30 12:06:29 +00:00
LOOHP 013135bf9a Only send frames if they are different from the last for GIFs 2023-01-29 00:13:04 +00:00
LOOHP 21b5971b24 Account for custom worlds folder path 2023-01-05 20:41:08 +00:00
LOOHP 9fc17997b2 Fixed map creation 2023-01-02 14:00:29 +00:00
LOOHP fa46d3a552 Hot Fix for Image loading and migration 2022-12-29 04:11:17 +00:00
LOOHP ad1520bb85 Fixed a race condition when creating maps 2022-12-22 18:25:51 +00:00
LOOHP dd5a7e56e8 Better error message 2022-12-12 17:15:04 +00:00
LOOHP 9ba60be63b No need to bump version 2022-12-09 22:39:36 +00:00
LOOHP ecb034bb64 Minecraft 1.19.3 2022-12-09 22:38:33 +00:00
LOOHP a74a8d7252 Made Set Creator command response a bit more informative 2022-12-05 19:59:25 +00:00
LOOHP 8e1da37d1b Add access sharing of image maps and reworked commands 2022-12-05 00:39:10 +00:00
LOOHP 6f1538bc0a Incorrect plugin name 2022-11-26 22:17:30 +00:00
LOOHP bb0703d123 Fixed Image Fitting and crash in a certain situation 2022-11-21 20:44:13 +00:00
LOOHP 751b2ce5a2 Added Combined ImageMap 2022-11-19 15:54:31 +00:00
LOOHP 94bb83692a Do not take away maps for adminclone 2022-11-17 22:59:10 +00:00
LOOHP 7c959ab10f get and admin get on selected item frames not working 2022-11-17 22:58:03 +00:00
LOOHP 2c56682115 Missing tab complete for adminclone 2022-11-17 22:56:54 +00:00
LOOHP 7f2fd61790 Incorrect rotation for item frames on the ceiling 2022-11-17 22:55:48 +00:00
LOOHP a90c557a1a Flipped width and height in ImageOnMap migration 2022-11-17 22:55:17 +00:00
LOOHP 13a7ff04a0 Max image file size option and Better intelligence in identifying GIFs 2022-11-15 20:40:05 +00:00
LOOHP ae8383a9ef More memory efficient way of storing deleted map ids 2022-11-15 20:07:18 +00:00
LOOHP a186ccf707 Added Migration for DrMap and ImageOnMap 2022-11-15 20:06:22 +00:00
LOOHP ad02c8c006 Add overlays on Minecraft Vanilla maps! 2022-11-11 22:36:14 +00:00
LOOHP baf6914f6d Track deleted map ids & block image maps from being zoomed out or locked 2022-11-11 03:46:08 +00:00
LOOHP f3505bc28c Bump version 2022-11-10 01:03:53 +00:00
LOOHP c49fa30d1e Fixed error while creating image maps 2022-11-09 22:51:19 +00:00
LOOHP 5b0edb02e2 Fixed incorrect error message when not using create command correctly 2022-11-09 17:58:00 +00:00
LOOHP f2f525c37d Maps does not save after refreshing 2022-11-08 20:24:10 +00:00
LOOHP 92a698205f Check whether an ItemFrame is protected before placing maps 2022-11-06 17:57:42 +00:00
LOOHP cdb7a52aac Drop usage of fastutl, Fixes #2 2022-11-06 04:49:18 +00:00
LOOHP 68fabd8b10 Fixed occasional packet error when refreshing maps 2022-11-06 00:44:08 +00:00
LOOHP 26c8250d53 Incorrect api version, Fixes #1 2022-11-05 12:59:05 +00:00
LOOHP 879829c2c0 Fixed and improved image refreshing 2022-11-04 23:47:51 +00:00
LOOHP ce69b915a1 Bump version 2022-11-04 14:36:32 +00:00
LOOHP b5b8e40138 Improved marker editor 2022-11-04 14:31:41 +00:00
LOOHP 370ed67d8f Clone markers as well 2022-11-04 13:27:49 +00:00
LOOHP adfc4e8f69 Added map cloning and adminsetcreator 2022-11-04 13:01:11 +00:00
LOOHP 72ce0a236e Incorrect default permissions 2022-11-04 02:57:35 +00:00
LOOHP ac8a977683 Update pom.xml 2022-11-04 02:03:34 +00:00
LOOHP 1ce29654a9 Partial packet based with ProtocolLib 2022-11-04 02:02:24 +00:00
LOOHP 3e0fd9be94 Add markers to your maps! 2022-11-03 23:47:33 +00:00
LOOHP e69dc2a16d Improved item frame selection validation check 2022-11-03 14:35:33 +00:00
LOOHP a76f14e87b Added ability to add directly to item frames, Added rename commands, Various improvements 2022-11-02 21:36:31 +00:00
LOOHP 8343d79f3c Update pom.xml 2022-11-01 23:53:47 +00:00
LOOHP 7051155fa1 Whole load more features 2022-11-01 23:33:43 +00:00
LOOHP 2a11c93fc9 Update README.md 2022-11-01 16:09:50 +00:00
LOOHP 628ab4123c sync item add 2022-11-01 15:14:07 +00:00
LOOHP 10f0c15e7a reloate simpleyaml 2022-11-01 14:41:22 +00:00
LOOHP 4127f8b643 Incorrect chart name 2022-11-01 14:35:09 +00:00
217 changed files with 34501 additions and 2035 deletions

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# 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/
+9 -3
View File
@@ -1,7 +1,9 @@
[![Build Status](https://ci.loohpjames.com/job/ImageFrame/badge/icon)](https://ci.loohpjames.com/job/ImageFrame/)
[![Build Status](https://ci.loohpjames.com/job/ImageFrame/badge/icon)](https://ci.loohpjames.com/job/ImageFrame/) [![Crowdin](https://badges.crowdin.net/imageframe/localized.svg)](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!
@@ -32,6 +34,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 +48,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](https://www.bisecthosting.com/partners/custom-banners/fc7f7b10-8d1a-4478-a23a-8a357538a180.png)
[![](https://www.bisecthosting.com/partners/custom-banners/96e11ee5-50e4-494f-854d-8c1708813abd.png)](https://www.bisecthosting.com/loohp)
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_16</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.16.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,424 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_16_2</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.16.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,424 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_16_4</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.16.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,424 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_17</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.17.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.17.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,412 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_18</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.18.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.18.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,412 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_18_2</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.18.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.18.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,411 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_19</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.19.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,407 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_19_3</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.19.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_19_4</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_20</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.20.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_20_2</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.20.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+100
View File
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_20_3</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.20.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,365 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_20_5</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.20.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_20_6</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_1</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_10</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.10-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.10-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_11</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.11-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.11-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,391 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_2</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_3</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,369 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_4</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,378 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_5</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_6</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_7</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.7-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.7-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_8</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V1_21_9</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.9-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.21.9-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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(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());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V26_1</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>26.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,386 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ChatFormatting;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.MapIndex;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.map.CraftMapCursor;
import org.bukkit.craftbukkit.map.CraftMapView;
import org.bukkit.craftbukkit.map.RenderData;
import org.bukkit.craftbukkit.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 V26_1 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V26_1() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, EntityDataAccessor.class, "DATA_SHARED_FLAGS_ID");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, int.class, "lastMapId");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, AtomicInteger.class, "lastMapId");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public MapItemSavedData getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (MapItemSavedData) 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);
}
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return !humansMap.isEmpty();
}
@Override
public MapDecoration toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
net.minecraft.network.chat.Component iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapDecoration(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.value().showOnItemFrame();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
ServerLevel worldServer = ((CraftWorld) world).getHandle();
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(mapIndex) + 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();
ServerLevel worldServer = ((CraftWorld) world).getHandle();
ResourceKey<Level> dimension = worldServer.dimension();
MapItemSavedData mapItemSavedData = MapItemSavedData.createFresh(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, dimension);
MapId mapId = new MapId(id);
worldServer.setMapData(mapId, mapItemSavedData);
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(mapIndex);
persistentIdCountsLastMapIdField.setInt(mapIndex, Math.max(lastMapId, id));
}
mapIndex.setDirty();
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 ClientboundMapItemDataPacket createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapDecoration> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
MapItemSavedData.MapPatch mapPatch = colors == null ? null : new MapItemSavedData.MapPatch(0, 0, 128, 128, colors);
return new ClientboundMapItemDataPacket(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(mapPatch));
}
@Override
public ClientboundSetEntityDataPacket createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM, CraftItemStack.asNMSCopy(itemStack)));
return new ClientboundSetEntityDataPacket(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);
SynchedEntityData watcher = nmsEntity.getEntityData();
EntityDataAccessor<Byte> byteField = (EntityDataAccessor<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<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(byteField, value));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().connection.send((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(CombinedMapItemInfo.KEY, -1);
if (!tag.contains(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.contains(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloatOr(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.putFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.putIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.getIntOr(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.putInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
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.getOrDefault(DataComponents.LORE, ItemLore.EMPTY);
List<net.minecraft.network.chat.Component> loreLines = new ArrayList<>(itemLore.lines());
loreLines.add(0, net.minecraft.network.chat.Component.translatable("effect.minecraft.invisibility").withStyle(net.minecraft.network.chat.Style.EMPTY.withColor(ChatFormatting.GRAY).withItalic(false)));
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.LORE, new ItemLore(loreLines)).build());
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<>();
ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
net.minecraft.world.entity.player.Inventory inventory = nmsPlayer.getInventory();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.add(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.containerMenu.broadcastChanges();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V26_1_1</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.1.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>26.1.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,386 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ChatFormatting;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.MapIndex;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.map.CraftMapCursor;
import org.bukkit.craftbukkit.map.CraftMapView;
import org.bukkit.craftbukkit.map.RenderData;
import org.bukkit.craftbukkit.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 V26_1_1 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V26_1_1() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, EntityDataAccessor.class, "DATA_SHARED_FLAGS_ID");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, int.class, "lastMapId");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, AtomicInteger.class, "lastMapId");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public MapItemSavedData getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (MapItemSavedData) 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);
}
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return !humansMap.isEmpty();
}
@Override
public MapDecoration toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
net.minecraft.network.chat.Component iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapDecoration(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.value().showOnItemFrame();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
ServerLevel worldServer = ((CraftWorld) world).getHandle();
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(mapIndex) + 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();
ServerLevel worldServer = ((CraftWorld) world).getHandle();
ResourceKey<Level> dimension = worldServer.dimension();
MapItemSavedData mapItemSavedData = MapItemSavedData.createFresh(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, dimension);
MapId mapId = new MapId(id);
worldServer.setMapData(mapId, mapItemSavedData);
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(mapIndex);
persistentIdCountsLastMapIdField.setInt(mapIndex, Math.max(lastMapId, id));
}
mapIndex.setDirty();
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 ClientboundMapItemDataPacket createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapDecoration> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
MapItemSavedData.MapPatch mapPatch = colors == null ? null : new MapItemSavedData.MapPatch(0, 0, 128, 128, colors);
return new ClientboundMapItemDataPacket(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(mapPatch));
}
@Override
public ClientboundSetEntityDataPacket createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM, CraftItemStack.asNMSCopy(itemStack)));
return new ClientboundSetEntityDataPacket(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);
SynchedEntityData watcher = nmsEntity.getEntityData();
EntityDataAccessor<Byte> byteField = (EntityDataAccessor<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<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(byteField, value));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().connection.send((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(CombinedMapItemInfo.KEY, -1);
if (!tag.contains(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.contains(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloatOr(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.putFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.putIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.getIntOr(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.putInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
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.getOrDefault(DataComponents.LORE, ItemLore.EMPTY);
List<net.minecraft.network.chat.Component> loreLines = new ArrayList<>(itemLore.lines());
loreLines.add(0, net.minecraft.network.chat.Component.translatable("effect.minecraft.invisibility").withStyle(net.minecraft.network.chat.Style.EMPTY.withColor(ChatFormatting.GRAY).withItalic(false)));
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.LORE, new ItemLore(loreLines)).build());
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<>();
ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
net.minecraft.world.entity.player.Inventory inventory = nmsPlayer.getInventory();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.add(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.containerMenu.broadcastChanges();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V26_1_2</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.1.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>26.1.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,386 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ChatFormatting;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.MapIndex;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.map.CraftMapCursor;
import org.bukkit.craftbukkit.map.CraftMapView;
import org.bukkit.craftbukkit.map.RenderData;
import org.bukkit.craftbukkit.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 V26_1_2 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V26_1_2() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, EntityDataAccessor.class, "DATA_SHARED_FLAGS_ID");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, int.class, "lastMapId");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, AtomicInteger.class, "lastMapId");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public MapItemSavedData getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (MapItemSavedData) 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);
}
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return !humansMap.isEmpty();
}
@Override
public MapDecoration toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
net.minecraft.network.chat.Component iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapDecoration(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.value().showOnItemFrame();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
ServerLevel worldServer = ((CraftWorld) world).getHandle();
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(mapIndex) + 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();
ServerLevel worldServer = ((CraftWorld) world).getHandle();
ResourceKey<Level> dimension = worldServer.dimension();
MapItemSavedData mapItemSavedData = MapItemSavedData.createFresh(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, dimension);
MapId mapId = new MapId(id);
worldServer.setMapData(mapId, mapItemSavedData);
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(mapIndex);
persistentIdCountsLastMapIdField.setInt(mapIndex, Math.max(lastMapId, id));
}
mapIndex.setDirty();
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 ClientboundMapItemDataPacket createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapDecoration> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
MapItemSavedData.MapPatch mapPatch = colors == null ? null : new MapItemSavedData.MapPatch(0, 0, 128, 128, colors);
return new ClientboundMapItemDataPacket(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(mapPatch));
}
@Override
public ClientboundSetEntityDataPacket createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM, CraftItemStack.asNMSCopy(itemStack)));
return new ClientboundSetEntityDataPacket(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);
SynchedEntityData watcher = nmsEntity.getEntityData();
EntityDataAccessor<Byte> byteField = (EntityDataAccessor<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<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(byteField, value));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().connection.send((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(CombinedMapItemInfo.KEY, -1);
if (!tag.contains(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.contains(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloatOr(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.putFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.putIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.getIntOr(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.putInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
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.getOrDefault(DataComponents.LORE, ItemLore.EMPTY);
List<net.minecraft.network.chat.Component> loreLines = new ArrayList<>(itemLore.lines());
loreLines.add(0, net.minecraft.network.chat.Component.translatable("effect.minecraft.invisibility").withStyle(net.minecraft.network.chat.Style.EMPTY.withColor(ChatFormatting.GRAY).withItalic(false)));
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.LORE, new ItemLore(loreLines)).build());
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<>();
ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
net.minecraft.world.entity.player.Inventory inventory = nmsPlayer.getInventory();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.add(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.containerMenu.broadcastChanges();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V26_2</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>26.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,395 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ChatFormatting;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.MapIndex;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.map.CraftMapCursor;
import org.bukkit.craftbukkit.map.CraftMapView;
import org.bukkit.craftbukkit.map.RenderData;
import org.bukkit.craftbukkit.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 V26_2 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field nmsItemFrameDataItemField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V26_2() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, EntityDataAccessor.class, "DATA_SHARED_FLAGS_ID");
nmsItemFrameDataItemField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.decoration.ItemFrame.class, EntityDataAccessor.class, "DATA_ITEM");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, int.class, "lastMapId");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, AtomicInteger.class, "lastMapId");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public MapItemSavedData getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (MapItemSavedData) 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);
}
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return !humansMap.isEmpty();
}
@Override
public MapDecoration toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
net.minecraft.network.chat.Component iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapDecoration(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.value().showOnItemFrame();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
ServerLevel worldServer = ((CraftWorld) world).getHandle();
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(mapIndex) + 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();
ServerLevel worldServer = ((CraftWorld) world).getHandle();
ResourceKey<Level> dimension = worldServer.dimension();
MapItemSavedData mapItemSavedData = MapItemSavedData.createFresh(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, dimension);
MapId mapId = new MapId(id);
worldServer.setMapData(mapId, mapItemSavedData);
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(mapIndex);
persistentIdCountsLastMapIdField.setInt(mapIndex, Math.max(lastMapId, id));
}
mapIndex.setDirty();
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 ClientboundMapItemDataPacket createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapDecoration> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
MapItemSavedData.MapPatch mapPatch = colors == null ? null : new MapItemSavedData.MapPatch(0, 0, 128, 128, colors);
return new ClientboundMapItemDataPacket(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(mapPatch));
}
@Override
public ClientboundSetEntityDataPacket createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameDataItemField.setAccessible(true);
@SuppressWarnings("unchecked")
EntityDataAccessor<net.minecraft.world.item.ItemStack> dataItemAccessor = (EntityDataAccessor<net.minecraft.world.item.ItemStack>) nmsItemFrameDataItemField.get(null);
List<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(dataItemAccessor, CraftItemStack.asNMSCopy(itemStack)));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} 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.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
SynchedEntityData watcher = nmsEntity.getEntityData();
EntityDataAccessor<Byte> byteField = (EntityDataAccessor<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<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(byteField, value));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().connection.send((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(CombinedMapItemInfo.KEY, -1);
if (!tag.contains(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.contains(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloatOr(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.putFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.putIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.getIntOr(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.putInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
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.getOrDefault(DataComponents.LORE, ItemLore.EMPTY);
List<net.minecraft.network.chat.Component> loreLines = new ArrayList<>(itemLore.lines());
loreLines.add(0, net.minecraft.network.chat.Component.translatable("effect.minecraft.invisibility").withStyle(net.minecraft.network.chat.Style.EMPTY.withColor(ChatFormatting.GRAY).withItalic(false)));
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.LORE, new ItemLore(loreLines)).build());
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<>();
ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
net.minecraft.world.entity.player.Inventory inventory = nmsPlayer.getInventory();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.add(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.containerMenu.broadcastChanges();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-V26_3</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>26.3-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,414 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ChatFormatting;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemInstance;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.MapIndex;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.map.CraftMapCursor;
import org.bukkit.craftbukkit.map.CraftMapView;
import org.bukkit.craftbukkit.map.RenderData;
import org.bukkit.craftbukkit.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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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 V26_3 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field nmsItemFrameDataItemField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
private final Method asBukkitCopyMethod;
public V26_3() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, EntityDataAccessor.class, "DATA_SHARED_FLAGS_ID");
nmsItemFrameDataItemField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.decoration.ItemFrame.class, EntityDataAccessor.class, "DATA_ITEM");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, int.class, "lastMapId");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(MapIndex.class, AtomicInteger.class, "lastMapId");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
Method asBukkitCopyMethod0;
try {
asBukkitCopyMethod0 = CraftItemStack.class.getMethod("asCraftMirror", net.minecraft.world.item.ItemStack.class);
} catch (NoSuchMethodException e) {
asBukkitCopyMethod0 = CraftItemStack.class.getMethod("asBukkitCopy", ItemInstance.class);
}
asBukkitCopyMethod = asBukkitCopyMethod0;
} catch (NoSuchFieldException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
private ItemStack asBukkitCopy(net.minecraft.world.item.ItemStack nmsItemStack) {
try {
return (ItemStack) asBukkitCopyMethod.invoke(null, nmsItemStack);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public MapItemSavedData getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (MapItemSavedData) 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);
}
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
MapItemSavedData nmsWorldMap = getWorldMap(mapView);
Map<net.minecraft.world.entity.player.Player, MapItemSavedData.HoldingPlayer> humansMap = nmsWorldMap.carriedByPlayers;
return !humansMap.isEmpty();
}
@Override
public MapDecoration toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
net.minecraft.network.chat.Component iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapDecoration(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.value().showOnItemFrame();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
ServerLevel worldServer = ((CraftWorld) world).getHandle();
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(mapIndex) + 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();
ServerLevel worldServer = ((CraftWorld) world).getHandle();
ResourceKey<Level> dimension = worldServer.dimension();
MapItemSavedData mapItemSavedData = MapItemSavedData.createFresh(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, dimension);
MapId mapId = new MapId(id);
worldServer.setMapData(mapId, mapItemSavedData);
MapIndex mapIndex = worldServer.getServer().getDataStorage().computeIfAbsent(MapIndex.TYPE);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(mapIndex);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(mapIndex);
persistentIdCountsLastMapIdField.setInt(mapIndex, Math.max(lastMapId, id));
}
mapIndex.setDirty();
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 ClientboundMapItemDataPacket createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapDecoration> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
MapItemSavedData.MapPatch mapPatch = colors == null ? null : new MapItemSavedData.MapPatch(0, 0, 128, 128, colors);
return new ClientboundMapItemDataPacket(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(mapPatch));
}
@Override
public ClientboundSetEntityDataPacket createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameDataItemField.setAccessible(true);
@SuppressWarnings("unchecked")
EntityDataAccessor<net.minecraft.world.item.ItemStack> dataItemAccessor = (EntityDataAccessor<net.minecraft.world.item.ItemStack>) nmsItemFrameDataItemField.get(null);
List<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(dataItemAccessor, CraftItemStack.asNMSCopy(itemStack)));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} 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.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
SynchedEntityData watcher = nmsEntity.getEntityData();
EntityDataAccessor<Byte> byteField = (EntityDataAccessor<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<SynchedEntityData.DataValue<?>> dataWatchers = Collections.singletonList(SynchedEntityData.DataValue.create(byteField, value));
return new ClientboundSetEntityDataPacket(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().connection.send((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(CombinedMapItemInfo.KEY, -1);
if (!tag.contains(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.contains(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloatOr(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.putFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.putIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
return asBukkitCopy(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return null;
}
CompoundTag tag = customData.copyTag();
if (!tag.contains(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getIntOr(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.getIntOr(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.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tag = customData.copyTag();
tag.putInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.putInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.CUSTOM_DATA, CustomData.of(tag)).build());
return asBukkitCopy(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.getOrDefault(DataComponents.LORE, ItemLore.EMPTY);
List<net.minecraft.network.chat.Component> loreLines = new ArrayList<>(itemLore.lines());
loreLines.add(0, net.minecraft.network.chat.Component.translatable("effect.minecraft.invisibility").withStyle(net.minecraft.network.chat.Style.EMPTY.withColor(ChatFormatting.GRAY).withItalic(false)));
nmsItemStack.applyComponents(DataComponentPatch.builder().set(DataComponents.LORE, new ItemLore(loreLines)).build());
ItemStack modified = asBukkitCopy(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<>();
ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
net.minecraft.world.entity.player.Inventory inventory = nmsPlayer.getInventory();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.add(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.containerMenu.broadcastChanges();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Parent</artifactId>
<version>2026.1.5.5</version>
</parent>
<artifactId>ImageFrame-Abstraction</artifactId>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.16.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,85 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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);
}
}
}
+569
View File
@@ -0,0 +1,569 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2026. 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame</artifactId>
<version>2026.1.5.5</version>
<packaging>jar</packaging>
<name>ImageFrame</name>
<description>Put images on maps and walls!</description>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.number></project.build.number>
<project.fullVersion>${project.version}</project.fullVersion>
</properties>
<build>
<defaultGoal>clean install</defaultGoal>
<sourceDirectory>src/main/java</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>**/*.java</exclude>
<exclude>upload/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>upload/**</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>com.loohp.platformscheduler</pattern>
<shadedPattern>com.loohp.imageframe.libs.com.loohp.platformscheduler</shadedPattern>
</relocation>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>com.loohp.imageframe.libs.com.zaxxer.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.simpleyaml</pattern>
<shadedPattern>com.loohp.imageframe.libs.org.simpleyaml</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>com.loohp.imageframe.libs.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.madgag.gif</pattern>
<shadedPattern>com.loohp.imageframe.libs.com.madgag.gif</shadedPattern>
</relocation>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.loohp.imageframe.libs.net.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons</pattern>
<shadedPattern>com.loohp.imageframe.libs.org.apache.commons</shadedPattern>
</relocation>
<relocation>
<pattern>org.jsoup</pattern>
<shadedPattern>com.loohp.imageframe.libs.org.jsoup</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<failOnError>false</failOnError>
<encoding>ISO-8859-1</encoding>
<doctitle>ImageFrame JavaDocs</doctitle>
<windowtitle>ImageFrame JavaDocs</windowtitle>
<additionalOptions>
<additionalOption>-Xdoclint:none</additionalOption>
</additionalOptions>
</configuration>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>jenkins</id>
<activation>
<property>
<name>env.BUILD_NUMBER</name>
</property>
</activation>
<properties>
<project.build.number>-b${env.BUILD_NUMBER}</project.build.number>
<!--<project.fullVersion>${project.version}${project.build.number}</project.fullVersion> -->
<project.fullVersion>${project.version}</project.fullVersion>
</properties>
</profile>
<profile>
<id>release-sign-artifacts</id>
<activation>
<property>
<name>performRelease</name>
<value>true</value>
</property>
</activation>
<properties>
<project.fullVersion>${project.version}</project.fullVersion>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>paper-repo</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>CodeMC</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>dmulloy2-repo</id>
<url>https://repo.dmulloy2.net/nexus/repository/public/</url>
</repository>
<repository>
<id>viaversion-repo</id>
<url>https://repo.viaversion.com</url>
</repository>
<repository>
<id>loohp-repo</id>
<url>https://repo.loohpjames.com/repository</url>
</repository>
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/releases/</url>
</repository>
<repository>
<id>opencollab-snapshot</id>
<url>https://repo.opencollab.dev/main/</url>
</repository>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>7.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>PlatformScheduler</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.viaversion</groupId>
<artifactId>viaversion-api</artifactId>
<version>4.5.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.carleslc.Simple-YAML</groupId>
<artifactId>Simple-Yaml</artifactId>
<version>1.8.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>5.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>5.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>5.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>5.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.madgag</groupId>
<artifactId>animated-gif-lib</artifactId>
<version>1.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.geysermc.geyser</groupId>
<artifactId>api</artifactId>
<version>2.11.2-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.geysermc.floodgate</groupId>
<artifactId>api</artifactId>
<version>2.2.5-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.folia</groupId>
<artifactId>folia-api</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>3.10.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.22.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-Abstraction</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_16</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_16_2</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_16_4</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_17</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_18</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_18_2</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_19</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_19_3</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_19_4</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_20</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_20_2</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_20_3</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_20_5</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_20_6</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_1</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_2</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_3</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_4</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_5</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_6</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_7</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_8</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_9</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_10</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V1_21_11</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V26_1</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V26_1_1</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V26_1_2</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V26_2</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.loohp</groupId>
<artifactId>ImageFrame-V26_3</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,416 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.hooks.bedrock.BedrockHook;
import com.loohp.imageframe.hooks.bedrock.floodgate.FloodgateHookPlatform;
import com.loohp.imageframe.hooks.bedrock.geyser.GeyserHookPlatform;
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.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.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 java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
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;
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 bedrockHook = false;
public static boolean bedrockDisableSmoothAnimationForBedrockPlayers = 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 boolean restrictImageUrlEnabled;
public static List<URL> restrictImageUrls;
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 CustomClientNetworkManager customClientNetworkManager;
public static boolean isURLAllowed(String link) {
if (!restrictImageUrlEnabled) {
return true;
}
if ("upload".equals(link) || imageUploadManager.wasUploaded(link)) {
return true;
}
try {
URL url = new URL(link);
return restrictImageUrls.stream().anyMatch(whitelisted -> {
if (!url.getProtocol().equalsIgnoreCase(whitelisted.getProtocol())) {
return false;
}
if (!url.getHost().equalsIgnoreCase(whitelisted.getHost())) {
return false;
}
return url.getPath().toLowerCase().startsWith(whitelisted.getPath().toLowerCase());
});
} catch (MalformedURLException e) {
return false;
}
}
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;
}
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;
}
if (isPluginEnabled("Geyser-Spigot") || isPluginEnabled("floodgate")) {
if (isPluginEnabled("floodgate")) {
getServer().getConsoleSender().sendMessage(ChatColor.AQUA + "[ImageFrame] ImageFrame has hooked into Floodgate!");
BedrockHook.setBedrockHookPlatform(new FloodgateHookPlatform());
} else {
getServer().getConsoleSender().sendMessage(ChatColor.AQUA + "[ImageFrame] ImageFrame has hooked into Geyser!");
BedrockHook.setBedrockHookPlatform(new GeyserHookPlatform());
}
bedrockHook = 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);
customClientNetworkManager = new CustomClientNetworkManager(imageFrameClientEnabled);
if (isPluginEnabled("PlaceholderAPI")) {
new Placeholders().register();
}
ImageMapLoaders.init();
Scheduler.runTaskAsynchronously(this, () -> imageMapManager.loadMaps(ifPlayerManager));
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");
bedrockDisableSmoothAnimationForBedrockPlayers = config.getConfiguration().getBoolean("Hooks.Geyser.DisableSmoothAnimationForBedrockPlayers");
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");
restrictImageUrlEnabled = config.getConfiguration().getBoolean("Settings.RestrictImageUrl.Enabled");
restrictImageUrls = config.getConfiguration().getStringList("Settings.RestrictImageUrl.Whitelist").stream().map(s -> {
try {
return new URL(s);
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
}).filter(u -> u != null).collect(Collectors.toList());
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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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;
}
}
@@ -1,8 +1,8 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2022. Contributors
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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
@@ -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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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,37 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.bedrock;
import java.util.UUID;
public class BedrockHook {
private static BedrockHookPlatform PLATFORM;
public static void setBedrockHookPlatform(BedrockHookPlatform bedrockHookPlatform) {
PLATFORM = bedrockHookPlatform;
}
public static boolean isBedrockPlayer(UUID uuid) {
return PLATFORM.isBedrockPlayer(uuid);
}
}
@@ -0,0 +1,32 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.bedrock;
import java.util.UUID;
import org.geysermc.cumulus.form.util.FormBuilder;
public interface BedrockHookPlatform {
boolean isBedrockPlayer(UUID uuid);
boolean sendForm(UUID uuid, FormBuilder<?, ?, ?> formBuilder);
}
@@ -0,0 +1,40 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.bedrock.floodgate;
import com.loohp.imageframe.hooks.bedrock.BedrockHookPlatform;
import java.util.UUID;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.floodgate.api.FloodgateApi;
public class FloodgateHookPlatform implements BedrockHookPlatform {
@Override
public boolean isBedrockPlayer(UUID uuid) {
return FloodgateApi.getInstance().isFloodgatePlayer(uuid);
}
@Override
public boolean sendForm(UUID uuid, FormBuilder<?, ?, ?> formBuilder) {
return FloodgateApi.getInstance().sendForm(uuid, formBuilder);
}
}
@@ -0,0 +1,40 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.bedrock.geyser;
import com.loohp.imageframe.hooks.bedrock.BedrockHookPlatform;
import java.util.UUID;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.geyser.api.GeyserApi;
public class GeyserHookPlatform implements BedrockHookPlatform {
@Override
public boolean isBedrockPlayer(UUID uuid) {
return GeyserApi.api().isBedrockPlayer(uuid);
}
@Override
public boolean sendForm(UUID uuid, FormBuilder<?, ?, ?> formBuilder) {
return GeyserApi.api().sendForm(uuid, formBuilder);
}
}
@@ -0,0 +1,35 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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,421 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.CraftingRecipe;
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))));
if (ImageFrame.version.isNewerOrEqualTo(MCVersion.V1_20)) {
CraftingRecipe vanillaRecipe = (CraftingRecipe) Bukkit.getRecipesFor(result).stream()
.filter(r -> r instanceof CraftingRecipe)
.findFirst()
.orElse(null);
if (vanillaRecipe != null) {
recipe.setCategory(vanillaRecipe.getCategory());
recipe.setGroup(vanillaRecipe.getGroup());
}
}
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.getBasePotionType();
if (baseType != null) {
if (baseType.getPotionEffects().stream().anyMatch(e -> e.getType().equals(PotionEffectType.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,245 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.common.collect.Sets;
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 Set<String> getTranslationKeys(String language) {
return Sets.union(translations.getOrDefault(language, Collections.emptyMap()).keySet(), translations.get(ImageFrame.language).keySet());
}
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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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,335 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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 CRAFTER: {
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), 9)) {
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.runTaskLater(ImageFrame.plugin, () -> {
if (!chunk.isLoaded()) {
return;
}
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);
}
}
}
}
}, 1, 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,49 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ImageResolver;
import java.util.Iterator;
import java.util.List;
import net.kyori.adventure.key.Key;
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(ImageResolver.getResolvedImageInputStream(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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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.ImageResolver;
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(ImageResolver.downloadResolvedImage(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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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;
}
}));
}
}
@@ -1,8 +1,8 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2022. Contributors
* Copyright (C) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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) 2026. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2026. 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;
}
}
}
Loaded 100 of 217 files, more files were not shown because too many files have changed in this diff. Show more