Commit 897f082b authored by Melledy's avatar Melledy
Browse files

Implement AbilityManager

parent 176f3e91
......@@ -9,6 +9,8 @@ import java.util.Map;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.Utils;
import emu.grasscutter.data.custom.AbilityEmbryoEntry;
import emu.grasscutter.data.custom.AbilityModifier;
import emu.grasscutter.data.custom.AbilityModifierEntry;
import emu.grasscutter.data.custom.OpenConfigEntry;
import emu.grasscutter.data.custom.ScenePointEntry;
import emu.grasscutter.data.def.*;
......@@ -22,6 +24,7 @@ public class GameData {
// BinOutputs
private static final Int2ObjectMap<String> abilityHashes = new Int2ObjectOpenHashMap<>();
private static final Map<String, AbilityEmbryoEntry> abilityEmbryos = new HashMap<>();
private static final Map<String, AbilityModifierEntry> abilityModifiers = new HashMap<>();
private static final Map<String, OpenConfigEntry> openConfigEntries = new HashMap<>();
private static final Map<String, ScenePointEntry> scenePointEntries = new HashMap<>();
......@@ -101,6 +104,10 @@ public class GameData {
return abilityEmbryos;
}
public static Map<String, AbilityModifierEntry> getAbilityModifiers() {
return abilityModifiers;
}
public static Map<String, OpenConfigEntry> getOpenConfigEntries() {
return openConfigEntries;
}
......
......@@ -18,6 +18,11 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.common.PointData;
import emu.grasscutter.data.common.ScenePointConfig;
import emu.grasscutter.data.custom.AbilityEmbryoEntry;
import emu.grasscutter.data.custom.AbilityModifier;
import emu.grasscutter.data.custom.AbilityModifier.AbilityConfigData;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType;
import emu.grasscutter.data.custom.AbilityModifierEntry;
import emu.grasscutter.data.custom.OpenConfigEntry;
import emu.grasscutter.data.custom.ScenePointEntry;
import emu.grasscutter.game.world.SpawnDataEntry;
......@@ -47,6 +52,7 @@ public class ResourceLoader {
// Load ability lists
loadAbilityEmbryos();
loadOpenConfig();
loadAbilityModifiers();
// Load resources
loadResources();
// Process into depots
......@@ -244,6 +250,69 @@ public class ResourceLoader {
}
}
private static void loadAbilityModifiers() {
// Load from BinOutput
File folder = new File(Utils.toFilePath(Grasscutter.getConfig().RESOURCE_FOLDER + "BinOutput/Ability/Temp/AvatarAbilities/"));
File[] files = folder.listFiles();
if (files == null) {
Grasscutter.getLogger().error("Error loading ability modifiers: no files found in " + folder.getAbsolutePath());
return;
}
for (File file : files) {
List<AbilityConfigData> abilityConfigList = null;
try (FileReader fileReader = new FileReader(file)) {
abilityConfigList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, AbilityConfigData.class).getType());
} catch (Exception e) {
e.printStackTrace();
continue;
}
for (AbilityConfigData data : abilityConfigList) {
if (data.Default.modifiers == null || data.Default.modifiers.size() == 0) {
continue;
}
AbilityModifierEntry modifierEntry = new AbilityModifierEntry(data.Default.abilityName);
for (Entry<String, AbilityModifier> entry : data.Default.modifiers.entrySet()) {
AbilityModifier modifier = entry.getValue();
// Stare.
if (modifier.onAdded != null) {
for (AbilityModifierAction action : modifier.onAdded) {
if (action.$type.contains("HealHP")) {
action.type = AbilityModifierActionType.HealHP;
modifierEntry.getOnAdded().add(action);
}
}
}
if (modifier.onThinkInterval != null) {
for (AbilityModifierAction action : modifier.onThinkInterval) {
if (action.$type.contains("HealHP")) {
action.type = AbilityModifierActionType.HealHP;
modifierEntry.getOnThinkInterval().add(action);
}
}
}
if (modifier.onRemoved != null) {
for (AbilityModifierAction action : modifier.onRemoved) {
if (action.$type.contains("HealHP")) {
action.type = AbilityModifierActionType.HealHP;
modifierEntry.getOnRemoved().add(action);
}
}
}
}
GameData.getAbilityModifiers().put(modifierEntry.getName(), modifierEntry);
}
}
}
private static void loadSpawnData() {
// Read from cached file if exists
File spawnDataEntries = new File(Grasscutter.getConfig().DATA_FOLDER + "Spawns.json");
......
package emu.grasscutter.data.custom;
import java.util.Map;
public class AbilityModifier {
public AbilityModifierAction[] onAdded;
public AbilityModifierAction[] onThinkInterval;
public AbilityModifierAction[] onRemoved;
public static class AbilityConfigData {
public AbilityData Default;
}
public static class AbilityData {
public String abilityName;
public Map<String, AbilityModifier> modifiers;
}
public static class AbilityModifierAction {
public String $type;
public AbilityModifierActionType type;
public String target;
public AbilityModifierValue amount;
public AbilityModifierValue amountByTargetCurrentHPRatio;
}
public static class AbilityModifierValue {
public boolean isFormula;
public boolean isDynamic;
public String dynamicKey;
}
public enum AbilityModifierActionType {
HealHP, ApplyModifier, LoseHP;
}
}
package emu.grasscutter.data.custom;
import java.util.ArrayList;
import java.util.List;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction;
public class AbilityModifierEntry {
private String name; // Custom value
public List<AbilityModifierAction> onModifierAdded;
public List<AbilityModifierAction> onThinkInterval;
public List<AbilityModifierAction> onRemoved;
public AbilityModifierEntry(String name) {
this.name = name;
this.onModifierAdded = new ArrayList<>();
this.onThinkInterval = new ArrayList<>();
this.onRemoved = new ArrayList<>();
}
public String getName() {
return name;
}
public List<AbilityModifierAction> getOnAdded() {
return onModifierAdded;
}
public List<AbilityModifierAction> getOnThinkInterval() {
return onThinkInterval;
}
public List<AbilityModifierAction> getOnRemoved() {
return onRemoved;
}
}
package emu.grasscutter.game.ability;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.custom.AbilityModifier;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType;
import emu.grasscutter.data.custom.AbilityModifierEntry;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.AbilityInvokeArgumentOuterClass.AbilityInvokeArgument;
import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange;
import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap;
import emu.grasscutter.net.proto.AbilityScalarTypeOuterClass.AbilityScalarType;
import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry;
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import emu.grasscutter.utils.Utils;
public class AbilityManager {
private Player player;
public AbilityManager(Player player) {
this.player = player;
}
public Player getPlayer() {
return this.player;
}
public void onAbilityInvoke(AbilityInvokeEntry invoke) throws Exception {
//System.out.println(invoke.getArgumentType() + " (" + invoke.getArgumentTypeValue() + "): " + Utils.bytesToHex(invoke.toByteArray()));
switch (invoke.getArgumentType()) {
case ABILITY_META_OVERRIDE_PARAM:
handleOverrideParam(invoke);
break;
case ABILITY_META_REINIT_OVERRIDEMAP:
handleReinitOverrideMap(invoke);
break;
case ABILITY_META_MODIFIER_CHANGE:
handleModifierChange(invoke);
break;
case ABILITY_MIXIN_COST_STAMINA:
handleMixinCostStamina(invoke);
break;
case ABILITY_ACTION_GENERATE_ELEM_BALL:
handleGenerateElemBall(invoke);
break;
default:
break;
}
}
private void handleOverrideParam(AbilityInvokeEntry invoke) throws Exception {
GameEntity entity = player.getScene().getEntityById(invoke.getEntityId());
if (entity == null) {
return;
}
AbilityScalarValueEntry entry = AbilityScalarValueEntry.parseFrom(invoke.getAbilityData());
entity.getMetaOverrideMap().put(entry.getKey().getStr(), entry.getFloatValue());
}
private void handleReinitOverrideMap(AbilityInvokeEntry invoke) throws Exception {
GameEntity entity = player.getScene().getEntityById(invoke.getEntityId());
if (entity == null) {
return;
}
AbilityMetaReInitOverrideMap map = AbilityMetaReInitOverrideMap.parseFrom(invoke.getAbilityData());
for (AbilityScalarValueEntry entry : map.getOverrideMapList()) {
entity.getMetaOverrideMap().put(entry.getKey().getStr(), entry.getFloatValue());
}
}
private void handleModifierChange(AbilityInvokeEntry invoke) throws Exception {
GameEntity target = player.getScene().getEntityById(invoke.getEntityId());
if (target == null) {
return;
}
AbilityInvokeEntryHead head = invoke.getHead();
if (head == null) {
return;
}
AbilityMetaModifierChange data = AbilityMetaModifierChange.parseFrom(invoke.getAbilityData());
if (data == null) {
return;
}
GameEntity sourceEntity = player.getScene().getEntityById(data.getApplyEntityId());
if (sourceEntity == null) {
return;
}
// This is not how it works but we will keep it for now since healing abilities dont work properly anyways
if (data.getAction() == ModifierAction.ADDED && data.getParentAbilityName() != null) {
// Handle add modifier here
String modifierString = data.getParentAbilityName().getStr();
AbilityModifierEntry modifier = GameData.getAbilityModifiers().get(modifierString);
if (modifier != null && modifier.getOnAdded().size() > 0) {
for (AbilityModifierAction action : modifier.getOnAdded()) {
invokeAction(action, target, sourceEntity);
}
}
// Add to meta modifier list
target.getMetaModifiers().put(head.getInstancedModifierId(), modifierString);
} else if (data.getAction() == ModifierAction.REMOVED) {
String modifierString = target.getMetaModifiers().get(head.getInstancedModifierId());
if (modifierString != null) {
// Get modifier and call on remove event
AbilityModifierEntry modifier = GameData.getAbilityModifiers().get(modifierString);
if (modifier != null && modifier.getOnRemoved().size() > 0) {
for (AbilityModifierAction action : modifier.getOnRemoved()) {
invokeAction(action, target, sourceEntity);
}
}
// Remove from meta modifiers
target.getMetaModifiers().remove(head.getInstancedModifierId());
}
}
}
private void handleMixinCostStamina(AbilityInvokeEntry invoke) {
// Not the right way of doing this
if (Grasscutter.getConfig().OpenStamina) {
// getPlayer().getStaminaManager().updateStamina(getPlayer().getSession(), -450);
// TODO
// set flag in stamina/movement manager that specifies the player is currently using an alternate sprint
}
}
private void handleGenerateElemBall(AbilityInvokeEntry invoke) {
// TODO create elemental energy orbs
}
private void invokeAction(AbilityModifierAction action, GameEntity target, GameEntity sourceEntity) {
switch (action.type) {
case HealHP -> {
if (action.amount == null) {
return;
}
float healAmount = 0;
if (action.amount.isDynamic && action.amount.dynamicKey != null) {
healAmount = sourceEntity.getMetaOverrideMap().getOrDefault(action.amount.dynamicKey, 0f);
}
if (healAmount > 0) {
target.heal(healAmount);
}
}
case LoseHP -> {
if (action.amountByTargetCurrentHPRatio == null) {
return;
}
float damageAmount = 0;
if (action.amount.isDynamic && action.amount.dynamicKey != null) {
damageAmount = sourceEntity.getMetaOverrideMap().getOrDefault(action.amount.dynamicKey, 0f);
}
if (damageAmount > 0) {
target.damage(damageAmount);
}
}
}
}
}
......@@ -17,17 +17,21 @@ import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlo
import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo;
import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo;
import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair;
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo;
import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData;
import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo;
import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
import emu.grasscutter.net.proto.PropPairOuterClass.PropPair;
import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType;
import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo;
import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import emu.grasscutter.utils.Position;
import emu.grasscutter.utils.ProtoHelper;
import emu.grasscutter.utils.Utils;
......@@ -110,6 +114,19 @@ public class EntityAvatar extends GameEntity {
this.killedBy = killerId;
}
@Override
public float heal(float amount) {
float healed = super.heal(amount);
if (healed > 0f) {
getScene().broadcastPacket(
new PacketEntityFightPropChangeReasonNotify(this, FightProperty.FIGHT_PROP_CUR_HP, healed, PropChangeReason.PROP_CHANGE_ABILITY, ChangeHpReason.ChangeHpAddAbility)
);
}
return healed;
}
public SceneAvatarInfo getSceneAvatarInfo() {
SceneAvatarInfo.Builder avatarInfo = SceneAvatarInfo.newBuilder()
.setUid(this.getPlayer().getUid())
......
package emu.grasscutter.game.entity;
import java.util.HashMap;
import java.util.Map;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.LifeState;
import emu.grasscutter.game.world.Scene;
......@@ -9,8 +12,11 @@ import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import emu.grasscutter.utils.Position;
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public abstract class GameEntity {
protected int id;
......@@ -25,6 +31,10 @@ public abstract class GameEntity {
private int lastMoveSceneTimeMs;
private int lastMoveReliableSeq;
// Abilities
private Map<String, Float> metaOverrideMap;
private Int2ObjectMap<String> metaModifiers;
public GameEntity(Scene scene) {
this.scene = scene;
this.moveState = MotionState.MOTION_NONE;
......@@ -54,6 +64,20 @@ public abstract class GameEntity {
return isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD;
}
public Map<String, Float> getMetaOverrideMap() {
if (this.metaOverrideMap == null) {
this.metaOverrideMap = new HashMap<>();
}
return this.metaOverrideMap;
}
public Int2ObjectMap<String> getMetaModifiers() {
if (this.metaModifiers == null) {
this.metaModifiers = new Int2ObjectOpenHashMap<>();
}
return this.metaModifiers;
}
public abstract Int2FloatOpenHashMap getFightProperties();
public abstract Position getPosition();
......@@ -146,4 +170,53 @@ public abstract class GameEntity {
public void setSpawnEntry(SpawnDataEntry spawnEntry) {
this.spawnEntry = spawnEntry;
}
public float heal(float amount) {
if (this.getFightProperties() == null) {
return 0f;
}
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
if (curHp >= maxHp) {
return 0f;
}
float healed = Math.min(maxHp - curHp, amount);
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed);
getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
return healed;
}
public void damage(float amount) {
damage(amount, 0);
}
public void damage(float amount, int killerId) {
// Sanity check
if (getFightProperties() == null) {
return;
}
// Lose hp
addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -amount);
// Check if dead
boolean isDead = false;
if (getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
isDead = true;
}
// Packets
this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
// Check if dead
if (isDead) {
getScene().killEntity(this, 0);
}
}
}
......@@ -8,6 +8,7 @@ import emu.grasscutter.data.def.PlayerLevelData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.CoopRequest;
import emu.grasscutter.game.ability.AbilityManager;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.avatar.AvatarProfileData;
import emu.grasscutter.game.avatar.AvatarStorage;
......@@ -89,7 +90,8 @@ public class Player {
@Transient private FriendsList friendsList;
@Transient private MailHandler mailHandler;
@Transient private MessageHandler messageHandler;
@Transient private AbilityManager abilityManager;
@Transient private SotSManager sotsManager;
private TeamManager teamManager;
......@@ -142,6 +144,7 @@ public class Player {
this.friendsList = new FriendsList(this);
this.mailHandler = new MailHandler(this);
this.towerManager = new TowerManager(this);
this.abilityManager = new AbilityManager(this);
this.pos = new Position();
this.rotation = new Position();
this.properties = new HashMap<>();
......@@ -1025,6 +1028,10 @@ public class Player {
public SotSManager getSotSManager() { return sotsManager; }
public AbilityManager getAbilityManager() {
return abilityManager;
}
public synchronized void onTick() {
// Check ping
if (this.getLastPingTime() > System.currentTimeMillis() + 60000) {
......
......@@ -385,27 +385,7 @@ public class Scene {
}
// Sanity check
if (target.getFightProperties() == null) {
return;
}
// Lose hp
target.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -result.getDamage());
// Check if dead
boolean isDead = false;
if (target.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
target.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
isDead = true;
}
// Packets
this.broadcastPacket(new PacketEntityFightPropUpdateNotify(target, FightProperty.FIGHT_PROP_CUR_HP));
// Check if dead
if (isDead) {
this.killEntity(target, result.getAttackerId());
}
target.damage(result.getDamage(), result.getAttackerId());
}
public void killEntity(GameEntity target, int attackerId) {
......
......@@ -6,6 +6,7 @@ import emu.grasscutter.net.proto.AbilityInvocationsNotifyOuterClass.AbilityInvoc
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.utils.Utils;
@Opcodes(PacketOpcodes.AbilityInvocationsNotify)
public class HandlerAbilityInvocationsNotify extends PacketHandler {
......@@ -15,7 +16,7 @@ public class HandlerAbilityInvocationsNotify extends PacketHandler {
AbilityInvocationsNotify notif = AbilityInvocationsNotify.parseFrom(payload);
for (AbilityInvokeEntry entry : notif.getInvokesList()) {
//System.out.println(entry.getArgumentType() + ": " + Utils.bytesToHex(entry.getAbilityData().toByteArray()));
session.getPlayer().getAbilityManager().onAbilityInvoke(entry);
session.getPlayer().getAbilityInvokeHandler().addEntry(entry.getForwardType(), entry);
}
......
......@@ -6,6 +6,7 @@ import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry
import emu.grasscutter.net.proto.ClientAbilityInitFinishNotifyOuterClass.ClientAbilityInitFinishNotify;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.utils.Utils;
@Opcodes(PacketOpcodes.ClientAbilityInitFinishNotify)
public class HandlerClientAbilityInitFinishNotify extends PacketHandler {
......@@ -15,6 +16,7 @@ public class HandlerClientAbilityInitFinishNotify extends PacketHandler {
ClientAbilityInitFinishNotify notif = ClientAbilityInitFinishNotify.parseFrom(payload);
for (AbilityInvokeEntry entry : notif.getInvokesList()) {
session.getPlayer().getAbilityManager().onAbilityInvoke(entry);
session.getPlayer().getClientAbilityInitFinishHandler().addEntry(entry.getForwardType(), entry);
}
......
......@@ -14,11 +14,6 @@ public class HandlerEvtCreateGadgetNotify extends PacketHandler {
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
EvtCreateGadgetNotify notify = EvtCreateGadgetNotify.parseFrom(payload);
// Dont handle in singleplayer
if (!session.getPlayer().getWorld().isMultiplayer()) {
return;
}
// Sanity check - dont add duplicate entities
if (session.getPlayer().getScene().getEntityById(notify.getEntityId()) != null) {
return;
......
......@@ -12,11 +12,6 @@ public class HandlerEvtDestroyGadgetNotify extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
EvtDestroyGadgetNotify notify = EvtDestroyGadgetNotify.parseFrom(payload);
// Dont handle in singleplayer
if (!session.getPlayer().getWorld().isMultiplayer()) {
return;
}
session.getPlayer().getScene().onPlayerDestroyGadget(notify.getEntityId());
}
......
......@@ -23,7 +23,7 @@ public class HandlerSetEntityClientDataNotify extends PacketHandler {
BasePacket packet = new BasePacket(PacketOpcodes.SetEntityClientDataNotify, true);
packet.setData(notif);
session.getPlayer().getScene().broadcastPacketToOthers(session.getPlayer(), packet);
session.getPlayer().getScene().broadcastPacket(packet);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment