From 9c75d93834a5c6f0a17ed2b30bfd1485bf243d8c Mon Sep 17 00:00:00 2001 From: ItsNature Date: Thu, 16 Apr 2026 03:11:48 +0200 Subject: [PATCH 1/7] Deploy as `1.2.6-SNAPSHOT` --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 21d9025d..39390837 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.lunarclient -version=1.2.5 +version=1.2.6-SNAPSHOT description=The API for interacting with Lunar Client players. org.gradle.parallel=true From eef67077639368eb6b69bf7c4b2dfd546e0297d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 4 May 2026 23:50:41 +0200 Subject: [PATCH 2/7] Documentation Updates & Improvements (#277) * Restructure some docs * Restructure some docs * add integration callout redirects to all modules * add overview to private & public modules * update developers FAQs * update server owner FAQs * minor tweaks to FAQ pages * Mod Settings -> Mod Options, fix redirects * update module callouts * fix redirects --------- Co-authored-by: TrentinTheKid <25537885+TrentinTheKid@users.noreply.github.com> --- .github/README.md | 6 +- docs/acceptable-use.mdx | 16 ++-- docs/developers.mdx | 37 ++++++++++ docs/developers/_meta.json | 5 +- docs/developers/events.mdx | 74 ------------------- docs/developers/faq.mdx | 69 ++++------------- docs/developers/general.mdx | 6 ++ .../introduction.mdx => lightweight.mdx} | 4 +- docs/developers/lightweight/_meta.json | 1 - .../{json/getting-started.mdx => json.mdx} | 0 docs/developers/lightweight/json/_meta.json | 1 - .../getting-started.mdx => protobuf.mdx} | 0 .../lightweight/protobuf/_meta.json | 1 - docs/developers/modules.mdx | 40 ++++++++++ docs/developers/modules/autotexthotkey.mdx | 14 +++- docs/developers/modules/beam.mdx | 14 +++- docs/developers/modules/border.mdx | 15 +++- docs/developers/modules/chat.mdx | 14 +++- docs/developers/modules/coloredfire.mdx | 14 +++- docs/developers/modules/combat.mdx | 14 +++- docs/developers/modules/cooldown.mdx | 14 +++- docs/developers/modules/entity.mdx | 14 +++- docs/developers/modules/glow.mdx | 15 +++- docs/developers/modules/hologram.mdx | 14 +++- docs/developers/modules/limb.mdx | 14 +++- docs/developers/modules/modsetting.mdx | 14 +++- docs/developers/modules/nametag.mdx | 15 +++- docs/developers/modules/nickhider.mdx | 14 +++- docs/developers/modules/notification.mdx | 15 +++- docs/developers/modules/packetenrichment.mdx | 11 ++- docs/developers/modules/paynow.mdx | 15 +++- docs/developers/modules/richpresence.mdx | 15 +++- docs/developers/modules/serverlink.mdx | 14 +++- docs/developers/modules/serverrule.mdx | 15 +++- docs/developers/modules/staffmod.mdx | 14 +++- docs/developers/modules/stopwatch.mdx | 14 +++- docs/developers/modules/team.mdx | 14 +++- docs/developers/modules/tebex.mdx | 15 +++- docs/developers/modules/title.mdx | 14 +++- docs/developers/modules/tntcountdown.mdx | 15 +++- docs/developers/modules/transfer.mdx | 15 +++- docs/developers/modules/vignette.mdx | 14 +++- docs/developers/modules/waypoint.mdx | 15 +++- docs/developers/private-modules.mdx | 18 +++++ docs/developers/private-modules/_meta.json | 3 + docs/developers/private-modules/cosmetic.mdx | 0 docs/developers/utilities/_meta.json | 8 ++ docs/developers/{ => utilities}/adventure.mdx | 0 .../{ => utilities}/platform-utilities.mdx | 0 docs/introduction.mdx | 2 +- docs/server-owners.mdx | 9 +++ docs/server-owners/config.mdx | 4 +- docs/server-owners/faq.mdx | 70 +++++------------- 53 files changed, 544 insertions(+), 244 deletions(-) create mode 100644 docs/developers.mdx rename docs/developers/{lightweight/introduction.mdx => lightweight.mdx} (89%) rename docs/developers/lightweight/{json/getting-started.mdx => json.mdx} (100%) rename docs/developers/lightweight/{protobuf/getting-started.mdx => protobuf.mdx} (100%) create mode 100644 docs/developers/modules.mdx create mode 100644 docs/developers/private-modules.mdx create mode 100644 docs/developers/private-modules/_meta.json create mode 100644 docs/developers/private-modules/cosmetic.mdx create mode 100644 docs/developers/utilities/_meta.json rename docs/developers/{ => utilities}/adventure.mdx (100%) rename docs/developers/{ => utilities}/platform-utilities.mdx (100%) create mode 100644 docs/server-owners.mdx diff --git a/.github/README.md b/.github/README.md index 86531795..d5e3fa06 100644 --- a/.github/README.md +++ b/.github/README.md @@ -24,12 +24,12 @@ Our **Lightweight integration** allows you to use Apollo features **without runn This is useful for developers who want Apollo functionality but prefer a more minimal approach. There are two supported methods: -- [Lightweight JSON](https://lunarclient.dev/apollo/developers/lightweight/json/getting-started) -- [Lightweight Protobuf](https://lunarclient.dev/apollo/developers/lightweight/protobuf/getting-started) +- [Lightweight JSON](https://lunarclient.dev/apollo/developers/lightweight/json) +- [Lightweight Protobuf](https://lunarclient.dev/apollo/developers/lightweight/protobuf) Both approaches achieve the same goal, but with different trade-offs in terms of **complexity and flexibility**. -Read the [Lightweight introduction](https://lunarclient.dev/apollo/developers/lightweight/introduction) documentation to get started. +Read the [Lightweight introduction](https://lunarclient.dev/apollo/developers/lightweight) documentation to get started. ## Examples diff --git a/docs/acceptable-use.mdx b/docs/acceptable-use.mdx index 1ddf215c..49226e1c 100644 --- a/docs/acceptable-use.mdx +++ b/docs/acceptable-use.mdx @@ -7,20 +7,22 @@ import { Callout } from 'nextra-theme-docs' any experiences you create must still be compliant with the Minecraft EULA and Lunar Client Terms of Service. -## Current modules -All current Apollo modules are available for use by all servers, without charge. The intent of -Apollo is to help servers create amazing experiences for Lunar Client players. +## Public Modules +Apollo includes a wide range of publicly available modules that can be used by all servers at no cost. -## Future modules -We _may_ release future Apollo modules / features that require an approval process for servers -to use. This would be done if a new module / feature is: +The intent of Apollo is to encourage servers to integrate directly with Lunar Client to create better player experiences. + +## Private & Future Modules +Apollo includes [private modules](/apollo/developers/private-modules) that require additional approval before use. + +We typically reserve private modules for features that: * Something that requires backing infrastructure that we're unable to provide for free. * Something we're required to keep exclusive for a certain period of time. * Something that directly competes with our [store](https://store.lunarclient.com). ## Questions -If you have further questions about acceptable use, you can join the [Lunar Client Developers Discord](https://lunarclient.dev/discord). +If you have further questions about acceptable use, you can join the [Lunar Client Developers Discord](https://lunarclient.dev/discord). Alternatively, you can request additional verification by contacting us directly via email: **apollo@lunarclient.com**. _Disclaimer_: Our acceptable-use polices may be updated at any time to reflect changes in technology or legal requirements. Modules and features, both current and future, may require approval by Moonsworth, LLC for any reason. diff --git a/docs/developers.mdx b/docs/developers.mdx new file mode 100644 index 00000000..97dc082f --- /dev/null +++ b/docs/developers.mdx @@ -0,0 +1,37 @@ +import { Callout } from 'nextra-theme-docs' + +# Introduction + +Apollo is a powerful tool that allows developers to create custom integrations with Lunar Client. + +Apollo enables server owners and developers to tweak the capabilities of Lunar Client on their server, create custom experiences not possible in vanilla Minecraft, and add quality-of-life features. + +We have a lot planned for Apollo, and are very open to suggestions and contributions. + +This documentation will provide you with all information you'll need to know to start integrating Apollo into your server or plugins! +We'll cover the basics of Apollo, from changing the configuration file to integrating your own features using each module. +Inside the module breakdowns, for developers, we'll include code snippets along with photos, videos, and GIFs related to the module to ensure you get the most out of Apollo. + + + Need just the essentials? [Lightweight](/apollo/developers/lightweight) allows you to use Apollo features without the full plugin! + + +## Useful Links + +🔗 [Lunar Client Website](https://www.lunarclient.com/)
+🔗 [Lunar Client Developers Discord](https://lunarclient.dev/discord)
+🔗 [Apollo GitHub Repository](https://github.com/LunarClient/Apollo)
+ + +## Where to start + +🔗 [FAQ](/apollo/developers/faq)
+🔗 [General](/apollo/developers/general)
+🔗 [Events](/apollo/developers/events)
+🔗 [Options](/apollo/developers/options)
+🔗 [Modules](/apollo/developers/modules)
+🔗 [Private Modules](/apollo/developers/private-modules)
+🔗 [Mod Options](/apollo/developers/mods/2ditems)
+🔗 [Utilities](/apollo/developers/utilities/colors)
+🔗 [Minestom](/apollo/developers/minestom)
+🔗 [Lightweight](/apollo/developers/lightweight) diff --git a/docs/developers/_meta.json b/docs/developers/_meta.json index f6f66360..efe255c4 100644 --- a/docs/developers/_meta.json +++ b/docs/developers/_meta.json @@ -4,10 +4,9 @@ "events": "Events", "options": "Options", "modules": "Modules", - "mods": "Mods", + "private-modules": "Private Modules", + "mods": "Mod Options", "utilities": "Utilities", - "platform-utilities": "Platform Utilities", - "adventure": "Adventure", "minestom": "Minestom", "lightweight": "Lightweight" } diff --git a/docs/developers/events.mdx b/docs/developers/events.mdx index e3cb1f79..a8899eee 100644 --- a/docs/developers/events.mdx +++ b/docs/developers/events.mdx @@ -300,77 +300,3 @@ public class GeneralExample2 implements ApolloListener { } ``` -## Creating Apollo events - -### Creating the event class - -```java -// Normal event -public class CoolApolloEvent implements Event { - - private final ApolloPlayer player; - - public CoolApolloEvent(ApolloPlayer player) { - this.player = player; - } - - public ApolloPlayer getPlayer() { - return this.player; - } -} - -// Cancellable event -public class CoolApolloCancellableEvent implements EventCancellable { - - private final ApolloPlayer player; - - private boolean cancelled; - - public CoolApolloCancellableEvent(ApolloPlayer player) { - this.player = player; - } - - public ApolloPlayer getPlayer() { - return this.player; - } - - @Override - public boolean isCancelled() { - return this.cancelled; - } - - @Override - public void setCancelled(boolean cancel) { - this.cancelled = cancel; - } -} -``` - -### Calling the created event - -```java -// Calling a normal event -public void callCoolApolloEvent(ApolloPlayer player) { - CoolApolloEvent event = new CoolApolloEvent(player); - EventBus.EventResult result = EventBus.getBus().post(event); - - for (Throwable throwable : result.getThrowing()) { - throwable.printStackTrace(); - } -} - -// Calling a cancellable event -public void callCoolApolloCancellableEvent(ApolloPlayer player) { - CoolApolloCancellableEvent event = new CoolApolloCancellableEvent(player); - EventBus.EventResult result = EventBus.getBus().post(event); - - if (!result.getEvent().isCancelled()) { - // Do some action if the event is not cancelled - } - - for (Throwable throwable : result.getThrowing()) { - throwable.printStackTrace(); - } -} -``` - diff --git a/docs/developers/faq.mdx b/docs/developers/faq.mdx index 3ffa32b4..2b46f858 100644 --- a/docs/developers/faq.mdx +++ b/docs/developers/faq.mdx @@ -1,63 +1,22 @@ -# FAQs - -
-Will the legacy API still be supported and maintained? - -As of now, the legacy API will remain functional, but will **not** be improved, extended, or patched further. -We highly recommend switching to the updated version of Apollo. -As of the first release, all features inside the legacy API are available on Apollo. - -
- -
-Where can I view, fork or download the source code to Apollo? - -The source code can be viewed, forked and downloaded on the Lunar Client [GitHub](https://github.com/LunarClient/Apollo). -Feel free to download the source and fork it anyway you can imagine, as long as it's within the ToS and follows the license. - -
- -
-Where can I view the documentation for Apollo? - -You can view the documentation for Apollo, Lunar Client's API, on our Lunar Client [Developers website](https://www.lunarclient.dev/). - -
+import { Callout } from 'nextra-theme-docs' -
-Do I have to run Apollo to interact with Lunar Client? - -You do not need to run the Apollo plugin to interact with Lunar Client, however you do need to follow the protocols found within Apollo. Features might not function properly if you use an alternate implementation that does not implement the Apollo protocols properly. - -
- -
-Where can I suggest new features or enhancements for Apollo? - -You can join the Lunar Client Developers [Discord](https://www.lunarclient.dev/discord) and create a suggestion under `#suggestions`. - -Alternatively, if you have the skills and knowledge, you can create the feature yourself and open a pull request on the [Apollo repository](https://github.com/LunarClient/Apollo) on GitHub. If your feature requires new changes or enhancements to Lunar Client to function, we recommend connecting with us on the [Lunar Client Developers](https://www.lunarclient.dev/discord) Discord server. - -
- -
-Where can I report issues with Apollo or Lunar Client? - -You can report issues on the [Apollo repository](https://github.com/LunarClient/Apollo) issues page, on GitHub. -If you're having an issue with Lunar Client that doesn't involve Apollo, you can create a [support ticket](https://support.lunarclient.com/). +# FAQs -
+ +Have a question not currently covered here? Join our [Developer Discord](lunarclient.dev/discord) to ask! + -
-Where can I contribute to Apollo? +## Do I have to run Apollo to use Apollo features? +No, you do not need to run the Apollo plugin directly. However, you do need to follow the protocols found within Apollo for features to function properly. -You can open a pull request on the [Apollo repository](https://github.com/LunarClient/Apollo) page, on GitHub. +That said, for full compatibility, reliability, and broad integration, we recommend using Apollo directly. -
+## Where can I suggest features or improvements? Where can I report issues? +You are able to share your suggestions in the Lunar Client [developer discord](https://www.lunarclient.dev/discord) under `#suggestions`. -
-Where can I view the Apollo licensing and acceptable use policy? +If you've found an issue with Apollo, you can report it on our [GitHub repository](https://www.github.com/LunarClient/Apollo). For issues unrelated to Apollo, please use the proper channels to report them. -We've listed our [Acceptable Use policy](https://www.lunarclient.dev/apollo/acceptable-use) and [Licensing](https://www.lunarclient.dev/apollo/license) information on the Lunar Client developers website. +## What are Apollo's licensing and acceptable use policies? +Apollo has [licensing](https://www.lunarclient.dev/apollo/license) terms and an [acceptable use](https://www.lunarclient.dev/apollo/acceptable-use) policy that define how it can be used. These are subject to change, and should be reviewed accordingly. -
+You can find both on the Lunar Client documentation website at all times. Make sure your usage complies with these policies, or you may be prevented from using Apollo features in the future. If you have further questions, ask! diff --git a/docs/developers/general.mdx b/docs/developers/general.mdx index 5a2e05f5..806d0527 100644 --- a/docs/developers/general.mdx +++ b/docs/developers/general.mdx @@ -7,6 +7,12 @@ You will see these options appear throughout Apollo and while reading the docume We recommend you use and understand these general features. +### Sending Apollo data after a player joins + +Do **not** rely on the **`PlayerJoinEvent`** as the moment to send Apollo module packets to a player. At join time the client has often **not** completed the Apollo registration handshake yet, so your packets can be dropped or have no effect. + +Listen for **[ApolloRegisterPlayerEvent](/apollo/developers/events#apollo-register-player-event)** instead. It fires once Lunar Client has registered with the server after the client completes the registration flow Apollo expects. That is the right place to apply initial module state, waypoints, and similar setup. + ### Player #### Checking if player is running Lunar Client diff --git a/docs/developers/lightweight/introduction.mdx b/docs/developers/lightweight.mdx similarity index 89% rename from docs/developers/lightweight/introduction.mdx rename to docs/developers/lightweight.mdx index b581efbe..38a85c7f 100644 --- a/docs/developers/lightweight/introduction.mdx +++ b/docs/developers/lightweight.mdx @@ -24,5 +24,5 @@ The message format used to communicate, follows a loosely structured JSON schema ### Usage Methods -🔗 [Protobuf](/apollo/developers/lightweight/protobuf/getting-started)
-🔗 [JSON](/apollo/developers/lightweight/json/getting-started)
+🔗 [Protobuf](/apollo/developers/lightweight/protobuf)
+🔗 [JSON](/apollo/developers/lightweight/json)
diff --git a/docs/developers/lightweight/_meta.json b/docs/developers/lightweight/_meta.json index 88c256cf..2bd8f341 100644 --- a/docs/developers/lightweight/_meta.json +++ b/docs/developers/lightweight/_meta.json @@ -1,5 +1,4 @@ { - "introduction": "Introduction", "protobuf": "Protobuf", "json": "JSON" } diff --git a/docs/developers/lightweight/json/getting-started.mdx b/docs/developers/lightweight/json.mdx similarity index 100% rename from docs/developers/lightweight/json/getting-started.mdx rename to docs/developers/lightweight/json.mdx diff --git a/docs/developers/lightweight/json/_meta.json b/docs/developers/lightweight/json/_meta.json index 83eb3d8b..0959a1f2 100644 --- a/docs/developers/lightweight/json/_meta.json +++ b/docs/developers/lightweight/json/_meta.json @@ -1,5 +1,4 @@ { - "getting-started": "Getting Started", "player-detection": "Player Detection", "serverbound-packets": "Serverbound Packets", "roundtrip-packets": "Roundtrip Packets", diff --git a/docs/developers/lightweight/protobuf/getting-started.mdx b/docs/developers/lightweight/protobuf.mdx similarity index 100% rename from docs/developers/lightweight/protobuf/getting-started.mdx rename to docs/developers/lightweight/protobuf.mdx diff --git a/docs/developers/lightweight/protobuf/_meta.json b/docs/developers/lightweight/protobuf/_meta.json index 83eb3d8b..0959a1f2 100644 --- a/docs/developers/lightweight/protobuf/_meta.json +++ b/docs/developers/lightweight/protobuf/_meta.json @@ -1,5 +1,4 @@ { - "getting-started": "Getting Started", "player-detection": "Player Detection", "serverbound-packets": "Serverbound Packets", "roundtrip-packets": "Roundtrip Packets", diff --git a/docs/developers/modules.mdx b/docs/developers/modules.mdx new file mode 100644 index 00000000..16803d54 --- /dev/null +++ b/docs/developers/modules.mdx @@ -0,0 +1,40 @@ +# Public Modules + +Apollo provides a wide-range of public modules that allow you to integrate direct with Lunar Client. + +These modules are available to all servers using Apollo and do not require any pre-approval. You are still expected to follow our [Acceptable Use](/apollo/acceptable-use) guidelines. + +### Available Modules + +🔗 [Auto Text Hotkey](/apollo/developers/modules/autotexthotkey)
+🔗 [Beam](/apollo/developers/modules/beam)
+🔗 [Border](/apollo/developers/modules/border)
+🔗 [Chat](/apollo/developers/modules/chat)
+🔗 [Colored Fire](/apollo/developers/modules/coloredfire)
+🔗 [Combat](/apollo/developers/modules/combat)
+🔗 [Cooldown](/apollo/developers/modules/cooldown)
+🔗 [Entity](/apollo/developers/modules/entity)
+🔗 [Glint](/apollo/developers/modules/glint)
+🔗 [Glow](/apollo/developers/modules/glow)
+🔗 [Hologram](/apollo/developers/modules/hologram)
+🔗 [Inventory](/apollo/developers/modules/inventory)
+🔗 [Limb](/apollo/developers/modules/limb)
+🔗 [Mod Setting](/apollo/developers/modules/modsetting)
+🔗 [Nametag](/apollo/developers/modules/nametag)
+🔗 [Nick Hider](/apollo/developers/modules/nickhider)
+🔗 [Notification](/apollo/developers/modules/notification)
+🔗 [PayNow](/apollo/developers/modules/paynow)
+🔗 [Packet Enrichment](/apollo/developers/modules/packetenrichment)
+🔗 [Rich Presence](/apollo/developers/modules/richpresence)
+🔗 [Saturation](/apollo/developers/modules/saturation)
+🔗 [Server Link](/apollo/developers/modules/serverlink)
+🔗 [Server Rule](/apollo/developers/modules/serverrule)
+🔗 [Staff Mod](/apollo/developers/modules/staffmod)
+🔗 [Stopwatch](/apollo/developers/modules/stopwatch)
+🔗 [Team](/apollo/developers/modules/team)
+🔗 [Tebex](/apollo/developers/modules/tebex)
+🔗 [Title](/apollo/developers/modules/title)
+🔗 [TNT Countdown](/apollo/developers/modules/tntcountdown)
+🔗 [Transfer](/apollo/developers/modules/transfer)
+🔗 [Vignette](/apollo/developers/modules/vignette)
+🔗 [Waypoint](/apollo/developers/modules/waypoint)
diff --git a/docs/developers/modules/autotexthotkey.mdx b/docs/developers/modules/autotexthotkey.mdx index c154f013..712a745b 100644 --- a/docs/developers/modules/autotexthotkey.mdx +++ b/docs/developers/modules/autotexthotkey.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Auto Text Hotkey Module @@ -16,6 +16,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Set Blocked Text Inputs ```java @@ -38,6 +42,10 @@ public void setBlockChatMesssageTextInputs(boolean value) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + ### Set Blocked Text Inputs ```java @@ -73,6 +81,10 @@ public void setBlockChatMesssageTextInputs(boolean value) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + ### Set Blocked Text Inputs ```java diff --git a/docs/developers/modules/beam.mdx b/docs/developers/modules/beam.mdx index 57253cbb..22bc0cf6 100644 --- a/docs/developers/modules/beam.mdx +++ b/docs/developers/modules/beam.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Beam Module @@ -28,6 +28,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Beam ```java @@ -129,6 +133,10 @@ Custom colors can be created from any RGB values using `new Color(int red, int g + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Beam** ```java @@ -168,6 +176,10 @@ public void resetBeamsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Beam** ```java diff --git a/docs/developers/modules/border.mdx b/docs/developers/modules/border.mdx index 3532abea..9e708cab 100644 --- a/docs/developers/modules/border.mdx +++ b/docs/developers/modules/border.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Border Module @@ -26,6 +25,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Border ```java @@ -162,6 +165,10 @@ Custom colors can be created from any RGB values using `new Color(int red, int g + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Border** ```java @@ -206,6 +213,10 @@ public void resetBordersExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Border** ```java diff --git a/docs/developers/modules/chat.mdx b/docs/developers/modules/chat.mdx index 99cbe387..6065ed97 100644 --- a/docs/developers/modules/chat.mdx +++ b/docs/developers/modules/chat.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Chat Module @@ -24,6 +24,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Live Chat Message ```java @@ -99,6 +103,10 @@ public void removeLiveChatMessageExample() { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Live Chat Message** ```java @@ -185,6 +193,10 @@ public void removeLiveChatMessageExample() { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Live Chat Message** ```java diff --git a/docs/developers/modules/coloredfire.mdx b/docs/developers/modules/coloredfire.mdx index d4256840..b3d876fb 100644 --- a/docs/developers/modules/coloredfire.mdx +++ b/docs/developers/modules/coloredfire.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Colored Fire Module @@ -21,6 +21,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override a Fire Color ```java @@ -62,6 +66,10 @@ public void resetColoredFiresExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override a Fire Color** ```java @@ -100,6 +108,10 @@ public void resetColoredFiresExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override a Fire Color** ```java diff --git a/docs/developers/modules/combat.mdx b/docs/developers/modules/combat.mdx index 06e6382c..22803ec5 100644 --- a/docs/developers/modules/combat.mdx +++ b/docs/developers/modules/combat.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Combat Module @@ -15,6 +15,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Toggle Miss Penalty ```java @@ -27,6 +31,10 @@ public void setDisableMissPenalty(boolean value) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + ### Toggle Miss Penalty ```java @@ -43,6 +51,10 @@ public void setDisableMissPenalty(boolean value) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + ### Toggle Miss Penalty ```java diff --git a/docs/developers/modules/cooldown.mdx b/docs/developers/modules/cooldown.mdx index 16c48fe4..6b0e2888 100644 --- a/docs/developers/modules/cooldown.mdx +++ b/docs/developers/modules/cooldown.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Cooldown Module @@ -25,6 +25,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Cooldown with an item ```java @@ -174,6 +178,10 @@ All `CooldownStyle` fields are optional; any field left unset will fall back to + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Cooldown with an item** ```java @@ -253,6 +261,10 @@ public void resetCooldownsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Cooldown with an item** ```java diff --git a/docs/developers/modules/entity.mdx b/docs/developers/modules/entity.mdx index 0bbeaff1..9361f0db 100644 --- a/docs/developers/modules/entity.mdx +++ b/docs/developers/modules/entity.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Entity Module @@ -18,6 +18,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override Sheep rainbow state ```java @@ -100,6 +104,10 @@ public void resetFlippedEntityExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override Sheep rainbow state** ```java @@ -172,6 +180,10 @@ public void resetFlippedEntityExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override Sheep rainbow state** ```java diff --git a/docs/developers/modules/glow.mdx b/docs/developers/modules/glow.mdx index 992d6c04..cfd31d39 100644 --- a/docs/developers/modules/glow.mdx +++ b/docs/developers/modules/glow.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Glow Module @@ -34,6 +33,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override a Glow Effect ```java @@ -76,6 +79,10 @@ public void resetGlowEffectsExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override a Glow Effect** ```java @@ -114,6 +121,10 @@ public void resetGlowEffectsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override a Glow Effect** ```java diff --git a/docs/developers/modules/hologram.mdx b/docs/developers/modules/hologram.mdx index db5ab913..92c86898 100644 --- a/docs/developers/modules/hologram.mdx +++ b/docs/developers/modules/hologram.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Hologram Module @@ -21,6 +21,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Hologram ```java @@ -125,6 +129,10 @@ public void resetHologramsExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Hologram** ```java @@ -182,6 +190,10 @@ public void resetHologramsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Hologram** ```java diff --git a/docs/developers/modules/limb.mdx b/docs/developers/modules/limb.mdx index 106c6a2d..c642a9f8 100644 --- a/docs/developers/modules/limb.mdx +++ b/docs/developers/modules/limb.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Limb Module @@ -24,6 +24,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Hide Armor Pieces ```java @@ -122,6 +126,10 @@ public void resetBodyExample(Player viewer, Player target) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Hide Armor Pieces** ```java @@ -178,6 +186,10 @@ public void resetBodyExample(Player viewer, Player target) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Hide Armor Pieces** ```java diff --git a/docs/developers/modules/modsetting.mdx b/docs/developers/modules/modsetting.mdx index a4bf847a..127b46e3 100644 --- a/docs/developers/modules/modsetting.mdx +++ b/docs/developers/modules/modsetting.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Mod Setting Module @@ -91,6 +91,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + **Disable Lighting Mod** ```java @@ -148,6 +152,10 @@ public void requestInstalledModsExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Disable Lighting Mod** ```java @@ -213,6 +221,10 @@ public void requestInstalledModsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Disable Lighting Mod** ```java diff --git a/docs/developers/modules/nametag.mdx b/docs/developers/modules/nametag.mdx index 5ca13ec0..081f8acd 100644 --- a/docs/developers/modules/nametag.mdx +++ b/docs/developers/modules/nametag.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Nametag Module @@ -31,6 +30,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override a Nametag ```java @@ -101,6 +104,10 @@ public void resetNametagsExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override a Nametag** ```java @@ -153,6 +160,10 @@ public void resetNametagsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override a Nametag** ```java diff --git a/docs/developers/modules/nickhider.mdx b/docs/developers/modules/nickhider.mdx index 3b0cbe7c..f4ac0496 100644 --- a/docs/developers/modules/nickhider.mdx +++ b/docs/developers/modules/nickhider.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Nick Hider Module @@ -17,6 +17,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override a Nick ```java @@ -39,6 +43,10 @@ public void resetNickExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override a Nick** ```java @@ -64,6 +72,10 @@ public void resetNickExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override a Nick** ```java diff --git a/docs/developers/modules/notification.mdx b/docs/developers/modules/notification.mdx index f9b506b1..9732920a 100644 --- a/docs/developers/modules/notification.mdx +++ b/docs/developers/modules/notification.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Notification Module @@ -26,6 +25,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Notification ```java @@ -109,6 +112,10 @@ If this field is left empty (null) it'll display a generic info icon, as display + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Notification** ```java @@ -146,6 +153,10 @@ public void resetNotificationsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Notification** ```java diff --git a/docs/developers/modules/packetenrichment.mdx b/docs/developers/modules/packetenrichment.mdx index 5edf9a5a..3aba0e1c 100644 --- a/docs/developers/modules/packetenrichment.mdx +++ b/docs/developers/modules/packetenrichment.mdx @@ -1,5 +1,4 @@ -import { Callout } from 'nextra-theme-docs' -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Packet Enrichment Module @@ -22,6 +21,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + The majority of this module is handled through Apollo's [event system](/apollo/developers/events). The following events are related to the packet enrichment module, you can find more information about each event on the [events page](/apollo/developers/events). @@ -35,6 +38,10 @@ The following events are related to the packet enrichment module, you can find m + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + Visit [Apollo Serverbound packets](/apollo/developers/lightweight/protobuf/serverbound-packets) diff --git a/docs/developers/modules/paynow.mdx b/docs/developers/modules/paynow.mdx index ab731fe2..d3f8120d 100644 --- a/docs/developers/modules/paynow.mdx +++ b/docs/developers/modules/paynow.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # PayNow Module @@ -69,6 +68,10 @@ Explore each integration by cycling through each tab to find the best fit for yo + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + **Display PayNow Embedded Checkout** ```java @@ -102,6 +105,10 @@ public void displayPayNowEmbeddedCheckoutExample(Player viewer, String checkoutT + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Display PayNow Embedded Checkout** @@ -122,6 +129,10 @@ public void displayPayNowEmbeddedCheckoutExample(Player viewer, String checkoutT + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Display PayNow Embedded Checkout** ```java diff --git a/docs/developers/modules/richpresence.mdx b/docs/developers/modules/richpresence.mdx index e6a23f76..17f40429 100644 --- a/docs/developers/modules/richpresence.mdx +++ b/docs/developers/modules/richpresence.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Rich Presence Module @@ -35,6 +34,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override Rich Presence ```java @@ -120,6 +123,10 @@ public void resetServerRichPresenceExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override Rich Presence** ```java @@ -152,6 +159,10 @@ public void resetServerRichPresenceExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override Rich Presence** ```java diff --git a/docs/developers/modules/serverlink.mdx b/docs/developers/modules/serverlink.mdx index 081a2dec..9b6ff87f 100644 --- a/docs/developers/modules/serverlink.mdx +++ b/docs/developers/modules/serverlink.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Server Link Module @@ -22,6 +22,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Override the Server Link Menu resource ```java @@ -120,6 +124,10 @@ public void resetServerLinksExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Override the Server Link Menu resource** ```java @@ -201,6 +209,10 @@ public void resetServerLinksExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Override the Server Link Menu resource** ```java diff --git a/docs/developers/modules/serverrule.mdx b/docs/developers/modules/serverrule.mdx index 16133b80..0be83b20 100644 --- a/docs/developers/modules/serverrule.mdx +++ b/docs/developers/modules/serverrule.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Server Rule Module @@ -24,6 +23,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Toggle Anti Portal Traps ```java @@ -56,6 +59,10 @@ public void setNametagRenderDistanceExample(int value) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Toggle Anti Portal Traps** ```java @@ -96,6 +103,10 @@ public void setNametagRenderDistanceExample(int value) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Toggle Anti Portal Traps** ```java diff --git a/docs/developers/modules/staffmod.mdx b/docs/developers/modules/staffmod.mdx index 534c7524..92f02101 100644 --- a/docs/developers/modules/staffmod.mdx +++ b/docs/developers/modules/staffmod.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Staff Mod Module @@ -24,6 +24,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Enable Staff Mods ```java @@ -61,6 +65,10 @@ public void disableStaffModsExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + ### Enable Staff Mods ```java @@ -93,6 +101,10 @@ public void disableStaffModsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Enable Staff Mods** ```java diff --git a/docs/developers/modules/stopwatch.mdx b/docs/developers/modules/stopwatch.mdx index 37f16039..ade32cf4 100644 --- a/docs/developers/modules/stopwatch.mdx +++ b/docs/developers/modules/stopwatch.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Stopwatch Module @@ -26,6 +26,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Start Stopwatch ```java @@ -57,6 +61,10 @@ public void resetStopwatchExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Start Stopwatch** ```java @@ -88,6 +96,10 @@ public void resetStopwatchExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Start Stopwatch** ```java diff --git a/docs/developers/modules/team.mdx b/docs/developers/modules/team.mdx index e587e315..0b80896a 100644 --- a/docs/developers/modules/team.mdx +++ b/docs/developers/modules/team.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Team Module @@ -26,6 +26,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ```java private final Map teamsByTeamId = new ConcurrentHashMap<>(); private final Map teamsByPlayerUuid = new ConcurrentHashMap<>(); @@ -279,6 +283,10 @@ Custom colors can be created from any RGB values using `new Color(int red, int g + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + ```java private final Map teamsByTeamId = new ConcurrentHashMap<>(); private final Map teamsByPlayerUuid = new ConcurrentHashMap<>(); @@ -463,6 +471,10 @@ public class Team { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + ```java private final Map teamsByTeamId = new ConcurrentHashMap<>(); private final Map teamsByPlayerUuid = new ConcurrentHashMap<>(); diff --git a/docs/developers/modules/tebex.mdx b/docs/developers/modules/tebex.mdx index c622b880..4a736e6f 100644 --- a/docs/developers/modules/tebex.mdx +++ b/docs/developers/modules/tebex.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Tebex Module @@ -79,6 +78,10 @@ Explore each integration by cycling through each tab to find the best fit for yo + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + **Display Tebex Embedded Checkout** ```java @@ -112,6 +115,10 @@ public void displayTebexEmbeddedCheckoutExample(Player viewer, String basketIden + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Display Tebex Embedded Checkout** @@ -136,6 +143,10 @@ public void displayTebexEmbeddedCheckoutExample(Player viewer, String basketIden + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Display Tebex Embedded Checkout** ```java diff --git a/docs/developers/modules/title.mdx b/docs/developers/modules/title.mdx index f8b71cff..4d8bb03d 100644 --- a/docs/developers/modules/title.mdx +++ b/docs/developers/modules/title.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Title Module @@ -19,6 +19,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Title ```java @@ -141,6 +145,10 @@ public void setClearTitleOnServerSwitch(boolean value) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Displaying a Title** ```java @@ -214,6 +222,10 @@ public void setClearTitleOnServerSwitch(boolean value) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Displaying a Title** ```java diff --git a/docs/developers/modules/tntcountdown.mdx b/docs/developers/modules/tntcountdown.mdx index c1d9ebfe..3e527f4f 100644 --- a/docs/developers/modules/tntcountdown.mdx +++ b/docs/developers/modules/tntcountdown.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # TNT Countdown Module @@ -22,6 +21,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Set TNT Countdown Ticks Option ```java @@ -91,6 +94,10 @@ private void onTntSpawn(EntitySpawnEvent event) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Set TNT Countdown Ticks Option** ```java @@ -167,6 +174,10 @@ private void onTntSpawn(EntitySpawnEvent event) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Create TNT Countdown Message** ```java diff --git a/docs/developers/modules/transfer.mdx b/docs/developers/modules/transfer.mdx index 51d252ce..b2ea55b0 100644 --- a/docs/developers/modules/transfer.mdx +++ b/docs/developers/modules/transfer.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Transfer Module @@ -75,6 +74,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Transfer Player ```java @@ -162,6 +165,10 @@ public void pingExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Transfer Player** ```java @@ -246,6 +253,10 @@ public void pingExample(Player player) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Transfer Player** ```java diff --git a/docs/developers/modules/vignette.mdx b/docs/developers/modules/vignette.mdx index cda4e17b..b39b4183 100644 --- a/docs/developers/modules/vignette.mdx +++ b/docs/developers/modules/vignette.mdx @@ -1,4 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Vignette Module @@ -17,6 +17,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Display Vignette ```java @@ -60,6 +64,10 @@ public void resetVignetteExample(Player viewer) { + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + **Display Vignette** ```java @@ -86,6 +94,10 @@ public void resetVignetteExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + **Display Vignette** ```java diff --git a/docs/developers/modules/waypoint.mdx b/docs/developers/modules/waypoint.mdx index 57cedba4..6a541b60 100644 --- a/docs/developers/modules/waypoint.mdx +++ b/docs/developers/modules/waypoint.mdx @@ -1,5 +1,4 @@ -import { Tab, Tabs } from 'nextra-theme-docs' -import { Callout } from 'nextra-theme-docs' +import { Callout, Tab, Tabs } from 'nextra-theme-docs' # Waypoint Module @@ -29,6 +28,10 @@ Explore each integration by cycling through each tab, to find the best fit for y + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + ### Displaying a Waypoint ```java @@ -144,6 +147,10 @@ Custom colors can be created from any RGB values using `new Color(int red, int g + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + Make sure the server is sending the world name to the client as show in the [Player Detection](/apollo/developers/lightweight/protobuf/player-detection) example. @@ -189,6 +196,10 @@ public void resetWaypointsExample(Player viewer) { + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + Make sure the server is sending the world name to the client as show in the [Player Detection](/apollo/developers/lightweight/json/player-detection) example. diff --git a/docs/developers/private-modules.mdx b/docs/developers/private-modules.mdx new file mode 100644 index 00000000..bf57d20f --- /dev/null +++ b/docs/developers/private-modules.mdx @@ -0,0 +1,18 @@ +# Private Modules + +Apollo has private modules, that are access restricted extensions of the API. + +These modules are only available on pre-approved servers and will not work on servers that have not received approval. These private modules are still subject to our [Acceptable Use](/apollo/acceptable-use) guidelines. + +### How to Request Access + +We are not currently granting broad access to private modules. However, if you have a specific use-case in mind, you may request access by contacting **apollo@lunarclient.com**. + +Please include: +- Your server name & domain +- A description of your use-case +- Any relevant code snippets (if applicable) + +### Available Modules + +🔗 [Cosmetic](/apollo/developers/private-modules/cosmetic)
diff --git a/docs/developers/private-modules/_meta.json b/docs/developers/private-modules/_meta.json new file mode 100644 index 00000000..482e70cb --- /dev/null +++ b/docs/developers/private-modules/_meta.json @@ -0,0 +1,3 @@ +{ + "cosmetic": "Cosmetic" +} diff --git a/docs/developers/private-modules/cosmetic.mdx b/docs/developers/private-modules/cosmetic.mdx new file mode 100644 index 00000000..e69de29b diff --git a/docs/developers/utilities/_meta.json b/docs/developers/utilities/_meta.json new file mode 100644 index 00000000..9007de2b --- /dev/null +++ b/docs/developers/utilities/_meta.json @@ -0,0 +1,8 @@ +{ + "colors": "Colors", + "cuboids": "Cuboids", + "icons": "Icons", + "locations": "Locations", + "adventure": "Adventure", + "platform-utilities": "Platform Utilities" +} diff --git a/docs/developers/adventure.mdx b/docs/developers/utilities/adventure.mdx similarity index 100% rename from docs/developers/adventure.mdx rename to docs/developers/utilities/adventure.mdx diff --git a/docs/developers/platform-utilities.mdx b/docs/developers/utilities/platform-utilities.mdx similarity index 100% rename from docs/developers/platform-utilities.mdx rename to docs/developers/utilities/platform-utilities.mdx diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 3c167a99..3dad62b7 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -13,7 +13,7 @@ We'll cover the basics of Apollo, from changing the configuration file to integr Inside the module breakdowns, for developers, we'll include code snippets along with photos, videos, and GIFs related to the module to ensure you get the most out of Apollo. - Need just the essentials? [Lightweight](/apollo/developers/lightweight/introduction) allows you to use Apollo features without the full plugin! + Need just the essentials? [Lightweight](/apollo/developers/lightweight) allows you to use Apollo features without the full plugin! ## Useful Links diff --git a/docs/server-owners.mdx b/docs/server-owners.mdx new file mode 100644 index 00000000..33bcb94a --- /dev/null +++ b/docs/server-owners.mdx @@ -0,0 +1,9 @@ +# Server Owners + +Use this section if you run a server and want Apollo’s **configuration** and **commands**, without writing custom plugin code. + +## Topics + +🔗 [Configuration](/apollo/server-owners/config) `config.yml`, `mods.yml`, and examples.
+🔗 [Commands](/apollo/server-owners/commands) operator commands exposed by the Apollo plugin.
+🔗 [FAQ](/apollo/server-owners/faq) downloads, setup, and common questions. diff --git a/docs/server-owners/config.mdx b/docs/server-owners/config.mdx index 4fdd7459..e9952fad 100644 --- a/docs/server-owners/config.mdx +++ b/docs/server-owners/config.mdx @@ -8,14 +8,14 @@ The Apollo configurations are split into two parts: The settings for the `config.yml` are generated with examples and comments, which you can modify to your liking. -The `mods.yml` is not generated with its settings or comments, which will require you to refer to each mods documentation ([example](/apollo/developers/mods/2ditems)) for the keys and value(s) you can set. +The `mods.yml` is not generated with its settings or comments, which will require you to refer to each mod’s documentation under [Mod Options](/apollo/developers/mods/2ditems) for the keys and value(s) you can set. The settings in the `mods.yml` will override the mod options for players on Lunar Client while they are on your server and revert them back after they leave. ## Mod Configuration Example The following example is not a complete list of mod settings or guaranteed to be up-to-date. - You should refer to the mods documentation ([example](/apollo/developers/mods/2ditems)) for the latest mod settings. + You should refer to the [Mod Settings](/apollo/developers/mods/2ditems) documentation for the latest mod settings. ```yaml diff --git a/docs/server-owners/faq.mdx b/docs/server-owners/faq.mdx index db322deb..05d951bf 100644 --- a/docs/server-owners/faq.mdx +++ b/docs/server-owners/faq.mdx @@ -1,63 +1,27 @@ -# FAQs - -
-What is the difference between the legacy Lunar Client API and Apollo API? - -The legacy API was hastily created specifically for features Lunar Network needed. -Whereas, Apollo was created with the intention to add more general functionality for all types of servers and gamemodes. - -
- -
-Can I continue to use the legacy Lunar Client API? - -Yes, as of now you're able to keep using the legacy Lunar Client API. -However, we no longer recommend the legacy API, and you won't be able to use the newest features added to Apollo. -We will eventually discontinue support for the legacy API, so it's highly recommended updating to use Apollo. - -
- -
-Why should I use Apollo? What benefits does Apollo have for my server? +import { Callout } from 'nextra-theme-docs' -Apollo offers you the chance to implement features that aren't possible in your current version of Minecraft and provide quality of life features to your players. -In addition, we have plans to add multiple server-sided improvements such as chuck caching, client-side rendering, and much more. - -
- -
-What Minecraft Versions does Apollo support? - -Apollo is currently set up to work with all versions that Lunar Client supports, with a single installation. - -
- -
-Where can I download a compiled version of Apollo? - -You can download a compiled version of Apollo and the official Apollo implementation plugin on the [downloads page](https://www.lunarclient.dev/apollo/downloads). - -
+# FAQs -
-Can I use Apollo without any programming knowledge? + +Have a question not currently covered here? Join our [Developer Discord](lunarclient.dev/discord) to ask! + -Yes! -You can [download](https://www.lunarclient.dev/apollo/downloads) the Apollo implementation plugin, and drop it into your plugins' folder. -The Apollo implementation plugin has a prebuilt configuration file, which can be edited very easily. +## Where can I download Apollo? +You can find direct downloads to Apollo on our official [download page](https://www.lunarclient.dev/apollo/downloads). -
+## Why should I use Apollo? What benefits does it provide? +Apollo unlocks features that go beyond what standard Minecraft supports, while also adding quality-of-life improvements for your players. -
-Can I use Apollo for free? +## What Minecraft versions does Apollo support? +Apollo has been fully tested and confirmed to work on all versions supported by Lunar Client. -Yes! Lunar Client has no intentions of ever charging for usage of Apollo. +## Can I use Apollo without programming experience? +Yes! You can download the official Apollo plugin and then place it into your `plugins` folder. It includes a pre-built configuration file that can be easily modified without developer experience required. -
+That said, there is far more potential when you have a developer work on custom integrations. -
-How can I get my server name to appear on the Lunar Client friend menu, launcher and other places? +## How can I get my server listed on Discord & Lunar Client rich presence placements? +To have your server appear in places like the friend menu, friend activity, and Discord rich presence, you can submit your server to the Lunar Client [ServerMappings](https://www.lunarclient.dev/server-mappings/introduction). -As of now, you can submit your server to the [Lunar Client ServerMappings](https://github.com/LunarClient/ServerMappings) repository on GitHub, where you'll need to provide information and branding around your server. +This requires you to provide relevant information and branding for your server, so we're able to promote your server accordingly. -
From 923a023f1eeaecaef69c7704a7a1fa7dae74eb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 5 May 2026 00:00:14 +0200 Subject: [PATCH 3/7] Add `Profile` to `ItemStackIcon` (#274) * Add `Profile` to `ItemStackIcon` * Publish * Fix typo * make profile uuid nullable * dont publish on this branch --- .../apollo/common/icon/ItemStackIcon.java | 10 +++ .../apollo/common/profile/Profile.java | 64 +++++++++++++++++ .../apollo/network/NetworkTypes.java | 56 ++++++++++++++- .../lightweight/json/object-util.mdx | 18 +++++ docs/developers/lightweight/protobuf.mdx | 6 +- .../lightweight/protobuf/object-util.mdx | 20 ++++++ docs/developers/modules/cooldown.mdx | 70 +++++++++++++++++++ docs/developers/utilities/icons.mdx | 65 +++++++++++++++++ .../api/listener/ApolloPlayerApiListener.java | 1 + .../api/module/CooldownApiExample.java | 25 +++++++ .../example/command/CooldownCommand.java | 10 ++- .../example/module/impl/CooldownExample.java | 2 + .../json/module/CooldownJsonExample.java | 19 +++++ .../apollo/example/json/util/JsonUtil.java | 18 +++++ .../proto/module/CooldownProtoExample.java | 21 ++++++ .../example/proto/util/ProtobufUtil.java | 21 ++++++ gradle/libs.versions.toml | 2 +- 17 files changed, 419 insertions(+), 9 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/common/profile/Profile.java diff --git a/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java b/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java index 97ca66ab..2df8cb7f 100644 --- a/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java +++ b/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java @@ -23,8 +23,10 @@ */ package com.lunarclient.apollo.common.icon; +import com.lunarclient.apollo.common.profile.Profile; import lombok.Builder; import lombok.Getter; +import org.jetbrains.annotations.Nullable; /** * Represents an item stack icon. @@ -59,4 +61,12 @@ public final class ItemStackIcon extends Icon { */ int customModelData; + /** + * Returns the icon {@link Profile}. + * + * @return the icon profile + * @since 1.2.6 + */ + @Nullable Profile profile; + } diff --git a/api/src/main/java/com/lunarclient/apollo/common/profile/Profile.java b/api/src/main/java/com/lunarclient/apollo/common/profile/Profile.java new file mode 100644 index 00000000..2420a46b --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/profile/Profile.java @@ -0,0 +1,64 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.common.profile; + +import java.util.UUID; +import lombok.Builder; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a profile attached to the {@link com.lunarclient.apollo.common.icon.ItemStackIcon}. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class Profile { + + /** + * Returns the profile {@link UUID} id. + * + * @return the profile id + * @since 1.2.6 + */ + @Nullable UUID id; + + /** + * Returns the profile {@link String} texture. + * + * @return the profile texture + * @since 1.2.6 + */ + String texture; + + /** + * Returns the profile {@link String} signature. + * + * @return the profile signature + * @since 1.2.6 + */ + String signature; + +} diff --git a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java index 6d20395f..df89e825 100644 --- a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java +++ b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java @@ -35,6 +35,7 @@ import com.lunarclient.apollo.common.location.ApolloBlockLocation; import com.lunarclient.apollo.common.location.ApolloLocation; import com.lunarclient.apollo.common.location.ApolloPlayerLocation; +import com.lunarclient.apollo.common.profile.Profile; import com.lunarclient.apollo.common.v1.EntityId; import com.lunarclient.apollo.common.v1.Uuid; import com.lunarclient.apollo.module.packetenrichment.PlayerInfo; @@ -538,6 +539,10 @@ public static com.lunarclient.apollo.common.v1.ItemStackIcon toProtobuf(ItemStac builder.setItemName(icon.getItemName()); } + if (icon.getProfile() != null) { + builder.setProfile(NetworkTypes.toProtobuf(icon.getProfile())); + } + return builder.build(); } @@ -550,11 +555,56 @@ public static com.lunarclient.apollo.common.v1.ItemStackIcon toProtobuf(ItemStac * @since 1.2.5 */ public static ItemStackIcon fromProtobuf(com.lunarclient.apollo.common.v1.ItemStackIcon icon) { - return ItemStackIcon.builder() + ItemStackIcon.ItemStackIconBuilder builder = ItemStackIcon.builder() .itemName(icon.getItemName()) .itemId(icon.getItemId()) - .customModelData(icon.getCustomModelData()) - .build(); + .customModelData(icon.getCustomModelData()); + + if (icon.hasProfile()) { + builder.profile(NetworkTypes.fromProtobuf(icon.getProfile())); + } + + return builder.build(); + } + + /** + * Converts a {@link Profile} object to a + * {@link com.lunarclient.apollo.common.v1.Profile} proto message. + * + * @param object the profile + * @return the proto profile message + * @since 1.2.6 + */ + public static com.lunarclient.apollo.common.v1.Profile toProtobuf(Profile object) { + com.lunarclient.apollo.common.v1.Profile.Builder builder = com.lunarclient.apollo.common.v1.Profile.newBuilder() + .setTexture(object.getTexture()) + .setSignature(object.getSignature()); + + if (object.getId() != null) { + builder.setId(NetworkTypes.toProtobuf(object.getId())); + } + + return builder.build(); + } + + /** + * Converts a {@link com.lunarclient.apollo.common.v1.Profile} + * proto message to a {@link Profile} object. + * + * @param message the profile message + * @return the profile object + * @since 1.2.6 + */ + public static Profile fromProtobuf(com.lunarclient.apollo.common.v1.Profile message) { + Profile.ProfileBuilder builder = Profile.builder() + .texture(message.getTexture()) + .signature(message.getSignature()); + + if (message.hasId()) { + builder.id(NetworkTypes.fromProtobuf(message.getId())); + } + + return builder.build(); } /** diff --git a/docs/developers/lightweight/json/object-util.mdx b/docs/developers/lightweight/json/object-util.mdx index de62365e..809b7210 100644 --- a/docs/developers/lightweight/json/object-util.mdx +++ b/docs/developers/lightweight/json/object-util.mdx @@ -85,6 +85,10 @@ Icon-related methods ```java public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData) { + return JsonUtil.createItemStackIconObject(itemName, itemId, customModelData, null); +} + +public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData, @Nullable JsonObject profile) { JsonObject itemIconObject = new JsonObject(); if (itemName != null) { itemIconObject.addProperty("item_name", itemName); @@ -94,11 +98,25 @@ public static JsonObject createItemStackIconObject(@Nullable String itemName, in itemIconObject.addProperty("custom_model_data", customModelData); + if (profile != null) { + itemIconObject.add("profile", profile); + } + JsonObject iconObject = new JsonObject(); iconObject.add("item_stack", itemIconObject); return iconObject; } +public static JsonObject createProfileObject(@Nullable UUID id, @NotNull String texture, @NotNull String signature) { + JsonObject profileObject = new JsonObject(); + if (id != null) { + profileObject.add("id", JsonUtil.createUuidObject(id)); + } + profileObject.addProperty("texture", texture); + profileObject.addProperty("signature", signature); + return profileObject; +} + public static JsonObject createResourceLocationIconObject(@NotNull String resourceLocation) { JsonObject resourceIconObject = new JsonObject(); resourceIconObject.addProperty("resource_location", resourceLocation); diff --git a/docs/developers/lightweight/protobuf.mdx b/docs/developers/lightweight/protobuf.mdx index a90ec074..c1129b9d 100644 --- a/docs/developers/lightweight/protobuf.mdx +++ b/docs/developers/lightweight/protobuf.mdx @@ -26,7 +26,7 @@ Available fields for each message, including their types, are available on the B com.lunarclient apollo-protos - 0.1.0 + 0.1.6 ``` @@ -41,7 +41,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api 'com.lunarclient:apollo-protos:0.1.0' + api 'com.lunarclient:apollo-protos:0.1.6' } ```
@@ -55,7 +55,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api("com.lunarclient:apollo-protos:0.1.0") + api("com.lunarclient:apollo-protos:0.1.6") } ```
diff --git a/docs/developers/lightweight/protobuf/object-util.mdx b/docs/developers/lightweight/protobuf/object-util.mdx index bff0bbde..7f5817b7 100644 --- a/docs/developers/lightweight/protobuf/object-util.mdx +++ b/docs/developers/lightweight/protobuf/object-util.mdx @@ -98,6 +98,10 @@ Icon-related methods ```java public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData) { + return ProtobufUtil.createItemStackIconProto(itemName, itemId, customModelData, null); +} + +public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData, @Nullable Profile profile) { ItemStackIcon.Builder iconBuilder = ItemStackIcon.newBuilder() .setItemId(itemId) .setCustomModelData(customModelData); @@ -106,9 +110,25 @@ public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, iconBuilder.setItemName(itemName); } + if (profile != null) { + iconBuilder.setProfile(profile); + } + return iconBuilder.build(); } +public static Profile createProfileProto(@Nullable UUID id, String texture, String signature) { + Profile.Builder builder = Profile.newBuilder() + .setTexture(texture) + .setSignature(signature); + + if (id != null) { + builder.setId(ProtobufUtil.createUuidProto(id)); + } + + return builder.build(); +} + public static ResourceLocationIcon createResourceLocationIconProto(String resourceLocation) { return ResourceLocationIcon.newBuilder() .setResourceLocation(resourceLocation) diff --git a/docs/developers/modules/cooldown.mdx b/docs/developers/modules/cooldown.mdx index 6b0e2888..6c695fea 100644 --- a/docs/developers/modules/cooldown.mdx +++ b/docs/developers/modules/cooldown.mdx @@ -74,6 +74,31 @@ public void displayCooldownWithStyleExample(Player viewer) { } ``` +### Displaying a Cooldown with a player texture + +```java +public void displayCooldownWithPlayerTextureExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.cooldownModule.displayCooldown(apolloPlayer, Cooldown.builder() + .name("player-head-cooldown") + .duration(Duration.ofSeconds(15)) + .icon(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build() + ) + .build() + ); + }); +} +``` + ### Displaying a Cooldown with a resource ```java @@ -104,6 +129,7 @@ public void removeCooldownExample(Player viewer) { apolloPlayerOpt.ifPresent(apolloPlayer -> { this.cooldownModule.removeCooldown(apolloPlayer, "enderpearl-cooldown"); this.cooldownModule.removeCooldown(apolloPlayer, "book-cooldown"); + this.cooldownModule.removeCooldown(apolloPlayer, "player-head-cooldown"); this.cooldownModule.removeCooldown(apolloPlayer, "lunar-cooldown"); }); } @@ -220,6 +246,29 @@ public void displayCooldownWithStyleExample(Player viewer) { } ``` +**Displaying a Cooldown with a player texture** + +```java +public void displayCooldownWithPlayerTextureExample(Player viewer) { + DisplayCooldownMessage message = DisplayCooldownMessage.newBuilder() + .setName("player-head-cooldown") + .setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(15))) + .setIcon(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto( + "PLAYER_HEAD", 0, 0, + ProtobufUtil.createProfileProto( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )) + .build()) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + **Displaying a Cooldown with a resource** ```java @@ -300,6 +349,27 @@ public void displayCooldownWithStyleExample(Player viewer) { } ``` +**Displaying a Cooldown with a player texture** + +```java +public void displayCooldownWithPlayerTextureExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cooldown.v1.DisplayCooldownMessage"); + message.addProperty("name", "player-head-cooldown"); + message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(15))); + message.add("icon", JsonUtil.createItemStackIconObject( + "PLAYER_HEAD", 0, 0, + JsonUtil.createProfileObject( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + **Displaying a Cooldown with a resource** ```java diff --git a/docs/developers/utilities/icons.mdx b/docs/developers/utilities/icons.mdx index 449151b7..1b1b488a 100644 --- a/docs/developers/utilities/icons.mdx +++ b/docs/developers/utilities/icons.mdx @@ -42,6 +42,14 @@ public final class ItemStackIcon extends Icon { */ int customModelData; + /** + * Returns the icon {@link Profile}. + * + * @return the icon profile + * @since 1.2.6 + */ + @Nullable Profile profile; + } ``` @@ -59,6 +67,63 @@ public static ItemStackIcon itemStackNameIconExample() { .itemName("ENDER_PEARL") .build(); } + +public static ItemStackIcon itemStackPlayerTextureIconExample() { + return ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build(); +} +``` + +## `Profile` Builder + +The `Profile` builder is used to apply a player skin texture to a player head `ItemStackIcon`. + +```java +public final class Profile { + + /** + * Returns the profile {@link UUID} id. + * + * @return the profile id + * @since 1.2.6 + */ + @Nullable UUID id; + + /** + * Returns the profile {@link String} texture. + * + * @return the profile texture + * @since 1.2.6 + */ + String texture; + + /** + * Returns the profile {@link String} signature. + * + * @return the profile signature + * @since 1.2.6 + */ + String signature; + +} +``` + +### Sample Code + +```java +public static Profile profileExample() { + return Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build(); +} ``` ## `ResourceLocationIcon` Builder diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java index 0913c1e2..e4dd4579 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java @@ -66,6 +66,7 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) { CooldownExample cooldownExample = this.example.getCooldownExample(); cooldownExample.displayCooldownItemExample(player); cooldownExample.displayCooldownWithStyleExample(player); + cooldownExample.displayCooldownWithPlayerTextureExample(player); cooldownExample.displayCooldownResourceExample(player); ServerLinkExample serverLinkExample = this.example.getServerLinkExample(); diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CooldownApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CooldownApiExample.java index 121eb27e..99eb2596 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CooldownApiExample.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CooldownApiExample.java @@ -27,6 +27,7 @@ import com.lunarclient.apollo.common.ApolloColors; import com.lunarclient.apollo.common.icon.ItemStackIcon; import com.lunarclient.apollo.common.icon.SimpleResourceLocationIcon; +import com.lunarclient.apollo.common.profile.Profile; import com.lunarclient.apollo.example.module.impl.CooldownExample; import com.lunarclient.apollo.module.cooldown.Cooldown; import com.lunarclient.apollo.module.cooldown.CooldownModule; @@ -34,6 +35,7 @@ import com.lunarclient.apollo.player.ApolloPlayer; import java.time.Duration; import java.util.Optional; +import java.util.UUID; import org.bukkit.entity.Player; public class CooldownApiExample extends CooldownExample { @@ -79,6 +81,28 @@ public void displayCooldownWithStyleExample(Player viewer) { }); } + @Override + public void displayCooldownWithPlayerTextureExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.cooldownModule.displayCooldown(apolloPlayer, Cooldown.builder() + .name("player-head-cooldown") + .duration(Duration.ofSeconds(15)) + .icon(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build() + ) + .build() + ); + }); + } + @Override public void displayCooldownResourceExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); @@ -104,6 +128,7 @@ public void removeCooldownExample(Player viewer) { apolloPlayerOpt.ifPresent(apolloPlayer -> { this.cooldownModule.removeCooldown(apolloPlayer, "enderpearl-cooldown"); this.cooldownModule.removeCooldown(apolloPlayer, "book-cooldown"); + this.cooldownModule.removeCooldown(apolloPlayer, "player-head-cooldown"); this.cooldownModule.removeCooldown(apolloPlayer, "lunar-cooldown"); }); } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CooldownCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CooldownCommand.java index bb122312..ceefef91 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CooldownCommand.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CooldownCommand.java @@ -43,7 +43,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command Player player = (Player) sender; if (args.length != 1) { - player.sendMessage("Usage: /cooldown "); + player.sendMessage("Usage: /cooldown "); return true; } @@ -62,6 +62,12 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command break; } + case "displaywithplayertexture": { + cooldownExample.displayCooldownWithPlayerTextureExample(player); + player.sendMessage("Displaying cooldown with player texture...."); + break; + } + case "displayresource": { cooldownExample.displayCooldownResourceExample(player); player.sendMessage("Displaying cooldown resource...."); @@ -81,7 +87,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } default: { - player.sendMessage("Usage: /cooldown "); + player.sendMessage("Usage: /cooldown "); break; } } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CooldownExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CooldownExample.java index e0c8ebc8..a7d6a887 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CooldownExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CooldownExample.java @@ -32,6 +32,8 @@ public abstract class CooldownExample extends ApolloModuleExample { public abstract void displayCooldownWithStyleExample(Player viewer); + public abstract void displayCooldownWithPlayerTextureExample(Player viewer); + public abstract void displayCooldownResourceExample(Player viewer); public abstract void removeCooldownExample(Player viewer); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java index a254be03..2b853532 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java @@ -29,6 +29,7 @@ import com.lunarclient.apollo.example.module.impl.CooldownExample; import java.awt.Color; import java.time.Duration; +import java.util.UUID; import org.bukkit.entity.Player; public class CooldownJsonExample extends CooldownExample { @@ -62,6 +63,24 @@ public void displayCooldownWithStyleExample(Player viewer) { JsonPacketUtil.sendPacket(viewer, message); } + @Override + public void displayCooldownWithPlayerTextureExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cooldown.v1.DisplayCooldownMessage"); + message.addProperty("name", "player-head-cooldown"); + message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(15))); + message.add("icon", JsonUtil.createItemStackIconObject( + "PLAYER_HEAD", 0, 0, + JsonUtil.createProfileObject( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )); + + JsonPacketUtil.sendPacket(viewer, message); + } + @Override public void displayCooldownResourceExample(Player viewer) { JsonObject message = new JsonObject(); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java index 841a9694..974f621e 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java @@ -136,6 +136,10 @@ public static Location toBukkitPlayerLocation(JsonObject message) { } public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData) { + return JsonUtil.createItemStackIconObject(itemName, itemId, customModelData, null); + } + + public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData, @Nullable JsonObject profile) { JsonObject itemIconObject = new JsonObject(); if (itemName != null) { itemIconObject.addProperty("item_name", itemName); @@ -145,11 +149,25 @@ public static JsonObject createItemStackIconObject(@Nullable String itemName, in itemIconObject.addProperty("custom_model_data", customModelData); + if (profile != null) { + itemIconObject.add("profile", profile); + } + JsonObject iconObject = new JsonObject(); iconObject.add("item_stack", itemIconObject); return iconObject; } + public static JsonObject createProfileObject(@Nullable UUID id, @NotNull String texture, @NotNull String signature) { + JsonObject profileObject = new JsonObject(); + if (id != null) { + profileObject.add("id", JsonUtil.createUuidObject(id)); + } + profileObject.addProperty("texture", texture); + profileObject.addProperty("signature", signature); + return profileObject; + } + public static JsonObject createResourceLocationIconObject(@NotNull String resourceLocation) { JsonObject resourceIconObject = new JsonObject(); resourceIconObject.addProperty("resource_location", resourceLocation); diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java index 1834aab5..cdcbec81 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java @@ -33,6 +33,7 @@ import com.lunarclient.apollo.example.proto.util.ProtobufUtil; import java.awt.Color; import java.time.Duration; +import java.util.UUID; import org.bukkit.entity.Player; public class CooldownProtoExample extends CooldownExample { @@ -69,6 +70,26 @@ public void displayCooldownWithStyleExample(Player viewer) { ProtobufPacketUtil.sendPacket(viewer, message); } + @Override + public void displayCooldownWithPlayerTextureExample(Player viewer) { + DisplayCooldownMessage message = DisplayCooldownMessage.newBuilder() + .setName("player-head-cooldown") + .setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(15))) + .setIcon(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto( + "PLAYER_HEAD", 0, 0, + ProtobufUtil.createProfileProto( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )) + .build()) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + @Override public void displayCooldownResourceExample(Player viewer) { DisplayCooldownMessage message = DisplayCooldownMessage.newBuilder() diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java index 2c35b3c3..373fb9fa 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java @@ -29,6 +29,7 @@ import com.lunarclient.apollo.common.v1.Cuboid2D; import com.lunarclient.apollo.common.v1.EntityId; import com.lunarclient.apollo.common.v1.ItemStackIcon; +import com.lunarclient.apollo.common.v1.Profile; import com.lunarclient.apollo.common.v1.ResourceLocationIcon; import com.lunarclient.apollo.common.v1.SimpleResourceLocationIcon; import com.lunarclient.apollo.common.v1.Uuid; @@ -115,6 +116,10 @@ public static Location toBukkitLocation(com.lunarclient.apollo.common.v1.PlayerL } public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData) { + return ProtobufUtil.createItemStackIconProto(itemName, itemId, customModelData, null); + } + + public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData, @Nullable Profile profile) { ItemStackIcon.Builder iconBuilder = ItemStackIcon.newBuilder() .setItemId(itemId) .setCustomModelData(customModelData); @@ -123,9 +128,25 @@ public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, iconBuilder.setItemName(itemName); } + if (profile != null) { + iconBuilder.setProfile(profile); + } + return iconBuilder.build(); } + public static Profile createProfileProto(@Nullable UUID id, String texture, String signature) { + Profile.Builder builder = Profile.newBuilder() + .setTexture(texture) + .setSignature(signature); + + if (id != null) { + builder.setId(ProtobufUtil.createUuidProto(id)); + } + + return builder.build(); + } + public static ResourceLocationIcon createResourceLocationIconProto(String resourceLocation) { return ResourceLocationIcon.newBuilder() .setResourceLocation(resourceLocation) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 573ee4a2..8f6d9054 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ geantyref = "1.3.11" idea = "1.1.7" jetbrains = "24.0.1" lombok = "1.18.38" -protobuf = "0.1.0" +protobuf = "0.1.6" gson = "2.10.1" shadow = "8.1.1" spotless = "6.13.0" From 62f2856f3a8e8baa416545519f727cd61defcc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 5 May 2026 00:28:28 +0200 Subject: [PATCH 4/7] Stopwatch Module v2 (#272) * Stopwatch module overhaul * Add `hideWhenStopped` to stopwatches & timers * Update Module Overview * Overview touchups * fix typos * format fix * update fields to match protos --------- Co-authored-by: Trentin <25537885+TrentinTheKid@users.noreply.github.com> --- .../apollo/common/location/HudPosition.java | 54 ++ .../apollo/module/cooldown/CooldownStyle.java | 2 +- .../apollo/module/stopwatch/Stopwatch.java | 119 ++++ .../module/stopwatch/StopwatchModule.java | 113 ++++ .../apollo/module/stopwatch/Timer.java | 150 +++++ .../module/stopwatch/StopwatchModuleImpl.java | 161 +++++ .../apollo/network/NetworkTypes.java | 31 + docs/developers/modules/stopwatch.mdx | 555 +++++++++++++++++- .../api/module/StopwatchApiExample.java | 102 +++- .../example/command/StopwatchCommand.java | 108 +++- .../example/module/impl/StopwatchExample.java | 18 + .../json/module/StopwatchJsonExample.java | 115 ++++ .../proto/module/StopwatchProtoExample.java | 133 ++++- 13 files changed, 1622 insertions(+), 39 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/stopwatch/Stopwatch.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/stopwatch/Timer.java diff --git a/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java b/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java new file mode 100644 index 00000000..c6f7f4fa --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java @@ -0,0 +1,54 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.common.location; + +import lombok.Builder; +import lombok.Getter; + +/** + * Represents a HUD element position on the client screen. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class HudPosition { + + /** + * Returns the {@code float} X coordinate for this HUD position. + * + * @return the x coordinate + * @since 1.2.6 + */ + float x; + + /** + * Returns the {@code float} Y coordinate for this HUD position. + * + * @return the y coordinate + * @since 1.2.6 + */ + float y; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cooldown/CooldownStyle.java b/api/src/main/java/com/lunarclient/apollo/module/cooldown/CooldownStyle.java index 2641e5a3..9886e19f 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/cooldown/CooldownStyle.java +++ b/api/src/main/java/com/lunarclient/apollo/module/cooldown/CooldownStyle.java @@ -29,7 +29,7 @@ import org.jetbrains.annotations.Nullable; /** - * Represents the {@link Cooldown} style, allowing customization of the circle start, end, edge & text color. + * Represents the {@link Cooldown} style, allowing customization of the circle start, end, edge and text color. * * @since 1.2.5 */ diff --git a/api/src/main/java/com/lunarclient/apollo/module/stopwatch/Stopwatch.java b/api/src/main/java/com/lunarclient/apollo/module/stopwatch/Stopwatch.java new file mode 100644 index 00000000..3afd1df2 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/stopwatch/Stopwatch.java @@ -0,0 +1,119 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.stopwatch; + +import com.lunarclient.apollo.common.location.HudPosition; +import java.awt.Color; +import lombok.Builder; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a stopwatch which can be displayed on the client HUD. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class Stopwatch { + + /** + * Returns the stopwatch {@link String} id. + * + * @return the stopwatch id + * @since 1.2.6 + */ + String id; + + /** + * Returns the stopwatch {@link String} name. + * + * @return the stopwatch name + * @since 1.2.6 + */ + String name; + + /** + * Returns the stopwatch {@code boolean} reset on start. + * + *

If {@code true}, elapsed time is reset on each start.

+ * + * @return whether to reset on start + * @since 1.2.6 + */ + boolean resetOnStart; + + /** + * Returns the stopwatch {@code boolean} prevent modification. + * + *

If {@code true}, the user cannot modify the options for this + * stopwatch on the client side.

+ * + * @return whether modification is prevented + * @since 1.2.6 + */ + boolean preventModification; + + /** + * Returns the stopwatch {@code boolean} hide when stopped. + * + *

If {@code true}, the stopwatch is hidden from the HUD when stopped.

+ * + * @return whether to hide when stopped + * @since 1.2.6 + */ + boolean hideWhenStopped; + + /** + * Returns the stopwatch {@link String} display format. + * + *

A format string (e.g. {@code "mm:ss"}), or {@code null} + * for the default display format.

+ * + * @return the display format string + * @since 1.2.6 + */ + @Nullable String displayFormat; + + /** + * Returns the stopwatch {@link Color} text color. + * + *

If {@code null}, the default color (white) is used.

+ * + * @return the text color + * @since 1.2.6 + */ + @Nullable Color textColor; + + /** + * Returns the stopwatch {@link HudPosition} HUD position. + * + *

If {@code null}, the stopwatch is auto-stacked at the default position.

+ * + * @return the HUD position + * @since 1.2.6 + */ + @Nullable HudPosition hudPosition; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModule.java b/api/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModule.java index bec98316..f39af579 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModule.java +++ b/api/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModule.java @@ -45,28 +45,141 @@ public Collection getSupportedPlatforms() { return Arrays.asList(ApolloPlatform.Kind.SERVER, ApolloPlatform.Kind.PROXY); } + /** + * Adds a {@link Stopwatch} to the HUD for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param stopwatch the stopwatch to add + * @since 1.2.6 + */ + public abstract void addStopwatch(Recipients recipients, Stopwatch stopwatch); + + /** + * Removes the {@link Stopwatch} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the stopwatch id + * @since 1.2.6 + */ + public abstract void removeStopwatch(Recipients recipients, String id); + + /** + * Starts the {@link Stopwatch} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the stopwatch id + * @since 1.2.6 + */ + public abstract void startStopwatch(Recipients recipients, String id); + + /** + * Stops the {@link Stopwatch} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the stopwatch id + * @since 1.2.6 + */ + public abstract void stopStopwatch(Recipients recipients, String id); + + /** + * Resets the {@link Stopwatch} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the stopwatch id + * @since 1.2.6 + */ + public abstract void resetStopwatch(Recipients recipients, String id); + + /** + * Resets all {@link Stopwatch}es for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.6 + */ + public abstract void resetStopwatches(Recipients recipients); + + /** + * Adds a {@link Timer} to the HUD for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param timer the timer to add + * @since 1.2.6 + */ + public abstract void addTimer(Recipients recipients, Timer timer); + + /** + * Removes the {@link Timer} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the timer id + * @since 1.2.6 + */ + public abstract void removeTimer(Recipients recipients, String id); + + /** + * Starts the {@link Timer} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the timer id + * @since 1.2.6 + */ + public abstract void startTimer(Recipients recipients, String id); + + /** + * Stops the {@link Timer} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the timer id + * @since 1.2.6 + */ + public abstract void stopTimer(Recipients recipients, String id); + + /** + * Resets the {@link Timer} with the given id for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param id the timer id + * @since 1.2.6 + */ + public abstract void resetTimer(Recipients recipients, String id); + + /** + * Resets all {@link Timer}s for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.6 + */ + public abstract void resetTimers(Recipients recipients); + /** * Starts the stopwatch for the {@link Recipients}. * * @param recipients the recipients that are receiving the packet + * @deprecated for removal since 1.2.6, use {@link #addStopwatch(Recipients, Stopwatch)} + * and {@link #startStopwatch(Recipients, String)} instead. * @since 1.0.0 */ + @Deprecated public abstract void startStopwatch(Recipients recipients); /** * Stops the stopwatch for the {@link Recipients}. * * @param recipients the recipients that are receiving the packet + * @deprecated for removal since 1.2.6, use {@link #stopStopwatch(Recipients, String)} instead. * @since 1.0.0 */ + @Deprecated public abstract void stopStopwatch(Recipients recipients); /** * Resets the stopwatch for the {@link Recipients}. * * @param recipients the recipients that are receiving the packet + * @deprecated for removal since 1.2.6, use {@link #resetStopwatch(Recipients, String)} instead. * @since 1.0.0 */ + @Deprecated public abstract void resetStopwatch(Recipients recipients); } diff --git a/api/src/main/java/com/lunarclient/apollo/module/stopwatch/Timer.java b/api/src/main/java/com/lunarclient/apollo/module/stopwatch/Timer.java new file mode 100644 index 00000000..99d71861 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/stopwatch/Timer.java @@ -0,0 +1,150 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.stopwatch; + +import com.lunarclient.apollo.common.location.HudPosition; +import java.awt.Color; +import java.time.Duration; +import lombok.Builder; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a timer which can be displayed on the client HUD. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class Timer { + + /** + * Returns the timer {@link String} id. + * + * @return the timer id + * @since 1.2.6 + */ + String id; + + /** + * Returns the timer {@link String} name. + * + * @return the timer name + * @since 1.2.6 + */ + String name; + + /** + * Returns the timer {@link Duration}. + * + * @return the timer duration + * @since 1.2.6 + */ + Duration duration; + + /** + * Returns the timer {@code boolean} loop. + * + *

If {@code true}, the timer restarts automatically when finished.

+ * + * @return whether the timer loops + * @since 1.2.6 + */ + boolean loop; + + /** + * Returns the timer {@code boolean} prevent modification. + * + *

If {@code true}, the user cannot modify the options for this + * timer on the client side.

+ * + * @return whether modification is prevented + * @since 1.2.6 + */ + boolean preventModification; + + /** + * Returns the timer {@code boolean} hide when stopped. + * + *

If {@code true}, the timer is hidden from the HUD when stopped.

+ * + * @return whether to hide when stopped + * @since 1.2.6 + */ + boolean hideWhenStopped; + + /** + * Returns the timer {@link String} display format. + * + *

A format string (e.g. {@code "mm:ss"}), or {@code null} + * for the default display format.

+ * + * @return the display format string + * @since 1.2.6 + */ + @Nullable String displayFormat; + + /** + * Returns the timer {@link Component} title text. + * + *

The on-screen title shown when the timer finishes, + * or {@code null} to skip.

+ * + * @return the title text component + * @since 1.2.6 + */ + @Nullable Component titleText; + + /** + * Returns the timer {@code boolean} in-game notification. + * + *

If {@code true}, an in-game popup is shown when the timer finishes.

+ * + * @return whether an in-game notification is shown + * @since 1.2.6 + */ + boolean inGameNotification; + + /** + * Returns the timer {@link Color} text color. + * + *

If {@code null}, the default color (white) is used.

+ * + * @return the text color + * @since 1.2.6 + */ + @Nullable Color textColor; + + /** + * Returns the timer {@link HudPosition} HUD position. + * + *

If {@code null}, the timer is auto-stacked at the default position.

+ * + * @return the HUD position + * @since 1.2.6 + */ + @Nullable HudPosition hudPosition; + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModuleImpl.java index efba37df..ded30e4b 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/stopwatch/StopwatchModuleImpl.java @@ -23,12 +23,26 @@ */ package com.lunarclient.apollo.module.stopwatch; +import com.lunarclient.apollo.common.ApolloComponent; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.network.NetworkTypes; import com.lunarclient.apollo.player.AbstractApolloPlayer; import com.lunarclient.apollo.recipients.Recipients; +import com.lunarclient.apollo.stopwatch.v1.AddStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.AddTimerMessage; +import com.lunarclient.apollo.stopwatch.v1.RemoveStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.RemoveTimerMessage; import com.lunarclient.apollo.stopwatch.v1.ResetStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.ResetStopwatchesMessage; +import com.lunarclient.apollo.stopwatch.v1.ResetTimerMessage; +import com.lunarclient.apollo.stopwatch.v1.ResetTimersMessage; import com.lunarclient.apollo.stopwatch.v1.StartStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.StartTimerMessage; import com.lunarclient.apollo.stopwatch.v1.StopStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.StopTimerMessage; +import java.awt.Color; import lombok.NonNull; +import net.kyori.adventure.text.Component; /** * Provides the stopwatch module. @@ -55,4 +69,151 @@ public void resetStopwatch(@NonNull Recipients recipients) { recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); } + @Override + public void addStopwatch(@NonNull Recipients recipients, @NonNull Stopwatch stopwatch) { + AddStopwatchMessage.Builder builder = AddStopwatchMessage.newBuilder() + .setId(stopwatch.getId()) + .setName(stopwatch.getName()) + .setResetOnStart(stopwatch.isResetOnStart()) + .setPreventModification(stopwatch.isPreventModification()) + .setHideWhenStopped(stopwatch.isHideWhenStopped()); + + String displayFormat = stopwatch.getDisplayFormat(); + if (displayFormat != null) { + builder.setDisplayFormat(displayFormat); + } + + Color textColor = stopwatch.getTextColor(); + if (textColor != null) { + builder.setTextColor(NetworkTypes.toProtobuf(textColor)); + } + + HudPosition hudPosition = stopwatch.getHudPosition(); + if (hudPosition != null) { + builder.setHudPosition(NetworkTypes.toProtobuf(hudPosition)); + } + + AddStopwatchMessage message = builder.build(); + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void removeStopwatch(@NonNull Recipients recipients, @NonNull String id) { + RemoveStopwatchMessage message = RemoveStopwatchMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void startStopwatch(@NonNull Recipients recipients, @NonNull String id) { + StartStopwatchMessage message = StartStopwatchMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void stopStopwatch(@NonNull Recipients recipients, @NonNull String id) { + StopStopwatchMessage message = StopStopwatchMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void resetStopwatch(@NonNull Recipients recipients, @NonNull String id) { + ResetStopwatchMessage message = ResetStopwatchMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void resetStopwatches(@NonNull Recipients recipients) { + ResetStopwatchesMessage message = ResetStopwatchesMessage.getDefaultInstance(); + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void addTimer(@NonNull Recipients recipients, @NonNull Timer timer) { + AddTimerMessage.Builder builder = AddTimerMessage.newBuilder() + .setId(timer.getId()) + .setName(timer.getName()) + .setDuration(NetworkTypes.toProtobuf(timer.getDuration())) + .setLoop(timer.isLoop()) + .setPreventModification(timer.isPreventModification()) + .setHideWhenStopped(timer.isHideWhenStopped()) + .setInGameNotification(timer.isInGameNotification()); + + String displayFormat = timer.getDisplayFormat(); + if (displayFormat != null) { + builder.setDisplayFormat(displayFormat); + } + + Component titleText = timer.getTitleText(); + if (titleText != null) { + builder.setTitleTextAdventureJsonLines(ApolloComponent.toJson(titleText)); + } + + Color textColor = timer.getTextColor(); + if (textColor != null) { + builder.setTextColor(NetworkTypes.toProtobuf(textColor)); + } + + HudPosition hudPosition = timer.getHudPosition(); + if (hudPosition != null) { + builder.setHudPosition(NetworkTypes.toProtobuf(hudPosition)); + } + + AddTimerMessage message = builder.build(); + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void removeTimer(@NonNull Recipients recipients, @NonNull String id) { + RemoveTimerMessage message = RemoveTimerMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void startTimer(@NonNull Recipients recipients, @NonNull String id) { + StartTimerMessage message = StartTimerMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void stopTimer(@NonNull Recipients recipients, @NonNull String id) { + StopTimerMessage message = StopTimerMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void resetTimer(@NonNull Recipients recipients, @NonNull String id) { + ResetTimerMessage message = ResetTimerMessage.newBuilder() + .setId(id) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void resetTimers(@NonNull Recipients recipients) { + ResetTimersMessage message = ResetTimersMessage.getDefaultInstance(); + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + } diff --git a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java index df89e825..2a0a40ff 100644 --- a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java +++ b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java @@ -35,6 +35,7 @@ import com.lunarclient.apollo.common.location.ApolloBlockLocation; import com.lunarclient.apollo.common.location.ApolloLocation; import com.lunarclient.apollo.common.location.ApolloPlayerLocation; +import com.lunarclient.apollo.common.location.HudPosition; import com.lunarclient.apollo.common.profile.Profile; import com.lunarclient.apollo.common.v1.EntityId; import com.lunarclient.apollo.common.v1.Uuid; @@ -142,6 +143,36 @@ public static Color fromProtobuf(com.lunarclient.apollo.common.v1.Color message) return new Color(message.getColor()); } + /** + * Converts a {@link HudPosition} object to a + * {@link com.lunarclient.apollo.hud.v1.HudPosition} proto message. + * + * @param object the hud position + * @return the proto hud position message + * @since 1.2.6 + */ + public static com.lunarclient.apollo.hud.v1.HudPosition toProtobuf(HudPosition object) { + return com.lunarclient.apollo.hud.v1.HudPosition.newBuilder() + .setX(object.getX()) + .setY(object.getY()) + .build(); + } + + /** + * Converts a {@link com.lunarclient.apollo.hud.v1.HudPosition} + * proto message to a {@link HudPosition} object. + * + * @param message the hud position message + * @return the hud position object + * @since 1.2.6 + */ + public static HudPosition fromProtobuf(com.lunarclient.apollo.hud.v1.HudPosition message) { + return HudPosition.builder() + .x(message.getX()) + .y(message.getY()) + .build(); + } + /** * Converts an {@link Duration} object to an * {@link com.google.protobuf.Duration} proto message. diff --git a/docs/developers/modules/stopwatch.mdx b/docs/developers/modules/stopwatch.mdx index ade32cf4..7d48472b 100644 --- a/docs/developers/modules/stopwatch.mdx +++ b/docs/developers/modules/stopwatch.mdx @@ -1,15 +1,20 @@ -import { Callout, Tab, Tabs } from 'nextra-theme-docs' +import { Tab, Tabs } from 'nextra-theme-docs' +import { Callout } from 'nextra-theme-docs' # Stopwatch Module ## Overview -The stopwatch module allows you to control the stopwatch mod inside Lunar Client. +The stopwatch module allows you to add custom Stopwatch and Timers directly to player's screens in any HUD position you want. -- Ability to fully control a player's stopwatch mod. - - Start a player's stopwatch - - Stop a player's stopwatch - - Reset a player's stopwatch +- Ability to create unique Stopwatch counters for a player. + - Control when it starts, stops, and when it resets. + - Customize the name, display color, and display format. + - Display the stopwatch anywhere on the player's screen. +- Ability to create and control unique Timers for a player. + - Control when the timer starts, stops, and when it resets. + - Customize the duration, name, looping, display format, and display color of the timer. + - Display the timer HUD in the position you'd like on the player's screen. ![Stopwatch Module Example](/modules/stopwatch/overview.gif#center) @@ -30,64 +35,436 @@ Explore each integration by cycling through each tab, to find the best fit for y **Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers.
-### Start Stopwatch +### Adding a Stopwatch + +```java +public void addStopwatchExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.stopwatchModule.addStopwatch(apolloPlayer, Stopwatch.builder() + .id("parkour-stopwatch") + .name("Parkour") + .resetOnStart(true) + .preventModification(true) + .hideWhenStopped(false) + .displayFormat("mm:ss") + .textColor(ApolloColors.DARK_AQUA) + .hudPosition(HudPosition.builder() + .x(-30) + .y(30) + .build() + ) + .build()); + }); +} +``` + +### Removing a Stopwatch + +```java +public void removeStopwatchExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.removeStopwatch(apolloPlayer, "parkour-stopwatch")); +} +``` + +### Starting a Stopwatch ```java public void startStopwatchExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); - apolloPlayerOpt.ifPresent(this.stopwatchModule::startStopwatch); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.startStopwatch(apolloPlayer, "parkour-stopwatch")); } ``` -### Stop Stopwatch +### Stopping a Stopwatch ```java public void stopStopwatchExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); - apolloPlayerOpt.ifPresent(this.stopwatchModule::stopStopwatch); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.stopStopwatch(apolloPlayer, "parkour-stopwatch")); } ``` -### Reset Stopwatch +### Resetting a Stopwatch ```java public void resetStopwatchExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); - apolloPlayerOpt.ifPresent(this.stopwatchModule::resetStopwatch); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.resetStopwatch(apolloPlayer, "parkour-stopwatch")); +} +``` + +### Resetting all Stopwatches + +```java +public void resetStopwatchesExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.stopwatchModule::resetStopwatches); +} +``` + +### Adding a Timer + +```java +public void addTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.stopwatchModule.addTimer(apolloPlayer, Timer.builder() + .id("game-timer") + .name("Countdown") + .duration(Duration.ofSeconds(45)) + .loop(false) + .preventModification(true) + .hideWhenStopped(false) + .displayFormat("mm:ss") + .titleText(Component.text("Time's up!", NamedTextColor.RED)) + .inGameNotification(true) + .textColor(ApolloColors.LIGHT_PURPLE) + .hudPosition(HudPosition.builder() + .x(-10) + .y(30) + .build() + ) + .build()); + }); +} +``` + +### Removing a Timer + +```java +public void removeTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.removeTimer(apolloPlayer, "game-timer")); +} +``` + +### Starting a Timer + +```java +public void startTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.startTimer(apolloPlayer, "game-timer")); +} +``` + +### Stopping a Timer + +```java +public void stopTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.stopTimer(apolloPlayer, "game-timer")); +} +``` + +### Resetting a Timer + +```java +public void resetTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.resetTimer(apolloPlayer, "game-timer")); +} +``` + +### Resetting all Timers + +```java +public void resetTimersExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.stopwatchModule::resetTimers); } ``` +#### `Stopwatch` Options + +`.id(String)` is the unique server-assigned id for the stopwatch. + +```java +.id("parkour-stopwatch") +``` + +`.name(String)` is the HUD display name. + +```java +.name("Parkour") +``` + +`.resetOnStart(boolean)` determines whether to reset elapsed time on each start. + +```java +.resetOnStart(true) +``` + +`.preventModification(boolean)` prevents the user from modifying the options for this stopwatch on the client side. + +```java +.preventModification(true) +``` + +`.hideWhenStopped(boolean)` determines whether the stopwatch is hidden from the HUD when stopped. + +```java +.hideWhenStopped(false) +``` + +`.displayFormat(String)` is a format string (e.g. `"mm:ss"`). Uses the default display format if not provided. + +```java +.displayFormat("mm:ss") +``` + +`.textColor(java.awt.Color)` is the text color. Defaults to white if not provided. See the [colors page](/apollo/developers/utilities/colors) for more. + +```java +.textColor(ApolloColors.DARK_AQUA) +``` + +`.hudPosition(HudPosition)` is the HUD position on the client screen. If not provided, the stopwatch is auto-stacked. + +```java +.hudPosition(HudPosition.builder().x(-30).y(30).build()) +``` + +#### `Timer` Options + +`.id(String)` is the unique server-assigned id for the timer. + +```java +.id("game-timer") +``` + +`.name(String)` is the HUD display name. + +```java +.name("Countdown") +``` + +`.duration(java.time.Duration)` is the countdown duration. + +```java +.duration(Duration.ofSeconds(45)) +``` + +`.loop(boolean)` determines whether the timer restarts automatically when finished. + +```java +.loop(false) +``` + +`.preventModification(boolean)` prevents the user from modifying the options for this timer on the client side. + +```java +.preventModification(true) +``` + +`.hideWhenStopped(boolean)` determines whether the timer is hidden from the HUD when stopped. + +```java +.hideWhenStopped(false) +``` + +`.displayFormat(String)` is a format string. Uses the default display format if not provided. + +```java +.displayFormat("mm:ss") +``` + +`.titleText(Component)` is the on-screen title shown when the timer finishes. Set to `null` or omit to skip. See the [chat components](https://docs.advntr.dev/text.html) page for more. + +```java +.titleText(Component.text("Time's up!", NamedTextColor.RED)) +``` + +`.inGameNotification(boolean)` determines whether to show an in-game popup when the timer finishes. + +```java +.inGameNotification(true) +``` + +`.textColor(java.awt.Color)` is the text color. Defaults to white if not provided. See the [colors page](/apollo/developers/utilities/colors) for more. + +```java +.textColor(ApolloColors.LIGHT_PURPLE) +``` + +`.hudPosition(HudPosition)` is the HUD position on the client screen. If not provided, the timer is auto-stacked. + +```java +.hudPosition(HudPosition.builder().x(-10).y(30).build()) +``` +
+### Adding a Stopwatch + **Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. -**Start Stopwatch** +```java +public void addStopwatchExample(Player viewer) { + AddStopwatchMessage message = AddStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .setName("Parkour") + .setResetOnStart(true) + .setPreventModification(true) + .setHideWhenStopped(false) + .setDisplayFormat("mm:ss") + .setTextColor(ProtobufUtil.createColorProto(new Color(0, 170, 170))) + .setHudPosition(HudPosition.newBuilder() + .setX(-30) + .setY(30) + .build() + ) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Removing a Stopwatch + +```java +public void removeStopwatchExample(Player viewer) { + RemoveStopwatchMessage message = RemoveStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Starting a Stopwatch ```java public void startStopwatchExample(Player viewer) { - StartStopwatchMessage message = StartStopwatchMessage.getDefaultInstance(); + StartStopwatchMessage message = StartStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + ProtobufPacketUtil.sendPacket(viewer, message); } ``` -**Stop Stopwatch** +### Stopping a Stopwatch ```java public void stopStopwatchExample(Player viewer) { - StopStopwatchMessage message = StopStopwatchMessage.getDefaultInstance(); + StopStopwatchMessage message = StopStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + ProtobufPacketUtil.sendPacket(viewer, message); } ``` -**Reset Stopwatch** +### Resetting a Stopwatch ```java public void resetStopwatchExample(Player viewer) { - ResetStopwatchMessage message = ResetStopwatchMessage.getDefaultInstance(); + ResetStopwatchMessage message = ResetStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Resetting all Stopwatches + +```java +public void resetStopwatchesExample(Player viewer) { + ResetStopwatchesMessage message = ResetStopwatchesMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Adding a Timer + +```java +public void addTimerExample(Player viewer) { + AddTimerMessage message = AddTimerMessage.newBuilder() + .setId("game-timer") + .setName("Countdown") + .setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(45))) + .setLoop(false) + .setPreventModification(true) + .setHideWhenStopped(false) + .setDisplayFormat("mm:ss") + .setTitleTextAdventureJsonLines(AdventureUtil.toJson( + Component.text("Time's up!", NamedTextColor.RED) + )) + .setInGameNotification(true) + .setTextColor(ProtobufUtil.createColorProto(new Color(255, 85, 255))) + .setHudPosition(HudPosition.newBuilder() + .setX(-10) + .setY(30) + .build() + ) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Removing a Timer + +```java +public void removeTimerExample(Player viewer) { + RemoveTimerMessage message = RemoveTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Starting a Timer + +```java +public void startTimerExample(Player viewer) { + StartTimerMessage message = StartTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Stopping a Timer + +```java +public void stopTimerExample(Player viewer) { + StopTimerMessage message = StopTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Resetting a Timer + +```java +public void resetTimerExample(Player viewer) { + ResetTimerMessage message = ResetTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +### Resetting all Timers + +```java +public void resetTimersExample(Player viewer) { + ResetTimersMessage message = ResetTimersMessage.getDefaultInstance(); ProtobufPacketUtil.sendPacket(viewer, message); } ``` @@ -100,34 +477,170 @@ public void resetStopwatchExample(Player viewer) { **Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup.
-**Start Stopwatch** +### Adding a Stopwatch + +```java +public void addStopwatchExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.AddStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); + message.addProperty("name", "Parkour"); + message.addProperty("reset_on_start", true); + message.addProperty("prevent_modification", true); + message.addProperty("hide_when_stopped", false); + message.addProperty("display_format", "mm:ss"); + message.add("text_color", JsonUtil.createColorObject(new Color(0, 170, 170))); + + JsonObject stopwatchPosition = new JsonObject(); + stopwatchPosition.addProperty("x", -30); + stopwatchPosition.addProperty("y", 30); + message.add("hud_position", stopwatchPosition); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Removing a Stopwatch + +```java +public void removeStopwatchExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.RemoveStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Starting a Stopwatch ```java public void startStopwatchExample(Player viewer) { JsonObject message = new JsonObject(); message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StartStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); JsonPacketUtil.sendPacket(viewer, message); } ``` -**Stop Stopwatch** +### Stopping a Stopwatch ```java public void stopStopwatchExample(Player viewer) { JsonObject message = new JsonObject(); message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StopStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); JsonPacketUtil.sendPacket(viewer, message); } ``` -**Reset Stopwatch** +### Resetting a Stopwatch ```java public void resetStopwatchExample(Player viewer) { JsonObject message = new JsonObject(); message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Resetting all Stopwatches + +```java +public void resetStopwatchesExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetStopwatchesMessage"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Adding a Timer + +```java +public void addTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.AddTimerMessage"); + message.addProperty("id", "game-timer"); + message.addProperty("name", "Countdown"); + message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(45))); + message.addProperty("loop", false); + message.addProperty("prevent_modification", true); + message.addProperty("hide_when_stopped", false); + message.addProperty("display_format", "mm:ss"); + message.addProperty("title_text_adventure_json_lines", AdventureUtil.toJson( + Component.text("Time's up!", NamedTextColor.RED) + )); + message.addProperty("in_game_notification", true); + message.add("text_color", JsonUtil.createColorObject(new Color(255, 85, 255))); + + JsonObject timerPosition = new JsonObject(); + timerPosition.addProperty("x", -10); + timerPosition.addProperty("y", 30); + message.add("hud_position", timerPosition); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Removing a Timer + +```java +public void removeTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.RemoveTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Starting a Timer + +```java +public void startTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StartTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Stopping a Timer + +```java +public void stopTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StopTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Resetting a Timer + +```java +public void resetTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +### Resetting all Timers + +```java +public void resetTimersExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetTimersMessage"); JsonPacketUtil.sendPacket(viewer, message); } diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/StopwatchApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/StopwatchApiExample.java index 620bb4fb..2ff0d6c4 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/StopwatchApiExample.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/StopwatchApiExample.java @@ -24,32 +24,128 @@ package com.lunarclient.apollo.example.api.module; import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.ApolloColors; +import com.lunarclient.apollo.common.location.HudPosition; import com.lunarclient.apollo.example.module.impl.StopwatchExample; +import com.lunarclient.apollo.module.stopwatch.Stopwatch; import com.lunarclient.apollo.module.stopwatch.StopwatchModule; +import com.lunarclient.apollo.module.stopwatch.Timer; import com.lunarclient.apollo.player.ApolloPlayer; +import java.time.Duration; import java.util.Optional; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; public class StopwatchApiExample extends StopwatchExample { private final StopwatchModule stopwatchModule = Apollo.getModuleManager().getModule(StopwatchModule.class); + @Override + public void addStopwatchExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.stopwatchModule.addStopwatch(apolloPlayer, Stopwatch.builder() + .id("parkour-stopwatch") + .name("Parkour") + .resetOnStart(true) + .preventModification(true) + .hideWhenStopped(false) + .displayFormat("mm:ss") + .textColor(ApolloColors.DARK_AQUA) + .hudPosition(HudPosition.builder() + .x(-30) + .y(30) + .build() + ) + .build()); + }); + } + + @Override + public void removeStopwatchExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.removeStopwatch(apolloPlayer, "parkour-stopwatch")); + } + @Override public void startStopwatchExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); - apolloPlayerOpt.ifPresent(this.stopwatchModule::startStopwatch); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.startStopwatch(apolloPlayer, "parkour-stopwatch")); } @Override public void stopStopwatchExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); - apolloPlayerOpt.ifPresent(this.stopwatchModule::stopStopwatch); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.stopStopwatch(apolloPlayer, "parkour-stopwatch")); } @Override public void resetStopwatchExample(Player viewer) { Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); - apolloPlayerOpt.ifPresent(this.stopwatchModule::resetStopwatch); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.resetStopwatch(apolloPlayer, "parkour-stopwatch")); + } + + @Override + public void resetStopwatchesExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.stopwatchModule::resetStopwatches); + } + + @Override + public void addTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.stopwatchModule.addTimer(apolloPlayer, Timer.builder() + .id("game-timer") + .name("Countdown") + .duration(Duration.ofSeconds(45)) + .loop(false) + .preventModification(true) + .hideWhenStopped(false) + .displayFormat("mm:ss") + .titleText(Component.text("Time's up!", NamedTextColor.RED)) + .inGameNotification(true) + .textColor(ApolloColors.LIGHT_PURPLE) + .hudPosition(HudPosition.builder() + .x(-10) + .y(30) + .build() + ) + .build()); + }); + } + + @Override + public void removeTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.removeTimer(apolloPlayer, "game-timer")); + } + + @Override + public void startTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.startTimer(apolloPlayer, "game-timer")); + } + + @Override + public void stopTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.stopTimer(apolloPlayer, "game-timer")); + } + + @Override + public void resetTimerExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.stopwatchModule.resetTimer(apolloPlayer, "game-timer")); + } + + @Override + public void resetTimersExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.stopwatchModule::resetTimers); } } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/StopwatchCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/StopwatchCommand.java index a4f081a1..a4bbbc7e 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/StopwatchCommand.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/StopwatchCommand.java @@ -42,38 +42,124 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command Player player = (Player) sender; - if (args.length != 1) { - player.sendMessage("Usage: /stopwatch "); + if (args.length < 2) { + player.sendMessage("Usage: /stopwatch stopwatch "); + player.sendMessage("Usage: /stopwatch timer "); return true; } StopwatchExample stopwatchExample = ApolloExamplePlugin.getInstance().getStopwatchExample(); + String type = args[0].toLowerCase(); + String action = args[1].toLowerCase(); + + switch (type) { + case "stopwatch": { + this.handleStopwatch(player, stopwatchExample, action); + break; + } + + case "timer": { + this.handleTimer(player, stopwatchExample, action); + break; + } + + default: { + player.sendMessage("Usage: /stopwatch stopwatch "); + player.sendMessage("Usage: /stopwatch timer "); + break; + } + } + + return true; + } + + private void handleStopwatch(Player player, StopwatchExample example, String action) { + switch (action) { + case "add": { + example.addStopwatchExample(player); + player.sendMessage("Adding stopwatch..."); + break; + } + + case "remove": { + example.removeStopwatchExample(player); + player.sendMessage("Removing stopwatch..."); + break; + } - switch (args[0].toLowerCase()) { case "start": { - stopwatchExample.startStopwatchExample(player); - player.sendMessage("Starting stopwatch...."); + example.startStopwatchExample(player); + player.sendMessage("Starting stopwatch..."); break; } case "stop": { - stopwatchExample.stopStopwatchExample(player); - player.sendMessage("Stopping stopwatch...."); + example.stopStopwatchExample(player); + player.sendMessage("Stopping stopwatch..."); break; } case "reset": { - stopwatchExample.resetStopwatchExample(player); - player.sendMessage("Resetting stopwatch...."); + example.resetStopwatchExample(player); + player.sendMessage("Resetting stopwatch..."); + break; + } + + case "resetall": { + example.resetStopwatchesExample(player); + player.sendMessage("Resetting all stopwatches..."); break; } default: { - player.sendMessage("Usage: /stopwatch "); + player.sendMessage("Usage: /stopwatch stopwatch "); break; } } + } - return true; + private void handleTimer(Player player, StopwatchExample example, String action) { + switch (action) { + case "add": { + example.addTimerExample(player); + player.sendMessage("Adding timer..."); + break; + } + + case "remove": { + example.removeTimerExample(player); + player.sendMessage("Removing timer..."); + break; + } + + case "start": { + example.startTimerExample(player); + player.sendMessage("Starting timer..."); + break; + } + + case "stop": { + example.stopTimerExample(player); + player.sendMessage("Stopping timer..."); + break; + } + + case "reset": { + example.resetTimerExample(player); + player.sendMessage("Resetting timer..."); + break; + } + + case "resetall": { + example.resetTimersExample(player); + player.sendMessage("Resetting all timers..."); + break; + } + + default: { + player.sendMessage("Usage: /stopwatch timer "); + break; + } + } } } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/StopwatchExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/StopwatchExample.java index 3994bbd4..57d70710 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/StopwatchExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/StopwatchExample.java @@ -28,10 +28,28 @@ public abstract class StopwatchExample extends ApolloModuleExample { + public abstract void addStopwatchExample(Player viewer); + + public abstract void removeStopwatchExample(Player viewer); + public abstract void startStopwatchExample(Player viewer); public abstract void stopStopwatchExample(Player viewer); public abstract void resetStopwatchExample(Player viewer); + public abstract void resetStopwatchesExample(Player viewer); + + public abstract void addTimerExample(Player viewer); + + public abstract void removeTimerExample(Player viewer); + + public abstract void startTimerExample(Player viewer); + + public abstract void stopTimerExample(Player viewer); + + public abstract void resetTimerExample(Player viewer); + + public abstract void resetTimersExample(Player viewer); + } diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/StopwatchJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/StopwatchJsonExample.java index 96a972ca..33031f2e 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/StopwatchJsonExample.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/StopwatchJsonExample.java @@ -24,16 +24,52 @@ package com.lunarclient.apollo.example.json.module; import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.AdventureUtil; import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; import com.lunarclient.apollo.example.module.impl.StopwatchExample; +import java.awt.Color; +import java.time.Duration; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; public class StopwatchJsonExample extends StopwatchExample { + @Override + public void addStopwatchExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.AddStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); + message.addProperty("name", "Parkour"); + message.addProperty("reset_on_start", true); + message.addProperty("prevent_modification", true); + message.addProperty("hide_when_stopped", false); + message.addProperty("display_format", "mm:ss"); + message.add("text_color", JsonUtil.createColorObject(new Color(0, 170, 170))); + + JsonObject stopwatchPosition = new JsonObject(); + stopwatchPosition.addProperty("x", -30); + stopwatchPosition.addProperty("y", 30); + message.add("hud_position", stopwatchPosition); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeStopwatchExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.RemoveStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); + + JsonPacketUtil.sendPacket(viewer, message); + } + @Override public void startStopwatchExample(Player viewer) { JsonObject message = new JsonObject(); message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StartStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); JsonPacketUtil.sendPacket(viewer, message); } @@ -42,6 +78,7 @@ public void startStopwatchExample(Player viewer) { public void stopStopwatchExample(Player viewer) { JsonObject message = new JsonObject(); message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StopStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); JsonPacketUtil.sendPacket(viewer, message); } @@ -50,6 +87,84 @@ public void stopStopwatchExample(Player viewer) { public void resetStopwatchExample(Player viewer) { JsonObject message = new JsonObject(); message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetStopwatchMessage"); + message.addProperty("id", "parkour-stopwatch"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetStopwatchesExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetStopwatchesMessage"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void addTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.AddTimerMessage"); + message.addProperty("id", "game-timer"); + message.addProperty("name", "Countdown"); + message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(45))); + message.addProperty("loop", false); + message.addProperty("prevent_modification", true); + message.addProperty("hide_when_stopped", false); + message.addProperty("display_format", "mm:ss"); + message.addProperty("title_text_adventure_json_lines", AdventureUtil.toJson( + Component.text("Time's up!", NamedTextColor.RED) + )); + message.addProperty("in_game_notification", true); + message.add("text_color", JsonUtil.createColorObject(new Color(255, 85, 255))); + + JsonObject timerPosition = new JsonObject(); + timerPosition.addProperty("x", -10); + timerPosition.addProperty("y", 30); + message.add("hud_position", timerPosition); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.RemoveTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void startTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StartTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void stopTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.StopTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetTimerExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetTimerMessage"); + message.addProperty("id", "game-timer"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetTimersExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.stopwatch.v1.ResetTimersMessage"); JsonPacketUtil.sendPacket(viewer, message); } diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/StopwatchProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/StopwatchProtoExample.java index 5f8960b5..9f001529 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/StopwatchProtoExample.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/StopwatchProtoExample.java @@ -24,29 +24,156 @@ package com.lunarclient.apollo.example.proto.module; import com.lunarclient.apollo.example.module.impl.StopwatchExample; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import com.lunarclient.apollo.stopwatch.v1.AddStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.AddTimerMessage; +import com.lunarclient.apollo.stopwatch.v1.RemoveStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.RemoveTimerMessage; import com.lunarclient.apollo.stopwatch.v1.ResetStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.ResetStopwatchesMessage; +import com.lunarclient.apollo.stopwatch.v1.ResetTimerMessage; +import com.lunarclient.apollo.stopwatch.v1.ResetTimersMessage; import com.lunarclient.apollo.stopwatch.v1.StartStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.StartTimerMessage; import com.lunarclient.apollo.stopwatch.v1.StopStopwatchMessage; +import com.lunarclient.apollo.stopwatch.v1.StopTimerMessage; +import java.awt.Color; +import java.time.Duration; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; public class StopwatchProtoExample extends StopwatchExample { + @Override + public void addStopwatchExample(Player viewer) { + AddStopwatchMessage message = AddStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .setName("Parkour") + .setResetOnStart(true) + .setPreventModification(true) + .setHideWhenStopped(false) + .setDisplayFormat("mm:ss") + .setTextColor(ProtobufUtil.createColorProto(new Color(0, 170, 170))) + .setHudPosition(HudPosition.newBuilder() + .setX(-30) + .setY(30) + .build() + ) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeStopwatchExample(Player viewer) { + RemoveStopwatchMessage message = RemoveStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + @Override public void startStopwatchExample(Player viewer) { - StartStopwatchMessage message = StartStopwatchMessage.getDefaultInstance(); + StartStopwatchMessage message = StartStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + ProtobufPacketUtil.sendPacket(viewer, message); } @Override public void stopStopwatchExample(Player viewer) { - StopStopwatchMessage message = StopStopwatchMessage.getDefaultInstance(); + StopStopwatchMessage message = StopStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + ProtobufPacketUtil.sendPacket(viewer, message); } @Override public void resetStopwatchExample(Player viewer) { - ResetStopwatchMessage message = ResetStopwatchMessage.getDefaultInstance(); + ResetStopwatchMessage message = ResetStopwatchMessage.newBuilder() + .setId("parkour-stopwatch") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetStopwatchesExample(Player viewer) { + ResetStopwatchesMessage message = ResetStopwatchesMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void addTimerExample(Player viewer) { + AddTimerMessage message = AddTimerMessage.newBuilder() + .setId("game-timer") + .setName("Countdown") + .setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(45))) + .setLoop(false) + .setPreventModification(true) + .setHideWhenStopped(false) + .setDisplayFormat("mm:ss") + .setTitleTextAdventureJsonLines(AdventureUtil.toJson( + Component.text("Time's up!", NamedTextColor.RED) + )) + .setInGameNotification(true) + .setTextColor(ProtobufUtil.createColorProto(new Color(255, 85, 255))) + .setHudPosition(HudPosition.newBuilder() + .setX(-10) + .setY(30) + .build() + ) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeTimerExample(Player viewer) { + RemoveTimerMessage message = RemoveTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void startTimerExample(Player viewer) { + StartTimerMessage message = StartTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void stopTimerExample(Player viewer) { + StopTimerMessage message = StopTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetTimerExample(Player viewer) { + ResetTimerMessage message = ResetTimerMessage.newBuilder() + .setId("game-timer") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetTimersExample(Player viewer) { + ResetTimersMessage message = ResetTimersMessage.getDefaultInstance(); ProtobufPacketUtil.sendPacket(viewer, message); } From 6cb827d2db6ab126d39cdfdfd900c3161b9051ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 5 May 2026 01:23:12 +0200 Subject: [PATCH 5/7] Feature - Cosmetics Module (#275) * NPC Cosmetic module! * Bump CI to java 21 * Cosmetics module! * example: dont run nametag example on join * fix tests setting options for everyone * example: fix spray example npe * `Spray` & `Cosmetic` builder docs * add cosmetic module overview * fix overview typo * gardle fixes * move cosmetic module docs to private modules * final docs changes --------- Co-authored-by: TrentinTheKid <25537885+TrentinTheKid@users.noreply.github.com> --- .checkstyle/suppressions.xml | 2 +- .github/workflows/deploy.yml | 2 +- .../apollo/module/cosmetic/Cosmetic.java | 66 +++ .../module/cosmetic/CosmeticModule.java | 121 +++++ .../apollo/module/cosmetic/Spray.java | 90 ++++ .../module/cosmetic/options/BodyOptions.java | 66 +++ .../module/cosmetic/options/CloakOptions.java | 48 ++ .../cosmetic/options/CosmeticOptions.java | 37 ++ .../module/cosmetic/options/HatOptions.java | 66 +++ .../module/cosmetic/options/PetOptions.java | 48 ++ build-logic/build.gradle.kts | 12 +- .../kotlin/apollo.base-conventions.gradle.kts | 2 +- .../apollo.shadow-conventions.gradle.kts | 2 +- build-logic/src/main/kotlin/extensions.kt | 2 +- .../module/cosmetic/CosmeticModuleImpl.java | 172 +++++++ .../apollo/network/NetworkTypes.java | 13 +- .../com/lunarclient/apollo/util/Ranges.java | 15 + .../lightweight/json/object-util.mdx | 8 +- docs/developers/private-modules/cosmetic.mdx | 483 ++++++++++++++++++ example/bukkit/api/build.gradle.kts | 6 + .../example/api/ApolloApiExamplePlatform.java | 2 + .../api/debug/impl/ModSettingsTest.java | 2 +- .../api/listener/ApolloPlayerApiListener.java | 11 +- .../api/module/CosmeticApiExample.java | 149 ++++++ .../bukkit/api/src/main/resources/plugin.yml | 4 + example/bukkit/common/build.gradle.kts | 2 + .../apollo/example/ApolloExamplePlugin.java | 16 +- .../example/command/CosmeticCommand.java | 198 +++++++ .../apollo/example/command/NpcCommand.java | 115 +++++ .../example/module/impl/CosmeticExample.java | 49 ++ example/bukkit/json/build.gradle.kts | 6 + .../json/ApolloJsonExamplePlatform.java | 2 + .../json/module/CosmeticJsonExample.java | 173 +++++++ .../example/json/util/JsonPacketUtil.java | 2 +- .../apollo/example/json/util/JsonUtil.java | 8 +- .../bukkit/json/src/main/resources/plugin.yml | 4 + example/bukkit/nms/build.gradle.kts | 43 ++ .../apollo/example/nms/NpcManager.java | 186 +++++++ .../apollo/example/nms/PlayerNpc.java | 45 ++ example/bukkit/proto/build.gradle.kts | 6 + .../proto/ApolloProtoExamplePlatform.java | 2 + .../proto/module/CosmeticProtoExample.java | 166 ++++++ .../proto/util/ProtobufPacketUtil.java | 2 +- .../proto/src/main/resources/plugin.yml | 4 + example/minestom/api/build.gradle.kts | 4 + extra/adventure4/build.gradle.kts | 2 +- gradle/libs.versions.toml | 10 +- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 25 +- gradlew.bat | 26 +- .../apollo/ApolloBukkitPlatform.java | 3 + .../apollo/ApolloBungeePlatform.java | 3 + .../apollo/ApolloFoliaPlatform.java | 3 + .../apollo/ApolloMinestomPlatform.java | 3 + .../apollo/ApolloVelocityPlatform.java | 3 + settings.gradle.kts | 9 +- 57 files changed, 2498 insertions(+), 53 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/Cosmetic.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/Spray.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/BodyOptions.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CloakOptions.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CosmeticOptions.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/HatOptions.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/PetOptions.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModuleImpl.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java create mode 100644 example/bukkit/nms/build.gradle.kts create mode 100644 example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java create mode 100644 example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java diff --git a/.checkstyle/suppressions.xml b/.checkstyle/suppressions.xml index 530d6e64..12f09771 100644 --- a/.checkstyle/suppressions.xml +++ b/.checkstyle/suppressions.xml @@ -2,7 +2,7 @@ - + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d9e8de16..4c3e26d2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "zulu" - java-version: 8 + java-version: 21 - name: Gradle Build run: ./gradlew build diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/Cosmetic.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/Cosmetic.java new file mode 100644 index 00000000..36e2cf12 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/Cosmetic.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic; + +import com.lunarclient.apollo.module.cosmetic.options.BodyOptions; +import com.lunarclient.apollo.module.cosmetic.options.CloakOptions; +import com.lunarclient.apollo.module.cosmetic.options.CosmeticOptions; +import com.lunarclient.apollo.module.cosmetic.options.HatOptions; +import com.lunarclient.apollo.module.cosmetic.options.PetOptions; +import lombok.Builder; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +/** + * Represents a single cosmetic with optional per-type display settings. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class Cosmetic { + + /** + * Returns the Lunar Client cosmetic id for this entry. + * + *

The value must be greater than 0.

+ * + * @return the cosmetic id + * @since 1.2.6 + */ + @Range(from = 1, to = Integer.MAX_VALUE) int id; + + /** + * Returns optional cosmetic display options for this cosmetic id. + * + *

Expected concrete types are {@link HatOptions}, {@link CloakOptions}, {@link PetOptions}, or + * {@link BodyOptions}.

+ * + * @return cosmetic options, or {@code null} + * @since 1.2.6 + */ + @Nullable CosmeticOptions options; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java new file mode 100644 index 00000000..c7a77f33 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java @@ -0,0 +1,121 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic; + +import com.lunarclient.apollo.common.location.ApolloBlockLocation; +import com.lunarclient.apollo.module.ApolloModule; +import com.lunarclient.apollo.module.ModuleDefinition; +import com.lunarclient.apollo.module.cosmetic.options.BodyOptions; +import com.lunarclient.apollo.module.cosmetic.options.CloakOptions; +import com.lunarclient.apollo.module.cosmetic.options.CosmeticOptions; +import com.lunarclient.apollo.module.cosmetic.options.HatOptions; +import com.lunarclient.apollo.module.cosmetic.options.PetOptions; +import com.lunarclient.apollo.recipients.Recipients; +import java.util.List; +import java.util.UUID; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +/** + * Represents the cosmetic module. + * + * @since 1.2.6 + */ +@ApiStatus.NonExtendable +@ModuleDefinition(id = "cosmetic", name = "Cosmetic") +public abstract class CosmeticModule extends ApolloModule { + + /** + * Equips the provided cosmetics on an NPC for the given {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param npcUuid the {@link UUID} of the NPC to equip the cosmetics on + * @param cosmetics the cosmetics to equip, including optional {@link CosmeticOptions} per entry + * ({@link HatOptions}, {@link CloakOptions}, {@link PetOptions}, or {@link BodyOptions}) + * @since 1.2.6 + */ + public abstract void equipNpcCosmetics(Recipients recipients, UUID npcUuid, List cosmetics); + + /** + * Unequips the provided cosmetics from an NPC for the given {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param npcUuid the {@link UUID} of the NPC to unequip the cosmetics from + * @param cosmeticIds the list of cosmetic ids to unequip + * @since 1.2.6 + */ + public abstract void unequipNpcCosmetics(Recipients recipients, UUID npcUuid, List cosmeticIds); + + /** + * Resets all cosmetics on an NPC for the given {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param npcUuid the {@link UUID} of the NPC to reset the cosmetics on + * @since 1.2.6 + */ + public abstract void resetNpcCosmetics(Recipients recipients, UUID npcUuid); + + /** + * Displays a spray for the given {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param spray the spray to display (durations under one second are raised to one second before sending) + * @since 1.2.6 + */ + public abstract void displaySpray(Recipients recipients, Spray spray); + + /** + * Removes every instance of a spray id for the given {@link Recipients}. + * + *

The spray id must be greater than 0.

+ * + * @param recipients the recipients that are receiving the packet + * @param sprayId the spray cosmetic id + * @since 1.2.6 + */ + public abstract void removeSpray(Recipients recipients, @Range(from = 1, to = Integer.MAX_VALUE) int sprayId); + + /** + * Removes every instance of a spray id at a specific block for the given {@link Recipients}. + * + *

The spray id must be greater than 0. If {@code location} is {@code null}, every + * instance of the spray id is removed regardless of position.

+ * + * @param recipients the recipients that are receiving the packet + * @param sprayId the spray cosmetic id + * @param location the block location of the spray to remove, or {@code null} to remove all + * @since 1.2.6 + */ + public abstract void removeSpray(Recipients recipients, @Range(from = 1, to = Integer.MAX_VALUE) int sprayId, @Nullable ApolloBlockLocation location); + + /** + * Resets all server sprays for the given {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.6 + */ + public abstract void resetSprays(Recipients recipients); + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/Spray.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/Spray.java new file mode 100644 index 00000000..4366cc76 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/Spray.java @@ -0,0 +1,90 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic; + +import com.lunarclient.apollo.common.location.ApolloBlockLocation; +import com.lunarclient.apollo.module.packetenrichment.raytrace.Direction; +import java.time.Duration; +import lombok.Builder; +import lombok.Getter; +import org.jetbrains.annotations.Range; + +/** + * Represents a spray. + * + *

Sprays are client-local and validated against loaded chunks; + * they're removed when their backing chunk unloads and won't reappear + * unless the server resends the spray packet.

+ * + * @since 1.2.6 + */ +@Getter +@Builder +public final class Spray { + + /** + * Returns the Lunar Client spray cosmetic id. + * + *

The value must be greater than 0.

+ * + * @return the spray cosmetic id + * @since 1.2.6 + */ + @Range(from = 1, to = Integer.MAX_VALUE) int sprayId; + + /** + * Returns the {@link ApolloBlockLocation} of the block the spray is placed on. + * + * @return the block location + * @since 1.2.6 + */ + ApolloBlockLocation location; + + /** + * Returns the {@link Direction} indicating which side the spray faces. + * + * @return the facing direction + * @since 1.2.6 + */ + Direction facing; + + /** + * Returns the spray rotation in degrees on the client. + * + * @return the rotation in degrees + * @since 1.2.6 + */ + @Builder.Default + float rotation = 0f; + + /** + * Returns the {@link Duration} for how long the spray remains visible on the client. + * + * @return the display duration + * @since 1.2.6 + */ + @Builder.Default + Duration duration = Duration.ofSeconds(30); + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/BodyOptions.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/BodyOptions.java new file mode 100644 index 00000000..c3472923 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/BodyOptions.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic.options; + +import com.lunarclient.apollo.module.cosmetic.Cosmetic; +import lombok.Builder; +import lombok.Getter; + +/** + * Represents body cosmetic display settings for use with {@link Cosmetic}. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class BodyOptions extends CosmeticOptions { + + /** + * Returns whether the body cosmetic should render on top of an equipped chestplate when present. + * + * @return {@code true} to draw over the chestplate, {@code false} otherwise + * @since 1.2.6 + */ + @Builder.Default + boolean showOverChestplate = true; + + /** + * Returns whether the body cosmetic should render on top of equipped leggings when present. + * + * @return {@code true} to draw over the leggings, {@code false} otherwise + * @since 1.2.6 + */ + @Builder.Default + boolean showOverLeggings = true; + + /** + * Returns whether the body cosmetic should render on top of equipped boots when present. + * + * @return {@code true} to draw over the boots, {@code false} otherwise + * @since 1.2.6 + */ + @Builder.Default + boolean showOverBoots = true; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CloakOptions.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CloakOptions.java new file mode 100644 index 00000000..888b07db --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CloakOptions.java @@ -0,0 +1,48 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic.options; + +import com.lunarclient.apollo.module.cosmetic.Cosmetic; +import lombok.Builder; +import lombok.Getter; + +/** + * Represents cloak cosmetic display settings for use with {@link Cosmetic}. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class CloakOptions extends CosmeticOptions { + + /** + * Returns whether the cloak should use cloth-style physics on the client. + * + * @return {@code true} to enable cloth physics, {@code false} otherwise + * @since 1.2.6 + */ + @Builder.Default + boolean useClothPhysics = false; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CosmeticOptions.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CosmeticOptions.java new file mode 100644 index 00000000..b80f2029 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/CosmeticOptions.java @@ -0,0 +1,37 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic.options; + +import com.lunarclient.apollo.module.cosmetic.Cosmetic; + +/** + * The abstract base class for cosmetic display options used with {@link Cosmetic}. + * + *

Concrete types are {@link HatOptions}, {@link CloakOptions}, {@link PetOptions}, and {@link BodyOptions}.

+ * + * @since 1.2.6 + */ +public abstract class CosmeticOptions { + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/HatOptions.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/HatOptions.java new file mode 100644 index 00000000..4f79b926 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/HatOptions.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic.options; + +import com.lunarclient.apollo.module.cosmetic.Cosmetic; +import lombok.Builder; +import lombok.Getter; + +/** + * Represents hat cosmetic display settings for use with {@link Cosmetic}. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class HatOptions extends CosmeticOptions { + + /** + * Returns whether the hat should render on top of an equipped helmet when present. + * + * @return {@code true} to draw over the helmet, {@code false} otherwise + * @since 1.2.6 + */ + @Builder.Default + boolean showOverHelmet = true; + + /** + * Returns whether the hat should render on top of the player's outer skin layer when present. + * + * @return {@code true} to draw over the skin layer, {@code false} otherwise + * @since 1.2.6 + */ + @Builder.Default + boolean showOverSkinLayer = true; + + /** + * Returns the vertical height offset applied to this hat on the client, in world units. + * + * @return the height offset + * @since 1.2.6 + */ + @Builder.Default + float heightOffset = 0f; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/PetOptions.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/PetOptions.java new file mode 100644 index 00000000..314eb391 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/options/PetOptions.java @@ -0,0 +1,48 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic.options; + +import com.lunarclient.apollo.module.cosmetic.Cosmetic; +import lombok.Builder; +import lombok.Getter; + +/** + * Represents shoulder-pet cosmetic display settings for use with {@link Cosmetic}. + * + * @since 1.2.6 + */ +@Getter +@Builder +public final class PetOptions extends CosmeticOptions { + + /** + * Returns whether the shoulder pet should appear on the opposite shoulder from the default side. + * + * @return {@code true} to mirror the pet to the other shoulder, {@code false} for default placement + * @since 1.2.6 + */ + @Builder.Default + boolean flipShoulder = false; + +} diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 819e383a..cba24b36 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -15,16 +15,12 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlin { - target { - compilations.configureEach { - kotlinOptions { - jvmTarget = "1.8" - } - } + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } diff --git a/build-logic/src/main/kotlin/apollo.base-conventions.gradle.kts b/build-logic/src/main/kotlin/apollo.base-conventions.gradle.kts index d87f820e..362d3d5f 100644 --- a/build-logic/src/main/kotlin/apollo.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/apollo.base-conventions.gradle.kts @@ -36,7 +36,7 @@ spotless { fun FormatExtension.applyCommon() { trimTrailingWhitespace() endWithNewline() - indentWithSpaces(4) + leadingTabsToSpaces(4) targetExclude("**/build/generated/source/proto/**/*.*") targetExclude("**/org/spongepowered/configurate/yaml/**/*.*") } diff --git a/build-logic/src/main/kotlin/apollo.shadow-conventions.gradle.kts b/build-logic/src/main/kotlin/apollo.shadow-conventions.gradle.kts index 38b138f8..424a87f7 100644 --- a/build-logic/src/main/kotlin/apollo.shadow-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/apollo.shadow-conventions.gradle.kts @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { id("apollo.base-conventions") - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") } tasks { diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt index 375c85d0..7f57c7f9 100644 --- a/build-logic/src/main/kotlin/extensions.kt +++ b/build-logic/src/main/kotlin/extensions.kt @@ -147,7 +147,7 @@ fun Project.setupDynamicLoader() { archiveClassifier.set("all") } - val shadowJarLoader by tasks.creating(ShadowJar::class) { + val shadowJarLoader by tasks.registering(ShadowJar::class) { archiveClassifier.set("") configurations = listOf(loaderImplementationConfig.get()) diff --git a/common/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModuleImpl.java new file mode 100644 index 00000000..2189d3a7 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModuleImpl.java @@ -0,0 +1,172 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.cosmetic; + +import com.lunarclient.apollo.common.location.ApolloBlockLocation; +import com.lunarclient.apollo.cosmetic.v1.DisplaySprayMessage; +import com.lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage; +import com.lunarclient.apollo.cosmetic.v1.RemoveSprayMessage; +import com.lunarclient.apollo.cosmetic.v1.ResetNpcCosmeticsMessage; +import com.lunarclient.apollo.cosmetic.v1.ResetSpraysMessage; +import com.lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage; +import com.lunarclient.apollo.module.cosmetic.options.BodyOptions; +import com.lunarclient.apollo.module.cosmetic.options.CloakOptions; +import com.lunarclient.apollo.module.cosmetic.options.CosmeticOptions; +import com.lunarclient.apollo.module.cosmetic.options.HatOptions; +import com.lunarclient.apollo.module.cosmetic.options.PetOptions; +import com.lunarclient.apollo.network.NetworkTypes; +import com.lunarclient.apollo.player.AbstractApolloPlayer; +import com.lunarclient.apollo.recipients.Recipients; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +import static com.lunarclient.apollo.util.Ranges.checkStrictlyPositive; + +/** + * Provides the cosmetic module. + * + * @since 1.2.6 + */ +public final class CosmeticModuleImpl extends CosmeticModule { + + @Override + public void equipNpcCosmetics(@NonNull Recipients recipients, @NonNull UUID npcUuid, @NonNull List cosmetics) { + List cosmeticsProto = cosmetics.stream() + .map(this::toProtobuf) + .collect(Collectors.toList()); + + EquipNpcCosmeticsMessage message = EquipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(NetworkTypes.toProtobuf(npcUuid)) + .addAllCosmetics(cosmeticsProto) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void unequipNpcCosmetics(@NonNull Recipients recipients, @NonNull UUID npcUuid, @NonNull List cosmeticIds) { + List validatedIds = cosmeticIds.stream() + .map(id -> checkStrictlyPositive(id, "Cosmetic#id")) + .collect(Collectors.toList()); + + UnequipNpcCosmeticsMessage message = UnequipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(NetworkTypes.toProtobuf(npcUuid)) + .addAllCosmeticIds(validatedIds) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void resetNpcCosmetics(@NonNull Recipients recipients, @NonNull UUID npcUuid) { + ResetNpcCosmeticsMessage message = ResetNpcCosmeticsMessage.newBuilder() + .setNpcUuid(NetworkTypes.toProtobuf(npcUuid)) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void displaySpray(@NonNull Recipients recipients, @NonNull Spray spray) { + DisplaySprayMessage message = DisplaySprayMessage.newBuilder() + .setSprayId(checkStrictlyPositive(spray.getSprayId(), "Spray#sprayId")) + .setLocation(NetworkTypes.toProtobuf(spray.getLocation())) + .setFacing(NetworkTypes.toProtobuf(spray.getFacing())) + .setRotation(spray.getRotation()) + .setDuration(NetworkTypes.toProtobuf(spray.getDuration())) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void removeSpray(@NonNull Recipients recipients, int sprayId) { + RemoveSprayMessage message = RemoveSprayMessage.newBuilder() + .setSprayId(checkStrictlyPositive(sprayId, "Spray#sprayId")) + .build(); + + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void removeSpray(@NonNull Recipients recipients, int sprayId, @Nullable ApolloBlockLocation location) { + RemoveSprayMessage.Builder builder = RemoveSprayMessage.newBuilder() + .setSprayId(checkStrictlyPositive(sprayId, "Spray#sprayId")); + + if (location != null) { + builder.setLocation(NetworkTypes.toProtobuf(location)); + } + + RemoveSprayMessage message = builder.build(); + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + @Override + public void resetSprays(@NonNull Recipients recipients) { + ResetSpraysMessage message = ResetSpraysMessage.getDefaultInstance(); + recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message)); + } + + private com.lunarclient.apollo.cosmetic.v1.Cosmetic toProtobuf(Cosmetic cosmetic) { + com.lunarclient.apollo.cosmetic.v1.Cosmetic.Builder builder = com.lunarclient.apollo.cosmetic.v1.Cosmetic.newBuilder() + .setId(checkStrictlyPositive(cosmetic.getId(), "Cosmetic#id")); + + CosmeticOptions options = cosmetic.getOptions(); + if (options == null) { + return builder.build(); + } + + if (options instanceof HatOptions) { + HatOptions hatOptions = (HatOptions) options; + builder.setHatOptions(com.lunarclient.apollo.cosmetic.v1.HatOptions.newBuilder() + .setShowOverHelmet(hatOptions.isShowOverHelmet()) + .setShowOverSkinLayer(hatOptions.isShowOverSkinLayer()) + .setHeightOffset(hatOptions.getHeightOffset()) + .build()); + } else if (options instanceof CloakOptions) { + CloakOptions cloakOptions = (CloakOptions) options; + builder.setCloakOptions(com.lunarclient.apollo.cosmetic.v1.CloakOptions.newBuilder() + .setUseClothPhysics(cloakOptions.isUseClothPhysics()) + .build()); + } else if (options instanceof PetOptions) { + PetOptions petOptions = (PetOptions) options; + builder.setPetOptions(com.lunarclient.apollo.cosmetic.v1.PetOptions.newBuilder() + .setFlipShoulder(petOptions.isFlipShoulder()) + .build()); + } else if (options instanceof BodyOptions) { + BodyOptions bodyOptions = (BodyOptions) options; + builder.setBodyOptions(com.lunarclient.apollo.cosmetic.v1.BodyOptions.newBuilder() + .setShowOverChestplate(bodyOptions.isShowOverChestplate()) + .setShowOverLeggings(bodyOptions.isShowOverLeggings()) + .setShowOverBoots(bodyOptions.isShowOverBoots()) + .build()); + } + + return builder.build(); + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java index 2a0a40ff..1bc39264 100644 --- a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java +++ b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java @@ -284,7 +284,7 @@ public static com.lunarclient.apollo.packetenrichment.v1.RayTraceResult toProtob BlockHit blockHit = BlockHit.newBuilder() .setHitLocation(NetworkTypes.toProtobuf(result.getHitLocation())) .setBlockLocation(NetworkTypes.toProtobuf(result.getBlockLocation())) - .setDirection(com.lunarclient.apollo.packetenrichment.v1.Direction.forNumber(result.getDirection().ordinal() + 1)) + .setDirection(NetworkTypes.toProtobuf(result.getDirection())) .build(); builder.setBlock(blockHit); @@ -333,6 +333,17 @@ public static RayTraceResult fromProtobuf(com.lunarclient.apollo.packetenrichmen return new MissResult(); } + /** + * Converts a {@link Direction} enum to a {@link com.lunarclient.apollo.packetenrichment.v1.Direction} proto message. + * + * @param direction the apollo direction + * @return the proto direction enum message + * @since 1.2.6 + */ + public static com.lunarclient.apollo.packetenrichment.v1.Direction toProtobuf(Direction direction) { + return com.lunarclient.apollo.packetenrichment.v1.Direction.forNumber(direction.ordinal() + 1); + } + /** * Converts an {@link ApolloLocation} object to an * {@link com.lunarclient.apollo.common.v1.Location} proto message. diff --git a/common/src/main/java/com/lunarclient/apollo/util/Ranges.java b/common/src/main/java/com/lunarclient/apollo/util/Ranges.java index bf7bc754..655726a6 100644 --- a/common/src/main/java/com/lunarclient/apollo/util/Ranges.java +++ b/common/src/main/java/com/lunarclient/apollo/util/Ranges.java @@ -62,6 +62,21 @@ public static int checkPositive(int value, String name) { return value; } + /** + * Returns the value if it is strictly positive (greater than 0), + * otherwise throws an {@link IllegalArgumentException}. + * + * @param value the value to check + * @param name the name of the value + * @return the value + * @throws IllegalArgumentException if the value is not strictly positive + * @since 1.2.6 + */ + public static int checkStrictlyPositive(int value, String name) { + if(value <= 0) throw new IllegalArgumentException(name + " must be greater than 0"); + return value; + } + /** * Returns the value if it is positive, otherwise throws an * {@link IllegalArgumentException}. diff --git a/docs/developers/lightweight/json/object-util.mdx b/docs/developers/lightweight/json/object-util.mdx index 809b7210..f60dbbfd 100644 --- a/docs/developers/lightweight/json/object-util.mdx +++ b/docs/developers/lightweight/json/object-util.mdx @@ -52,9 +52,13 @@ public static JsonObject createCuboid2DObject(double minX, double minZ, double m } public static JsonObject createEntityIdObject(@NotNull Entity entity) { + return JsonUtil.createEntityIdObject(entity.getEntityId(), entity.getUniqueId()); +} + +public static JsonObject createEntityIdObject(int entityId, @NotNull UUID uuid) { JsonObject entityIdObject = new JsonObject(); - entityIdObject.addProperty("entity_id", entity.getEntityId()); - entityIdObject.add("entity_uuid", JsonUtil.createUuidObject(entity.getUniqueId())); + entityIdObject.addProperty("entity_id", entityId); + entityIdObject.add("entity_uuid", JsonUtil.createUuidObject(uuid)); return entityIdObject; } ``` diff --git a/docs/developers/private-modules/cosmetic.mdx b/docs/developers/private-modules/cosmetic.mdx index e69de29b..6b18d76f 100644 --- a/docs/developers/private-modules/cosmetic.mdx +++ b/docs/developers/private-modules/cosmetic.mdx @@ -0,0 +1,483 @@ +import { Callout, Tab, Tabs } from 'nextra-theme-docs' + +# Cosmetic Module + +## Overview + +The cosmetic module allows you to interact and utilize all the various cosmetic types found in Lunar Client. + +- Add wearable cosmetics to NPC entities + - Grants the ability to put any cosmetic on an NPC + - Uniquely customize various cosmetic options +- Add sprays to any location + - Grants the ability to put any spray on a block + - Customize the rotation, direction options, and duration + + + The cosmetic module is a **private module** and not publicly available. + To use this module, you must be granted access by the Lunar Client team. [Learn More](/apollo/developers/private-modules) + + +## Integration + +### Sample Code + +Explore each integration by cycling through each tab to find the best fit for your requirements and needs. + + + NPC UUIDs **must** have their least significant bits set to `0` (i.e. `new UUID(msb, 0L)`). + Lunar Client rejects cosmetic packets for UUIDs that do not follow this format. + + + + + + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + +**Equip cosmetics on an NPC** + +```java +public void equipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmetics = Lists.newArrayList( + Cosmetic.builder() + .id(434) + .build(), + Cosmetic.builder() + .id(3654) + .build(), + Cosmetic.builder() + .id(5095) + .options(PetOptions.builder() + .flipShoulder(true) + .build()) + .build(), + Cosmetic.builder() + .id(3) + .options(CloakOptions.builder() + .useClothPhysics(true) + .build()) + .build(), + Cosmetic.builder() + .id(3977) + .build() + ); + + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.cosmeticModule.equipNpcCosmetics(apolloPlayer, npcUuid, cosmetics)); +} +``` + +**Unequip cosmetics from an NPC** + +```java +public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> { + List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); + this.cosmeticModule.unequipNpcCosmetics(apolloPlayer, npcUuid, cosmeticIds); + }); +} +``` + +**Reset NPC cosmetics** + +```java +public void resetNpcCosmeticsExample(UUID npcUuid) { + this.cosmeticModule.resetNpcCosmetics(Recipients.ofEveryone(), npcUuid); +} +``` + +**Display spray** + +```java +public void displaySprayExample(Player viewer, int sprayId) { + Block block = viewer.getTargetBlockExact(10); + if (block == null) { + return; + } + + Material material = block.getType(); + if (material.isAir() || !material.isSolid()) { + return; + } + + ApolloBlockLocation location = ApolloBlockLocation.builder() + .world(block.getWorld().getName()) + .x(block.getX()) + .y(block.getY()) + .z(block.getZ()) + .build(); + + Spray spray = Spray.builder() + .sprayId(sprayId) + .location(location) + .facing(Direction.UP) + .rotation(0f) + .duration(Duration.ofSeconds(60)) + .build(); + + this.cosmeticModule.displaySpray(Recipients.ofEveryone(), spray); +} +``` + + + **Sprays** are client-local and validated against loaded chunks. When the backing chunk unloads, sprays are removed and **do not** come back until the server sends a display spray packet again. + + +**Remove sprays** + +```java +public void removeSprayExample(int sprayId) { + this.cosmeticModule.removeSpray(Recipients.ofEveryone(), sprayId); +} +``` + +**Reset sprays** + +```java +public void resetSpraysExample() { + this.cosmeticModule.resetSprays(Recipients.ofEveryone()); +} +``` + +### `Cosmetic` Options + +`.id(int)` is the Lunar Client cosmetic id for this entry. + +```java +.id(434) +``` + +`.options(CosmeticOptions)` is optional. Pass one concrete subtype such as `HatOptions`, `CloakOptions`, `PetOptions`, or `BodyOptions` (omit or pass `null` for id-only cosmetics). + +**`HatOptions`** + +```java +.options(HatOptions.builder() + .showOverHelmet(true) + .showOverSkinLayer(true) + .heightOffset(0.05f) + .build()) +``` + +**`CloakOptions`** + +```java +.options(CloakOptions.builder() + .useClothPhysics(true) + .build()) +``` + +**`PetOptions`** + +```java +.options(PetOptions.builder() + .flipShoulder(true) + .build()) +``` + +**`BodyOptions`** + +```java +.options(BodyOptions.builder() + .showOverChestplate(true) + .showOverLeggings(true) + .showOverBoots(true) + .build()) +``` + +### `Spray` Options + +`.sprayId(int)` is the Lunar Client spray cosmetic id. + +```java +.sprayId(sprayId) +``` + +`.location(ApolloBlockLocation)` is the block the spray is placed on. See the [locations page](/apollo/developers/utilities/locations) for more. + +```java +.location(ApolloBlockLocation.builder() + .world(block.getWorld().getName()) + .x(block.getX()) + .y(block.getY()) + .z(block.getZ()) + .build()) +``` + +`.facing(Direction)` selects which face of the block the spray uses. + +```java +.facing(Direction.UP) +``` + +`.rotation(float)` is rotation in degrees on the client (default `0f`). + +```java +.rotation(0f) +``` + +`.duration(java.time.Duration)` is how long the spray stays visible. See the [java.time.Duration Javadocs](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html) for more. + +```java +.duration(Duration.ofSeconds(60)) +``` + + + + + + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + +**Equip cosmetics on an NPC** + +```java +public void equipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmetics = Lists.newArrayList( + Cosmetic.newBuilder() + .setId(434) + .build(), + Cosmetic.newBuilder() + .setId(3654) + .build(), + Cosmetic.newBuilder() + .setId(5095) + .setPetOptions(PetOptions.newBuilder() + .setFlipShoulder(true) + .build()) + .build(), + Cosmetic.newBuilder() + .setId(3) + .setCloakOptions(CloakOptions.newBuilder() + .setUseClothPhysics(true) + .build()) + .build(), + Cosmetic.newBuilder() + .setId(3977) + .build() + ); + + EquipNpcCosmeticsMessage message = EquipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addAllCosmetics(cosmetics) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Unequip cosmetics from an NPC** + +```java +public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); + + UnequipNpcCosmeticsMessage message = UnequipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addAllCosmeticIds(cosmeticIds) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); +} +``` + +**Reset NPC cosmetics** + +```java +public void resetNpcCosmeticsExample(Player viewer, UUID npcUuid) { + ResetNpcCosmeticsMessage message = ResetNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); +} +``` + +**Display spray** + +```java +public void displaySprayExample(Player viewer, int sprayId) { + Block block = viewer.getTargetBlockExact(10); + if (block == null) { + return; + } + + Material material = block.getType(); + if (material.isAir() || !material.isSolid()) { + return; + } + + DisplaySprayMessage message = DisplaySprayMessage.newBuilder() + .setSprayId(sprayId) + .setLocation(ProtobufUtil.createBlockLocationProto(block.getLocation())) + .setFacing(Direction.DIRECTION_UP) + .setRotation(0f) + .setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(60))) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); +} +``` + + + **Sprays** are client-local and validated against loaded chunks. When the backing chunk unloads, sprays are removed and **do not** come back until the server sends a display spray packet again. + + +**Remove sprays** + +```java +public void removeSprayExample(int sprayId) { + RemoveSprayMessage message = RemoveSprayMessage.newBuilder() + .setSprayId(sprayId) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); +} +``` + +**Reset sprays** + +```java +public void resetSpraysExample() { + ProtobufPacketUtil.broadcastPacket(ResetSpraysMessage.getDefaultInstance()); +} +``` + + + + + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + +**Equip cosmetics on an NPC** + +```java +public void equipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + JsonArray cosmeticsArray = new JsonArray(); + + JsonObject first = new JsonObject(); + first.addProperty("id", 434); + cosmeticsArray.add(first); + + JsonObject second = new JsonObject(); + second.addProperty("id", 3654); + cosmeticsArray.add(second); + + JsonObject third = new JsonObject(); + third.addProperty("id", 5095); + JsonObject petOptions = new JsonObject(); + petOptions.addProperty("flip_shoulder", true); + third.add("pet_options", petOptions); + cosmeticsArray.add(third); + + JsonObject fourth = new JsonObject(); + fourth.addProperty("id", 3); + JsonObject cloakOptions = new JsonObject(); + cloakOptions.addProperty("use_cloth_physics", true); + fourth.add("cloak_options", cloakOptions); + cosmeticsArray.add(fourth); + + JsonObject fifth = new JsonObject(); + fifth.addProperty("id", 3977); + cosmeticsArray.add(fifth); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetics", cosmeticsArray); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Unequip cosmetics from an NPC** + +```java +public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); + + JsonArray cosmeticIdsArray = new JsonArray(); + cosmeticIds.forEach(cosmeticIdsArray::add); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetic_ids", cosmeticIdsArray); + + JsonPacketUtil.broadcastPacket(message); +} +``` + +**Reset NPC cosmetics** + +```java +public void resetNpcCosmeticsExample(Player viewer, UUID npcUuid) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.ResetNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + + JsonPacketUtil.broadcastPacket(message); +} +``` + +**Display spray** + +```java +public void displaySprayExample(Player viewer, int sprayId) { + Block block = viewer.getTargetBlockExact(10); + if (block == null) { + return; + } + + Material material = block.getType(); + if (material.isAir() || !material.isSolid()) { + return; + } + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.DisplaySprayMessage"); + message.addProperty("spray_id", sprayId); + message.add("location", JsonUtil.createBlockLocationObject(block.getLocation())); + message.addProperty("facing", "UP"); + message.addProperty("rotation", 0f); + message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(60))); + + JsonPacketUtil.broadcastPacket(message); +} +``` + + + **Sprays** are client-local and validated against loaded chunks. When the backing chunk unloads, sprays are removed and **do not** come back until the server sends a display spray packet again. + + +**Remove sprays** + +```java +public void removeSprayExample(int sprayId) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.RemoveSprayMessage"); + message.addProperty("spray_id", sprayId); + + JsonPacketUtil.broadcastPacket(message); +} +``` + +**Reset sprays** + +```java +public void resetSpraysExample() { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.ResetSpraysMessage"); + + JsonPacketUtil.broadcastPacket(message); +} +``` + + + + diff --git a/example/bukkit/api/build.gradle.kts b/example/bukkit/api/build.gradle.kts index 901f2477..60846287 100644 --- a/example/bukkit/api/build.gradle.kts +++ b/example/bukkit/api/build.gradle.kts @@ -15,3 +15,9 @@ dependencies { compileOnly(libs.folia) implementation(project(":example:bukkit:apollo-example-bukkit-common")) } + +tasks.shadowJar { + manifest { + attributes["paperweight-mappings-namespace"] = "mojang+yarn" + } +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java index 8575bc78..5b26d6f8 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java @@ -37,6 +37,7 @@ import com.lunarclient.apollo.example.api.module.ColoredFireApiExample; import com.lunarclient.apollo.example.api.module.CombatApiExample; import com.lunarclient.apollo.example.api.module.CooldownApiExample; +import com.lunarclient.apollo.example.api.module.CosmeticApiExample; import com.lunarclient.apollo.example.api.module.EntityApiExample; import com.lunarclient.apollo.example.api.module.GlowApiExample; import com.lunarclient.apollo.example.api.module.HologramApiExample; @@ -85,6 +86,7 @@ public void registerModuleExamples() { this.setBeamExample(new BeamApiExample()); this.setBorderExample(new BorderApiExample()); this.setChatExample(new ChatApiExample()); + this.setCosmeticExample(new CosmeticApiExample()); this.setColoredFireExample(new ColoredFireApiExample()); this.setCombatExample(new CombatApiExample()); this.setCooldownExample(new CooldownApiExample()); diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/debug/impl/ModSettingsTest.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/debug/impl/ModSettingsTest.java index 8601a6a2..1f6d1568 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/debug/impl/ModSettingsTest.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/debug/impl/ModSettingsTest.java @@ -165,7 +165,7 @@ private void changeOptions(ApolloPlayer player) { } private void changeOption(ApolloPlayer player, Option option, Object value) { - this.modSettingModule.getOptions().set(option, value); + this.modSettingModule.getOptions().set(player, option, value); this.changes.computeIfAbsent(player.getUniqueId(), k -> new ArrayList<>()) .add(new OptionChange(option.getKey(), value)); diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java index e4dd4579..0949ac90 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java @@ -30,8 +30,12 @@ import com.lunarclient.apollo.example.ApolloExamplePlugin; import com.lunarclient.apollo.example.api.module.TeamApiExample; import com.lunarclient.apollo.example.module.impl.CooldownExample; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; import com.lunarclient.apollo.example.module.impl.ServerLinkExample; +import com.lunarclient.apollo.example.nms.NpcManager; +import com.lunarclient.apollo.example.nms.PlayerNpc; import com.lunarclient.apollo.player.ApolloPlayer; +import java.util.Optional; import org.bukkit.entity.Player; public class ApolloPlayerApiListener implements ApolloListener { @@ -60,7 +64,6 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) { this.example.getBeamExample().displayBeamExample(player); this.example.getBorderExample().displayBorderExample(player); - this.example.getNametagExample().overrideNametagExample(player); this.example.getWaypointExample().displayWaypointExample(player); CooldownExample cooldownExample = this.example.getCooldownExample(); @@ -72,6 +75,12 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) { ServerLinkExample serverLinkExample = this.example.getServerLinkExample(); serverLinkExample.overrideServerLinkResourceExample(player); serverLinkExample.addServerLinkExample(player); + + CosmeticExample cosmeticExample = this.example.getCosmeticExample(); + NpcManager npcManager = this.example.getNpcManager(); + + Optional npc = npcManager.findByName("Apollo"); + npc.ifPresent(playerNpc -> cosmeticExample.equipNpcCosmeticsExample(player, playerNpc.getUuid())); } } diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java new file mode 100644 index 00000000..df3f8c38 --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java @@ -0,0 +1,149 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module; + +import com.google.common.collect.Lists; +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.location.ApolloBlockLocation; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.module.cosmetic.Cosmetic; +import com.lunarclient.apollo.module.cosmetic.CosmeticModule; +import com.lunarclient.apollo.module.cosmetic.Spray; +import com.lunarclient.apollo.module.cosmetic.options.CloakOptions; +import com.lunarclient.apollo.module.cosmetic.options.PetOptions; +import com.lunarclient.apollo.module.packetenrichment.raytrace.Direction; +import com.lunarclient.apollo.player.ApolloPlayer; +import com.lunarclient.apollo.recipients.Recipients; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +public class CosmeticApiExample extends CosmeticExample { + + private final CosmeticModule cosmeticModule = Apollo.getModuleManager().getModule(CosmeticModule.class); + + @Override + public void equipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmetics = Lists.newArrayList( + Cosmetic.builder() + .id(434) + .build(), + Cosmetic.builder() + .id(3654) + .build(), + Cosmetic.builder() + .id(5095) + .options(PetOptions.builder() + .flipShoulder(true) + .build()) + .build(), + Cosmetic.builder() + .id(3) + .options(CloakOptions.builder() + .useClothPhysics(true) + .build()) + .build(), + Cosmetic.builder() + .id(3977) + .build() + ); + + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.cosmeticModule.equipNpcCosmetics(apolloPlayer, npcUuid, cosmetics)); + } + + @Override + public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds) { + List cosmetics = cosmeticIds.stream() + .map(id -> Cosmetic.builder().id(id).build()) + .collect(Collectors.toList()); + + this.cosmeticModule.equipNpcCosmetics(Recipients.ofEveryone(), npcUuid, cosmetics); + } + + @Override + public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> { + List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); + this.cosmeticModule.unequipNpcCosmetics(apolloPlayer, npcUuid, cosmeticIds); + }); + } + + @Override + public void unequipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds) { + this.cosmeticModule.unequipNpcCosmetics(Recipients.ofEveryone(), npcUuid, cosmeticIds); + } + + @Override + public void resetNpcCosmeticsExample(Player viewer, UUID npcUuid) { + this.cosmeticModule.resetNpcCosmetics(Recipients.ofEveryone(), npcUuid); + } + + @Override + public void displaySprayExample(Player viewer, int sprayId) { + Block block = viewer.getTargetBlockExact(10); + if (block == null) { + return; + } + + Material material = block.getType(); + if (material.isAir() || !material.isSolid()) { + return; + } + + ApolloBlockLocation location = ApolloBlockLocation.builder() + .world(block.getWorld().getName()) + .x(block.getX()) + .y(block.getY()) + .z(block.getZ()) + .build(); + + Spray spray = Spray.builder() + .sprayId(sprayId) + .location(location) + .facing(Direction.UP) + .rotation(0f) + .duration(Duration.ofSeconds(60)) + .build(); + + this.cosmeticModule.displaySpray(Recipients.ofEveryone(), spray); + } + + @Override + public void removeSprayExample(int sprayId) { + this.cosmeticModule.removeSpray(Recipients.ofEveryone(), sprayId); + } + + @Override + public void resetSpraysExample() { + this.cosmeticModule.resetSprays(Recipients.ofEveryone()); + } + +} diff --git a/example/bukkit/api/src/main/resources/plugin.yml b/example/bukkit/api/src/main/resources/plugin.yml index e2d9ee4c..afd8f1d0 100644 --- a/example/bukkit/api/src/main/resources/plugin.yml +++ b/example/bukkit/api/src/main/resources/plugin.yml @@ -7,6 +7,8 @@ api-version: 1.13 folia-supported: true commands: + npc: + description: "NPCs!" apollodebug: description: "Apollo Debug!" modstatus: @@ -25,6 +27,8 @@ commands: description: "Combat!" cooldown: description: "Cooldowns!" + cosmetic: + description: "Cosmetics!" entity: description: "Entity!" glint: diff --git a/example/bukkit/common/build.gradle.kts b/example/bukkit/common/build.gradle.kts index 5c643f06..61b202e0 100644 --- a/example/bukkit/common/build.gradle.kts +++ b/example/bukkit/common/build.gradle.kts @@ -9,4 +9,6 @@ java { dependencies { compileOnly(libs.bukkit.api) + + api(project(":example:bukkit:apollo-example-bukkit-nms")) } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java index 81d28966..b524b605 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java @@ -30,6 +30,7 @@ import com.lunarclient.apollo.example.command.ColoredFireCommand; import com.lunarclient.apollo.example.command.CombatCommand; import com.lunarclient.apollo.example.command.CooldownCommand; +import com.lunarclient.apollo.example.command.CosmeticCommand; import com.lunarclient.apollo.example.command.EntityCommand; import com.lunarclient.apollo.example.command.GlintCommand; import com.lunarclient.apollo.example.command.GlowCommand; @@ -40,6 +41,7 @@ import com.lunarclient.apollo.example.command.NametagCommand; import com.lunarclient.apollo.example.command.NickHiderCommand; import com.lunarclient.apollo.example.command.NotificationCommand; +import com.lunarclient.apollo.example.command.NpcCommand; import com.lunarclient.apollo.example.command.PayNowCommand; import com.lunarclient.apollo.example.command.RichPresenceCommand; import com.lunarclient.apollo.example.command.SaturationCommand; @@ -61,6 +63,7 @@ import com.lunarclient.apollo.example.module.impl.ColoredFireExample; import com.lunarclient.apollo.example.module.impl.CombatExample; import com.lunarclient.apollo.example.module.impl.CooldownExample; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; import com.lunarclient.apollo.example.module.impl.EntityExample; import com.lunarclient.apollo.example.module.impl.GlintExample; import com.lunarclient.apollo.example.module.impl.GlowExample; @@ -85,6 +88,7 @@ import com.lunarclient.apollo.example.module.impl.TransferExample; import com.lunarclient.apollo.example.module.impl.VignetteExample; import com.lunarclient.apollo.example.module.impl.WaypointExample; +import com.lunarclient.apollo.example.nms.NpcManager; import lombok.Getter; import lombok.Setter; import org.bukkit.plugin.java.JavaPlugin; @@ -95,10 +99,13 @@ public abstract class ApolloExamplePlugin extends JavaPlugin { @Getter private static ApolloExamplePlugin instance; + private NpcManager npcManager; + private AutoTextHotkeyExample autoTextHotkeyExample; private BeamExample beamExample; private BorderExample borderExample; private ChatExample chatExample; + private CosmeticExample cosmeticExample; private ColoredFireExample coloredFireExample; private CombatExample combatExample; private CooldownExample cooldownExample; @@ -131,6 +138,8 @@ public abstract class ApolloExamplePlugin extends JavaPlugin { public void onEnable() { instance = this; + this.npcManager = new NpcManager(this); + this.registerCommonCommands(); this.registerCommonModulesExamples(); @@ -142,10 +151,14 @@ public void onEnable() { @Override public void onDisable() { - + if (this.npcManager != null) { + this.npcManager.removeAll(); + } } private void registerCommonCommands() { + this.getCommand("npc").setExecutor(new NpcCommand()); + this.getCommand("autotexthotkey").setExecutor(new AutoTextHotkeyCommand()); this.getCommand("beam").setExecutor(new BeamCommand()); this.getCommand("border").setExecutor(new BorderCommand()); @@ -153,6 +166,7 @@ private void registerCommonCommands() { this.getCommand("coloredfire").setExecutor(new ColoredFireCommand()); this.getCommand("combat").setExecutor(new CombatCommand()); this.getCommand("cooldown").setExecutor(new CooldownCommand()); + this.getCommand("cosmetic").setExecutor(new CosmeticCommand()); this.getCommand("entity").setExecutor(new EntityCommand()); this.getCommand("glint").setExecutor(new GlintCommand()); this.getCommand("glow").setExecutor(new GlowCommand()); diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java new file mode 100644 index 00000000..fb88c1b5 --- /dev/null +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java @@ -0,0 +1,198 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.command; + +import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.example.nms.PlayerNpc; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class CosmeticCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("Player only!"); + return true; + } + + Player player = (Player) sender; + + if (args.length < 1) { + this.sendUsage(player); + return true; + } + + CosmeticExample example = ApolloExamplePlugin.getInstance().getCosmeticExample(); + + if ("spray".equalsIgnoreCase(args[0])) { + return this.handleSpray(player, example, args); + } + + if (args.length < 2) { + this.sendUsage(player); + return true; + } + + String npcName = args[1]; + boolean uuidParam = npcName.contains("-"); + + Optional npcOpt = ApolloExamplePlugin.getInstance().getNpcManager().findByName(npcName); + if (!npcOpt.isPresent() && !uuidParam) { + player.sendMessage(ChatColor.RED + "No NPC found with name: " + npcName); + return true; + } + + UUID uuid; + if (uuidParam) { + uuid = UUID.fromString(npcName); + } else { + uuid = npcOpt.get().getUuid(); + } + + switch (args[0].toLowerCase()) { + case "equip": { + List cosmeticIds = this.parseCosmeticIds(args); + example.equipNpcCosmeticsInternal(player, uuid, cosmeticIds); + player.sendMessage(ChatColor.GREEN + "Equipped cosmetics " + cosmeticIds + " on NPC " + npcName); + break; + } + + case "unequip": { + List cosmeticIds = this.parseCosmeticIds(args); + example.unequipNpcCosmeticsInternal(player, uuid, cosmeticIds); + player.sendMessage(ChatColor.GREEN + "Unequipped cosmetics " + cosmeticIds + " from NPC " + npcName); + break; + } + + case "reset": { + example.resetNpcCosmeticsExample(player, uuid); + player.sendMessage(ChatColor.GREEN + "Reset all cosmetics on NPC " + npcName); + break; + } + + default: { + this.sendUsage(player); + break; + } + } + + return true; + } + + private boolean handleSpray(Player player, CosmeticExample example, String[] args) { + if (args.length < 2) { + this.sendSprayUsage(player); + return true; + } + + switch (args[1].toLowerCase()) { + case "display": { + if (args.length < 3) { + this.sendSprayUsage(player); + return true; + } + + int sprayId; + try { + sprayId = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + player.sendMessage("Spray id must be an integer."); + return true; + } + + example.displaySprayExample(player, sprayId); + player.sendMessage(ChatColor.GREEN + "Displayed spray " + sprayId + " at your target block"); + break; + } + + case "remove": { + if (args.length < 3) { + this.sendSprayUsage(player); + return true; + } + + int sprayId; + try { + sprayId = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + player.sendMessage("Spray id must be an integer."); + return true; + } + + example.removeSprayExample(sprayId); + player.sendMessage(ChatColor.GREEN + "Removed all sprays with id " + sprayId); + break; + } + + case "reset": { + example.resetSpraysExample(); + player.sendMessage(ChatColor.GREEN + "Reset all server sprays"); + break; + } + + default: { + this.sendSprayUsage(player); + break; + } + } + + return true; + } + + private List parseCosmeticIds(String[] args) { + List ids = new ArrayList<>(); + for (int i = 2; i < args.length; i++) { + try { + ids.add(Integer.parseInt(args[i])); + } catch (NumberFormatException ignored) { + } + } + return ids; + } + + private void sendUsage(Player player) { + player.sendMessage("Usage:"); + player.sendMessage(" - /cosmetic equip [cosmeticIds]"); + player.sendMessage(" - /cosmetic unequip [cosmeticIds]"); + player.sendMessage(" - /cosmetic reset "); + this.sendSprayUsage(player); + } + + private void sendSprayUsage(Player player) { + player.sendMessage(" - /cosmetic spray display "); + player.sendMessage(" - /cosmetic spray remove "); + player.sendMessage(" - /cosmetic spray reset"); + } + +} diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java new file mode 100644 index 00000000..1f03492a --- /dev/null +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java @@ -0,0 +1,115 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.command; + +import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.nms.NpcManager; +import com.lunarclient.apollo.example.nms.PlayerNpc; +import java.util.Optional; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class NpcCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("Player only!"); + return true; + } + + Player player = (Player) sender; + + if (args.length < 1) { + this.sendUsage(player); + return true; + } + + NpcManager npcManager = ApolloExamplePlugin.getInstance().getNpcManager(); + + switch (args[0].toLowerCase()) { + case "spawn": { + if (args.length < 2) { + this.sendUsage(player); + break; + } + + PlayerNpc npc = npcManager.spawnNpc(args[1], player.getLocation()); + if (npc == null) { + player.sendMessage(ChatColor.RED + "Failed to spawn NPC."); + break; + } + + player.sendMessage(ChatColor.GREEN + "Spawned NPC " + npc.getName() + " (" + npc.getUuid() + ")"); + break; + } + + case "remove": { + if (args.length < 2) { + this.sendUsage(player); + break; + } + + Optional byName = npcManager.findByName(args[1]); + if (!byName.isPresent()) { + player.sendMessage(ChatColor.RED + "No NPC found with name: " + args[1]); + break; + } + + npcManager.removeNpc(byName.get().getUuid()); + player.sendMessage(ChatColor.GREEN + "Removed NPC " + args[1]); + break; + } + + case "reset": { + if (npcManager.getNpcs().isEmpty()) { + player.sendMessage(ChatColor.YELLOW + "There are no NPCs to remove."); + break; + } + + npcManager.removeAll(); + player.sendMessage(ChatColor.GREEN + "Removed all tracked NPCs."); + break; + } + + default: { + this.sendUsage(player); + break; + } + } + + return true; + } + + private void sendUsage(Player player) { + player.sendMessage("Usage: /npc spawn "); + player.sendMessage("Usage: /npc remove "); + player.sendMessage("Usage: /npc reset"); + } + +} diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java new file mode 100644 index 00000000..d997de77 --- /dev/null +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java @@ -0,0 +1,49 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.module.impl; + +import com.lunarclient.apollo.example.module.ApolloModuleExample; +import java.util.List; +import java.util.UUID; +import org.bukkit.entity.Player; + +public abstract class CosmeticExample extends ApolloModuleExample { + + public abstract void equipNpcCosmeticsExample(Player viewer, UUID npcUuid); + + public abstract void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds); + + public abstract void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid); + + public abstract void unequipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds); + + public abstract void resetNpcCosmeticsExample(Player viewer, UUID npcUuid); + + public abstract void displaySprayExample(Player viewer, int sprayId); + + public abstract void removeSprayExample(int sprayId); + + public abstract void resetSpraysExample(); + +} diff --git a/example/bukkit/json/build.gradle.kts b/example/bukkit/json/build.gradle.kts index c37f0cbf..797fa87c 100644 --- a/example/bukkit/json/build.gradle.kts +++ b/example/bukkit/json/build.gradle.kts @@ -18,3 +18,9 @@ dependencies { compileOnly(libs.folia) implementation(project(":example:bukkit:apollo-example-bukkit-common")) } + +tasks.shadowJar { + manifest { + attributes["paperweight-mappings-namespace"] = "mojang+yarn" + } +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java index 196b6f81..7240c067 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java @@ -33,6 +33,7 @@ import com.lunarclient.apollo.example.json.module.ColoredFireJsonExample; import com.lunarclient.apollo.example.json.module.CombatJsonExample; import com.lunarclient.apollo.example.json.module.CooldownJsonExample; +import com.lunarclient.apollo.example.json.module.CosmeticJsonExample; import com.lunarclient.apollo.example.json.module.EntityJsonExample; import com.lunarclient.apollo.example.json.module.GlowJsonExample; import com.lunarclient.apollo.example.json.module.HologramJsonExample; @@ -73,6 +74,7 @@ public void registerModuleExamples() { this.setBeamExample(new BeamJsonExample()); this.setBorderExample(new BorderJsonExample()); this.setChatExample(new ChatJsonExample()); + this.setCosmeticExample(new CosmeticJsonExample()); this.setColoredFireExample(new ColoredFireJsonExample()); this.setCombatExample(new CombatJsonExample()); this.setCooldownExample(new CooldownJsonExample()); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java new file mode 100644 index 00000000..6fe78acc --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java @@ -0,0 +1,173 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module; + +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +public class CosmeticJsonExample extends CosmeticExample { + + @Override + public void equipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + JsonArray cosmeticsArray = new JsonArray(); + + JsonObject first = new JsonObject(); + first.addProperty("id", 434); + cosmeticsArray.add(first); + + JsonObject second = new JsonObject(); + second.addProperty("id", 3654); + cosmeticsArray.add(second); + + JsonObject third = new JsonObject(); + third.addProperty("id", 5095); + JsonObject petOptions = new JsonObject(); + petOptions.addProperty("flip_shoulder", true); + third.add("pet_options", petOptions); + cosmeticsArray.add(third); + + JsonObject fourth = new JsonObject(); + fourth.addProperty("id", 3); + JsonObject cloakOptions = new JsonObject(); + cloakOptions.addProperty("use_cloth_physics", true); + fourth.add("cloak_options", cloakOptions); + cosmeticsArray.add(fourth); + + JsonObject fifth = new JsonObject(); + fifth.addProperty("id", 3977); + cosmeticsArray.add(fifth); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetics", cosmeticsArray); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds) { + JsonArray cosmeticsArray = new JsonArray(); + for (int cosmeticId : cosmeticIds) { + JsonObject cosmetic = new JsonObject(); + cosmetic.addProperty("id", cosmeticId); + cosmeticsArray.add(cosmetic); + } + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetics", cosmeticsArray); + + JsonPacketUtil.broadcastPacket(message); + } + + @Override + public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); + + JsonArray cosmeticIdsArray = new JsonArray(); + cosmeticIds.forEach(cosmeticIdsArray::add); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetic_ids", cosmeticIdsArray); + + JsonPacketUtil.broadcastPacket(message); + } + + @Override + public void unequipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds) { + JsonArray cosmeticIdsArray = new JsonArray(); + cosmeticIds.forEach(cosmeticIdsArray::add); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetic_ids", cosmeticIdsArray); + + JsonPacketUtil.broadcastPacket(message); + } + + @Override + public void resetNpcCosmeticsExample(Player viewer, UUID npcUuid) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.ResetNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + + JsonPacketUtil.broadcastPacket(message); + } + + @Override + public void displaySprayExample(Player viewer, int sprayId) { + Block block = viewer.getTargetBlockExact(10); + if (block == null) { + return; + } + + Material material = block.getType(); + if (material.isAir() || !material.isSolid()) { + return; + } + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.DisplaySprayMessage"); + message.addProperty("spray_id", sprayId); + message.add("location", JsonUtil.createBlockLocationObject(block.getLocation())); + message.addProperty("facing", "UP"); + message.addProperty("rotation", 0f); + message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(60))); + + JsonPacketUtil.broadcastPacket(message); + } + + @Override + public void removeSprayExample(int sprayId) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.RemoveSprayMessage"); + message.addProperty("spray_id", sprayId); + + JsonPacketUtil.broadcastPacket(message); + } + + @Override + public void resetSpraysExample() { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.ResetSpraysMessage"); + + JsonPacketUtil.broadcastPacket(message); + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java index 3e1b9750..d6ff7b8f 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java @@ -41,7 +41,7 @@ public final class JsonPacketUtil { private static final List APOLLO_MODULES = Arrays.asList("auto_text_hotkey", "beam", "border", "chat", "colored_fire", "combat", "cooldown", - "entity", "glint", "glow", "hologram", "inventory", "limb", "mod_setting", "nametag", "nick_hider", "notification", "pay_now", "packet_enrichment", + "cosmetic", "entity", "glint", "glow", "hologram", "inventory", "limb", "mod_setting", "nametag", "nick_hider", "notification", "pay_now", "packet_enrichment", "rich_presence", "saturation", "server_rule", "staff_mod", "stopwatch", "team", "tebex", "title", "tnt_countdown", "transfer", "vignette", "waypoint" ); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java index 974f621e..694ce2f4 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java @@ -95,9 +95,13 @@ public static JsonObject createCuboid2DObject(double minX, double minZ, double m } public static JsonObject createEntityIdObject(@NotNull Entity entity) { + return JsonUtil.createEntityIdObject(entity.getEntityId(), entity.getUniqueId()); + } + + public static JsonObject createEntityIdObject(int entityId, @NotNull UUID uuid) { JsonObject entityIdObject = new JsonObject(); - entityIdObject.addProperty("entity_id", entity.getEntityId()); - entityIdObject.add("entity_uuid", JsonUtil.createUuidObject(entity.getUniqueId())); + entityIdObject.addProperty("entity_id", entityId); + entityIdObject.add("entity_uuid", JsonUtil.createUuidObject(uuid)); return entityIdObject; } diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml index f4a02a77..d289fbdd 100644 --- a/example/bukkit/json/src/main/resources/plugin.yml +++ b/example/bukkit/json/src/main/resources/plugin.yml @@ -7,6 +7,8 @@ api-version: 1.13 folia-supported: true commands: + npc: + description: "NPCs!" autotexthotkey: description: "Auto Text Hotkey!" beam: @@ -21,6 +23,8 @@ commands: description: "Combat!" cooldown: description: "Cooldowns!" + cosmetic: + description: "Cosmetics!" entity: description: "Entity!" glint: diff --git a/example/bukkit/nms/build.gradle.kts b/example/bukkit/nms/build.gradle.kts new file mode 100644 index 00000000..8158fb3c --- /dev/null +++ b/example/bukkit/nms/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("apollo.base-conventions") + id("io.papermc.paperweight.userdev") version "2.0.0-beta.21" +} + +java { + javaTarget(25) +} + +paperweight.reobfArtifactConfiguration = + io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +tasks.named("reobfJar") { + enabled = false +} + +tasks.compileJava { + options.compilerArgs.addAll(listOf("--release", "21")) +} + +configurations.named("compileClasspath") { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25) + } +} + +configurations.named("runtimeClasspath") { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25) + } +} + +listOf("apiElements", "runtimeElements").forEach { name -> + configurations.named(name) { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21) + } + } +} + +dependencies { + paperweight.paperDevBundle("26.1.1.build.+") +} diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java new file mode 100644 index 00000000..22f1d0b2 --- /dev/null +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java @@ -0,0 +1,186 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.nms; + +import com.mojang.authlib.GameProfile; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ParticleStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.HumanoidArm; +import net.minecraft.world.entity.player.ChatVisiblity; +import net.minecraft.world.level.GameType; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.entity.CraftPlayer; +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.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; + +public final class NpcManager implements Listener { + + private static final ClientInformation NPC_CLIENT_INFO = new ClientInformation( + "en_us", 2, ChatVisiblity.HIDDEN, false, 0x7F, + HumanoidArm.RIGHT, false, false, ParticleStatus.ALL + ); + + private final Map npcs = new HashMap<>(); + private final JavaPlugin plugin; + + public NpcManager(JavaPlugin plugin) { + this.plugin = plugin; + + Bukkit.getPluginManager().registerEvents(this, plugin); + Bukkit.getScheduler().runTask(plugin, this::spawnDefaultNpcs); + } + + public void removeNpc(UUID uuid) { + PlayerNpc npc = this.npcs.remove(uuid); + if (npc == null) { + return; + } + + this.despawnNpcs(npc); + } + + public void removeAll() { + for (PlayerNpc npc : new ArrayList<>(this.npcs.values())) { + this.despawnNpcs(npc); + } + + this.npcs.clear(); + } + + public Optional findByName(String name) { + return this.npcs.values().stream() + .filter(npc -> npc.getName().equalsIgnoreCase(name)) + .findFirst(); + } + + public Collection getNpcs() { + return new ArrayList<>(this.npcs.values()); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + ServerPlayer viewer = ((CraftPlayer) event.getPlayer()).getHandle(); + for (PlayerNpc npc : this.npcs.values()) { + this.showNpc(viewer, npc); + } + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + ServerPlayer viewer = ((CraftPlayer) event.getPlayer()).getHandle(); + for (PlayerNpc npc : this.npcs.values()) { + this.hideNpc(viewer, npc); + } + } + + private void spawnDefaultNpcs() { + this.spawnNpc("Apollo", new Location(Bukkit.getWorld("world"), 20.5, 65, 5.5, 90f, 0f)); + } + + public @Nullable PlayerNpc spawnNpc(String name, Location location) { + World world = location.getWorld(); + if (world == null) { + return null; + } + + MinecraftServer server = MinecraftServer.getServer(); + ServerLevel level = ((CraftWorld) world).getHandle(); + + UUID npcUuid = new UUID(UUID.randomUUID().getMostSignificantBits(), 0L); + GameProfile profile = new GameProfile(npcUuid, name); + ServerPlayer npc = new ServerPlayer(server, level, profile, NpcManager.NPC_CLIENT_INFO); + + npc.setPos(location.getX(), location.getY(), location.getZ()); + npc.setYRot(location.getYaw()); + npc.setXRot(location.getPitch()); + + PlayerNpc playerNpc = new PlayerNpc(npc.getUUID(), name, location.clone(), npc); + this.npcs.put(playerNpc.getUuid(), playerNpc); + + for (ServerPlayer viewer : server.getPlayerList().getPlayers()) { + this.showNpc(viewer, playerNpc); + } + + return playerNpc; + } + + private void showNpc(ServerPlayer viewer, PlayerNpc npc) { + ServerPlayer entity = npc.getHandle(); + Location location = npc.getLocation(); + GameProfile profile = entity.getGameProfile(); + + ClientboundPlayerInfoUpdatePacket.Entry entry = new ClientboundPlayerInfoUpdatePacket.Entry( + entity.getUUID(), profile, true, 0, GameType.CREATIVE, null, true, 0, null); + + viewer.connection.send(new ClientboundPlayerInfoUpdatePacket( + EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER), entry)); + viewer.connection.send(new ClientboundAddEntityPacket( + entity.getId(), entity.getUUID(), + location.getX(), location.getY(), location.getZ(), + location.getPitch(), location.getYaw(), + entity.getType(), 0, Vec3.ZERO, location.getYaw())); + viewer.connection.send(new ClientboundSetEntityDataPacket( + entity.getId(), entity.getEntityData().getNonDefaultValues())); + + Bukkit.getScheduler().runTaskLater(this.plugin, () -> { + viewer.connection.send(new ClientboundPlayerInfoRemovePacket(List.of(entity.getUUID()))); + }, 5L); + } + + private void hideNpc(ServerPlayer viewer, PlayerNpc npc) { + viewer.connection.send(new ClientboundRemoveEntitiesPacket(npc.getEntityId())); + viewer.connection.send(new ClientboundPlayerInfoRemovePacket(List.of(npc.getUuid()))); + } + + private void despawnNpcs(PlayerNpc npc) { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + this.hideNpc(player, npc); + } + } + +} diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java new file mode 100644 index 00000000..1237a4af --- /dev/null +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java @@ -0,0 +1,45 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.nms; + +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.Location; + +@Getter +@RequiredArgsConstructor +public final class PlayerNpc { + + private final UUID uuid; + private final String name; + private final Location location; + private final ServerPlayer handle; + + public int getEntityId() { + return this.handle.getId(); + } + +} diff --git a/example/bukkit/proto/build.gradle.kts b/example/bukkit/proto/build.gradle.kts index ea3f1a41..37a8c4bc 100644 --- a/example/bukkit/proto/build.gradle.kts +++ b/example/bukkit/proto/build.gradle.kts @@ -20,3 +20,9 @@ dependencies { compileOnly(libs.folia) implementation(project(":example:bukkit:apollo-example-bukkit-common")) } + +tasks.shadowJar { + manifest { + attributes["paperweight-mappings-namespace"] = "mojang+yarn" + } +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java index f02c1bfd..9112f059 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java @@ -33,6 +33,7 @@ import com.lunarclient.apollo.example.proto.module.ColoredFireProtoExample; import com.lunarclient.apollo.example.proto.module.CombatProtoExample; import com.lunarclient.apollo.example.proto.module.CooldownProtoExample; +import com.lunarclient.apollo.example.proto.module.CosmeticProtoExample; import com.lunarclient.apollo.example.proto.module.EntityProtoExample; import com.lunarclient.apollo.example.proto.module.GlowProtoExample; import com.lunarclient.apollo.example.proto.module.HologramProtoExample; @@ -73,6 +74,7 @@ public void registerModuleExamples() { this.setBeamExample(new BeamProtoExample()); this.setBorderExample(new BorderProtoExample()); this.setChatExample(new ChatProtoExample()); + this.setCosmeticExample(new CosmeticProtoExample()); this.setColoredFireExample(new ColoredFireProtoExample()); this.setCombatExample(new CombatProtoExample()); this.setCooldownExample(new CooldownProtoExample()); diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java new file mode 100644 index 00000000..4454da14 --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java @@ -0,0 +1,166 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module; + +import com.google.common.collect.Lists; +import com.lunarclient.apollo.cosmetic.v1.CloakOptions; +import com.lunarclient.apollo.cosmetic.v1.Cosmetic; +import com.lunarclient.apollo.cosmetic.v1.DisplaySprayMessage; +import com.lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage; +import com.lunarclient.apollo.cosmetic.v1.PetOptions; +import com.lunarclient.apollo.cosmetic.v1.RemoveSprayMessage; +import com.lunarclient.apollo.cosmetic.v1.ResetNpcCosmeticsMessage; +import com.lunarclient.apollo.cosmetic.v1.ResetSpraysMessage; +import com.lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.packetenrichment.v1.Direction; +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +public class CosmeticProtoExample extends CosmeticExample { + + @Override + public void equipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmetics = Lists.newArrayList( + Cosmetic.newBuilder() + .setId(434) + .build(), + Cosmetic.newBuilder() + .setId(3654) + .build(), + Cosmetic.newBuilder() + .setId(5095) + .setPetOptions(PetOptions.newBuilder() + .setFlipShoulder(true) + .build()) + .build(), + Cosmetic.newBuilder() + .setId(3) + .setCloakOptions(CloakOptions.newBuilder() + .setUseClothPhysics(true) + .build()) + .build(), + Cosmetic.newBuilder() + .setId(3977) + .build() + ); + + EquipNpcCosmeticsMessage message = EquipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addAllCosmetics(cosmetics) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds) { + List cosmetics = cosmeticIds.stream() + .map(id -> Cosmetic.newBuilder().setId(id).build()) + .collect(Collectors.toList()); + + EquipNpcCosmeticsMessage message = EquipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addAllCosmetics(cosmetics) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); + } + + @Override + public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { + List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); + + UnequipNpcCosmeticsMessage message = UnequipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addAllCosmeticIds(cosmeticIds) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); + } + + @Override + public void unequipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds) { + UnequipNpcCosmeticsMessage message = UnequipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addAllCosmeticIds(cosmeticIds) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); + } + + @Override + public void resetNpcCosmeticsExample(Player viewer, UUID npcUuid) { + ResetNpcCosmeticsMessage message = ResetNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); + } + + @Override + public void displaySprayExample(Player viewer, int sprayId) { + Block block = viewer.getTargetBlockExact(10); + if (block == null) { + return; + } + + Material material = block.getType(); + if (material.isAir() || !material.isSolid()) { + return; + } + + DisplaySprayMessage message = DisplaySprayMessage.newBuilder() + .setSprayId(sprayId) + .setLocation(ProtobufUtil.createBlockLocationProto(block.getLocation())) + .setFacing(Direction.DIRECTION_UP) + .setRotation(0f) + .setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(60))) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); + } + + @Override + public void removeSprayExample(int sprayId) { + RemoveSprayMessage message = RemoveSprayMessage.newBuilder() + .setSprayId(sprayId) + .build(); + + ProtobufPacketUtil.broadcastPacket(message); + } + + @Override + public void resetSpraysExample() { + ProtobufPacketUtil.broadcastPacket(ResetSpraysMessage.getDefaultInstance()); + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java index e2130a6b..f719ecfc 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java @@ -42,7 +42,7 @@ public final class ProtobufPacketUtil { private static final List APOLLO_MODULES = Arrays.asList("auto_text_hotkey", "beam", "border", "chat", "colored_fire", "combat", "cooldown", - "entity", "glint", "glow", "hologram", "inventory", "limb", "mod_setting", "nametag", "nick_hider", "notification", "pay_now", "packet_enrichment", + "cosmetic", "entity", "glint", "glow", "hologram", "inventory", "limb", "mod_setting", "nametag", "nick_hider", "notification", "pay_now", "packet_enrichment", "rich_presence", "saturation", "server_rule", "staff_mod", "stopwatch", "team", "tebex", "title", "tnt_countdown", "transfer", "vignette", "waypoint" ); diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml index 6b06ae18..c24755f6 100644 --- a/example/bukkit/proto/src/main/resources/plugin.yml +++ b/example/bukkit/proto/src/main/resources/plugin.yml @@ -7,6 +7,8 @@ api-version: 1.13 folia-supported: true commands: + npc: + description: "NPCs!" autotexthotkey: description: "Auto Text Hotkey!" beam: @@ -21,6 +23,8 @@ commands: description: "Combat!" cooldown: description: "Cooldowns!" + cosmetic: + description: "Cosmetics!" entity: description: "Entity!" glint: diff --git a/example/minestom/api/build.gradle.kts b/example/minestom/api/build.gradle.kts index 47474fed..74e85d9a 100644 --- a/example/minestom/api/build.gradle.kts +++ b/example/minestom/api/build.gradle.kts @@ -20,4 +20,8 @@ tasks { attributes["Main-Class"] = "com.lunarclient.apollo.example.ApolloMinestomExample" } } + + shadowJar { + relocators.empty() + } } diff --git a/extra/adventure4/build.gradle.kts b/extra/adventure4/build.gradle.kts index a362a2a0..ff57f185 100644 --- a/extra/adventure4/build.gradle.kts +++ b/extra/adventure4/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("apollo.publish-conventions") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f6d9054..cb68d5c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,10 +13,11 @@ jetbrains = "24.0.1" lombok = "1.18.38" protobuf = "0.1.6" gson = "2.10.1" -shadow = "8.1.1" -spotless = "6.13.0" +shadow = "9.4.1" +spotless = "8.4.0" velocity = "3.3.0-SNAPSHOT" folia = "1.20.1-R0.1-SNAPSHOT" +paper = "26.1.1.build.+" asm = "9.7.1" minestom = "2025.08.18-1.21.8" @@ -48,6 +49,9 @@ bukkit = { module = "org.spigotmc:spigot", version.ref = "bukkit" } # folia folia = { module = "dev.folia:folia-api", version.ref = "folia" } +# paper +paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper" } + # bungee bungee = { module = "net.md-5:bungeecord-api", version.ref = "bungee" } @@ -60,7 +64,7 @@ minestom = { module = "net.minestom:minestom", version.ref = "minestom" } # build artifactregistry = { module = "gradle.plugin.com.google.cloud.artifactregistry:artifactregistry-gradle-plugin", version.ref = "artifactregistry" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } -shadow = { module = "com.github.johnrengelman:shadow", version.ref = "shadow" } +shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow" } idea = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "idea" } asm = { module = "org.ow2.asm:asm", version.ref = "asm" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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≫L+crb^Bz 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 /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -111,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,16 +204,16 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 6689b85b..5eed7ee8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java index 8965682c..220be9b1 100644 --- a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java +++ b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java @@ -43,6 +43,8 @@ import com.lunarclient.apollo.module.combat.CombatModule; import com.lunarclient.apollo.module.cooldown.CooldownModule; import com.lunarclient.apollo.module.cooldown.CooldownModuleImpl; +import com.lunarclient.apollo.module.cosmetic.CosmeticModule; +import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; import com.lunarclient.apollo.module.glint.GlintModule; @@ -138,6 +140,7 @@ public void onEnable() { .addModule(BeamModule.class, new BeamModuleImpl()) .addModule(BorderModule.class, new BorderModuleImpl()) .addModule(ChatModule.class, new ChatModuleImpl()) + .addModule(CosmeticModule.class, new CosmeticModuleImpl()) .addModule(ColoredFireModule.class, new ColoredFireModuleImpl()) .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) diff --git a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java index 7e958dc0..76cad083 100644 --- a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java +++ b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java @@ -42,6 +42,8 @@ import com.lunarclient.apollo.module.combat.CombatModule; import com.lunarclient.apollo.module.cooldown.CooldownModule; import com.lunarclient.apollo.module.cooldown.CooldownModuleImpl; +import com.lunarclient.apollo.module.cosmetic.CosmeticModule; +import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; @@ -122,6 +124,7 @@ public void onEnable() { .addModule(BeamModule.class, new BeamModuleImpl()) .addModule(BorderModule.class, new BorderModuleImpl()) .addModule(ChatModule.class, new ChatModuleImpl()) + .addModule(CosmeticModule.class, new CosmeticModuleImpl()) .addModule(ColoredFireModule.class, new ColoredFireModuleImpl()) .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) diff --git a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java index e28530da..ecf29b87 100644 --- a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java +++ b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java @@ -42,6 +42,8 @@ import com.lunarclient.apollo.module.combat.CombatModule; import com.lunarclient.apollo.module.cooldown.CooldownModule; import com.lunarclient.apollo.module.cooldown.CooldownModuleImpl; +import com.lunarclient.apollo.module.cosmetic.CosmeticModule; +import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; import com.lunarclient.apollo.module.glow.GlowModule; @@ -128,6 +130,7 @@ public void onEnable() { .addModule(BeamModule.class, new BeamModuleImpl()) .addModule(BorderModule.class, new BorderModuleImpl()) .addModule(ChatModule.class, new ChatModuleImpl()) + .addModule(CosmeticModule.class, new CosmeticModuleImpl()) .addModule(ColoredFireModule.class, new ColoredFireModuleImpl()) .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) diff --git a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java index a461c642..330f4545 100644 --- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java +++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java @@ -42,6 +42,8 @@ import com.lunarclient.apollo.module.combat.CombatModule; import com.lunarclient.apollo.module.cooldown.CooldownModule; import com.lunarclient.apollo.module.cooldown.CooldownModuleImpl; +import com.lunarclient.apollo.module.cosmetic.CosmeticModule; +import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; import com.lunarclient.apollo.module.glint.GlintModule; @@ -160,6 +162,7 @@ public static void init(ApolloMinestomProperties properties) { .addModule(BeamModule.class, new BeamModuleImpl()) .addModule(BorderModule.class, new BorderModuleImpl()) .addModule(ChatModule.class, new ChatModuleImpl()) + .addModule(CosmeticModule.class, new CosmeticModuleImpl()) .addModule(ColoredFireModule.class, new ColoredFireModuleImpl()) .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) diff --git a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java index 23a0919f..63255b72 100644 --- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java +++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java @@ -42,6 +42,8 @@ import com.lunarclient.apollo.module.combat.CombatModule; import com.lunarclient.apollo.module.cooldown.CooldownModule; import com.lunarclient.apollo.module.cooldown.CooldownModuleImpl; +import com.lunarclient.apollo.module.cosmetic.CosmeticModule; +import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; @@ -187,6 +189,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { .addModule(BeamModule.class, new BeamModuleImpl()) .addModule(BorderModule.class, new BorderModuleImpl()) .addModule(ChatModule.class, new ChatModuleImpl()) + .addModule(CosmeticModule.class, new CosmeticModuleImpl()) .addModule(ColoredFireModule.class, new ColoredFireModuleImpl()) .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8afeca4a..ff9bce4d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ listOfNotNull( "extra:adventure4", "api", "common", + "example:bukkit:nms", "example:bukkit:common", "example:bukkit:api", "example:bukkit:json", @@ -42,7 +43,9 @@ listOfNotNull( "example:minestom:api" ).forEach { include(it) - findProject(":$it")?.name = "apollo-${it.replace(':', '-')}" + findProject(":$it")?.let { proj -> + proj.name = "apollo-${it.replace(':', '-')}" + } } @@ -54,5 +57,7 @@ listOfNotNull( if (loadAllVersions) "platform:minestom" else null ).forEach { include(it) - findProject(":$it")?.name = "apollo-${it.split(':').last()}" + findProject(":$it")?.let { proj -> + proj.name = "apollo-${it.split(':').last()}" + } } From c0adc99ae7022f111a0f7380a81e9206cdb94081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 5 May 2026 05:12:38 +0200 Subject: [PATCH 6/7] Sync LunarClient Mods & Options (#278) * Sync LunarClient Mods & Options * Update version tags to 1.2.6 --------- Co-authored-by: LunarClient Bot --- .../apollo/mods/impl/ModArmorstatus.java | 2 +- .../apollo/mods/impl/ModParticleChanger.java | 125 ++++++++++-------- docs/developers/mods/armorstatus.mdx | 2 +- docs/developers/mods/particlechanger.mdx | 66 +++------ 4 files changed, 90 insertions(+), 105 deletions(-) diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModArmorstatus.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModArmorstatus.java index cf54d8b4..eaea4670 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModArmorstatus.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModArmorstatus.java @@ -76,7 +76,7 @@ public final class ModArmorstatus { */ public static final SimpleOption HIDE_UNBREAKABLE_DURABILITY = SimpleOption.builder() .node("armorstatus", "hide-unbreakable-durability").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + .defaultValue(true) .notifyClient() .build(); diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModParticleChanger.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModParticleChanger.java index dc998ce9..06cd6a0d 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModParticleChanger.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModParticleChanger.java @@ -49,56 +49,70 @@ public final class ModParticleChanger { /** * No documentation available. * - * @since 1.2.2 + * @since 1.0.0 */ - public static final SimpleOption SHOW_BLOOD_PARTICLES = SimpleOption.builder() - .node("particle-changer", "show-blood-particles").type(TypeToken.get(Boolean.class)) + public static final SimpleOption ALWAYS_ENCHANT_STRIKES = SimpleOption.builder() + .node("particle-changer", "always-enchant-strikes").type(TypeToken.get(Boolean.class)) .defaultValue(false) .notifyClient() .build(); /** - * No documentation available. + * Hide particles which are emitted in first person (e.g. potion effects). * - * @since 1.2.2 + * @since 1.0.0 */ - public static final NumberOption BLOOD_MULTIPLIER = NumberOption.number() - .node("particle-changer", "blood-multiplier").type(TypeToken.get(Integer.class)) - .min(1).max(10) - .defaultValue(1) + public static final SimpleOption HIDE_FIRST_PERSON_PARTICLES = SimpleOption.builder() + .comment("Hide particles which are emitted in first person (e.g. potion effects)") + .node("particle-changer", "hide-first-person-particles").type(TypeToken.get(Boolean.class)) + .defaultValue(false) .notifyClient() .build(); /** * No documentation available. * + * @since 1.2.6 + */ + public static final SimpleOption HIDE_BLOCK_BREAK_PARTICLES = SimpleOption.builder() + .node("particle-changer", "hide-block-break-particles").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * Simple toggle for clean game rendering. This may cause issues with certain server mechanics being more challenging to see!. + * * @since 1.2.2 */ - public static final SimpleOption PLAYER_BLOOD_PARTICLES = SimpleOption.builder() - .node("particle-changer", "player-blood-particles").type(TypeToken.get(Boolean.class)) - .defaultValue(true) + public static final SimpleOption HIDE_ALL_PARTICLES = SimpleOption.builder() + .comment("Simple toggle for clean game rendering. This may cause issues with certain server mechanics being more challenging to see!") + .node("particle-changer", "hide-all-particles").type(TypeToken.get(Boolean.class)) + .defaultValue(false) .notifyClient() .build(); /** * No documentation available. * - * @since 1.2.2 + * @since 1.0.0 */ - public static final SimpleOption ENTITY_BLOOD_PARTICLES = SimpleOption.builder() - .node("particle-changer", "entity-blood-particles").type(TypeToken.get(Boolean.class)) - .defaultValue(true) + public static final NumberOption SCALE = NumberOption.number() + .node("particle-changer", "scale").type(TypeToken.get(Float.class)) + .min(0.25F).max(2.0F) + .defaultValue(1.0F) .notifyClient() .build(); /** * No documentation available. * - * @since 1.2.2 + * @since 1.0.0 */ - public static final SimpleOption PLAY_BLOOD_SOUND = SimpleOption.builder() - .node("particle-changer", "play-blood-sound").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + public static final NumberOption PARTICLE_MULTIPLIER = NumberOption.number() + .node("particle-changer", "particle-multiplier").type(TypeToken.get(Float.class)) + .min(0.25F).max(10.0F) + .defaultValue(1.0F) .notifyClient() .build(); @@ -107,8 +121,8 @@ public final class ModParticleChanger { * * @since 1.0.0 */ - public static final SimpleOption ALWAYS_ENCHANT_STRIKES = SimpleOption.builder() - .node("particle-changer", "always-enchant-strikes").type(TypeToken.get(Boolean.class)) + public static final SimpleOption HIDE_PARTICLE = SimpleOption.builder() + .node("particle-changer", "hide-particle").type(TypeToken.get(Boolean.class)) .defaultValue(false) .notifyClient() .build(); @@ -118,32 +132,31 @@ public final class ModParticleChanger { * * @since 1.0.0 */ - public static final SimpleOption AFFECT_ENCHANTED_WEAPONS = SimpleOption.builder() - .node("particle-changer", "affect-enchanted-weapons").type(TypeToken.get(Boolean.class)) - .defaultValue(true) + public static final SimpleOption OVERLAY_COLOR = SimpleOption.builder() + .node("particle-changer", "overlay-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) .notifyClient() .build(); /** - * Hide your players potion effect particles when you're in first person. + * No documentation available. * * @since 1.0.0 */ - public static final SimpleOption HIDE_FIRST_PERSON_PARTICLES = SimpleOption.builder() - .comment("Hide your players potion effect particles when you're in first person") - .node("particle-changer", "hide-first-person-particles").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + public static final SimpleOption COLOR = SimpleOption.builder() + .node("particle-changer", "color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 255, 255)) .notifyClient() .build(); /** - * Simple toggle for clean game rendering. This may cause issues with certain server mechanics being more challenging to see!. + * No documentation available. * * @since 1.2.2 */ - public static final SimpleOption HIDE_ALL_PARTICLES = SimpleOption.builder() - .comment("Simple toggle for clean game rendering. This may cause issues with certain server mechanics being more challenging to see!") - .node("particle-changer", "hide-all-particles").type(TypeToken.get(Boolean.class)) + @Deprecated + public static final SimpleOption SHOW_BLOOD_PARTICLES = SimpleOption.builder() + .node("particle-changer", "show-blood-particles").type(TypeToken.get(Boolean.class)) .defaultValue(false) .notifyClient() .build(); @@ -151,45 +164,48 @@ public final class ModParticleChanger { /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.2 */ - public static final SimpleOption COLOR = SimpleOption.builder() - .node("particle-changer", "color").type(TypeToken.get(Color.class)) - .defaultValue(new Color(255, 255, 255)) + @Deprecated + public static final NumberOption BLOOD_MULTIPLIER = NumberOption.number() + .node("particle-changer", "blood-multiplier").type(TypeToken.get(Integer.class)) + .min(1).max(10) + .defaultValue(1) .notifyClient() .build(); /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.2 */ - public static final NumberOption SCALE = NumberOption.number() - .node("particle-changer", "scale").type(TypeToken.get(Float.class)) - .min(0.5F).max(2.0F) - .defaultValue(1.0F) + @Deprecated + public static final SimpleOption PLAYER_BLOOD_PARTICLES = SimpleOption.builder() + .node("particle-changer", "player-blood-particles").type(TypeToken.get(Boolean.class)) + .defaultValue(true) .notifyClient() .build(); /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.2 */ - public static final NumberOption PARTICLE_MULTIPLIER = NumberOption.number() - .node("particle-changer", "particle-multiplier").type(TypeToken.get(Integer.class)) - .min(1).max(10) - .defaultValue(1) + @Deprecated + public static final SimpleOption ENTITY_BLOOD_PARTICLES = SimpleOption.builder() + .node("particle-changer", "entity-blood-particles").type(TypeToken.get(Boolean.class)) + .defaultValue(true) .notifyClient() .build(); /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.2 */ - public static final SimpleOption HIDE_PARTICLE = SimpleOption.builder() - .node("particle-changer", "hide-particle").type(TypeToken.get(Boolean.class)) + @Deprecated + public static final SimpleOption PLAY_BLOOD_SOUND = SimpleOption.builder() + .node("particle-changer", "play-blood-sound").type(TypeToken.get(Boolean.class)) .defaultValue(false) .notifyClient() .build(); @@ -199,9 +215,10 @@ public final class ModParticleChanger { * * @since 1.0.0 */ - public static final SimpleOption OVERLAY_COLOR = SimpleOption.builder() - .node("particle-changer", "overlay-color").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + @Deprecated + public static final SimpleOption AFFECT_ENCHANTED_WEAPONS = SimpleOption.builder() + .node("particle-changer", "affect-enchanted-weapons").type(TypeToken.get(Boolean.class)) + .defaultValue(true) .notifyClient() .build(); diff --git a/docs/developers/mods/armorstatus.mdx b/docs/developers/mods/armorstatus.mdx index 1c2a8556..8f186505 100644 --- a/docs/developers/mods/armorstatus.mdx +++ b/docs/developers/mods/armorstatus.mdx @@ -39,7 +39,7 @@ public void toggleArmorStatusExample(Player viewer, boolean value) { - Config Key: `hide-unbreakable-durability` - Values - Type: `Boolean` - - Default: `false` + - Default: `true` - __`ITEM_NAME`__ - Config Key: `item-name` diff --git a/docs/developers/mods/particlechanger.mdx b/docs/developers/mods/particlechanger.mdx index 81a23392..f403aef9 100644 --- a/docs/developers/mods/particlechanger.mdx +++ b/docs/developers/mods/particlechanger.mdx @@ -21,53 +21,21 @@ public void toggleParticleChangerExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `false` -- __`SHOW_BLOOD_PARTICLES`__ - - Config Key: `show-blood-particles` - - Values - - Type: `Boolean` - - Default: `false` - -- __`BLOOD_MULTIPLIER`__ - - Config Key: `blood-multiplier` - - Values - - Type: `Integer` - - Default: `1` - - Minimum: `1` - - Maximum: `10` - -- __`PLAYER_BLOOD_PARTICLES`__ - - Config Key: `player-blood-particles` - - Values - - Type: `Boolean` - - Default: `true` - -- __`ENTITY_BLOOD_PARTICLES`__ - - Config Key: `entity-blood-particles` - - Values - - Type: `Boolean` - - Default: `true` - -- __`PLAY_BLOOD_SOUND`__ - - Config Key: `play-blood-sound` - - Values - - Type: `Boolean` - - Default: `false` - - __`ALWAYS_ENCHANT_STRIKES`__ - Config Key: `always-enchant-strikes` - Values - Type: `Boolean` - Default: `false` -- __`AFFECT_ENCHANTED_WEAPONS`__ - - Config Key: `affect-enchanted-weapons` +- __`HIDE_FIRST_PERSON_PARTICLES`__ + - Hide particles which are emitted in first person (e.g. potion effects) + - Config Key: `hide-first-person-particles` - Values - Type: `Boolean` - - Default: `true` + - Default: `false` -- __`HIDE_FIRST_PERSON_PARTICLES`__ - - Hide your players potion effect particles when you're in first person - - Config Key: `hide-first-person-particles` +- __`HIDE_BLOCK_BREAK_PARTICLES`__ + - Config Key: `hide-block-break-particles` - Values - Type: `Boolean` - Default: `false` @@ -79,27 +47,21 @@ public void toggleParticleChangerExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `false` -- __`COLOR`__ - - Config Key: `color` - - Values - - Type: `String` - - Default: `#FFFFFFFF` - - __`SCALE`__ - Config Key: `scale` - Values - Type: `Float` - Default: `1.0F` - - Minimum: `0.5F` + - Minimum: `0.25F` - Maximum: `2.0F` - __`PARTICLE_MULTIPLIER`__ - Config Key: `particle-multiplier` - Values - - Type: `Integer` - - Default: `1` - - Minimum: `1` - - Maximum: `10` + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.25F` + - Maximum: `10.0F` - __`HIDE_PARTICLE`__ - Config Key: `hide-particle` @@ -113,3 +75,9 @@ public void toggleParticleChangerExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `false` +- __`COLOR`__ + - Config Key: `color` + - Values + - Type: `String` + - Default: `#FFFFFFFF` + From 80d8d59809aae3973fb3c7c0cb7c6cd6349a23ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 5 May 2026 05:19:48 +0200 Subject: [PATCH 7/7] Bump to 1.2.6 (#279) --- docs/developers/minestom.mdx | 6 +++--- example/bukkit/api/src/main/resources/plugin.yml | 2 +- example/bukkit/json/src/main/resources/plugin.yml | 2 +- example/bukkit/proto/src/main/resources/plugin.yml | 2 +- gradle.properties | 2 +- platform/bukkit/src/platform-loader/resources/plugin.yml | 2 +- platform/bungee/src/platform-loader/resources/plugin.yml | 2 +- platform/folia/src/main/resources/plugin.yml | 2 +- .../java/com/lunarclient/apollo/ApolloMinestomPlatform.java | 2 +- .../java/com/lunarclient/apollo/ApolloVelocityPlatform.java | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/developers/minestom.mdx b/docs/developers/minestom.mdx index 5bbccfea..c1b779d6 100644 --- a/docs/developers/minestom.mdx +++ b/docs/developers/minestom.mdx @@ -52,14 +52,14 @@ Next, add the `apollo-minestom` dependency to your project. ```kotlin filename="build.gradle.kts" dependencies { - implementation("com.lunarclient:apollo-minestom:1.2.5") + implementation("com.lunarclient:apollo-minestom:1.2.6") } ``` ```groovy filename="build.gradle" dependencies { - implementation 'com.lunarclient:apollo-minestom:1.2.5' + implementation 'com.lunarclient:apollo-minestom:1.2.6' } ``` @@ -69,7 +69,7 @@ Next, add the `apollo-minestom` dependency to your project. com.lunarclient apollo-minestom - 1.2.5 + 1.2.6 compile diff --git a/example/bukkit/api/src/main/resources/plugin.yml b/example/bukkit/api/src/main/resources/plugin.yml index afd8f1d0..2549361d 100644 --- a/example/bukkit/api/src/main/resources/plugin.yml +++ b/example/bukkit/api/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-API-Example main: com.lunarclient.apollo.example.api.ApolloApiExamplePlatform -version: 1.2.5 +version: 1.2.6 author: Moonsworth softdepend: [ Apollo-Bukkit, Apollo-Folia ] api-version: 1.13 diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml index d289fbdd..027ac7db 100644 --- a/example/bukkit/json/src/main/resources/plugin.yml +++ b/example/bukkit/json/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Json-Example main: com.lunarclient.apollo.example.json.ApolloJsonExamplePlatform -version: 1.2.5 +version: 1.2.6 author: Moonsworth softdepend: [ Apollo-Bukkit ] api-version: 1.13 diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml index c24755f6..442e5445 100644 --- a/example/bukkit/proto/src/main/resources/plugin.yml +++ b/example/bukkit/proto/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Proto-Example main: com.lunarclient.apollo.example.proto.ApolloProtoExamplePlatform -version: 1.2.5 +version: 1.2.6 author: Moonsworth softdepend: [ Apollo-Bukkit ] api-version: 1.13 diff --git a/gradle.properties b/gradle.properties index 39390837..54bb2c7c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.lunarclient -version=1.2.6-SNAPSHOT +version=1.2.6 description=The API for interacting with Lunar Client players. org.gradle.parallel=true diff --git a/platform/bukkit/src/platform-loader/resources/plugin.yml b/platform/bukkit/src/platform-loader/resources/plugin.yml index 590a4f9c..42078f15 100644 --- a/platform/bukkit/src/platform-loader/resources/plugin.yml +++ b/platform/bukkit/src/platform-loader/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Bukkit main: com.lunarclient.apollo.loader.BukkitPlatformLoader -version: 1.2.5 +version: 1.2.6 author: Moonsworth api-version: 1.13 soft-depend: [LunarClient-API] diff --git a/platform/bungee/src/platform-loader/resources/plugin.yml b/platform/bungee/src/platform-loader/resources/plugin.yml index 1fdab7a2..9778623f 100644 --- a/platform/bungee/src/platform-loader/resources/plugin.yml +++ b/platform/bungee/src/platform-loader/resources/plugin.yml @@ -1,4 +1,4 @@ name: Apollo-Bungee main: com.lunarclient.apollo.loader.BungeePlatformLoader -version: 1.2.5 +version: 1.2.6 author: Moonsworth diff --git a/platform/folia/src/main/resources/plugin.yml b/platform/folia/src/main/resources/plugin.yml index cfe42463..af84c17c 100644 --- a/platform/folia/src/main/resources/plugin.yml +++ b/platform/folia/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Folia main: com.lunarclient.apollo.ApolloFoliaPlatform -version: 1.2.5 +version: 1.2.6 author: Moonsworth api-version: 1.13 folia-supported: true diff --git a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java index 330f4545..fb47f48c 100644 --- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java +++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java @@ -243,7 +243,7 @@ public Options getOptions() { @Override public String getApolloVersion() { - return "1.2.5"; + return "1.2.6"; } @Override diff --git a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java index 63255b72..2825214a 100644 --- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java +++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java @@ -109,7 +109,7 @@ @Plugin( id = "apollo", name = "Apollo-Velocity", - version = "1.2.5", + version = "1.2.6", url = "https://moonsworth.com", description = "Implementation of Apollo for Velocity", authors = {"Moonsworth"}