Compare commits

...
225 Commits
Author SHA1 Message Date
NotAlexNoyle 16cfe03908 Exclude CODEOWNERS from language statistics 2026-10-29 16:18:59 -04:00
NotAlexNoyle 87c9f7c4ec Add CODEOWNERS (one staff approval) 2026-10-29 16:18:59 -04:00
NotAlexNoyle 3df8a6714a Fix bundled maps 2026-10-08 17:30:34 -04:00
NotAlexNoyle e715691b2b New image storage format 2026-07-15 16:22:41 -04:00
NotAlexNoyle fab4fdcb0e Edge to edge frames, add instructions 2026-07-01 15:08:37 -04:00
NotAlexNoyle 4747f0cbaf Fix console map accessibility 2026-06-25 18:07:47 -04:00
NotAlexNoyle 9ce4964da2 Add preloaded images to count 2026-06-25 16:46:12 -04:00
NotAlexNoyle 7a37baeff3 Fix config loading 2026-06-24 16:03:55 -04:00
NotAlexNoyleandClaude Opus 4.8 31cb6cc430 Fix server hang: run map loading off the main thread
The preloaded-maps completion callback was wired by calling
ImageMapManager.loadMaps() directly on the main thread during onEnable.
loadMaps()'s synchronous prefix loads saved maps via
URLStaticImageMapLoader.load(), which blocks on future.get() for a
MapUtils.createMap() task scheduled onto the main thread -> the main
thread waits on itself and the server hangs while enabling ImageFrame.

Restore the Scheduler.runTaskAsynchronously wrapper so loadMaps runs off
the main thread (as upstream did), while keeping the completion callback
so preloaded maps are still created only after saved maps finish loading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:52:57 -04:00
NotAlexNoyle c7fcba7e2b Fix duplicate preloaded images 2026-06-24 06:53:03 -04:00
NotAlexNoyle e7a04a1148 Preload old maps 2026-06-22 07:22:59 -04:00
NotAlexNoyle d97900d5ed Rename jar to project name 2026-03-25 15:24:20 -04:00
NotAlexNoyle f1792477a5 Remove leftovers 2026-03-25 14:40:53 -04:00
NotAlexNoyle 7daa7a484f Backport to minecraft 1.19.4 2026-03-25 14:40:21 -04:00
NotAlexNoyle e278b8eb88 Remove junk 2026-03-25 14:00:04 -04:00
NotAlexNoyle 20cde1715c Maven -> Gradle 8.14.3 2026-03-25 13:54:59 -04: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
194 changed files with 30046 additions and 2048 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+121
View File
@@ -0,0 +1,121 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Gradle files
.gradle
build
# Eclipse files
.classpath
.project
.settings/
# Package Files #
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/
+63 -3
View File
@@ -1,12 +1,68 @@
[![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!
More information (screenshots, commands, permissions) about the plugin can be found on the Spigot page linked above.
## Creating Images In-Game
The main command is `/imageframe` (aliases: `/iframe`, `/if`, `/frame`). All creation commands are op-only by default.
Sizes are measured in **maps** — one map fills one item frame block, so a `4 x 3` image needs a 4-wide by 3-tall grid of item frames.
Image URLs are **not accepted** — `create`, `overlay` and `refresh` reject any link. Images come from:
- **The images folder** — drop image files (png, jpg, gif, webp, …) into `plugins/ImageFrame/images/` and pass the file name, e.g. `/imageframe create poster poster.png selection`. No restart needed; new files show up in tab completion straight away. File names can't contain spaces.
- **The bundled maps** (below).
- **The upload service**, when `UploadService.Enabled` is `true` in `config.yml` — pass `upload` instead of a file name, then open the link the plugin sends you to upload a file.
### Quick way — create straight onto a wall
1. Place a grid of item frames on the wall (e.g. 4 wide × 3 tall).
2. Run `/imageframe select`, then left-click one corner frame and right-click the opposite corner frame to select the whole grid.
3. Run `/imageframe create <name> <file> selection`, where `<file>` is an image in `plugins/ImageFrame/images/`.
The image is sized to your selection and placed into the frames automatically.
### Manual way — create, then place yourself
1. `/imageframe create <name> <file> <width> <height>` — creates the image map (`width`/`height` in maps).
2. Put it on frames one of two ways:
- `/imageframe get <name>` — gives you the map item(s); place each on the item frames in order (left→right, top→bottom).
- or select a frame grid (as above) and run `/imageframe get <name> selection` to auto-fill it.
### Using bundled maps
Bundled maps are canonical maps shipped inside the plugin jar, under `maps/`. They are re-rendered from the jar on every startup and never touch the plugin data folder, so they survive data wipes — their map ids are kept so existing item frames in the world keep pointing at the right maps, the same way LaunchPads-OG preserves pad locations in its config.
Bundled maps are owned by **Console**. Anyone with access can see them with `/imageframe list` and use them by name:
- `/imageframe get <name> selection`, or reference them explicitly as `Console:<name>`.
#### Adding a bundled map
It works like the images folder: drop the file in and it becomes a map named after the file.
1. Optional, to try it out live first: put the image in `plugins/ImageFrame/images/` and run `imageframe create Console:<name> <file> <width> <height>` from the console, using the file name without its extension as `<name>`. Refresh or recreate it until it looks right, and place it on frames if you like.
2. Copy the image into `common/src/main/resources/maps/`, rebuild the jar, deploy it and restart. No `maps.yml` entry is needed.
On first load the server sets the map up:
- If you did step 1, the bundled map takes over that map's size, dithering and map ids, and the copy in the data folder is removed. Frames you already placed keep working.
- Otherwise the size comes from the image at 128 pixels per map (a 512×384 image becomes 4×3), and the server assigns new map ids. Images that would go over `MaxSize` are skipped with a console message.
The server saves what it decided to `imageframe-bundled-maps.yml` in the main world folder. It stays the same on every restart and survives plugin data folder wipes, so there is nothing to copy back. Keep that file with the world.
Replacing a bundled image with a new file of the same name keeps its size and map ids, and frames update after the restart. Animated GIFs are supported. File names can't contain spaces or colons.
To override the name, size or dithering, or to pin exact map ids, add an entry to `common/src/main/resources/maps.yml` (see the comments at the top of that file). Never change the `MapIds` of an existing map — item frames in the world reference those ids.
### Useful extras
- `/imageframe info <name>` — shows a map's size (handy to build a matching frame grid).
- `/imageframe refresh <name> [file]` — re-renders an existing map. Maps made from the images folder re-read their file, so edit the image in place and refresh to update it live. Pass a different file name (or `upload`) to swap the image. Bundled maps re-render from the jar. Maps whose stored source is a web URL can no longer be refreshed.
- Optional flags on `create`/`get`: a dithering type, and `combined`/`separated`.
### Behaviour of this fork
- Image URLs are not accepted anywhere; the old `RestrictImageUrl` whitelist is gone.
- Images are **stretched to fill** the whole frame grid (edge to edge) instead of being letterboxed with a transparent border. Match your frame grid's width:height to the image's aspect ratio to avoid distortion.
- Item frames filled by the plugin are automatically made **invisible**, so neighbouring tiles join seamlessly with no thin border between them.
## Built against Spigot
Built against [Spigot's API](https://www.spigotmc.org/wiki/buildtools/) (required mc versions are listed on the spigot page above).
Plugins built against Spigot usually also work with [Paper](https://papermc.io/).
@@ -32,6 +88,10 @@ Plugins built against Spigot usually also work with [Paper](https://papermc.io/)
```
Replace `VERSION` with the version number.
## Official Addons
- [ImageFrameClient](https://github.com/LOOHP/ImageFrameClient) (Complementary client mod for servers with ImageFrame to display HD and full color images)
## Partnerships
### Server Hosting
@@ -42,4 +102,4 @@ It also **supports my development**, take it as an alternative way to donate whi
**https://www.bisecthosting.com/loohp**
![https://www.bisecthosting.com/loohp](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)
@@ -0,0 +1,424 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import net.kyori.adventure.key.Key;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.server.v1_16_R1.ChunkProviderServer;
import net.minecraft.server.v1_16_R1.DataWatcher;
import net.minecraft.server.v1_16_R1.DataWatcherObject;
import net.minecraft.server.v1_16_R1.EntityHuman;
import net.minecraft.server.v1_16_R1.EntityItemFrame;
import net.minecraft.server.v1_16_R1.EntityPlayer;
import net.minecraft.server.v1_16_R1.IChatBaseComponent;
import net.minecraft.server.v1_16_R1.ItemWorldMap;
import net.minecraft.server.v1_16_R1.MapIcon;
import net.minecraft.server.v1_16_R1.MinecraftKey;
import net.minecraft.server.v1_16_R1.NBTTagCompound;
import net.minecraft.server.v1_16_R1.NBTTagList;
import net.minecraft.server.v1_16_R1.NBTTagString;
import net.minecraft.server.v1_16_R1.Packet;
import net.minecraft.server.v1_16_R1.PacketPlayOutEntityMetadata;
import net.minecraft.server.v1_16_R1.PacketPlayOutMap;
import net.minecraft.server.v1_16_R1.PersistentIdCounts;
import net.minecraft.server.v1_16_R1.PlayerChunkMap;
import net.minecraft.server.v1_16_R1.PlayerInventory;
import net.minecraft.server.v1_16_R1.ResourceKey;
import net.minecraft.server.v1_16_R1.WorldMap;
import net.minecraft.server.v1_16_R1.WorldServer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.bukkit.craftbukkit.v1_16_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_16_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_16_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_16_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_16_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_16_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_16_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_16 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field nmsMapIconTypeDisplayField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_16() {
try {
nmsEntityByteDataWatcherField = net.minecraft.server.v1_16_R1.Entity.class.getDeclaredField("T");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsMapIconTypeDisplayField = MapIcon.Type.class.getDeclaredField("C");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ITEM");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("a");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
try {
nmsMapIconTypeDisplayField.setAccessible(true);
return nmsMapIconTypeDisplayField.getBoolean(mapIconType);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"unchecked", "resource"})
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().D().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked", "resource"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.server.v1_16_R1.World> worldTypeKey = worldServer.getDimensionKey();
String mapId = ItemWorldMap.a(id);
WorldMap worldMap = new WorldMap(mapId);
worldMap.a(spawnLocation.getBlockX(), spawnLocation.getBlockZ(), 3, false, false, worldTypeKey);
worldServer.a(worldMap);
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().D().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
PlayerChunkMap playerChunkMap = chunkProviderServer.playerChunkMap;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.trackedEntities;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (EntityPlayer player : entityTracker.trackedPlayers) {
players.add(player.getBukkitEntity());
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? Collections.emptyList() : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
byte[] colors0;
int size;
if (colors == null) {
colors0 = EMPTY_BYTE_ARRAY;
size = 0;
} else {
colors0 = colors;
size = 128;
}
return new PacketPlayOutMap(mapId, (byte) 0, false, false, mapIcons, colors0, 0, 0, size, size);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.server.v1_16_R1.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.server.v1_16_R1.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata();
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.server.v1_16_R1.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.getDataWatcher();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.get(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().playerConnection.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == 9) {
NBTTagList loreLineTagList = displayTag.getList("Lore", 8);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.getString(i));
}
} else {
loreLines = new ArrayList<>(1);
}
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
translatableComponent.setColor(ChatColor.GRAY);
translatableComponent.setItalic(false);
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.set("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.inventory;
for (ItemStack itemStack : itemStacks) {
net.minecraft.server.v1_16_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.pickup(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.activeContainer.c();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
MinecraftKey key = nmsWorld.getTypeKey().a();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,424 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import net.kyori.adventure.key.Key;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.server.v1_16_R2.ChunkProviderServer;
import net.minecraft.server.v1_16_R2.DataWatcher;
import net.minecraft.server.v1_16_R2.DataWatcherObject;
import net.minecraft.server.v1_16_R2.EntityHuman;
import net.minecraft.server.v1_16_R2.EntityItemFrame;
import net.minecraft.server.v1_16_R2.EntityPlayer;
import net.minecraft.server.v1_16_R2.IChatBaseComponent;
import net.minecraft.server.v1_16_R2.ItemWorldMap;
import net.minecraft.server.v1_16_R2.MapIcon;
import net.minecraft.server.v1_16_R2.MinecraftKey;
import net.minecraft.server.v1_16_R2.NBTTagCompound;
import net.minecraft.server.v1_16_R2.NBTTagList;
import net.minecraft.server.v1_16_R2.NBTTagString;
import net.minecraft.server.v1_16_R2.Packet;
import net.minecraft.server.v1_16_R2.PacketPlayOutEntityMetadata;
import net.minecraft.server.v1_16_R2.PacketPlayOutMap;
import net.minecraft.server.v1_16_R2.PersistentIdCounts;
import net.minecraft.server.v1_16_R2.PlayerChunkMap;
import net.minecraft.server.v1_16_R2.PlayerInventory;
import net.minecraft.server.v1_16_R2.ResourceKey;
import net.minecraft.server.v1_16_R2.WorldMap;
import net.minecraft.server.v1_16_R2.WorldServer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.bukkit.craftbukkit.v1_16_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_16_R2.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_16_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_16_R2.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_16_R2.map.CraftMapView;
import org.bukkit.craftbukkit.v1_16_R2.map.RenderData;
import org.bukkit.craftbukkit.v1_16_R2.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_16_2 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field nmsMapIconTypeDisplayField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_16_2() {
try {
nmsEntityByteDataWatcherField = net.minecraft.server.v1_16_R2.Entity.class.getDeclaredField("S");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsMapIconTypeDisplayField = MapIcon.Type.class.getDeclaredField("C");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ITEM");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("a");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
try {
nmsMapIconTypeDisplayField.setAccessible(true);
return nmsMapIconTypeDisplayField.getBoolean(mapIconType);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"unchecked", "resource"})
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked", "resource"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.server.v1_16_R2.World> worldTypeKey = worldServer.getDimensionKey();
String mapId = ItemWorldMap.a(id);
WorldMap worldMap = new WorldMap(mapId);
worldMap.a(spawnLocation.getBlockX(), spawnLocation.getBlockZ(), 3, false, false, worldTypeKey);
worldServer.a(worldMap);
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
PlayerChunkMap playerChunkMap = chunkProviderServer.playerChunkMap;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.trackedEntities;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (EntityPlayer player : entityTracker.trackedPlayers) {
players.add(player.getBukkitEntity());
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? Collections.emptyList() : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
byte[] colors0;
int size;
if (colors == null) {
colors0 = EMPTY_BYTE_ARRAY;
size = 0;
} else {
colors0 = colors;
size = 128;
}
return new PacketPlayOutMap(mapId, (byte) 0, false, false, mapIcons, colors0, 0, 0, size, size);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.server.v1_16_R2.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.server.v1_16_R2.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata();
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.server.v1_16_R2.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.getDataWatcher();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.get(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().playerConnection.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == 9) {
NBTTagList loreLineTagList = displayTag.getList("Lore", 8);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.getString(i));
}
} else {
loreLines = new ArrayList<>(1);
}
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
translatableComponent.setColor(ChatColor.GRAY);
translatableComponent.setItalic(false);
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.set("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.inventory;
for (ItemStack itemStack : itemStacks) {
net.minecraft.server.v1_16_R2.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.pickup(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.activeContainer.c();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
MinecraftKey key = nmsWorld.getTypeKey().a();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,424 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import net.kyori.adventure.key.Key;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.server.v1_16_R3.ChunkProviderServer;
import net.minecraft.server.v1_16_R3.DataWatcher;
import net.minecraft.server.v1_16_R3.DataWatcherObject;
import net.minecraft.server.v1_16_R3.EntityHuman;
import net.minecraft.server.v1_16_R3.EntityItemFrame;
import net.minecraft.server.v1_16_R3.EntityPlayer;
import net.minecraft.server.v1_16_R3.IChatBaseComponent;
import net.minecraft.server.v1_16_R3.ItemWorldMap;
import net.minecraft.server.v1_16_R3.MapIcon;
import net.minecraft.server.v1_16_R3.MinecraftKey;
import net.minecraft.server.v1_16_R3.NBTTagCompound;
import net.minecraft.server.v1_16_R3.NBTTagList;
import net.minecraft.server.v1_16_R3.NBTTagString;
import net.minecraft.server.v1_16_R3.Packet;
import net.minecraft.server.v1_16_R3.PacketPlayOutEntityMetadata;
import net.minecraft.server.v1_16_R3.PacketPlayOutMap;
import net.minecraft.server.v1_16_R3.PersistentIdCounts;
import net.minecraft.server.v1_16_R3.PlayerChunkMap;
import net.minecraft.server.v1_16_R3.PlayerInventory;
import net.minecraft.server.v1_16_R3.ResourceKey;
import net.minecraft.server.v1_16_R3.WorldMap;
import net.minecraft.server.v1_16_R3.WorldServer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.bukkit.craftbukkit.v1_16_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_16_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_16_R3.map.CraftMapView;
import org.bukkit.craftbukkit.v1_16_R3.map.RenderData;
import org.bukkit.craftbukkit.v1_16_R3.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_16_4 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field nmsMapIconTypeDisplayField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_16_4() {
try {
nmsEntityByteDataWatcherField = net.minecraft.server.v1_16_R3.Entity.class.getDeclaredField("S");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsMapIconTypeDisplayField = MapIcon.Type.class.getDeclaredField("C");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ITEM");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("a");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.colors = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.humans;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
try {
nmsMapIconTypeDisplayField.setAccessible(true);
return nmsMapIconTypeDisplayField.getBoolean(mapIconType);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"unchecked", "resource"})
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked", "resource"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.server.v1_16_R3.World> worldTypeKey = worldServer.getDimensionKey();
String mapId = ItemWorldMap.a(id);
WorldMap worldMap = new WorldMap(mapId);
worldMap.a(spawnLocation.getBlockX(), spawnLocation.getBlockZ(), 3, false, false, worldTypeKey);
worldServer.a(worldMap);
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts");
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
PlayerChunkMap playerChunkMap = chunkProviderServer.playerChunkMap;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.trackedEntities;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (EntityPlayer player : entityTracker.trackedPlayers) {
players.add(player.getBukkitEntity());
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? Collections.emptyList() : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
byte[] colors0;
int size;
if (colors == null) {
colors0 = EMPTY_BYTE_ARRAY;
size = 0;
} else {
colors0 = colors;
size = 128;
}
return new PacketPlayOutMap(mapId, (byte) 0, false, false, mapIcons, colors0, 0, 0, size, size);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.server.v1_16_R3.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.server.v1_16_R3.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata();
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.server.v1_16_R3.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.getDataWatcher();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.get(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().playerConnection.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == 9) {
NBTTagList loreLineTagList = displayTag.getList("Lore", 8);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.getString(i));
}
} else {
loreLines = new ArrayList<>(1);
}
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
translatableComponent.setColor(ChatColor.GRAY);
translatableComponent.setItalic(false);
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.set("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.inventory;
for (ItemStack itemStack : itemStacks) {
net.minecraft.server.v1_16_R3.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.pickup(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.activeContainer.c();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
MinecraftKey key = nmsWorld.getTypeKey().a();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,412 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import net.kyori.adventure.key.Key;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.MinecraftKey;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.bukkit.craftbukkit.libs.it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.bukkit.craftbukkit.v1_17_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_17_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_17_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_17_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_17 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_17() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ao");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked", "resource"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.getDimensionKey();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.getChunkProvider();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.G;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.getDataWatcher();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.get(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().b.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(CombinedMapItemInfo.KEY);
if (!tag.hasKey(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.hasKey(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.getFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.getIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.setFloat(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.setIntArray(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getTag();
if (tag == null || !tag.hasKey(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.getInt(FilledMapItemInfo.KEY);
int mapPartIndex = tag.getInt(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.getOrCreateTag();
tag.setInt(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.setInt(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.getList("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.getString(i));
}
} else {
loreLines = new ArrayList<>(1);
}
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
translatableComponent.setColor(ChatColor.GRAY);
translatableComponent.setItalic(false);
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.set("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.getInventory();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.pickup(nmsItemStack);
if (!added || !nmsItemStack.isEmpty()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bU.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
MinecraftKey key = nmsWorld.getTypeKey().a();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,412 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.MinecraftKey;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_18_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_18_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_18_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_18_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_18_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_18 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_18() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("aa");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ap");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().D().u().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aa();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().D().u().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.I;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ai();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.s();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.t();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.s();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.t();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
translatableComponent.setColor(ChatColor.GRAY);
translatableComponent.setItalic(false);
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fq();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bW.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
WorldServer nmsWorld = ((CraftWorld) world).getHandle();
MinecraftKey key = nmsWorld.getTypeKey().a();
return Key.key(key.b(), key.a());
}
}
@@ -0,0 +1,411 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_18_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_18_R2.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_18_R2.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_18_R2.map.CraftMapView;
import org.bukkit.craftbukkit.v1_18_R2.map.RenderData;
import org.bukkit.craftbukkit.v1_18_R2.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_18_2 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_18_2() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ao");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().D().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aa();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().D().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.J;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ai();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.t();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.t();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
TranslatableComponent translatableComponent = new TranslatableComponent("effect.minecraft.invisibility");
translatableComponent.setColor(ChatColor.GRAY);
translatableComponent.setItalic(false);
loreLines.add(0, ComponentSerializer.toString(translatableComponent));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fr();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bU.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,407 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import com.loohp.imageframe.utils.UnsafeAccessor;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_19_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_19_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_19_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_19_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_19_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_19 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field[] nmsPacketPlayOutEntityMetadataFields;
private final Field nmsItemFrameItemStackDataWatcherField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_19() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
nmsPacketPlayOutEntityMetadataFields = PacketPlayOutEntityMetadata.class.getDeclaredFields();
nmsItemFrameItemStackDataWatcherField = EntityItemFrame.class.getDeclaredField("ao");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ab();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.K;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@SuppressWarnings("unchecked")
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
try {
nmsItemFrameItemStackDataWatcherField.setAccessible(true);
DataWatcherObject<net.minecraft.world.item.ItemStack> dataWatcherObject = (DataWatcherObject<net.minecraft.world.item.ItemStack>) nmsItemFrameItemStackDataWatcherField.get(null);
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(dataWatcherObject, CraftItemStack.asNMSCopy(itemStack)));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ai();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.Item<?>> dataWatchers = Collections.singletonList(new DataWatcher.Item<>(byteField, value));
PacketPlayOutEntityMetadata packet = (PacketPlayOutEntityMetadata) UnsafeAccessor.getUnsafe().allocateInstance(PacketPlayOutEntityMetadata.class);
nmsPacketPlayOutEntityMetadataFields[0].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[0].setInt(packet, entityId);
nmsPacketPlayOutEntityMetadataFields[1].setAccessible(true);
nmsPacketPlayOutEntityMetadataFields[1].set(packet, dataWatchers);
return packet;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fA();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bU.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_19_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_19_R2.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_19_R2.map.CraftMapView;
import org.bukkit.craftbukkit.v1_19_R2.map.RenderData;
import org.bukkit.craftbukkit.v1_19_R2.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_19_3 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_19_3() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("Z");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ac();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().C().t().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.L;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.ao, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.al();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fE();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bU.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_19_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_19_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_19_R3.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_19_R3.map.CraftMapView;
import org.bukkit.craftbukkit.v1_19_R3.map.RenderData;
import org.bukkit.craftbukkit.v1_19_R3.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_19_4 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_19_4() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("an");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ab();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.L;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.aj();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().b.a((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.u();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fJ();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bP.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_20_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_20_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_20 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_20() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("an");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.b();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ac();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts::b, PersistentIdCounts::new, PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.b();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.K;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.aj();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.b(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.a((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.w();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.w();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fN();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bR.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,385 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ChunkProviderServer;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.PlayerChunkMap;
import net.minecraft.server.level.WorldServer;
import net.minecraft.server.network.PlayerConnection;
import net.minecraft.server.network.ServerPlayerConnection;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R2.map.CraftMapView;
import org.bukkit.craftbukkit.v1_20_R2.map.RenderData;
import org.bukkit.craftbukkit.v1_20_R2.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_20_2 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_20_2() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("ao");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.d();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ac();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.n().D().s().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
WorldServer worldServer = ((CraftWorld) entity.getWorld()).getHandle();
ChunkProviderServer chunkProviderServer = worldServer.k();
PlayerChunkMap playerChunkMap = chunkProviderServer.a;
Int2ObjectMap<PlayerChunkMap.EntityTracker> entityTrackers = playerChunkMap.K;
PlayerChunkMap.EntityTracker entityTracker = entityTrackers.get(entity.getEntityId());
if (entityTracker == null) {
return Collections.emptySet();
} else {
Set<Player> players = new HashSet<>();
for (ServerPlayerConnection connection : entityTracker.f) {
if (connection instanceof PlayerConnection) {
players.add(((PlayerConnection) connection).getCraftPlayer());
}
}
return players;
}
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.al();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.b(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.b((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.w();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.w();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fR();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bS.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,365 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.ItemWorldMap;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R3.map.CraftMapView;
import org.bukkit.craftbukkit.v1_20_R3.map.RenderData;
import org.bukkit.craftbukkit.v1_20_R3.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_20_3 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_20_3() {
try {
nmsEntityByteDataWatcherField = net.minecraft.world.entity.Entity.class.getDeclaredField("ao");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = PersistentIdCounts.class.getDeclaredField("b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
MapIcon.Type mapIconType = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(mapIconType, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), iChat);
}
@SuppressWarnings("deprecation")
@Override
public MapIcon.Type toNMSMapIconType(MapCursor.Type type) {
return MapIcon.Type.a(type.getValue());
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
MapIcon.Type mapIconType = toNMSMapIconType(type);
return mapIconType.d();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.o().F().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ae();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
worldServer.a(ItemWorldMap.a(id), worldMap);
PersistentIdCounts persistentIdCounts = worldServer.o().F().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(mapId, (byte) 0, false, mapIcons, b);
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.an();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.b(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.b<?>> dataWatchers = Collections.singletonList(DataWatcher.b.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.b((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
if (tag == null || !tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.w();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.v();
if (tag == null || !tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound tag = nmsItemStack.w();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
NBTTagCompound displayTag = nmsItemStack.a("display");
List<String> loreLines;
if (displayTag.d("Lore") == NBTBase.q) {
NBTTagList loreLineTagList = displayTag.c("Lore", NBTBase.p);
loreLines = new ArrayList<>(loreLineTagList.size());
for (int i = 0; i < loreLineTagList.size(); i++) {
loreLines.add(loreLineTagList.j(i));
}
} else {
loreLines = new ArrayList<>(1);
}
loreLines.add(0, CraftChatMessage.toJSON(IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))));
NBTTagList loreLineTagList = new NBTTagList();
for (int i = 0; i < loreLines.size(); i++) {
loreLineTagList.b(i, NBTTagString.a(loreLines.get(i)));
}
displayTag.a("Lore", loreLineTagList);
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fS();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.e(nmsItemStack);
if (!added || !nmsItemStack.b()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bS.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R4.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapView;
import org.bukkit.craftbukkit.v1_20_R4.map.RenderData;
import org.bukkit.craftbukkit.v1_20_R4.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_20_5 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_20_5() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ap();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gc();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.e()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cb.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R4.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_20_R4.map.CraftMapView;
import org.bukkit.craftbukkit.v1_20_R4.map.RenderData;
import org.bukkit.craftbukkit.v1_20_R4.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_20_6 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_20_6() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.o;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.g, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ap();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gc();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.e()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cb.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_21() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.f, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ar();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fY();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.e()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cd.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R1.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R1.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R1.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_1 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_21_1() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "ap");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.af();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.o().I().u().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.b b = colors == null ? null : new WorldMap.b(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(b));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.f, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ar();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().c.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.i, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.i, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.fY();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.e()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cd.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R6.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R6.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R6.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R6.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_10 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_10() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("deprecation")
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.al();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.t();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.aC();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.b();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.b();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.b();
if (!tag.b(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.b();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gB();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cl.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,391 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R7.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R7.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R7.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R7.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R7.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R7.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_11 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_11() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("DataFlowIssue")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.s().N().A().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "DataFlowIssue"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aq();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.s().N().A().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.u();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.aD();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.b();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.b();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.b();
if (!tag.b(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.b();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.m, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.m, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gK();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cn.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,370 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R2.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R2.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R2.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_2 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_21_2() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ah();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.au();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gi();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cd.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,369 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R2.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R2.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R2.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R2.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_3 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
public V1_21_3() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ah();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.p().J().v().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, renderData.cursors);
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.au();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.c();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.c();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gi();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cd.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,378 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R3.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R3.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R3.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R3.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R3.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_4 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsUsedAuxIdsField;
private final Field renderDataCursorsField;
public V1_21_4() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
persistentIdCountsUsedAuxIdsField = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, Object2IntMap.class, "usedAuxIds", "b");
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.g = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.p;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@SuppressWarnings("unchecked")
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsUsedAuxIdsField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
return usedAuxIds.getInt("map") + 1;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsUsedAuxIdsField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.ai();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.a(), PersistentIdCounts.a);
Object2IntMap<String> usedAuxIds = (Object2IntMap<String>) persistentIdCountsUsedAuxIdsField.get(persistentIdCounts);
int freeAuxValue = usedAuxIds.getInt("map");
if (freeAuxValue < id) {
usedAuxIds.put("map", id);
persistentIdCounts.c();
}
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.au();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
}
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.e(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(CombinedMapItemInfo.KEY);
if (!tag.e(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.e(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.j(CombinedMapItemInfo.PLACEMENT_YAW_KEY);
UUID uuid = UUIDUtils.fromIntArray(tag.n(CombinedMapItemInfo.PLACEMENT_UUID_KEY));
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.e(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.h(FilledMapItemInfo.KEY);
int mapPartIndex = tag.h(FilledMapItemInfo.INDEX_KEY);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gi();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.f(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cd.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R4.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R4.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R4.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R4.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R4.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R4.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_5 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_5() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "am");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("deprecation")
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.p().J().w().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.f();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.e, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.ar();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().f.sendPacket((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gj();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.bR.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R5.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R5.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R5.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_6 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_6() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "az");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("deprecation")
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.e();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.d, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.au();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().g.sendPacket((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gs();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cn.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R5.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R5.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R5.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_7 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_7() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "az");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("deprecation")
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.e();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.d, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.au();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gs();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cn.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R5.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R5.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R5.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R5.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_8 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_8() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "az");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("deprecation")
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.aj();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.q().J().x().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.e();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.d, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.au();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.d();
if (!tag.b(FilledMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.d();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gs();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cn.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,390 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.google.common.collect.Collections2;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import com.loohp.imageframe.utils.ReflectionUtils;
import com.loohp.imageframe.utils.UUIDUtils;
import net.kyori.adventure.key.Key;
import net.minecraft.EnumChatFormat;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.chat.ChatModifier;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
import net.minecraft.network.protocol.game.PacketPlayOutMap;
import net.minecraft.network.syncher.DataWatcher;
import net.minecraft.network.syncher.DataWatcherObject;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.EntityPlayer;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.entity.decoration.EntityItemFrame;
import net.minecraft.world.entity.player.EntityHuman;
import net.minecraft.world.entity.player.PlayerInventory;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.saveddata.maps.MapDecorationType;
import net.minecraft.world.level.saveddata.maps.MapIcon;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.PersistentIdCounts;
import net.minecraft.world.level.saveddata.maps.WorldMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R6.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_21_R6.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R6.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapCursor;
import org.bukkit.craftbukkit.v1_21_R6.map.CraftMapView;
import org.bukkit.craftbukkit.v1_21_R6.map.RenderData;
import org.bukkit.craftbukkit.v1_21_R6.util.CraftChatMessage;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class V1_21_9 extends NMSWrapper {
private final Field nmsEntityByteDataWatcherField;
private final Field craftMapViewWorldMapField;
private final Field persistentIdCountsLastMapIdField;
private final Field renderDataCursorsField;
public V1_21_9() {
try {
nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA");
craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap");
Field persistentIdCountsLastMapIdField0;
try {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d");
} catch (NoSuchFieldException e) {
persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d");
}
persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0;
renderDataCursorsField = RenderData.class.getField("cursors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public WorldMap getWorldMap(MapView mapView) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
craftMapViewWorldMapField.setAccessible(true);
return (WorldMap) craftMapViewWorldMapField.get(craftMapView);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void setColors(MapView mapView, byte[] colors) {
if (colors.length != COLOR_ARRAY_LENGTH) {
throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH);
}
WorldMap nmsWorldMap = getWorldMap(mapView);
nmsWorldMap.h = colors;
}
@Override
public Collection<Player> getViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity());
}
@Override
public boolean hasViewers(MapView mapView) {
WorldMap nmsWorldMap = getWorldMap(mapView);
Map<EntityHuman, WorldMap.WorldMapHumanTracker> humansMap = nmsWorldMap.q;
return !humansMap.isEmpty();
}
@Override
public MapIcon toNMSMapIcon(MapCursor mapCursor) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(mapCursor.getType());
IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption());
return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat));
}
@Override
public Holder<MapDecorationType> toNMSMapIconType(MapCursor.Type type) {
return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type);
}
@Override
public boolean isRenderOnFrame(MapCursor.Type type) {
Holder<MapDecorationType> decorationTypeHolder = toNMSMapIconType(type);
return decorationTypeHolder.a().c();
}
@Override
public int getNextAvailableMapId(World world) {
try {
persistentIdCountsLastMapIdField.setAccessible(true);
WorldServer worldServer = ((CraftWorld) world).getHandle();
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
return atomicInteger.get() + 1;
} else {
return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("deprecation")
@Override
public MapView getMapOrCreateMissing(World world, int id) {
try {
MapView mapView = Bukkit.getMap(id);
if (mapView != null) {
return mapView;
}
persistentIdCountsLastMapIdField.setAccessible(true);
Location spawnLocation = world.getSpawnLocation();
WorldServer worldServer = ((CraftWorld) world).getHandle();
ResourceKey<net.minecraft.world.level.World> worldTypeKey = worldServer.al();
WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey);
MapId mapId = new MapId(id);
worldServer.a(mapId, worldMap);
PersistentIdCounts persistentIdCounts = worldServer.q().O().y().a(PersistentIdCounts.b);
if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) {
AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts);
atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id));
} else {
int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts);
persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id));
}
persistentIdCounts.t();
return Bukkit.getMap(id);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player) {
try {
CraftMapView craftMapView = (CraftMapView) mapView;
CraftPlayer craftPlayer = (CraftPlayer) player;
RenderData renderData = craftMapView.render(craftPlayer);
return new MutablePair<>(renderData.buffer, (List<MapCursor>) renderDataCursorsField.get(renderData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Player> getEntityTrackers(Entity entity) {
return new HashSet<>(entity.getTrackedBy());
}
@Override
public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors) {
List<MapIcon> mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList());
WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors);
return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c));
}
@Override
public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) {
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack)));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
}
@SuppressWarnings("unchecked")
@Override
public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) {
try {
int entityId = entity.getEntityId();
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
nmsEntityByteDataWatcherField.setAccessible(true);
DataWatcher watcher = nmsEntity.aC();
DataWatcherObject<Byte> byteField = (DataWatcherObject<Byte>) nmsEntityByteDataWatcherField.get(null);
byte value = watcher.a(byteField);
if (invisible != null) {
if (invisible) {
value = (byte) (value | 0x20);
} else {
value = (byte) (value & ~0x20);
}
}
if (glowing != null) {
if (glowing) {
value = (byte) (value | 0x40);
} else {
value = (byte) (value & ~0x40);
}
}
List<DataWatcher.c<?>> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value));
return new PacketPlayOutEntityMetadata(entityId, dataWatchers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void sendPacket(Player player, Object packet) {
((CraftPlayer) player).getHandle().g.b((Packet<?>) packet);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.b();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1);
if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) {
return new CombinedMapItemInfo(imageMapIndex);
}
float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F);
UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get());
return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
}
@Override
public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.b();
tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex());
if (combinedMapItemInfo.hasPlacement()) {
CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement();
tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw());
tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId()));
}
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b);
if (customData == null) {
return null;
}
NBTTagCompound tag = customData.b();
if (!tag.b(CombinedMapItemInfo.KEY)) {
return null;
}
int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1);
int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1);
return new FilledMapItemInfo(imageMapIndex, mapPartIndex);
}
@Override
public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a);
NBTTagCompound tag = customData.b();
tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex());
tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getImageMapIndex());
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a());
return CraftItemStack.asCraftMirror(nmsItemStack);
}
@Override
public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
ItemLore itemLore = nmsItemStack.a(DataComponents.j, ItemLore.a);
List<IChatBaseComponent> loreLines = new ArrayList<>(itemLore.a());
loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false)));
nmsItemStack.b(DataComponentPatch.a().a(DataComponents.j, new ItemLore(loreLines)).a());
ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack);
ItemMeta itemMeta = modified.getItemMeta();
if (itemMeta == null) {
return itemStack;
}
itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
modified.setItemMeta(itemMeta);
return modified;
}
@Override
public List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks) {
List<ItemStack> leftovers = new ArrayList<>();
EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
PlayerInventory inventory = nmsPlayer.gB();
for (ItemStack itemStack : itemStacks) {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
boolean added = inventory.g(nmsItemStack);
if (!added || !nmsItemStack.f()) {
leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack));
}
}
nmsPlayer.cl.d();
return leftovers;
}
@SuppressWarnings("PatternValidation")
public Key getWorldNamespacedKey(World world) {
NamespacedKey key = world.getKey();
return Key.key(key.getNamespace(), key.getKey());
}
}
@@ -0,0 +1,85 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.loohp.imageframe.objectholders.CombinedMapItemInfo;
import com.loohp.imageframe.objectholders.FilledMapItemInfo;
import com.loohp.imageframe.objectholders.MutablePair;
import net.kyori.adventure.key.Key;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public abstract class NMSWrapper {
public static final int COLOR_ARRAY_LENGTH = 16384;
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
public abstract void setColors(MapView mapView, byte[] colors);
public abstract Collection<Player> getViewers(MapView mapView);
public abstract boolean hasViewers(MapView mapView);
public abstract Object toNMSMapIcon(MapCursor mapCursor);
public abstract Object toNMSMapIconType(MapCursor.Type type);
public abstract boolean isRenderOnFrame(MapCursor.Type type);
public abstract int getNextAvailableMapId(World world);
public abstract MapView getMapOrCreateMissing(World world, int id);
public abstract MutablePair<byte[], List<MapCursor>> bukkitRenderMap(MapView mapView, Player player);
public abstract Set<Player> getEntityTrackers(Entity entity);
public abstract Object createMapPacket(int mapId, byte[] colors, Collection<MapCursor> cursors);
public abstract Object createItemFrameItemChangePacket(int entityId, ItemStack itemStack);
public abstract Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing);
public abstract void sendPacket(Player player, Object packet);
public abstract CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack);
public abstract ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo);
public abstract FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack);
public abstract ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo);
public abstract ItemStack withInvisibleItemFrameMeta(ItemStack itemStack);
public abstract List<ItemStack> giveItems(Player player, List<ItemStack> itemStacks);
public abstract Key getWorldNamespacedKey(World world);
}
@@ -0,0 +1,74 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.UUID;
public class CombinedMapItemInfo {
public static final String KEY = "CombinedImageMap";
public static final String PLACEMENT_YAW_KEY = "CombinedImageMapPlacementYaw";
public static final String PLACEMENT_UUID_KEY = "CombinedImageMapPlacementUUID";
private final int imageMapIndex;
private final PlacementInfo placementInfo;
public CombinedMapItemInfo(int imageMapIndex, PlacementInfo placementInfo) {
this.imageMapIndex = imageMapIndex;
this.placementInfo = placementInfo;
}
public CombinedMapItemInfo(int imageMapIndex) {
this(imageMapIndex, null);
}
public int getImageMapIndex() {
return imageMapIndex;
}
public boolean hasPlacement() {
return placementInfo != null;
}
public PlacementInfo getPlacement() {
return placementInfo;
}
public static class PlacementInfo {
private final float yaw;
private final UUID uuid;
public PlacementInfo(float yaw, UUID uuid) {
this.yaw = yaw;
this.uuid = uuid;
}
public float getYaw() {
return yaw;
}
public UUID getUniqueId() {
return uuid;
}
}
}
@@ -0,0 +1,44 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
public class FilledMapItemInfo {
public static final String KEY = "ImageMap";
public static final String INDEX_KEY = "ImageMapPart";
private final int imageMapIndex;
private final int mapPartIndex;
public FilledMapItemInfo(int imageMapIndex, int mapPartIndex) {
this.imageMapIndex = imageMapIndex;
this.mapPartIndex = mapPartIndex;
}
public int getImageMapIndex() {
return imageMapIndex;
}
public int getMapPartIndex() {
return mapPartIndex;
}
}
@@ -0,0 +1,78 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
public class MutablePair<F, S> {
private F first;
private S second;
public MutablePair(F first, S second) {
this.first = first;
this.second = second;
}
public void setFirst(F first) {
this.first = first;
}
public void setSecond(S second) {
this.second = second;
}
public F getFirst() {
return first;
}
public S getSecond() {
return second;
}
public <T> T product(BiFunction<F, S, T> function) {
return function.apply(first, second);
}
public void consume(BiConsumer<F, S> consumer) {
consumer.accept(first, second);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MutablePair<?, ?> that = (MutablePair<?, ?>) o;
return Objects.equals(first, that.first) && Objects.equals(second, that.second);
}
@Override
public int hashCode() {
return Objects.hash(first, second);
}
}
@@ -0,0 +1,63 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionUtils {
public static Field findDeclaredField(Class<?> clazz, Class<?> fieldType, String... names) throws NoSuchFieldException {
NoSuchFieldException exception = null;
for (String name : names) {
try {
Field field = clazz.getDeclaredField(name);
if (field.getType().equals(fieldType)) {
return field;
}
} catch (NoSuchFieldException e) {
exception = e;
}
}
if (exception == null) {
throw new NoSuchFieldException();
} else {
throw exception;
}
}
public static Method findDeclaredMethod(Class<?> clazz, Class<?>[] parameters, String... names) throws NoSuchMethodException {
NoSuchMethodException exception = null;
for (String name : names) {
try {
return clazz.getDeclaredMethod(name, parameters);
} catch (NoSuchMethodException e) {
exception = e;
}
}
if (exception == null) {
throw new NoSuchMethodException();
} else {
throw exception;
}
}
}
@@ -0,0 +1,40 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.utils;
import java.util.UUID;
public class UUIDUtils {
public static UUID fromIntArray(int[] array) {
if (array.length != 4) {
throw new IllegalArgumentException("array length is not 4");
}
return new UUID((long) array[0] << 32 | (long) array[1] & 4294967295L, (long) array[2] << 32 | (long) array[3] & 4294967295L);
}
public static int[] toIntArray(UUID uuid) {
long high = uuid.getMostSignificantBits();
long low = uuid.getLeastSignificantBits();
return new int[] {(int) (high >>> 32), (int) high, (int) (high >>> low), (int) low};
}
}
@@ -0,0 +1,52 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.utils;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeAccessor {
private static final Field unsafeField;
private static Unsafe unsafe;
static {
try {
unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public static Unsafe getUnsafe() {
if (unsafe != null) {
return unsafe;
}
try {
unsafeField.setAccessible(true);
return unsafe = (Unsafe) unsafeField.get(null);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
+265
View File
@@ -0,0 +1,265 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.attributes.java.TargetJvmVersion
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.jvm.toolchain.JvmVendorSpec
import org.gradle.jvm.tasks.Jar
import java.io.File
plugins {
id("java")
id("com.gradleup.shadow") version "8.3.9" apply false
eclipse
}
group = "com.loohp"
version = "1.9.0.3"
val adventureVersion = "4.25.0"
val moduleMinecraftVersions =
mapOf(
"V1_19_4" to "1.19.4-R0.1-SNAPSHOT",
)
fun localMavenRepoDirs(project: Project): List<File> {
val repos = mutableListOf(
File(System.getProperty("user.home"), ".m2/repository"),
)
System.getProperty("SELF_MAVEN_LOCAL_REPO")?.let { path ->
repos.add(project.file(path))
}
return repos.distinct()
}
fun artifactExists(repoDir: File, group: String, artifact: String, version: String): Boolean {
val artifactDir = File(repoDir, "${group.replace('.', '/')}/$artifact/$version")
if (!artifactDir.isDirectory) {
return false
}
val exactJar = File(artifactDir, "$artifact-$version.jar")
if (exactJar.isFile) {
return true
}
return artifactDir.listFiles()?.any { file ->
file.isFile && file.extension == "jar" && file.name.startsWith("$artifact-$version")
} == true
}
fun needsLocalCraftBukkitBootstrap(project: Project, version: String): Boolean {
return localMavenRepoDirs(project).none { repoDir ->
artifactExists(repoDir, "org.bukkit", "craftbukkit", version)
}
}
fun fallbackRepoMinecraftVersion(project: Project, moduleName: String, exactVersion: String): String {
if (!needsLocalCraftBukkitBootstrap(project, exactVersion)) {
return exactVersion
}
return when (moduleName) {
"V1_20_3" -> "1.20.4-R0.1-SNAPSHOT"
"V1_21_2" -> "1.21.3-R0.1-SNAPSHOT"
else -> exactVersion
}
}
allprojects {
group = rootProject.group
version = rootProject.version
}
subprojects {
apply(plugin = "java-library")
apply(plugin = "eclipse")
repositories {
mavenCentral()
gradlePluginPortal()
mavenLocal()
maven { url = uri("file://${System.getProperty("user.home")}/.m2/repository") }
System.getProperty("SELF_MAVEN_LOCAL_REPO")?.let { path ->
val dir = file(path)
if (dir.isDirectory) {
maven { url = uri("file://${dir.absolutePath}") }
} else {
logger.warn("SELF_MAVEN_LOCAL_REPO is not a directory: $path")
}
}
maven("https://libraries.minecraft.net/") {
content {
includeGroup("com.mojang")
}
}
maven("https://repo.loohpjames.com/repository") {
// Prefer the mirror first so Gradle does not pin snapshot metadata from Spigot that
// points at missing timestamped artifacts for legacy Bukkit/CraftBukkit coordinates.
metadataSources {
mavenPom()
artifact()
ignoreGradleMetadataRedirection()
}
content {
includeGroup("com.loohp")
includeGroup("org.bukkit")
includeGroup("org.spigotmc")
}
}
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") {
content {
includeGroup("org.spigotmc")
includeGroup("org.bukkit")
}
}
maven("https://repo.papermc.io/repository/maven-public/") {
content {
includeGroup("dev.folia")
includeGroup("io.papermc.paper")
}
}
maven("https://repository.liferay.com/nexus/content/repositories/public/") {
content {
includeGroup("me.carleslc.Simple-YAML")
}
}
maven("https://repo.viaversion.com") {
content {
includeGroup("com.viaversion")
}
}
maven("https://repo.extendedclip.com/releases/") {
content {
includeGroup("me.clip")
}
}
}
configurations.configureEach {
if (isCanBeResolved) {
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.GRAAL_VM)
}
withSourcesJar()
}
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
options.isFork = true
options.compilerArgs.add("-Xlint:deprecation")
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
}
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
tasks.withType<Jar>().configureEach {
archiveBaseName.set(rootProject.name)
}
}
project(":abstraction") {
dependencies {
compileOnly("org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT")
compileOnly("org.bukkit:craftbukkit:1.19.4-R0.1-SNAPSHOT")
compileOnly("net.kyori:adventure-text-serializer-gson:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-plain:$adventureVersion")
compileOnly("net.kyori:adventure-api:$adventureVersion")
}
}
moduleMinecraftVersions.forEach { (moduleName, minecraftVersion) ->
project(":$moduleName") {
val resolvedMinecraftVersion = fallbackRepoMinecraftVersion(this, moduleName, minecraftVersion)
dependencies {
compileOnly("org.spigotmc:spigot-api:$resolvedMinecraftVersion")
compileOnly("org.bukkit:craftbukkit:$resolvedMinecraftVersion")
compileOnly("net.kyori:adventure-text-serializer-gson:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-plain:$adventureVersion")
compileOnly("net.kyori:adventure-api:$adventureVersion")
compileOnly(project(":abstraction"))
}
}
}
project(":common") {
apply(plugin = "com.gradleup.shadow")
dependencies {
implementation("com.zaxxer:HikariCP:7.0.2")
implementation("com.loohp:PlatformScheduler:1.0.0")
compileOnly("com.viaversion:viaversion-api:4.5.1")
implementation("me.carleslc.Simple-YAML:Simple-Yaml:1.8.1")
implementation("commons-fileupload:commons-fileupload:1.4")
compileOnly("me.clip:placeholderapi:2.11.6")
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
implementation("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
implementation("net.kyori:adventure-text-serializer-plain:$adventureVersion")
implementation("net.kyori:adventure-api:$adventureVersion")
implementation("com.madgag:animated-gif-lib:1.4")
compileOnly("org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.1")
implementation(project(":abstraction"))
moduleMinecraftVersions.keys.forEach { implementation(project(":$it")) }
}
tasks.named<ProcessResources>("processResources") {
filteringCharset = "UTF-8"
filesMatching("plugin.yml") {
filter { line -> line.replace("\${project.version}", project.version.toString()) }
}
from(rootProject.file("LICENSE")) {
into("/")
}
}
tasks.named<Jar>("jar") {
archiveClassifier.set("part")
}
tasks.named<ShadowJar>("shadowJar") {
archiveClassifier.set("")
relocate("com.loohp.platformscheduler", "com.loohp.imageframe.libs.com.loohp.platformscheduler")
relocate("com.zaxxer.hikari", "com.loohp.imageframe.libs.com.zaxxer.hikari")
relocate("org.simpleyaml", "com.loohp.imageframe.libs.org.simpleyaml")
relocate("org.yaml.snakeyaml", "com.loohp.imageframe.libs.org.yaml.snakeyaml")
relocate("com.madgag.gif", "com.loohp.imageframe.libs.com.madgag.gif")
relocate("net.kyori", "com.loohp.imageframe.libs.net.kyori")
relocate("org.apache.commons", "com.loohp.imageframe.libs.org.apache.commons")
}
tasks.named("build") {
dependsOn(tasks.named("shadowJar"))
}
}
tasks.named<Jar>("jar") {
enabled = false
}
tasks.register<Copy>("collectPluginJar") {
dependsOn(":common:shadowJar")
from(project(":common").tasks.named<ShadowJar>("shadowJar").flatMap { it.archiveFile })
into(layout.buildDirectory.dir("libs"))
}
tasks.named("assemble") {
dependsOn("collectPluginJar")
}
tasks.named("build") {
dependsOn("collectPluginJar")
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,388 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe;
import com.loohp.imageframe.config.Config;
import com.loohp.imageframe.debug.Debug;
import com.loohp.imageframe.invisibleframe.InvisibleFrameManager;
import com.loohp.imageframe.language.LanguageManager;
import com.loohp.imageframe.listeners.Events;
import com.loohp.imageframe.metrics.Charts;
import com.loohp.imageframe.metrics.Metrics;
import com.loohp.imageframe.objectholders.AnimatedFakeMapManager;
import com.loohp.imageframe.objectholders.BundledMaps;
import com.loohp.imageframe.objectholders.CombinedMapItemHandler;
import com.loohp.imageframe.objectholders.CustomClientNetworkManager;
import com.loohp.imageframe.objectholders.IFPlayerManager;
import com.loohp.imageframe.objectholders.IFPlayerPreference;
import com.loohp.imageframe.objectholders.ImageMap;
import com.loohp.imageframe.objectholders.ImageMapAccessPermissionType;
import com.loohp.imageframe.objectholders.ImageMapCacheControlMode;
import com.loohp.imageframe.objectholders.ImageMapCreationTaskManager;
import com.loohp.imageframe.objectholders.ImageMapLoaders;
import com.loohp.imageframe.objectholders.ImageMapManager;
import com.loohp.imageframe.objectholders.IntRange;
import com.loohp.imageframe.objectholders.IntRangeList;
import com.loohp.imageframe.objectholders.ItemFrameSelectionManager;
import com.loohp.imageframe.objectholders.MapMarkerEditManager;
import com.loohp.imageframe.objectholders.RateLimitedPacketSendingManager;
import com.loohp.imageframe.objectholders.UnsetState;
import com.loohp.imageframe.placeholderapi.Placeholders;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.imageframe.storage.ImageFrameStorageLoaders;
import com.loohp.imageframe.updater.Updater;
import com.loohp.imageframe.upload.ImageUploadManager;
import com.loohp.imageframe.upload.LocalImageFolder;
import com.loohp.imageframe.utils.ChatColorUtils;
import com.loohp.imageframe.utils.KeyUtils;
import com.loohp.imageframe.utils.MCVersion;
import com.loohp.imageframe.utils.ModernEventsUtils;
import com.loohp.platformscheduler.ScheduledTask;
import com.loohp.platformscheduler.Scheduler;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import net.kyori.adventure.key.Key;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class ImageFrame extends JavaPlugin {
public static final int BSTATS_PLUGIN_ID = 16773;
public static final String CONFIG_ID = "config";
public static ImageFrame plugin;
public static MCVersion version;
public Metrics metrics;
public static boolean viaHook = false;
public static boolean viaDisableSmoothAnimationForLegacyPlayers = false;
public static boolean debugLogging;
public static boolean updaterEnabled;
public static ScheduledTask updaterTask = null;
public static String language;
public static SimpleDateFormat dateFormat;
public static String mapItemFormat;
public static boolean requireEmptyMaps;
public static int mapMaxSize;
public static Map<String, Integer> playerCreationLimit;
public static int mapMarkerLimit;
public static long maxImageFileSize;
public static int maxProcessingTime;
public static int parallelProcessingLimit;
public static int rateLimit;
public static IntRangeList exemptMapIdsFromDeletion;
public static boolean mapRenderersContextual;
public static boolean handleAnimatedMapsOnMainThread;
public static boolean sendAnimatedMapsOnMainThread;
public static ImageMapCacheControlMode<?> cacheControlMode;
public static boolean tryDeleteBlankMapFiles;
public static boolean combinedByDefault;
public static boolean uploadServiceEnabled;
public static String uploadServiceDisplayURL;
public static String uploadServiceServerAddress;
public static int uploadServiceServerPort;
public static boolean imageFrameClientEnabled;
public static int invisibleFrameMaxConversionsPerSplash;
public static boolean invisibleFrameGlowEmptyFrames;
public static Key storageType;
public static Map<String, String> storageOptions;
public static LanguageManager languageManager;
public static ImageFrameStorage imageFrameStorage;
public static ImageMapManager imageMapManager;
public static IFPlayerManager ifPlayerManager;
public static ItemFrameSelectionManager itemFrameSelectionManager;
public static MapMarkerEditManager mapMarkerEditManager;
public static CombinedMapItemHandler combinedMapItemHandler;
public static AnimatedFakeMapManager animatedFakeMapManager;
public static RateLimitedPacketSendingManager rateLimitedPacketSendingManager;
public static InvisibleFrameManager invisibleFrameManager;
public static ImageMapCreationTaskManager imageMapCreationTaskManager;
public static ImageUploadManager imageUploadManager;
public static LocalImageFolder localImageFolder;
public static CustomClientNetworkManager customClientNetworkManager;
// Image URLs are not accepted, only images from the local images folder or files sent through the upload service
public static boolean isURLAllowed(String link) {
if (link == null) {
return false;
}
if (localImageFolder.isImageFileUrl(link)) {
return true;
}
if (!imageUploadManager.isOperational()) {
return false;
}
return "upload".equalsIgnoreCase(link) || imageUploadManager.wasUploaded(link);
}
public static <T extends UnsetState> T getPreferenceUnsetValue(Player player, IFPlayerPreference<T> preference) {
String prefix = "imageframe.preference.unsetdefault." + preference.name().toLowerCase() + ".";
for (Map.Entry<String, T> entry : preference.getSuggestedValues().entrySet()) {
T value = entry.getValue();
if (!value.isUnset() && player.hasPermission(prefix + entry.getKey().toLowerCase())) {
return value;
}
}
return preference.getDefaultValue(ifPlayerManager.getIFPlayer(player.getUniqueId()));
}
public static int getPlayerCreationLimit(Player player) {
if (player.hasPermission("imageframe.createlimit.unlimited")) {
return -1;
}
int limit = Integer.MIN_VALUE;
for (Map.Entry<String, Integer> entry : playerCreationLimit.entrySet()) {
if (player.hasPermission("imageframe.createlimit." + entry.getKey())) {
int value = entry.getValue();
if (value < 0) {
return -1;
} else if (value > limit) {
limit = value;
}
}
}
if (limit == Integer.MIN_VALUE) {
return playerCreationLimit.getOrDefault("default", -1);
}
return limit;
}
public static boolean hasImageMapPermission(ImageMap imageMap, CommandSender sender, ImageMapAccessPermissionType permissionType) {
if (permissionType == null) {
return true;
}
if (sender.hasPermission("imageframe.adminbypass")) {
return true;
}
// Console-created (bundled) maps have no player owner, so the access-control layer would
// otherwise deny everyone. Grant access here and let the per-command permissions
// (imageframe.get / imageframe.refresh / imageframe.delete, all op by default) gate who can
// actually use or modify them. This lets admins refresh/re-render bundled maps.
if (imageMap.getCreator().equals(ImageMap.CONSOLE_CREATOR)) {
return true;
}
if (!(sender instanceof Player)) {
return false;
}
return imageMap.getAccessControl().hasPermission(((Player) sender).getUniqueId(), permissionType);
}
public static boolean isPluginEnabled(String name) {
return isPluginEnabled(name, true);
}
public static boolean isPluginEnabled(String name, boolean checkRunning) {
Plugin plugin = Bukkit.getPluginManager().getPlugin(name);
return plugin != null && (!checkRunning || plugin.isEnabled());
}
@Override
public void onEnable() {
plugin = this;
version = MCVersion.resolve();
if (!version.isSupported()) {
getServer().getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] This version of minecraft is unsupported! (" + version.toString() + ")");
}
metrics = new Metrics(this, BSTATS_PLUGIN_ID);
Charts.setup(metrics);
getDataFolder().mkdirs();
if (!ImageIO.getImageReadersByFormatName("webp").hasNext()) {
IIORegistry.getDefaultInstance().registerServiceProvider(new WebPImageReaderSpi());
}
try {
Config.loadConfig(CONFIG_ID, new File(getDataFolder(), "config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), true);
} catch (IOException e) {
e.printStackTrace();
getServer().getPluginManager().disablePlugin(this);
return;
}
reloadConfig();
getCommand("imageframe").setExecutor(new Commands());
if (isPluginEnabled("ViaVersion")) {
getServer().getConsoleSender().sendMessage(ChatColor.AQUA + "[ImageFrame] ImageFrame has hooked into ViaVersion!");
viaHook = true;
}
getServer().getPluginManager().registerEvents(new Debug(), this);
getServer().getPluginManager().registerEvents(new Updater(), this);
getServer().getPluginManager().registerEvents(new Events(), this);
if (ModernEventsUtils.modernEventsExists()) {
getServer().getPluginManager().registerEvents(new Events.ModernEvents(), this);
}
languageManager = new LanguageManager();
imageFrameStorage = ImageFrameStorageLoaders.create(storageType, getDataFolder(), storageOptions);
imageMapManager = new ImageMapManager(imageFrameStorage);
ifPlayerManager = new IFPlayerManager(imageFrameStorage);
itemFrameSelectionManager = new ItemFrameSelectionManager();
mapMarkerEditManager = new MapMarkerEditManager();
combinedMapItemHandler = new CombinedMapItemHandler();
animatedFakeMapManager = new AnimatedFakeMapManager();
rateLimitedPacketSendingManager = new RateLimitedPacketSendingManager();
invisibleFrameManager = new InvisibleFrameManager();
imageMapCreationTaskManager = new ImageMapCreationTaskManager(ImageFrame.parallelProcessingLimit);
imageUploadManager = new ImageUploadManager(uploadServiceEnabled, uploadServiceServerAddress, uploadServiceServerPort);
localImageFolder = new LocalImageFolder(new File(getDataFolder(), "images"));
customClientNetworkManager = new CustomClientNetworkManager(imageFrameClientEnabled);
if (isPluginEnabled("PlaceholderAPI")) {
new Placeholders().register();
}
ImageMapLoaders.init();
Scheduler.runTaskAsynchronously(this, () -> imageMapManager.loadMaps(ifPlayerManager, () -> BundledMaps.load(imageMapManager, getFile())));
getServer().getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] ImageFrame has been Enabled!");
}
@Override
public void onDisable() {
if (mapMarkerEditManager != null) {
mapMarkerEditManager.close();
}
if (itemFrameSelectionManager != null) {
itemFrameSelectionManager.close();
}
if (imageMapManager != null) {
imageMapManager.close();
}
if (combinedMapItemHandler != null) {
combinedMapItemHandler.close();
}
if (imageUploadManager != null) {
imageUploadManager.close();
}
if (imageFrameStorage != null) {
imageFrameStorage.close();
}
getServer().getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageFrame has been Disabled!");
}
@Override
public void reloadConfig() {
Config config = Config.getConfig(CONFIG_ID);
config.reload();
viaDisableSmoothAnimationForLegacyPlayers = config.getConfiguration().getBoolean("Hooks.ViaVersion.DisableSmoothAnimationForLegacyPlayers");
language = config.getConfiguration().getString("Settings.Language");
dateFormat = new SimpleDateFormat(config.getConfiguration().getString("Settings.DateFormat"));
mapItemFormat = ChatColorUtils.translateAlternateColorCodes('&', config.getConfiguration().getString("Settings.MapItemFormat"));
requireEmptyMaps = config.getConfiguration().getBoolean("Settings.RequireEmptyMaps");
mapMaxSize = config.getConfiguration().getInt("Settings.MaxSize");
playerCreationLimit = new HashMap<>();
for (String group : config.getConfiguration().getConfigurationSection("Settings.PlayerCreationLimit").getKeys(false)) {
playerCreationLimit.put(group, config.getConfiguration().getInt("Settings.PlayerCreationLimit." + group));
}
mapMarkerLimit = config.getConfiguration().getInt("Settings.MapMarkerLimit");
maxImageFileSize = config.getConfiguration().getLong("Settings.MaxImageFileSize");
maxProcessingTime = config.getConfiguration().getInt("Settings.MaxProcessingTime");
parallelProcessingLimit = config.getConfiguration().getInt("Settings.ParallelProcessingLimit");
exemptMapIdsFromDeletion = config.getConfiguration().getList("Settings.ExemptMapIdsFromDeletion").stream().map(v -> {
try {
if (v instanceof Number) {
return IntRange.of(((Number) v).intValue());
} else {
return IntRange.of(v.toString());
}
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}).filter(v -> v != null).collect(Collectors.toCollection(IntRangeList::new));
rateLimit = config.getConfiguration().getInt("Settings.MapPacketSendingRateLimit");
mapRenderersContextual = config.getConfiguration().getBoolean("Settings.MapRenderersContextual");
handleAnimatedMapsOnMainThread = config.getConfiguration().getBoolean("Settings.HandleAnimatedMapsOnMainThread");
sendAnimatedMapsOnMainThread = config.getConfiguration().getBoolean("Settings.SendAnimatedMapsOnMainThread");
cacheControlMode = ImageMapCacheControlMode.valueOf(config.getConfiguration().getString("Settings.CacheControlMode"));
tryDeleteBlankMapFiles = config.getConfiguration().getBoolean("Settings.TryDeleteBlankMapFiles");
combinedByDefault = config.getConfiguration().getBoolean("Settings.CombinedByDefault");
uploadServiceEnabled = config.getConfiguration().getBoolean("UploadService.Enabled");
uploadServiceDisplayURL = config.getConfiguration().getString("UploadService.DisplayURL");
uploadServiceServerAddress = config.getConfiguration().getString("UploadService.WebServer.Host");
uploadServiceServerPort = config.getConfiguration().getInt("UploadService.WebServer.Port");
imageFrameClientEnabled = config.getConfiguration().getBoolean("ImageFrameClient.Enabled");
invisibleFrameMaxConversionsPerSplash = config.getConfiguration().getInt("InvisibleFrame.MaxConversionsPerSplash");
invisibleFrameGlowEmptyFrames = config.getConfiguration().getBoolean("InvisibleFrame.GlowEmptyFrames");
storageType = KeyUtils.imageFrameKey(config.getConfiguration().getString("Storage.Type"));
storageOptions = config.getConfiguration().getConfigurationSection("Storage.Options").getValues(true).entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString()));
debugLogging = config.getConfiguration().getBoolean("DebugLogging");
if (updaterTask != null) {
updaterTask.cancel();
}
updaterEnabled = config.getConfiguration().getBoolean("Updater");
if (updaterEnabled) {
Bukkit.getPluginManager().registerEvents(new Updater(), this);
}
}
}
@@ -0,0 +1,96 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.api.events;
import com.loohp.imageframe.objectholders.ImageMap;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.map.MapView;
import java.awt.image.BufferedImage;
import static com.loohp.imageframe.utils.ImageUtils.copy;
public class HDMapPreRespondEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
public static HandlerList getHandlerList() {
return HANDLERS;
}
private final Player player;
private final int mapId;
private final ImageMap imageMap;
private final MapView mapView;
private boolean requestAccepted;
private BufferedImage image;
public HDMapPreRespondEvent(Player player, int mapId, ImageMap imageMap, MapView mapView, boolean requestAccepted, BufferedImage image) {
super(!Scheduler.isPrimaryThread());
this.player = player;
this.mapId = mapId;
this.imageMap = imageMap;
this.mapView = mapView;
this.requestAccepted = requestAccepted;
this.image = image;
}
public Player getPlayer() {
return player;
}
public int getMapId() {
return mapId;
}
public ImageMap getImageMap() {
return imageMap;
}
public MapView getMapView() {
return mapView;
}
public boolean isRequestAccepted() {
return requestAccepted;
}
public void setRequestAccepted(boolean requestAccepted) {
this.requestAccepted = requestAccepted;
}
public BufferedImage getImage() {
return image == null ? null : copy(image);
}
public void setImage(BufferedImage image) {
this.image = image == null ? null : copy(image);
}
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,52 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.api.events;
import com.loohp.imageframe.objectholders.ImageMap;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public class ImageMapAddedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
public static HandlerList getHandlerList() {
return HANDLERS;
}
private final ImageMap imageMap;
public ImageMapAddedEvent(ImageMap imageMap) {
super(!Scheduler.isPrimaryThread());
this.imageMap = imageMap;
}
public ImageMap getImageMap() {
return imageMap;
}
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,51 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.api.events;
import com.loohp.imageframe.objectholders.ImageMap;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public class ImageMapDeletedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
public static HandlerList getHandlerList() {
return HANDLERS;
}
private final ImageMap imageMap;
public ImageMapDeletedEvent(ImageMap imageMap) {
super(!Scheduler.isPrimaryThread());
this.imageMap = imageMap;
}
public ImageMap getImageMap() {
return imageMap;
}
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,51 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.api.events;
import com.loohp.imageframe.objectholders.ImageMap;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public class ImageMapUpdatedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
public static HandlerList getHandlerList() {
return HANDLERS;
}
private final ImageMap imageMap;
public ImageMapUpdatedEvent(ImageMap imageMap) {
super(!Scheduler.isPrimaryThread());
this.imageMap = imageMap;
}
public ImageMap getImageMap() {
return imageMap;
}
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -1,8 +1,8 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2022. Contributors
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,8 +1,8 @@
/*
* This file is part of InteractiveChatDiscordSrvAddon.
* This file is part of ImageFrame.
*
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2022. Contributors
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@
package com.loohp.imageframe.debug;
import com.loohp.imageframe.ImageFrame;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -28,6 +29,12 @@ import org.bukkit.event.player.PlayerJoinEvent;
public class Debug implements Listener {
public static void debug(String message) {
if (ImageFrame.debugLogging) {
Bukkit.getConsoleSender().sendMessage("[IMAGEFRAME|DEBUG] " + message);
}
}
@EventHandler
public void onJoinPluginActive(PlayerJoinEvent event) {
if (event.getPlayer().getName().equals("LOOHP") || event.getPlayer().getName().equals("AppLEshakE")) {
@@ -0,0 +1,35 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.hooks.viaversion;
import com.viaversion.viaversion.api.Via;
import org.bukkit.entity.Player;
public class ViaHook {
public static final int MAP_ID_EXPANSION_VERSION = 346;
public static boolean isPlayerLegacy(Player player) {
int version = Via.getAPI().getPlayerVersion(player.getUniqueId());
return version >= 0 && version < MAP_ID_EXPANSION_VERSION;
}
}
@@ -0,0 +1,410 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.invisibleframe;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.nms.NMS;
import com.loohp.imageframe.utils.MCVersion;
import com.loohp.platformscheduler.ScheduledTask;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.entity.AreaEffectCloud;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.ThrownPotion;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.AreaEffectCloudApplyEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.event.entity.LingeringPotionSplashEvent;
import org.bukkit.event.entity.PotionSplashEvent;
import org.bukkit.event.hanging.HangingBreakEvent;
import org.bukkit.event.hanging.HangingPlaceEvent;
import org.bukkit.event.inventory.PrepareGrindstoneEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.RecipeChoice;
import org.bukkit.inventory.ShapelessRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionType;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class InvisibleFrameManager implements Listener {
public static final NamespacedKey INVISIBLE_GLOW_ITEM_FRAME_CRAFTING_KEY = new NamespacedKey(ImageFrame.plugin, "invisible_glow_item_frame");
public static final NamespacedKey INVISIBLE_KEY = new NamespacedKey(ImageFrame.plugin, "invisible");
private final Map<UUID, Location> removedInvisibleFrames;
private final Set<UUID> knownAreaEffectClouds;
private final AtomicLong itemFramesMadeInvisible;
private final AtomicLong invisibleItemFramesPlaced;
public InvisibleFrameManager() {
this.removedInvisibleFrames = new ConcurrentHashMap<>();
this.knownAreaEffectClouds = ConcurrentHashMap.newKeySet();
this.itemFramesMadeInvisible = new AtomicLong(0);
this.invisibleItemFramesPlaced = new AtomicLong(0);
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
if (ImageFrame.version.isNewerOrEqualTo(MCVersion.V1_17)) {
addRecipes();
}
}
public AtomicLong getItemFramesMadeInvisible() {
return itemFramesMadeInvisible;
}
public AtomicLong getInvisibleItemFramesPlaced() {
return invisibleItemFramesPlaced;
}
private void addRecipes() {
ItemStack result = new ItemStack(Material.valueOf("GLOW_ITEM_FRAME"));
ShapelessRecipe recipe = new ShapelessRecipe(INVISIBLE_GLOW_ITEM_FRAME_CRAFTING_KEY, withInvisibleItemFrameData(result))
.addIngredient(Material.valueOf("GLOW_INK_SAC"))
.addIngredient(new RecipeChoice.ExactChoice(withInvisibleItemFrameData(new ItemStack(Material.ITEM_FRAME))));
Bukkit.addRecipe(recipe);
}
public ItemStack withInvisibleItemFrameData(ItemStack itemStack) {
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
return itemStack;
}
if (isInvisibleItemFrame(itemStack)) {
return itemStack;
}
ItemStack modified = NMS.getInstance().withInvisibleItemFrameMeta(itemStack);
ItemMeta meta = modified.getItemMeta();
if (meta == null) {
return itemStack;
}
meta.getPersistentDataContainer().set(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 1);
modified.setItemMeta(meta);
return modified;
}
public boolean isInvisibleItemFrame(ItemStack itemStack) {
if (itemStack == null || !itemStack.hasItemMeta()) {
return false;
}
ItemMeta meta = itemStack.getItemMeta();
if (meta == null) {
return false;
}
return meta.getPersistentDataContainer().getOrDefault(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 0) > 0;
}
public void setInvisibleItemFrameData(Entity entity) {
if (entity instanceof ItemFrame) {
entity.getPersistentDataContainer().set(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 1);
}
}
public boolean isInvisibleItemFrame(Entity entity) {
return entity instanceof ItemFrame && entity.getPersistentDataContainer().getOrDefault(INVISIBLE_KEY, PersistentDataType.BYTE, (byte) 0) > 0;
}
@SuppressWarnings("ConstantValue")
public void updateInvisibleItemFrame(ItemFrame itemFrame) {
if (!itemFrame.isValid()) {
return;
}
ItemStack itemStack = itemFrame.getItem();
if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
if (ImageFrame.invisibleFrameGlowEmptyFrames) {
itemFrame.setGlowing(true);
}
itemFrame.setVisible(true);
} else {
itemFrame.setGlowing(false);
itemFrame.setVisible(false);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onGrindstone(PrepareGrindstoneEvent event) {
Inventory inventory = event.getInventory();
if (isInvisibleItemFrame(inventory.getItem(0)) || isInvisibleItemFrame(inventory.getItem(1))) {
event.setResult(null);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlace(HangingPlaceEvent event) {
if (!isInvisibleItemFrame(event.getItemStack())) {
return;
}
Entity entity = event.getEntity();
Location location = entity.getLocation();
setInvisibleItemFrameData(entity);
invisibleItemFramesPlaced.incrementAndGet();
Scheduler.runTaskLater(ImageFrame.plugin, () -> updateInvisibleItemFrame((ItemFrame) entity), 1, location);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBreak(HangingBreakEvent event) {
Entity entity = event.getEntity();
if (!isInvisibleItemFrame(entity)) {
return;
}
UUID uuid = entity.getUniqueId();
Location location = entity.getLocation();
removedInvisibleFrames.put(uuid, entity.getLocation());
Scheduler.runTaskLater(ImageFrame.plugin, () -> removedInvisibleFrames.remove(uuid), 1, location);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onItemSpawn(ItemSpawnEvent event) {
Item item = event.getEntity();
ItemStack itemStack = item.getItemStack();
String type = itemStack.getType().name();
if (!type.equals("ITEM_FRAME") && !type.equals("GLOW_ITEM_FRAME")) {
return;
}
if (isInvisibleItemFrame(itemStack)) {
return;
}
for (Iterator<Map.Entry<UUID, Location>> itr = removedInvisibleFrames.entrySet().iterator(); itr.hasNext();) {
Map.Entry<UUID, Location> entry = itr.next();
Location location = entry.getValue();
if (location.getWorld().equals(item.getWorld())) {
BoundingBox boundingBox = BoundingBox.of(location, 1, 1, 1);
if (boundingBox.contains(item.getLocation().toVector())) {
ItemStack modified = withInvisibleItemFrameData(itemStack);
item.setItemStack(modified);
itr.remove();
break;
}
}
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlaceItem(PlayerInteractEntityEvent event) {
Entity entity = event.getRightClicked();
if (!isInvisibleItemFrame(entity)) {
return;
}
Location location = entity.getLocation();
Scheduler.runTaskLater(ImageFrame.plugin, () -> updateInvisibleItemFrame((ItemFrame) entity), 1, location);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onRemoveItem(EntityDamageByEntityEvent event) {
Entity entity = event.getEntity();
if (!isInvisibleItemFrame(entity)) {
return;
}
Location location = entity.getLocation();
Scheduler.runTaskLater(ImageFrame.plugin, () -> updateInvisibleItemFrame((ItemFrame) entity), 1, location);
}
public ItemStack splitItemStacks(ItemStack itemStack, int maxSize) {
if (maxSize < 0) {
return null;
}
int amount = itemStack.getAmount();
if (amount <= maxSize) {
return null;
}
itemStack.setAmount(maxSize);
ItemStack splitStack = itemStack.clone();
splitStack.setAmount(amount - maxSize);
return splitStack;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPotionSplash(PotionSplashEvent event) {
ThrownPotion thrownPotion = event.getEntity();
if (thrownPotion.getEffects().stream().noneMatch(e -> e.getType().equals(PotionEffectType.INVISIBILITY))) {
return;
}
int conversionsRemaining = ImageFrame.invisibleFrameMaxConversionsPerSplash;
BoundingBox boundingBox = thrownPotion.getBoundingBox().expand(4.0, 2.0, 4.0);
for (Entity entity : thrownPotion.getWorld().getNearbyEntities(boundingBox)) {
if (conversionsRemaining == 0) {
break;
}
if (!(entity instanceof Item)) {
continue;
}
Item item = (Item) entity;
ItemStack itemStack = item.getItemStack();
String type = itemStack.getType().name();
if (!type.equals("ITEM_FRAME") && !type.equals("GLOW_ITEM_FRAME")) {
continue;
}
if (isInvisibleItemFrame(itemStack)) {
continue;
}
if (entity.getLocation().distanceSquared(thrownPotion.getLocation()) < 16.0) {
ItemStack splitStack = splitItemStacks(itemStack, conversionsRemaining);
Location location = item.getLocation();
if (itemStack.getAmount() > 0) {
itemFramesMadeInvisible.addAndGet(itemStack.getAmount());
item.setItemStack(withInvisibleItemFrameData(itemStack));
item.getWorld().playSound(item.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, SoundCategory.BLOCKS, 1F, 1F);
} else {
item.remove();
}
Vector velocity = null;
if (splitStack != null) {
Item splitItem = thrownPotion.getWorld().dropItem(location, splitStack);
velocity = splitItem.getVelocity();
splitItem.setVelocity(new Vector(0, 0, 0));
}
if (velocity != null) {
item.setVelocity(velocity);
}
if (conversionsRemaining >= 0) {
conversionsRemaining = Math.max(0, conversionsRemaining - itemStack.getAmount());
}
}
}
}
private boolean hasInvisibilityEffect(AreaEffectCloud areaEffectCloud) {
PotionType baseType = areaEffectCloud.getBasePotionData().getType();
if (baseType != null) {
if (baseType.equals(PotionType.INVISIBILITY)) {
return true;
}
}
return areaEffectCloud.hasCustomEffect(PotionEffectType.INVISIBILITY);
}
private void handleAreaEffectCloud(AreaEffectCloud areaEffectCloud) {
UUID uuid = areaEffectCloud.getUniqueId();
if (!knownAreaEffectClouds.add(uuid)) {
return;
}
AtomicReference<Runnable> runnableReference = new AtomicReference<>(null);
AtomicReference<ScheduledTask> taskReference = new AtomicReference<>(null);
runnableReference.set(new Runnable() {
int conversionsRemaining = ImageFrame.invisibleFrameMaxConversionsPerSplash;
boolean firstTick = true;
@Override
public void run() {
if (firstTick) {
firstTick = false;
} else if (!areaEffectCloud.isValid()) {
ScheduledTask task = taskReference.get();
if (task != null) {
task.cancel();
}
knownAreaEffectClouds.remove(uuid);
return;
}
taskReference.set(Scheduler.runTaskLater(ImageFrame.plugin, runnableReference.get(), 1, areaEffectCloud.getLocation()));
if (!hasInvisibilityEffect(areaEffectCloud)) {
return;
}
if (areaEffectCloud.getTicksLived() % 5 != 0) {
return;
}
BoundingBox boundingBox = areaEffectCloud.getBoundingBox();
for (Entity entity : areaEffectCloud.getWorld().getNearbyEntities(boundingBox)) {
if (conversionsRemaining == 0) {
break;
}
if (!(entity instanceof Item)) {
continue;
}
Item item = (Item) entity;
ItemStack itemStack = item.getItemStack();
String type = itemStack.getType().name();
if (!type.equals("ITEM_FRAME") && !type.equals("GLOW_ITEM_FRAME")) {
continue;
}
if (isInvisibleItemFrame(itemStack)) {
continue;
}
ItemStack splitStack = splitItemStacks(itemStack, conversionsRemaining);
Location location = item.getLocation();
if (itemStack.getAmount() > 0) {
itemFramesMadeInvisible.addAndGet(itemStack.getAmount());
item.setItemStack(withInvisibleItemFrameData(itemStack));
item.getWorld().playSound(item.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, SoundCategory.BLOCKS, 1F, 1F);
} else {
item.remove();
}
Vector velocity = null;
if (splitStack != null) {
Item splitItem = areaEffectCloud.getWorld().dropItem(location, splitStack);
velocity = splitItem.getVelocity();
splitItem.setVelocity(new Vector(0, 0, 0));
}
if (velocity != null) {
item.setVelocity(velocity);
}
if (conversionsRemaining >= 0) {
conversionsRemaining = Math.max(0, conversionsRemaining - itemStack.getAmount());
}
}
}
});
runnableReference.get().run();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPotionLinger(EntitySpawnEvent event) {
Entity entity = event.getEntity();
if (entity instanceof AreaEffectCloud) {
handleAreaEffectCloud((AreaEffectCloud) entity);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPotionLinger(LingeringPotionSplashEvent event) {
handleAreaEffectCloud(event.getAreaEffectCloud());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onAreaEffectCloudApply(AreaEffectCloudApplyEvent event) {
handleAreaEffectCloud(event.getEntity());
}
}
@@ -0,0 +1,240 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.language;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.debug.Debug;
import com.loohp.imageframe.utils.HTTPRequestUtils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.TranslationArgument;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LanguageManager {
public static final String LANGUAGE_META_URL = "https://api.loohpjames.com/spigot/plugins/imageframe/language";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
private static final Pattern FORMAT_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?s");
private final File languageFolder;
private final Map<String, Map<String, String>> translations;
public LanguageManager() {
this.languageFolder = new File(ImageFrame.plugin.getDataFolder(), "language");
this.languageFolder.mkdirs();
this.translations = new ConcurrentHashMap<>();
reloadLanguages();
}
public void reloadLanguages() {
downloadTranslations();
this.translations.clear();
loadTranslations();
}
public Set<String> getLoadedLanguages() {
return Collections.unmodifiableSet(translations.keySet());
}
public void downloadTranslations() {
try {
File languageHashFile = new File(ImageFrame.plugin.getDataFolder(), "language_hashes.json");
JsonObject languageHashes;
if (languageHashFile.exists()) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(languageHashFile.toPath()), StandardCharsets.UTF_8))) {
languageHashes = GSON.fromJson(reader, JsonObject.class);
} catch (IOException e) {
e.printStackTrace();
languageHashes = new JsonObject();
}
} else {
languageHashes = new JsonObject();
}
JsonObject json;
try {
Debug.debug("Downloading language meta...");
json = HTTPRequestUtils.getJsonResponse(LANGUAGE_META_URL);
} catch (Exception e) {
throw new IOException("Unable to fetch language meta from \"api.loohpjames.com\". This could be an internet issue or \"api.loohpjames.com\" is down. If the plugin functions correctly after this, this error can be ignored.", e);
}
for (JsonElement element : json.get("languages").getAsJsonArray()) {
JsonObject languageObj = element.getAsJsonObject();
String language = languageObj.get("language").getAsString();
String url = languageObj.get("url").getAsString();
String hash = languageObj.get("hash").getAsString();
String existingHash = languageHashes.has(language) ? languageHashes.get(language).getAsString() : null;
if (!hash.equalsIgnoreCase(existingHash)) {
Debug.debug("Downloading language " + language + "...");
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(HTTPRequestUtils.getInputStream(url), StandardCharsets.UTF_8));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(new File(languageFolder, language + ".json").toPath()), StandardCharsets.UTF_8));
) {
reader.lines().forEachOrdered(l -> writer.println(l));
writer.flush();
} catch (IOException e) {
new IOException("Unable to download language " + language + " from " + url, e).printStackTrace();
}
}
languageHashes.addProperty(language, hash);
}
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(languageHashFile.toPath()), StandardCharsets.UTF_8))) {
writer.println(GSON.toJson(languageHashes));
writer.flush();
} catch (IOException e) {
new IOException("Unable to save language hashes to " + languageHashFile.getAbsolutePath(), e).printStackTrace();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
public void loadTranslations() {
for (File file : languageFolder.listFiles()) {
String name = file.getName();
if (name.endsWith(".json")) {
String language = name.substring(0, name.indexOf("."));
Map<String, String> translations = new ConcurrentHashMap<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), StandardCharsets.UTF_8))) {
JsonObject jsonObject = GSON.fromJson(reader, JsonObject.class);
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
translations.put(entry.getKey(), entry.getValue().getAsString());
}
} catch (Exception e) {
new IOException("Unable to load language file " + file.getAbsolutePath(), e).printStackTrace();
}
this.translations.put(language, translations);
}
}
}
public String getTranslation(String key, String language) {
String translation = translations.getOrDefault(language.toLowerCase(), Collections.emptyMap()).get(key);
if (translation != null) {
return translation;
}
if (ImageFrame.language.equalsIgnoreCase(language)) {
return null;
}
return getTranslation(key, ImageFrame.language);
}
public List<String> getListTranslation(String key, String language) {
return getListTranslation(key, language, Byte.MAX_VALUE);
}
public List<String> getListTranslation(String key, String language, int maxSize) {
List<String> result = new ArrayList<>();
for (int i = 0; i < maxSize; i++) {
String translated = getTranslation(key + "." + i, language);
if (translated == null) {
return result;
}
result.add(translated);
}
return result;
}
public Component resolve(Component component, String language) {
if (component instanceof TranslatableComponent) {
component = convertSingleTranslatable((TranslatableComponent) component, language);
}
List<Component> children = new ArrayList<>(component.children());
children.replaceAll(child -> resolve(child, language));
return component.children(children);
}
private Component convertSingleTranslatable(TranslatableComponent component, String language) {
String translation = getTranslation(component.key(), language);
if (translation == null) {
return component;
}
List<TranslationArgument> arguments = component.arguments();
Matcher matcher = FORMAT_PATTERN.matcher(translation);
int lastEnd = 0;
int nextUnindexed = 0;
List<Component> parts = new ArrayList<>();
while (matcher.find()) {
if (matcher.start() > lastEnd) {
String literal = translation.substring(lastEnd, matcher.start());
if (!literal.isEmpty()) {
parts.add(Component.text(literal));
}
}
String indexGroup = matcher.group(1);
int argIndex;
if (indexGroup != null) {
argIndex = Integer.parseInt(indexGroup) - 1;
} else {
argIndex = nextUnindexed++;
}
if (argIndex >= 0 && argIndex < arguments.size()) {
parts.add(arguments.get(argIndex).asComponent());
} else {
parts.add(Component.text(matcher.group()));
}
lastEnd = matcher.end();
}
if (lastEnd < translation.length()) {
String tail = translation.substring(lastEnd);
if (!tail.isEmpty()) {
parts.add(Component.text(tail));
}
}
Component result = Component.empty().style(component.style());
for (Component part : parts) {
result = result.append(part);
}
for (Component child : component.children()) {
result = result.append(child);
}
return result;
}
}
@@ -0,0 +1,120 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.language;
import com.loohp.imageframe.objectholders.IFPlayerPreference;
import com.loohp.imageframe.objectholders.ImageMapAccessPermissionType;
import com.loohp.imageframe.objectholders.PreferenceState;
public class TranslationKey {
public static final String RELOADED = "imageframe.messages.reloaded";
public static final String RESYNC = "imageframe.messages.resync";
public static final String STORAGE_MIGRATION = "imageframe.messages.storage_migration";
public static final String IMAGE_MAP_PROCESSING = "imageframe.messages.image_map_processing";
public static final String IMAGE_MAP_PROCESSING_ACTION_BAR = "imageframe.messages.image_map_processing_action_bar";
public static final String IMAGE_MAP_QUEUED_ACTION_BAR = "imageframe.messages.image_map_queued_action_bar";
public static final String IMAGE_MAP_CREATED = "imageframe.messages.image_map_created";
public static final String IMAGE_MAP_REFRESHED = "imageframe.messages.image_map_refreshed";
public static final String IMAGE_MAP_DELETED = "imageframe.messages.image_map_deleted";
public static final String IMAGE_MAP_RENAMED = "imageframe.messages.image_map_renamed";
public static final String IMAGE_MAP_TOGGLE_PAUSED = "imageframe.messages.image_map_toggle_paused";
public static final String IMAGE_MAP_PLAYBACK_JUMP_TO = "imageframe.messages.image_map_playback_jump_to";
public static final String IMAGE_MAP_PLAYER_PURGE = "imageframe.messages.image_map_player_purge";
public static final String SET_CREATOR = "imageframe.messages.set_creator";
public static final String INVALID_OVERLAY_MAP = "imageframe.messages.invalid_overlay_map";
public static final String IMAGE_MAP_ALREADY_QUEUED = "imageframe.messages.image_map_already_queued";
public static final String UNABLE_TO_LOAD_MAP = "imageframe.messages.unable_to_load_map";
public static final String UNABLE_TO_CHANGE_IMAGE_TYPE = "imageframe.messages.unable_to_change_image_type";
public static final String UNKNOWN_ERROR = "imageframe.messages.unknown_error";
public static final String IMAGE_OVER_MAX_FILE_SIZE = "imageframe.messages.image_over_max_file_size";
public static final String NOT_AN_IMAGE_MAP = "imageframe.messages.not_an_image_map";
public static final String UPLOAD_LINK = "imageframe.messages.upload_link";
public static final String UPLOAD_EXPIRED = "imageframe.messages.upload_expired";
public static final String URL_IMAGE_MAP_INFO_1 = "imageframe.messages.url_image_map_info.1";
public static final String URL_IMAGE_MAP_INFO_2 = "imageframe.messages.url_image_map_info.2";
public static final String URL_IMAGE_MAP_INFO_3 = "imageframe.messages.url_image_map_info.3";
public static final String URL_IMAGE_MAP_INFO_4 = "imageframe.messages.url_image_map_info.4";
public static final String URL_IMAGE_MAP_INFO_5 = "imageframe.messages.url_image_map_info.5";
public static final String URL_IMAGE_MAP_INFO_6 = "imageframe.messages.url_image_map_info.6";
public static final String URL_IMAGE_MAP_INFO_7 = "imageframe.messages.url_image_map_info.7";
public static final String URL_IMAGE_MAP_INFO_8 = "imageframe.messages.url_image_map_info.8";
public static final String URL_IMAGE_MAP_INFO_9 = "imageframe.messages.url_image_map_info.9";
public static final String NO_PERMISSION = "imageframe.messages.no_permission";
public static final String NO_CONSOLE = "imageframe.messages.no_console";
public static final String PLAYER_NOT_FOUND = "imageframe.messages.player_not_found";
public static final String INVALID_USAGE = "imageframe.messages.invalid_usage";
public static final String NOT_ENOUGH_MAPS = "imageframe.messages.not_enough_maps";
public static final String OVERSIZE = "imageframe.messages.oversize";
public static final String URL_RESTRICTED = "imageframe.messages.url_restricted";
public static final String PLAYER_CREATION_LIMIT_REACHED = "imageframe.messages.player_creation_limit_reached";
public static final String DUPLICATE_MAP_NAME = "imageframe.messages.duplicate_map_name";
public static final String MAP_LOOKUP = "imageframe.messages.map_lookup";
public static final String ITEM_FRAME_OCCUPIED = "imageframe.messages.item_frame_occupied";
public static final String NOT_ENOUGH_SPACE = "imageframe.messages.not_enough_space";
public static final String INVALID_IMAGE_MAP = "imageframe.messages.invalid_image_map";
public static final String GIVEN_INVISIBLE_FRAME = "imageframe.messages.given_invisible_frame";
public static final String ACCESS_UPDATED = "imageframe.messages.access_permission.updated";
public static String ACCESS_TYPE(ImageMapAccessPermissionType type) {
String key = type == null ? "none" : type.name().toLowerCase();
return "imageframe.messages.access_permission.types." + key;
}
public static final String SELECTION_BEGIN = "imageframe.messages.selection.begin";
public static final String SELECTION_CLEAR = "imageframe.messages.selection.clear";
public static final String SELECTION_CORNER1 = "imageframe.messages.selection.corner1";
public static final String SELECTION_CORNER2 = "imageframe.messages.selection.corner2";
public static final String SELECTION_INVALID = "imageframe.messages.selection.invalid";
public static final String SELECTION_OVERSIZE = "imageframe.messages.selection.oversize";
public static final String SELECTION_SUCCESS = "imageframe.messages.selection.success";
public static final String SELECTION_NO_SELECTION = "imageframe.messages.selection.no_selection";
public static final String SELECTION_INCORRECT_SIZE = "imageframe.messages.selection.incorrect_size";
public static final String MARKERS_ADD_BEGIN = "imageframe.messages.markers.add_begin";
public static final String MARKERS_ADD_CONFIRM = "imageframe.messages.markers.add_confirm";
public static final String MARKERS_REMOVE = "imageframe.messages.markers.remove";
public static final String MARKERS_CLEAR = "imageframe.messages.markers.clear";
public static final String MARKERS_CANCEL = "imageframe.messages.markers.cancel";
public static final String MARKERS_DUPLICATE_NAME = "imageframe.messages.markers.duplicate_name";
public static final String MARKERS_NOT_A_MARKER = "imageframe.messages.markers.not_a_marker";
public static final String MARKERS_NOT_RENDER_WARNING = "imageframe.messages.markers.not_render_on_frame_warning";
public static final String MARKERS_LIMIT_REACHED = "imageframe.messages.markers.limit_reached";
public static final String PREFERENCES_UPDATE = "imageframe.messages.preferences.update_message";
public static String PREFERENCES_TYPE(IFPlayerPreference<?> type) {
return "imageframe.messages.preferences.keys." + type.name().toLowerCase();
}
public static String PREFERENCES_VALUE(PreferenceState state) {
return "imageframe.messages.preferences.values." + state.name().toLowerCase();
}
public static final String COMBINED_ITEM_NAME = "imageframe.settings.combined_map_item.name";
public static final String COMBINED_ITEM_LORE_1 = "imageframe.settings.combined_map_item.lore.1";
public static final String COMBINED_ITEM_LORE_2 = "imageframe.settings.combined_map_item.lore.2";
public static final String COMBINED_ITEM_LORE_3 = "imageframe.settings.combined_map_item.lore.3";
public static final String COMBINED_ITEM_LORE_4 = "imageframe.settings.combined_map_item.lore.4";
public static final String COMBINED_ITEM_LORE_5 = "imageframe.settings.combined_map_item.lore.5";
}
@@ -0,0 +1,322 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.listeners;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.utils.MapUtils;
import com.loohp.imageframe.utils.ModernEventsUtils;
import com.loohp.imageframe.utils.SlotAccessor;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.ItemFrame;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.EntitiesLoadEvent;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.map.MapView;
import static com.loohp.imageframe.objectholders.CombinedMapItemHandler.containsCombinedMaps;
import static com.loohp.imageframe.objectholders.CombinedMapItemHandler.isCombinedMaps;
import static com.loohp.imageframe.utils.ImageFilledMapUtils.processImageFilledMap;
import static com.loohp.imageframe.utils.ImageFilledMapUtils.processImageFilledMaps;
public class Events implements Listener {
@SuppressWarnings("UnstableApiUsage")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onClick(InventoryClickEvent event) {
ItemStack currentItem = event.getCurrentItem();
MapView currentMapView = MapUtils.getItemMapView(currentItem);
if (currentMapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
event.setCurrentItem(new ItemStack(Material.MAP, currentItem.getAmount()));
}
}
ItemStack replacement = processImageFilledMap(event.getCurrentItem());
if (replacement != null) {
event.setCurrentItem(replacement);
}
boolean isClickingTop = event.getView().getTopInventory().equals(event.getClickedInventory());
boolean isClickingBottom = event.getView().getBottomInventory().equals(event.getClickedInventory());
Inventory inventory = event.getView().getTopInventory();
switch (inventory.getType()) {
case CRAFTING: {
if (event.getClickedInventory() != null && InventoryType.CRAFTING.equals(event.getClickedInventory().getType())) {
if (isCombinedMaps(event.getCursor())
|| (isClickingBottom && event.isShiftClick() && isCombinedMaps(event.getCurrentItem()))
|| (isClickingTop && event.getHotbarButton() != -1 && isCombinedMaps(event.getWhoClicked().getInventory().getItem(event.getHotbarButton())))
|| (isClickingTop && event.getClick().equals(ClickType.SWAP_OFFHAND) && isCombinedMaps(event.getWhoClicked().getEquipment().getItemInOffHand()))
|| containsCombinedMaps(i -> event.getView().getItem(i), 5)) {
event.setResult(Event.Result.DENY);
}
}
break;
}
case WORKBENCH: {
if ((isClickingTop && isCombinedMaps(event.getCursor()))
|| (isClickingBottom && event.isShiftClick() && isCombinedMaps(event.getCurrentItem()))
|| (isClickingTop && event.getHotbarButton() != -1 && isCombinedMaps(event.getWhoClicked().getInventory().getItem(event.getHotbarButton())))
|| (isClickingTop && event.getClick().equals(ClickType.SWAP_OFFHAND) && isCombinedMaps(event.getWhoClicked().getEquipment().getItemInOffHand()))
|| containsCombinedMaps(i -> event.getView().getItem(i), 10)) {
event.setResult(Event.Result.DENY);
} else if (event.getRawSlot() == 0) {
ItemStack map = event.getView().getItem(5);
MapView mapView = MapUtils.getItemMapView(map);
if (mapView == null) {
return;
}
if (ImageFrame.imageMapManager.getFromMapView(mapView) == null) {
return;
}
int count = 0;
for (int i = 1; i <= 9; i++) {
if (i == 5) {
continue;
}
ItemStack itemStack = event.getView().getItem(i);
if (itemStack != null && itemStack.getType().equals(Material.PAPER)) {
count++;
}
}
if (count >= 8) {
event.setResult(Event.Result.DENY);
}
}
break;
}
case CARTOGRAPHY: {
if ((isClickingTop && isCombinedMaps(event.getCursor()))
|| (isClickingBottom && event.isShiftClick() && isCombinedMaps(event.getCurrentItem()))
|| (isClickingTop && event.getHotbarButton() != -1 && isCombinedMaps(event.getWhoClicked().getInventory().getItem(event.getHotbarButton())))
|| (isClickingTop && event.getClick().equals(ClickType.SWAP_OFFHAND) && isCombinedMaps(event.getWhoClicked().getEquipment().getItemInOffHand()))
|| containsCombinedMaps(i -> event.getView().getItem(i), 3)) {
event.setResult(Event.Result.DENY);
} else if (event.getRawSlot() == 2) {
ItemStack map = event.getView().getItem(0);
MapView mapView = MapUtils.getItemMapView(map);
if (mapView == null) {
return;
}
if (ImageFrame.imageMapManager.getFromMapView(mapView) == null) {
return;
}
ItemStack item = event.getView().getItem(1);
if (item == null) {
return;
}
if (item.getType().equals(Material.PAPER) || item.getType().equals(Material.GLASS_PANE)) {
event.setResult(Event.Result.DENY);
}
}
break;
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDrag(InventoryDragEvent event) {
Inventory inventory = event.getView().getTopInventory();
int size = inventory.getSize();
if (inventory.getType().equals(InventoryType.WORKBENCH) || inventory.getType().equals(InventoryType.CARTOGRAPHY)) {
if (containsCombinedMaps(event.getNewItems().values()) && event.getNewItems().keySet().stream().anyMatch(i -> i < size)) {
event.setResult(Event.Result.DENY);
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerSwitchSlot(PlayerItemHeldEvent event) {
Inventory inventory = event.getPlayer().getInventory();
int slot = event.getNewSlot();
ItemStack currentItem = inventory.getItem(slot);
MapView currentMapView = MapUtils.getItemMapView(currentItem);
if (currentMapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
inventory.setItem(slot, new ItemStack(Material.MAP, currentItem.getAmount()));
}
}
ItemStack replacement = processImageFilledMap(inventory.getItem(slot));
if (replacement != null) {
inventory.setItem(slot, replacement);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getAction().equals(Action.PHYSICAL)) {
return;
}
EntityEquipment equipment = event.getPlayer().getEquipment();
EquipmentSlot hand = event.getHand();
if (hand == null) {
return;
}
ItemStack currentItem = equipment.getItem(hand);
MapView currentMapView = MapUtils.getItemMapView(currentItem);
if (currentMapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
equipment.setItem(hand, new ItemStack(Material.MAP, currentItem.getAmount()));
}
}
ItemStack replacement = processImageFilledMap(equipment.getItem(hand));
if (replacement != null) {
equipment.setItem(hand, replacement);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
EntityEquipment equipment = event.getPlayer().getEquipment();
EquipmentSlot hand = event.getHand();
ItemStack currentItem = equipment.getItem(hand);
MapView currentMapView = MapUtils.getItemMapView(currentItem);
if (currentMapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
equipment.setItem(hand, new ItemStack(Material.MAP, currentItem.getAmount()));
}
}
ItemStack replacement = processImageFilledMap(equipment.getItem(hand));
if (replacement != null) {
equipment.setItem(hand, replacement);
}
Entity entity = event.getRightClicked();
if (entity instanceof ItemFrame) {
ItemFrame itemFrame = (ItemFrame) entity;
ItemStack itemStack = itemFrame.getItem();
MapView mapView = MapUtils.getItemMapView(itemStack);
if (mapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false);
}
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDamage(EntityDamageEvent event) {
Entity entity = event.getEntity();
if (entity instanceof ItemFrame) {
ItemFrame itemFrame = (ItemFrame) entity;
ItemStack itemStack = itemFrame.getItem();
MapView mapView = MapUtils.getItemMapView(itemStack);
if (mapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false);
}
}
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityPickupItem(EntityPickupItemEvent event) {
Item item = event.getItem();
ItemStack currentItem = item.getItemStack();
MapView currentMapView = MapUtils.getItemMapView(currentItem);
if (currentMapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(currentMapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(currentMapView.getId())) {
item.setItemStack(new ItemStack(Material.MAP, currentItem.getAmount()));
}
}
ItemStack replacement = processImageFilledMap(item.getItemStack());
if (replacement != null) {
item.setItemStack(replacement);
}
}
@EventHandler(priority = EventPriority.NORMAL)
public void onChunkLoad(ChunkLoadEvent event) {
if (ModernEventsUtils.modernEventsExists()) {
return;
}
Chunk chunk = event.getChunk();
Scheduler.executeOrScheduleSync(ImageFrame.plugin, () -> {
for (Entity entity : chunk.getEntities()) {
if (entity instanceof ItemFrame) {
ItemFrame itemFrame = (ItemFrame) entity;
ItemStack itemStack = itemFrame.getItem();
MapView mapView = MapUtils.getItemMapView(itemStack);
if (mapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
Scheduler.runTask(ImageFrame.plugin, () -> itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false), itemFrame);
}
}
}
}
}, chunk);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerJoin(PlayerJoinEvent event) {
Inventory inventory = event.getPlayer().getInventory();
processImageFilledMaps(SlotAccessor.of(i -> inventory.getItem(i), (i, s) -> inventory.setItem(i, s)), inventory.getSize());
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerQuit(PlayerQuitEvent event) {
Inventory inventory = event.getPlayer().getInventory();
processImageFilledMaps(SlotAccessor.of(i -> inventory.getItem(i), (i, s) -> inventory.setItem(i, s)), inventory.getSize());
}
public static class ModernEvents implements Listener {
@EventHandler(priority = EventPriority.NORMAL)
public void onEntityLoad(EntitiesLoadEvent event) {
for (Entity entity : event.getEntities()) {
if (entity instanceof ItemFrame) {
ItemFrame itemFrame = (ItemFrame) entity;
ItemStack itemStack = itemFrame.getItem();
MapView mapView = MapUtils.getItemMapView(itemStack);
if (mapView != null) {
if (ImageFrame.imageMapManager.isMapDeleted(mapView) && !ImageFrame.exemptMapIdsFromDeletion.satisfies(mapView.getId())) {
Scheduler.runTask(ImageFrame.plugin, () -> itemFrame.setItem(new ItemStack(Material.MAP, itemStack.getAmount()), false), itemFrame);
}
}
}
}
}
}
}
@@ -0,0 +1,50 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.media;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.utils.GifReader;
import com.loohp.imageframe.utils.HTTPRequestUtils;
import net.kyori.adventure.key.Key;
import java.util.Iterator;
import java.util.List;
public class GifReaderMediaLoader implements MediaLoader {
private static final Key IDENTIFIER = Key.key("imageframe", "gif_reader");
@Override
public Key getIdentifier() {
return IDENTIFIER;
}
@Override
public boolean shouldTryRead(String url) {
return true;
}
@Override
public Iterator<MediaFrame> tryLoad(String url) throws Exception {
List<GifReader.ImageFrame> frames = GifReader.readGif(HTTPRequestUtils.getInputStream(url), ImageFrame.maxImageFileSize).get();
return frames.stream().map(f -> MediaFrame.animatedFrame(f.getImage(), f.getDelay())).iterator();
}
}
@@ -0,0 +1,55 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.media;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.utils.HTTPRequestUtils;
import net.kyori.adventure.key.Key;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Iterator;
public class ImageIOMediaLoader implements MediaLoader {
private static final Key IDENTIFIER = Key.key("imageframe", "imageio");
@Override
public Key getIdentifier() {
return IDENTIFIER;
}
@Override
public boolean shouldTryRead(String url) {
return true;
}
@Override
public Iterator<MediaFrame> tryLoad(String url) throws Exception {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(HTTPRequestUtils.download(url, ImageFrame.maxImageFileSize)));
if (image == null) {
throw new IOException("ImageIO cannot read media type in " + url);
}
return MediaLoader.staticImage(image);
}
}
@@ -0,0 +1,50 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.media;
import java.awt.image.BufferedImage;
public class MediaFrame {
public static MediaFrame staticFrame(BufferedImage image) {
return new MediaFrame(image, -1);
}
public static MediaFrame animatedFrame(BufferedImage image, int durationMs) {
return new MediaFrame(image, durationMs);
}
private final BufferedImage image;
private final int durationMs;
private MediaFrame(BufferedImage image, int durationMs) {
this.image = image;
this.durationMs = durationMs;
}
public BufferedImage getImage() {
return image;
}
public int getDurationMs() {
return durationMs;
}
}
@@ -0,0 +1,41 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.media;
import net.kyori.adventure.key.Key;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.Iterator;
public interface MediaLoader {
static Iterator<MediaFrame> staticImage(BufferedImage image) {
return Collections.singletonList(MediaFrame.staticFrame(image)).iterator();
}
Key getIdentifier();
boolean shouldTryRead(String url);
Iterator<MediaFrame> tryLoad(String url) throws Exception;
}
@@ -0,0 +1,73 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.media;
import java.awt.image.BufferedImage;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class TimedMediaFrameIterator implements Iterator<BufferedImage> {
private final Iterator<MediaFrame> backing;
private final int stepMs;
private MediaFrame current;
private int elapsedInFrame = 0;
private boolean finished = false;
public TimedMediaFrameIterator(Iterator<MediaFrame> backing, int stepMs) {
this.backing = backing;
this.stepMs = stepMs;
if (backing.hasNext()) {
this.current = backing.next();
} else {
finished = true;
}
}
@Override
public boolean hasNext() {
return !finished;
}
@Override
public BufferedImage next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
MediaFrame result = current;
if (current.getDurationMs() == -1) {
return result.getImage();
}
elapsedInFrame += stepMs;
while (current.getDurationMs() != -1 && elapsedInFrame >= current.getDurationMs()) {
elapsedInFrame -= current.getDurationMs();
if (backing.hasNext()) {
current = backing.next();
} else {
finished = true;
break;
}
}
return result.getImage();
}
}
@@ -0,0 +1,160 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.metrics;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.objectholders.ImageMap;
import com.loohp.imageframe.utils.PlayerUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.map.MapCursor;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
@SuppressWarnings("Convert2Lambda")
public class Charts {
public static void setup(Metrics metrics) {
metrics.addCustomChart(new Metrics.SingleLineChart("total_images_created", new Callable<Integer>() {
@Override
public Integer call() {
return ImageFrame.imageMapManager.getMaps().size();
}
}));
metrics.addCustomChart(new Metrics.SingleLineChart("total_maps_created", new Callable<Integer>() {
@Override
public Integer call() {
return ImageFrame.imageMapManager.getMaps().stream().mapToInt(each -> each.getMapViews().size()).sum();
}
}));
metrics.addCustomChart(new Metrics.AdvancedPie("images_created_by_type_id", new Callable<Map<String, Integer>>() {
@Override
public Map<String, Integer> call() {
Map<String, Integer> valueMap = new HashMap<>();
for (ImageMap imageMap : ImageFrame.imageMapManager.getMaps()) {
String type = imageMap.getType().asString();
valueMap.merge(type, 1, Integer::sum);
}
return valueMap;
}
}));
metrics.addCustomChart(new Metrics.AdvancedPie("images_created_by_type", new Callable<Map<String, Integer>>() {
@Override
public Map<String, Integer> call() {
Map<String, Integer> valueMap = new HashMap<>();
for (ImageMap imageMap : ImageFrame.imageMapManager.getMaps()) {
String type = imageMap.getLegacyType();
if (type != null) {
valueMap.merge(type, 1, Integer::sum);
}
}
return valueMap;
}
}));
metrics.addCustomChart(new Metrics.SingleLineChart("total_markers_created", new Callable<Integer>() {
@Override
public Integer call() {
return ImageFrame.imageMapManager.getMaps().stream().flatMap(each -> each.getMapMarkers().stream()).mapToInt(each -> each.size()).sum();
}
}));
metrics.addCustomChart(new Metrics.AdvancedPie("markers_created_by_type", new Callable<Map<String, Integer>>() {
@Override
public Map<String, Integer> call() {
Map<String, Integer> valueMap = new HashMap<>();
for (ImageMap imageMap : ImageFrame.imageMapManager.getMaps()) {
for (Map<String, MapCursor> map : imageMap.getMapMarkers()) {
for (MapCursor mapCursor : map.values()) {
String type = mapCursor.getType().name().toLowerCase();
valueMap.merge(type, 1, Integer::sum);
}
}
}
return valueMap;
}
}));
metrics.addCustomChart(new Metrics.SingleLineChart("item_frames_made_invisible_in_last_interval", new Callable<Integer>() {
@Override
public Integer call() {
long value = ImageFrame.invisibleFrameManager.getItemFramesMadeInvisible().getAndSet(0);
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
}
}));
metrics.addCustomChart(new Metrics.SingleLineChart("invisible_item_frames_placed_in_last_interval", new Callable<Integer>() {
@Override
public Integer call() {
long value = ImageFrame.invisibleFrameManager.getInvisibleItemFramesPlaced().getAndSet(0);
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
}
}));
metrics.addCustomChart(new Metrics.SingleLineChart("embedded_service_image_uploaded_in_last_interval", new Callable<Integer>() {
@Override
public Integer call() {
long value = ImageFrame.imageUploadManager.getImagesUploadedCounter().getAndSet(0);
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
}
}));
metrics.addCustomChart(new Metrics.SimplePie("storage_type", new Callable<String>() {
@Override
public String call() {
return ImageFrame.imageFrameStorage.getLoader().getIdentifier().asString();
}
}));
metrics.addCustomChart(new Metrics.SingleLineChart("players_with_imageframe_client", new Callable<Integer>() {
@Override
public Integer call() {
return ImageFrame.customClientNetworkManager.getPlayers().size();
}
}));
metrics.addCustomChart(new Metrics.SimplePie("imageframe_language_by_server", new Callable<String>() {
@Override
public String call() {
return ImageFrame.language;
}
}));
metrics.addCustomChart(new Metrics.AdvancedPie("imageframe_language_by_players", new Callable<Map<String, Integer>>() {
@Override
public Map<String, Integer> call() {
Map<String, Integer> valueMap = new HashMap<>();
for (Player player : Bukkit.getOnlinePlayers()) {
String language = PlayerUtils.getPlayerLanguage(player);
valueMap.merge(language, 1, Integer::sum);
}
return valueMap;
}
}));
}
}
@@ -1,8 +1,8 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2022. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2022. Contributors
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -34,6 +34,13 @@
*/
package com.loohp.imageframe.metrics;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
@@ -51,8 +58,8 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -60,12 +67,6 @@ import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HttpsURLConnection;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
public class Metrics {
@@ -80,7 +81,7 @@ public class Metrics {
* @param serviceId The id of the service. It can be found at <a
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
*/
public Metrics(JavaPlugin plugin, int serviceId) {
public Metrics(Plugin plugin, int serviceId) {
this.plugin = plugin;
// Get the config file
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
@@ -113,21 +114,41 @@ public class Metrics {
boolean logErrors = config.getBoolean("logFailedRequests", false);
boolean logSentData = config.getBoolean("logSentData", false);
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
boolean isFolia = false;
try {
isFolia = Class.forName("io.papermc.paper.threadedregions.RegionizedServer") != null;
} catch (Exception e) {
}
metricsBase =
new MetricsBase(
new // See https://github.com/Bastian/bstats-metrics/pull/126
// See https://github.com/Bastian/bstats-metrics/pull/126
// See https://github.com/Bastian/bstats-metrics/pull/126
// See https://github.com/Bastian/bstats-metrics/pull/126
// See https://github.com/Bastian/bstats-metrics/pull/126
// See https://github.com/Bastian/bstats-metrics/pull/126
// See https://github.com/Bastian/bstats-metrics/pull/126
MetricsBase(
"bukkit",
serverUUID,
serviceId,
enabled,
this::appendPlatformData,
this::appendServiceData,
submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
isFolia
? null
: submitDataTask -> Scheduler.runTask(plugin, submitDataTask),
plugin::isEnabled,
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
(message) -> this.plugin.getLogger().log(Level.INFO, message),
logErrors,
logSentData,
logResponseStatusText);
logResponseStatusText,
false);
}
/** Shuts down the underlying scheduler service. */
public void shutdown() {
metricsBase.shutdown();
}
/**
@@ -173,13 +194,12 @@ public class Metrics {
public static class MetricsBase {
/** The version of the Metrics class. */
public static final String METRICS_VERSION = "3.0.0";
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
public static final String METRICS_VERSION = "3.1.0";
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
private final ScheduledExecutorService scheduler;
private final String platform;
private final String serverUuid;
@@ -228,6 +248,7 @@ public class Metrics {
* @param logErrors Whether or not errors should be logged.
* @param logSentData Whether or not the sent data should be logged.
* @param logResponseStatusText Whether or not the response status text should be logged.
* @param skipRelocateCheck Whether or not the relocate check should be skipped.
*/
public MetricsBase(
String platform,
@@ -242,7 +263,22 @@ public class Metrics {
Consumer<String> infoLogger,
boolean logErrors,
boolean logSentData,
boolean logResponseStatusText) {
boolean logResponseStatusText,
boolean skipRelocateCheck) {
ScheduledThreadPoolExecutor scheduler =
new ScheduledThreadPoolExecutor(
1,
task -> {
Thread thread = new Thread(task, "bStats-Metrics");
thread.setDaemon(true);
return thread;
});
// We want delayed tasks (non-periodic) that will execute in the future to be
// cancelled when the scheduler is shutdown.
// Otherwise, we risk preventing the server from shutting down even when
// MetricsBase#shutdown() is called
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
this.scheduler = scheduler;
this.platform = platform;
this.serverUuid = serverUuid;
this.serviceId = serviceId;
@@ -256,9 +292,12 @@ public class Metrics {
this.logErrors = logErrors;
this.logSentData = logSentData;
this.logResponseStatusText = logResponseStatusText;
checkRelocation();
if (!skipRelocateCheck) {
checkRelocation();
}
if (enabled) {
// WARNING: Removing the option to opt-out will get your plugin banned from bStats
// WARNING: Removing the option to opt-out will get your plugin banned from
// bStats
startSubmitting();
}
}
@@ -267,6 +306,10 @@ public class Metrics {
this.customCharts.add(chart);
}
public void shutdown() {
scheduler.shutdown();
}
private void startSubmitting() {
final Runnable submitTask =
() -> {
@@ -281,13 +324,14 @@ public class Metrics {
this.submitData();
}
};
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
// of requests on the
// bStats backend. To circumvent this problem, we introduce some randomness into the initial
// and second delay.
// WARNING: You must not modify and part of this Metrics class, including the submit delay or
// frequency!
// WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven
// distribution of requests on the
// bStats backend. To circumvent this problem, we introduce some randomness into
// the initial and second delay.
// WARNING: You must not modify and part of this Metrics class, including the
// submit delay or frequency!
// WARNING: Modifying this code will get your plugin banned on bStats. Just
// don't do it!
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
@@ -362,14 +406,14 @@ public class Metrics {
// You can use the property to disable the check in your test environment
if (System.getProperty("bstats.relocatecheck") == null
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
// Maven's Relocate is clever and changes strings, too. So we have to use this little
// "trick" ... :D
// Maven's Relocate is clever and changes strings, too. So we have to use this
// little "trick" ... :D
final String defaultPackage =
new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
final String examplePackage =
new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
// We want to make sure no one just copy & pastes the example and uses the wrong package
// names
// We want to make sure no one just copy & pastes the example and uses the wrong
// package names
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
@@ -395,6 +439,72 @@ public class Metrics {
}
}
public static class AdvancedBarChart extends CustomChart {
private final Callable<Map<String, int[]>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, int[]> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, int[]> entry : map.entrySet()) {
if (entry.getValue().length == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class SimplePie extends CustomChart {
private final Callable<String> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimplePie(String chartId, Callable<String> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
String value = callable.call();
if (value == null || value.isEmpty()) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
public static class DrilldownPie extends CustomChart {
private final Callable<Map<String, Map<String, Integer>>> callable;
@@ -439,9 +549,9 @@ public class Metrics {
}
}
public static class AdvancedPie extends CustomChart {
public static class SingleLineChart extends CustomChart {
private final Callable<Map<String, Integer>> callable;
private final Callable<Integer> callable;
/**
* Class constructor.
@@ -449,33 +559,19 @@ public class Metrics {
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
public SingleLineChart(String chartId, Callable<Integer> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
int value = callable.call();
if (value == 0) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
return new JsonObjectBuilder().appendField("value", value).build();
}
}
@@ -519,7 +615,7 @@ public class Metrics {
}
}
public static class SimpleBarChart extends CustomChart {
public static class AdvancedPie extends CustomChart {
private final Callable<Map<String, Integer>> callable;
@@ -529,7 +625,7 @@ public class Metrics {
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@@ -542,8 +638,18 @@ public class Metrics {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
if (entry.getValue() == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
@@ -583,9 +689,9 @@ public class Metrics {
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
}
public static class SimplePie extends CustomChart {
public static class SimpleBarChart extends CustomChart {
private final Callable<String> callable;
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
@@ -593,33 +699,7 @@ public class Metrics {
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimplePie(String chartId, Callable<String> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
String value = callable.call();
if (value == null || value.isEmpty()) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
public static class AdvancedBarChart extends CustomChart {
private final Callable<Map<String, int[]>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@@ -627,54 +707,18 @@ public class Metrics {
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, int[]> map = callable.call();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, int[]> entry : map.entrySet()) {
if (entry.getValue().length == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class SingleLineChart extends CustomChart {
private final Callable<Integer> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SingleLineChart(String chartId, Callable<Integer> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
int value = callable.call();
if (value == 0) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
/**
* An extremely simple JSON builder.
*
@@ -0,0 +1,91 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.migration;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.objectholders.DitheringType;
import com.loohp.imageframe.objectholders.ImageMapLoaders;
import com.loohp.imageframe.objectholders.NonUpdatableImageMapCreateInfo;
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMap;
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMapLoader;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.UUID;
public class DrMapMigration implements ExternalPluginMigration {
public static final String PLUGIN_NAME = "DrMap";
@Override
public String externalPluginName() {
return PLUGIN_NAME;
}
@Override
public boolean requirePlayer() {
return true;
}
@Override
public void migrate(UUID owner) {
if (Bukkit.getPluginManager().isPluginEnabled(PLUGIN_NAME)) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] DrMap must be disabled for migration to begin");
return;
}
File migrationMarker = new File(ImageFrame.plugin.getDataFolder().getParent() + "/DrMap/imageframe-migrated.bin");
if (migrationMarker.exists()) {
Bukkit.getConsoleSender().sendMessage(ChatColor.YELLOW + "[ImageFrame] DrMap data already marked as migrated");
return;
}
File folder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/DrMap/images");
if (!folder.exists() || !folder.isDirectory()) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] DrMap plugin data folder not found");
return;
}
NonUpdatableStaticImageMapLoader loader = ImageMapLoaders.getLoader(NonUpdatableStaticImageMapLoader.class);
for (File file : folder.listFiles()) {
try {
BufferedImage[] images = new BufferedImage[] {ImageIO.read(file)};
int mapId = Integer.parseInt(file.getName().substring(0, file.getName().lastIndexOf(".")));
String name = "DrMap_" + mapId;
NonUpdatableStaticImageMap imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, name, images, Collections.singletonList(mapId), 1, 1, DitheringType.NEAREST_COLOR, owner)).get();
ImageFrame.imageMapManager.addMap(imageMap);
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Migrated DrMap " + file.getName() + " to " + name);
} catch (Exception e) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate DrMap " + file.getAbsolutePath());
e.printStackTrace();
}
}
try {
migrationMarker.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] DrMap migration complete!");
}
}
@@ -0,0 +1,40 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.migration;
import java.util.UUID;
public interface ExternalPluginMigration {
String externalPluginName();
boolean requirePlayer();
default void migrate() {
if (requirePlayer()) {
throw new IllegalStateException("Player required!");
}
migrate(null);
}
void migrate(UUID player);
}
@@ -0,0 +1,191 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.migration;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.objectholders.DitheringType;
import com.loohp.imageframe.objectholders.ImageMapLoaders;
import com.loohp.imageframe.objectholders.NonUpdatableImageMapCreateInfo;
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMap;
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMapLoader;
import com.loohp.imageframe.utils.MapUtils;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.simpleyaml.configuration.file.YamlConfiguration;
import org.simpleyaml.configuration.serialization.ConfigurationSerializable;
import org.simpleyaml.configuration.serialization.ConfigurationSerialization;
import org.simpleyaml.configuration.serialization.SerializableAs;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ImageMapMigration implements ExternalPluginMigration {
public static final String PLUGIN_NAME = "ImageMaps";
@Override
public String externalPluginName() {
return PLUGIN_NAME;
}
@Override
public boolean requirePlayer() {
return true;
}
@Override
public void migrate(UUID owner) {
if (Bukkit.getPluginManager().isPluginEnabled(PLUGIN_NAME)) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageMaps must be disabled for migration to begin");
return;
}
File migrationMarker = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageMaps/imageframe-migrated.bin");
if (migrationMarker.exists()) {
Bukkit.getConsoleSender().sendMessage(ChatColor.YELLOW + "[ImageFrame] ImageMaps data already marked as migrated");
return;
}
File folder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageMaps/images");
if (!folder.exists() || !folder.isDirectory()) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageMaps plugin data folder not found");
return;
}
try {
NonUpdatableStaticImageMapLoader loader = ImageMapLoaders.getLoader(NonUpdatableStaticImageMapLoader.class);
ConfigurationSerialization.registerClass(ImageMap.class);
YamlConfiguration mapConfig = YamlConfiguration.loadConfiguration(new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageMaps/maps.yml"));
for (Map.Entry<String, Object> entry : mapConfig.getConfigurationSection("maps").getValues(false).entrySet()) {
String key = entry.getKey();
try {
int mapId = Integer.parseInt(key);
ImageMap data = (ImageMap) entry.getValue();
String fileName = data.getFilename();
BufferedImage[] images = new BufferedImage[] {MapUtils.getSubImage(ImageIO.read(new File(folder, fileName)), data.getX(), data.getY())};
String name = "ImageMaps_" + mapId;
NonUpdatableStaticImageMap imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, name, images, Collections.singletonList(mapId), 1, 1, DitheringType.NEAREST_COLOR, owner)).get();
ImageFrame.imageMapManager.addMap(imageMap);
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Migrated ImageMaps " + key + " to " + name);
} catch (Exception e) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate ImageMaps " + key);
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
try {
migrationMarker.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] ImageMaps migration complete!");
}
@SerializableAs("ImageMaps.Map")
public static class ImageMap implements ConfigurationSerializable {
private final String filename;
private final int x;
private final int y;
private final double scale;
public ImageMap(String filename, int x, int y, double scale) {
this.filename = filename;
this.x = x;
this.y = y;
this.scale = scale;
}
public ImageMap(Map<?, ?> map) {
this.filename = map.get("image").toString();
this.x = (Integer) map.get("x");
this.y = (Integer) map.get("y");
this.scale = (Double) map.get("scale");
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<>();
map.put("image", filename);
map.put("x", x);
map.put("y", y);
map.put("scale", scale);
return map;
}
public String getFilename() {
return filename;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public double getScale() {
return scale;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((filename == null) ? 0 : filename.hashCode());
long temp;
temp = Double.doubleToLongBits(scale);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof ImageMap))
return false;
ImageMap other = (ImageMap) obj;
if (filename == null) {
if (other.filename != null)
return false;
} else if (!filename.equals(other.filename))
return false;
if (Double.doubleToLongBits(scale) != Double.doubleToLongBits(other.scale))
return false;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
}
}
@@ -0,0 +1,147 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.migration;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.objectholders.DitheringType;
import com.loohp.imageframe.objectholders.ImageMapLoaders;
import com.loohp.imageframe.objectholders.NonUpdatableImageMapCreateInfo;
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMap;
import com.loohp.imageframe.objectholders.NonUpdatableStaticImageMapLoader;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.simpleyaml.configuration.file.YamlFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class ImageOnMapMigration implements ExternalPluginMigration {
public static final String PLUGIN_NAME = "ImageOnMap";
@Override
public String externalPluginName() {
return PLUGIN_NAME;
}
@Override
public boolean requirePlayer() {
return false;
}
@Override
@SuppressWarnings("unchecked")
public void migrate(UUID unused) {
if (Bukkit.getPluginManager().isPluginEnabled(PLUGIN_NAME)) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageOnMap must be disabled for migration to begin");
return;
}
File migrationMarker = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageOnMap/imageframe-migrated.bin");
if (migrationMarker.exists()) {
Bukkit.getConsoleSender().sendMessage(ChatColor.YELLOW + "[ImageFrame] ImageOnMap data already marked as migrated");
return;
}
File userFolder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageOnMap/maps");
if (!userFolder.exists() || !userFolder.isDirectory()) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] ImageOnMap plugin data folder not found");
return;
}
NonUpdatableStaticImageMapLoader loader = ImageMapLoaders.getLoader(NonUpdatableStaticImageMapLoader.class);
File imageFolder = new File(ImageFrame.plugin.getDataFolder().getParent() + "/ImageOnMap/images");
for (File file : userFolder.listFiles()) {
String fileNameWithoutExtension = file.getName().substring(0, file.getName().lastIndexOf("."));
UUID owner;
try {
owner = UUID.fromString(fileNameWithoutExtension);
} catch (IllegalArgumentException e) {
owner = Bukkit.getOfflinePlayer(fileNameWithoutExtension).getUniqueId();
}
try {
YamlFile yaml = YamlFile.loadConfiguration(file);
List<Map<String, Object>> mapList = (List<Map<String, Object>>) yaml.getList("PlayerMapStore.mapList");
if (mapList == null) {
continue;
}
int index = 0;
for (Map<String, Object> section : mapList) {
try {
String name = ((String) section.get("name")).replace(" ", "_");
String iomId = (String) section.get("id");
String type = (String) section.get("type");
BufferedImage[] images;
List<Integer> mapIds;
int width;
int height;
if (type.equalsIgnoreCase("SINGLE")) {
int mapId = (int) section.get("mapID");
mapIds = Collections.singletonList(mapId);
images = new BufferedImage[] {ImageIO.read(new File(imageFolder, "map" + mapId + ".png"))};
width = 1;
height = 1;
} else if (type.equalsIgnoreCase("POSTER")) {
mapIds = new ArrayList<>((List<Integer>) section.get("mapsIDs"));
images = new BufferedImage[mapIds.size()];
width = (int) section.get("columns");
height = (int) section.get("rows");
for (int i = 0; i < images.length; i++) {
images[i] = ImageIO.read(new File(imageFolder, "map" + mapIds.get(i) + ".png"));
}
} else {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate " + name + " ImageOnMap user file " + file.getName() + ": Unknown type " + type);
continue;
}
NonUpdatableStaticImageMap imageMap;
if (ImageFrame.imageMapManager.getFromCreator(owner, name) == null) {
imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, name, images, mapIds, width, height, DitheringType.NEAREST_COLOR, owner)).get();
} else if (ImageFrame.imageMapManager.getFromCreator(owner, iomId) == null) {
imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, iomId, images, mapIds, width, height, DitheringType.NEAREST_COLOR, owner)).get();
} else {
imageMap = loader.create(new NonUpdatableImageMapCreateInfo(ImageFrame.imageMapManager, "ImageOnMap-" + iomId, images, mapIds, width, height, DitheringType.NEAREST_COLOR, owner)).get();
}
ImageFrame.imageMapManager.addMap(imageMap);
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Migrated ImageOnMap " + file.getName() + " to " + name + " of " + owner);
} catch (Exception e) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate ImageOnMap " + file.getName() + " of index " + index);
e.printStackTrace();
}
index++;
}
} catch (IOException e) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to migrate ImageOnMap user file " + file.getAbsolutePath());
e.printStackTrace();
}
}
try {
migrationMarker.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] ImageOnMap migration complete!");
}
}
@@ -0,0 +1,56 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.migration;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class PluginMigrationRegistry {
private static final Map<String, ExternalPluginMigration> REGISTERED_MIGRATIONS = new ConcurrentHashMap<>();
public static final DrMapMigration DR_MAP_MIGRATION = register(new DrMapMigration());
public static final ImageOnMapMigration IMAGE_ON_MAP_MIGRATION = register(new ImageOnMapMigration());
public static final ImageMapMigration IMAGE_MAP_MIGRATION = register(new ImageMapMigration());
public static Set<String> getPluginNames() {
return REGISTERED_MIGRATIONS.keySet();
}
public static ExternalPluginMigration getMigration(String plugin) {
ExternalPluginMigration migration = REGISTERED_MIGRATIONS.get(plugin);
if (migration != null) {
return migration;
}
return REGISTERED_MIGRATIONS.entrySet().stream()
.filter(e -> e.getKey().equalsIgnoreCase(plugin))
.findFirst()
.map(e -> e.getValue())
.orElse(null);
}
public static <T extends ExternalPluginMigration> T register(T migration) {
REGISTERED_MIGRATIONS.put(migration.externalPluginName(), migration);
return migration;
}
}
@@ -0,0 +1,48 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.nms;
import com.loohp.imageframe.ImageFrame;
import java.lang.reflect.InvocationTargetException;
public class NMS {
private static NMSWrapper instance;
@SuppressWarnings("unchecked")
public synchronized static NMSWrapper getInstance() {
if (instance != null) {
return instance;
}
try {
Class<NMSWrapper> nmsImplClass = (Class<NMSWrapper>) Class.forName("com.loohp.imageframe.nms." + ImageFrame.version.name());
return instance = nmsImplClass.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
if (ImageFrame.version.isSupported()) {
throw new RuntimeException("Missing NMSWrapper implementation for version " + ImageFrame.version.name(), e);
} else {
throw new RuntimeException("No NMSWrapper implementation for UNSUPPORTED version " + ImageFrame.version.name(), e);
}
}
}
}
@@ -0,0 +1,468 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
import com.loohp.imageframe.hooks.viaversion.ViaHook;
import com.loohp.imageframe.nms.NMS;
import com.loohp.imageframe.utils.FakeItemUtils;
import com.loohp.imageframe.utils.MapUtils;
import com.loohp.imageframe.utils.ModernEventsUtils;
import com.loohp.platformscheduler.Scheduler;
import com.loohp.platformscheduler.platform.folia.FoliaScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityTeleportEvent;
import org.bukkit.event.hanging.HangingPlaceEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.EntitiesLoadEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class AnimatedFakeMapManager implements Listener, Runnable {
private final Map<UUID, TrackedItemFrameData> itemFrames;
private final Map<Player, Set<Integer>> knownMapIds;
private final Map<Player, Set<Integer>> pendingKnownMapIds;
public AnimatedFakeMapManager() {
this.itemFrames = new ConcurrentHashMap<>();
this.knownMapIds = new ConcurrentHashMap<>();
this.pendingKnownMapIds = new ConcurrentHashMap<>();
Scheduler.runTaskTimerAsynchronously(ImageFrame.plugin, this, 0, 1);
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
if (ModernEventsUtils.modernEventsExists()) {
Bukkit.getPluginManager().registerEvents(new ModernEvents(), ImageFrame.plugin);
}
for (World world : Bukkit.getWorlds()) {
for (Entity entity : world.getEntities()) {
Scheduler.executeOrScheduleSync(ImageFrame.plugin, () -> handleEntity(entity), entity);
}
}
for (Player player : Bukkit.getOnlinePlayers()) {
knownMapIds.put(player, ConcurrentHashMap.newKeySet());
pendingKnownMapIds.put(player, ConcurrentHashMap.newKeySet());
}
}
private Map<UUID, CompletableFuture<ItemFrameInfo>> collectItemFramesInfo(boolean async) {
boolean isFolia = Scheduler.getPlatform() instanceof FoliaScheduler;
Map<UUID, CompletableFuture<ItemFrameInfo>> futures = new HashMap<>();
for (Map.Entry<UUID, TrackedItemFrameData> entry : itemFrames.entrySet()) {
UUID uuid = entry.getKey();
ItemFrame itemFrame = entry.getValue().getItemFrame();
CompletableFuture<ItemFrameInfo> future = new CompletableFuture<>();
Runnable task = () -> {
try {
if (itemFrame.isValid()) {
Set<Player> trackedPlayers;
trackedPlayers = NMS.getInstance().getEntityTrackers(itemFrame);
future.complete(new ItemFrameInfo(itemFrame.getEntityId(), trackedPlayers, itemFrame.getItem()));
} else {
future.complete(null);
}
} catch (Throwable e) {
future.completeExceptionally(e);
}
};
if (async && !isFolia) {
task.run();
} else {
Scheduler.executeOrScheduleSync(ImageFrame.plugin, task, itemFrame);
}
futures.put(uuid, future);
}
return futures;
}
public void run() {
Map<UUID, CompletableFuture<ItemFrameInfo>> entityTrackers = collectItemFramesInfo(!ImageFrame.handleAnimatedMapsOnMainThread);
Map<Player, List<FakeItemUtils.ItemFrameUpdateData>> updateData = new HashMap<>();
long deadline = System.currentTimeMillis() + 2000;
for (Map.Entry<UUID, CompletableFuture<ItemFrameInfo>> entry : entityTrackers.entrySet()) {
UUID uuid = entry.getKey();
ItemFrameInfo frameInfo;
try {
frameInfo = entry.getValue().get(Math.max(0, deadline - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
frameInfo = null;
}
if (frameInfo == null) {
itemFrames.remove(uuid);
continue;
}
int entityId = frameInfo.getEntityId();
Set<Player> players = frameInfo.getTrackedPlayers();
ItemStack itemStack = frameInfo.getItemStack();
TrackedItemFrameData data = itemFrames.get(uuid);
if (data == null) {
continue;
}
AnimationData animationData = data.getAnimationData();
MapView mapView = MapUtils.getItemMapView(itemStack);
if (mapView == null) {
data.setAnimationData(AnimationData.EMPTY);
continue;
}
if (animationData.isEmpty() || !animationData.getMapView().equals(mapView)) {
ImageMap map = ImageFrame.imageMapManager.getFromMapView(mapView);
if (map == null || !map.requiresAnimationService()) {
if (!animationData.isEmpty()) {
data.setAnimationData(AnimationData.EMPTY);
}
continue;
}
data.setAnimationData(animationData = new AnimationData(map, mapView, map.getMapViews().indexOf(mapView)));
} else if (!animationData.isEmpty()) {
if (!animationData.getImageMap().isValid()) {
for (Player player : players) {
FakeItemUtils.sendFakeItemChange(player, entityId, itemStack);
}
data.setAnimationData(AnimationData.EMPTY);
continue;
}
}
ImageMap imageMap = animationData.getImageMap();
if (!imageMap.requiresAnimationService()) {
data.setAnimationData(AnimationData.EMPTY);
continue;
}
int index = animationData.getIndex();
int currentPosition = imageMap.getCurrentPositionInSequenceWithOffset();
int mapId = imageMap.getAnimationFakeMapId(currentPosition, index, imageMap.isAnimationPaused());
if (mapId < 0) {
continue;
}
Set<Player> requiresSending = new HashSet<>();
Set<Player> needReset = new HashSet<>();
for (Iterator<Player> itr = players.iterator(); itr.hasNext();) {
Player player = itr.next();
MapMarkerEditManager.MapMarkerEditData edit = ImageFrame.mapMarkerEditManager.getActiveEditing(player);
if (edit != null && Objects.equals(edit.getImageMap(), imageMap)) {
needReset.add(player);
itr.remove();
continue;
}
Set<Integer> knownIds = knownMapIds.get(player);
Set<Integer> pendingKnownIds = pendingKnownMapIds.get(player);
if (knownIds != null && !knownIds.contains(mapId)) {
if (pendingKnownIds != null && !pendingKnownIds.contains(mapId)) {
Set<Integer> fakeMapIds = imageMap.getFakeMapIds();
if (fakeMapIds != null) {
pendingKnownIds.addAll(fakeMapIds);
requiresSending.add(player);
}
}
itr.remove();
}
}
if (!requiresSending.isEmpty()) {
imageMap.sendAnimationFakeMaps(requiresSending, (p, i, r) -> {
Set<Integer> pendingKnownIds = pendingKnownMapIds.get(p);
if (pendingKnownIds != null && pendingKnownIds.remove(i) && r) {
Set<Integer> knownIds = knownMapIds.get(p);
if (knownIds != null) {
knownIds.add(i);
}
}
});
}
if (!needReset.isEmpty()) {
FakeItemUtils.ItemFrameUpdateData itemFrameUpdateData = new FakeItemUtils.ItemFrameUpdateData(entityId, itemStack, mapView.getId(), mapView, currentPosition);
needReset.forEach(p -> updateData.computeIfAbsent(p, k -> new ArrayList<>()).add(itemFrameUpdateData));
}
FakeItemUtils.ItemFrameUpdateData itemFrameUpdateData = new FakeItemUtils.ItemFrameUpdateData(entityId, getMapItem(mapId), mapView.getId(), mapView, currentPosition);
players.forEach(p -> updateData.computeIfAbsent(p, k -> new ArrayList<>()).add(itemFrameUpdateData));
}
Map<Player, List<Runnable>> sendingTasks = new HashMap<>();
for (Map.Entry<Player, List<FakeItemUtils.ItemFrameUpdateData>> entry : updateData.entrySet()) {
Player player = entry.getKey();
if (ImageFrame.ifPlayerManager.getIFPlayer(player.getUniqueId()).getPreference(IFPlayerPreference.VIEW_ANIMATED_MAPS, BooleanState.class).getCalculatedValue(() -> ImageFrame.getPreferenceUnsetValue(player, IFPlayerPreference.VIEW_ANIMATED_MAPS).getRawValue(true))) {
if (ImageFrame.viaHook && ViaHook.isPlayerLegacy(player)) {
if (!ImageFrame.viaDisableSmoothAnimationForLegacyPlayers) {
List<FakeItemUtils.ItemFrameUpdateData> list = entry.getValue();
for (FakeItemUtils.ItemFrameUpdateData data : list) {
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> MapUtils.sendImageMap(data.getRealMapId(), data.getMapView(), data.getCurrentPosition(), Collections.singleton(player), true));
}
}
} else {
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> FakeItemUtils.sendFakeItemChange(player, entry.getValue()));
}
}
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (ImageFrame.ifPlayerManager.getIFPlayer(player.getUniqueId()).getPreference(IFPlayerPreference.VIEW_ANIMATED_MAPS, BooleanState.class).getCalculatedValue(() -> ImageFrame.getPreferenceUnsetValue(player, IFPlayerPreference.VIEW_ANIMATED_MAPS).getRawValue(true))) {
ItemStack mainhand = player.getEquipment().getItemInMainHand();
ItemStack offhand = player.getEquipment().getItemInOffHand();
MapView mainHandView = MapUtils.getItemMapView(mainhand);
MapView offhandView = MapUtils.getItemMapView(offhand);
if (mainHandView != null) {
ImageMap mainHandMap = ImageFrame.imageMapManager.getFromMapView(mainHandView);
if (mainHandMap != null && mainHandMap.requiresAnimationService()) {
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> mainHandMap.send(player));
}
}
if (offhandView != null && !offhandView.equals(mainHandView)) {
ImageMap offHandMap = ImageFrame.imageMapManager.getFromMapView(offhandView);
if (offHandMap != null && offHandMap.requiresAnimationService()) {
sendingTasks.computeIfAbsent(player, k -> new ArrayList<>()).add(() -> offHandMap.send(player));
}
}
}
}
if (ImageFrame.sendAnimatedMapsOnMainThread) {
for (Map.Entry<Player, List<Runnable>> entry : sendingTasks.entrySet()) {
Scheduler.runTask(ImageFrame.plugin, () -> entry.getValue().forEach(Runnable::run), entry.getKey());
}
} else {
sendingTasks.values().forEach(l -> l.forEach(Runnable::run));
}
}
@SuppressWarnings("deprecation")
private ItemStack getMapItem(int mapId) {
ItemStack itemStack = new ItemStack(Material.FILLED_MAP);
MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
mapMeta.setMapId(mapId);
itemStack.setItemMeta(mapMeta);
return itemStack;
}
private void handleEntity(Entity entity) {
if (!(entity instanceof ItemFrame)) {
return;
}
ItemFrame itemFrame = (ItemFrame) entity;
UUID uuid = itemFrame.getUniqueId();
if (itemFrames.containsKey(uuid)) {
return;
}
MapView mapView = MapUtils.getItemMapView(itemFrame.getItem());
if (mapView == null) {
itemFrames.put(uuid, new TrackedItemFrameData(itemFrame, AnimationData.EMPTY));
return;
}
ImageMap map = ImageFrame.imageMapManager.getFromMapView(mapView);
if (map == null || !map.requiresAnimationService()) {
itemFrames.put(uuid, new TrackedItemFrameData(itemFrame, AnimationData.EMPTY));
return;
}
itemFrames.put(uuid, new TrackedItemFrameData(itemFrame, new AnimationData(map, mapView, map.getMapViews().indexOf(mapView))));
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onChunkLoad(ChunkLoadEvent event) {
Chunk chunk = event.getChunk();
Scheduler.executeOrScheduleSync(ImageFrame.plugin, () -> {
for (Entity entity : chunk.getEntities()) {
handleEntity(entity);
}
}, chunk);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onHangingPlace(HangingPlaceEvent event) {
handleEntity(event.getEntity());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEntityEvent event) {
handleEntity(event.getRightClicked());
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Scheduler.runTaskLater(ImageFrame.plugin, () -> {
if (player.isOnline()) {
knownMapIds.put(player, ConcurrentHashMap.newKeySet());
pendingKnownMapIds.put(player, ConcurrentHashMap.newKeySet());
}
}, 20);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
knownMapIds.remove(player);
pendingKnownMapIds.remove(player);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntityTeleport(EntityTeleportEvent event) {
Entity entity = event.getEntity();
if (entity instanceof ItemFrame) {
UUID uuid = entity.getUniqueId();
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
TrackedItemFrameData data = itemFrames.remove(uuid);
if (data != null) {
itemFrames.put(uuid, data);
}
});
}
}
@EventHandler
public void onImageMapUpdate(ImageMapUpdatedEvent event) {
ImageMap imageMap = event.getImageMap();
if (!imageMap.requiresAnimationService()) {
return;
}
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
Set<Integer> ids = imageMap.getFakeMapIds();
if (ids != null) {
for (Set<Integer> knownIds : knownMapIds.values()) {
knownIds.removeAll(ids);
}
for (Set<Integer> pendingKnownIds : pendingKnownMapIds.values()) {
pendingKnownIds.removeAll(ids);
}
}
});
}
public class ModernEvents implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntityLoad(EntitiesLoadEvent event) {
for (Entity entity : event.getEntities()) {
handleEntity(entity);
}
}
}
public static class TrackedItemFrameData {
private final ItemFrame itemFrame;
private AnimationData animationData;
public TrackedItemFrameData(ItemFrame itemFrame, AnimationData animationData) {
this.itemFrame = itemFrame;
this.animationData = animationData;
}
public ItemFrame getItemFrame() {
return itemFrame;
}
public AnimationData getAnimationData() {
return animationData;
}
public void setAnimationData(AnimationData animationData) {
this.animationData = animationData;
}
}
public static class AnimationData {
public static final AnimationData EMPTY = new AnimationData(null, null, -1);
private final ImageMap imageMap;
private final MapView mapView;
private final int index;
public AnimationData(ImageMap imageMap, MapView mapView, int index) {
this.imageMap = imageMap;
this.mapView = mapView;
this.index = index;
}
public boolean isEmpty() {
return imageMap == null;
}
public ImageMap getImageMap() {
return imageMap;
}
public MapView getMapView() {
return mapView;
}
public int getIndex() {
return index;
}
}
public static class ItemFrameInfo {
private final int entityId;
private final Set<Player> trackedPlayers;
private final ItemStack itemStack;
public ItemFrameInfo(int entityId, Set<Player> trackedPlayers, ItemStack itemStack) {
this.entityId = entityId;
this.trackedPlayers = trackedPlayers;
this.itemStack = itemStack;
}
public int getEntityId() {
return entityId;
}
public Set<Player> getTrackedPlayers() {
return trackedPlayers;
}
public ItemStack getItemStack() {
return itemStack;
}
}
}
@@ -0,0 +1,83 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.lang.ref.WeakReference;
public class BlockPosition {
private final WeakReference<World> world;
private final int x;
private final int y;
private final int z;
public BlockPosition(World world, int x, int y, int z) {
this.world = new WeakReference<>(world);
this.x = x;
this.y = y;
this.z = z;
}
public boolean isWorldLoaded() {
World world = this.world.get();
return world != null && Bukkit.getWorld(world.getUID()) != null;
}
public World getWorld() {
return world.get();
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public Block getBlock() {
return getWorld().getBlockAt(x, y, z);
}
public Location toLocation() {
return new Location(getWorld(), x, y, z);
}
public Vector toVector() {
return new Vector(x, y, z);
}
public BoundingBox toBoundingBox() {
return new BoundingBox(x, y, z, x + 1, y + 1, z + 1);
}
}
@@ -0,0 +1,112 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
public enum BooleanState implements UnsetState, PreferenceState {
TRUE(new JsonPrimitive(true), true, NamedTextColor.GREEN),
FALSE(new JsonPrimitive(false), false, NamedTextColor.RED),
UNSET(UNSET_JSON, null, NamedTextColor.GRAY);
public static final Map<String, BooleanState> STRING_VALUES_MAP = Collections.unmodifiableMap(Arrays.stream(values()).collect(Collectors.toMap(e -> e.toString(), e -> e, (x, y) -> y, LinkedHashMap::new)));
public static final Map<Boolean, BooleanState> BOOLEAN_VALUES_MAP = Collections.unmodifiableMap(Arrays.stream(values()).collect(Collectors.toMap(e -> e.booleanValue, e -> e, (x, y) -> y, LinkedHashMap::new)));
private final JsonElement jsonValue;
private final Boolean booleanValue;
private final TextColor displayColor;
BooleanState(JsonElement jsonValue, Boolean booleanValue, TextColor displayColor) {
this.jsonValue = jsonValue;
this.booleanValue = booleanValue;
this.displayColor = displayColor;
}
@Override
public String toString() {
return name().toLowerCase();
}
@Override
public boolean isUnset() {
return this.equals(UNSET);
}
@Override
public JsonElement getJsonValue() {
return jsonValue;
}
@Override
public TextColor getDisplayColor() {
return displayColor;
}
public Boolean getRawValue() {
return booleanValue;
}
public boolean getRawValue(boolean unsetIsTrue) {
return booleanValue == null ? unsetIsTrue : booleanValue;
}
public boolean getCalculatedValue(BooleanSupplier unsetValueFunction) {
return booleanValue == null ? unsetValueFunction.getAsBoolean() : booleanValue;
}
public static BooleanState fromBoolean(boolean value) {
return fromBoolean((Boolean) value);
}
public static BooleanState fromBoolean(Boolean value) {
return BOOLEAN_VALUES_MAP.getOrDefault(value, UNSET);
}
public static BooleanState fromString(String value) {
return STRING_VALUES_MAP.getOrDefault(value.toLowerCase(), UNSET);
}
public static BooleanState fromJsonValue(JsonElement jsonValue) {
if (jsonValue.isJsonPrimitive()) {
JsonPrimitive jsonPrimitive = jsonValue.getAsJsonPrimitive();
if (jsonPrimitive.isBoolean()) {
return fromBoolean(jsonPrimitive.getAsBoolean());
} else if (jsonPrimitive.isNumber()) {
return fromBoolean(jsonPrimitive.getAsNumber().intValue() != 0);
} else if (jsonPrimitive.isString()) {
return fromString(jsonPrimitive.getAsString());
}
}
return UNSET;
}
}
@@ -0,0 +1,70 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.media.MediaFrame;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.imageframe.utils.GifReader;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Animated (GIF) counterpart of {@link BundledStaticImageMap}: the GIF is a
* jar resource and the Minecraft map ids are pinned in maps.yml.
*/
public class BundledAnimatedImageMap extends URLAnimatedImageMap implements BundledImageMap {
protected BundledAnimatedImageMap(ImageMapManager manager, ImageMapLoader<?, ?> loader, String name, String resourcePath, LazyMappedBufferedImage[][] cachedImages, List<MapView> mapViews, List<Integer> mapIds, List<Map<String, MapCursor>> mapMarkers, int width, int height, DitheringType ditheringType, long creationTime) {
super(manager, loader, -1, name, resourcePath, cachedImages, mapViews, mapIds, mapMarkers, width, height, ditheringType, ImageMap.CONSOLE_CREATOR, Collections.emptyMap(), creationTime, -1, 0);
}
@Override
public String getResourcePath() {
return url;
}
@Override
public void setUrl(String url) {
//bundled maps always render from their jar resource, the path cannot change at runtime
}
@Override
protected Iterator<MediaFrame> loadMedia() throws Exception {
List<GifReader.ImageFrame> frames = GifReader.readGif(BundledStaticImageMap.openResource(getResourcePath()), Long.MAX_VALUE).get();
return frames.stream().map(f -> MediaFrame.animatedFrame(f.getImage(), f.getDelay())).iterator();
}
@Override
public void save(ImageFrameStorage storage, boolean saveAsCopy) {
//the jar is the canonical storage, nothing to save
}
@Override
public ImageMap deepClone(String name, UUID creator) {
throw new UnsupportedOperationException("bundled maps cannot be cloned");
}
}
@@ -0,0 +1,98 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonObject;
import com.loohp.imageframe.utils.FutureUtils;
import net.kyori.adventure.key.Key;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapRenderer;
import org.bukkit.map.MapView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
public class BundledAnimatedImageMapLoader extends ImageMapLoader<BundledAnimatedImageMap, BundledMapCreateInfo> {
private static final Key IDENTIFIER = Key.key("imageframe", "bundled_animated");
@Override
public Key getIdentifier() {
return IDENTIFIER;
}
@Override
public Class<BundledAnimatedImageMap> getImageMapClass() {
return BundledAnimatedImageMap.class;
}
@Override
public Class<BundledMapCreateInfo> getImageMapCreateInfoClass() {
return BundledMapCreateInfo.class;
}
@Override
public List<String> getExtraPermissions() {
return Collections.emptyList();
}
@Override
public boolean isSupported(String imageType) {
//bundled maps are only created from maps.yml in the jar, never through commands
return false;
}
@Override
public Future<BundledAnimatedImageMap> create(BundledMapCreateInfo createInfo) throws Exception {
int mapsCount = createInfo.getWidth() * createInfo.getHeight();
List<MapView> mapViews = BundledStaticImageMapLoader.getPinnedMapViews(createInfo);
List<Integer> mapIds = new ArrayList<>(mapsCount);
List<Map<String, MapCursor>> markers = new ArrayList<>(mapsCount);
for (MapView mapView : mapViews) {
mapIds.add(mapView.getId());
markers.add(new ConcurrentHashMap<>());
}
BundledAnimatedImageMap map = new BundledAnimatedImageMap(createInfo.getManager(), this, createInfo.getName(), createInfo.getResourcePath(), new LazyMappedBufferedImage[mapsCount][], mapViews, mapIds, markers, createInfo.getWidth(), createInfo.getHeight(), createInfo.getDitheringType(), createInfo.getCreationTime());
return FutureUtils.callAsyncMethod(() -> {
FutureUtils.callSyncMethod(() -> {
for (int i = 0; i < mapViews.size(); i++) {
MapView mapView = mapViews.get(i);
for (MapRenderer renderer : mapView.getRenderers()) {
mapView.removeRenderer(renderer);
}
mapView.addRenderer(new URLAnimatedImageMap.URLAnimatedImageMapRenderer(map, i));
}
}).get();
map.update(false);
return map;
});
}
@Override
public Future<BundledAnimatedImageMap> load(ImageMapManager manager, JsonObject json) {
throw new UnsupportedOperationException("bundled maps are loaded from maps.yml in the plugin jar, not from storage");
}
}
@@ -0,0 +1,32 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
/**
* An image map whose canonical definition lives inside the plugin jar
* (maps.yml + an image under maps/). Bundled maps are rebuilt from the jar
* on every startup and are never persisted to the plugin data folder.
*/
public interface BundledImageMap {
String getResourcePath();
}
@@ -0,0 +1,50 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Collections;
import java.util.List;
public class BundledMapCreateInfo extends ImageMapCreateInfo {
private final String resourcePath;
private final List<Integer> mapIds;
private final long creationTime;
public BundledMapCreateInfo(ImageMapManager manager, String name, String resourcePath, int width, int height, DitheringType ditheringType, List<Integer> mapIds, long creationTime) {
super(manager, name, width, height, ditheringType, ImageMap.CONSOLE_CREATOR);
this.resourcePath = resourcePath;
this.mapIds = Collections.unmodifiableList(mapIds);
this.creationTime = creationTime;
}
public String getResourcePath() {
return resourcePath;
}
public List<Integer> getMapIds() {
return mapIds;
}
public long getCreationTime() {
return creationTime;
}
}
@@ -0,0 +1,417 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.utils.MapUtils;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Loads the maps whose canonical definition is bundled inside the plugin jar.
* <p>
* Every image directly under maps/ in the jar becomes a Console owned map
* named after the file (without its extension); a .gif becomes an animated
* map. maps.yml can optionally add an entry per map to override its name,
* size, dithering or map ids. Anything not set there is decided once, on the
* first load:
* <ul>
* <li>If a stored Console map with the same name exists (for example one
* previewed from plugins/ImageFrame/images/), the bundled map takes over
* its size, dithering and map ids, and the stored copy is deleted.</li>
* <li>Otherwise the size is the image size divided by 128 pixels per map,
* and the server assigns new map ids.</li>
* </ul>
* Those decisions are remembered in the main world folder, so the maps keep
* their map ids even if the plugin data folder is wiped. MapIds pinned in
* maps.yml always win.
*/
public class BundledMaps {
public static final String MAPS_YML = "maps.yml";
public static final String MAPS_FOLDER = "maps/";
public static final String ASSIGNED_MAPS_FILE_NAME = "imageframe-bundled-maps.yml";
public static final Set<String> IMAGE_EXTENSIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("png", "jpg", "jpeg", "gif", "bmp", "webp")));
public static int load(ImageMapManager manager, File pluginJar) {
return new BundledMaps(manager, pluginJar).load();
}
private final ImageMapManager manager;
private final File pluginJar;
private final CommandSender console;
private BundledMaps(ImageMapManager manager, File pluginJar) {
this.manager = manager;
this.pluginJar = pluginJar;
this.console = Bukkit.getConsoleSender();
}
private int load() {
List<Definition> definitions = new ArrayList<>(readMapsYml());
Set<String> definedImages = new HashSet<>();
for (Definition definition : definitions) {
definedImages.add(definition.image);
}
for (String image : listBundledImages()) {
if (!definedImages.contains(image)) {
definitions.add(new Definition(getFileNameWithoutExtension(image), image));
}
}
// Pinned maps load first, so map ids the server assigns to other maps never collide with pinned ones
definitions.sort(Comparator.comparing(definition -> definition.pinnedMapIds == null));
World world = MapUtils.getMainWorld();
File assignedMapsFile = new File(world.getWorldFolder(), ASSIGNED_MAPS_FILE_NAME);
Map<String, Assigned> assignedMaps = readAssignedMaps(assignedMapsFile);
boolean assignedMapsChanged = false;
int count = 0;
for (Definition definition : definitions) {
String name = definition.name;
try {
if (name.isEmpty() || name.indexOf(':') >= 0 || name.chars().anyMatch(Character::isWhitespace)) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping bundled image \"" + definition.image + "\": map names cannot contain spaces or colons, rename the file or set a Name in maps.yml");
continue;
}
// Validate the bundled image is present and decodable before touching
// any stored copy of this map, so a bad jar cannot destroy working maps.
BufferedImage image = BundledStaticImageMap.readResourceImage(definition.image);
ImageMap existing = manager.getFromCreator(ImageMap.CONSOLE_CREATOR, name);
if (existing instanceof BundledImageMap) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\" from " + definition.image + ": " + ((BundledImageMap) existing).getResourcePath() + " already uses this name");
continue;
}
Assigned assigned = definition.pinnedMapIds == null ? assignedMaps.get(name) : null;
int width;
int height;
if (definition.width != null && definition.height != null) {
width = definition.width;
height = definition.height;
} else if (existing != null) {
width = existing.getWidth();
height = existing.getHeight();
} else if (assigned != null) {
width = assigned.width;
height = assigned.height;
} else {
width = Math.max(1, Math.round(image.getWidth() / (float) MapUtils.MAP_WIDTH));
height = Math.max(1, Math.round(image.getHeight() / (float) MapUtils.MAP_WIDTH));
if (width * height > ImageFrame.mapMaxSize) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping bundled image \"" + definition.image + "\": at " + MapUtils.MAP_WIDTH + " pixels per map it would be " + width + "x" + height + " maps, over the MaxSize of " + ImageFrame.mapMaxSize + ". Shrink the image or set Width and Height in maps.yml");
continue;
}
}
if (width <= 0 || height <= 0) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\": Width and Height must be positive (got " + width + "x" + height + ")");
continue;
}
DitheringType ditheringType;
if (definition.ditheringType != null) {
ditheringType = definition.ditheringType;
} else if (existing != null) {
ditheringType = existing.getDitheringType();
} else if (assigned != null) {
ditheringType = DitheringType.fromName(assigned.ditheringType);
} else {
ditheringType = DitheringType.fromName(null);
}
long creationTime;
if (definition.creationTime != null) {
creationTime = definition.creationTime;
} else if (existing != null) {
creationTime = existing.getCreationTime();
} else if (assigned != null) {
creationTime = assigned.creationTime;
} else {
creationTime = System.currentTimeMillis();
}
List<Integer> mapIds;
if (definition.pinnedMapIds != null) {
mapIds = definition.pinnedMapIds;
if (mapIds.size() != width * height) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\": MapIds must contain exactly Width * Height (" + (width * height) + ") map ids");
continue;
}
} else if (existing != null) {
if (existing.getMapIds().size() != width * height) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Bundled ImageMap \"" + name + "\" is set to " + width + "x" + height + " in maps.yml but the copy in storage is " + existing.getWidth() + "x" + existing.getHeight() + ", keeping the stored copy. Remove Width and Height from maps.yml to migrate it.");
continue;
}
mapIds = new ArrayList<>(existing.getMapIds());
} else if (assigned != null && assigned.mapIds.size() == width * height) {
mapIds = assigned.mapIds;
} else {
mapIds = new ArrayList<>(width * height);
for (int i = 0; i < width * height; i++) {
mapIds.add(MapUtils.createMap(world).get().getId());
}
}
if (existing != null) {
if (!existing.getMapIds().equals(mapIds)) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Bundled ImageMap \"" + name + "\" has different map ids than the copy in storage, keeping the stored copy. Remove MapIds from maps.yml, or set them to " + existing.getMapIds() + ", to migrate it.");
continue;
}
// Same name and same map ids: the jar is now the canonical
// definition, so drop the copy from the data folder.
manager.deleteMap(existing.getImageIndex());
console.sendMessage(ChatColor.YELLOW + "[ImageFrame] Migrated ImageMap \"" + name + "\" to the bundled definition in the plugin jar, its data folder copy has been removed");
}
if (definition.pinnedMapIds != null) {
if (assignedMaps.remove(name) != null) {
assignedMapsChanged = true;
}
} else {
Assigned decided = new Assigned(width, height, ditheringType.getName(), mapIds, creationTime);
if (!decided.equals(assigned)) {
assignedMaps.put(name, decided);
assignedMapsChanged = true;
console.sendMessage(ChatColor.YELLOW + "[ImageFrame] Bundled ImageMap \"" + name + "\" uses map ids " + mapIds + ", saved to " + assignedMapsFile.getPath());
}
}
BundledMapCreateInfo createInfo = new BundledMapCreateInfo(manager, name, definition.image, width, height, ditheringType, mapIds, creationTime);
boolean animated = definition.image.toLowerCase().endsWith(".gif");
ImageMap imageMap;
if (animated) {
imageMap = ImageMapLoaders.BUNDLED_ANIMATED_IMAGE_MAP_LOADER.create(createInfo).get();
} else {
imageMap = ImageMapLoaders.BUNDLED_STATIC_IMAGE_MAP_LOADER.create(createInfo).get();
}
manager.addBundledMap(imageMap);
count++;
console.sendMessage(ChatColor.GREEN + "[ImageFrame] Loaded bundled " + (animated ? "animated " : "") + "ImageMap \"" + name + "\" (" + width + "x" + height + ", map ids " + mapIds.get(0) + "-" + mapIds.get(mapIds.size() - 1) + ") from the plugin jar");
} catch (Exception e) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to load bundled ImageMap \"" + name + "\" from the plugin jar");
new IOException("Unable to load bundled ImageMap \"" + name + "\" from the plugin jar", e).printStackTrace();
}
}
if (assignedMapsChanged) {
saveAssignedMaps(assignedMapsFile, assignedMaps);
}
return count;
}
private List<Definition> readMapsYml() {
InputStream stream = ImageFrame.plugin.getClass().getClassLoader().getResourceAsStream(MAPS_YML);
if (stream == null) {
return Collections.emptyList();
}
YamlConfiguration mapsYml;
try (Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
mapsYml = YamlConfiguration.loadConfiguration(reader);
} catch (IOException e) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to read bundled maps.yml from the plugin jar");
e.printStackTrace();
return Collections.emptyList();
}
List<Definition> definitions = new ArrayList<>();
for (Map<?, ?> entry : mapsYml.getMapList("Maps")) {
Object image = entry.get("Image");
if (image == null) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping invalid maps.yml entry (missing Image): " + entry);
continue;
}
String imagePath = image.toString();
Definition definition = new Definition(entry.get("Name") == null ? getFileNameWithoutExtension(imagePath) : entry.get("Name").toString(), imagePath);
definition.width = entry.get("Width") instanceof Number ? ((Number) entry.get("Width")).intValue() : null;
definition.height = entry.get("Height") instanceof Number ? ((Number) entry.get("Height")).intValue() : null;
if ((definition.width == null) != (definition.height == null)) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Skipping maps.yml entry for " + imagePath + ": set both Width and Height, or neither");
continue;
}
definition.ditheringType = entry.get("Dithering") == null ? null : DitheringType.fromName(entry.get("Dithering").toString());
definition.pinnedMapIds = entry.get("MapIds") instanceof List ? parseMapIds(entry.get("MapIds")) : null;
definition.creationTime = entry.get("CreationTime") instanceof Number ? ((Number) entry.get("CreationTime")).longValue() : null;
definitions.add(definition);
}
return definitions;
}
private List<String> listBundledImages() {
List<String> images = new ArrayList<>();
if (pluginJar == null || !pluginJar.isFile()) {
return images;
}
try (ZipFile zip = new ZipFile(pluginJar)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String path = entry.getName();
if (entry.isDirectory() || !path.startsWith(MAPS_FOLDER) || path.indexOf('/', MAPS_FOLDER.length()) >= 0) {
continue;
}
int dot = path.lastIndexOf('.');
if (dot > MAPS_FOLDER.length() && IMAGE_EXTENSIONS.contains(path.substring(dot + 1).toLowerCase())) {
images.add(path);
}
}
} catch (IOException e) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to list bundled images in the plugin jar");
e.printStackTrace();
}
// Sorted so the server assigns map ids in a predictable order
Collections.sort(images);
return images;
}
private Map<String, Assigned> readAssignedMaps(File file) {
Map<String, Assigned> assignedMaps = new LinkedHashMap<>();
if (!file.exists()) {
return assignedMaps;
}
for (Map<?, ?> entry : YamlConfiguration.loadConfiguration(file).getMapList("Maps")) {
if (entry.get("Name") == null || !(entry.get("Width") instanceof Number) || !(entry.get("Height") instanceof Number)) {
continue;
}
int width = ((Number) entry.get("Width")).intValue();
int height = ((Number) entry.get("Height")).intValue();
String ditheringType = entry.get("Dithering") == null ? null : entry.get("Dithering").toString();
long creationTime = entry.get("CreationTime") instanceof Number ? ((Number) entry.get("CreationTime")).longValue() : System.currentTimeMillis();
assignedMaps.put(entry.get("Name").toString(), new Assigned(width, height, ditheringType, parseMapIds(entry.get("MapIds")), creationTime));
}
return assignedMaps;
}
private void saveAssignedMaps(File file, Map<String, Assigned> assignedMaps) {
YamlConfiguration yaml = new YamlConfiguration();
List<Map<String, Object>> entries = new ArrayList<>(assignedMaps.size());
for (Map.Entry<String, Assigned> assigned : assignedMaps.entrySet()) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("Name", assigned.getKey());
entry.put("Width", assigned.getValue().width);
entry.put("Height", assigned.getValue().height);
entry.put("Dithering", assigned.getValue().ditheringType);
entry.put("MapIds", assigned.getValue().mapIds);
entry.put("CreationTime", assigned.getValue().creationTime);
entries.add(entry);
}
yaml.set("Maps", entries);
String header = "# Written by ImageFrame. Size, dithering and map ids of the bundled maps that do not\n" +
"# pin MapIds in maps.yml, decided the first time each map loaded. Item frames in this\n" +
"# world show these map ids, so keep this file with the world and do not edit the ids.\n";
try {
Files.write(file.toPath(), (header + yaml.saveToString()).getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
console.sendMessage(ChatColor.RED + "[ImageFrame] Unable to save " + file.getPath());
e.printStackTrace();
}
}
private static List<Integer> parseMapIds(Object value) {
List<Integer> mapIds = new ArrayList<>();
if (value instanceof List) {
for (Object id : (List<?>) value) {
if (id instanceof Number) {
mapIds.add(((Number) id).intValue());
}
}
}
return mapIds;
}
private static String getFileNameWithoutExtension(String path) {
String fileName = path.substring(path.lastIndexOf('/') + 1);
int dot = fileName.lastIndexOf('.');
return dot > 0 ? fileName.substring(0, dot) : fileName;
}
private static class Definition {
private final String name;
private final String image;
private Integer width;
private Integer height;
private DitheringType ditheringType;
private List<Integer> pinnedMapIds;
private Long creationTime;
private Definition(String name, String image) {
this.name = name;
this.image = image;
}
}
private static class Assigned {
private final int width;
private final int height;
private final String ditheringType;
private final List<Integer> mapIds;
private final long creationTime;
private Assigned(int width, int height, String ditheringType, List<Integer> mapIds, long creationTime) {
this.width = width;
this.height = height;
this.ditheringType = ditheringType;
this.mapIds = mapIds;
this.creationTime = creationTime;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Assigned)) {
return false;
}
Assigned assigned = (Assigned) o;
return width == assigned.width && height == assigned.height && creationTime == assigned.creationTime && Objects.equals(ditheringType, assigned.ditheringType) && mapIds.equals(assigned.mapIds);
}
@Override
public int hashCode() {
return Objects.hash(width, height, ditheringType, mapIds, creationTime);
}
}
}
@@ -0,0 +1,105 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.imageframe.utils.MapUtils;
import org.bukkit.Bukkit;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An image map whose canonical definition lives inside the plugin jar:
* the image is a jar resource and the Minecraft map ids are pinned in
* maps.yml. Bundled maps are re-rendered from the jar on every startup
* and are never persisted to the plugin data folder.
*/
public class BundledStaticImageMap extends URLStaticImageMap implements BundledImageMap {
public static InputStream openResource(String resourcePath) throws IOException {
InputStream stream = ImageFrame.plugin.getClass().getClassLoader().getResourceAsStream(resourcePath);
if (stream == null) {
throw new IOException("Missing bundled image resource in jar: " + resourcePath);
}
return stream;
}
public static BufferedImage readResourceImage(String resourcePath) throws IOException {
try (InputStream stream = openResource(resourcePath)) {
BufferedImage image = ImageIO.read(stream);
if (image == null) {
throw new IOException("Unable to decode bundled image resource in jar: " + resourcePath);
}
return image;
}
}
protected BundledStaticImageMap(ImageMapManager manager, ImageMapLoader<?, ?> loader, String name, String resourcePath, LazyMappedBufferedImage[] cachedImages, List<MapView> mapViews, List<Integer> mapIds, List<Map<String, MapCursor>> mapMarkers, int width, int height, DitheringType ditheringType, long creationTime) {
super(manager, loader, -1, name, resourcePath, cachedImages, mapViews, mapIds, mapMarkers, width, height, ditheringType, ImageMap.CONSOLE_CREATOR, Collections.emptyMap(), creationTime);
}
@Override
public String getResourcePath() {
return url;
}
@Override
public void setUrl(String url) {
//bundled maps always render from their jar resource, the path cannot change at runtime
}
@Override
public void update(boolean save) throws Exception {
BufferedImage image = readResourceImage(getResourcePath());
int hdMapWidth = image.getWidth() / width;
image = MapUtils.resize(image, width, height, hdMapWidth);
int i = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
cachedImages[i++] = StandardLazyMappedBufferedImage.fromImage(MapUtils.getSubImage(image, x, y, hdMapWidth));
}
}
reloadColorCache();
Bukkit.getPluginManager().callEvent(new ImageMapUpdatedEvent(this));
send(getViewers());
}
@Override
public void save(ImageFrameStorage storage, boolean saveAsCopy) {
//the jar is the canonical storage, nothing to save
}
@Override
public ImageMap deepClone(String name, UUID creator) {
throw new UnsupportedOperationException("bundled maps cannot be cloned");
}
}
@@ -0,0 +1,122 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonObject;
import com.loohp.imageframe.utils.FutureUtils;
import com.loohp.imageframe.utils.MapUtils;
import net.kyori.adventure.key.Key;
import org.bukkit.World;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapRenderer;
import org.bukkit.map.MapView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
public class BundledStaticImageMapLoader extends ImageMapLoader<BundledStaticImageMap, BundledMapCreateInfo> {
private static final Key IDENTIFIER = Key.key("imageframe", "bundled_static");
@Override
public Key getIdentifier() {
return IDENTIFIER;
}
@Override
public Class<BundledStaticImageMap> getImageMapClass() {
return BundledStaticImageMap.class;
}
@Override
public Class<BundledMapCreateInfo> getImageMapCreateInfoClass() {
return BundledMapCreateInfo.class;
}
@Override
public List<String> getExtraPermissions() {
return Collections.emptyList();
}
@Override
public boolean isSupported(String imageType) {
//bundled maps are only created from maps.yml in the jar, never through commands
return false;
}
/**
* Resolves the pinned map ids of a bundled map to their map views,
* creating any map id that does not exist in the world yet.
*/
public static List<MapView> getPinnedMapViews(BundledMapCreateInfo createInfo) throws Exception {
int mapsCount = createInfo.getWidth() * createInfo.getHeight();
List<Integer> pinnedMapIds = createInfo.getMapIds();
if (pinnedMapIds.size() != mapsCount) {
throw new IllegalArgumentException("MapIds size " + pinnedMapIds.size() + " does not equal Width * Height (" + mapsCount + ")");
}
World world = MapUtils.getMainWorld();
List<Future<MapView>> mapViewsFuture = new ArrayList<>(mapsCount);
for (int mapId : pinnedMapIds) {
mapViewsFuture.add(MapUtils.getMapOrCreateMissing(world, mapId));
}
List<MapView> mapViews = new ArrayList<>(mapsCount);
for (Future<MapView> future : mapViewsFuture) {
mapViews.add(future.get());
}
return mapViews;
}
@Override
public Future<BundledStaticImageMap> create(BundledMapCreateInfo createInfo) throws Exception {
int mapsCount = createInfo.getWidth() * createInfo.getHeight();
List<MapView> mapViews = getPinnedMapViews(createInfo);
List<Integer> mapIds = new ArrayList<>(mapsCount);
List<Map<String, MapCursor>> markers = new ArrayList<>(mapsCount);
for (MapView mapView : mapViews) {
mapIds.add(mapView.getId());
markers.add(new ConcurrentHashMap<>());
}
BundledStaticImageMap map = new BundledStaticImageMap(createInfo.getManager(), this, createInfo.getName(), createInfo.getResourcePath(), new LazyMappedBufferedImage[mapsCount], mapViews, mapIds, markers, createInfo.getWidth(), createInfo.getHeight(), createInfo.getDitheringType(), createInfo.getCreationTime());
return FutureUtils.callAsyncMethod(() -> {
FutureUtils.callSyncMethod(() -> {
for (int i = 0; i < mapViews.size(); i++) {
MapView mapView = mapViews.get(i);
for (MapRenderer renderer : mapView.getRenderers()) {
mapView.removeRenderer(renderer);
}
mapView.addRenderer(new URLStaticImageMap.URLStaticImageMapRenderer(map, i));
}
}).get();
map.update(false);
return map;
});
}
@Override
public Future<BundledStaticImageMap> load(ImageMapManager manager, JsonObject json) {
throw new UnsupportedOperationException("bundled maps are loaded from maps.yml in the plugin jar, not from storage");
}
}
@@ -0,0 +1,360 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.language.TranslationKey;
import com.loohp.imageframe.nms.NMS;
import com.loohp.imageframe.utils.CommandSenderUtils;
import com.loohp.imageframe.utils.ComponentUtils;
import com.loohp.imageframe.utils.ItemFrameUtils;
import com.loohp.imageframe.utils.PlayerUtils;
import com.loohp.imageframe.utils.SlotAccessor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.hanging.HangingBreakEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CombinedMapItemHandler implements Listener, AutoCloseable {
public static boolean containsCombinedMaps(SlotAccessor slotAccess, int size) {
return containsCombinedMaps(slotAccess, 0, size);
}
public static boolean containsCombinedMaps(SlotAccessor slotAccess, int begin, int size) {
for (int i = begin; i < size; i++) {
ItemStack itemStack = slotAccess.apply(i);
if (isCombinedMaps(itemStack)) {
return true;
}
}
return false;
}
public static boolean containsCombinedMaps(Collection<ItemStack> itemStacks) {
return itemStacks.stream().anyMatch(itemStack -> isCombinedMaps(itemStack));
}
public static boolean isCombinedMaps(ItemStack itemStack) {
if (itemStack != null && itemStack.getType().equals(Material.PAPER)) {
return NMS.getInstance().getCombinedMapItemInfo(itemStack) != null;
}
return false;
}
private final Set<Player> entityInteractionChecking;
private final Set<Player> entityDamageChecking;
private final Set<ItemFrame> itemFrameProcessing;
public CombinedMapItemHandler() {
this.entityInteractionChecking = new HashSet<>();
this.entityDamageChecking = new HashSet<>();
this.itemFrameProcessing = new HashSet<>();
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
}
public ItemStack getCombinedMap(ImageMap imageMap) {
ItemStack itemStack = new ItemStack(Material.PAPER);
ItemMeta meta = itemStack.getItemMeta();
Component name = ComponentUtils.translatable(
TranslationKey.COMBINED_ITEM_NAME,
Component.text(imageMap.getName()).color(NamedTextColor.YELLOW),
Component.text(imageMap.getWidth()).color(NamedTextColor.WHITE),
Component.text(imageMap.getHeight()).color(NamedTextColor.WHITE)
).color(NamedTextColor.AQUA);
meta.setDisplayName(LegacyComponentSerializer.legacySection().serialize(ImageFrame.languageManager.resolve(name, ImageFrame.language)));
Component lore1 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_1, imageMap.getWidth(), imageMap.getHeight()).color(NamedTextColor.GREEN);
Component lore2 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_2).color(NamedTextColor.GRAY);
Component lore3 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_3, imageMap.getImageIndex()).color(NamedTextColor.GRAY);
Component lore4 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_4, imageMap.getCreatorName(), imageMap.getCreator()).color(NamedTextColor.GRAY);
Component lore5 = ComponentUtils.translatable(TranslationKey.COMBINED_ITEM_LORE_5, ImageFrame.dateFormat.format(new Date(imageMap.getCreationTime()))).color(NamedTextColor.GRAY);
meta.setLore(Stream.of(lore1, lore2, lore3, lore4, lore5).map(c -> LegacyComponentSerializer.legacySection().serialize(ImageFrame.languageManager.resolve(c, ImageFrame.language))).collect(Collectors.toList()));
itemStack.setItemMeta(meta);
return NMS.getInstance().withCombinedMapItemInfo(itemStack, new CombinedMapItemInfo(imageMap.getImageIndex()));
}
public void giveCombinedMap(ImageMap imageMap, Player player) {
giveCombinedMap(imageMap, Collections.singleton(player));
}
public void giveCombinedMap(ImageMap imageMap, Collection<? extends Player> players) {
ItemStack map = getCombinedMap(imageMap);
for (Player player : players) {
PlayerUtils.giveItem(player, map.clone());
}
}
@Override
public void close() {
HandlerList.unregisterAll(this);
}
public ItemFrameSelectionManager.SelectedItemFrameResult findItemFrames(ItemFrame itemFrame, float yaw, int width, int height, Predicate<ItemStack> itemCheck) {
Vector up;
Vector left;
BlockFace facing = itemFrame.getFacing();
if (ItemFrameUtils.isOnWalls(itemFrame)) {
up = new Vector(0, 1, 0);
left = facing.getDirection().rotateAroundY(Math.toRadians(-90));
} else if (ItemFrameUtils.isOnCeiling(itemFrame)) {
up = ItemFrameUtils.getClosestCardinalDirection(yaw + 180F);
left = up.clone().rotateAroundY(Math.toRadians(-90));
} else {
up = ItemFrameUtils.getClosestCardinalDirection(yaw);
left = up.clone().rotateAroundY(Math.toRadians(90));
}
List<Location> checkingLocations = getCheckingLocations(itemFrame.getLocation(), up, left, width, height);
outer: for (Location location : checkingLocations) {
itemFrame = getItemFrame(location, facing);
if (itemFrame == null) {
continue;
}
ItemFrame opposite = getOppositeItemFrameCorner(itemFrame, up, left, width, height);
if (opposite == null) {
continue;
}
ItemFrameSelectionManager.SelectedItemFrameResult result = ImageFrame.itemFrameSelectionManager.getSelectedItemFrames(yaw, itemFrame, opposite);
if (result == null) {
continue;
}
List<ItemFrame> selectedFrames = result.getItemFrames();
if (selectedFrames.size() != width * height) {
continue;
}
for (ItemFrame frame : selectedFrames) {
ItemStack item = frame.getItem();
if (!itemCheck.test(item)) {
continue outer;
}
}
return result;
}
return null;
}
public ItemFrame getOppositeItemFrameCorner(ItemFrame itemFrame, Vector up, Vector left, int width, int height) {
Location location = itemFrame.getLocation().add(left.clone().multiply(-(width - 1))).add(up.clone().multiply(-(height - 1)));
BoundingBox boundingBox = BoundingBox.of(location.getBlock(), location.getBlock());
Collection<Entity> entities = location.getWorld().getNearbyEntities(boundingBox, e -> e instanceof ItemFrame);
for (Entity entity : entities) {
if (itemFrame.getFacing().equals(entity.getFacing())) {
return (ItemFrame) entity;
}
}
return null;
}
public ItemFrame getItemFrame(Location location, BlockFace facing) {
BoundingBox boundingBox = BoundingBox.of(location.getBlock(), location.getBlock());
Collection<Entity> entities = location.getWorld().getNearbyEntities(boundingBox, e -> e instanceof ItemFrame);
for (Entity entity : entities) {
if (facing.equals(entity.getFacing())) {
return (ItemFrame) entity;
}
}
return null;
}
public List<Location> getCheckingLocations(Location origin, Vector up, Vector left, int width, int height) {
List<Location> locations = new ArrayList<>(width * height);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
locations.add(origin.clone().add(up.clone().multiply(y)).add(left.clone().multiply(x)));
}
}
locations.sort(Comparator.comparing(each -> each.distanceSquared(origin)));
return locations;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInteractEntity(PlayerInteractEntityEvent event) {
Player player = event.getPlayer();
if (!entityInteractionChecking.add(player)) {
return;
}
try {
EquipmentSlot slot = event.getHand();
ItemStack itemStack = player.getEquipment().getItem(slot);
if (itemStack == null || !itemStack.getType().equals(Material.PAPER)) {
return;
}
CombinedMapItemInfo mapItemInfo = NMS.getInstance().getCombinedMapItemInfo(itemStack);
if (mapItemInfo == null) {
return;
}
Entity entity = event.getRightClicked();
if (!(entity instanceof ItemFrame)) {
event.setCancelled(true);
return;
}
ItemFrame itemFrame = (ItemFrame) entity;
int id = mapItemInfo.getImageMapIndex();
ImageMap imageMap = ImageFrame.imageMapManager.getFromImageId(id);
if (imageMap == null) {
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.INVALID_IMAGE_MAP).color(NamedTextColor.RED));
event.setCancelled(true);
return;
}
float yaw = player.getLocation().getYaw();
ItemFrameSelectionManager.SelectedItemFrameResult selection = findItemFrames(itemFrame, yaw, imageMap.getWidth(), imageMap.getHeight(), item -> item == null || item.getType().equals(Material.AIR));
if (selection == null) {
event.setCancelled(true);
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.NOT_ENOUGH_SPACE).color(NamedTextColor.RED));
return;
}
List<ItemFrame> selectedFrames = selection.getItemFrames();
for (ItemFrame frame : selectedFrames) {
ItemStack item = frame.getItem();
if ((item != null && !item.getType().equals(Material.AIR)) || !PlayerUtils.isInteractionAllowed(player, frame)) {
event.setCancelled(true);
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.ITEM_FRAME_OCCUPIED).color(NamedTextColor.RED));
return;
}
}
itemFrameProcessing.addAll(selectedFrames);
UUID uuid = UUID.randomUUID();
imageMap.fillItemFrames(selectedFrames, selection.getRotation(), (frame, item) -> true, (frame, item) -> {}, ImageFrame.mapItemFormat, item -> {
CombinedMapItemInfo newInfo = new CombinedMapItemInfo(imageMap.getImageIndex(), new CombinedMapItemInfo.PlacementInfo(yaw, uuid));
return NMS.getInstance().withCombinedMapItemInfo(item, newInfo);
}, () -> selectedFrames.forEach(f -> itemFrameProcessing.remove(f)));
} finally {
entityInteractionChecking.remove(player);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDamageHanging(EntityDamageEvent event) {
Entity entity = event.getEntity();
if (!(entity instanceof ItemFrame)) {
return;
}
ItemFrame itemFrame = (ItemFrame) entity;
if (itemFrameProcessing.contains(itemFrame)) {
event.setCancelled(true);
return;
}
Player player = null;
if (event instanceof EntityDamageByEntityEvent) {
EntityDamageByEntityEvent entityDamageByEntityEvent = (EntityDamageByEntityEvent) event;
Entity damager = entityDamageByEntityEvent.getDamager();
if (damager instanceof Player) {
player = (Player) damager;
}
}
handleItemFrameBreak(player, itemFrame);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onRemoveHanging(HangingBreakEvent event) {
Entity entity = event.getEntity();
if (!(entity instanceof ItemFrame)) {
return;
}
ItemFrame itemFrame = (ItemFrame) entity;
if (itemFrameProcessing.contains(itemFrame)) {
event.setCancelled(true);
return;
}
handleItemFrameBreak(null, itemFrame);
}
public void handleItemFrameBreak(Player player, ItemFrame itemFrame) {
if (player != null && !entityDamageChecking.add(player)) {
return;
}
try {
ItemStack itemStack = itemFrame.getItem();
if (itemStack == null || !itemStack.getType().equals(Material.FILLED_MAP)) {
return;
}
CombinedMapItemInfo mapItemInfo = NMS.getInstance().getCombinedMapItemInfo(itemStack);
if (mapItemInfo == null || !mapItemInfo.hasPlacement()) {
return;
}
CombinedMapItemInfo.PlacementInfo placement = mapItemInfo.getPlacement();
int id = mapItemInfo.getImageMapIndex();
float yaw = placement.getYaw();
UUID uuid = placement.getUniqueId();
ImageMap imageMap = ImageFrame.imageMapManager.getFromImageId(id);
if (imageMap == null) {
return;
}
ItemFrameSelectionManager.SelectedItemFrameResult selection = findItemFrames(itemFrame, yaw, imageMap.getWidth(), imageMap.getHeight(), item -> {
if (item == null || !item.getType().equals(Material.FILLED_MAP)) {
return false;
}
CombinedMapItemInfo info = NMS.getInstance().getCombinedMapItemInfo(item);
if (info == null || !info.hasPlacement()) {
return false;
}
return info.getImageMapIndex() == id && info.getPlacement().getUniqueId().equals(uuid);
});
if (selection == null) {
return;
}
List<ItemFrame> itemFrames = selection.getItemFrames();
if (player == null || itemFrames.stream().allMatch(each -> PlayerUtils.isDamageAllowed(player, each))) {
itemFrames.forEach(each -> each.setItem(null, false));
itemFrame.setItem(getCombinedMap(imageMap), false);
} else {
CommandSenderUtils.sendMessage(player, Component.translatable(TranslationKey.ITEM_FRAME_OCCUPIED).color(NamedTextColor.RED));
}
} finally {
entityDamageChecking.remove(player);
}
}
}
@@ -0,0 +1,47 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.utils.ComponentUtils;
import net.kyori.adventure.text.TranslatableComponent;
public class ComponentTemplate {
public static ComponentTemplate template(String key, Object... arguments) {
return new ComponentTemplate(key, arguments);
}
private final String key;
private final Object[] arguments;
private ComponentTemplate(String key, Object... arguments) {
this.key = key;
this.arguments = arguments;
}
public TranslatableComponent build(Object... extraArguments) {
Object[] args = new Object[arguments.length + extraArguments.length];
System.arraycopy(arguments, 0, args, 0, arguments.length);
System.arraycopy(extraArguments, 0, args, arguments.length, extraArguments.length);
return ComponentUtils.translatable(key, args);
}
}
@@ -0,0 +1,320 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.api.events.HDMapPreRespondEvent;
import com.loohp.imageframe.api.events.ImageMapDeletedEvent;
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
import com.loohp.imageframe.utils.MapUtils;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.map.MapView;
import org.bukkit.plugin.messaging.PluginMessageListener;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class CustomClientNetworkManager implements PluginMessageListener, Listener {
public static final String CLIENTBOUND_ACKNOWLEDGEMENT = "imageframe:clientbound_ack";
public static final String SERVERBOUND_ACKNOWLEDGEMENT = "imageframe:serverbound_ack";
public static final String SERVERBOUND_HD_IMAGE_REQUEST = "imageframe:serverbound_hd_image";
public static final String CLIENTBOUND_HD_IMAGE_RESPONSE = "imageframe:clientbound_hd_image";
public static final String CLIENTBOUND_HD_IMAGE_MULTIPART_RESPONSE = "imageframe:clientbound_hd_image_multi";
public static final String CLIENTBOUND_HD_UPDATE_SIGNAL = "imageframe:clientbound_update";
public static final String SERVERBOUND_IMAGEMAP_DETAILS_REQUEST = "imageframe:serverbound_imagemap_details";
public static final String CLIENTBOUND_IMAGEMAP_DETAILS_RESPONSE = "imageframe:clientbound_imagemap_details";
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private final Map<UUID, Long> acknowledgements;
private final Set<UUID> acknowledged;
public CustomClientNetworkManager(boolean enabled) {
this.acknowledgements = new ConcurrentHashMap<>();
this.acknowledged = ConcurrentHashMap.newKeySet();
if (!enabled) {
return;
}
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_ACKNOWLEDGEMENT);
Bukkit.getServer().getMessenger().registerIncomingPluginChannel(ImageFrame.plugin, SERVERBOUND_ACKNOWLEDGEMENT, this);
Bukkit.getServer().getMessenger().registerIncomingPluginChannel(ImageFrame.plugin, SERVERBOUND_HD_IMAGE_REQUEST, this);
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_RESPONSE);
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_MULTIPART_RESPONSE);
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_HD_UPDATE_SIGNAL);
Bukkit.getServer().getMessenger().registerIncomingPluginChannel(ImageFrame.plugin, SERVERBOUND_IMAGEMAP_DETAILS_REQUEST, this);
Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(ImageFrame.plugin, CLIENTBOUND_IMAGEMAP_DETAILS_RESPONSE);
Bukkit.getServer().getPluginManager().registerEvents(this, ImageFrame.plugin);
}
public Set<UUID> getPlayers() {
return Collections.unmodifiableSet(acknowledged);
}
public boolean hasPlayer(Player player) {
return hasPlayer(player.getUniqueId());
}
public boolean hasPlayer(UUID uuid) {
return acknowledged.contains(uuid);
}
public void sendAcknowledgement(Player player) {
long id = ThreadLocalRandom.current().nextLong();
acknowledgements.put(player.getUniqueId(), id);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeLong(id);
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_ACKNOWLEDGEMENT, out.toByteArray());
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Scheduler.runTaskLater(ImageFrame.plugin, () -> {
if (player.isOnline()) {
sendAcknowledgement(player);
}
}, 40);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
acknowledgements.remove(uuid);
acknowledged.remove(uuid);
}
@EventHandler
public void onUpdate(ImageMapUpdatedEvent event) {
ImageMap imageMap = event.getImageMap();
notifyHdMapUpdated(acknowledged, Collections.singletonList(imageMap), imageMap.getMapIds());
}
@EventHandler
public void onDelete(ImageMapDeletedEvent event) {
ImageMap imageMap = event.getImageMap();
notifyHdMapUpdated(acknowledged, Collections.singletonList(imageMap), imageMap.getMapIds());
}
public void notifyHdMapUpdated(Collection<UUID> players, Collection<ImageMap> imageMaps, Collection<Integer> mapIds) {
handle(() -> {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
writeVarInt(out, imageMaps.size());
for (ImageMap imageMap : imageMaps) {
out.writeInt(imageMap.getImageIndex());
}
writeVarInt(out, mapIds.size());
for (int mapId : mapIds) {
out.writeInt(mapId);
}
return out.toByteArray();
}, () -> true, out -> {
for (UUID uuid : players) {
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_HD_UPDATE_SIGNAL, out);
}
}
});
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] data) {
try {
switch (channel) {
case SERVERBOUND_ACKNOWLEDGEMENT: {
ByteArrayDataInput in = ByteStreams.newDataInput(data);
long id = in.readLong();
UUID uuid = player.getUniqueId();
if (acknowledgements.get(uuid) == id) {
acknowledgements.remove(uuid);
acknowledged.add(uuid);
}
break;
}
case SERVERBOUND_HD_IMAGE_REQUEST: {
ByteArrayDataInput in = ByteStreams.newDataInput(data);
int mapId = in.readInt();
ImageMap imageMap = ImageFrame.imageMapManager.getFromMapId(mapId);
handle(() -> {
List<byte[]> list = new ArrayList<>();
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeInt(mapId);
try {
MapView mapView = imageMap == null ? null : imageMap.getMapViewFromMapId(mapId);
boolean requestAccepted = imageMap == null || MapUtils.canViewMap(player, mapView).get();
BufferedImage image = imageMap == null ? null : imageMap.getOriginalImage(mapId);
HDMapPreRespondEvent event = new HDMapPreRespondEvent(player, mapId, imageMap, mapView, requestAccepted, image);
Bukkit.getPluginManager().callEvent(event);
requestAccepted = event.isRequestAccepted();
image = event.getImage();
out.writeBoolean(requestAccepted);
if (image == null) {
writeVarInt(out, 0);
out.writeBoolean(false);
} else {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] array;
try {
ImageIO.write(image, "png", byteArrayOutputStream);
array = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
array = EMPTY_BYTE_ARRAY;
}
if (array.length == 0) {
writeVarInt(out, 0);
out.writeBoolean(false);
} else {
List<byte[]> chunked = chunked(array, 32000);
writeVarInt(out, chunked.get(0).length);
out.write(chunked.get(0));
if (chunked.size() <= 1) {
out.writeBoolean(false);
} else {
int multipartId = ThreadLocalRandom.current().nextInt();
out.writeBoolean(true);
out.writeInt(multipartId);
for (int i = 1; i < chunked.size(); i++) {
ByteArrayDataOutput outMulti = ByteStreams.newDataOutput();
outMulti.writeInt(mapId);
outMulti.writeInt(multipartId);
outMulti.writeInt(i);
writeVarInt(outMulti, chunked.get(i).length);
outMulti.write(chunked.get(i));
outMulti.writeBoolean(i + 1 >= chunked.size());
list.add(outMulti.toByteArray());
}
}
}
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
list.add(0, out.toByteArray());
return list;
}, () -> player.isOnline(), out -> {
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_RESPONSE, out.get(0));
for (int i = 1; i < out.size(); i++) {
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_HD_IMAGE_MULTIPART_RESPONSE, out.get(i));
}
});
break;
}
case SERVERBOUND_IMAGEMAP_DETAILS_REQUEST: {
ByteArrayDataInput in = ByteStreams.newDataInput(data);
int index = in.readInt();
ImageMap imageMap = ImageFrame.imageMapManager.getFromImageId(index);
handle(() -> {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeInt(index);
if (imageMap == null) {
out.writeInt(0);
out.writeInt(0);
writeVarInt(out, 0);
} else {
out.writeInt(imageMap.getWidth());
out.writeInt(imageMap.getHeight());
writeVarInt(out, imageMap.getMapIds().size());
for (int mapId : imageMap.getMapIds()) {
out.writeInt(mapId);
}
}
return out.toByteArray();
}, () -> player.isOnline(), out -> {
player.sendPluginMessage(ImageFrame.plugin, CLIENTBOUND_IMAGEMAP_DETAILS_RESPONSE, out);
if (imageMap != null) {
imageMap.send(player);
}
});
break;
}
}
} catch (Throwable e) {
new RuntimeException("Illegal payload received from " + player.getName() + " on channel " + channel, e).printStackTrace();
}
}
private static void writeVarInt(ByteArrayDataOutput out, int i) {
while ((i & -128) != 0) {
out.writeByte(i & 127 | 128);
i >>>= 7;
}
out.writeByte(i);
}
private static <T> void handle(Supplier<T> handleAsync, BooleanSupplier syncPredicate, Consumer<T> completeSync) {
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
T value = handleAsync.get();
if (syncPredicate.getAsBoolean()) {
Scheduler.runTask(ImageFrame.plugin, () -> completeSync.accept(value));
}
});
}
private static List<byte[]> chunked(byte[] data, int chunkSize) {
if (data.length <= chunkSize) {
return Collections.singletonList(data);
}
List<byte[]> chunks = new ArrayList<>();
for (int i = 0; i < data.length; i += chunkSize) {
int end = Math.min(data.length, i + chunkSize);
byte[] chunk = Arrays.copyOfRange(data, i, end);
chunks.add(chunk);
}
return chunks;
}
}
@@ -0,0 +1,73 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.utils.dithering.FloydSteinbergDithering;
import org.bukkit.map.MapPalette;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
public class DitheringType {
private static final Map<String, DitheringType> REGISTERED_TYPES = new LinkedHashMap<>();
@SuppressWarnings("removal")
public static final DitheringType NEAREST_COLOR = register(new DitheringType("nearest-color", image -> MapPalette.imageToBytes(image)));
public static final DitheringType FLOYD_STEINBERG = register(new DitheringType("floyd-steinberg", image -> FloydSteinbergDithering.floydSteinbergDithering(image)));
public static DitheringType register(DitheringType ditheringType) {
REGISTERED_TYPES.put(ditheringType.getName(), ditheringType);
return ditheringType;
}
public static Map<String, DitheringType> values() {
return Collections.unmodifiableMap(REGISTERED_TYPES);
}
public static DitheringType fromName(String name) {
return REGISTERED_TYPES.getOrDefault(name, NEAREST_COLOR);
}
public static DitheringType fromNameOrNull(String name) {
return REGISTERED_TYPES.get(name);
}
private final String name;
private final Function<BufferedImage, byte[]> applyDithering;
public DitheringType(String name, Function<BufferedImage, byte[]> applyDithering) {
this.name = name;
this.applyDithering = applyDithering;
}
public String getName() {
return name;
}
public byte[] applyDithering(BufferedImage image) {
return applyDithering.apply(image);
}
}
@@ -0,0 +1,134 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class IFPlayer {
public static IFPlayer create(IFPlayerManager manager, UUID player) throws Exception {
IFPlayer ifPlayer = new IFPlayer(manager, player, Collections.emptyMap());
ifPlayer.save();
return ifPlayer;
}
public static IFPlayer load(IFPlayerManager manager, JsonObject json) {
UUID uuid = UUID.fromString(json.get("uuid").getAsString());
Map<IFPlayerPreference<?>, PreferenceState> preferences = new HashMap<>();
JsonObject preferenceJson = json.get("preferences").getAsJsonObject();
for (IFPlayerPreference<?> preference : IFPlayerPreference.values()) {
JsonElement element = preferenceJson.get(preference.getJsonName());
if (element != null) {
preferences.put(preference, preference.getDeserializer().apply(element));
}
}
return new IFPlayer(manager, uuid, preferences);
}
private final IFPlayerManager manager;
private final UUID uuid;
private final Map<IFPlayerPreference<?>, PreferenceState> preferences;
private IFPlayer(IFPlayerManager manager, UUID uuid, Map<IFPlayerPreference<?>, PreferenceState> preferences) {
this.manager = manager;
this.uuid = uuid;
this.preferences = new ConcurrentHashMap<>(preferences);
}
public void applyUpdate(JsonObject json) {
Map<IFPlayerPreference<?>, PreferenceState> preferences = new HashMap<>();
JsonObject preferenceJson = json.get("preferences").getAsJsonObject();
for (IFPlayerPreference<?> preference : IFPlayerPreference.values()) {
JsonElement element = preferenceJson.get(preference.getJsonName());
if (element != null) {
preferences.put(preference, preference.getDeserializer().apply(element));
}
}
this.preferences.clear();
this.preferences.putAll(preferences);
}
public OfflinePlayer getLocalPlayer() {
return Bukkit.getOfflinePlayer(uuid);
}
public String getName() {
return getLocalPlayer().getName();
}
public UUID getUniqueId() {
return uuid;
}
public PreferenceState getPreference(IFPlayerPreference<?> preference) {
return preferences.computeIfAbsent(preference, k -> preference.getDefaultValue(this));
}
@SuppressWarnings("unchecked")
public <T extends PreferenceState> T getPreference(IFPlayerPreference<?> preference, Class<T> type) {
return (T) preferences.computeIfAbsent(preference, k -> preference.getDefaultValue(this, type));
}
public void setPreference(IFPlayerPreference<?> preference, PreferenceState value) {
preferences.put(preference, value);
saveInternal();
}
private void saveInternal() {
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
try {
save();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
public void save() throws Exception {
save(manager.getStorage());
}
public void save(ImageFrameStorage storage) throws Exception {
JsonObject json = new JsonObject();
json.addProperty("uuid", uuid.toString());
JsonObject preferenceJson = new JsonObject();
for (Map.Entry<IFPlayerPreference<?>, PreferenceState> entry : preferences.entrySet()) {
IFPlayerPreference<?> preference = entry.getKey();
preferenceJson.add(preference.getJsonName(), preference.getSerializer(PreferenceState.class).apply(entry.getValue()));
}
json.add("preferences", preferenceJson);
storage.savePlayerData(uuid, json);
}
}
@@ -0,0 +1,107 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.JsonObject;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.platformscheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class IFPlayerManager implements AutoCloseable, Listener {
private final ImageFrameStorage imageFrameStorage;
private final Map<UUID, IFPlayer> loadedPlayers;
private final Set<IFPlayer> persistentLoadedPlayers;
public IFPlayerManager(ImageFrameStorage imageFrameStorage) {
this.imageFrameStorage = imageFrameStorage;
Cache<UUID, IFPlayer> playersCache = CacheBuilder.newBuilder().weakValues().build();
this.loadedPlayers = playersCache.asMap();
this.persistentLoadedPlayers = ConcurrentHashMap.newKeySet();
Bukkit.getPluginManager().registerEvents(this, ImageFrame.plugin);
}
public ImageFrameStorage getStorage() {
return imageFrameStorage;
}
@Override
public void close() {
HandlerList.unregisterAll(this);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onJoin(PlayerJoinEvent event) {
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> persistentLoadedPlayers.add(getIFPlayer(event.getPlayer().getUniqueId())));
}
@EventHandler(priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent event) {
IFPlayer ifPlayer = loadedPlayers.get(event.getPlayer().getUniqueId());
if (ifPlayer != null) {
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
try {
ifPlayer.save();
} catch (Exception e) {
e.printStackTrace();
}
persistentLoadedPlayers.remove(ifPlayer);
});
}
}
public IFPlayer getIFPlayer(UUID uuid) {
return loadedPlayers.computeIfAbsent(uuid, id -> {
JsonObject json = imageFrameStorage.loadPlayerData(this, id);
if (json != null) {
return IFPlayer.load(this, json);
}
return createNewIfPlayer(id);
});
}
public IFPlayer getIFPlayerIfLoaded(UUID uuid) {
return loadedPlayers.get(uuid);
}
private IFPlayer createNewIfPlayer(UUID uuid) {
try {
return IFPlayer.create(this, uuid);
} catch (Exception e) {
throw new RuntimeException("Unable to create ImageFrame player data for " + uuid, e);
}
}
}
@@ -0,0 +1,172 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonElement;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
public class IFPlayerPreference<T extends PreferenceState> {
private static final Map<String, IFPlayerPreference<?>> VALUES = new ConcurrentHashMap<>();
public static final IFPlayerPreference<BooleanState> VIEW_ANIMATED_MAPS = register(new IFPlayerPreference<>("VIEW_ANIMATED_MAPS", "viewAnimatedMaps", BooleanState.class, () -> BooleanState.STRING_VALUES_MAP, i -> BooleanState.UNSET, v -> v.getJsonValue(), j -> BooleanState.fromJsonValue(j), s -> StringDeserializerResult.accepted(BooleanState.fromString(s))));
public static final IFPlayerPreference<LanguageState> LANGUAGE = register(new IFPlayerPreference<>("LANGUAGE", "language", LanguageState.class, LanguageState.VALUES_MAP_SUPPLIER, i -> LanguageState.UNSET, v -> v.getJsonValue(), j -> LanguageState.fromJsonValue(j), s -> StringDeserializerResult.accepted(LanguageState.fromString(s))));
public static <T extends IFPlayerPreference<?>> T register(T preference) {
VALUES.put(preference.name(), preference);
return preference;
}
public static Collection<IFPlayerPreference<?>> values() {
return Collections.unmodifiableCollection(VALUES.values());
}
public static IFPlayerPreference<?> valueOf(String name) {
return VALUES.get(name.toUpperCase());
}
private final String name;
private final String jsonName;
private final Class<T> type;
private final Supplier<Map<String, T>> suggestedValues;
private final Function<IFPlayer, T> defaultValue;
private final Function<T, JsonElement> serializer;
private final Function<JsonElement, T> deserializer;
private final Function<String, StringDeserializerResult<T>> stringDeserializer;
IFPlayerPreference(String name, String jsonName, Class<T> type, Supplier<Map<String, T>> suggestedValues, Function<IFPlayer, T> defaultValue, Function<T, JsonElement> serializer, Function<JsonElement, T> deserializer, Function<String, StringDeserializerResult<T>> stringDeserializer) {
this.name = name;
this.jsonName = jsonName;
this.type = type;
this.suggestedValues = suggestedValues;
this.defaultValue = defaultValue;
this.serializer = serializer;
this.deserializer = deserializer;
this.stringDeserializer = stringDeserializer;
}
public String name() {
return name;
}
public String getJsonName() {
return jsonName;
}
public Class<T> getType() {
return type;
}
public Map<String, T> getSuggestedValues() {
return suggestedValues.get();
}
public Function<T, JsonElement> getSerializer() {
return serializer;
}
@SuppressWarnings("unchecked")
public <E extends PreferenceState> Function<E, JsonElement> getSerializer(Class<E> type) {
return (Function<E, JsonElement>) serializer;
}
public Function<JsonElement, T> getDeserializer() {
return deserializer;
}
@SuppressWarnings("unchecked")
public <E extends PreferenceState> Function<JsonElement, E> getDeserializer(Class<E> type) {
return (Function<JsonElement, E>) deserializer;
}
public Function<String, StringDeserializerResult<T>> getStringDeserializer() {
return stringDeserializer;
}
@SuppressWarnings("unchecked")
public <E extends PreferenceState> Function<String, StringDeserializerResult<E>> getStringDeserializer(Class<E> type) {
return (Function<String, StringDeserializerResult<E>>) (Function<String, ?>) stringDeserializer;
}
public T getDefaultValue(IFPlayer ifPlayer) {
return defaultValue.apply(ifPlayer);
}
@SuppressWarnings("unchecked")
public <E extends PreferenceState> E getDefaultValue(IFPlayer ifPlayer, Class<E> type) {
return (E) defaultValue.apply(ifPlayer);
}
@Override
public String toString() {
return name();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IFPlayerPreference<?> that = (IFPlayerPreference<?>) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
public static class StringDeserializerResult<T extends PreferenceState> {
public static StringDeserializerResult<?> REJECTED_INSTANCE = new StringDeserializerResult<>(false, null);
@SuppressWarnings("unchecked")
public static <T extends PreferenceState> StringDeserializerResult<T> rejected() {
return (StringDeserializerResult<T>) REJECTED_INSTANCE;
}
public static <T extends PreferenceState> StringDeserializerResult<T> accepted(T result) {
return new StringDeserializerResult<>(true, result);
}
private final boolean accepted;
private final T value;
private StringDeserializerResult(boolean accepted, T value) {
this.accepted = accepted;
this.value = value;
}
public boolean isAccepted() {
return accepted;
}
public T getValue() {
return value;
}
}
}
@@ -0,0 +1,557 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.nms.NMS;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.imageframe.utils.MapUtils;
import com.loohp.imageframe.utils.PlayerUtils;
import com.loohp.imageframe.utils.StringUtils;
import com.loohp.platformscheduler.Scheduler;
import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.key.Key;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Rotation;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapCanvas;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapRenderer;
import org.bukkit.map.MapView;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
public abstract class ImageMap {
public static final UUID CONSOLE_CREATOR = new UUID(0, 0);
public static final String CONSOLE_CREATOR_NAME = "Console";
public static final String UNKNOWN_CREATOR_NAME = "???";
protected final ImageMapManager manager;
protected final ImageMapLoader<?, ?> loader;
protected int imageIndex;
protected String name;
protected final List<MapView> mapViews;
protected final List<Integer> mapIds;
protected final List<Map<String, MapCursor>> mapMarkers;
protected final int width;
protected final int height;
protected DitheringType ditheringType;
protected UUID creator;
protected ImageMapAccessControl accessControl;
protected final long creationTime;
protected final ImageMapCacheControlTask cacheControlTask;
private boolean isValid;
public ImageMap(ImageMapManager manager, ImageMapLoader<?, ?> loader, int imageIndex, String name, List<MapView> mapViews, List<Integer> mapIds, List<Map<String, MapCursor>> mapMarkers, int width, int height, DitheringType ditheringType, UUID creator, Map<UUID, ImageMapAccessPermissionType> hasAccess, long creationTime) {
if (mapViews.size() != width * height) {
throw new IllegalArgumentException("mapViews size does not equal width * height");
}
if (mapViews.size() != mapMarkers.size()) {
throw new IllegalArgumentException("mapViews size does not equal mapMarkers size");
}
if (mapViews.size() != mapIds.size()) {
throw new IllegalArgumentException("mapViews size does not equal mapIds size");
}
this.manager = manager;
this.loader = loader;
this.imageIndex = imageIndex;
this.name = StringUtils.sanitize(name);
this.mapViews = Collections.unmodifiableList(mapViews);
this.mapIds = Collections.unmodifiableList(mapIds);
this.mapMarkers = Collections.unmodifiableList(mapMarkers);
this.width = width;
this.height = height;
this.ditheringType = ditheringType;
this.creator = creator;
this.accessControl = new ImageMapAccessControl(this, hasAccess);
this.creationTime = creationTime;
this.cacheControlTask = ImageFrame.cacheControlMode.newInstance(this);
this.isValid = true;
this.accessControl.setPermissionWithoutSave(creator, null);
}
public ImageMapManager getManager() {
return manager;
}
public Key getType() {
return loader.getIdentifier();
}
@Deprecated
public String getLegacyType() {
return loader.getLegacyType();
}
public boolean applyUpdate(JsonObject json) {
this.name = json.has("name") ? json.get("name").getAsString() : "Unnamed";
this.creator = UUID.fromString(json.get("creator").getAsString());
DitheringType previousDitheringType = ditheringType;
this.ditheringType = DitheringType.fromName(json.has("ditheringType") ? json.get("ditheringType").getAsString() : null);
if (json.has("hasAccess")) {
JsonObject accessJson = json.get("hasAccess").getAsJsonObject();
Map<UUID, ImageMapAccessPermissionType> hasAccess = new HashMap<>(accessJson.size());
for (Map.Entry<String, JsonElement> entry : accessJson.entrySet()) {
hasAccess.put(UUID.fromString(entry.getKey()), ImageMapAccessPermissionType.valueOf(entry.getValue().getAsString().toUpperCase()));
}
this.accessControl.reapply(hasAccess);
}
JsonArray mapDataJson = json.get("mapdata").getAsJsonArray();
int i = 0;
for (JsonElement dataJson : mapDataJson) {
JsonObject jsonObject = dataJson.getAsJsonObject();
Map<String, MapCursor> mapCursors = new ConcurrentHashMap<>();
if (jsonObject.has("markers")) {
JsonArray markerArray = jsonObject.get("markers").getAsJsonArray();
for (JsonElement element : markerArray) {
JsonObject markerData = element.getAsJsonObject();
String markerName = markerData.get("name").getAsString();
byte x = markerData.get("x").getAsByte();
byte y = markerData.get("y").getAsByte();
MapCursor.Type type = MapCursor.Type.valueOf(markerData.get("type").getAsString().toUpperCase());
byte direction = markerData.get("direction").getAsByte();
boolean visible = markerData.get("visible").getAsBoolean();
JsonElement caption = markerData.get("caption");
mapCursors.put(markerName, new MapCursor(x, y, direction, type, visible, caption.isJsonNull() ? null : caption.getAsString()));
}
}
Map<String, MapCursor> markers = this.mapMarkers.get(i++);
markers.clear();
markers.putAll(mapCursors);
}
return previousDitheringType != ditheringType;
}
protected abstract void loadColorCache();
protected void reloadColorCache() {
if (hasColorCached()) {
loadColorCache();
} else {
cacheControlTask.loadCacheIfManual();
}
}
protected abstract boolean hasColorCached();
protected abstract void unloadColorCache();
public BufferedImage getOriginalImage(int mapId) {
return null;
}
public int getImageIndex() {
return imageIndex;
}
public String getName() {
return name;
}
public void rename(String name) throws Exception {
this.name = StringUtils.sanitize(name);
save();
}
public List<Integer> getMapIds() {
return mapIds;
}
protected void markInvalid() {
this.isValid = false;
this.cacheControlTask.close();
}
public boolean isValid() {
return isValid;
}
public void stop() {
for (MapView mapView : mapViews) {
for (MapRenderer mapRenderer : mapView.getRenderers()) {
mapView.removeRenderer(mapRenderer);
}
}
}
public boolean requiresAnimationService() {
return false;
}
public int getCurrentPositionInSequenceWithOffset() {
return 0;
}
public boolean isAnimationPaused() {
throw new UnsupportedOperationException("this map does not require animation");
}
public void setAnimationPause(boolean pause) throws Exception {
throw new UnsupportedOperationException("this map does not require animation");
}
public void setCurrentPositionInSequence(int position) {
//do nothing
}
public void setAnimationPlaybackTime(double seconds) throws Exception {
//do nothing
}
public int getCurrentPositionInSequence() {
return getCurrentPositionInSequenceWithOffset() % getSequenceLength();
}
public int getSequenceLength() {
return 1;
}
public byte[] getRawAnimationColors(int currentTick, int index) {
throw new UnsupportedOperationException("this map does not require animation");
}
public int getAnimationFakeMapId(int currentTick, int index, boolean lookbehind) {
throw new UnsupportedOperationException("this map does not require animation");
}
public void sendAnimationFakeMaps(Collection<? extends Player> players, MapPacketSentCallback completionCallback) {
throw new UnsupportedOperationException("this map does not require animation");
}
public Set<Integer> getFakeMapIds() {
throw new UnsupportedOperationException("this map does not require animation");
}
public boolean trackDeletedMaps() {
return true;
}
public DitheringType getDitheringType() {
return ditheringType == null ? DitheringType.NEAREST_COLOR : ditheringType;
}
public void setDitheringType(DitheringType ditheringType) throws Exception {
this.ditheringType = ditheringType;
save();
}
public abstract ImageMap deepClone(String name, UUID creator) throws Exception;
public abstract void update(boolean save) throws Exception;
public void update() throws Exception {
update(true);
}
public void send(Player player) {
send(Collections.singleton(player));
}
public void send(Collection<? extends Player> players) {
for (MapView mapView : mapViews) {
MapUtils.sendImageMap(mapView, players);
}
}
public void save() throws Exception {
save(manager.getStorage(), false);
}
public abstract void save(ImageFrameStorage storage, boolean saveAsCopy) throws Exception;
public ItemStack getMap(int x, int y, String mapNameFormat) {
return getMap(x, y, mapNameFormat, itemStack -> itemStack);
}
public ItemStack getMap(int x, int y, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
if (x >= width || y >= height) {
throw new IndexOutOfBoundsException("x, y position out of image map size");
}
MapView mapView = mapViews.get(y * width + x);
ItemStack itemStack = new ItemStack(Material.FILLED_MAP);
MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
mapMeta.setMapView(mapView);
mapMeta.setLore(Collections.singletonList(mapNameFormat
.replace("{ImageID}", getImageIndex() + "")
.replace("{X}", x + "")
.replace("{Y}", y + "")
.replace("{Name}", getName())
.replace("{Width}", getWidth() + "")
.replace("{Height}", getHeight() + "")
.replace("{DitheringType}", getDitheringType().getName())
.replace("{CreatorName}", getCreatorName())
.replace("{CreatorUUID}", getCreator().toString())
.replace("{TimeCreated}", ImageFrame.dateFormat.format(new Date(getCreationTime())))));
itemStack.setItemMeta(mapMeta);
return postCreationFunction.apply(itemStack);
}
public List<ItemStack> getMaps(String mapNameFormat) {
return getMaps(mapNameFormat, itemStack -> itemStack);
}
public List<ItemStack> getMaps(String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
List<ItemStack> maps = new ArrayList<>(mapViews.size());
int i = 0;
for (MapView mapView : mapViews) {
ItemStack itemStack = new ItemStack(Material.FILLED_MAP);
MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
mapMeta.setMapView(mapView);
mapMeta.setLore(Collections.singletonList(mapNameFormat
.replace("{ImageID}", getImageIndex() + "")
.replace("{X}", (i % width) + "")
.replace("{Y}", (i / width) + "")
.replace("{Name}", getName())
.replace("{Width}", getWidth() + "")
.replace("{Height}", getHeight() + "")
.replace("{DitheringType}", getDitheringType().getName())
.replace("{CreatorName}", getCreatorName())
.replace("{CreatorUUID}", getCreator().toString())
.replace("{TimeCreated}", ImageFrame.dateFormat.format(new Date(getCreationTime())))));
itemStack.setItemMeta(mapMeta);
maps.add(postCreationFunction.apply(itemStack));
i++;
}
return maps;
}
public void giveMap(Player player, int x, int y, String mapNameFormat) {
giveMap(Collections.singleton(player), x, y, mapNameFormat, itemStack -> itemStack);
}
public void giveMap(Player player, int x, int y, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
giveMap(Collections.singleton(player), x, y, mapNameFormat, postCreationFunction);
}
public void giveMap(Collection<? extends Player> players, int x, int y, String mapNameFormat) {
giveMap(players, x, y, mapNameFormat, itemStack -> itemStack);
}
public void giveMap(Collection<? extends Player> players, int x, int y, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
ItemStack map = getMap(x, y, mapNameFormat, postCreationFunction);
for (Player player : players) {
PlayerUtils.giveItem(player, map.clone());
}
}
public void giveMaps(Player player, String mapNameFormat) {
giveMaps(Collections.singleton(player), mapNameFormat, itemStack -> itemStack);
}
public void giveMaps(Player player, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
giveMaps(Collections.singleton(player), mapNameFormat, postCreationFunction);
}
public void giveMaps(Collection<? extends Player> players, String mapNameFormat) {
giveMaps(players, mapNameFormat, itemStack -> itemStack);
}
public void giveMaps(Collection<? extends Player> players, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction) {
List<ItemStack> maps = getMaps(mapNameFormat, postCreationFunction);
for (ItemStack map : maps) {
for (Player player : players) {
PlayerUtils.giveItem(player, map.clone());
}
}
}
public void fillItemFrames(List<ItemFrame> itemFrames, Rotation rotation, BiPredicate<ItemFrame, ItemStack> prePlaceCheck, BiConsumer<ItemFrame, ItemStack> unableToPlaceAction, String mapNameFormat) {
fillItemFrames(itemFrames, rotation, prePlaceCheck, unableToPlaceAction, mapNameFormat, itemStack -> itemStack, null);
}
public void fillItemFrames(List<ItemFrame> itemFrames, Rotation rotation, BiPredicate<ItemFrame, ItemStack> prePlaceCheck, BiConsumer<ItemFrame, ItemStack> unableToPlaceAction, String mapNameFormat, Function<ItemStack, ItemStack> postCreationFunction, Runnable onComplete) {
if (itemFrames.size() != mapViews.size()) {
throw new IllegalArgumentException("itemFrames size does not equal to mapView size");
}
List<CompletableFuture<Void>> futures = new ArrayList<>();
List<ItemStack> items = getMaps(mapNameFormat, postCreationFunction);
Iterator<ItemFrame> itr0 = itemFrames.iterator();
Iterator<ItemStack> itr1 = items.iterator();
while (itr0.hasNext() && itr1.hasNext()) {
ItemFrame frame = itr0.next();
ItemStack item = itr1.next();
CompletableFuture<Void> future = new CompletableFuture<>();
futures.add(future);
Scheduler.runTask(ImageFrame.plugin, () -> {
try {
if (frame.isValid()) {
if (prePlaceCheck.test(frame, item)) {
frame.setItem(item, false);
frame.setRotation(rotation);
// Hide the item frame's own border so adjacent map tiles tile
// seamlessly with no thin pixel border between them.
frame.setVisible(false);
return;
}
}
unableToPlaceAction.accept(frame, item);
} finally {
future.complete(null);
}
}, frame);
}
if (onComplete != null) {
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
Scheduler.runTask(ImageFrame.plugin, onComplete);
});
}
}
public Set<Player> getViewers() {
return mapViews.stream().flatMap(m -> NMS.getInstance().getViewers(m).stream()).collect(Collectors.toSet());
}
public boolean hasViewers() {
return mapViews.stream().anyMatch(m -> NMS.getInstance().hasViewers(m));
}
public UUID getCreator() {
return creator;
}
public void changeCreator(UUID creator) throws Exception {
this.creator = creator;
this.accessControl.setPermissionWithoutSave(creator, null);
save();
}
public ImageMapAccessControl getAccessControl() {
return accessControl;
}
public String getCreatorName() {
if (creator.equals(CONSOLE_CREATOR)) {
return CONSOLE_CREATOR_NAME;
}
String name = Bukkit.getOfflinePlayer(creator).getName();
return name == null ? UNKNOWN_CREATOR_NAME : name;
}
public long getCreationTime() {
return creationTime;
}
public List<MapView> getMapViews() {
return mapViews;
}
public MapView getMapViewFromMapId(int mapId) {
return mapViews.get(mapIds.indexOf(mapId));
}
public List<Map<String, MapCursor>> getMapMarkers() {
return mapMarkers;
}
public Map<String, MapCursor> getMapMarkers(MapView mapView) {
return mapMarkers.get(mapViews.indexOf(mapView));
}
public MapCursor getMapMarker(String name) {
return mapMarkers.stream().flatMap(each -> each.entrySet().stream()).filter(each -> each.getKey().equalsIgnoreCase(name)).findFirst().map(each -> each.getValue()).orElse(null);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public static abstract class ImageMapRenderer extends MapRenderer {
protected final ImageMapManager manager;
protected final ImageMap imageMap;
protected final int index;
public ImageMapRenderer(ImageMapManager manager, ImageMap imageMap, int index) {
super(ImageFrame.mapRenderersContextual);
this.manager = manager;
this.imageMap = imageMap;
this.index = index;
}
@Override
public void render(MapView mapView, MapCanvas canvas, Player player) {
MutablePair<byte[], Collection<MapCursor>> renderData = renderMap(mapView, 0, player);
manager.callRenderEventListener(manager, imageMap, mapView, player, renderData);
byte[] colors = renderData.getFirst();
if (colors != null) {
for (int i = 0; i < colors.length; i++) {
canvas.setPixel(i % MapUtils.MAP_WIDTH, i / MapUtils.MAP_WIDTH, colors[i]);
}
}
canvas.setCursors(MapUtils.toMapCursorCollection(renderData.getSecond()));
}
public MutablePair<byte[], Collection<MapCursor>> renderPacketData(MapView mapView, int currentTick, Player player) {
MutablePair<byte[], Collection<MapCursor>> renderData = renderMap(mapView, currentTick, player);
manager.callRenderEventListener(manager, imageMap, mapView, player, renderData);
return renderData;
}
public MutablePair<byte[], Collection<MapCursor>> renderPacketData(MapView mapView, Player player) {
MutablePair<byte[], Collection<MapCursor>> renderData = renderMap(mapView, player);
manager.callRenderEventListener(manager, imageMap, mapView, player, renderData);
return renderData;
}
public MutablePair<byte[], Collection<MapCursor>> renderMap(MapView mapView, int currentTick, Player player) {
return renderMap(mapView, player);
}
public abstract MutablePair<byte[], Collection<MapCursor>> renderMap(MapView mapView, Player player);
}
}
@@ -0,0 +1,115 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class ImageMapAccessControl {
public static final UUID EVERYONE = new UUID(-1, -1);
private final ImageMap imageMap;
private final Map<UUID, ImageMapAccessPermissionType> permissions;
public ImageMapAccessControl(ImageMap imageMap, Map<UUID, ImageMapAccessPermissionType> hasAccess) {
this.imageMap = imageMap;
this.permissions = new ConcurrentHashMap<>(hasAccess);
}
public ImageMapAccessPermissionType getPermission(UUID player) {
if (player.equals(imageMap.getCreator())) {
return ImageMapAccessPermissionType.ALL;
}
return permissions.get(player);
}
public ImageMapAccessPermissionType getPermissionForEveryone() {
return getPermission(EVERYONE);
}
public Map<UUID, ImageMapAccessPermissionType> getPermissions() {
return Collections.unmodifiableMap(permissions);
}
public boolean hasPermission(UUID player, ImageMapAccessPermissionType permissionType) {
if (permissionType == null) {
return true;
}
if (player.equals(imageMap.getCreator())) {
return true;
}
if (!player.equals(EVERYONE) && hasPermissionForEveryone(permissionType)) {
return true;
}
ImageMapAccessPermissionType type = permissions.get(player);
if (type == null) {
return false;
}
return type.containsPermission(permissionType);
}
public boolean hasPermissionForEveryone(ImageMapAccessPermissionType permissionType) {
return hasPermission(EVERYONE, permissionType);
}
public void setPermissionForEveryone(ImageMapAccessPermissionType permissionType) throws Exception {
setPermission(EVERYONE, permissionType);
}
protected void setPermissionForEveryoneWithoutSave(ImageMapAccessPermissionType permissionType) {
setPermissionWithoutSave(EVERYONE, permissionType);
}
public void setPermission(UUID player, ImageMapAccessPermissionType permissionType) throws Exception {
setPermission(player, permissionType, true);
}
protected void setPermissionWithoutSave(UUID player, ImageMapAccessPermissionType permissionType) {
try {
setPermission(player, permissionType, false);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected void reapply(Map<UUID, ImageMapAccessPermissionType> hasAccess) {
this.permissions.clear();
this.permissions.putAll(hasAccess);
}
private void setPermission(UUID player, ImageMapAccessPermissionType permissionType, boolean save) throws Exception {
if (player.equals(imageMap.getCreator())) {
return;
}
if (permissionType == null) {
permissions.remove(player);
} else {
permissions.put(player, permissionType);
}
if (save) {
imageMap.save();
}
}
}
@@ -0,0 +1,135 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ImageMapAccessPermissionType implements Comparable<ImageMapAccessPermissionType> {
private static final Map<String, ImageMapAccessPermissionType> TYPES = new ConcurrentHashMap<>();
public static final ImageMapAccessPermissionType GET = register(new ImageMapAccessPermissionType("GET"));
public static final ImageMapAccessPermissionType ADJUST_PLAYBACK = register(new ImageMapAccessPermissionType("ADJUST_PLAYBACK", GET));
public static final ImageMapAccessPermissionType MARKER = register(new ImageMapAccessPermissionType("MARKER", GET));
public static final ImageMapAccessPermissionType EDIT = register(new ImageMapAccessPermissionType("EDIT", MARKER));
public static final ImageMapAccessPermissionType EDIT_CLONE = register(new ImageMapAccessPermissionType("EDIT_CLONE", EDIT));
public static final ImageMapAccessPermissionType ALL = register(new ImageMapAccessPermissionType("ALL", EDIT_CLONE));
/**
* Special case type: All registered ImageMapAccessPermissionTypes inherits this
*/
public static final ImageMapAccessPermissionType BASE = new ImageMapAccessPermissionType("BASE") {
@Override
public boolean containsPermission(ImageMapAccessPermissionType type) {
return TYPES.containsKey(type.name());
}
};
public static ImageMapAccessPermissionType register(ImageMapAccessPermissionType type) {
TYPES.put(type.name(), type);
return type;
}
public static ImageMapAccessPermissionType valueOf(String name) {
ImageMapAccessPermissionType type = TYPES.get(name);
if (type != null) {
return type;
}
throw new IllegalArgumentException(name + " is not a registered ImageMapAccessPermissionType");
}
public static Map<String, ImageMapAccessPermissionType> values() {
return Collections.unmodifiableMap(TYPES);
}
private final String name;
private final Set<ImageMapAccessPermissionType> inheritance;
public ImageMapAccessPermissionType(String name, ImageMapAccessPermissionType... inheritance) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
this.name = name;
this.inheritance = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(inheritance)));
}
public String name() {
return name;
}
public boolean containsPermission(ImageMapAccessPermissionType type) {
if (this.equals(type)) {
return true;
}
for (ImageMapAccessPermissionType inheritedType : inheritance) {
if (!inheritedType.equals(this) && inheritedType.containsPermission(type)) {
return true;
}
}
return false;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImageMapAccessPermissionType type = (ImageMapAccessPermissionType) o;
return name.equals(type.name) && inheritance.equals(type.inheritance);
}
@Override
public int hashCode() {
return Objects.hash(name, inheritance);
}
private int calculateDepth() {
if (this == BASE) {
return 0;
}
return 1 + inheritance.stream().mapToInt(ImageMapAccessPermissionType::calculateDepth).max().orElse(0);
}
@Override
public int compareTo(ImageMapAccessPermissionType o) {
if (this == BASE && o == BASE) {
return 0;
}
if (this == BASE) {
return -1;
}
if (o == BASE) {
return 1;
}
int depthThis = this.calculateDepth();
int depthOther = o.calculateDepth();
int diff = Integer.compare(depthThis, depthOther);
if (diff != 0) {
return diff;
}
return this.name.compareTo(o.name);
}
}
@@ -0,0 +1,70 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class ImageMapCacheControlMode<T extends ImageMapCacheControlTask> {
private static final Map<String, ImageMapCacheControlMode<?>> MODES = new HashMap<>();
public static final ImageMapCacheControlMode<ImageMapDynamicCacheControlTask> DYNAMIC = register(new ImageMapCacheControlMode<>("DYNAMIC", ImageMapDynamicCacheControlTask.class, ImageMapDynamicCacheControlTask::new));
public static final ImageMapCacheControlMode<ImageMapManualPersistentCacheControlTask> MANUAL_PERSISTENT = register(new ImageMapCacheControlMode<>("MANUAL_PERSISTENT", ImageMapManualPersistentCacheControlTask.class, ImageMapManualPersistentCacheControlTask::new));
public static <T extends ImageMapCacheControlTask> ImageMapCacheControlMode<T> register(ImageMapCacheControlMode<T> cacheControlTasks) {
MODES.put(cacheControlTasks.getIdentifier(), cacheControlTasks);
return cacheControlTasks;
}
public static Map<String, ImageMapCacheControlMode<?>> values() {
return Collections.unmodifiableMap(MODES);
}
public static ImageMapCacheControlMode<?> valueOf(String identifier) {
return MODES.get(identifier);
}
private final String identifier;
private final Class<T> clazz;
private final Function<ImageMap, T> constructor;
public ImageMapCacheControlMode(String identifier, Class<T> clazz, Function<ImageMap, T> constructor) {
this.identifier = identifier;
this.clazz = clazz;
this.constructor = constructor;
}
public String getIdentifier() {
return identifier;
}
public Class<T> getClazz() {
return clazz;
}
public T newInstance(ImageMap imageMap) {
return constructor.apply(imageMap);
}
}
@@ -0,0 +1,34 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
public interface ImageMapCacheControlTask extends AutoCloseable {
ImageMap getImageMap();
void loadCacheIfManual();
boolean isClosed();
@Override
void close();
}
@@ -0,0 +1,66 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.UUID;
public abstract class ImageMapCreateInfo {
private final ImageMapManager manager;
private final String name;
private final int width;
private final int height;
private final DitheringType ditheringType;
private final UUID creator;
public ImageMapCreateInfo(ImageMapManager manager, String name, int width, int height, DitheringType ditheringType, UUID creator) {
this.manager = manager;
this.name = name;
this.width = width;
this.height = height;
this.ditheringType = ditheringType;
this.creator = creator;
}
public ImageMapManager getManager() {
return manager;
}
public String getName() {
return name;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public DitheringType getDitheringType() {
return ditheringType;
}
public UUID getCreator() {
return creator;
}
}
@@ -0,0 +1,161 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.common.base.Strings;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.language.TranslationKey;
import com.loohp.imageframe.utils.CommandSenderUtils;
import com.loohp.imageframe.utils.ThrowingSupplier;
import com.loohp.platformscheduler.ScheduledTask;
import com.loohp.platformscheduler.Scheduler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class ImageMapCreationTask<T> extends CompletableFuture<T> implements Runnable {
private final ImageMapCreationTaskManager manager;
private final UUID creationTaskId;
private final UUID creator;
private final ThrowingSupplier<T> creationTask;
private final ScheduledTask monitorTask;
private final ComponentTemplate queuingMessageTemplate;
private final ComponentTemplate processingMessageTemplate;
private final AtomicBoolean queuing;
private final AtomicInteger tick;
private final AtomicInteger processingStart;
private final Future<?> submission;
public ImageMapCreationTask(ImageMapCreationTaskManager manager, UUID creator, ThrowingSupplier<T> creationTask, ExecutorService executor, String imageMapName) {
this.manager = manager;
this.creationTaskId = UUID.randomUUID();
this.creator = creator;
this.creationTask = creationTask;
this.manager.getTaskInQueue().add(this);
this.queuingMessageTemplate = ComponentTemplate.template(TranslationKey.IMAGE_MAP_QUEUED_ACTION_BAR, imageMapName);
this.processingMessageTemplate = ComponentTemplate.template(TranslationKey.IMAGE_MAP_PROCESSING_ACTION_BAR, imageMapName);
this.monitorTask = Scheduler.runTaskTimerAsynchronously(ImageFrame.plugin, new MonitorTask(), 0, 10);
this.tick = new AtomicInteger(0);
this.processingStart = new AtomicInteger(-1);
this.queuing = new AtomicBoolean(true);
this.submission = executor.submit(this);
}
public ImageMapCreationTaskManager getManager() {
return manager;
}
public UUID getCreator() {
return creator;
}
public UUID getCreationTaskId() {
return creationTaskId;
}
public boolean isQueuing() {
return queuing.get();
}
public int getPositionInQueue() {
return manager.getPositionInQueue(this);
}
@Override
public void run() {
this.manager.getTaskInQueue().remove(this);
this.queuing.set(false);
try {
complete(creationTask.get());
} catch (Throwable e) {
completeExceptionally(e);
}
}
public boolean isCompleted() {
return monitorTask.isCancelled();
}
@SuppressWarnings("deprecation")
public void complete(Component message) {
if (!isCompleted()) {
monitorTask.cancel();
manager.getCreatorsInQueue().remove(creator);
Player player = Bukkit.getPlayer(creator);
if (player != null) {
CommandSenderUtils.sendMessage(player, ChatMessageType.ACTION_BAR, message);
}
}
}
private class MonitorTask implements Runnable {
@SuppressWarnings("deprecation")
@Override
public void run() {
processingStart.updateAndGet(currentProcessingStart -> {
int currentTick = tick.get();
if (currentProcessingStart < 0) {
if (!queuing.get()) {
return currentTick;
}
} else {
if (currentTick - currentProcessingStart > (ImageFrame.maxProcessingTime * 20)) {
submission.cancel(true);
}
}
return currentProcessingStart;
});
Player player = Bukkit.getPlayer(creator);
if (player != null) {
TranslatableComponent message;
if (queuing.get()) {
int position = getPositionInQueue() + 1;
message = queuingMessageTemplate.build(position);
} else {
int dots = tick.getAndIncrement() % 4;
message = processingMessageTemplate.build(Strings.repeat(".", dots));
}
if (!message.key().isEmpty()) {
CommandSenderUtils.sendMessage(player, ChatMessageType.ACTION_BAR, message);
}
}
}
}
}
@@ -0,0 +1,114 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.loohp.imageframe.utils.ThrowingSupplier;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ImageMapCreationTaskManager {
private final ThreadPoolExecutor executor;
private final Set<UUID> creatorsInQueue;
private final Queue<ImageMapCreationTask<?>> taskInQueue;
public ImageMapCreationTaskManager(int parallelCount) {
ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("ImageFrame Image Map Creation Thread #%d").build();
this.executor = new ThreadPoolExecutor(parallelCount, parallelCount, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), factory);
this.creatorsInQueue = ConcurrentHashMap.newKeySet();
this.taskInQueue = new ConcurrentLinkedQueue<>();
}
public <T> ImageMapCreationTask<T> enqueue(UUID creator, String imageMapName, ThrowingSupplier<T> creationTask) throws EnqueueRejectedException {
if (!creatorsInQueue.add(creator)) {
throw new EnqueueRejectedException("Creator " + creator + " already in queue");
}
return new ImageMapCreationTask<>(this, creator, creationTask, executor, imageMapName);
}
public boolean isCreatorInQueue(UUID creator) {
return creatorsInQueue.contains(creator);
}
protected Set<UUID> getCreatorsInQueue() {
return creatorsInQueue;
}
protected Queue<ImageMapCreationTask<?>> getTaskInQueue() {
return taskInQueue;
}
public int getPositionInQueue(ImageMapCreationTask<?> creationTask) {
int index = 0;
for (ImageMapCreationTask<?> queuedTask : taskInQueue) {
if (creationTask.getCreationTaskId().equals(queuedTask.getCreationTaskId())) {
return index;
}
index++;
}
return -1;
}
public int getPositionInQueue(UUID creator) {
int index = 0;
for (ImageMapCreationTask<?> queuedTask : taskInQueue) {
if (creator.equals(queuedTask.getCreator())) {
return index;
}
index++;
}
return -1;
}
public int getQueueSize() {
return executor.getQueue().size();
}
public static class EnqueueRejectedException extends Exception {
public EnqueueRejectedException() {
super();
}
public EnqueueRejectedException(String message) {
super(message);
}
public EnqueueRejectedException(String message, Throwable cause) {
super(message, cause);
}
public EnqueueRejectedException(Throwable cause) {
super(cause);
}
}
}
@@ -0,0 +1,102 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.loohp.imageframe.ImageFrame;
import com.loohp.platformscheduler.ScheduledTask;
import com.loohp.platformscheduler.Scheduler;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class ImageMapDynamicCacheControlTask implements ImageMapCacheControlTask, Runnable {
private final ImageMap imageMap;
private final AtomicBoolean locked;
private final AtomicBoolean closed;
private final AtomicReference<ScheduledTask> task;
private final AtomicInteger noViewerCounts;
public ImageMapDynamicCacheControlTask(ImageMap imageMap) {
this.imageMap = imageMap;
this.noViewerCounts = new AtomicInteger(0);
this.locked = new AtomicBoolean(false);
this.closed = new AtomicBoolean(false);
this.task = new AtomicReference<>(Scheduler.runTaskLaterAsynchronously(ImageFrame.plugin, this, 5));
}
@Override
public ImageMap getImageMap() {
return imageMap;
}
@Override
public void loadCacheIfManual() {
}
public boolean isLocked() {
return locked.get();
}
public void setLocked(boolean locked) {
this.locked.set(locked);
}
@Override
public void run() {
if (closed.get()) {
return;
}
if (!locked.get()) {
if (imageMap.hasViewers()) {
noViewerCounts.set(0);
if (!imageMap.hasColorCached()) {
imageMap.loadColorCache();
}
} else {
if (noViewerCounts.getAndIncrement() > 200 && imageMap.hasColorCached()) {
imageMap.unloadColorCache();
}
}
}
this.task.set(Scheduler.runTaskLaterAsynchronously(ImageFrame.plugin, this, 5));
}
@Override
public boolean isClosed() {
return closed.get();
}
@Override
public void close() {
closed.set(true);
task.updateAndGet(t -> {
if (t != null) {
t.cancel();
}
return t;
});
}
}
@@ -0,0 +1,61 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import org.bukkit.entity.ItemFrame;
import org.bukkit.util.Vector;
public class ImageMapHitTargetResult {
private final ItemFrame itemFrame;
private final ImageMap imageMap;
private final IntPosition localTargetPixel;
private final IntPosition targetPixel;
private final Vector worldTargetPosition;
public ImageMapHitTargetResult(ItemFrame itemFrame, ImageMap imageMap, IntPosition localTargetPixel, IntPosition targetPixel, Vector worldTargetPosition) {
this.itemFrame = itemFrame;
this.imageMap = imageMap;
this.localTargetPixel = localTargetPixel;
this.targetPixel = targetPixel;
this.worldTargetPosition = worldTargetPosition;
}
public ItemFrame getItemFrame() {
return itemFrame;
}
public ImageMap getImageMap() {
return imageMap;
}
public IntPosition getLocalTargetPixel() {
return localTargetPixel;
}
public IntPosition getTargetPixel() {
return targetPixel;
}
public Vector getWorldTargetPosition() {
return worldTargetPosition;
}
}
@@ -0,0 +1,118 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonObject;
import com.loohp.imageframe.media.MediaFrame;
import com.loohp.imageframe.media.MediaLoader;
import net.kyori.adventure.key.Key;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
public abstract class ImageMapLoader<T extends ImageMap, C extends ImageMapCreateInfo> {
protected final List<MediaLoader> mediaLoaders;
protected ImageMapLoader() {
this.mediaLoaders = new CopyOnWriteArrayList<>();
}
public abstract Key getIdentifier();
public String getLegacyType() {
return null;
}
public abstract Class<T> getImageMapClass();
public abstract Class<C> getImageMapCreateInfoClass();
public abstract List<String> getExtraPermissions();
public abstract boolean isSupported(String imageType);
public ImageMapLoaderPriority getPriority(String imageType) {
return ImageMapLoaderPriority.NORMAL;
}
public void registerMediaLoaderFirst(MediaLoader mediaLoader) {
mediaLoaders.add(0, mediaLoader);
}
public void registerMediaLoaderLast(MediaLoader mediaLoader) {
mediaLoaders.add(mediaLoader);
}
public void registerMediaLoaderBefore(Key identifier, MediaLoader mediaLoader) {
int index = indexOfMediaLoader(identifier);
if (index < 0) {
registerMediaLoaderFirst(mediaLoader);
} else {
mediaLoaders.add(index, mediaLoader);
}
}
public void registerMediaLoaderAfter(Key identifier, MediaLoader mediaLoader) {
int index = indexOfMediaLoader(identifier);
if (index < 0) {
registerMediaLoaderLast(mediaLoader);
} else {
mediaLoaders.add(index + 1, mediaLoader);
}
}
protected int indexOfMediaLoader(Key identifier) {
for (int i = 0; i < mediaLoaders.size(); i++) {
if (mediaLoaders.get(i).getIdentifier().equals(identifier)) {
return i;
}
}
return -1;
}
public Iterator<MediaFrame> tryLoadMedia(String url) throws IOException {
List<IOException> exceptions = new ArrayList<>();
for (MediaLoader mediaLoader : mediaLoaders) {
try {
if (mediaLoader.shouldTryRead(url)) {
return mediaLoader.tryLoad(url);
}
} catch (Exception e) {
exceptions.add(new IOException("Unable to read or download media with MediaLoader " + mediaLoader.getIdentifier(), e));
}
}
IOException e = new IOException("Unable to read or download media with ImageMapLoader " + getIdentifier().asString() + ", does this url directly links to the image? (" + url + ")");
for (IOException ex : exceptions) {
e.addSuppressed(ex);
}
throw e;
}
public abstract Future<T> create(C createInfo) throws Exception;
public abstract Future<T> load(ImageMapManager manager, JsonObject json) throws Exception;
}
@@ -0,0 +1,27 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
public enum ImageMapLoaderPriority {
LOWEST, LOW, NORMAL, HIGH, HIGHEST;
}
@@ -0,0 +1,116 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.gson.JsonObject;
import com.loohp.imageframe.media.GifReaderMediaLoader;
import com.loohp.imageframe.media.ImageIOMediaLoader;
import com.loohp.imageframe.media.MediaLoader;
import net.kyori.adventure.key.Key;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
public class ImageMapLoaders {
private static final List<ImageMapLoader<?, ?>> LOADERS_ORDERED = new CopyOnWriteArrayList<>();
private static final Map<Key, ImageMapLoader<?, ?>> LOADERS_MAP = new ConcurrentHashMap<>();
public static final URLStaticImageMapLoader URL_STATIC_IMAGE_MAP_LOADER = registerLoader(new URLStaticImageMapLoader());
public static final URLAnimatedImageMapLoader URL_ANIMATED_IMAGE_MAP_LOADER = registerLoader(new URLAnimatedImageMapLoader());
public static final MinecraftURLOverlayImageMapLoader MINECRAFT_URL_OVERLAY_IMAGE_MAP_LOADER = registerLoader(new MinecraftURLOverlayImageMapLoader());
public static final NonUpdatableStaticImageMapLoader NON_UPDATABLE_IMAGE_MAP_LOADER = registerLoader(new NonUpdatableStaticImageMapLoader());
public static final BundledStaticImageMapLoader BUNDLED_STATIC_IMAGE_MAP_LOADER = registerLoader(new BundledStaticImageMapLoader());
public static final BundledAnimatedImageMapLoader BUNDLED_ANIMATED_IMAGE_MAP_LOADER = registerLoader(new BundledAnimatedImageMapLoader());
public static final GifReaderMediaLoader GIF_READER_MEDIA_LOADER = registerLoader(new GifReaderMediaLoader(), URL_ANIMATED_IMAGE_MAP_LOADER);
public static final ImageIOMediaLoader IMAGE_IO_MEDIA_LOADER = registerLoader(new ImageIOMediaLoader(), URL_STATIC_IMAGE_MAP_LOADER, MINECRAFT_URL_OVERLAY_IMAGE_MAP_LOADER);
public static void init() {
/* do nothing */
}
public static <T extends ImageMapLoader<?, ?>> T registerLoader(T loader) {
LOADERS_ORDERED.add(loader);
LOADERS_MAP.put(loader.getIdentifier(), loader);
Bukkit.getServer().getConsoleSender().sendMessage(ChatColor.GRAY + "[ImageFrame] Registered ImageMapLoader " + loader.getIdentifier().asString());
return loader;
}
@SuppressWarnings("unchecked")
public static <T extends ImageMapLoader<?, ?>> T getLoader(Class<T> loaderClass) {
return (T) LOADERS_ORDERED.stream()
.filter(l -> loaderClass.equals(l.getClass()))
.findFirst()
.orElse(null);
}
@SuppressWarnings("unchecked")
public static <T extends ImageMap, C extends ImageMapCreateInfo> ImageMapLoader<? extends T, C> getLoader(Class<T> imageMapClass, Class<C> createInfoClass, String imageType, CommandSender sender) {
return (ImageMapLoader<? extends T, C>) LOADERS_ORDERED.stream()
.filter(l -> imageMapClass.isAssignableFrom(l.getImageMapClass()))
.filter(l -> createInfoClass.equals(l.getImageMapCreateInfoClass()))
.filter(l -> l.isSupported(imageType))
.filter(l -> l.getExtraPermissions().stream().allMatch(p -> sender.hasPermission(p)))
.max(Comparator.comparing((ImageMapLoader<?, ?> l) -> l.getPriority(imageType)))
.orElse(null);
}
public static <T extends ImageMap> ImageMapLoader<?, ?> getLoader(Key identifier) {
return LOADERS_MAP.get(identifier);
}
@SuppressWarnings("PatternValidation")
public static <T extends ImageMap> ImageMapLoader<?, ?> getLoader(String identifier) {
if (identifier.contains(":")) {
return getLoader(Key.key(identifier));
}
return LOADERS_ORDERED.stream()
.filter(l -> identifier.equalsIgnoreCase(l.getLegacyType()))
.findFirst()
.orElse(null);
}
public static Future<? extends ImageMap> load(ImageMapManager manager, JsonObject json) throws Exception {
String type = json.get("type").getAsString();
ImageMapLoader<?, ?> loader = getLoader(type);
if (loader == null) {
throw new IllegalStateException("Unable to find ImageMapLoader " + type);
}
return loader.load(manager, json);
}
public static <T extends MediaLoader> T registerLoader(T loader, ImageMapLoader<?, ?>... imageMapLoaders) {
for (ImageMapLoader<?, ?> imageMapLoader : imageMapLoaders) {
imageMapLoader.registerMediaLoaderLast(loader);
Bukkit.getServer().getConsoleSender().sendMessage(ChatColor.GRAY + "[ImageFrame] Registered MediaLoader " + loader.getIdentifier().asString() + " to ImageMapLoader " + imageMapLoader.getIdentifier().asString());
}
return loader;
}
}
@@ -0,0 +1,376 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.loohp.imageframe.ImageFrame;
import com.loohp.imageframe.api.events.ImageMapAddedEvent;
import com.loohp.imageframe.api.events.ImageMapDeletedEvent;
import com.loohp.imageframe.api.events.ImageMapUpdatedEvent;
import com.loohp.imageframe.storage.ImageFrameStorage;
import com.loohp.imageframe.utils.MapUtils;
import com.loohp.platformscheduler.Scheduler;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.map.MapCanvas;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapRenderer;
import org.bukkit.map.MapView;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntSupplier;
import java.util.stream.Collectors;
public class ImageMapManager implements AutoCloseable {
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
public static final int FAKE_MAP_ID_START_RANGE = Integer.MAX_VALUE / 4 * 3;
public static final int BUNDLED_IMAGE_INDEX_START_RANGE = 1000000000;
private static final AtomicInteger FAKE_MAP_ID_COUNTER = new AtomicInteger(FAKE_MAP_ID_START_RANGE);
public static int getNextFakeMapId() {
return FAKE_MAP_ID_COUNTER.getAndUpdate(i -> i < FAKE_MAP_ID_START_RANGE ? FAKE_MAP_ID_START_RANGE : i + 1);
}
public static boolean isBundledImageIndex(int imageIndex) {
return imageIndex >= BUNDLED_IMAGE_INDEX_START_RANGE;
}
private final ImageFrameStorage imageFrameStorage;
private final Map<Integer, ImageMap> maps;
private final Map<MapView, ImageMap> mapsByView;
private final List<ImageMapRenderEventListener> renderEventListeners;
private final Set<Integer> deletedMapIds;
private final AtomicInteger bundledImageIndexCounter;
public ImageMapManager(ImageFrameStorage imageFrameStorage) {
this.maps = new ConcurrentHashMap<>();
this.mapsByView = new ConcurrentHashMap<>();
this.imageFrameStorage = imageFrameStorage;
this.renderEventListeners = new CopyOnWriteArrayList<>();
this.deletedMapIds = ConcurrentHashMap.newKeySet();
this.bundledImageIndexCounter = new AtomicInteger(BUNDLED_IMAGE_INDEX_START_RANGE);
}
public ImageFrameStorage getStorage() {
return imageFrameStorage;
}
protected long getCurrentAnimationTick() {
return System.currentTimeMillis() / 50;
}
@Override
public void close() {
saveDeletedMaps();
}
public void appendRenderEventListener(ImageMapRenderEventListener listener) {
renderEventListeners.add(listener);
}
public void prependRenderEventListener(ImageMapRenderEventListener listener) {
renderEventListeners.add(0, listener);
}
public void removeRenderEventListener(ImageMapRenderEventListener listener) {
renderEventListeners.remove(listener);
}
protected void callRenderEventListener(ImageMapManager manager, ImageMap imageMap, MapView map, Player player, MutablePair<byte[], Collection<MapCursor>> renderData) {
renderEventListeners.forEach(each -> each.accept(manager, imageMap, map, player, renderData));
}
public synchronized void addMap(ImageMap map) throws Exception {
if (map.getManager() != this) {
throw new IllegalArgumentException("ImageMap's manager is not set to this");
}
if (getFromCreator(map.getCreator(), map.getName()) != null) {
throw new IllegalArgumentException("Duplicated map name for this creator");
}
int originalImageIndex = map.getImageIndex();
imageFrameStorage.prepareImageIndex(map, i -> map.imageIndex = i);
maps.put(map.getImageIndex(), map);
for (MapView mapView : map.getMapViews()) {
mapsByView.put(mapView, map);
deletedMapIds.remove(mapView.getId());
}
try {
map.save();
Bukkit.getPluginManager().callEvent(new ImageMapAddedEvent(map));
} catch (Throwable e) {
maps.remove(originalImageIndex);
for (MapView mapView : map.getMapViews()) {
mapsByView.remove(mapView);
}
throw e;
}
saveDeletedMaps();
}
/**
* Registers a map whose canonical definition lives inside the plugin jar
* (maps.yml + bundled image). Unlike {@link #addMap}, nothing is written
* to storage: bundled maps occupy a reserved image index range and are
* rebuilt from the jar on every startup.
*/
public synchronized void addBundledMap(ImageMap map) {
if (map.getManager() != this) {
throw new IllegalArgumentException("ImageMap's manager is not set to this");
}
if (getFromCreator(map.getCreator(), map.getName()) != null) {
throw new IllegalArgumentException("Duplicated map name for this creator");
}
if (map.getImageIndex() < 0) {
map.imageIndex = bundledImageIndexCounter.getAndIncrement();
} else if (!isBundledImageIndex(map.getImageIndex())) {
throw new IllegalArgumentException("Bundled ImageMap image index must be in the bundled range");
}
maps.put(map.getImageIndex(), map);
for (MapView mapView : map.getMapViews()) {
mapsByView.put(mapView, map);
deletedMapIds.remove(mapView.getId());
}
Bukkit.getPluginManager().callEvent(new ImageMapAddedEvent(map));
saveDeletedMaps();
}
public boolean hasMap(int imageIndex) {
return maps.containsKey(imageIndex);
}
public Collection<ImageMap> getMaps() {
return Collections.unmodifiableCollection(maps.values());
}
public ImageMap getFromMapId(int id) {
MapView mapView = Bukkit.getMap(id);
if (mapView == null) {
return null;
}
return getFromMapView(Bukkit.getMap(id));
}
public ImageMap getFromImageId(int imageId) {
return maps.get(imageId);
}
public ImageMap getFromMapView(MapView mapView) {
return mapsByView.get(mapView);
}
public Set<ImageMap> getFromCreator(UUID uuid) {
return maps.values().stream().filter(each -> each.getCreator().equals(uuid)).collect(Collectors.toSet());
}
public List<ImageMap> getFromCreator(UUID uuid, Comparator<ImageMap> order) {
return maps.values().stream().filter(each -> each.getCreator().equals(uuid)).sorted(order).collect(Collectors.toList());
}
public ImageMap getFromCreator(UUID uuid, String name) {
return maps.values().stream().filter(each -> each.getCreator().equals(uuid) && each.getName().equalsIgnoreCase(name)).findFirst().orElse(null);
}
public Set<UUID> getCreators() {
return maps.values().stream().map(each -> each.getCreator()).collect(Collectors.toSet());
}
public ImageMap getFromFakeMapId(int fakeMapId) {
return maps.values().stream().filter(each -> each.requiresAnimationService() && each.getFakeMapIds().contains(fakeMapId)).findFirst().orElse(null);
}
public boolean deleteMap(int imageIndex) {
ImageMap imageMap = maps.remove(imageIndex);
if (imageMap == null) {
return false;
}
List<MapView> mapViews = imageMap.getMapViews();
for (MapView mapView : mapViews) {
mapsByView.remove(mapView);
}
if (imageMap.trackDeletedMaps()) {
mapViews.forEach(each -> deletedMapIds.add(each.getId()));
}
imageMap.markInvalid();
if (!isBundledImageIndex(imageIndex)) {
imageFrameStorage.deleteMap(imageIndex);
}
imageMap.stop();
saveDeletedMaps();
Bukkit.getPluginManager().callEvent(new ImageMapDeletedEvent(imageMap));
Scheduler.runTask(ImageFrame.plugin, () -> {
mapViews.forEach(each -> {
if (each.getRenderers().isEmpty()) {
each.addRenderer(DeletedMapRenderer.INSTANCE);
}
});
});
return true;
}
public synchronized void updateMap(int imageIndex, boolean exist) {
ImageMap imageMap = maps.get(imageIndex);
try {
if (imageMap == null) {
if (exist) {
JsonObject json = imageFrameStorage.loadImageMapData(imageIndex);
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
try {
addMap(ImageMapLoaders.load(this, json).get());
} catch (Exception e) {
throw new RuntimeException("Unable to update map " + imageIndex + " from source", e);
}
});
}
} else {
if (exist) {
JsonObject json = imageFrameStorage.loadImageMapData(imageIndex);
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
if (imageMap.applyUpdate(json)) {
imageMap.reloadColorCache();
Bukkit.getPluginManager().callEvent(new ImageMapUpdatedEvent(imageMap));
}
});
} else {
deleteMap(imageIndex);
}
}
} catch (Exception e) {
throw new RuntimeException("Unable to update map " + imageIndex + " from source", e);
}
}
public Set<Integer> getDeletedMapIds() {
return Collections.unmodifiableSet(deletedMapIds);
}
public boolean isMapDeleted(int mapId) {
return deletedMapIds.contains(mapId);
}
public boolean isMapDeleted(MapView mapView) {
return isMapDeleted(mapView.getId());
}
public synchronized void loadMaps(IFPlayerManager ifPlayerManager) {
loadMaps(ifPlayerManager, null);
}
public synchronized void loadMaps(IFPlayerManager ifPlayerManager, IntSupplier completionCallback) {
maps.clear();
mapsByView.clear();
List<MutablePair<String, Future<? extends ImageMap>>> futures = imageFrameStorage.loadMaps(this, deletedMapIds, ifPlayerManager);
Scheduler.runTaskAsynchronously(ImageFrame.plugin, () -> {
int count = 0;
for (MutablePair<String, Future<? extends ImageMap>> pair : futures) {
try {
addMap(pair.getSecond().get());
count++;
} catch (Throwable e) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to load ImageMap data in " + pair.getFirst());
e.printStackTrace();
}
}
if (completionCallback != null) {
count += completionCallback.getAsInt();
}
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Data loading completed! Loaded " + count + " ImageMaps!");
});
}
public void syncMaps() {
syncMaps(false);
}
public synchronized void syncMaps(boolean verbose) {
Set<Integer> indexesFromStorage = imageFrameStorage.getAllImageIndexes();
Set<Integer> indexesFromLocal = maps.keySet().stream().filter(i -> !isBundledImageIndex(i)).collect(Collectors.toSet());
int added = 0;
int deleted = 0;
for (int index : Sets.symmetricDifference(indexesFromStorage, indexesFromLocal)) {
try {
boolean exist = indexesFromStorage.contains(index);
updateMap(index, exist);
if (exist) {
added++;
} else {
deleted++;
}
} catch (Throwable e) {
if (verbose) {
Bukkit.getConsoleSender().sendMessage(ChatColor.RED + "[ImageFrame] Unable to sync ImageMap data for index " + index);
}
e.printStackTrace();
}
}
if (verbose) {
Bukkit.getConsoleSender().sendMessage(ChatColor.GREEN + "[ImageFrame] Data sync completed! Newly loaded " + added + " and deleted " + deleted + " ImageMaps!");
}
}
public synchronized void saveDeletedMaps() {
imageFrameStorage.saveDeletedMaps(deletedMapIds);
}
public void sendAllMaps(Collection<? extends Player> players) {
maps.values().forEach(m -> m.send(players));
}
public static class DeletedMapRenderer extends MapRenderer {
public static final DeletedMapRenderer INSTANCE = new DeletedMapRenderer();
private DeletedMapRenderer() {}
@Override
public void render(MapView map, MapCanvas canvas, Player player) {
List<MapRenderer> mapRenderers = map.getRenderers();
if (mapRenderers.size() != 1 || mapRenderers.get(0) != this) {
Scheduler.runTaskLater(ImageFrame.plugin, () -> map.removeRenderer(this), 1);
return;
}
Random random = new Random(map.getId());
byte[] colors = MapUtils.PALETTE_GRAYSCALE;
for (int y = 0; y < MapUtils.MAP_WIDTH; y++) {
for (int x = 0; x < MapUtils.MAP_WIDTH; x++) {
canvas.setPixel(x, y, colors[random.nextInt(colors.length)]);
}
}
}
}
}
@@ -0,0 +1,54 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.concurrent.atomic.AtomicBoolean;
public class ImageMapManualPersistentCacheControlTask implements ImageMapCacheControlTask {
private final ImageMap imageMap;
private final AtomicBoolean closed;
public ImageMapManualPersistentCacheControlTask(ImageMap imageMap) {
this.imageMap = imageMap;
this.closed = new AtomicBoolean(false);
}
@Override
public ImageMap getImageMap() {
return imageMap;
}
@Override
public void loadCacheIfManual() {
imageMap.loadColorCache();
}
@Override
public boolean isClosed() {
return closed.get();
}
@Override
public void close() {
closed.set(true);
}
}
@@ -0,0 +1,34 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import org.bukkit.entity.Player;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapView;
import java.util.Collection;
@FunctionalInterface
public interface ImageMapRenderEventListener {
void accept(ImageMapManager manager, ImageMap imageMap, MapView map, Player player, MutablePair<byte[], Collection<MapCursor>> renderData);
}
@@ -0,0 +1,55 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Objects;
public class IntPosition {
private final int x;
private final int y;
public IntPosition(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IntPosition intPosition = (IntPosition) o;
return x == intPosition.x && y == intPosition.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
@@ -0,0 +1,78 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.Arrays;
import java.util.Objects;
public class IntRange {
public static IntRange of(String input) {
String[] parts = Arrays.stream(input.split("-")).map(s -> s.trim()).filter(s -> !s.isEmpty()).toArray(String[]::new);
if (parts.length == 1) {
return of(Integer.parseInt(parts[0]));
} else if (parts.length == 2) {
return of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
}
throw new IllegalArgumentException("Invalid range format: " + input);
}
public static IntRange of(int value) {
return new IntRange(value, value);
}
public static IntRange of(int start, int end) {
return start <= end ? new IntRange(start, end) : new IntRange(end, start);
}
private final int start;
private final int end;
private IntRange(int start, int end) {
this.start = start;
this.end = end;
}
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
public boolean satisfies(int value) {
return start <= value && value <= end;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IntRange intRange = (IntRange) o;
return start == intRange.start && end == intRange.end;
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
}
@@ -0,0 +1,31 @@
/*
* This file is part of ImageFrame.
*
* Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
* Copyright (C) 2025. Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.loohp.imageframe.objectholders;
import java.util.ArrayList;
public class IntRangeList extends ArrayList<IntRange> {
public boolean satisfies(int value) {
return stream().anyMatch(r -> r.satisfies(value));
}
}
Loaded 100 of 194 files, more files were not shown because too many files have changed in this diff. Show more