Skip to content

Commit b3b4718

Browse files
authored
Merge pull request #710 from webhead1104/npc
New NPC system
2 parents 8db3979 + 8dfbce4 commit b3b4718

26 files changed

Lines changed: 863 additions & 734 deletions

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ jobs:
1515
- uses: actions/checkout@v6.0.1
1616

1717
- name: Get version
18+
id: version
1819
run: |
1920
VERSION=$(grep '^version=' gradle.properties | cut -d'=' -f2)
2021
RELEASE_VERSION=${VERSION%-SNAPSHOT}
2122
echo "version=$RELEASE_VERSION" >> $GITHUB_OUTPUT
2223
2324
- name: Check if tag exists
25+
id: tag_check
2426
run: |
2527
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
2628
echo "exists=true" >> $GITHUB_OUTPUT

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=0.3.1-SNAPSHOT
1+
version=0.3.2-SNAPSHOT
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package net.cytonic.cytosis.entity.hologram;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.UUID;
6+
import java.util.function.Function;
7+
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import net.kyori.adventure.text.Component;
11+
import net.minestom.server.coordinate.Pos;
12+
import net.minestom.server.coordinate.Vec;
13+
import net.minestom.server.entity.Entity;
14+
import net.minestom.server.entity.EntityType;
15+
import net.minestom.server.entity.MetadataDef;
16+
import net.minestom.server.instance.Instance;
17+
import net.minestom.server.network.packet.server.play.DestroyEntitiesPacket;
18+
import net.minestom.server.network.packet.server.play.EntityTeleportPacket;
19+
import net.minestom.server.network.packet.server.play.SpawnEntityPacket;
20+
21+
import net.cytonic.cytosis.player.CytosisPlayer;
22+
import net.cytonic.cytosis.utils.MetadataPacketBuilder;
23+
24+
public class PlayerHolograms {
25+
26+
public static final double SPACE = 0.28;
27+
28+
public static final List<Hologram> holograms = new ArrayList<>();
29+
30+
public static void addHologram(Hologram hologram) {
31+
List<Integer> entities = new ArrayList<>();
32+
for (int i = 0; i < hologram.lines.size(); i++) {
33+
double y = (hologram.lines.size() - 1 - i) * SPACE;
34+
int entityId = Entity.generateId();
35+
entities.add(entityId);
36+
37+
hologram.player.sendPackets(
38+
new SpawnEntityPacket(entityId, UUID.randomUUID(), EntityType.TEXT_DISPLAY, hologram.pos.add(0, y, 0),
39+
0, 0, Vec.ZERO),
40+
MetadataPacketBuilder.empty(entityId)
41+
.setTextComponent(MetadataDef.TextDisplay.TEXT.index(), hologram.lines.get(i))
42+
.setByte(MetadataDef.TextDisplay.BILLBOARD_CONSTRAINTS.index(), (byte) 3) // CENTER
43+
.setBoolean(MetadataDef.TextDisplay.HAS_NO_GRAVITY.index(), true)
44+
.setVarInt(MetadataDef.TextDisplay.LINE_WIDTH.index(), 1000)
45+
.build()
46+
);
47+
}
48+
hologram.entityIds.addAll(entities);
49+
holograms.add(hologram);
50+
}
51+
52+
public static void updateHologram(Hologram hologram, Pos pos) {
53+
List<Integer> entities = hologram.entityIds;
54+
if (entities != null) {
55+
for (int i = 0; i < hologram.lines.size(); i++) {
56+
double y = (hologram.lines.size() - 1 - i) * SPACE;
57+
int entityId = entities.get(i);
58+
hologram.player.sendPacket(new EntityTeleportPacket(entityId, pos.add(0, y, 0), Vec.ZERO, 0, false));
59+
}
60+
}
61+
}
62+
63+
public static void removeHologram(Hologram hologram) {
64+
holograms.remove(hologram);
65+
List<Integer> entities = hologram.entityIds;
66+
if (entities != null) {
67+
hologram.player.sendPacket(new DestroyEntitiesPacket(entities));
68+
}
69+
}
70+
71+
public static void removePlayer(CytosisPlayer player) {
72+
holograms.removeIf(hologram -> {
73+
if (hologram.getPlayer().equals(player)) {
74+
player.sendPacket(new DestroyEntitiesPacket(hologram.entityIds));
75+
return true;
76+
}
77+
return false;
78+
});
79+
}
80+
81+
@Builder
82+
@Getter
83+
public static class Hologram {
84+
85+
private final CytosisPlayer player;
86+
private final Pos pos;
87+
private final List<Component> lines;
88+
private final Instance instance;
89+
private final Function<CytosisPlayer, List<Component>> displayFunction;
90+
@Builder.Default
91+
private final double spacing = 0.3;
92+
@Builder.Default
93+
private final List<Integer> entityIds = new ArrayList<>();
94+
}
95+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package net.cytonic.cytosis.entity.npc;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.UUID;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
9+
import lombok.Getter;
10+
import net.kyori.adventure.text.Component;
11+
import net.kyori.adventure.text.format.NamedTextColor;
12+
import net.minestom.server.MinecraftServer;
13+
import net.minestom.server.coordinate.Pos;
14+
import net.minestom.server.coordinate.Vec;
15+
import net.minestom.server.entity.EntityType;
16+
import net.minestom.server.entity.GameMode;
17+
import net.minestom.server.entity.MetadataDef;
18+
import net.minestom.server.entity.ai.EntityAIGroup;
19+
import net.minestom.server.network.packet.server.play.DestroyEntitiesPacket;
20+
import net.minestom.server.network.packet.server.play.EntityHeadLookPacket;
21+
import net.minestom.server.network.packet.server.play.EntityRotationPacket;
22+
import net.minestom.server.network.packet.server.play.EntityTeleportPacket;
23+
import net.minestom.server.network.packet.server.play.PlayerInfoRemovePacket;
24+
import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket;
25+
import net.minestom.server.network.packet.server.play.SpawnEntityPacket;
26+
import net.minestom.server.network.packet.server.play.TeamsPacket;
27+
import net.minestom.server.timer.TaskSchedule;
28+
import org.jetbrains.annotations.ApiStatus.OverrideOnly;
29+
30+
import net.cytonic.cytosis.Cytosis;
31+
import net.cytonic.cytosis.entity.hologram.PlayerHolograms;
32+
import net.cytonic.cytosis.entity.npc.configuration.NPCConfiguration;
33+
import net.cytonic.cytosis.entity.npc.dialogs.Dialog;
34+
import net.cytonic.cytosis.entity.npc.impl.NPCEntityImpl;
35+
import net.cytonic.cytosis.entity.npc.pathfinding.NavigatePathGoal;
36+
import net.cytonic.cytosis.entity.npc.pathfinding.Path;
37+
import net.cytonic.cytosis.events.npcs.NPCInteractEvent;
38+
import net.cytonic.cytosis.managers.NpcManager;
39+
import net.cytonic.cytosis.player.CytosisPlayer;
40+
import net.cytonic.cytosis.utils.MetadataPacketBuilder;
41+
42+
public abstract class NPC {
43+
44+
public static final int SPAWN_DISTANCE = 48;
45+
public static final int SPAWN_DISTANCE_SQUARED = SPAWN_DISTANCE * SPAWN_DISTANCE;
46+
public static final int LOOK_DISTANCE = 5;
47+
public static final int LOOK_DISTANCE_SQUARED = LOOK_DISTANCE * LOOK_DISTANCE;
48+
49+
@Getter
50+
private final UUID uuid = UUID.randomUUID();
51+
@Getter
52+
private final NPCConfiguration config;
53+
@Getter
54+
private final Component name;
55+
56+
private final Map<UUID, ViewerData> viewers = new ConcurrentHashMap<>();
57+
private final Map<UUID, Dialog<? extends CytosisPlayer>> activeDialogs = new ConcurrentHashMap<>();
58+
59+
public NPC(NPCConfiguration config) {
60+
this.config = config;
61+
String className = getClass().getSimpleName().replaceAll("NPC", "");
62+
this.name =
63+
config.chatName() != null ? config.chatName()
64+
: Component.text(className.replaceAll("(?<=.)(?=\\p{Lu})", " "));
65+
}
66+
67+
@OverrideOnly
68+
public void onClick(NPCInteractEvent event) {
69+
}
70+
71+
public boolean hasActiveDialog(CytosisPlayer player) {
72+
return activeDialogs.containsKey(player.getUuid());
73+
}
74+
75+
protected <P extends CytosisPlayer> Dialog<P> startDialog(P player) {
76+
if (activeDialogs.containsKey(player.getUuid())) {
77+
//noinspection unchecked
78+
return (Dialog<P>) activeDialogs.get(player.getUuid());
79+
}
80+
Dialog<P> dialog = new Dialog<>(this);
81+
activeDialogs.put(player.getUuid(), dialog);
82+
return dialog;
83+
}
84+
85+
public void register() {
86+
Cytosis.get(NpcManager.class).register(this);
87+
}
88+
89+
public void remove() {
90+
viewers.forEach((uuid, data) ->
91+
Cytosis.getPlayer(uuid).ifPresent(player -> {
92+
player.sendPacket(new DestroyEntitiesPacket(List.of(data.entity().getEntityId())));
93+
PlayerHolograms.removeHologram(data.hologram());
94+
}));
95+
viewers.clear();
96+
Cytosis.get(NpcManager.class).removeNPC(this);
97+
}
98+
99+
public void updateForPlayer(CytosisPlayer player) {
100+
Pos npcPos = config.position(player);
101+
double distanceSquared = player.getPosition().distanceSquared(npcPos);
102+
boolean inRange = distanceSquared <= SPAWN_DISTANCE_SQUARED;
103+
104+
ViewerData data = viewers.get(player.getUuid());
105+
106+
if (data == null) {
107+
if (!inRange) return;
108+
109+
// has not been loaded by player before
110+
List<Component> holograms = config.holograms(player);
111+
String username = "";
112+
113+
NPCEntityImpl entity = new NPCEntityImpl(
114+
username,
115+
config.texture(player),
116+
config.signature(player),
117+
holograms
118+
);
119+
entity.setInstance(config.instance(), npcPos);
120+
Path path = config.path(player);
121+
if (path != null) {
122+
EntityAIGroup group = new EntityAIGroup();
123+
group.getGoalSelectors().add(new NavigatePathGoal(entity, path));
124+
entity.addAIGroup(group);
125+
}
126+
127+
sendPackets(player, entity);
128+
129+
MinecraftServer.getSchedulerManager()
130+
.scheduleTask(() -> player.sendPacket(new PlayerInfoRemovePacket(entity.getUuid())),
131+
TaskSchedule.tick(2), TaskSchedule.stop());
132+
133+
PlayerHolograms.Hologram hologram = PlayerHolograms.Hologram.builder()
134+
.pos(config.position(player).add(0, 2.0, 0))
135+
.lines(holograms)
136+
.player(player)
137+
.instance(config.instance())
138+
.build();
139+
140+
PlayerHolograms.addHologram(hologram);
141+
viewers.put(player.getUuid(), new ViewerData(entity, hologram));
142+
return;
143+
}
144+
145+
// player has seen it before
146+
NPCEntityImpl entity = data.entity();
147+
PlayerHolograms.Hologram hologram = data.hologram();
148+
149+
if (!inRange) {
150+
player.sendPacket(new DestroyEntitiesPacket(List.of(entity.getEntityId())));
151+
PlayerHolograms.removeHologram(hologram);
152+
viewers.remove(player.getUuid());
153+
return;
154+
}
155+
156+
player.sendPacket(new EntityTeleportPacket(entity.getEntityId(), entity.getPosition(), Vec.ZERO, 0, false));
157+
PlayerHolograms.updateHologram(hologram, entity.getPosition().add(0, 2.0, 0));
158+
159+
if (config.looking(player) && distanceSquared <= LOOK_DISTANCE_SQUARED) {
160+
Pos lookPos = entity.getPosition().withLookAt(player.getPosition().add(0, player.getEyeHeight(), 0));
161+
player.sendPackets(
162+
new EntityRotationPacket(entity.getEntityId(), lookPos.yaw(), lookPos.pitch(), player.isOnGround()),
163+
new EntityHeadLookPacket(entity.getEntityId(), lookPos.yaw()));
164+
} else {
165+
player.sendPackets(
166+
new EntityRotationPacket(entity.getEntityId(), entity.getPosition().yaw(),
167+
entity.getPosition().pitch(), player.isOnGround()),
168+
new EntityHeadLookPacket(entity.getEntityId(), entity.getPosition().yaw()));
169+
}
170+
}
171+
172+
public void removePlayer(CytosisPlayer player) {
173+
activeDialogs.remove(player.getUuid());
174+
ViewerData data = viewers.remove(player.getUuid());
175+
if (data != null) {
176+
player.sendPacket(new DestroyEntitiesPacket(List.of(data.entity().getEntityId())));
177+
PlayerHolograms.removeHologram(data.hologram());
178+
}
179+
}
180+
181+
public void removeDialog(CytosisPlayer player) {
182+
activeDialogs.remove(player.getUuid());
183+
}
184+
185+
public boolean hasEntityId(CytosisPlayer player, int entityId) {
186+
ViewerData data = viewers.get(player.getUuid());
187+
return data != null && data.entity().getEntityId() == entityId;
188+
}
189+
190+
private void sendPackets(CytosisPlayer player, NPCEntityImpl entity) {
191+
List<PlayerInfoUpdatePacket.Property> properties = new ArrayList<>();
192+
if (entity.getSkinTexture() != null && entity.getSkinSignature() != null) {
193+
properties.add(new PlayerInfoUpdatePacket.Property("textures", entity.getSkinTexture(),
194+
entity.getSkinSignature()));
195+
}
196+
Pos npcPos = entity.getPosition();
197+
player.sendPackets(
198+
new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.ADD_PLAYER,
199+
new PlayerInfoUpdatePacket.Entry(
200+
entity.getUuid(),
201+
entity.getUsername(),
202+
properties,
203+
false,
204+
0,
205+
GameMode.ADVENTURE,
206+
Component.empty(),
207+
null,
208+
0,
209+
false)),
210+
new SpawnEntityPacket(entity.getEntityId(), entity.getUuid(), EntityType.PLAYER,
211+
npcPos,
212+
npcPos.yaw(),
213+
0,
214+
Vec.ZERO),
215+
new EntityHeadLookPacket(entity.getEntityId(), npcPos.yaw()),
216+
MetadataPacketBuilder.empty(entity.getEntityId())
217+
.setByte(MetadataDef.Avatar.DISPLAYED_MODEL_PARTS_FLAGS.index(), (byte) 127)
218+
.setBoolean(MetadataDef.Player.HAS_NO_GRAVITY.index(), true)
219+
.build(),
220+
new TeamsPacket("npc_team", new TeamsPacket.CreateTeamAction(
221+
Component.text("NPCs"),
222+
(byte) 0,
223+
TeamsPacket.NameTagVisibility.NEVER,
224+
TeamsPacket.CollisionRule.NEVER,
225+
NamedTextColor.WHITE,
226+
Component.empty(),
227+
Component.empty(),
228+
List.of(entity.getUsername())
229+
))
230+
);
231+
}
232+
233+
private record ViewerData(NPCEntityImpl entity, PlayerHolograms.Hologram hologram) {
234+
235+
}
236+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package net.cytonic.cytosis.entity.npc.configuration;
2+
3+
import java.util.List;
4+
5+
import net.kyori.adventure.text.Component;
6+
import net.minestom.server.coordinate.Pos;
7+
import net.minestom.server.instance.Instance;
8+
import net.minestom.server.instance.InstanceContainer;
9+
import org.jetbrains.annotations.Nullable;
10+
11+
import net.cytonic.cytosis.Cytosis;
12+
import net.cytonic.cytosis.entity.npc.pathfinding.Path;
13+
import net.cytonic.cytosis.player.CytosisPlayer;
14+
15+
public interface NPCConfiguration {
16+
17+
List<Component> holograms(CytosisPlayer player);
18+
19+
Pos position(CytosisPlayer player);
20+
21+
@Nullable
22+
default Path path(CytosisPlayer player) {
23+
return null;
24+
}
25+
26+
default boolean looking(CytosisPlayer player) {
27+
return false;
28+
}
29+
30+
@Nullable
31+
default Component chatName() {
32+
return null;
33+
}
34+
35+
default Instance instance() {
36+
return Cytosis.get(InstanceContainer.class);
37+
}
38+
39+
@Nullable
40+
String texture(CytosisPlayer player);
41+
42+
@Nullable
43+
String signature(CytosisPlayer player);
44+
}

0 commit comments

Comments
 (0)