From 86fe6f2138be3d22fab8e9efd2d442d51a5a3d8b Mon Sep 17 00:00:00 2001 From: Kuroppoi Date: Mon, 5 Feb 2024 01:11:38 +0100 Subject: [PATCH 01/47] Add consumables (#32) * Consumable item abstraction & add convert action handler * Add heal & refill action handlers * Pass action details * Add teleport action handler * Notify player if consumable action isn't implemented * Add skill action handler * Convert tabs to spaces * Remove annoying line break * Add skill reset action handler * Init bumped skills map * Better dialog cancellation handling * Add name changer functionality * Add stealth cloak functionality * Remove note * Fix stealth cloak duration * Fix convert consumable crash caused by duplicate item names --- deepworld-config | 2 +- .../main/java/brainwine/gameserver/Timer.java | 38 ++++++++ .../parts/RandomlyTargetBehavior.java | 2 +- .../brainwine/gameserver/dialog/Dialog.java | 27 ++++++ .../dialog/input/DialogSelectInput.java | 13 +++ .../gameserver/entity/player/NameChange.java | 35 +++++++ .../gameserver/entity/player/Player.java | 96 ++++++++++++++++--- .../entity/player/PlayerConfigFile.java | 14 +++ .../entity/player/PlayerManager.java | 13 +++ .../brainwine/gameserver/item/Action.java | 54 ++++++++++- .../java/brainwine/gameserver/item/Item.java | 20 ++++ .../item/consumables/Consumable.java | 9 ++ .../item/consumables/ConvertConsumable.java | 88 +++++++++++++++++ .../item/consumables/HealConsumable.java | 16 ++++ .../consumables/NameChangeConsumable.java | 67 +++++++++++++ .../item/consumables/RefillConsumable.java | 16 ++++ .../item/consumables/SkillConsumable.java | 93 ++++++++++++++++++ .../consumables/SkillResetConsumable.java | 62 ++++++++++++ .../item/consumables/StealthConsumable.java | 26 +++++ .../item/consumables/TeleportConsumable.java | 53 ++++++++++ .../server/requests/DialogRequest.java | 3 +- .../server/requests/InventoryUseRequest.java | 2 +- 22 files changed, 730 insertions(+), 19 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/Timer.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/NameChange.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java diff --git a/deepworld-config b/deepworld-config index ec2749d..8444603 160000 --- a/deepworld-config +++ b/deepworld-config @@ -1 +1 @@ -Subproject commit ec2749d94b86dbbdefa52e0146ce278688e28370 +Subproject commit 8444603ae9eb369fc468fb2fb2fe83b040efc4c1 diff --git a/gameserver/src/main/java/brainwine/gameserver/Timer.java b/gameserver/src/main/java/brainwine/gameserver/Timer.java new file mode 100644 index 0000000..fb02f7e --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/Timer.java @@ -0,0 +1,38 @@ +package brainwine.gameserver; + +/** + * Model for synchronous timers. + */ +public class Timer { + + private T key; + private long time; + private Runnable action; + + public Timer(T key, long delay, Runnable action) { + this.key = key; + this.time = System.currentTimeMillis() + delay; + this.action = action; + } + + public boolean process() { + if(System.currentTimeMillis() >= time) { + action.run(); + return true; + } + + return false; + } + + public T getKey() { + return key; + } + + public long getTime() { + return time; + } + + public Runnable getAction() { + return action; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java index 3311393..afc982a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java @@ -31,7 +31,7 @@ public class RandomlyTargetBehavior extends Behavior { if(!entity.hasTarget()) { Player target = entity.getZone().getRandomPlayerInRange(entity.getX(), entity.getY(), range); - if(target != null && !target.isGodMode() && !target.isDead() && !entity.isOwnedBy(target) && (!blockable || entity.canSee(target))) { + if(target != null && !target.isGodMode() && !target.isStealthy() && !target.isDead() && !entity.isOwnedBy(target) && (!blockable || entity.canSee(target))) { entity.setTarget(target); targetLockedAt = now; } diff --git a/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java b/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java index d931eed..cd2690c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java +++ b/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java @@ -1,12 +1,15 @@ package brainwine.gameserver.dialog; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonSetter; @JsonInclude(Include.NON_DEFAULT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -15,6 +18,7 @@ public class Dialog { private DialogType type = DialogType.STANDARD; private DialogAlignment alignment = DialogAlignment.LEFT; private List sections = new ArrayList<>(); + private Object actions; private String title; private String target; @@ -61,6 +65,29 @@ public class Dialog { return sections; } + @JsonSetter + private void setActions(Object actions) { + this.actions = actions; + } + + public Dialog setActions(String actions) { + this.actions = actions; + return this; + } + + public Dialog setActions(String... actions) { + return setActions(Arrays.asList(actions)); + } + + public Dialog setActions(Collection actions) { + this.actions = actions; + return this; + } + + public Object getActions() { + return actions; + } + public Dialog setTitle(String title) { this.title = title; return this; diff --git a/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java b/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java index cffab2c..39ce767 100644 --- a/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java +++ b/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java @@ -3,9 +3,12 @@ package brainwine.gameserver.dialog.input; import java.util.Arrays; import java.util.Collection; +import com.fasterxml.jackson.annotation.JsonProperty; + public class DialogSelectInput extends DialogInput { private Collection options; + private int maxColumns; public DialogSelectInput setOptions(String... options) { return setOptions(Arrays.asList(options)); @@ -19,4 +22,14 @@ public class DialogSelectInput extends DialogInput { public Collection getOptions() { return options; } + + public DialogSelectInput setMaxColumns(int maxColumns) { + this.maxColumns = maxColumns; + return this; + } + + @JsonProperty("max columns") + public int getMaxColumns() { + return maxColumns; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/NameChange.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/NameChange.java new file mode 100644 index 0000000..5252cda --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/NameChange.java @@ -0,0 +1,35 @@ +package brainwine.gameserver.entity.player; + +import java.time.OffsetDateTime; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; + +@JsonIncludeProperties({"new_name", "previous_name", "date"}) +public class NameChange { + + private String newName; + private String previousName; + private OffsetDateTime date; + + @JsonCreator + private NameChange() {} + + public NameChange(String newName, String previousName) { + this.newName = newName; + this.previousName = previousName; + date = OffsetDateTime.now(); + } + + public String getNewName() { + return newName; + } + + public String getPreviousName() { + return previousName; + } + + public OffsetDateTime getDate() { + return date; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index dd55321..7fc29a7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import brainwine.gameserver.GameConfiguration; import brainwine.gameserver.GameServer; +import brainwine.gameserver.Timer; import brainwine.gameserver.achievements.Achievement; import brainwine.gameserver.achievements.AchievementManager; import brainwine.gameserver.achievements.JourneymanAchievement; @@ -29,14 +30,13 @@ import brainwine.gameserver.dialog.DialogSection; import brainwine.gameserver.dialog.DialogType; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityStatus; -import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; -import brainwine.gameserver.item.Action; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.MiningBonus; +import brainwine.gameserver.item.consumables.Consumable; import brainwine.gameserver.loot.Loot; import brainwine.gameserver.server.Message; import brainwine.gameserver.server.messages.AchievementMessage; @@ -95,16 +95,19 @@ public class Player extends Entity implements CommandExecutor { private Inventory inventory; private PlayerStatistics statistics; private List authTokens; + private List nameChanges; private List mutes; private List bans; private Set achievements; private Map ignoredHints; private Map skills; + private Map> bumpedSkills; private Map equippedClothing; private Map equippedColors; private final Map settings = new HashMap<>(); private final Set activeChunks = new HashSet<>(); private final Map> dialogs = new HashMap<>(); + private final List> timers = new ArrayList<>(); private final List trackedEntities = new ArrayList<>(); private String clientVersion; private Placement lastPlacement; @@ -112,6 +115,7 @@ public class Player extends Entity implements CommandExecutor { private Vector2i spawnPoint = new Vector2i(0, 0); private int teleportX; private int teleportY; + private boolean stealth; private boolean godMode; private long lastHeartbeat; private long lastTrackedEntityUpdate; @@ -132,11 +136,13 @@ public class Player extends Entity implements CommandExecutor { this.inventory = config.getInventory(); this.statistics = config.getStatistics(); this.authTokens = config.getAuthTokens(); + this.nameChanges = config.getNameChanges(); this.mutes = config.getMutes(); this.bans = config.getBans(); this.achievements = config.getAchievements(); this.ignoredHints = config.getIgnoredHints(); this.skills = config.getSkills(); + this.bumpedSkills = config.getBumpedSkills(); this.equippedClothing = config.getEquippedClothing(); this.equippedColors = config.getEquippedColors(); health = getMaxHealth(); @@ -151,11 +157,13 @@ public class Player extends Entity implements CommandExecutor { this.inventory = new Inventory(this); this.statistics = new PlayerStatistics(this); this.authTokens = new ArrayList<>(); + this.nameChanges = new ArrayList<>(); this.mutes = new ArrayList<>(); this.bans = new ArrayList<>(); this.achievements = new HashSet<>(); this.ignoredHints = new HashMap<>(); this.skills = new HashMap<>(); + this.bumpedSkills = new HashMap<>(); this.equippedClothing = new HashMap<>(); this.equippedColors = new HashMap<>(); } @@ -182,6 +190,9 @@ public class Player extends Entity implements CommandExecutor { heal(BASE_REGEN_AMOUNT * deltaTime); } + // Process timers + timers.removeIf(Timer::process); + // Update tracked entities if(now - lastTrackedEntityUpdate >= TRACKED_ENTITY_UPDATE_INTERVAL) { updateTrackedEntities(); @@ -223,6 +234,15 @@ public class Player extends Entity implements CommandExecutor { sendMessage(new HealthMessage(health)); } + @Override + public void setProperties(Map properties, boolean sendMessage) { + super.setProperties(properties, sendMessage); + + if(sendMessage) { + sendMessage(new EntityChangeMessage(id, properties)); + } + } + /** * @return A {@link Map} containing all the data necessary for use in {@link EntityStatusMessage}. */ @@ -322,6 +342,8 @@ public class Player extends Entity implements CommandExecutor { /** * Called from {@link Connection} when the channel becomes inactive. + * + * TODO Should we force process all timers on disconnect? */ public void onDisconnect() { lastHeartbeat = 0; @@ -413,20 +435,31 @@ public class Player extends Entity implements CommandExecutor { } public void handleDialogInput(int id, Object[] input) { - if(id == 0 || (input.length == 1 && input[0].equals("cancel"))) { + if(id == 0) { return; } Consumer handler = dialogs.remove(id); if(handler == null) { - notify("Sorry, the request has expired."); + if(!(input.length == 1 && input[0].equals("cancel"))) { + notify("Sorry, the request has expired."); + } } else { // TODO since we're dealing with user input, should we just try-catch this? handler.accept(input); } } + public void addTimer(String key, long delay, Runnable action) { + removeTimer(key); + timers.add(new Timer<>(key, delay, action)); + } + + public void removeTimer(String key) { + timers.removeIf(timer -> timer.getKey().equals(key)); + } + public void checkRegistration() { if(!isRegistered()) { sendMessage(new EventMessage("playerRegistered", false)); @@ -502,6 +535,15 @@ public class Player extends Entity implements CommandExecutor { return teleportY; } + public void setStealth(boolean stealth) { + this.stealth = stealth; + setProperty("xs", stealth ? 1 : 0, true); + } + + public boolean isStealthy() { + return stealth; + } + public void setGodMode(boolean godMode) { this.godMode = godMode; } @@ -657,6 +699,14 @@ public class Player extends Entity implements CommandExecutor { return authTokens; } + public void trackNameChange(String newName) { + nameChanges.add(new NameChange(newName, name)); + } + + public List getNameChanges() { + return nameChanges; + } + public void mute(String reason, OffsetDateTime until) { mute(null, reason, until); } @@ -984,19 +1034,39 @@ public class Player extends Entity implements CommandExecutor { return Collections.unmodifiableMap(skills); } + public void trackSkillBump(Item item, Skill skill) { + List skills = bumpedSkills.get(item); + + if(skills == null) { + skills = new ArrayList<>(); + bumpedSkills.put(item, skills); + } + + skills.add(skill); + } + + public boolean hasSkillBeenBumped(Item item, Skill skill) { + return bumpedSkills.getOrDefault(item, Collections.emptyList()).contains(skill); + } + + public Map> getBumpedSkills() { + return bumpedSkills; + } + public void consume(Item item) { - Action action = item.getAction(); + consume(item, null); + } + + public void consume(Item item, Object details) { + Consumable consumable = item.getAction().getConsumable(); - // TODO some kind of abstraction for things like this would be pretty cool - switch(action) { - case HEAL: heal(item.getPower()); break; - default: break; + if(consumable == null) { + sendMessage(new InventoryMessage(inventory.getClientConfig(item))); + notify("Sorry, this action hasn't been implemented yet."); + return; } - // (Temporary?) measure to prevent consuming unimplemented consumables - if(action != Action.NONE) { - inventory.removeItem(item); - } + consumable.consume(item, this, details); } public void awardLoot(Loot loot) { diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java index 623a321..76c6443 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java @@ -31,11 +31,13 @@ public class PlayerConfigFile { private Inventory inventory = new Inventory(); private PlayerStatistics statistics = new PlayerStatistics(); private List authTokens = new ArrayList<>(); + private List nameChanges = new ArrayList<>(); private List mutes = new ArrayList<>(); private List bans = new ArrayList<>(); private Set achievements = new HashSet<>(); private Map ignoredHints = new HashMap<>(); private Map skills = new HashMap<>(); + private Map> bumpedSkills = new HashMap<>(); private Map equippedClothing = new HashMap<>(); private Map equippedColors = new HashMap<>(); @@ -52,11 +54,13 @@ public class PlayerConfigFile { this.inventory = player.getInventory(); this.statistics = player.getStatistics(); this.authTokens = player.getAuthTokens(); + this.nameChanges = player.getNameChanges(); this.mutes = player.getMutes(); this.bans = player.getBans(); this.achievements = player.getAchievements(); this.ignoredHints = player.getIgnoredHints(); this.skills = player.getSkills(); + this.bumpedSkills = player.getBumpedSkills(); this.equippedClothing = player.getEquippedClothing(); this.equippedColors = player.getEquippedColors(); } @@ -90,6 +94,11 @@ public class PlayerConfigFile { return authTokens; } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public List getNameChanges() { + return nameChanges; + } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) public List getMutes() { return mutes; @@ -141,6 +150,11 @@ public class PlayerConfigFile { return skills; } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Map> getBumpedSkills() { + return bumpedSkills; + } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) public Map getEquippedClothing() { return equippedClothing; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java index b6a2f94..463ed3f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java @@ -139,6 +139,19 @@ public class PlayerManager { return false; } + public void changePlayerName(Player player, String name) { + if(playersByName.containsKey(name)) { + logger.warn("Tried to rename player {} to already existing name {}", player.getDocumentId(), name); + return; + } + + // Track name change and re-index the player + playersByName.remove(player.getName().toLowerCase()); + player.trackNameChange(name); + player.setName(name); + playersByName.put(name.toLowerCase(), player); + } + public void onPlayerConnect(Player player) { Connection connection = player.getConnection(); playersByConnection.put(connection, player); diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Action.java b/gameserver/src/main/java/brainwine/gameserver/item/Action.java index 960392c..6c2acf6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Action.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Action.java @@ -1,13 +1,63 @@ package brainwine.gameserver.item; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import brainwine.gameserver.item.consumables.Consumable; +import brainwine.gameserver.item.consumables.ConvertConsumable; +import brainwine.gameserver.item.consumables.HealConsumable; +import brainwine.gameserver.item.consumables.NameChangeConsumable; +import brainwine.gameserver.item.consumables.RefillConsumable; +import brainwine.gameserver.item.consumables.SkillConsumable; +import brainwine.gameserver.item.consumables.SkillResetConsumable; +import brainwine.gameserver.item.consumables.StealthConsumable; +import brainwine.gameserver.item.consumables.TeleportConsumable; + +/** + * Action types for items. + * + * All consumables depend on their action type, but not all items with actions are consumables. + * This creates a bit of an awkward situation in terms of implementation, but we're just gonna have to deal with that. + */ public enum Action { + CONVERT(new ConvertConsumable()), DIG, - HEAL, - REFILL, + HEAL(new HealConsumable()), + NAME_CHANGE(new NameChangeConsumable()), + REFILL(new RefillConsumable()), + SKILL(new SkillConsumable()), + SKILL_RESET(new SkillResetConsumable()), + STEALTH(new StealthConsumable()), + TELEPORT(new TeleportConsumable()), @JsonEnumDefaultValue NONE; + + private final Consumable consumable; + + private Action(Consumable consumable) { + this.consumable = consumable; + } + + private Action() { + this(null); + } + + @JsonCreator + public static Action fromId(String id) { + String formatted = id.toUpperCase().replace(" ", "_"); + + for(Action value : values()) { + if(value.toString().equals(formatted)) { + return value; + } + } + + return NONE; + } + + public Consumable getConsumable() { + return consumable; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java index fa2e36c..fb00377 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -16,6 +17,7 @@ import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.util.Pair; import brainwine.gameserver.util.Vector2i; +// TODO I don't like some parts of this, maybe they can be reworked. @JsonIgnoreProperties(ignoreUnknown = true) public class Item { @@ -132,6 +134,9 @@ public class Item { @JsonProperty("skill_bonuses") private Map skillBonuses = new HashMap<>(); + @JsonProperty("power_bonus") + private Pair powerBonus; + @JsonProperty("mining skill") private Pair miningSkill; @@ -153,6 +158,9 @@ public class Item { @JsonProperty("use") private Map useConfigs = new HashMap<>(); + @JsonProperty("convert") + private Map conversions = new HashMap<>(); + @JsonCreator private Item(@JsonProperty(value = "id", required = true) String id, @JsonProperty(value = "code", required = true) int code) { @@ -359,6 +367,14 @@ public class Item { return skillBonuses; } + public boolean hasPowerBonus() { + return powerBonus != null; + } + + public Pair getPowerBonus() { + return powerBonus; + } + public boolean requiresMiningSkill() { return miningSkill != null; } @@ -456,4 +472,8 @@ public class Item { public Map getUses() { return useConfigs; } + + public Map getConversions() { + return conversions.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey().get(), entry -> entry.getValue().get())); + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java new file mode 100644 index 0000000..32b3a77 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java @@ -0,0 +1,9 @@ +package brainwine.gameserver.item.consumables; + +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; + +public interface Consumable { + + public void consume(Item item, Player player, Object details); +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java new file mode 100644 index 0000000..8bf729c --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java @@ -0,0 +1,88 @@ +package brainwine.gameserver.item.consumables; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.dialog.input.DialogSelectInput; +import brainwine.gameserver.entity.player.Inventory; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.server.messages.InventoryMessage; + +/** + * Consumable handler for upgrade kits + */ +public class ConvertConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + Map conversions = item.getConversions(); + Inventory inventory = player.getInventory(); + + // Find items in the player's inventory that can be upgraded + Set convertables = conversions.keySet().stream().filter(i -> inventory.hasItem(i)).collect(Collectors.toSet()); + + // Don't do anything if the player has no items that can be converted + if(convertables.isEmpty()) { + player.notify("You do not have any upgradeable items."); + player.sendMessage(new InventoryMessage(inventory.getClientConfig(item))); + return; + } + + // Map item titles to their id + Map keyMap = convertables.stream().collect(Collectors.toMap(Item::getTitle, Item::getId, (a, b) -> a)); + + // Create upgrade dialog + Dialog dialog = new Dialog().addSection(new DialogSection() + .setTitle("Which item would you like to upgrade?") + .setInput(new DialogSelectInput() + .setOptions(convertables.stream().map(Item::getTitle).collect(Collectors.toList())) + .setMaxColumns(3) + .setKey("item"))); + + player.showDialog(dialog, data -> { + // Handle cancellation + if(data.length == 1 && data[0].equals("cancel")) { + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + // Fail if there is no data + if(data.length == 0) { + fail(item, player); + return; + } + + String key = keyMap.get(data[0]); + + // Fail if the chosen item title doesn't map to an id + if(key == null) { + fail(item, player); + return; + } + + Item itemToUpgrade = ItemRegistry.getItem(key); + Item targetItem = conversions.get(itemToUpgrade); + + // Fail if the player doesn't have the item they want to upgrade or there is no upgrade for it + if(!inventory.hasItem(itemToUpgrade) || targetItem == null) { + fail(item, player); + return; + } + + inventory.removeItem(item, true); // Remove the consumable + inventory.removeItem(itemToUpgrade, true); // Remove the item that was upgraded + inventory.addItem(targetItem, true); // Add the item that the item upgraded to :) + player.notify(String.format("%s upgraded to %s!", itemToUpgrade.getTitle(), targetItem.getTitle())); + }); + } + + private void fail(Item item, Player player) { + player.notify("Oops! There was a problem with the upgrade."); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java new file mode 100644 index 0000000..5473f5b --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java @@ -0,0 +1,16 @@ +package brainwine.gameserver.item.consumables; + +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; + +/** + * Consumable handler for healing items + */ +public class HealConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + player.heal(item.getPower()); + player.getInventory().removeItem(item); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java new file mode 100644 index 0000000..90b3a1f --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java @@ -0,0 +1,67 @@ +package brainwine.gameserver.item.consumables; + +import java.util.regex.Pattern; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.player.PlayerManager; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.server.messages.EventMessage; +import brainwine.gameserver.server.messages.InventoryMessage; + +/** + * Consumable handler for name changers + */ +public class NameChangeConsumable implements Consumable { + + private static final Pattern namePattern = Pattern.compile("^[a-zA-Z0-9_.-]{4,20}$"); + + @Override + public void consume(Item item, Player player, Object details) { + PlayerManager playerManager = GameServer.getInstance().getPlayerManager(); + Dialog dialog = DialogHelper.inputDialog("Change your name", + "Your in-game name can include letters, numbers, dashes and periods, and must be between 4 and 20 characters in length."); + + player.showDialog(dialog, data -> { + // Handle cancellation + if(data.length == 1 && data[0].equals("cancel")) { + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + String name = data.length == 1 ? "" + data[0] : null; + + // Check if the data is present + if(name == null) { + fail(item, player, "Oops! There was a problem with your request."); + return; + } + + // Check if the name is valid + if(!namePattern.matcher(name).matches()) { + fail(item, player, "Please enter a valid name."); + return; + } + + // Check if name is already taken + if(playerManager.getPlayer(name) != null) { + fail(item, player, "That name is taken already."); + return; + } + + player.getInventory().removeItem(item); // Remove the consumable + playerManager.changePlayerName(player, name); // Process the name change + + // TODO this creates a race condition + player.sendMessage(new EventMessage("playerNameDidChange", name)); // Client side processing stuff + player.kick("Your name has been changed."); // Force the player to reconnect + }); + } + + private void fail(Item item, Player player, String message) { + player.showDialog(DialogHelper.messageDialog(message)); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java new file mode 100644 index 0000000..a965a13 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java @@ -0,0 +1,16 @@ +package brainwine.gameserver.item.consumables; + +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; + +/** + * Consumable handler for steam canisters + */ +public class RefillConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + // All we do is remove the item because steam functionality is pretty much entirely client-side + player.getInventory().removeItem(item); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java new file mode 100644 index 0000000..d62a661 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java @@ -0,0 +1,93 @@ +package brainwine.gameserver.item.consumables; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.text.WordUtils; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.dialog.input.DialogSelectInput; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.server.messages.InventoryMessage; + +/** + * Consumable handler for skill upgrade items + */ +public class SkillConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + List bumpedSkills = player.getBumpedSkills().getOrDefault(item, Collections.emptyList()); + + // Check if all skills have been bumped already + if(bumpedSkills.size() >= Skill.values().length) { + player.notify(String.format("You have already increased all of your skills with %ss.", item.getTitle().toLowerCase())); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + // Assemble a list of skills that can be upgraded with this consumable + List upgradeableSkills = Arrays.asList(Skill.values()).stream() + .filter(skill -> !bumpedSkills.contains(skill) && player.getSkillLevel(skill) < 10) + .collect(Collectors.toList()); + + // Check if there are any skills to upgrade + if(upgradeableSkills.isEmpty()) { + player.notify("You have maximized all skills available for mastery."); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + List upgradeableSkillNames = upgradeableSkills.stream() + .map(Skill::getId) + .map(WordUtils::capitalize) + .collect(Collectors.toList()); + + // Create dialog + Dialog dialog = new Dialog().addSection(new DialogSection() + .setTitle("Which skill would you like to increase?") + .setInput(new DialogSelectInput() + .setOptions(upgradeableSkillNames) + .setMaxColumns(3) + .setKey("skill"))); + + player.showDialog(dialog, data -> { + // Handle cancellation + if(data.length == 1 && data[0].equals("cancel")) { + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + // Verify data + if(data.length != 1) { + fail(item, player); + return; + } + + Skill skill = Skill.fromId("" + data[0]); + + // Make sure that the skill is still eligible for upgrading + if(skill == null || player.hasSkillBeenBumped(item, skill) || player.getSkillLevel(skill) >= 10) { + fail(item, player); + return; + } + + player.getInventory().removeItem(item, true); // Remove consumable + player.trackSkillBump(item, skill); // Track skill bump + player.setSkillLevel(skill, player.getSkillLevel(skill) + 1); // Increase skill level + player.showDialog(DialogHelper.messageDialog(String.format("%s increased!", WordUtils.capitalize(skill.toString().toLowerCase())), + String.format("You now have additional mastery of %s.", skill.toString().toLowerCase()))); + }); + } + + private void fail(Item item, Player player) { + player.notify("Oops! There was a problem with upgrading your skill."); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java new file mode 100644 index 0000000..3820566 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java @@ -0,0 +1,62 @@ +package brainwine.gameserver.item.consumables; + +import java.util.Map.Entry; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.server.messages.InventoryMessage; + +/** + * Consumable handler for skill resets + */ +public class SkillResetConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + // Create dialog + Dialog dialog = new Dialog() + .setActions("yesno") + .addSection(new DialogSection() + .setTitle("Confirm skill reset") + .setText("Are you sure that you want to reset all of your skills back to level 1?")); + + player.showDialog(dialog, data -> { + // Handle cancellation + if(data.length == 1 && data[0].equals("cancel")) { + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + // Check if there are any skills to reset + if(!player.getSkills().values().stream().anyMatch(level -> level > 1)) { + player.showDialog(DialogHelper.messageDialog("You don't have any skills to reset.")); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + return; + } + + int pointsToRefund = 0; + + // Reset skill levels and calculate point refund total + for(Entry entry : player.getSkills().entrySet()) { + Skill skill = entry.getKey(); + int level = entry.getValue(); + + // Skip if skill hasn't been upgraded at all + if(level <= 1) { + continue; + } + + pointsToRefund += level - 1; + player.setSkillLevel(skill, 1); // Reset skill level + } + + player.getInventory().removeItem(item, true); // Remove the consumable + player.setSkillPoints(player.getSkillPoints() + pointsToRefund); // Refund skill points + player.showDialog(DialogHelper.getDialog("skill_reset")); + }); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java new file mode 100644 index 0000000..562b49e --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java @@ -0,0 +1,26 @@ +package brainwine.gameserver.item.consumables; + +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; + +/** + * Consumable handler for stealth cloaks + */ +public class StealthConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + player.getInventory().removeItem(item); + player.setStealth(true); + float seconds = item.getPower(); + + // Apply skill power bonus + if(item.hasPowerBonus()) { + seconds += player.getTotalSkillLevel(item.getPowerBonus().getFirst()) * item.getPowerBonus().getLast(); + } + + // Create timer + long delay = (long)(seconds * 1000); + player.addTimer("end stealth", delay, () -> player.setStealth(false)); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java new file mode 100644 index 0000000..fa6138e --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java @@ -0,0 +1,53 @@ +package brainwine.gameserver.item.consumables; + +import java.util.List; + +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.server.messages.InventoryMessage; +import brainwine.gameserver.zone.MetaBlock; + +/** + * Consumable handler for portable teleporters. + * + * These do not seem to function correctly on v3 clients. + */ +public class TeleportConsumable implements Consumable { + + @Override + public void consume(Item item, Player player, Object details) { + // Verify details + if(details == null || !(details instanceof List)) { + fail(item, player); + return; + } + + @SuppressWarnings("unchecked") + List coordinates = (List)details; + + // Verify coordinates + if(coordinates.size() != 2 || !(coordinates.get(0) instanceof Integer) || !(coordinates.get(1) instanceof Integer)) { + fail(item, player); + return; + } + + int x = (int)coordinates.get(0); + int y = (int)coordinates.get(1); + MetaBlock block = player.getZone().getMetaBlock(x, y); + + // Check if there is a teleporter at the target location + if(block == null || !block.getItem().hasUse(ItemUseType.TELEPORT, ItemUseType.ZONE_TELEPORT)) { + fail(item, player); + return; + } + + player.getInventory().removeItem(item); + player.teleport(x, y); + } + + private void fail(Item item, Player player) { + player.notify("Oops! There was a problem teleporting you to your target destination."); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java index 44e1b21..e40904c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java @@ -73,10 +73,11 @@ public class DialogRequest extends PlayerRequest { "Note: Additional skills like Combat and Engineering are unlocked as you progress." : null) .setInput(new DialogSelectInput() .setOptions(upgradeableSkillNames) + .setMaxColumns(3) .setKey("skill"))); player.showDialog(dialog, input -> { - if(input.length == 0) { + if(input.length == 0 || input[0].equals("cancel")) { return; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java index 7847ebc..98a6d7a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java @@ -36,7 +36,7 @@ public class InventoryUseRequest extends PlayerRequest { // Try to consume item if it is a consumable if(item.isConsumable()) { if(status == 1) { - player.consume(item); + player.consume(item, details); } } else { // Set current held item if applicable From 8a9bc80c491328387cf651e81e27ebe02a62836e Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Mon, 5 Feb 2024 01:13:52 +0100 Subject: [PATCH 02/47] Send skill data before inventory data --- .../java/brainwine/gameserver/entity/player/Player.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 7fc29a7..0c2221a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -296,15 +296,16 @@ public class Player extends Entity implements CommandExecutor { sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); sendMessage(new PlayerPositionMessage((int)x, (int)y)); sendMessage(new HealthMessage(health)); - sendMessage(new InventoryMessage(inventory)); - sendMessage(new WardrobeMessage(inventory.getWardrobe())); - sendMessage(new BlockMetaMessage(zone.getGlobalMetaBlocks())); // Send skill data for(Skill skill : skills.keySet()) { sendMessage(new SkillMessage(skill, skills.get(skill))); } + sendMessage(new InventoryMessage(inventory)); + sendMessage(new WardrobeMessage(inventory.getWardrobe())); + sendMessage(new BlockMetaMessage(zone.getGlobalMetaBlocks())); + // Send peer data Collection peers = zone.getPlayers(); sendMessage(new EntityStatusMessage(peers, EntityStatus.ENTERING)); From 81ceaa45427ec68d22c180981b80e74c2a566230 Mon Sep 17 00:00:00 2001 From: Kuroppoi Date: Sat, 17 Feb 2024 21:57:36 +0100 Subject: [PATCH 03/47] Implement a bunch of block interactions (#34) * Block interaction abstraction, block timers & timed switch functionality Also fixes #31 :) * Add chest o' plenty functionality * Add spawn teleporter functionality * Add transmitter functionality * Ignore transmit limit if the player has god mode enabled * Oops, gotta use this one * Bomb explosions test * Explosion raycasting * Oops forgot this * Add proximity burst & spawn functionality * Forgot to switch this back * Add player stealth check for mines * Touchplates, spawners, exploders, and other goodies * Diamond & onyx target teleporter functionality * Fireplace stuff * Check for entity spawns on block mine * Spawn bomb functionality * Liquid bomb functionality --- deepworld-config | 2 +- .../main/java/brainwine/gameserver/Timer.java | 6 +- .../brainwine/gameserver/entity/Entity.java | 49 +++- .../brainwine/gameserver/entity/npc/Npc.java | 18 +- .../gameserver/entity/player/Player.java | 101 ++++++-- .../entity/player/PlayerConfigFile.java | 7 + .../java/brainwine/gameserver/item/Item.java | 55 +++++ .../gameserver/item/ItemUseType.java | 52 ++++- .../item/interactions/BurstInteraction.java | 63 +++++ .../item/interactions/ChangeInteraction.java | 26 +++ .../interactions/ContainerInteraction.java | 88 +++++++ .../item/interactions/DialogInteraction.java | 98 ++++++++ .../item/interactions/ItemInteraction.java | 13 ++ .../item/interactions/SpawnInteraction.java | 39 ++++ .../SpawnTeleportInteraction.java | 39 ++++ .../item/interactions/SwitchInteraction.java | 215 ++++++++++++++++++ .../TargetTeleportInteraction.java | 102 +++++++++ .../interactions/TeleportInteraction.java | 56 +++++ .../interactions/TransmitInteraction.java | 58 +++++ .../java/brainwine/gameserver/loot/Loot.java | 7 + .../server/messages/EffectMessage.java | 6 +- .../server/requests/BlockMineRequest.java | 48 ++++ .../server/requests/BlockPlaceRequest.java | 117 +++++++++- .../server/requests/BlockUseRequest.java | 163 ++----------- .../brainwine/gameserver/util/MathUtils.java | 6 +- .../java/brainwine/gameserver/zone/Block.java | 64 +++--- .../brainwine/gameserver/zone/MetaBlock.java | 58 +++++ .../java/brainwine/gameserver/zone/Zone.java | 194 ++++++++++++++-- .../gameserver/zone/ZoneManager.java | 7 +- 29 files changed, 1528 insertions(+), 229 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java diff --git a/deepworld-config b/deepworld-config index 8444603..827c1fa 160000 --- a/deepworld-config +++ b/deepworld-config @@ -1 +1 @@ -Subproject commit 8444603ae9eb369fc468fb2fb2fe83b040efc4c1 +Subproject commit 827c1fac075ed2a9d92f56696f36101d54485109 diff --git a/gameserver/src/main/java/brainwine/gameserver/Timer.java b/gameserver/src/main/java/brainwine/gameserver/Timer.java index fb02f7e..7c817fc 100644 --- a/gameserver/src/main/java/brainwine/gameserver/Timer.java +++ b/gameserver/src/main/java/brainwine/gameserver/Timer.java @@ -16,7 +16,11 @@ public class Timer { } public boolean process() { - if(System.currentTimeMillis() >= time) { + return process(false); + } + + public boolean process(boolean force) { + if(force || System.currentTimeMillis() >= time) { action.run(); return true; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 202e395..6009cc9 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -6,11 +6,16 @@ import java.util.List; import java.util.Map; import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; import brainwine.gameserver.server.Message; import brainwine.gameserver.server.messages.EntityChangeMessage; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MathUtils; +import brainwine.gameserver.zone.Block; +import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; public abstract class Entity { @@ -29,6 +34,10 @@ public abstract class Entity { protected float y; protected float velocityX; protected float velocityY; + protected int blockX; + protected int blockY; + protected int lastBlockX; + protected int lastBlockY; protected int targetX; protected int targetY; protected FacingDirection direction = FacingDirection.WEST; @@ -40,7 +49,16 @@ public abstract class Entity { } public void tick(float deltaTime) { - // Override + // Update block position + lastBlockX = blockX; + lastBlockY = blockY; + blockX = (int)x; + blockY = (int)y; + + // Check if block position has changed + if(lastBlockX != blockX || lastBlockY != blockY) { + blockPositionChanged(); + } } public void die(Player killer) { @@ -67,6 +85,21 @@ public abstract class Entity { lastDamagedAt = System.currentTimeMillis(); } + public void blockPositionChanged() { + // Check for touchplates + if(zone != null && zone.isChunkLoaded(blockX, blockY)) { + MetaBlock metaBlock = zone.getMetaBlock(blockX, blockY); + Block block = zone.getBlock(blockX, blockY); + Item item = block.getFrontItem(); + int mod = block.getFrontMod(); + + // Trigger a switch interaction if the entity stepped on a touchplate + if(item.hasUse(ItemUseType.TRIGGER)) { + ItemUseType.SWITCH.getInteraction().interact(zone, this, blockX, blockY, Layer.FRONT, item, mod, metaBlock, null, null); + } + } + } + public boolean canSee(Entity other) { return canSee((int)other.getX(), (int)other.getY()); } @@ -80,7 +113,7 @@ public abstract class Entity { return inRange(other.getX(), other.getY(), range); } - public boolean inRange(float x, float y, float range) { + public boolean inRange(float x, float y, double range) { return MathUtils.inRange(this.x, this.y, x, y, range); } @@ -204,6 +237,14 @@ public abstract class Entity { return targetY; } + public int getBlockX() { + return blockX; + } + + public int getBlockY() { + return blockY; + } + public void setDirection(FacingDirection direction) { this.direction = direction; } @@ -228,6 +269,10 @@ public abstract class Entity { return zone; } + public final boolean isPlayer() { + return this instanceof Player; // Not very OOP + } + /** * @return A {@link Map} containing all the data necessary for use in {@link EntityStatusMessage}. */ diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index 9953c74..bc75fb3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -52,6 +52,7 @@ public class Npc extends Entity { private Vector2i mountBlock; private Entity owner; private Entity target; + private boolean artificial; private long lastBehavedAt = System.currentTimeMillis(); private long lastTrackedAt = System.currentTimeMillis(); @@ -120,6 +121,7 @@ public class Npc extends Entity { @Override public void tick(float deltaTime) { + super.tick(deltaTime); long now = System.currentTimeMillis(); // Clear expired recent attacks @@ -148,7 +150,7 @@ public class Npc extends Entity { @Override public void die(Player killer) { // Grant loot & track kill - if(killer != null) { + if(!artificial && killer != null) { if(!isPlayerPlaced()) { // Track assists for(Player attacker : recentAttacks.keySet()) { @@ -176,7 +178,11 @@ public class Npc extends Entity { MetaBlock metaBlock = zone.getMetaBlock(guardBlock.getX(), guardBlock.getY()); if(metaBlock != null) { - MapHelper.getList(metaBlock.getMetadata(), "!", Collections.emptyList()).remove(typeName); + List guards = MapHelper.getList(metaBlock.getMetadata(), "!"); + + if(guards != null) { + guards.remove(typeName); + } } } @@ -372,6 +378,14 @@ public class Npc extends Entity { return target; } + public void setArtificial(boolean artificial) { + this.artificial = artificial; + } + + public boolean isArtificial() { + return artificial; + } + public void setSpeed(float speed) { this.speed = speed; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 0c2221a..f3bab42 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -98,6 +98,7 @@ public class Player extends Entity implements CommandExecutor { private List nameChanges; private List mutes; private List bans; + private Set lootCodes; private Set achievements; private Map ignoredHints; private Map skills; @@ -112,11 +113,13 @@ public class Player extends Entity implements CommandExecutor { private String clientVersion; private Placement lastPlacement; private Item heldItem = Item.AIR; - private Vector2i spawnPoint = new Vector2i(0, 0); + private int spawnX; + private int spawnY; private int teleportX; private int teleportY; private boolean stealth; private boolean godMode; + private boolean customSpawn; private long lastHeartbeat; private long lastTrackedEntityUpdate; private Zone nextZone; @@ -139,6 +142,7 @@ public class Player extends Entity implements CommandExecutor { this.nameChanges = config.getNameChanges(); this.mutes = config.getMutes(); this.bans = config.getBans(); + this.lootCodes = config.getLootCodes(); this.achievements = config.getAchievements(); this.ignoredHints = config.getIgnoredHints(); this.skills = config.getSkills(); @@ -160,6 +164,7 @@ public class Player extends Entity implements CommandExecutor { this.nameChanges = new ArrayList<>(); this.mutes = new ArrayList<>(); this.bans = new ArrayList<>(); + this.lootCodes = new HashSet<>(); this.achievements = new HashSet<>(); this.ignoredHints = new HashMap<>(); this.skills = new HashMap<>(); @@ -175,6 +180,7 @@ public class Player extends Entity implements CommandExecutor { @Override public void tick(float deltaTime) { + super.tick(deltaTime); long now = System.currentTimeMillis(); statistics.trackPlayTime(deltaTime); @@ -258,17 +264,29 @@ public class Player extends Entity implements CommandExecutor { * Called by {@link Zone#addEntity(Entity)} when the player is added to it. */ public void onZoneChanged() { - // TODO handle spawns better + // Set spawn location + if(customSpawn) { + x = spawnX; + y = spawnY; + } + MetaBlock spawn = zone.getRandomSpawnBlock(); if(spawn == null) { - x = zone.getWidth() / 2; - y = 2; + spawnX = zone.getWidth() / 2; + spawnY = 2; } else { - x = spawn.getX() + 1; - y = spawn.getY(); + spawnX = spawn.getX() + 1; + spawnY = spawn.getY(); } + if(!customSpawn) { + x = spawnX; + y = spawnY; + } + + customSpawn = false; + // Set skills for new players for(Skill skill : Skill.values()) { if(!skills.containsKey(skill)) { @@ -289,8 +307,6 @@ public class Player extends Entity implements CommandExecutor { inventory.moveItemToContainer(jetpack, ContainerType.ACCESSORIES, 0); } - spawnPoint.setX((int)x); - spawnPoint.setY((int)y); sendMessage(new ConfigurationMessage(id, getClientConfig(), GameConfiguration.getClientConfig(this), zone.getClientConfig(this))); sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); @@ -416,7 +432,14 @@ public class Player extends Entity implements CommandExecutor { } public void changeZone(Zone zone) { + changeZone(zone, -1, -1); + } + + public void changeZone(Zone zone, int x, int y) { nextZone = zone; + spawnX = x; + spawnY = y; + customSpawn = x != -1 && y != -1; sendMessage(new EventMessage("playerWillChangeZone", null)); kick("Teleporting...", true); } @@ -506,11 +529,9 @@ public class Player extends Entity implements CommandExecutor { setHealth(getMaxHealth()); } - int x = spawnPoint.getX(); - int y = spawnPoint.getY(); - sendMessage(new PlayerPositionMessage(x, y)); + sendMessage(new PlayerPositionMessage(spawnX, spawnY)); sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.REVIVED)); - zone.sendMessage(new EffectMessage(x, y, "spawn", 20)); + zone.sendMessage(new EffectMessage(spawnX, spawnY, "spawn", 20)); } /** @@ -605,6 +626,8 @@ public class Player extends Entity implements CommandExecutor { if(lastPlacement != null) { if(item.hasUse(ItemUseType.SWITCHED) && !item.hasUse(ItemUseType.SWITCH)) { linked = tryLinkSwitchedItem(x, y, item); + } else if(item.hasUse(ItemUseType.TRANSMITTED)) { + linked = tryLinkTransmittedItem(x, y, item); } } @@ -619,7 +642,7 @@ public class Player extends Entity implements CommandExecutor { Item pItem = lastPlacement.getItem(); boolean linked = false; - if(pItem.hasUse(ItemUseType.SWITCH)) { + if(pItem.hasUse(ItemUseType.SWITCH, ItemUseType.TRIGGER)) { MetaBlock metaBlock = zone.getMetaBlock(pX, pY); Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); @@ -642,6 +665,39 @@ public class Player extends Entity implements CommandExecutor { return linked; } + private boolean tryLinkTransmittedItem(int x, int y, Item item) { + int pX = lastPlacement.getX(); + int pY = lastPlacement.getY(); + Item pItem = lastPlacement.getItem(); + + // Do nothing if the last placed item is not a transmitter + if(!pItem.hasUse(ItemUseType.TRANSMIT)) { + return false; + } + + int maxTransmitDistance = getTotalSkillLevel(Skill.ENGINEERING) * 10; + + // Notify the player if the distance is beyond the maximum transmit distance + if(!isGodMode() && !MathUtils.inRange(x, y, pX, pY, maxTransmitDistance)) { + notify(String.format("You can only transmit %s blocks at your current engineering level.", maxTransmitDistance)); + return false; + } + + MetaBlock metaBlock = zone.getMetaBlock(pX, pY); + Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); + + // Do nothing if metadata is null for whatever reason + if(metadata == null) { + return false; + } + + // Link transmitter to beacon + MapHelper.appendList(metadata, ">", Arrays.asList(x, y)); // Make it a list for compatibility reasons + zone.updateBlock(pX, pY, Layer.FRONT, pItem, 1, null, metadata); + lastPlacement = null; + return true; + } + public double getMiningRange() { return 5 + getTotalSkillLevel(Skill.MINING) / 3.0; } @@ -662,6 +718,13 @@ public class Player extends Entity implements CommandExecutor { return bonus.getChance() * (getTotalSkillLevel(bonus.getSkill()) / (double)MAX_SKILL_LEVEL) * heldItem.getToolBonus(); } + /** + * @return The hash to be stored in blocks placed by this player. + */ + public int getBlockHash() { + return 1 + ((documentId.hashCode() & 2047) % 2047); + } + public String getDocumentId() { return documentId; } @@ -700,6 +763,18 @@ public class Player extends Entity implements CommandExecutor { return authTokens; } + public void addLootCode(String lootCode) { + lootCodes.add(lootCode); + } + + public boolean hasLootCode(String lootCode) { + return lootCodes.contains(lootCode); + } + + public Set getLootCodes() { + return Collections.unmodifiableSet(lootCodes); + } + public void trackNameChange(String newName) { nameChanges.add(new NameChange(newName, name)); } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java index 76c6443..e786466 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java @@ -34,6 +34,7 @@ public class PlayerConfigFile { private List nameChanges = new ArrayList<>(); private List mutes = new ArrayList<>(); private List bans = new ArrayList<>(); + private Set lootCodes = new HashSet<>(); private Set achievements = new HashSet<>(); private Map ignoredHints = new HashMap<>(); private Map skills = new HashMap<>(); @@ -57,6 +58,7 @@ public class PlayerConfigFile { this.nameChanges = player.getNameChanges(); this.mutes = player.getMutes(); this.bans = player.getBans(); + this.lootCodes = player.getLootCodes(); this.achievements = player.getAchievements(); this.ignoredHints = player.getIgnoredHints(); this.skills = player.getSkills(); @@ -135,6 +137,11 @@ public class PlayerConfigFile { return statistics; } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Set getLootCodes() { + return lootCodes; + } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) public Set getAchievements() { return achievements; diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java index fb00377..2014db1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java @@ -16,6 +16,7 @@ import brainwine.gameserver.dialog.DialogType; import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.util.Pair; import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.util.WeightedMap; // TODO I don't like some parts of this, maybe they can be reworked. @JsonIgnoreProperties(ignoreUnknown = true) @@ -77,6 +78,9 @@ public class Item { @JsonProperty("power") private float power; + @JsonProperty("toughness") + private float toughness; + @JsonProperty("earthy") private boolean earthy; @@ -95,6 +99,9 @@ public class Item { @JsonProperty("custom_place") private boolean customPlace; + @JsonProperty("field_place") + private boolean fieldPlace; + @JsonProperty("base") private boolean base; @@ -149,6 +156,15 @@ public class Item { @JsonProperty("damage") private Pair damageInfo; + @JsonProperty("timer") + private Pair timer; + + @JsonProperty("timer_delay") + private int timerDelay; + + @JsonProperty("timer_mine") + private boolean processTimerOnBreak; + @JsonProperty("ingredients") private List craftingIngredients = new ArrayList<>(); @@ -161,6 +177,9 @@ public class Item { @JsonProperty("convert") private Map conversions = new HashMap<>(); + @JsonProperty("spawn_entity") + private WeightedMap entitySpawns = new WeightedMap<>(); + @JsonCreator private Item(@JsonProperty(value = "id", required = true) String id, @JsonProperty(value = "code", required = true) int code) { @@ -315,6 +334,10 @@ public class Item { return power; } + public float getToughness() { + return toughness; + } + public boolean isEarthy() { return earthy; } @@ -343,6 +366,10 @@ public class Item { return customPlace; } + public boolean canPlaceInField() { + return fieldPlace; + } + public boolean isWhole() { return whole; } @@ -439,6 +466,26 @@ public class Item { return isWeapon() ? damageInfo.getLast() : 0; } + public boolean hasTimer() { + return timer != null; + } + + public String getTimerType() { + return hasTimer() ? timer.getFirst() : null; + } + + public int getTimerValue() { + return hasTimer() ? timer.getLast() : 0; + } + + public int getTimerDelay() { + return timerDelay; + } + + public boolean shouldProcessTimerOnBreak() { + return processTimerOnBreak; + } + public boolean isCraftable() { return !craftingIngredients.isEmpty(); } @@ -476,4 +523,12 @@ public class Item { public Map getConversions() { return conversions.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey().get(), entry -> entry.getValue().get())); } + + public boolean hasEntitySpawns() { + return !entitySpawns.isEmpty(); + } + + public WeightedMap getEntitySpawns() { + return entitySpawns; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java index f2995fe..7ba2908 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java @@ -3,27 +3,61 @@ package brainwine.gameserver.item; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import brainwine.gameserver.item.interactions.BurstInteraction; +import brainwine.gameserver.item.interactions.ChangeInteraction; +import brainwine.gameserver.item.interactions.ContainerInteraction; +import brainwine.gameserver.item.interactions.DialogInteraction; +import brainwine.gameserver.item.interactions.ItemInteraction; +import brainwine.gameserver.item.interactions.SpawnInteraction; +import brainwine.gameserver.item.interactions.SpawnTeleportInteraction; +import brainwine.gameserver.item.interactions.SwitchInteraction; +import brainwine.gameserver.item.interactions.TargetTeleportInteraction; +import brainwine.gameserver.item.interactions.TeleportInteraction; +import brainwine.gameserver.item.interactions.TransmitInteraction; + +/** + * Much like with {@link Action}, block interactions depend on their use type. + */ public enum ItemUseType { AFTERBURNER, - CONTAINER, - CREATE_DIALOG, - DIALOG, + BURST(new BurstInteraction()), + CONTAINER(new ContainerInteraction()), + CREATE_DIALOG(new DialogInteraction(true)), + DESTROY, + DIALOG(new DialogInteraction(false)), GUARD, - CHANGE, + CHANGE(new ChangeInteraction()), FIELDABLE, FLY, MULTI, + PLENTY, PROTECTED, PUBLIC, - SWITCH, + SPAWN(new SpawnInteraction()), + SPAWN_TELEPORT(new SpawnTeleportInteraction()), + SWITCH(new SwitchInteraction()), SWITCHED, - TELEPORT, + TARGET_TELEPORT(new TargetTeleportInteraction()), + TELEPORT(new TeleportInteraction()), + TRIGGER, + TRANSMIT(new TransmitInteraction()), + TRANSMITTED, ZONE_TELEPORT, @JsonEnumDefaultValue UNKNOWN; - + + private final ItemInteraction interaction; + + private ItemUseType(ItemInteraction interaction) { + this.interaction = interaction; + } + + private ItemUseType() { + this(null); + } + @JsonCreator public static ItemUseType fromId(String id) { String formatted = id.toUpperCase().replace(" ", "_"); @@ -36,4 +70,8 @@ public enum ItemUseType { return UNKNOWN; } + + public ItemInteraction getInteraction() { + return interaction; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java new file mode 100644 index 0000000..ecd1b71 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java @@ -0,0 +1,63 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Map; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.Block; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for items that explode if you get too close + */ +@SuppressWarnings("unchecked") +public class BurstInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if data is invalid + if(!(config instanceof Map)) { + return; + } + + Player player = (Player)entity; + Map configMap = (Map)config; + boolean dodge = MapHelper.getBoolean(configMap, "dodge"); + + // Do nothing if the player is lucky enough :) + if(dodge && Math.random() * Player.MAX_SKILL_LEVEL <= player.getTotalSkillLevel(Skill.AGILITY) / 2.0F) { + return; + } + + boolean natural = MapHelper.getBoolean(configMap, "natural"); + boolean enemy = MapHelper.getBoolean(configMap, "enemy"); + Block block = zone.getBlock(x, y); + + // Check if the block has to be be natural or triggered by an enemy + if((natural && !block.isNatural()) || (enemy && (player.isStealthy() || block.getOwnerHash() == player.getBlockHash()))) { + return; + } + + DamageType damageType = DamageType.fromName(MapHelper.getString(configMap, "damage_type")); + String effect = MapHelper.getString(configMap, "effect", "bomb"); + float range = MapHelper.getFloat(configMap, "range"); + float damage = MapHelper.getFloat(configMap, "damage"); + boolean destructive = MapHelper.getBoolean(configMap, "destructive"); + + // Create explosion and destroy block + zone.explode(x, y, range, player, destructive, damage, damageType, effect); + zone.updateBlock(x, y, layer, 0); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java new file mode 100644 index 0000000..5b07ae8 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java @@ -0,0 +1,26 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for blocks that can change between two states + */ +public class ChangeInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + Player player = (Player)entity; + zone.updateBlock(x, y, layer, item, mod == 0 ? 1 : 0, player); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java new file mode 100644 index 0000000..192db11 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java @@ -0,0 +1,88 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.loot.Loot; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for lootable containers + */ +public class ContainerInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Check if the right data is present + if(metaBlock == null || data != null) { + return; + } + + Player player = (Player)entity; + String dungeonId = metaBlock.getStringProperty("@"); + + // Check if container is protected by a dungeon + if(item.hasUse(ItemUseType.FIELDABLE) && dungeonId != null && zone.isDungeonIntact(dungeonId)) { + player.notify("This container is secured by protectors in the area."); + return; + } + + boolean plenty = item.hasUse(ItemUseType.PLENTY); + String lootCode = metaBlock.getStringProperty("y"); + + // Check loot code + if(plenty) { + if(lootCode == null) { + player.notify("This chest cannot be plundered."); + return; + } + + if(player.hasLootCode(lootCode)) { + player.notify("You've already plundered this chest."); + return; + } + } + + String specialItem = metaBlock.getStringProperty("$"); + + // Award loot + if(specialItem != null) { + if(specialItem.equals("?")) { + Loot loot = metaBlock.hasProperty("l") ? new Loot(Item.get(metaBlock.getStringProperty("l")), metaBlock.getIntProperty("q")) + : GameServer.getInstance().getLootManager().getRandomLoot(player, item.getLootCategories()); + int experience = metaBlock.getIntProperty("xp"); + + if(loot != null) { + if(plenty) { + player.addLootCode(lootCode); + } else { + metaBlock.removeProperty("$"); + metaBlock.removeProperty("xp"); + } + + player.awardLoot(loot, item.getLootGraphic()); + player.addExperience(experience); + player.getStatistics().trackContainerLooted(item); + } else { + player.notify("No eligible loot could be found for this container."); + } + } + } + + // Update container mod + if(!plenty && !metaBlock.hasProperty("$")) { + Player owner = GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); + zone.updateBlock(x, y, Layer.FRONT, item, 0, owner, metaBlock.getMetadata()); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java new file mode 100644 index 0000000..cd999e3 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java @@ -0,0 +1,98 @@ +package brainwine.gameserver.item.interactions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for blocks that may be configured through a dialog + */ +@SuppressWarnings("unchecked") +public class DialogInteraction implements ItemInteraction { + + private boolean creationOnly; + + public DialogInteraction(boolean creationOnly) { + this.creationOnly = creationOnly; + } + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if the required data isn't present + if(data == null || !(config instanceof Map)) { + return; + } + + // Do nothing if this block has already been configured and cannot be re-configured with this interaction + if(creationOnly && metaBlock != null && metaBlock.getBooleanProperty("cd")) { + return; + } + + Player player = (Player)entity; + Map configMap = (Map)config; + String target = MapHelper.getString(configMap, "target", "none"); + + // Do nothing for now if the target isn't the block's metadata + if(!target.equals("meta")) { + player.notify("Sorry, this action isn't implemented yet."); + return; + } + + // Update block metadata + Map metadata = new HashMap<>(); + List> sections = MapHelper.getList(configMap, "sections"); + + if(metaBlock != null) { + metadata.putAll(metaBlock.getMetadata()); + } + + if(sections != null && data.length == sections.size()) { + for(int i = 0; i < sections.size(); i++) { + Map section = sections.get(i); + String key = MapHelper.getString(section, "input.key"); + + if(key != null) { + String text = String.valueOf(data[i]); + + // Get rid of text if player is currently muted + if(player.isMuted() && MapHelper.getBoolean(section, "input.sanitize")) { + text = text.replaceAll(".", "*"); + } + + metadata.put(key, text); + } else if(MapHelper.getBoolean(section, "input.mod")) { + List options = MapHelper.getList(section, "input.options"); + + if(options != null) { + mod = options.indexOf(data[i]); + mod = mod == -1 ? 0 : mod; + mod *= MapHelper.getInt(section, "input.mod_multiple", 1); + zone.updateBlock(x, y, layer, item, mod, player); + } + } + } + } + + // Set configured flag + if(creationOnly) { + metadata.put("cd", true); + } + + // Update meta block + zone.setMetaBlock(x, y, item, player, metadata); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java new file mode 100644 index 0000000..ea95a94 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java @@ -0,0 +1,13 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public interface ItemInteraction { + + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data); +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java new file mode 100644 index 0000000..e7633c7 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java @@ -0,0 +1,39 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityConfig; +import brainwine.gameserver.entity.EntityRegistry; +import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for items that can spawn entities + */ +public class SpawnInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if item can't spawn entities + if(!item.hasEntitySpawns() || mod != 0) { + return; + } + + EntityConfig entityConfig = EntityRegistry.getEntityConfig(item.getEntitySpawns().next()); + + // Do nothing if type is invalid + if(entityConfig == null) { + return; + } + + // Spawn the entity + Npc npc = new Npc(zone, entityConfig); + zone.spawnEntity(npc, x, y); + + // Update block mod + zone.updateBlock(x, y, layer, item, 1); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java new file mode 100644 index 0000000..f8f3c63 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java @@ -0,0 +1,39 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for that one white teleporter in the tutorial world + */ +public class SpawnTeleportInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + Player player = (Player)entity; + + // Find a random suitable zone + Zone targetZone = GameServer.getInstance().getZoneManager().getRandomZone(z -> z.getBiome() == Biome.PLAIN); + + // Notify the player if no zone could be found + if(targetZone == null) { + player.notify("Couldn't find a suitable zone to teleport to. Try again later."); + return; + } + + // Teleport the player to the target zone + player.changeZone(targetZone); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java new file mode 100644 index 0000000..3fb9ff5 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -0,0 +1,215 @@ +package brainwine.gameserver.item.interactions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.text.WordUtils; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityConfig; +import brainwine.gameserver.entity.EntityRegistry; +import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.server.messages.BlockMetaMessage; +import brainwine.gameserver.server.messages.EffectMessage; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.zone.Chunk; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for switches + */ +public class SwitchInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if the required data isn't present + if(data != null || metaBlock == null) { + return; + } + + int timer = metaBlock.getIntProperty("t"); + + // Do nothing if this switch has a timer and is already flipped + if(timer > 0 && mod % 2 == 1) { + return; + } + + // Show configured message to nearby players + String message = metaBlock.getStringProperty("m"); + + if(message != null && !message.isEmpty()) { + float effectX = x + item.getBlockWidth() / 2.0F; + float effectY = y - item.getBlockHeight() + 1; + zone.sendMessageToChunk(new EffectMessage(effectX, effectY, "emote", message), zone.getChunk(x, y)); + } + + // Prepare list of targets + List> positions = MapHelper.getList(metaBlock.getMetadata(), ">", Collections.emptyList()); + List targets = new ArrayList<>(); + targets.add(new Vector2i(x, y)); + positions.stream().map(position -> new Vector2i(position.get(0), position.get(1))).forEach(targets::add); + int switchedMod = mod % 2 == 0 ? mod + 1 : mod - 1; + + // Switch all target blocks + for(Vector2i target : targets) { + switchBlock(zone, entity, target.getX(), target.getY(), switchedMod, metaBlock); + } + + // Create block timer if this is a timed switch + if(timer > 0) { + int unswitchedMod = switchedMod % 2 == 0 ? switchedMod + 1 : switchedMod - 1; + + zone.addBlockTimer(x, y, timer * 1000, () -> { + for(Vector2i target : targets) { + switchBlock(zone, entity, target.getX(), target.getY(), unswitchedMod, metaBlock); + } + }); + } + } + + private void switchBlock(Zone zone, Entity entity, int x, int y, int mod, MetaBlock switchMeta) { + // Do nothing if the target chunk isn't loaded + if(!zone.isChunkLoaded(x, y)) { + return; + } + + MetaBlock metaBlock = zone.getMetaBlock(x, y); + + // Do nothing if there is no metadata + if(metaBlock == null) { + return; + } + + Player owner = metaBlock == null ? null : GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); + Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); + Item item = metaBlock.getItem(); + Object config = item.getUse(ItemUseType.SWITCHED); + + if(config instanceof String) { + String type = (String)config; + + // Not the prettiest way to do this but it will have to do. + switch(type.toLowerCase()) { + case "spawner": switchSpawner(zone, metaBlock); break; + case "exploder": switchExploder(zone, entity, metaBlock); break; + case "messagesign": switchSign(zone, entity, metaBlock, switchMeta); break; + default: break; + } + } else if(item.hasUse(ItemUseType.SWITCH, ItemUseType.SWITCHED, ItemUseType.TRIGGER)) { + zone.updateBlock(x, y, Layer.FRONT, item, mod, owner, metadata); + } + } + + private void switchSpawner(Zone zone, MetaBlock metaBlock) { + // Kill existing entity + if(metaBlock.hasProperty("eid")) { + Entity entity = zone.getEntity(metaBlock.getIntProperty("eid")); + + if(entity != null && !entity.isDead()) { + entity.setHealth(0); + zone.sendMessageToChunk(new EffectMessage(entity.getX(), entity.getY(), "bomb-teleport", 4), zone.getChunk(metaBlock.getX(), metaBlock.getY())); + } + } + + Object config = metaBlock.getItem().getUse(ItemUseType.SPAWN); + + // Do nothing if use config data is invalid + if(!(config instanceof Map)) { + return; + } + + // Determine entity type + String entityType = MapHelper.getString((Map)config, metaBlock.getStringProperty("e")); + EntityConfig entityConfig = EntityRegistry.getEntityConfig(entityType); + + // Do nothing if entity config doesn't exist + if(entityConfig == null) { + return; + } + + // Create & spawn the entity + Npc npc = new Npc(zone, entityConfig); + npc.setArtificial(true); + zone.spawnEntity(npc, metaBlock.getX(), metaBlock.getY(), true); + + // Track entity id in spawner metadata + metaBlock.setProperty("eid", npc.getId()); + } + + // TODO exploders were used to create lag machines back in the day, so maybe we should put a cooldown on this + private void switchExploder(Zone zone, Entity entity, MetaBlock metaBlock) { + String type = metaBlock.getStringProperty("e"); + + // Do nothing if explosion type doesn't exist in block metadata + if(type == null) { + return; + } + + // Create explosion + DamageType damageType = DamageType.fromName(type); + String effect = String.format("bomb-%s", type.toLowerCase()); + zone.explode(metaBlock.getX(), metaBlock.getY(), 6, entity, false, 6, damageType, effect); + } + + private void switchSign(Zone zone, Entity entity, MetaBlock metaBlock, MetaBlock switchMeta) { + String message = switchMeta.hasProperty("m") ? switchMeta.getStringProperty("m").trim() : ""; + boolean lock = metaBlock.getStringProperty("lock").equalsIgnoreCase("yes"); + Item item = metaBlock.getItem(); + + // Check and update lock status + if(lock) { + boolean locked = metaBlock.getBooleanProperty("locked"); + + if(!message.isEmpty()) { + if(locked) { + return; + } + + metaBlock.setProperty("locked", true); + } else if(locked) { + metaBlock.removeProperty("locked"); + } + } + + // Update sign text + String name = entity.getName(); + + if(name != null) { + message = message.replaceAll("%t%", name); + } + + String separator = "\n"; + String[] keys = {"t1", "t2", "t3", "t4"}; + String[] segments = WordUtils.wrap(message, 20, separator, true).split(separator, 4); + + for(int i = 0; i < keys.length; i++) { + String key = keys[i]; + String text = i < segments.length ? segments[i] : ""; + int separatorIndex = text.lastIndexOf(separator); + + if(separatorIndex != -1) { + text = text.substring(0, separatorIndex); + } + + metaBlock.setProperty(key, text); + } + + // Send data to players + float effectX = metaBlock.getX() + (float)item.getBlockWidth() / 2; + float effectY = metaBlock.getY() - (float)item.getBlockHeight() / 2 + 1; + Chunk chunk = zone.getChunk(metaBlock.getX(), metaBlock.getY()); + zone.sendMessageToChunk(new EffectMessage(effectX, effectY, "area steam", 10), chunk); + zone.sendMessageToChunk(new BlockMetaMessage(metaBlock), chunk); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java new file mode 100644 index 0000000..01f8033 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java @@ -0,0 +1,102 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public class TargetTeleportInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if data is invalid + if(data != null) { + return; + } + + Player player = (Player)entity; + Zone targetZone = zone; + String zoneName = metaBlock.getStringProperty("pz"); + + // Validate target zone + if(zoneName != null) { + targetZone = GameServer.getInstance().getZoneManager().getZoneByName(zoneName); + + if(targetZone == null) { + player.notify(String.format("Cannot locate world '%s', please recalibrate.", zoneName)); + return; + } + } + + // Parse target position + int targetX = -1; + int targetY = metaBlock.getIntProperty("py") + (targetZone.getBiome() == Biome.DEEP ? -1000 : 200); + int centerX = zone.getWidth() / 2; + + try { + String strX = metaBlock.getStringProperty("px"); + + if(strX != null) { + if(strX.endsWith("w")) { + targetX = centerX - Integer.parseInt(strX.replace("w", "")); + } else { + targetX = centerX + Integer.parseInt(strX.replace("e", "")); + } + } + } catch(NumberFormatException e) { + // Discard silently + } + + // Do nothing if target is out of bounds + if(!targetZone.areCoordinatesInBounds(targetX, targetY)) { + player.notify("Cannot locate destination, please recalibrate."); + return; + } + + // Do nothing if target location is unexplored + if(!player.isGodMode() && !targetZone.isAreaExplored(targetX, targetY)) { + player.notify("That area hasn't been explored yet."); + return; + } + + // Do nothing if target location is protected + if(!player.isGodMode() && targetZone.isBlockProtected(targetX, targetY, player)) { + player.notify("That area is protected."); + return; + } + + // Teleport the player to the target location + if(targetZone == zone) { + player.teleport(targetX, targetY); + } else { + // Create confirmation dialog + Dialog dialog = new Dialog() + .setActions("yesno") + .addSection(new DialogSection() + .setTitle("Attention") + .setText(String.format("Teleport to world '%s'?", targetZone.getName()))); + + // Show confirmation dialog for zone change + Zone _targetZone = targetZone; + int _targetX = targetX; + int _targetY = targetY; + player.showDialog(dialog, input -> { + if(input.length == 1 && input[0].equals("Yes")) { + player.changeZone(_targetZone, _targetX, _targetY); + } + }); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java new file mode 100644 index 0000000..70989e2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java @@ -0,0 +1,56 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.NotificationType; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for teleporters + */ +public class TeleportInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + Player player = (Player)entity; + + // Try to repair teleporter + if(mod == 0) { + zone.updateBlock(x, y, layer, item, 1); + player.getStatistics().trackDiscovery(item); + player.notify("You repaired a teleporter!", NotificationType.ACCOMPLISHMENT); + player.notifyPeers(String.format("%s repaired a teleporter.", player.getName()), NotificationType.SYSTEM); + return; + } + + // Verify data + if(data == null || data.length != 2 || mod != 1) { + return; + } + + int targetX = data[0] instanceof Integer ? (int)data[0] : -1; + int targetY = data[1] instanceof Integer ? (int)data[1] : -1; + MetaBlock targetMeta = zone.getMetaBlock(targetX, targetY); + + // Do nothing if target has no metadata + if(targetMeta == null) { + return; + } + + // Teleport the player if the target location is valid + if((targetMeta.getItem().hasUse(ItemUseType.TELEPORT) && zone.getBlock(targetX, targetY).getFrontMod() == 1) + || targetMeta.getItem().hasUse(ItemUseType.ZONE_TELEPORT)) { + player.teleport(targetX + 1, targetY); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java new file mode 100644 index 0000000..20bbc94 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java @@ -0,0 +1,58 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Collections; +import java.util.List; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for target teleporters + */ +public class TransmitInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if the required data isn't present + if(data != null || metaBlock == null) { + return; + } + + Player player = (Player)entity; + List> positions = MapHelper.getList(metaBlock.getMetadata(), ">", Collections.emptyList()); + + // Do nothing if there is no linked position + if(positions.isEmpty()) { + return; + } + + List position = positions.get(0); + int targetX = position.get(0); + int targetY = position.get(1); + + // Make sure that the target location is in bounds + if(!zone.areCoordinatesInBounds(targetX, targetY)) { + return; + } + + // Notify the player if the target beacon is missing + if(!zone.getBlock(targetX, targetY).getFrontItem().hasUse(ItemUseType.TRANSMITTED)) { + player.notify("There is no beacon at the target location."); + return; + } + + player.teleport(targetX, targetY); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java b/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java index f1a3900..90f7bbf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java +++ b/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java @@ -28,6 +28,13 @@ public class Loot { @JsonCreator private Loot() {} + /** + * Arbitrary constructor for chests o' plenty + */ + public Loot(Item item, int quantity) { + this.items.put(item, quantity); + } + public Map getItems() { return items; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java index ec3fc2c..adae916 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java @@ -10,12 +10,12 @@ public class EffectMessage extends Message { public int x; public int y; public String name; - public int count; + public Object data; - public EffectMessage(float x, float y, String name, int count) { + public EffectMessage(float x, float y, String name, Object data) { this.x = (int)(x * Entity.POSITION_MODIFIER); this.y = (int)(y * Entity.POSITION_MODIFIER); this.name = name; - this.count = count; + this.data = data; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java index 777f5d6..3c80eec 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java @@ -5,6 +5,10 @@ import java.util.List; import java.util.Map; import brainwine.gameserver.annotations.RequestInfo; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityConfig; +import brainwine.gameserver.entity.EntityRegistry; +import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Skill; @@ -17,6 +21,7 @@ import brainwine.gameserver.item.MiningBonus; import brainwine.gameserver.item.ModType; import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.messages.BlockChangeMessage; +import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.server.messages.InventoryMessage; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MathUtils; @@ -125,6 +130,34 @@ public class BlockMineRequest extends PlayerRequest { } } + if(item.shouldProcessTimerOnBreak()) { + zone.processBlockTimer(x, y); + } + + // Pretty much only used for spawners + if(item.hasUse(ItemUseType.DESTROY)) { + Object config = item.getUse(ItemUseType.DESTROY); + + if(config instanceof String) { + String type = (String)config; + + switch(type.toLowerCase()) { + case "spawner": destroySpawner(zone, metaBlock); break; + default: break; + } + } + } + + // Check for entity spawns + if(item.hasEntitySpawns() && block.getMod(layer) == 0 && !item.hasTimer() && !item.hasUse(ItemUseType.SPAWN)) { + EntityConfig type = EntityRegistry.getEntityConfig(item.getEntitySpawns().next()); + + if(type != null) { + Npc npc = new Npc(zone, type); + zone.spawnEntity(npc, x, y); + } + } + zone.updateBlock(x, y, layer, 0, 0, player); player.getStatistics().trackItemMined(item); Item inventoryItem = item.getMod() == ModType.DECAY && block.getMod(layer) > 0 ? item.getDecayInventoryItem() : item.getInventoryItem(); @@ -151,6 +184,21 @@ public class BlockMineRequest extends PlayerRequest { } } + private void destroySpawner(Zone zone, MetaBlock metaBlock) { + // Do nothing if spawner doesn't have an entity + if(!metaBlock.hasProperty("eid")) { + return; + } + + Entity entity = zone.getEntity(metaBlock.getIntProperty("eid")); + + // Kill entity if it exists + if(entity != null && !entity.isDead()) { + entity.setHealth(0); + zone.sendMessageToChunk(new EffectMessage(entity.getX(), entity.getY(), "bomb-teleport", 4), zone.getChunk(metaBlock.getX(), metaBlock.getY())); + } + } + private void fail(Player player, String reason) { player.notify(reason); Block block = player.getZone().getBlock(x, y); diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java index d74fbca..c6cd015 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java @@ -1,8 +1,14 @@ package brainwine.gameserver.server.requests; +import java.util.UUID; + import brainwine.gameserver.annotations.RequestInfo; +import brainwine.gameserver.entity.EntityConfig; +import brainwine.gameserver.entity.EntityRegistry; +import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.ModType; @@ -12,6 +18,7 @@ import brainwine.gameserver.server.messages.InventoryMessage; import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.Pair; import brainwine.gameserver.zone.Block; +import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; @RequestInfo(id = 12) @@ -56,7 +63,7 @@ public class BlockPlaceRequest extends PlayerRequest { return; } - if(!player.isGodMode() && zone.isBlockProtected(x, y, player)) { + if(!player.isGodMode() && !item.canPlaceInField() && zone.isBlockProtected(x, y, player)) { fail(player, "This block is protected."); return; } @@ -100,18 +107,108 @@ public class BlockPlaceRequest extends PlayerRequest { player.getStatistics().trackItemPlaced(); player.trackPlacement(x, y, item); + // Create block timer if applicable + if(item.hasTimer()) { + createBlockTimer(zone, player); + } + // Process custom place if applicable if(item.hasCustomPlace()) { - processCustomPlace(player); + processCustomPlace(zone, player); } } - private void processCustomPlace(Player player) { - Zone zone = player.getZone(); + private void createBlockTimer(Zone zone, Player player) { + String type = item.getTimerType(); + int value = item.getTimerValue(); + Runnable task = null; + switch(type) { + case "front mod": + task = () -> zone.updateBlock(x, y, layer, item, value); + break; + case "bomb": + task = () -> zone.explode(x, y, value, player, true, value, DamageType.FIRE, value >= 6 ? "bomb-large" : "bomb"); + break; + case "bomb-fire": + task = () -> zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-fire"); + break; + case "bomb-electric": + task = () -> zone.explode(x, y, value, player, false, value, DamageType.ENERGY, "bomb-electric"); + break; + case "bomb-frost": + task = () -> zone.explode(x, y, value, player, false, value, DamageType.COLD, "bomb-frost"); + break; + case "bomb-dig": + task = () -> { + zone.explode(x, y, value, player, "bomb-fire"); + int distance = value * 10; + + // Dig until we reach the maximum distance or hit a solid block + for(int i = 1; i <= distance; i++) { + if(!zone.digBlock(x, y + i)) { + break; + } + } + }; + break; + case "bomb-spawner": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-fire"); + + // Spawn a bunch of entities + for(int i = 0; i < value; i++) { + EntityConfig entityType = EntityRegistry.getEntityConfig(item.getEntitySpawns().next()); + + if(entityType != null) { + Npc npc = new Npc(zone, entityType); + zone.spawnEntity(npc, x, y); + } + } + }; + break; + case "bomb-water": + case "bomb-acid": + case "bomb-lava": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-large"); + Item liquid = Item.get(String.format("liquid/%s", type.replace("bomb-", ""))); + int range = 4; + + // Do nothing if there is no liquid to place + if(liquid.isAir()) { + return; + } + + // Place liquid blocks around the explosion + for(int i = x - range; i <= x + range; i++) { + for(int j = y - range; j <= y + range; j++) { + // Skip if not in range + if(!MathUtils.inRange(x, y, i, j, range)) { + continue; + } + + // Place liquid if target block isn't solid + if(!zone.isBlockSolid(i, j, true)) { + zone.updateBlock(i, j, Layer.LIQUID, liquid, 5); + } + } + } + }; + break; + default: + break; + } + + if(task != null) { + zone.addBlockTimer(x, y, item.getTimerDelay() * 1000, task); + } + } + + private void processCustomPlace(Zone zone, Player player) { switch(item.getId()) { - // See if we can plug a maw or pipe case "building/plug": + // See if we can plug a maw or pipe Item baseItem = zone.getBlock(x, y).getBaseItem(); String plugged = baseItem.hasId("base/maw") ? "base/maw-plugged" : baseItem.hasId("base/pipe") ? "base/pipe-plugged" : null; @@ -122,6 +219,16 @@ public class BlockPlaceRequest extends PlayerRequest { player.getStatistics().trackMawPlugged(); } + break; + case "containers/chest-plenty": + case "containers/sack-plenty": + // Create additional metadata for chests o' plenty + MetaBlock metaBlock = zone.getMetaBlock(x, y); + + if(metaBlock != null) { + metaBlock.setProperty("y", UUID.randomUUID().toString()); // Generate random loot code + metaBlock.setProperty("$", "?"); + } break; // No valid item; do nothing default: break; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java index 6f4a00c..dd669bb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java @@ -1,27 +1,20 @@ package brainwine.gameserver.server.requests; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; import brainwine.gameserver.GameServer; import brainwine.gameserver.annotations.OptionalField; import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; -import brainwine.gameserver.loot.Loot; +import brainwine.gameserver.item.interactions.ItemInteraction; import brainwine.gameserver.server.PlayerRequest; -import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.zone.Block; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; -@SuppressWarnings("unchecked") @RequestInfo(id = 21) public class BlockUseRequest extends PlayerRequest { @@ -36,10 +29,17 @@ public class BlockUseRequest extends PlayerRequest { public void process(Player player) { Zone zone = player.getZone(); + // Do nothing if player is dead or if the target chunk is not active if(player.isDead() || !player.isChunkActive(x, y)) { return; } + // Do nothing if player is too far away + if(!player.isGodMode() && !player.inRange(x, y, player.getMiningRange())) { + return; + } + + // Transform usage data if necessary if(data != null && data.length == 1 && data[0] instanceof Map) { data = ((Map)data[0]).values().toArray(); } @@ -49,6 +49,7 @@ public class BlockUseRequest extends PlayerRequest { Item item = block.getItem(layer); int mod = block.getMod(layer); + // Check if block is owned by another player if(metaBlock != null && item.hasUse(ItemUseType.PROTECTED)) { Player owner = GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); @@ -56,154 +57,28 @@ public class BlockUseRequest extends PlayerRequest { if(item.hasUse(ItemUseType.PUBLIC)) { String publicUse = item.getUse(ItemUseType.PUBLIC).toString(); + // TODO implement other cases switch(publicUse) { case "owner": player.notify(String.format("This %s is owned by %s.", - item.getTitle().toLowerCase(), owner == null ? "nobody.." : owner.getName())); + item.getTitle().toLowerCase(), owner == null ? "somebody else" : owner.getName())); break; } } else { player.notify("Sorry, that belongs to somebody else."); - return; } + + return; } } - for(Entry entry : item.getUses().entrySet()) { - ItemUseType use = entry.getKey(); - Object value = entry.getValue(); + // Try to interact with the block + item.getUses().forEach((use, config) -> { + ItemInteraction interaction = use.getInteraction(); - switch(use) { - case DIALOG: - case CREATE_DIALOG: - if(data != null && value instanceof Map) { - Map config = (Map)value; - String target = MapHelper.getString(config, "target", "none"); - - switch(target) { - case "meta": - Map metadata = new HashMap<>(); - List> sections = MapHelper.getList(config, "sections"); - - if(sections != null && data.length == sections.size()) { - for(int i = 0; i < sections.size(); i++) { - Map section = sections.get(i); - String key = MapHelper.getString(section, "input.key"); - - if(key != null) { - String text = String.valueOf(data[i]); - - // Get rid of text if player is currently muted - if(player.isMuted() && MapHelper.getBoolean(section, "input.sanitize")) { - text = text.replaceAll(".", "*"); - } - - metadata.put(key, text); - } else if(MapHelper.getBoolean(section, "input.mod")) { - List options = MapHelper.getList(section, "input.options"); - - if(options != null) { - mod = options.indexOf(data[i]); - mod = mod == -1 ? 0 : mod; - mod *= MapHelper.getInt(section, "input.mod_multiple", 1); - zone.updateBlock(x, y, layer, item, mod, player); - } - } - } - } - - // TODO find out what this is for - if(use == ItemUseType.CREATE_DIALOG) { - metadata.put("cd", true); - } - - zone.setMetaBlock(x, y, item, player, metadata); - break; - } - } - break; - case CHANGE: - zone.updateBlock(x, y, layer, item, mod == 0 ? 1 : 0, player); - break; - case CONTAINER: - if(metaBlock != null) { - Map metadata = metaBlock.getMetadata(); - String specialItem = MapHelper.getString(metadata, "$"); - - if(specialItem != null) { - String dungeonId = MapHelper.getString(metadata, "@"); - - if(dungeonId != null && item.hasUse(ItemUseType.FIELDABLE) && zone.isDungeonIntact(dungeonId)) { - player.notify("This container is secured by protectors in the area."); - break; - } - - if(specialItem.equals("?")) { - Loot loot = GameServer.getInstance().getLootManager().getRandomLoot(player, item.getLootCategories()); - - if(loot == null) { - player.notify("No eligible loot could be found for this container."); - } else { - metadata.remove("$"); - player.awardLoot(loot, item.getLootGraphic()); - player.getStatistics().trackContainerLooted(item); - } - } else { - player.notify("Sorry, this container can't be looted right now."); - } - - if(mod != 0) { - zone.updateBlock(x, y, Layer.FRONT, item, 0); - } - } - } - break; - case TELEPORT: - if(data != null && mod == 1 && data.length == 2 && data[0] instanceof Integer && data[1] instanceof Integer) { - int tX = (int)data[0]; - int tY = (int)data[1]; - MetaBlock target = zone.getMetaBlock(tX, tY); - - if(target != null && target.getItem().hasUse(ItemUseType.TELEPORT, ItemUseType.ZONE_TELEPORT)) { - player.teleport(tX + 1, tY); - } - } else if(mod == 0) { - zone.updateBlock(x, y, layer, item, 1); - player.getStatistics().trackDiscovery(item); - player.notify("You repaired a teleporter!", NotificationType.ACCOMPLISHMENT); - player.notifyPeers(String.format("%s repaired a teleporter.", player.getName()), NotificationType.SYSTEM); - } - break; - case SWITCH: - if(data == null) { - if(metaBlock != null) { - // TODO timed switches - Map metadata = metaBlock.getMetadata(); - List> positions = MapHelper.getList(metadata, ">", Collections.emptyList()); - zone.updateBlock(x, y, layer, item, mod % 2 == 0 ? mod + 1 : mod - 1, player, metadata); - - for(List position : positions) { - int sX = position.get(0); - int sY = position.get(1); - Block target = zone.getBlock(sX, sY); - - if(target != null) { - Item switchedItem = target.getFrontItem(); - - if(switchedItem.hasUse(ItemUseType.SWITCHED)) { - if(!(item.getUse(ItemUseType.SWITCHED) instanceof String)) { - int switchedMod = target.getFrontMod(); - zone.updateBlock(sX, sY, Layer.FRONT, switchedItem, switchedMod % 2 == 0 ? switchedMod + 1 : switchedMod - 1, null); - } - } - } - } - } - } - break; - default: - break; + if(interaction != null) { + interaction.interact(zone, player, x, y, layer, item, mod, metaBlock, config, data); } - } + }); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java b/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java index f7d68e7..508c356 100644 --- a/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java +++ b/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java @@ -34,7 +34,11 @@ public class MathUtils { return clamp(value, 0.0F, 1.0F); } + public static double distance(double x, double y, double x2, double y2) { + return Math.hypot(x - x2, y - y2); + } + public static boolean inRange(double x, double y, double x2, double y2, double range) { - return Math.hypot(x - x2, y - y2) <= range; + return distance(x, y, x2, y2) <= range; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Block.java b/gameserver/src/main/java/brainwine/gameserver/zone/Block.java index 5ca3c8c..aa71fbc 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Block.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Block.java @@ -4,40 +4,35 @@ import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.Layer; -/** - * TODO store block owners. - */ public class Block { private Item baseItem; private Item backItem; - private int backMod; + private byte backMod; private Item frontItem; - private int frontMod; + private byte frontMod; private Item liquidItem; - private int liquidMod; + private byte liquidMod; + private short ownerHash; public Block() { - this(0, 0, 0, 0, 0, 0, 0); + this(0, 0, 0, 0, 0, 0, 0, 0); } public Block(int base, int back, int front) { - this(base & 15, back & 65535, back >> 16 & 31, front & 65535, front >> 16 & 31, base >> 8 & 255, base >> 16 & 31); + this(base & 15, back & 65535, back >> 16 & 31, front & 65535, front >> 16 & 31, base >> 8 & 255, base >> 16 & 31, front >> 21 & 2047); } - public Block(int baseItem, int backItem, int backMod, int frontItem, int frontMod, int liquidItem, int liquidMod) { + public Block(int baseItem, int backItem, int backMod, int frontItem, int frontMod, int liquidItem, int liquidMod, int ownerHash) { this(ItemRegistry.getItem(baseItem), ItemRegistry.getItem(backItem), backMod, - ItemRegistry.getItem(frontItem), frontMod, ItemRegistry.getItem(liquidItem), liquidMod); + ItemRegistry.getItem(frontItem), frontMod, ItemRegistry.getItem(liquidItem), liquidMod, ownerHash); } - public Block(Item baseItem, Item backItem, int backMod, Item frontItem, int frontMod, Item liquidItem, int liquidMod) { - this.baseItem = baseItem; - this.backItem = backItem; - this.backMod = backMod; - this.frontItem = frontItem; - this.frontMod = frontMod; - this.liquidItem = liquidItem; - this.liquidMod = liquidMod; + public Block(Item baseItem, Item backItem, int backMod, Item frontItem, int frontMod, Item liquidItem, int liquidMod, int ownerHash) { + updateLayer(Layer.BASE, baseItem, 0, ownerHash); + updateLayer(Layer.BACK, backItem, backMod, ownerHash); + updateLayer(Layer.FRONT, frontItem, frontMod, ownerHash); + updateLayer(Layer.LIQUID, liquidItem, liquidMod, ownerHash); } public void updateLayer(Layer layer, int item) { @@ -45,7 +40,11 @@ public class Block { } public void updateLayer(Layer layer, int item, int mod) { - updateLayer(layer, ItemRegistry.getItem(item), mod); + updateLayer(layer, item, mod, 0); + } + + public void updateLayer(Layer layer, int item, int mod, int owner) { + updateLayer(layer, ItemRegistry.getItem(item), mod, owner); } public void updateLayer(Layer layer, Item item) { @@ -53,21 +52,26 @@ public class Block { } public void updateLayer(Layer layer, Item item, int mod) { + updateLayer(layer, item, mod, 0); + } + + public void updateLayer(Layer layer, Item item, int mod, int owner) { switch(layer) { case BASE: baseItem = item; break; case BACK: backItem = item; - backMod = mod; + backMod = (byte)(mod & 31); break; case FRONT: frontItem = item; - frontMod = mod; + frontMod = (byte)(mod & 31); + ownerHash = (short)(item.isAir() ? 0 : owner & 2047); break; case LIQUID: liquidItem = item; - liquidMod = mod; + liquidMod = (byte)(mod & 31); break; default: break; @@ -115,13 +119,13 @@ public class Block { public void setMod(Layer layer, int mod) { switch(layer) { case BACK: - backMod = mod; + backMod = (byte)(mod & 31); break; case FRONT: - frontMod = mod; + frontMod = (byte)(mod & 31); break; case LIQUID: - liquidMod = mod; + liquidMod = (byte)(mod & 31); break; default: break; @@ -170,7 +174,7 @@ public class Block { } public int getFront() { - return frontItem.getCode() | ((frontMod & 31) << 16); + return frontItem.getCode() | ((ownerHash & 2047) << 21) | ((frontMod & 31) << 16); } public Item getLiquidItem() { @@ -180,4 +184,12 @@ public class Block { public int getLiquidMod() { return liquidMod; } + + public boolean isNatural() { + return ownerHash == 0; + } + + public int getOwnerHash() { + return ownerHash; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java b/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java index f16ae05..b2e40ae 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java @@ -2,6 +2,7 @@ package brainwine.gameserver.zone; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -13,6 +14,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; +/** + * I hate this class and everything in it. + */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class MetaBlock { @@ -83,6 +87,60 @@ public class MetaBlock { return item; } + public void setProperty(String key, Object value) { + metadata.put(key, value); + } + + public void removeProperty(String key) { + metadata.remove(key); + } + + public boolean hasProperty(String key) { + return metadata.containsKey(key); + } + + public Object getProperty(String key) { + return metadata.get(key); + } + + public int getIntProperty(String key) { + return tryParse(key, Integer::parseInt, 0); + } + + public float getFloatProperty(String key) { + return tryParse(key, Float::parseFloat, 0.0f); + } + + public boolean getBooleanProperty(String key) { + return Boolean.parseBoolean(String.valueOf(getProperty(key))); + } + + public String getStringProperty(String key) { + Object value = metadata.get(key); + return value != null && value instanceof String ? (String)value : null; + } + + /** + * Generic function for parsing a number from a string. + */ + private T tryParse(String key, Function parseFunction, T def) { + Object value = metadata.get(key); + + if(value == null) { + return def; + } + + T result = def; + + try { + result = parseFunction.apply(String.valueOf(value)); + } catch(NumberFormatException e) { + // Discard silently + } + + return result; + } + public void setMetadata(Map metadata) { this.metadata = metadata; } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 97cedf5..f8cd994 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -2,7 +2,6 @@ package brainwine.gameserver.zone; import java.io.File; import java.time.OffsetDateTime; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -11,7 +10,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Queue; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -24,11 +22,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import brainwine.gameserver.GameServer; +import brainwine.gameserver.Timer; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.player.ChatType; import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Fieldability; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemUseType; @@ -41,6 +42,7 @@ import brainwine.gameserver.server.messages.BlockChangeMessage; import brainwine.gameserver.server.messages.BlockMetaMessage; import brainwine.gameserver.server.messages.ChatMessage; import brainwine.gameserver.server.messages.ConfigurationMessage; +import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.server.messages.LightMessage; import brainwine.gameserver.server.messages.ZoneExploredMessage; import brainwine.gameserver.server.messages.ZoneStatusMessage; @@ -76,8 +78,8 @@ public class Zone { private final WeatherManager weatherManager = new WeatherManager(); private final EntityManager entityManager = new EntityManager(this); private final LiquidManager liquidManager = new LiquidManager(this); - private final Queue digQueue = new ArrayDeque<>(); private final List blockChanges = new ArrayList<>(); + private final List> blockTimers = new ArrayList<>(); private final Set pendingSunlight = new HashSet<>(); private final Map dungeons = new HashMap<>(); private final Map metaBlocks = new HashMap<>(); @@ -143,19 +145,11 @@ public class Zone { } } - if(!digQueue.isEmpty()) { - DugBlock dugBlock = digQueue.peek(); - - if(now >= dugBlock.getTime()) { - digQueue.poll(); - int x = dugBlock.getX(); - int y = dugBlock.getY(); - Block block = getBlock(x, y); - - if(block != null && block.getFrontItem().hasId("ground/earth-dug")) { - updateBlock(x, y, Layer.FRONT, dugBlock.getItem(), dugBlock.getMod()); - } - } + // Process block timers + if(!blockTimers.isEmpty()) { + List> readyTimers = blockTimers.stream().filter(timer -> now >= timer.getTime()).collect(Collectors.toList()); + blockTimers.removeAll(readyTimers); + readyTimers.forEach(Timer::process); } // Send block changes to players who they are relevant to @@ -299,6 +293,123 @@ public class Zone { return all ? coords : null; } + public void explode(int x, int y, float radius, Entity cause, String effect) { + explode(x, y, radius, cause, false, 0, null, effect); + } + + public void explode(int x, int y, float radius, Entity cause, boolean destructive, float damage, DamageType damageType, String effect) { + // Do nothing if the chunk at the target location isn't loaded + if(!isChunkLoaded(x, y)) { + return; + } + + sendMessageToChunk(new EffectMessage(x + 0.5f, y + 0.5f, effect, radius), getChunk(x, y)); + Player player = cause instanceof Player ? (Player)cause : null; + + // Try to destroy the block at the source of the explosion + if(getBlock(x, y).getFrontItem().getFieldability() == Fieldability.FALSE) { + updateBlock(x, y, Layer.FRONT, 0); + + if(destructive && !isBlockProtected(x, y, player)) { + updateBlock(x, y, Layer.BACK, 0); + } + } + + // Destroy blocks within range if the explosion is destructive + if(destructive) { + int rayCount = (int)Math.ceil(radius * 8); + List> rays = new ArrayList<>(); + List affectedBlocks = new ArrayList<>(); + Set processed = new HashSet<>(); + + // Determine the outer points of the blast circle and cast rays to them + for(int i = 0; i < rayCount; i++) { + float rayDistance = (float)(radius * (Math.random() * 0.4F + 0.8F)); + float angle = (float)Math.toRadians(i * (360.0F / rayCount)); + int targetX = (int)(x + rayDistance * Math.sin(angle)); + int targetY = (int)(y + rayDistance * Math.cos(angle)); + rays.add(raycast(x, y, targetX, targetY, true, true, false)); + } + + // Fetch list of field blocks that are within range of the explosion (drastically speeds up the protection check) + Collection fieldBlocksInRange = fieldBlocks.values().stream() + .filter(metaBlock -> MathUtils.inRange(x, y, metaBlock.getX(), metaBlock.getY(), metaBlock.getItem().getField() + radius * 2)) + .collect(Collectors.toList()); + + // Determine which blocks to destroy by figuring out where each ray should stop + for(List ray : rays) { + for(Vector2i position : ray) { + int positionX = position.getX(); + int positionY = position.getY(); + int index = positionY * width + positionX; + + // Skip if block has been processed + if(processed.contains(index)) { + continue; + } + + // Skip if not in bounds + if(!areCoordinatesInBounds(positionX, positionY)) { + break; + } + + Item frontItem = getBlock(positionX, positionY).getFrontItem(); + double distance = MathUtils.distance(x, y, positionX, positionY); + double power = radius - distance; + + // Do not destroy block if it invulnerable or too tough + if(!frontItem.isAir() && (frontItem.isInvulnerable() || frontItem.getToughness() >= power)) { + break; + } + + // Do not destroy block if it is protected + if(isBlockProtected(positionX, positionY, player, fieldBlocksInRange) || frontItem.hasField()) { + // Keep following this ray if the block isn't occupied + if(!frontItem.isWhole()) { + continue; + } + + break; + } + + affectedBlocks.add(position); + processed.add(index); + } + } + + // Sort affected blocks by their distance from the explosion center + affectedBlocks.sort((a, b) -> { + double distanceA = MathUtils.distance(x, y, a.getX(), a.getY()); + double distanceB = MathUtils.distance(x, y, b.getX(), b.getY()); + return distanceA > distanceB ? 1 : distanceB > distanceA ? -1 : 0; + }); + + // Destroy affected blocks + for(Vector2i position : affectedBlocks) { + updateBlock(position.getX(), position.getY(), Layer.FRONT, 0); + updateBlock(position.getX(), position.getY(), Layer.BACK, 0); + } + } + + // Fetch list of nearby entities + List nearbyEntities = getEntitiesInRange(x, y, radius); + + // Damage nearby entities based on their distance from the explosion + for(Entity entity : nearbyEntities) { + // Cast a ray from the explosion to the entity and damage it if it reaches it + if(entity.canSee(x, y)) { + double distance = MathUtils.distance(x, y, entity.getX(), entity.getY()); + entity.damage((float)(damage - distance), player); + } + + // TODO generic entity damaging is not very intuitive and needs to be worked on. + } + } + + public boolean isBlockNatural(int x, int y) { + return areCoordinatesInBounds(x, y) && getBlock(x, y).isNatural(); + } + public boolean isBlockSolid(int x, int y) { return isBlockSolid(x, y, true); } @@ -357,7 +468,11 @@ public class Zone { } public boolean isBlockProtected(int x, int y, Player from) { - for(MetaBlock fieldBlock : fieldBlocks.values()) { + return isBlockProtected(x, y, from, fieldBlocks.values()); + } + + public boolean isBlockProtected(int x, int y, Player from, Collection fieldBlocks) { + for(MetaBlock fieldBlock : fieldBlocks) { Item item = fieldBlock.getItem(); int fX = fieldBlock.getX(); int fY = fieldBlock.getY(); @@ -405,7 +520,7 @@ public class Zone { for(int j = 0; j < height; j++) { int index = j * width + i; Block block = getBlock(x + i, y + j); - blocks[index] = new Block(block.getBaseItem(), block.getBackItem(), block.getBackMod(), block.getFrontItem(), block.getFrontMod(), block.getLiquidItem(), block.getLiquidMod()); + blocks[index] = new Block(block.getBaseItem(), block.getBackItem(), block.getBackMod(), block.getFrontItem(), block.getFrontMod(), block.getLiquidItem(), block.getLiquidMod(), 0); MetaBlock metaBlock = metaBlocks.get(getBlockIndex(x + i, j + y)); if(metaBlock != null) { @@ -655,14 +770,44 @@ public class Zone { return dungeons.containsKey(id); } - public void digBlock(int x, int y) { + public boolean digBlock(int x, int y) { if(!areCoordinatesInBounds(x, y)) { - return; + return false; } Block block = getBlock(x, y); - digQueue.add(new DugBlock(x, y, block.getFrontItem(), block.getFrontMod(), System.currentTimeMillis() + 10000)); + Item item = block.getFrontItem(); + + if(!item.isDiggable()) { + return !item.isWhole(); + } + + int mod = block.getFrontMod(); updateBlock(x, y, Layer.FRONT, "ground/earth-dug"); + addBlockTimer(x, y, 10000, () -> { + if(block.getFrontItem().hasId("ground/earth-dug")) { + updateBlock(x, y, Layer.FRONT, item, mod); + } + }); + + return true; + } + + public void addBlockTimer(int x, int y, long delay, Runnable task) { + removeBlockTimer(x, y); + blockTimers.add(new Timer<>(getBlockIndex(x, y), delay, task)); + } + + public void removeBlockTimer(int x, int y) { + blockTimers.removeIf(timer -> timer.getKey() == getBlockIndex(x, y)); + } + + public void processBlockTimer(int x, int y) { + Timer timer = blockTimers.stream().filter(t -> t.getKey() == getBlockIndex(x, y)).findFirst().orElse(null); + + if(timer != null) { + timer.process(true); + } } public void updateBlock(int x, int y, Layer layer, int item) { @@ -715,7 +860,7 @@ public class Zone { } Chunk chunk = getChunk(x, y); - chunk.getBlock(x, y).updateLayer(layer, item, mod); + chunk.getBlock(x, y).updateLayer(layer, item, mod, owner == null ? 0 : owner.getBlockHash()); // TODO owner hash should get updated on place only!! chunk.setModified(true); // Queue block update if there are players in this zone. @@ -739,6 +884,7 @@ public class Zone { removeMetaBlock(x, y); } + removeBlockTimer(x, y); entityManager.trySpawnBlockEntity(x, y); if(item.isWhole() && y < sunlight[x]) { @@ -1165,6 +1311,10 @@ public class Zone { return chunksExplored[chunkIndex] = true; } + public boolean isAreaExplored(int x, int y) { + return areCoordinatesInBounds(x, y) && chunksExplored[getChunkIndex(x, y)]; + } + public File getDirectory() { return new File("zones", documentId); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java index 9c8e1eb..4398e73 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java @@ -178,8 +178,11 @@ public class ZoneManager { } public Zone getRandomZone() { - List zones = new ArrayList<>(); - zones.addAll(getZones()); + return getRandomZone(null); + } + + public Zone getRandomZone(Predicate predicate) { + List zones = searchZones(predicate); return zones.get((int)(Math.random() * zones.size())); } From ac1069c3a9a4c043f7d832179bc1c993bc9b4d81 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:05:25 +0100 Subject: [PATCH 04/47] Change default deep biome size to 1200x1000 --- .../gameserver/command/commands/GenerateZoneCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java index f2f66ee..6c316aa 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java @@ -19,8 +19,8 @@ public class GenerateZoneCommand extends Command { @Override public void execute(CommandExecutor executor, String[] args) { Biome biome = Biome.getRandomBiome(); - int width = 2000; - int height = 600; + int width = biome == Biome.DEEP ? 1200 : 2000; + int height = biome == Biome.DEEP ? 1000 : 600; int seed = (int)(Math.random() * Integer.MAX_VALUE); if(args.length > 0 && args.length < 2) { From c6779f0f61827d23708e4201fa2c4bf4326eb5d8 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:10:59 +0100 Subject: [PATCH 05/47] Place spawn buildings closer to the top in `fill` terrain types --- .../gameserver/zone/gen/tasks/StructureGeneratorTask.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java index f484ffe..c4aa59c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java @@ -152,7 +152,8 @@ public class StructureGeneratorTask implements GeneratorTask { Prefab spawnBuilding = spawnBuildings.next(ctx.getRandom()); if(filled) { - int y = ctx.getHeight() / 8 + ctx.nextInt(Math.max(1, ctx.nextInt(ctx.getHeight() / 8))); + int min = ctx.getHeight() / 32; + int y = min + ctx.nextInt(Math.max(1, ctx.nextInt(min))); ctx.placePrefab(spawnBuilding, x, y); } else { ctx.placePrefabSurface(spawnBuilding, x); From 3bba0893b5d43d469d4844eed0a3d3c819c79e22 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:33:20 +0100 Subject: [PATCH 06/47] Add layer separator zone generator feature --- .../gameserver/zone/gen/GeneratorConfig.java | 6 +++ .../zone/gen/models/LayerSeparator.java | 51 +++++++++++++++++++ .../zone/gen/tasks/CaveGeneratorTask.java | 7 +-- .../zone/gen/tasks/TerrainGeneratorTask.java | 23 +++++++++ .../resources/defaults/generators/deep.json | 7 +++ .../resources/defaults/generators/hell.json | 7 +++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java index eafb3f1..9fbbd19 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java @@ -15,6 +15,7 @@ import brainwine.gameserver.util.WeightedMap; import brainwine.gameserver.zone.gen.caves.CaveDecorator; import brainwine.gameserver.zone.gen.caves.CaveType; import brainwine.gameserver.zone.gen.models.Deposit; +import brainwine.gameserver.zone.gen.models.LayerSeparator; import brainwine.gameserver.zone.gen.models.OreDeposit; import brainwine.gameserver.zone.gen.models.SpecialStructure; import brainwine.gameserver.zone.gen.models.StoneType; @@ -33,6 +34,7 @@ public class GeneratorConfig { private double dungeonChance = 0.25; private double backgroundAccentChance = 0.033; private double backgroundDrawingChance = 0.001; + private LayerSeparator layerSeparator; private WeightedMap stoneTypes = new WeightedMap<>(); private WeightedMap spawnBuildings = new WeightedMap<>(); private WeightedMap dungeons = new WeightedMap<>(); @@ -86,6 +88,10 @@ public class GeneratorConfig { return backgroundDrawingChance; } + public LayerSeparator getLayerSeparator() { + return layerSeparator; + } + @JsonSetter(value = "stone_types", nulls = Nulls.SKIP) public WeightedMap getStoneTypes() { return stoneTypes; diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java new file mode 100644 index 0000000..8bc144a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java @@ -0,0 +1,51 @@ +package brainwine.gameserver.zone.gen.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import brainwine.gameserver.item.Item; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class LayerSeparator { + + @JsonProperty("item") + private Item item; + + @JsonProperty("min_thickness") + private int minThickness = 3; + + @JsonProperty("max_thickness") + private int maxThickness = 6; + + @JsonProperty("min_amplitude") + private double minAmplitude = 20; + + @JsonProperty("max_amplitude") + private double maxAimplitude = 20; + + @JsonCreator + private LayerSeparator(@JsonProperty(value = "item", required = true) Item item) { + this.item = item; + } + + public Item getItem() { + return item; + } + + public int getMinThickness() { + return minThickness; + } + + public int getMaxThickness() { + return maxThickness; + } + + public double getMinAmplitude() { + return minAmplitude; + } + + public double getMaxAmplitude() { + return maxAimplitude; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java index 40f5868..3921ee7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java @@ -99,11 +99,11 @@ public class CaveGeneratorTask implements GeneratorTask { // Generate a cave wall with a thickness depending on the size of the cave if(asteroids || stoneType != StoneType.DEFAULT) { ctx.updateBlock(x, y, Layer.BASE, stoneType.getBaseItem()); - int checkDistance = asteroids? 5 : 3; + int checkDistance = asteroids ? 5 : 3; for(int i = x - checkDistance; i <= x + checkDistance; i++) { for(int j = y - checkDistance; j <= y + checkDistance; j++) { - if(ctx.inBounds(i, j) && !cells[i][j]) { + if((asteroids ? ctx.isAir(i, j, Layer.FRONT) : ctx.isEarthy(i, j)) && !cells[i][j]) { double maxDistance = asteroids ? 4.5 + ctx.nextDouble() - 1 : MathUtils.clamp(cave.getSize() / 16.0, 1.8, checkDistance) + (ctx.nextDouble() - 0.5); double distance = Math.hypot(i - x, j - y); @@ -169,7 +169,8 @@ public class CaveGeneratorTask implements GeneratorTask { for(int x = 0; x < width; x++) { for(int y = 0; y < height; y++) { - if((y >= ctx.getSurface(x) + ctx.nextInt(3)) && ctx.nextDouble() <= cellRate) { + if((terrainType == TerrainType.ASTEROIDS ? ctx.isAir(x, y, Layer.FRONT) : ctx.isEarthy(x, y)) + && (y >= ctx.getSurface(x) + ctx.nextInt(3)) && ctx.nextDouble() <= cellRate) { cells[x][y] = true; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java index 26783f8..21e4dc2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java @@ -1,10 +1,12 @@ package brainwine.gameserver.zone.gen.tasks; +import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.util.SimplexNoise; import brainwine.gameserver.util.WeightedMap; import brainwine.gameserver.zone.gen.GeneratorConfig; import brainwine.gameserver.zone.gen.GeneratorContext; +import brainwine.gameserver.zone.gen.models.LayerSeparator; import brainwine.gameserver.zone.gen.models.TerrainType; import brainwine.gameserver.zone.gen.surface.SurfaceRegion; import brainwine.gameserver.zone.gen.surface.SurfaceRegionType; @@ -14,6 +16,7 @@ public class TerrainGeneratorTask implements GeneratorTask { private final TerrainType type; private final double minAmplitude; private final double maxAmplitude; + private final LayerSeparator layerSeparator; private final int surfaceRegionSize; private final WeightedMap surfaceRegionTypes; @@ -21,6 +24,7 @@ public class TerrainGeneratorTask implements GeneratorTask { type = config.getTerrainType(); minAmplitude = config.getMinAmplitude(); maxAmplitude = config.getMaxAmplitude(); + layerSeparator = config.getLayerSeparator(); surfaceRegionSize = config.getSurfaceRegionSize(); surfaceRegionTypes = config.getSurfaceRegionTypes(); } @@ -72,5 +76,24 @@ public class TerrainGeneratorTask implements GeneratorTask { ctx.updateBlock(x, y, Layer.BASE, "base/earth"); } } + + // Generate layer separators + if(layerSeparator != null) { + Item item = layerSeparator.getItem(); + int minThickness = layerSeparator.getMinThickness(); + int maxThickness = layerSeparator.getMaxThickness(); + double amplitude = ctx.nextDouble() * (layerSeparator.getMaxAmplitude() - layerSeparator.getMinAmplitude()) + layerSeparator.getMinAmplitude(); + + for(int depth : ctx.getZone().getDepths()) { + for(int x = 0; x < width; x++) { + int start = (int)(SimplexNoise.noise2(ctx.getSeed(), x / 256.0, 0, 7) * amplitude) + depth - maxThickness / 2; + int size = ctx.nextInt(maxThickness - minThickness) + minThickness; + + for(int y = start; y < start + size; y++) { + ctx.updateBlock(x, y, item.getLayer(), item); + } + } + } + } } } diff --git a/gameserver/src/main/resources/defaults/generators/deep.json b/gameserver/src/main/resources/defaults/generators/deep.json index a218fd0..f23768f 100644 --- a/gameserver/src/main/resources/defaults/generators/deep.json +++ b/gameserver/src/main/resources/defaults/generators/deep.json @@ -4,6 +4,13 @@ "dungeon_chance": 0.4, "background_accent_chance": 0.033, "background_drawing_chance": 0.001, + "layer_separator": { + "item": "ground/blackrock", + "min_thickness": 3, + "max_thickness": 6, + "min_amplitude": 20, + "max_amplitude": 20 + }, "stone_types": { "default": 17, "limestone": 4 diff --git a/gameserver/src/main/resources/defaults/generators/hell.json b/gameserver/src/main/resources/defaults/generators/hell.json index c577dc7..67c5ddf 100644 --- a/gameserver/src/main/resources/defaults/generators/hell.json +++ b/gameserver/src/main/resources/defaults/generators/hell.json @@ -7,6 +7,13 @@ "dungeon_chance": 0.375, "background_accent_chance": 0.033, "background_drawing_chance": 0.001, + "layer_separator": { + "item": "ground/blackrock", + "min_thickness": 3, + "max_thickness": 6, + "min_amplitude": 20, + "max_amplitude": 20 + }, "stone_types": { "default": 1 }, From 2b0900b3504bee8ea418f3d18930aa6f83c8c0d8 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:39:04 +0100 Subject: [PATCH 07/47] Fix space biome not configuring properly --- deepworld-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepworld-config b/deepworld-config index 827c1fa..6052c53 160000 --- a/deepworld-config +++ b/deepworld-config @@ -1 +1 @@ -Subproject commit 827c1fac075ed2a9d92f56696f36101d54485109 +Subproject commit 6052c53944e3d4469819e725c59914ea136b2007 From 887e235d9da83f50e48abd9827bae5fca7c8f905 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 17 Feb 2024 23:22:03 +0100 Subject: [PATCH 08/47] Fix potential mechanical sign crash --- .../gameserver/item/interactions/SwitchInteraction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java index 3fb9ff5..8adf44e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -164,7 +164,7 @@ public class SwitchInteraction implements ItemInteraction { private void switchSign(Zone zone, Entity entity, MetaBlock metaBlock, MetaBlock switchMeta) { String message = switchMeta.hasProperty("m") ? switchMeta.getStringProperty("m").trim() : ""; - boolean lock = metaBlock.getStringProperty("lock").equalsIgnoreCase("yes"); + boolean lock = metaBlock.hasProperty("lock") && metaBlock.getStringProperty("lock").equalsIgnoreCase("yes"); Item item = metaBlock.getItem(); // Check and update lock status From 310174864ee87d6be8e141375f52648cf27435d6 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 17 Feb 2024 23:25:37 +0100 Subject: [PATCH 09/47] Move behavior to NPC package --- .../brainwine/gameserver/entity/npc/Npc.java | 2 +- .../{ => entity/npc}/behavior/Behavior.java | 40 +++++++++---------- .../npc}/behavior/CompositeBehavior.java | 2 +- .../npc}/behavior/SelectorBehavior.java | 2 +- .../npc}/behavior/SequenceBehavior.java | 2 +- .../behavior/composed/CrawlerBehavior.java | 14 +++---- .../behavior/composed/DiggerBehavior.java | 14 +++---- .../npc}/behavior/composed/FlyerBehavior.java | 10 ++--- .../behavior/composed/WalkerBehavior.java | 12 +++--- .../npc}/behavior/parts/ClimbBehavior.java | 4 +- .../npc}/behavior/parts/DigBehavior.java | 4 +- .../parts/EruptionAttackBehavior.java | 4 +- .../npc}/behavior/parts/FallBehavior.java | 4 +- .../npc}/behavior/parts/FlyBehavior.java | 4 +- .../behavior/parts/FlyTowardBehavior.java | 2 +- .../npc}/behavior/parts/FollowBehavior.java | 4 +- .../npc}/behavior/parts/IdleBehavior.java | 4 +- .../parts/RandomlyTargetBehavior.java | 4 +- .../npc}/behavior/parts/ReporterBehavior.java | 4 +- .../npc}/behavior/parts/ShielderBehavior.java | 4 +- .../behavior/parts/SpawnAttackBehavior.java | 4 +- .../npc}/behavior/parts/TurnBehavior.java | 4 +- .../npc}/behavior/parts/UnblockBehavior.java | 4 +- .../npc}/behavior/parts/WalkBehavior.java | 4 +- 24 files changed, 78 insertions(+), 78 deletions(-) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/Behavior.java (61%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/CompositeBehavior.java (97%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/SelectorBehavior.java (91%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/SequenceBehavior.java (97%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/composed/CrawlerBehavior.java (67%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/composed/DiggerBehavior.java (66%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/composed/FlyerBehavior.java (71%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/composed/WalkerBehavior.java (68%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/ClimbBehavior.java (90%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/DigBehavior.java (86%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/EruptionAttackBehavior.java (96%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/FallBehavior.java (87%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/FlyBehavior.java (94%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/FlyTowardBehavior.java (95%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/FollowBehavior.java (84%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/IdleBehavior.java (96%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/RandomlyTargetBehavior.java (93%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/ReporterBehavior.java (80%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/ShielderBehavior.java (96%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/SpawnAttackBehavior.java (96%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/TurnBehavior.java (85%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/UnblockBehavior.java (90%) rename gameserver/src/main/java/brainwine/gameserver/{ => entity/npc}/behavior/parts/WalkBehavior.java (89%) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index bc75fb3..4046b13 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -9,12 +9,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ThreadLocalRandom; -import brainwine.gameserver.behavior.SequenceBehavior; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityLoot; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.FacingDirection; +import brainwine.gameserver.entity.npc.behavior.SequenceBehavior; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/Behavior.java similarity index 61% rename from gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/Behavior.java index 610f801..36fe0cd 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/Behavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior; +package brainwine.gameserver.entity.npc.behavior; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -7,26 +7,26 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; -import brainwine.gameserver.behavior.composed.CrawlerBehavior; -import brainwine.gameserver.behavior.composed.DiggerBehavior; -import brainwine.gameserver.behavior.composed.FlyerBehavior; -import brainwine.gameserver.behavior.composed.WalkerBehavior; -import brainwine.gameserver.behavior.parts.ClimbBehavior; -import brainwine.gameserver.behavior.parts.DigBehavior; -import brainwine.gameserver.behavior.parts.EruptionAttackBehavior; -import brainwine.gameserver.behavior.parts.FallBehavior; -import brainwine.gameserver.behavior.parts.FlyBehavior; -import brainwine.gameserver.behavior.parts.FlyTowardBehavior; -import brainwine.gameserver.behavior.parts.FollowBehavior; -import brainwine.gameserver.behavior.parts.IdleBehavior; -import brainwine.gameserver.behavior.parts.RandomlyTargetBehavior; -import brainwine.gameserver.behavior.parts.ReporterBehavior; -import brainwine.gameserver.behavior.parts.ShielderBehavior; -import brainwine.gameserver.behavior.parts.SpawnAttackBehavior; -import brainwine.gameserver.behavior.parts.TurnBehavior; -import brainwine.gameserver.behavior.parts.UnblockBehavior; -import brainwine.gameserver.behavior.parts.WalkBehavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.composed.CrawlerBehavior; +import brainwine.gameserver.entity.npc.behavior.composed.DiggerBehavior; +import brainwine.gameserver.entity.npc.behavior.composed.FlyerBehavior; +import brainwine.gameserver.entity.npc.behavior.composed.WalkerBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.ClimbBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.DigBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.EruptionAttackBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FlyBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FlyTowardBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FollowBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.RandomlyTargetBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.ReporterBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.ShielderBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.SpawnAttackBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.UnblockBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior; /** * Heavily based on Deepworld's original "rubyhave" (ha ha very punny) behavior system. diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/CompositeBehavior.java similarity index 97% rename from gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/CompositeBehavior.java index 2f56955..ef07508 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/CompositeBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior; +package brainwine.gameserver.entity.npc.behavior; import static brainwine.shared.LogMarkers.SERVER_MARKER; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SelectorBehavior.java similarity index 91% rename from gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SelectorBehavior.java index bf1025a..85bbbb6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SelectorBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior; +package brainwine.gameserver.entity.npc.behavior; import java.util.Map; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SequenceBehavior.java similarity index 97% rename from gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SequenceBehavior.java index 9bc5890..0999021 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SequenceBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior; +package brainwine.gameserver.entity.npc.behavior; import static brainwine.shared.LogMarkers.SERVER_MARKER; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/CrawlerBehavior.java similarity index 67% rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/CrawlerBehavior.java index 53575bc..4c2821d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/CrawlerBehavior.java @@ -1,17 +1,17 @@ -package brainwine.gameserver.behavior.composed; +package brainwine.gameserver.entity.npc.behavior.composed; import java.util.Map; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.SelectorBehavior; -import brainwine.gameserver.behavior.parts.ClimbBehavior; -import brainwine.gameserver.behavior.parts.FallBehavior; -import brainwine.gameserver.behavior.parts.IdleBehavior; -import brainwine.gameserver.behavior.parts.TurnBehavior; -import brainwine.gameserver.behavior.parts.WalkBehavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.SelectorBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.ClimbBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior; import brainwine.gameserver.util.MapHelper; public class CrawlerBehavior extends SelectorBehavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/DiggerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/DiggerBehavior.java similarity index 66% rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/DiggerBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/DiggerBehavior.java index d6fb917..bf95bf7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/DiggerBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/DiggerBehavior.java @@ -1,17 +1,17 @@ -package brainwine.gameserver.behavior.composed; +package brainwine.gameserver.entity.npc.behavior.composed; import java.util.Map; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.SelectorBehavior; -import brainwine.gameserver.behavior.parts.DigBehavior; -import brainwine.gameserver.behavior.parts.FallBehavior; -import brainwine.gameserver.behavior.parts.IdleBehavior; -import brainwine.gameserver.behavior.parts.TurnBehavior; -import brainwine.gameserver.behavior.parts.WalkBehavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.SelectorBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.DigBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior; import brainwine.gameserver.util.MapHelper; public class DiggerBehavior extends SelectorBehavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/FlyerBehavior.java similarity index 71% rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/FlyerBehavior.java index d511c6f..1f5b60d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/FlyerBehavior.java @@ -1,15 +1,15 @@ -package brainwine.gameserver.behavior.composed; +package brainwine.gameserver.entity.npc.behavior.composed; import java.util.Map; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.SelectorBehavior; -import brainwine.gameserver.behavior.parts.FlyBehavior; -import brainwine.gameserver.behavior.parts.FlyTowardBehavior; -import brainwine.gameserver.behavior.parts.IdleBehavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.SelectorBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FlyBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FlyTowardBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior; import brainwine.gameserver.util.MapHelper; public class FlyerBehavior extends SelectorBehavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/WalkerBehavior.java similarity index 68% rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/WalkerBehavior.java index 4ad4a9a..15eb95d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/WalkerBehavior.java @@ -1,16 +1,16 @@ -package brainwine.gameserver.behavior.composed; +package brainwine.gameserver.entity.npc.behavior.composed; import java.util.Map; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.SelectorBehavior; -import brainwine.gameserver.behavior.parts.FallBehavior; -import brainwine.gameserver.behavior.parts.IdleBehavior; -import brainwine.gameserver.behavior.parts.TurnBehavior; -import brainwine.gameserver.behavior.parts.WalkBehavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.SelectorBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior; +import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior; import brainwine.gameserver.util.MapHelper; public class WalkerBehavior extends SelectorBehavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ClimbBehavior.java similarity index 90% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ClimbBehavior.java index dc86bc9..9b3c7cb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ClimbBehavior.java @@ -1,11 +1,11 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; public class ClimbBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/DigBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/DigBehavior.java similarity index 86% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/DigBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/DigBehavior.java index ab133f9..109bf24 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/DigBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/DigBehavior.java @@ -1,10 +1,10 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.zone.Zone; public class DigBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/EruptionAttackBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/EruptionAttackBehavior.java similarity index 96% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/EruptionAttackBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/EruptionAttackBehavior.java index a21ff18..fa39697 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/EruptionAttackBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/EruptionAttackBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Map; @@ -6,10 +6,10 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonSetter; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.Vector2i; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FallBehavior.java similarity index 87% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FallBehavior.java index 686f1e5..3e713eb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FallBehavior.java @@ -1,11 +1,11 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; public class FallBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyBehavior.java similarity index 94% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyBehavior.java index 2067687..f8594f3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyBehavior.java @@ -1,12 +1,12 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import java.util.concurrent.ThreadLocalRandom; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.util.Vector2i; public class FlyBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyTowardBehavior.java similarity index 95% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyTowardBehavior.java index baa0a9d..1f60e15 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyTowardBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FollowBehavior.java similarity index 84% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FollowBehavior.java index 087b004..ecbee08 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FollowBehavior.java @@ -1,11 +1,11 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; public class FollowBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/IdleBehavior.java similarity index 96% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/IdleBehavior.java index 68112f6..d9f9597 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/IdleBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -7,9 +7,9 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonSetter; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.util.Vector2i; public class IdleBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/RandomlyTargetBehavior.java similarity index 93% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/RandomlyTargetBehavior.java index afc982a..0466adb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/RandomlyTargetBehavior.java @@ -1,10 +1,10 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.entity.player.Player; public class RandomlyTargetBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ReporterBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ReporterBehavior.java similarity index 80% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/ReporterBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ReporterBehavior.java index c77fc7e..a1beaf8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ReporterBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ReporterBehavior.java @@ -1,10 +1,10 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; public class ReporterBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java similarity index 96% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java index 0a0de4e..01b2eb6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Arrays; import java.util.Collection; @@ -9,8 +9,8 @@ import java.util.concurrent.ThreadLocalRandom; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.util.Pair; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java similarity index 96% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java index 6650737..4c8d615 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Map; @@ -6,10 +6,10 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonSetter; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.Vector2i; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/TurnBehavior.java similarity index 85% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/TurnBehavior.java index 8ae0134..9e2fcf0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/TurnBehavior.java @@ -1,11 +1,11 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; public class TurnBehavior extends Behavior { diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/UnblockBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java similarity index 90% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/UnblockBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java index 929b0dc..e1615b0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/UnblockBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; @@ -6,8 +6,8 @@ import java.util.concurrent.ThreadLocalRandom; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.zone.Zone; diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/WalkBehavior.java similarity index 89% rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/WalkBehavior.java index 8e746ee..62e23dd 100644 --- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/WalkBehavior.java @@ -1,12 +1,12 @@ -package brainwine.gameserver.behavior.parts; +package brainwine.gameserver.entity.npc.behavior.parts; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; -import brainwine.gameserver.behavior.Behavior; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.behavior.Behavior; public class WalkBehavior extends Behavior { From 49ab4c42f46a6c346f9bdbc3368ee04ad2ee5448 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 02:18:22 +0100 Subject: [PATCH 10/47] Abstract entity damaging --- .../brainwine/gameserver/entity/Entity.java | 40 +++++++++- .../gameserver/entity/EntityAttack.java | 41 ++++++++++ .../brainwine/gameserver/entity/npc/Npc.java | 77 +++++++------------ .../npc/behavior/parts/ShielderBehavior.java | 10 +-- .../gameserver/entity/player/Player.java | 24 +++++- .../server/requests/InventoryUseRequest.java | 5 +- .../java/brainwine/gameserver/zone/Zone.java | 7 +- 7 files changed, 139 insertions(+), 65 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 6009cc9..879871e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -1,11 +1,13 @@ package brainwine.gameserver.entity; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; @@ -23,8 +25,11 @@ public abstract class Entity { public static final float DEFAULT_HEALTH = 5; public static final float POSITION_MODIFIER = 100F; public static final int VELOCITY_MODIFIER = (int)POSITION_MODIFIER; + public static final int ATTACK_RETENTION_TIME = 2000; + public static final int ATTACK_INVINCIBLE_TIME = 333; protected final Map properties = new HashMap<>(); protected final List trackers = new ArrayList<>(); + protected final List recentAttacks = new ArrayList<>(); protected int type; protected String name; protected float health = DEFAULT_HEALTH; @@ -49,6 +54,8 @@ public abstract class Entity { } public void tick(float deltaTime) { + long now = System.currentTimeMillis(); + // Update block position lastBlockX = blockX; lastBlockY = blockY; @@ -59,9 +66,12 @@ public abstract class Entity { if(lastBlockX != blockX || lastBlockY != blockY) { blockPositionChanged(); } + + // Clear expired recent attacks + recentAttacks.removeIf(attack -> now >= attack.getTime() + ATTACK_RETENTION_TIME); } - public void die(Player killer) { + public void die(Entity killer) { // Override } @@ -75,7 +85,7 @@ public abstract class Entity { damage(amount, null); } - public void damage(float amount, Player attacker) { + public void damage(float amount, Entity attacker) { setHealth(health - amount); if(health <= 0) { @@ -85,6 +95,24 @@ public abstract class Entity { lastDamagedAt = System.currentTimeMillis(); } + public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) { + EntityAttack attack = new EntityAttack(attacker, weapon, baseDamage, damageType); + boolean ignoreDefense = attacker != null && attacker.isPlayer() && ((Player)attacker).isGodMode(); + float attackMultiplier = attacker != null ? Math.max(0.0F, attacker.getAttackMultiplier(attack)) : 1.0F; + float defense = Math.max(0.0F, 1.0F - getDefense(attack)); + float damage = baseDamage * attackMultiplier * (ignoreDefense ? 1.0F : defense); + damage(damage, attacker); + recentAttacks.add(attack); + } + + public float getAttackMultiplier(EntityAttack attack) { + return 1.0F; // Override + } + + public float getDefense(EntityAttack attack) { + return 1.0F; // Override + } + public void blockPositionChanged() { // Check for touchplates if(zone != null && zone.isChunkLoaded(blockX, blockY)) { @@ -161,6 +189,14 @@ public abstract class Entity { return trackers; } + public boolean wasAttackedRecently(Entity entity, int delay) { + return recentAttacks.stream().filter(attack -> attack.getAttacker() == entity && System.currentTimeMillis() < attack.getTime() + delay).findFirst().isPresent(); + } + + public List getRecentAttacks() { + return Collections.unmodifiableList(recentAttacks); + } + public void setId(int id) { this.id = id; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java new file mode 100644 index 0000000..70ccebf --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java @@ -0,0 +1,41 @@ +package brainwine.gameserver.entity; + +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; + +public class EntityAttack { + + private final Entity attacker; + private final Item weapon; + private final float baseDamage; + private final DamageType damageType; + private final long time; + + public EntityAttack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) { + this.attacker = attacker; + this.weapon = weapon; + this.baseDamage = baseDamage; + this.damageType = damageType; + this.time = System.currentTimeMillis(); + } + + public Entity getAttacker() { + return attacker; + } + + public Item getWeapon() { + return weapon; + } + + public float getBaseDamage() { + return baseDamage; + } + + public DamageType getDamageType() { + return damageType; + } + + public long getTime() { + return time; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index 4046b13..5dd3044 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -10,6 +10,7 @@ import java.util.Map.Entry; import java.util.concurrent.ThreadLocalRandom; import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityAttack; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityLoot; import brainwine.gameserver.entity.EntityRegistry; @@ -20,7 +21,6 @@ import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.util.MapHelper; -import brainwine.gameserver.util.Pair; import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.util.WeightedMap; import brainwine.gameserver.zone.MetaBlock; @@ -28,8 +28,6 @@ import brainwine.gameserver.zone.Zone; public class Npc extends Entity { - public static final int ATTACK_RETENTION_TIME = 2000; - public static final int ATTACK_INVINCIBLE_TIME = 333; private final EntityConfig config; private final String typeName; private final float maxHealth; @@ -43,7 +41,6 @@ public class Npc extends Entity { private final List animations; private final SequenceBehavior behaviorTree; private final Map activeDefenses = new HashMap<>(); - private final Map> recentAttacks = new HashMap<>(); private final List children = new ArrayList<>(); private float speed; private int moveX; @@ -124,9 +121,6 @@ public class Npc extends Entity { super.tick(deltaTime); long now = System.currentTimeMillis(); - // Clear expired recent attacks - recentAttacks.values().removeIf(attack -> now >= attack.getLast() + ATTACK_RETENTION_TIME); - // Tick behavior when it is ready if(now >= lastBehavedAt + (int)(1000 / speed)) { lastBehavedAt = now; @@ -148,27 +142,31 @@ public class Npc extends Entity { } @Override - public void die(Player killer) { + public void die(Entity killer) { // Grant loot & track kill - if(!artificial && killer != null) { + if(!artificial && killer != null && killer.isPlayer()) { + Player player = (Player)killer; + if(!isPlayerPlaced()) { // Track assists - for(Player attacker : recentAttacks.keySet()) { - if(attacker != killer) { - attacker.getStatistics().trackAssist(config); + for(EntityAttack attack : recentAttacks) { + Entity attacker = attack.getAttacker(); + + if(attacker != killer && attacker.isPlayer()) { + ((Player)attacker).getStatistics().trackAssist(config); } } - killer.getStatistics().trackKill(config); + player.getStatistics().trackKill(config); } - EntityLoot loot = getRandomLoot(killer); + EntityLoot loot = getRandomLoot(player); if(loot != null) { Item item = loot.getItem(); if(!item.isAir()) { - killer.getInventory().addItem(item, loot.getQuantity(), true); + player.getInventory().addItem(item, loot.getQuantity(), true); } } } @@ -197,6 +195,20 @@ public class Npc extends Entity { return maxHealth; } + @Override + public float getDefense(EntityAttack attack) { + Entity attacker = attack.getAttacker(); + Player player = attacker != null && attacker.isPlayer() ? (Player)attacker : null; + + // Full defense if block is mounted and is protected + if(isMounted() && zone.isBlockProtected(mountBlock.getX(), mountBlock.getY(), player)) { + return 1.0F; + } + + // Otherwise, calculate defense + return getBaseDefense(attack.getDamageType()) + activeDefenses.getOrDefault(attack.getBaseDamage(), 0F); + } + @Override public Map getStatusConfig() { Map config = super.getStatusConfig(); @@ -239,33 +251,6 @@ public class Npc extends Entity { return size; } - public void attack(Player attacker, Item weapon) { - // Prevent damage if this entity is mounted and its mount is protected - if(!attacker.isGodMode() && isMounted() && zone.isBlockProtected(mountBlock.getX(), mountBlock.getY(), attacker)) { - return; - } - - Pair recentAttack = recentAttacks.get(attacker); - long now = System.currentTimeMillis(); - - // Reject the attack if the player already attacked this entity recently - if(!attacker.isGodMode() && recentAttack != null && now < recentAttack.getLast() + ATTACK_INVINCIBLE_TIME) { - return; - } - - float damage = attacker.isGodMode() ? 9999 : calculateDamage(weapon.getDamage(), weapon.getDamageType()); - damage(damage, attacker); - recentAttacks.put(attacker, new Pair<>(weapon, now)); - } - - public float calculateDamage(float baseDamage, DamageType type) { - return baseDamage * (1 - getDefense(type)); - } - - public Collection> getRecentAttacks() { - return Collections.unmodifiableCollection(recentAttacks.values()); - } - public void setDefense(DamageType type, float amount) { if(amount == 0) { activeDefenses.remove(type); @@ -274,14 +259,6 @@ public class Npc extends Entity { } } - public float getDefense(DamageType type) { - return getDefense(type, true); - } - - public float getDefense(DamageType type, boolean includeBaseDefense) { - return (includeBaseDefense ? getBaseDefense(type) : 0) + activeDefenses.getOrDefault(type, 0F); - } - public boolean isTransient() { return !isGuard() && !isMounted(); } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java index 01b2eb6..532777f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java @@ -1,19 +1,18 @@ package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Arrays; -import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; +import brainwine.gameserver.entity.EntityAttack; import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.item.DamageType; -import brainwine.gameserver.item.Item; -import brainwine.gameserver.util.Pair; public class ShielderBehavior extends Behavior { @@ -32,11 +31,12 @@ public class ShielderBehavior extends Behavior { @Override public boolean behave() { long now = System.currentTimeMillis(); - Collection> recentAttacks = entity.getRecentAttacks(); + List recentAttacks = entity.getRecentAttacks(); if(!recentAttacks.isEmpty()) { lastAttackedAt = now; - DamageType type = recentAttacks.stream().findFirst().get().getFirst().getDamageType(); + DamageType type = recentAttacks.get(recentAttacks.size() - 1).getDamageType(); + if(currentShield == null && now >= shieldStart + (recharge * 1000)) { if(defenses.contains(type)) { setShield(type); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index f3bab42..76dde89 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -29,8 +29,10 @@ import brainwine.gameserver.dialog.DialogListItem; import brainwine.gameserver.dialog.DialogSection; import brainwine.gameserver.dialog.DialogType; import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityAttack; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemUseType; @@ -66,7 +68,6 @@ import brainwine.gameserver.server.models.EntityStatusData; import brainwine.gameserver.server.pipeline.Connection; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MathUtils; -import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.zone.Chunk; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; @@ -208,7 +209,7 @@ public class Player extends Entity implements CommandExecutor { } @Override - public void die(Player killer) { + public void die(Entity killer) { statistics.trackDeath(); sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD)); // TODO killer id GameServer.getInstance().notify(String.format("%s died", name), NotificationType.CHAT); @@ -240,6 +241,21 @@ public class Player extends Entity implements CommandExecutor { sendMessage(new HealthMessage(health)); } + @Override + public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) { + super.attack(attacker, weapon, isGodMode() ? 0.0F : baseDamage, damageType); + } + + @Override + public float getAttackMultiplier(EntityAttack attack) { + return isGodMode() ? 9999.0F : 1.0F; + } + + @Override + public float getDefense(EntityAttack attack) { + return getNormalizedSkill(Skill.SURVIVAL) * 0.5F; + } + @Override public void setProperties(Map properties, boolean sendMessage) { super.setProperties(properties, sendMessage); @@ -1090,6 +1106,10 @@ public class Player extends Entity implements CommandExecutor { return getSkillLevel(skill) + accessorySkillLevel; } + public float getNormalizedSkill(Skill skill) { + return getTotalSkillLevel(skill) / (float)MAX_SKILL_LEVEL; + } + public int getSkillLevel(Skill skill) { return skills.getOrDefault(skill, 1); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java index 98a6d7a..db5b057 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java @@ -5,6 +5,7 @@ import java.util.Collection; import brainwine.gameserver.annotations.OptionalField; import brainwine.gameserver.annotations.RequestInfo; +import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; @@ -63,8 +64,8 @@ public class InventoryUseRequest extends PlayerRequest { if(id instanceof Integer) { Npc npc = player.getZone().getNpc((int)id); - if(npc != null && (player.isGodMode() || player.canSee(npc))) { - npc.attack(player, item); + if(npc != null && (player.isGodMode() || (player.canSee(npc) && !npc.wasAttackedRecently(player, Entity.ATTACK_INVINCIBLE_TIME)))) { + npc.attack(player, item, item.getDamage(), item.getDamageType()); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index f8cd994..d81ef61 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -297,7 +297,7 @@ public class Zone { explode(x, y, radius, cause, false, 0, null, effect); } - public void explode(int x, int y, float radius, Entity cause, boolean destructive, float damage, DamageType damageType, String effect) { + public void explode(int x, int y, float radius, Entity cause, boolean destructive, float baseDamage, DamageType damageType, String effect) { // Do nothing if the chunk at the target location isn't loaded if(!isChunkLoaded(x, y)) { return; @@ -399,10 +399,9 @@ public class Zone { // Cast a ray from the explosion to the entity and damage it if it reaches it if(entity.canSee(x, y)) { double distance = MathUtils.distance(x, y, entity.getX(), entity.getY()); - entity.damage((float)(damage - distance), player); + float damage = (float)(baseDamage - distance); + entity.attack(player, null, damage, damageType); // TODO maybe track the bomb used to damage the entity } - - // TODO generic entity damaging is not very intuitive and needs to be worked on. } } From dcb5e8a107e08e6737cdea5483aae9ebda328ceb Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 02:31:53 +0100 Subject: [PATCH 11/47] Fix lava bombs --- .../server/requests/BlockPlaceRequest.java | 33 +++++++------------ .../java/brainwine/gameserver/zone/Zone.java | 30 +++++++++++++++++ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java index c6cd015..8c6688e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java @@ -168,32 +168,21 @@ public class BlockPlaceRequest extends PlayerRequest { }; break; case "bomb-water": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.COLD, "bomb-large"); + zone.explodeLiquid(x, y, 4, "liquid/water"); + }; + break; case "bomb-acid": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.ACID, "bomb-large"); + zone.explodeLiquid(x, y, 4, "liquid/acid"); + }; + break; case "bomb-lava": task = () -> { zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-large"); - Item liquid = Item.get(String.format("liquid/%s", type.replace("bomb-", ""))); - int range = 4; - - // Do nothing if there is no liquid to place - if(liquid.isAir()) { - return; - } - - // Place liquid blocks around the explosion - for(int i = x - range; i <= x + range; i++) { - for(int j = y - range; j <= y + range; j++) { - // Skip if not in range - if(!MathUtils.inRange(x, y, i, j, range)) { - continue; - } - - // Place liquid if target block isn't solid - if(!zone.isBlockSolid(i, j, true)) { - zone.updateBlock(i, j, Layer.LIQUID, liquid, 5); - } - } - } + zone.explodeLiquid(x, y, 4, "liquid/magma"); }; break; default: diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index d81ef61..ad4042b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -405,6 +405,36 @@ public class Zone { } } + public void explodeLiquid(int x, int y, int range, int liquid) { + explodeLiquid(x, y, range, ItemRegistry.getItem(liquid)); + } + + public void explodeLiquid(int x, int y, int range, String liquid) { + explodeLiquid(x, y, range, ItemRegistry.getItem(liquid)); + } + + public void explodeLiquid(int x, int y, int range, Item liquid) { + // Do nothing if liquid isn't actually a liquid + if(liquid.getLayer() != Layer.LIQUID) { + return; + } + + // Place liquid blocks around the explosion + for(int i = x - range; i <= x + range; i++) { + for(int j = y - range; j <= y + range; j++) { + // Skip if not in range + if(!MathUtils.inRange(x, y, i, j, range)) { + continue; + } + + // Place liquid if target block isn't solid + if(!isBlockSolid(i, j, true)) { + updateBlock(i, j, Layer.LIQUID, liquid, 5); + } + } + } + } + public boolean isBlockNatural(int x, int y) { return areCoordinatesInBounds(x, y) && getBlock(x, y).isNatural(); } From f986587ef1136e078e60ecbcea9f0999f2048c54 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:08:38 +0100 Subject: [PATCH 12/47] Fix inventory use sync issue with v2 clients --- .../gameserver/server/requests/InventoryUseRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java index db5b057..a7a6b4e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java @@ -30,7 +30,7 @@ public class InventoryUseRequest extends PlayerRequest { @Override public void process(Player player) { // Don't do anything if the player is dead or doesn't own this item - if(player.isDead() || !player.getInventory().hasItem(item)) { + if(player.isDead() || (!item.isAir() && !player.getInventory().hasItem(item))) { return; } @@ -46,7 +46,7 @@ public class InventoryUseRequest extends PlayerRequest { } // Send item use data to other players in the zone - player.sendMessageToPeers(new EntityItemUseMessage(player.getId(), type, item, status)); + player.sendMessageToTrackers(new EntityItemUseMessage(player.getId(), type, item, status)); // Lovely type ambiguity. Always nice. if(item.isWeapon() && status == 1) { From 29dd9ac540f7a5815664a5fd9cebe8558d1141fd Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:25:54 +0100 Subject: [PATCH 13/47] Add note functionality --- .../gameserver/dialog/DialogSection.java | 21 +++- .../entity/player/NotificationType.java | 1 + .../gameserver/item/ItemUseType.java | 2 + .../interactions/ContainerInteraction.java | 3 +- .../item/interactions/NoteInteraction.java | 98 +++++++++++++++++++ .../item/interactions/SwitchInteraction.java | 3 +- .../server/requests/BlockUseRequest.java | 15 +-- .../gameserver/zone/EntityManager.java | 2 +- .../brainwine/gameserver/zone/MetaBlock.java | 13 ++- .../java/brainwine/gameserver/zone/Zone.java | 32 +++--- 10 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java diff --git a/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java b/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java index 9374d8a..5f6f48f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java +++ b/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java @@ -3,13 +3,15 @@ package brainwine.gameserver.dialog; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; import brainwine.gameserver.dialog.input.DialogInput; - -import com.fasterxml.jackson.annotation.JsonProperty; +import brainwine.gameserver.util.Vector2i; @JsonInclude(Include.NON_DEFAULT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -20,6 +22,7 @@ public class DialogSection { private String text; private String textColor; private double textScale; + private Vector2i location; private DialogInput input; public DialogSection addItem(DialogListItem item) { @@ -75,6 +78,20 @@ public class DialogSection { return textScale; } + /** + * v2 clients only! + */ + public DialogSection setLocation(int x, int y) { + this.location = new Vector2i(x, y); + return this; + } + + @JsonProperty("map") + @JsonFormat(shape = Shape.ARRAY) + public Vector2i getLocation() { + return location; + } + public DialogSection setInput(DialogInput input) { this.input = input; return this; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java index aaeebc1..a6126f1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java @@ -13,6 +13,7 @@ public enum NotificationType { ACCOMPLISHMENT(10), PEER_ACCOMPLISHMENT(11), REWARD(12), // v2 only + NOTE(13), // v2 only CHAT(20), LEVEL_UP(21), // v3 only ACHIEVEMENT(22), // v3 only diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java index 7ba2908..0042bb5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java @@ -8,6 +8,7 @@ import brainwine.gameserver.item.interactions.ChangeInteraction; import brainwine.gameserver.item.interactions.ContainerInteraction; import brainwine.gameserver.item.interactions.DialogInteraction; import brainwine.gameserver.item.interactions.ItemInteraction; +import brainwine.gameserver.item.interactions.NoteInteraction; import brainwine.gameserver.item.interactions.SpawnInteraction; import brainwine.gameserver.item.interactions.SpawnTeleportInteraction; import brainwine.gameserver.item.interactions.SwitchInteraction; @@ -31,6 +32,7 @@ public enum ItemUseType { FIELDABLE, FLY, MULTI, + NOTE(new NoteInteraction()), PLENTY, PROTECTED, PUBLIC, diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java index 192db11..14971f1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java @@ -81,8 +81,7 @@ public class ContainerInteraction implements ItemInteraction { // Update container mod if(!plenty && !metaBlock.hasProperty("$")) { - Player owner = GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); - zone.updateBlock(x, y, Layer.FRONT, item, 0, owner, metaBlock.getMetadata()); + zone.updateBlock(x, y, Layer.FRONT, item, 0, metaBlock.getOwner(), metaBlock.getMetadata()); } } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java new file mode 100644 index 0000000..a58d587 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java @@ -0,0 +1,98 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Collections; +import java.util.List; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.player.NotificationType; +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public class NoteInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if the right data isn't present + if(metaBlock == null || data != null) { + return; + } + + Player player = (Player)entity; + + // Check if note contains a location + if(metaBlock.hasProperty("l")) { + List location = MapHelper.getList(metaBlock.getMetadata(), "l", Collections.emptyList()); + String text = metaBlock.getStringProperty("t"); + + // Do nothing if location data is invalid + if(location.size() != 2) { + return; + } + + int locationX = location.get(0); + int locationY = location.get(1); + + // Create dialog based on player version since v3 doesn't support map dialogs + if(player.isV3()) { + Dialog dialog = new Dialog() + .addSection(new DialogSection() + .setTitle("The note reads:") + .setText(text)) + .addSection(new DialogSection() + .setText(zone.getReadableCoordinates(locationX, locationY))); + player.showDialog(dialog); + } else { + // v2 dialog + Dialog dialog = new Dialog() + .addSection(new DialogSection() + .setTitle(text)) + .addSection(new DialogSection() + .setLocation(locationX, locationY)); + player.notify(dialog, NotificationType.NOTE); + } + + return; + } + + // Do nothing if player owns this note + if(metaBlock.isOwnedBy(player)) { + return; + } + + // Build string from note segments + String[] keys = { "t1", "t2", "t3", "t4", "t5", "t6" }; + StringBuilder builder = new StringBuilder(); + + for(int i = 0; i < keys.length; i++) { + String text = metaBlock.getStringProperty(keys[i]); + + // Skip if text is null or empty + if(text == null || text.isEmpty()) { + continue; + } + + // Append space if necessary + if(i > 0) { + builder.append(" "); + } + + builder.append(text); + } + + // Show note text in dialog + player.showDialog(DialogHelper.messageDialog("The note reads:", builder.toString())); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java index 8adf44e..9c2117b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -7,7 +7,6 @@ import java.util.Map; import org.apache.commons.text.WordUtils; -import brainwine.gameserver.GameServer; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityRegistry; @@ -91,7 +90,7 @@ public class SwitchInteraction implements ItemInteraction { return; } - Player owner = metaBlock == null ? null : GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); + Player owner = metaBlock == null ? null : metaBlock.getOwner(); Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); Item item = metaBlock.getItem(); Object config = item.getUse(ItemUseType.SWITCHED); diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java index dd669bb..c8ecdc3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java @@ -2,7 +2,6 @@ package brainwine.gameserver.server.requests; import java.util.Map; -import brainwine.gameserver.GameServer; import brainwine.gameserver.annotations.OptionalField; import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.entity.player.Player; @@ -51,7 +50,7 @@ public class BlockUseRequest extends PlayerRequest { // Check if block is owned by another player if(metaBlock != null && item.hasUse(ItemUseType.PROTECTED)) { - Player owner = GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); + Player owner = metaBlock.getOwner(); if(player != owner) { if(item.hasUse(ItemUseType.PUBLIC)) { @@ -59,10 +58,14 @@ public class BlockUseRequest extends PlayerRequest { // TODO implement other cases switch(publicUse) { - case "owner": - player.notify(String.format("This %s is owned by %s.", - item.getTitle().toLowerCase(), owner == null ? "somebody else" : owner.getName())); - break; + case "owner": + player.notify(String.format("This %s is owned by %s.", + item.getTitle().toLowerCase(), owner == null ? "somebody else" : owner.getName())); + break; + case "note": + ItemUseType.NOTE.getInteraction().interact(zone, player, x, y, layer, item, mod, metaBlock, null, data); + break; + default: break; } } else { player.notify("Sorry, that belongs to somebody else."); diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java index 0b71f8d..35ec860 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -239,7 +239,7 @@ public class EntityManager { // Set owner entity if it has one if(metaBlock != null && metaBlock.hasOwner()) { - entity.setOwner(GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner())); + entity.setOwner(metaBlock.getOwner()); } entity.setMountBlock(x, y); diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java b/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java index b2e40ae..b4f53e7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import brainwine.gameserver.GameServer; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; @@ -71,7 +72,17 @@ public class MetaBlock { return owner != null; } - public String getOwner() { + public boolean isOwnedBy(Player player) { + return player != null && player.getDocumentId().equals(owner); + } + + @JsonIgnore + public Player getOwner() { + return GameServer.getInstance().getPlayerManager().getPlayerById(owner); + } + + @JsonProperty("owner") + private String getOwnerId() { return owner; } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index ad4042b..03f1bb1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -496,18 +496,18 @@ public class Zone { return isBlockProtected(x, y, null); } - public boolean isBlockProtected(int x, int y, Player from) { - return isBlockProtected(x, y, from, fieldBlocks.values()); + public boolean isBlockProtected(int x, int y, Player player) { + return isBlockProtected(x, y, player, fieldBlocks.values()); } - public boolean isBlockProtected(int x, int y, Player from, Collection fieldBlocks) { + public boolean isBlockProtected(int x, int y, Player player, Collection fieldBlocks) { for(MetaBlock fieldBlock : fieldBlocks) { Item item = fieldBlock.getItem(); int fX = fieldBlock.getX(); int fY = fieldBlock.getY(); int field = fieldBlock.getItem().getField(); - if(from == null || !ownsMetaBlock(fieldBlock, from)) { + if(player == null || !fieldBlock.isOwnedBy(player)) { if(item.isDish()) { if(MathUtils.inRange(x, y, fX, fY, field)) { return true; @@ -529,7 +529,7 @@ public class Zone { int fY = fieldBlock.getY(); int fField = fieldBlock.getItem().getField(); - if(MathUtils.inRange(x, y, fX, fY, field + fField) && !ownsMetaBlock(fieldBlock, player)) { + if(MathUtils.inRange(x, y, fX, fY, field + fField) && !fieldBlock.isOwnedBy(player)) { return true; } } @@ -1035,14 +1035,6 @@ public class Zone { indexDungeons(); } - private boolean ownsMetaBlock(MetaBlock metaBlock, Player player) { - if(!metaBlock.hasOwner()) { - return false; - } - - return player.getDocumentId().equals(metaBlock.getOwner()); - } - public MetaBlock getMetaBlock(int x, int y) { return metaBlocks.get(getBlockIndex(x, y)); } @@ -1162,6 +1154,20 @@ public class Zone { return liquidManager.settleLiquids(); } + /** + * @return The specified coordinates in a player-readable format + * For example, {@code x: 200 y: 300} in a plain biome becomes {@code 800 west, 100 below} + */ + public String getReadableCoordinates(int x, int y) { + int center = width / 2; + int surface = biome == Biome.DEEP ? -1000 : 200; + String directionX = x < center ? "west" : x > center ? "east" : "central"; + String directionY = y > surface ? "below" : "above"; + String coordX = String.format("%s %s", Math.abs(x - center), directionX); + String coordY = String.format("%s %s", Math.abs(y - surface), directionY); + return String.format("%s, %s", coordX, coordY); + } + public boolean areCoordinatesInBounds(int x, int y) { return x >= 0 && y >= 0 && x < width && y < height; } From df698a349ba40b8b9b57bf62bf837411443c997a Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:39:51 +0100 Subject: [PATCH 14/47] Check if entity is dead before applying damage --- .../java/brainwine/gameserver/entity/Entity.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 879871e..e33c115 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -86,9 +86,15 @@ public abstract class Entity { } public void damage(float amount, Entity attacker) { + // Do nothing if entity is already dead + if(isDead()) { + return; + } + setHealth(health - amount); - if(health <= 0) { + // Handle entity death if it died as a result of taking this damage + if(isDead()) { die(attacker); } @@ -96,6 +102,11 @@ public abstract class Entity { } public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) { + // Ignore attack if entity is already dead + if(isDead()) { + return; + } + EntityAttack attack = new EntityAttack(attacker, weapon, baseDamage, damageType); boolean ignoreDefense = attacker != null && attacker.isPlayer() && ((Player)attacker).isGodMode(); float attackMultiplier = attacker != null ? Math.max(0.0F, attacker.getAttackMultiplier(attack)) : 1.0F; From 45787c3dba72330768e7da693201761b17b660b0 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:52:19 +0100 Subject: [PATCH 15/47] Damage check --- .../src/main/java/brainwine/gameserver/entity/Entity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index e33c115..3a6e138 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -107,6 +107,11 @@ public abstract class Entity { return; } + // Ignore attack if there is no damage to deal + if(baseDamage <= 0 || damageType == null || damageType == DamageType.NONE) { + return; + } + EntityAttack attack = new EntityAttack(attacker, weapon, baseDamage, damageType); boolean ignoreDefense = attacker != null && attacker.isPlayer() && ((Player)attacker).isGodMode(); float attackMultiplier = attacker != null ? Math.max(0.0F, attacker.getAttackMultiplier(attack)) : 1.0F; From 9ec3175eb1cc8e56de7f90a75c034a49b79b8037 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:30:40 +0100 Subject: [PATCH 16/47] Improve damage tracking --- .../brainwine/gameserver/entity/Entity.java | 72 ++++++++++++------- .../brainwine/gameserver/entity/npc/Npc.java | 71 +++++++++--------- .../npc/behavior/parts/ShielderBehavior.java | 6 +- .../gameserver/entity/player/Player.java | 13 ++-- .../item/interactions/SwitchInteraction.java | 2 +- .../server/requests/HealthRequest.java | 15 +++- .../java/brainwine/gameserver/zone/Zone.java | 5 +- 7 files changed, 109 insertions(+), 75 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 3a6e138..9700335 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -47,6 +47,8 @@ public abstract class Entity { protected int targetY; protected FacingDirection direction = FacingDirection.WEST; protected int animation; + protected boolean invulnerable; + protected EntityAttack lastAttack; // Used for tracking in entity deaths -- do not use this for anything else! protected long lastDamagedAt; public Entity(Zone zone) { @@ -71,7 +73,7 @@ public abstract class Entity { recentAttacks.removeIf(attack -> now >= attack.getTime() + ATTACK_RETENTION_TIME); } - public void die(Entity killer) { + public void die(EntityAttack cause) { // Override } @@ -81,29 +83,13 @@ public abstract class Entity { } } - public void damage(float amount) { - damage(amount, null); - } - - public void damage(float amount, Entity attacker) { - // Do nothing if entity is already dead - if(isDead()) { - return; - } - - setHealth(health - amount); - - // Handle entity death if it died as a result of taking this damage - if(isDead()) { - die(attacker); - } - - lastDamagedAt = System.currentTimeMillis(); - } - public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) { - // Ignore attack if entity is already dead - if(isDead()) { + attack(attacker, weapon, baseDamage, damageType, false); + } + + public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType, boolean trueDamage) { + // Ignore attack if entity is dead or invulnerable + if(isDead() || isInvulnerable()) { return; } @@ -113,12 +99,26 @@ public abstract class Entity { } EntityAttack attack = new EntityAttack(attacker, weapon, baseDamage, damageType); - boolean ignoreDefense = attacker != null && attacker.isPlayer() && ((Player)attacker).isGodMode(); + recentAttacks.add(attack); + lastAttack = attack; + lastDamagedAt = System.currentTimeMillis(); + + // Kill entity if attacker is a player in god mode + if(attacker != null && attacker.isPlayer() && ((Player)attacker).isGodMode()) { + setHealth(0.0F); + return; + } + + // Ignore multipliers if true damage should be dealt + if(trueDamage) { + setHealth(health - baseDamage); + return; + } + float attackMultiplier = attacker != null ? Math.max(0.0F, attacker.getAttackMultiplier(attack)) : 1.0F; float defense = Math.max(0.0F, 1.0F - getDefense(attack)); - float damage = baseDamage * attackMultiplier * (ignoreDefense ? 1.0F : defense); - damage(damage, attacker); - recentAttacks.add(attack); + float damage = baseDamage * attackMultiplier * defense; + setHealth(health - damage); } public float getAttackMultiplier(EntityAttack attack) { @@ -209,6 +209,10 @@ public abstract class Entity { return recentAttacks.stream().filter(attack -> attack.getAttacker() == entity && System.currentTimeMillis() < attack.getTime() + delay).findFirst().isPresent(); } + public EntityAttack getMostRecentAttack() { + return recentAttacks.isEmpty() ? null : recentAttacks.get(recentAttacks.size() - 1); + } + public List getRecentAttacks() { return Collections.unmodifiableList(recentAttacks); } @@ -244,6 +248,12 @@ public abstract class Entity { public void setHealth(float health) { float maxHealth = getMaxHealth(); this.health = health < 0 ? 0 : health > maxHealth ? maxHealth : health; + + if(this.health <= 0.0F) { + die(lastAttack); + } + + lastAttack = null; } public float getHealth() { @@ -313,6 +323,14 @@ public abstract class Entity { return animation; } + public void setInvulnerable(boolean invulnerable) { + this.invulnerable = invulnerable; + } + + public boolean isInvulnerable() { + return invulnerable; + } + public void setZone(Zone zone) { this.zone = zone; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index 5dd3044..eacd0f1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -142,35 +142,7 @@ public class Npc extends Entity { } @Override - public void die(Entity killer) { - // Grant loot & track kill - if(!artificial && killer != null && killer.isPlayer()) { - Player player = (Player)killer; - - if(!isPlayerPlaced()) { - // Track assists - for(EntityAttack attack : recentAttacks) { - Entity attacker = attack.getAttacker(); - - if(attacker != killer && attacker.isPlayer()) { - ((Player)attacker).getStatistics().trackAssist(config); - } - } - - player.getStatistics().trackKill(config); - } - - EntityLoot loot = getRandomLoot(player); - - if(loot != null) { - Item item = loot.getItem(); - - if(!item.isAir()) { - player.getInventory().addItem(item, loot.getQuantity(), true); - } - } - } - + public void die(EntityAttack cause) { // Remove itself from the guard block metadata if it was guarding one if(isGuard()) { MetaBlock metaBlock = zone.getMetaBlock(guardBlock.getX(), guardBlock.getY()); @@ -188,6 +160,41 @@ public class Npc extends Entity { if(isMounted()) { zone.updateBlock(mountBlock.getX(), mountBlock.getY(), Layer.FRONT, 0); } + + // Do nothing else if cause data isn't present + if(cause == null) { + return; + } + + Entity killer = cause.getAttacker(); + + // Grant loot & track kill + if(!artificial && killer != null && killer.isPlayer()) { + Player player = (Player)killer; + + if(!isPlayerPlaced()) { + // Track assists + for(EntityAttack recentAttack : recentAttacks) { + Entity attacker = recentAttack.getAttacker(); + + if(attacker != player && attacker.isPlayer()) { + ((Player)attacker).getStatistics().trackAssist(config); + } + } + + player.getStatistics().trackKill(config); + } + + EntityLoot loot = getRandomLoot(player, cause.getWeapon()); + + if(loot != null) { + Item item = loot.getItem(); + + if(!item.isAir()) { + player.getInventory().addItem(item, loot.getQuantity(), true); + } + } + } } @Override @@ -206,7 +213,7 @@ public class Npc extends Entity { } // Otherwise, calculate defense - return getBaseDefense(attack.getDamageType()) + activeDefenses.getOrDefault(attack.getBaseDamage(), 0F); + return getBaseDefense(attack.getDamageType()) + activeDefenses.getOrDefault(attack.getDamageType(), 0F); } @Override @@ -263,9 +270,7 @@ public class Npc extends Entity { return !isGuard() && !isMounted(); } - public EntityLoot getRandomLoot(Player awardee) { - Item weapon = awardee.getHeldItem(); - + public EntityLoot getRandomLoot(Player awardee, Item weapon) { if(isOwnedBy(awardee)) { return placedLoot.next(); } else if(lootByWeapon.containsKey(weapon)) { diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java index 532777f..5375743 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java @@ -31,11 +31,11 @@ public class ShielderBehavior extends Behavior { @Override public boolean behave() { long now = System.currentTimeMillis(); - List recentAttacks = entity.getRecentAttacks(); + EntityAttack attack = entity.getMostRecentAttack(); - if(!recentAttacks.isEmpty()) { + if(attack != null) { lastAttackedAt = now; - DamageType type = recentAttacks.get(recentAttacks.size() - 1).getDamageType(); + DamageType type = attack.getDamageType(); if(currentShield == null && now >= shieldStart + (recharge * 1000)) { if(defenses.contains(type)) { diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 76dde89..60f78bc 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -32,7 +32,6 @@ import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityAttack; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.entity.npc.Npc; -import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemUseType; @@ -209,7 +208,7 @@ public class Player extends Entity implements CommandExecutor { } @Override - public void die(Entity killer) { + public void die(EntityAttack cause) { statistics.trackDeath(); sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD)); // TODO killer id GameServer.getInstance().notify(String.format("%s died", name), NotificationType.CHAT); @@ -241,11 +240,6 @@ public class Player extends Entity implements CommandExecutor { sendMessage(new HealthMessage(health)); } - @Override - public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) { - super.attack(attacker, weapon, isGodMode() ? 0.0F : baseDamage, damageType); - } - @Override public float getAttackMultiplier(EntityAttack attack) { return isGodMode() ? 9999.0F : 1.0F; @@ -256,6 +250,11 @@ public class Player extends Entity implements CommandExecutor { return getNormalizedSkill(Skill.SURVIVAL) * 0.5F; } + @Override + public boolean isInvulnerable() { + return invulnerable || isGodMode(); + } + @Override public void setProperties(Map properties, boolean sendMessage) { super.setProperties(properties, sendMessage); diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java index 9c2117b..3bf963b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -156,7 +156,7 @@ public class SwitchInteraction implements ItemInteraction { } // Create explosion - DamageType damageType = DamageType.fromName(type); + DamageType damageType = type.equalsIgnoreCase("electric") ? DamageType.ENERGY : DamageType.fromName(type); String effect = String.format("bomb-%s", type.toLowerCase()); zone.explode(metaBlock.getX(), metaBlock.getY(), 6, entity, false, 6, damageType, effect); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java index b1e0e94..4929b31 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java @@ -2,7 +2,10 @@ package brainwine.gameserver.server.requests; import brainwine.gameserver.annotations.OptionalField; import brainwine.gameserver.annotations.RequestInfo; +import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; import brainwine.gameserver.server.PlayerRequest; @RequestInfo(id = 18) @@ -18,10 +21,18 @@ public class HealthRequest extends PlayerRequest { public void process(Player player) { float health = this.health / 1000.0F; - if(!player.isGodMode() && health >= player.getHealth()) { + // Prevent self-healing unless player has god mode enabled + if(health >= player.getHealth()) { + if(player.isGodMode()) { + player.setHealth(health); + } + return; } - player.damage(player.getHealth() - health, null); + // TODO attacker ID is always zero on v3 and damage type seems to do nothing on both v2 and v3 so we'll just have to do what we can here + Entity attacker = player.getZone().getEntity(attackerId); + float damage = player.getHealth() - health; + player.attack(attacker, Item.AIR, damage, DamageType.ACID, true); // Deal true damage; the client should have already applied any damage modifiers } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 03f1bb1..6ced37e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -305,9 +305,10 @@ public class Zone { sendMessageToChunk(new EffectMessage(x + 0.5f, y + 0.5f, effect, radius), getChunk(x, y)); Player player = cause instanceof Player ? (Player)cause : null; + Item item = getBlock(x, y).getFrontItem(); // Try to destroy the block at the source of the explosion - if(getBlock(x, y).getFrontItem().getFieldability() == Fieldability.FALSE) { + if(item.getFieldability() == Fieldability.FALSE) { updateBlock(x, y, Layer.FRONT, 0); if(destructive && !isBlockProtected(x, y, player)) { @@ -400,7 +401,7 @@ public class Zone { if(entity.canSee(x, y)) { double distance = MathUtils.distance(x, y, entity.getX(), entity.getY()); float damage = (float)(baseDamage - distance); - entity.attack(player, null, damage, damageType); // TODO maybe track the bomb used to damage the entity + entity.attack(cause, item, damage, damageType); } } } From 9dc5cbd34fb64bda2d51fd54e3fc50f93a1d65a1 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:42:50 +0100 Subject: [PATCH 17/47] Lazy fix for potential crashes --- .../main/java/brainwine/gameserver/util/MapHelper.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java b/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java index 5f0b88e..4890cf2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java +++ b/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java @@ -50,6 +50,10 @@ public class MapHelper { } public static void put(Map map, String path, Object value) { + if(path == null) { + return; + } + String[] segments = path.split("\\."); Map current = (Map)map; @@ -76,6 +80,10 @@ public class MapHelper { } public static T get(Map map, String path, Class type, T def) { + if(path == null) { + return def; + } + String[] segments = path.split("\\."); Map current = (Map)map; From 188b78d5b1e2099ae69b5f646fe1fcb2d49400c7 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 22:04:58 +0100 Subject: [PATCH 18/47] Implement persistence for certain NPCs --- .../java/brainwine/gameserver/Naming.java | 117 ++++++++++++++++++ .../brainwine/gameserver/entity/Entity.java | 23 ++-- .../gameserver/entity/EntityConfig.java | 10 ++ .../brainwine/gameserver/entity/npc/Npc.java | 14 ++- .../gameserver/entity/npc/NpcData.java | 47 +++++++ .../gameserver/zone/EntityManager.java | 36 ++++-- .../java/brainwine/gameserver/zone/Zone.java | 9 ++ .../gameserver/zone/ZoneManager.java | 33 ++++- .../gameserver/zone/gen/ZoneGenerator.java | 39 +----- 9 files changed, 266 insertions(+), 62 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/Naming.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java diff --git a/gameserver/src/main/java/brainwine/gameserver/Naming.java b/gameserver/src/main/java/brainwine/gameserver/Naming.java new file mode 100644 index 0000000..48cc0a2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/Naming.java @@ -0,0 +1,117 @@ +package brainwine.gameserver; + +/** + * TODO all I'm doing here is moving the problem somewhere else. + * + * Entity names are sourced from: https://github.com/bytebin/deepworld-gameserver/blob/master/config/fake.yml + */ +public class Naming { + + private static final String[] ZONE_FIRST_NAMES = { + "Malvern", "Tralee", "Horncastle", "Old", "Westwood", + "Citta", "Tadley", "Mossley", "West", "East", + "North", "South", "Wadpen", "Githam", "Soatnust", + "Highworth", "Creakynip", "Upper", "Lower", "Cannock", + "Dovercourt", "Limerick", "Pickering", "Glumshed", "Crusthack", + "Osyltyr", "Aberstaple", "New", "Stroud", "Crumclum", + "Crumsidle", "Bankswund", "Fiddletrast", "Bournpan", "St.", + "Funderbost", "Bexwoddly", "Pilkingheld", "Wittlepen", "Rabbitbleaker", + "Griffingumby", "Guilthead", "Bigglelund", "Bunnymold", "Rosesidle", + "Crushthorn", "Tanlyward", "Ahncrace", "Pilkingking", "Dingstrath", + "Axebury", "Ginglingtap", "Ballybibby", "Shadehoven" + }; + + private static final String[] ZONE_LAST_NAMES = { + "Falls", "Alloa", "Glen", "Way", "Dolente", + "Peak", "Heights", "Creek", "Banffshire", "Chagford", + "Gorge", "Valley", "Catacombs", "Depths", "Mines", + "Crickbridge", "Guildbost", "Pits", "Vaults", "Ruins", + "Dell", "Keep", "Chatterdin", "Scrimmance", "Gitwick", + "Ridge", "Alresford", "Place", "Bridge", "Glade", + "Mill", "Court", "Dooftory", "Hills", "Specklewint", + "Grove", "Aylesbury", "Wagwouth", "Russetcumby", "Point", + "Canyon", "Cranwarry", "Bluff", "Passage", "Crantippy", + "Kerbodome", "Dale", "Cemetery" + }; + + public static final String[] ENTITY_FIRST_NAMES = { + "Aaron", "Abby", "Abigale", "Abraham", "Ada", "Adella", "Agnes", "Alan", + "Albert", "Alexander", "Allie", "Almira", "Almyra", "Alonzo", "Alva", "Ambrose", + "Amelia", "Amon", "Amos", "Andrew", "Ann", "Annie", "Aquilla", "Archibald", + "Arnold", "Arrah", "Asa", "Augustus", "Barnabas", "Bartholomew", "Beatrice", "Becky", + "Benedict", "Benjamin", "Bennet", "Bernard", "Bernice", "Bertram", "Bess", "Bessie", + "Beth", "Betsy", "Buford", "Byron", "Calvin", "Charity", "Charles", "Charlotte", + "Chastity", "Christopher", "Claire", "Clarence", "Clement", "Clinton", "Cole", "Columbus", + "Commodore Perry", "Constance", "Cynthia", "Daniel", "David", "Dick", "Dorothy", "Edith", + "Edmund", "Edna", "Edward", "Edwin", "Edwina", "Eldon", "Eleanor", "Eli", + "Elijah", "Eliza", "Elizabeth", "Ella", "Ellie", "Elvira", "Emma", "Emmett", + "Enoch", "Esther", "Ethel", "Ettie", "Eudora", "Eva", "Ezekiel", "Ezra", + "Fanny", "Fidelia", "Flora", "Florence", "Frances", "Francis", "Franklin", "Frederick", + "Gabriel", "Garrett", "Geneve", "Genevieve", "George", "George", "Georgia", "Gertie", + "Gertrude", "Gideon", "Gilbert", "Ginny", "Gladys", "Grace", "Granville", "Hannah", + "Harland", "Harold", "Harrison", "Harvey", "Hattie", "Helen", "Helene", "Henrietta", + "Henry", "Hester", "Hettie", "Hiram", "Hope", "Horace", "Horatio", "Hortence", + "Hugh", "Isaac", "Isaac Newton", "Isabella", "Isaiah", "Israel", "Jacob", "James", + "Jane", "Jasper", "Jedediah", "Jefferson", "Jennie", "Jeptha", "Jessamine", "Jesse", + "Joel", "John Paul", "John Wesley", "Jonathan", "Joseph", "Josephine", "Josephus", "Joshua", + "Josiah", "Judith", "Julia", "Julian", "Juliet", "Julius", "Katherine", "Lafayette", + "Laura", "Lawrence", "Leah", "Leander", "Lenora", "Les", "Letitia", "Levi", + "Levi", "Lewis", "Lila", "Lilly", "Liza", "Lorena", "Lorraine", "Lottie", + "Louis", "Louisa", "Louise", "Lucas", "Lucas", "Lucian", "Lucian", "Lucius", + "Lucius", "Lucy", "Luke", "Luke", "Lulu", "Luther", "Luther", "Lydia", + "Mahulda", "Marcellus", "Margaret", "Mark", "Martha", "Martin", "Mary", "Mary Elizabeth", + "Mary Frances", "Masheck", "Matilda", "Matthew", "Maude", "Maurice", "Maxine", "Maxwell", + "Mercy", "Meriwether", "Meriwether Lewis", "Merrill", "Mildred", "Minerva", "Missouri", "Molly", + "Mordecai", "Morgan", "Morris", "Myrtle", "Nancy", "Natalie", "Nathaniel", "Ned", + "Nellie", "Nettie", "Newton", "Nicholas", "Nimrod", "Ninian", "Nora", "Obediah", + "Octavius", "Orpha", "Orville", "Oscar", "Owen", "Parthena", "Patrick", "Patrick Henry", + "Patsy", "Paul", "Paul", "Peggy", "Permelia", "Perry", "Peter", "Philomena", + "Phoebe", "Pleasant", "Polly", "Preshea", "Rachel", "Ralph", "Raymond", "Rebecca", + "Reuben", "Rhoda", "Richard", "Robert", "Robert Lee", "Roderick", "Rowena", "Rudolph", + "Rufina", "Rufus", "Ruth", "Sally", "Sam Houston", "Samantha", "Samuel", "Sarah", + "Sarah Ann", "Sarah Elizabeth", "Savannah", "Selina", "Seth", "Silas", "Simeon", "Simon", + "Sophronia", "Stanley", "Stella", "Stephen", "Thaddeus", "Theodore", "Theodosia", "Thomas", + "Timothy", "Ulysses", "Uriah", "Vertiline", "Victor", "Victoria", "Virginia", "Vivian", + "Walter", "Warren", "Washington", "Wilfred", "William", "Winnifred", "Zachariah", "Zebulon", + "Zedock", "Zona", "Zylphia" + }; + + public static final String[] ENTITY_LAST_NAMES = { + "Abraham", "Adams", "Alcorn", "Alderdice", "Angus", "Ashdown", "Ayre", "Backhaus", + "Baldwin", "Bamford", "Beaton", "Blackwood", "Blair", "Blewett", "Bornholdt", "Bowden", + "Burrows", "Cameron", "Carroll", "Clarke", "Claxton", "Collins", "Colson", "Connor", + "Conroy", "Cullen", "Cunningham", "Curd", "Curnow", "Cusack", "Dagon", "Dalton", + "Dawes", "Desmond", "Dewar", "Dickenson", "Donnell", "Drummond", "Dunstan", "English", + "Eveans", "Faraday", "Faulkner", "Fitzgerald", "Fitzpatrick", "Fletcher", "Foster", "Franklin", + "Fulton", "Gallagher", "Gibbons", "Gilmore", "Glover", "Goodfellow", "Goodwin", "Griffiths", + "Gullifer", "Hadley", "Haeffner", "Hanlon", "Harding", "Harris", "Holloway", "Hughes", + "Jarvis", "Jefferies", "Johnstone", "Kaylock", "Keane", "Kemp", "Kernaghan", "Kirby", + "Kirkland", "Knight", "LaFontaine", "Lawford", "Lawrence", "Lennox", "Longley", "Lonsdale", + "Luckett", "Lyons", "Macklin", "Madill", "Marsden", "Marshall", "Martin", "Mather", + "Mathieson", "Maunder", "McColl", "McDermott", "McGillicuddy", "McKenzie", "McLachlan", "McNeil", + "Meaklim", "Meighan", "Mellor", "Meyers", "Milsom", "Mitchell", "Mitchelson", "Moore", + "Morgan", "Morrison", "Mortimer", "Moulsdale", "Murphy", "Nelson", "Nolan", "Noonan", + "O'Keefe", "O'Sullivan", "Palmer", "Parnell", "Pattison", "Pettit", "Phillips", "Pinner", + "Porter", "Prosser", "Ramseyer", "Renton", "Rickard", "Riddington", "Roche", "Rowe", + "Russell", "Salisbury", "Saunders", "Sawyer", "Scanlan", "Scarborough", "Schwarer", "Sheary", + "Sheedy", "Shelton", "Shields", "Shinnick", "Skinner", "Sommer", "Spencer", "Stanbury", + "Stanton", "Storey", "Swaisbrick", "Thorley", "Thumpston", "Tichborne", "Tinning", "Tobin", + "Todd", "Trimble", "Twomey", "Upton", "Urwin", "Vandenburg", "Vinge", "Wakefield", + "Wakenshaw", "Walden", "Wallace", "Walton", "Warner", "Webb", "Whitehill", "Wickes", + "Wilberforce", "Wilkinson", "Wolstenholme", "Wright" + }; + + public static String getRandomZoneName() { + return getRandomName(ZONE_FIRST_NAMES, ZONE_LAST_NAMES); + } + + public static String getRandomEntityName() { + return getRandomName(ENTITY_FIRST_NAMES, ENTITY_LAST_NAMES); + } + + private static String getRandomName(String[] firstNames, String[] lastNames) { + String firstName = firstNames[(int)(Math.random() * firstNames.length)]; + String lastName = lastNames[(int)(Math.random() * lastNames.length)]; + return firstName + " " + lastName; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 9700335..43124bf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -59,15 +59,7 @@ public abstract class Entity { long now = System.currentTimeMillis(); // Update block position - lastBlockX = blockX; - lastBlockY = blockY; - blockX = (int)x; - blockY = (int)y; - - // Check if block position has changed - if(lastBlockX != blockX || lastBlockY != blockY) { - blockPositionChanged(); - } + updateBlockPosition(); // Clear expired recent attacks recentAttacks.removeIf(attack -> now >= attack.getTime() + ATTACK_RETENTION_TIME); @@ -129,6 +121,18 @@ public abstract class Entity { return 1.0F; // Override } + public void updateBlockPosition() { + lastBlockX = blockX; + lastBlockY = blockY; + blockX = (int)x; + blockY = (int)y; + + // Check if block position has changed + if(lastBlockX != blockX || lastBlockY != blockY) { + blockPositionChanged(); + } + } + public void blockPositionChanged() { // Check for touchplates if(zone != null && zone.isChunkLoaded(blockX, blockY)) { @@ -263,6 +267,7 @@ public abstract class Entity { public void setPosition(float x, float y) { this.x = x; this.y = y; + updateBlockPosition(); } public float getX() { diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java index 9fec361..0da885c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java @@ -29,6 +29,8 @@ public class EntityConfig { private int experienceYield; private float maxHealth = Entity.DEFAULT_HEALTH; private float baseSpeed = 3; + private boolean character; + private boolean named; private Vector2i size = new Vector2i(1, 1); private EntityGroup group = EntityGroup.NONE; private WeightedMap loot = new WeightedMap<>(); @@ -79,6 +81,14 @@ public class EntityConfig { return baseSpeed; } + public boolean isCharacter() { + return character; + } + + public boolean isNamed() { + return named; + } + @JsonSetter(nulls = Nulls.SKIP) private void setSize(Vector2i size) { this.size = size; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index eacd0f1..5989a5f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ThreadLocalRandom; +import brainwine.gameserver.Naming; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityAttack; import brainwine.gameserver.entity.EntityConfig; @@ -32,6 +33,7 @@ public class Npc extends Entity { private final String typeName; private final float maxHealth; private final float baseSpeed; + private final boolean persist; private final Vector2i size; private final WeightedMap loot; private final WeightedMap placedLoot; @@ -98,11 +100,17 @@ public class Npc extends Entity { properties.put("sl", slots); } + // Generate random name + if(config.isNamed()) { + this.name = Naming.getRandomEntityName(); + } + this.config = config; this.typeName = config.getName(); this.type = config.getType(); this.maxHealth = config.getMaxHealth(); this.baseSpeed = config.getBaseSpeed(); + this.persist = config.isCharacter(); this.size = config.getSize(); this.loot = config.getLoot(); this.placedLoot = config.getPlacedLoot(); @@ -267,7 +275,7 @@ public class Npc extends Entity { } public boolean isTransient() { - return !isGuard() && !isMounted(); + return !isGuard() && !isMounted() && !persist; } public EntityLoot getRandomLoot(Player awardee, Item weapon) { @@ -368,6 +376,10 @@ public class Npc extends Entity { return artificial; } + public boolean isPersistent() { + return persist; + } + public void setSpeed(float speed) { this.speed = speed; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java new file mode 100644 index 0000000..1d17330 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java @@ -0,0 +1,47 @@ +package brainwine.gameserver.entity.npc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import brainwine.gameserver.entity.EntityConfig; + +/** + * Storage data for persistent non-player characters. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class NpcData { + + private EntityConfig type; + private String name; + private int x; + private int y; + + @JsonCreator + public NpcData(@JsonProperty(value = "type", required = true) EntityConfig type) { + this.type = type; + } + + public NpcData(Npc npc) { + this.type = npc.getConfig(); + this.name = npc.getName(); + this.x = npc.getBlockX(); + this.y = npc.getBlockY(); + } + + public EntityConfig getType() { + return type; + } + + public String getName() { + return name; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java index 35ec860..8b7ac85 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -20,12 +20,12 @@ import org.apache.logging.log4j.Logger; import com.fasterxml.jackson.core.type.TypeReference; -import brainwine.gameserver.GameServer; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.NpcData; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; @@ -175,8 +175,8 @@ public class EntityManager { private void clearEntities() { npcs.values().stream() - .filter(npc -> npc.isDead() || !zone.isChunkLoaded((int)npc.getX(), (int)npc.getY()) || - (npc.isTransient() && System.currentTimeMillis() > npc.getLastTrackedAt() + ENTITY_CLEAR_TIME)) + .filter(npc -> npc.isDead() || (!npc.isPersistent() && (!zone.isChunkLoaded(npc.getBlockX(), npc.getBlockY()) || + (npc.isTransient() && System.currentTimeMillis() > npc.getLastTrackedAt() + ENTITY_CLEAR_TIME)))) .collect(Collectors.toList()) .forEach(this::removeEntity); } @@ -249,18 +249,28 @@ public class EntityManager { } } + public void spawnPersistentNpcs(Collection data) { + for(NpcData entry : data) { + if(entry.getType() == null) { + continue; + } + + Npc npc = new Npc(zone, entry.getType()); + npc.setName(entry.getName()); + spawnEntity(npc, entry.getX(), entry.getY()); + } + } + public void spawnEntity(Entity entity, int x, int y) { spawnEntity(entity, x, y, false); } public void spawnEntity(Entity entity, int x, int y, boolean effect) { - if(zone.isChunkLoaded(x, y)) { - addEntity(entity); - entity.setPosition(x, y); - - if(effect) { - zone.sendMessageToChunk(new EffectMessage(x + 0.5F, y + 0.5F, "bomb-teleport", 4), zone.getChunk(x, y)); - } + addEntity(entity); + entity.setPosition(x, y); + + if(effect && zone.isChunkLoaded(x, y)) { + zone.sendMessageToChunk(new EffectMessage(x + 0.5F, y + 0.5F, "bomb-teleport", 4), zone.getChunk(x, y)); } } @@ -332,13 +342,17 @@ public class EntityManager { } public int getTransientNpcCount() { - return (int)npcs.values().stream().filter(npc -> npc.isTransient()).count(); + return (int)npcs.values().stream().filter(Npc::isTransient).count(); } public Collection getNpcs() { return Collections.unmodifiableCollection(npcs.values()); } + public List getPersistentNpcs() { + return npcs.values().stream().filter(Npc::isPersistent).collect(Collectors.toList()); + } + public Player getPlayer(int entityId) { return players.get(entityId); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 6ced37e..0ce28ce 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -25,6 +25,7 @@ import brainwine.gameserver.GameServer; import brainwine.gameserver.Timer; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.entity.npc.NpcData; import brainwine.gameserver.entity.player.ChatType; import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; @@ -1095,6 +1096,10 @@ public class Zone { return entityManager.getPlayersInRange(x, y, range); } + public void spawnPersistentNpcs(Collection data) { + entityManager.spawnPersistentNpcs(data); + } + public void spawnEntity(Entity entity, int x, int y) { entityManager.spawnEntity(entity, x, y); } @@ -1135,6 +1140,10 @@ public class Zone { return entityManager.getNpcs(); } + public List getPersistentNpcs() { + return entityManager.getPersistentNpcs(); + } + public Player getPlayer(int entityId) { return entityManager.getPlayer(entityId); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java index 4398e73..bfcac9d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java @@ -13,6 +13,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.zip.DataFormatException; import org.apache.logging.log4j.LogManager; @@ -24,6 +25,7 @@ import org.msgpack.jackson.dataformat.MessagePackFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import brainwine.gameserver.entity.npc.NpcData; import brainwine.gameserver.util.ZipUtils; import brainwine.gameserver.zone.gen.ZoneGenerator; import brainwine.shared.JsonHelper; @@ -84,6 +86,9 @@ public class ZoneManager { String id = file.getName(); File dataFile = new File(file, "zone.dat"); File legacyDataFile = new File(file, "shape.cmp"); + File configFile = new File(file, "config.json"); + File metaBlocksFile = new File(file, "metablocks.json"); + File charactersFile = new File(file, "characters.json"); try { ZoneDataFile data = null; @@ -95,9 +100,19 @@ public class ZoneManager { data = mapper.readValue(ZipUtils.inflateBytes(Files.readAllBytes(dataFile.toPath())), ZoneDataFile.class); } - ZoneConfigFile config = JsonHelper.readValue(new File(file, "config.json"), ZoneConfigFile.class); + ZoneConfigFile config = JsonHelper.readValue(configFile, ZoneConfigFile.class); Zone zone = new Zone(id, config, data); - zone.setMetaBlocks(JsonHelper.readList(new File(file, "metablocks.json"), MetaBlock.class)); + + // Load meta blocks + if(metaBlocksFile.exists()) { + zone.setMetaBlocks(JsonHelper.readList(metaBlocksFile, MetaBlock.class)); + } + + // Load characters + if(charactersFile.exists()) { + zone.spawnPersistentNpcs(JsonHelper.readList(charactersFile, NpcData.class)); + } + addZone(zone); } catch (Exception e) { logger.error(SERVER_MARKER, "Zone load failure. id: {}", id, e); @@ -147,10 +162,18 @@ public class ZoneManager { file.mkdirs(); try { + // Serialize everything before writing to disk to minimize risk of data corruption if something goes wrong + byte[] charactersBytes = JsonHelper.writeValueAsBytes(zone.getPersistentNpcs().stream().map(NpcData::new).collect(Collectors.toList())); + byte[] metaBlocksBytes = JsonHelper.writeValueAsBytes(zone.getMetaBlocks()); + byte[] configBytes = JsonHelper.writeValueAsBytes(new ZoneConfigFile(zone)); + byte[] dataBytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(new ZoneDataFile(zone))); + + // Write data to files zone.saveChunks(); - JsonHelper.writeValue(new File(file, "metablocks.json"), zone.getMetaBlocks()); - JsonHelper.writeValue(new File(file, "config.json"), new ZoneConfigFile(zone)); - Files.write(new File(file, "zone.dat").toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(new ZoneDataFile(zone)))); + Files.write(new File(file, "characters.json").toPath(), charactersBytes); + Files.write(new File(file, "metablocks.json").toPath(), metaBlocksBytes); + Files.write(new File(file, "config.json").toPath(), configBytes); + Files.write(new File(file, "zone.dat").toPath(), dataBytes); } catch(Exception e) { logger.error(SERVER_MARKER, "Zone save failure. id: {}", zone.getDocumentId(), e); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java index 0daaa6d..542a526 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java @@ -13,6 +13,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import brainwine.gameserver.GameServer; +import brainwine.gameserver.Naming; import brainwine.gameserver.item.Layer; import brainwine.gameserver.util.ResourceUtils; import brainwine.gameserver.zone.Biome; @@ -26,34 +27,6 @@ import brainwine.shared.JsonHelper; public class ZoneGenerator { - // TODO Collect more names and create a name generator that's actually proper lmao - private static final String[] FIRST_NAMES = { - "Malvern", "Tralee", "Horncastle", "Old", "Westwood", - "Citta", "Tadley", "Mossley", "West", "East", - "North", "South", "Wadpen", "Githam", "Soatnust", - "Highworth", "Creakynip", "Upper", "Lower", "Cannock", - "Dovercourt", "Limerick", "Pickering", "Glumshed", "Crusthack", - "Osyltyr", "Aberstaple", "New", "Stroud", "Crumclum", - "Crumsidle", "Bankswund", "Fiddletrast", "Bournpan", "St.", - "Funderbost", "Bexwoddly", "Pilkingheld", "Wittlepen", "Rabbitbleaker", - "Griffingumby", "Guilthead", "Bigglelund", "Bunnymold", "Rosesidle", - "Crushthorn", "Tanlyward", "Ahncrace", "Pilkingking", "Dingstrath", - "Axebury", "Ginglingtap", "Ballybibby", "Shadehoven" - }; - - private static final String[] LAST_NAMES = { - "Falls", "Alloa", "Glen", "Way", "Dolente", - "Peak", "Heights", "Creek", "Banffshire", "Chagford", - "Gorge", "Valley", "Catacombs", "Depths", "Mines", - "Crickbridge", "Guildbost", "Pits", "Vaults", "Ruins", - "Dell", "Keep", "Chatterdin", "Scrimmance", "Gitwick", - "Ridge", "Alresford", "Place", "Bridge", "Glade", - "Mill", "Court", "Dooftory", "Hills", "Specklewint", - "Grove", "Aylesbury", "Wagwouth", "Russetcumby", "Point", - "Canyon", "Cranwarry", "Bluff", "Passage", "Crantippy", - "Kerbodome", "Dale", "Cemetery" - }; - private static final Logger logger = LogManager.getLogger(); private static final Map generators = new HashMap<>(); private static final ZoneGenerator defaultGenerator = new ZoneGenerator(); @@ -158,7 +131,7 @@ public class ZoneGenerator { public Zone generateZone(Biome biome, int width, int height, int seed) { String id = generateDocumentId(seed); - String name = getRandomName(); + String name = Naming.getRandomZoneName(); int retryCount = 0; while(GameServer.getInstance().getZoneManager().getZoneByName(name) != null) { @@ -168,7 +141,7 @@ public class ZoneGenerator { break; } - name = getRandomName(); + name = Naming.getRandomZoneName(); retryCount++; } @@ -210,12 +183,6 @@ public class ZoneGenerator { return new UUID(mostSigBits, leastSigBits).toString(); } - private static String getRandomName() { - String firstName = FIRST_NAMES[(int)(Math.random() * FIRST_NAMES.length)]; - String lastName = LAST_NAMES[(int)(Math.random() * LAST_NAMES.length)]; - return firstName + " " + lastName; - } - private static int getRandomSeed() { return (int)(Math.random() * Integer.MAX_VALUE); } From 20a9b5b8268991f0ebb9033a55bbe3de8fb6491e Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 22:05:04 +0100 Subject: [PATCH 19/47] Remove unused import --- .../gameserver/entity/npc/behavior/parts/ShielderBehavior.java | 1 - 1 file changed, 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java index 5375743..664fd2c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java @@ -2,7 +2,6 @@ package brainwine.gameserver.entity.npc.behavior.parts; import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; From 38cf04187060445c67f5406a570cf8e50cd2fbbb Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 18 Feb 2024 23:51:50 +0100 Subject: [PATCH 20/47] Add a bunch of utility functions --- .../brainwine/gameserver/entity/Entity.java | 27 ++++++++++++++++ .../brainwine/gameserver/entity/npc/Npc.java | 16 ++++------ .../behavior/parts/SpawnAttackBehavior.java | 6 ++-- .../npc/behavior/parts/UnblockBehavior.java | 6 ++-- .../gameserver/entity/player/Player.java | 4 +-- .../item/interactions/SwitchInteraction.java | 12 +++---- .../server/requests/BlockMineRequest.java | 3 +- .../gameserver/zone/EntityManager.java | 3 +- .../java/brainwine/gameserver/zone/Zone.java | 31 +++++++++++++++---- 9 files changed, 70 insertions(+), 38 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 43124bf..5efa62a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -12,6 +12,7 @@ import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.server.messages.EntityChangeMessage; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; @@ -45,6 +46,8 @@ public abstract class Entity { protected int lastBlockY; protected int targetX; protected int targetY; + protected int sizeX = 1; + protected int sizeY = 1; protected FacingDirection direction = FacingDirection.WEST; protected int animation; protected boolean invulnerable; @@ -121,6 +124,22 @@ public abstract class Entity { return 1.0F; // Override } + public void spawnEffect(String type) { + spawnEffect(type, 1); + } + + public void spawnEffect(String type, Object data) { + float effectX = x + sizeX / 2.0F; + float effectY = y + sizeY / 2.0F; + sendMessageToTrackers(new EffectMessage(effectX, effectY, type, data)); + } + + public void emote(String message) { + float effectX = x + sizeX / 2.0F; + float effectY = y - sizeY + 1; + sendMessageToTrackers(new EffectMessage(effectX, effectY, "emote", message)); + } + public void updateBlockPosition() { lastBlockX = blockX; lastBlockY = blockY; @@ -312,6 +331,14 @@ public abstract class Entity { return blockY; } + public int getSizeX() { + return sizeX; + } + + public int getSizeY() { + return sizeY; + } + public void setDirection(FacingDirection direction) { this.direction = direction; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index 5989a5f..1b93189 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -34,7 +34,6 @@ public class Npc extends Entity { private final float maxHealth; private final float baseSpeed; private final boolean persist; - private final Vector2i size; private final WeightedMap loot; private final WeightedMap placedLoot; private final Map> lootByWeapon; @@ -111,7 +110,8 @@ public class Npc extends Entity { this.maxHealth = config.getMaxHealth(); this.baseSpeed = config.getBaseSpeed(); this.persist = config.isCharacter(); - this.size = config.getSize(); + this.sizeX = config.getSize().getX(); + this.sizeY = config.getSize().getY(); this.loot = config.getLoot(); this.placedLoot = config.getPlacedLoot(); this.lootByWeapon = config.getLootByWeapon(); @@ -262,10 +262,6 @@ public class Npc extends Entity { return config; } - public Vector2i getSize() { - return size; - } - public void setDefense(DamageType type, float amount) { if(amount == 0) { activeDefenses.remove(type); @@ -411,15 +407,15 @@ public class Npc extends Entity { int tY = y + oY; boolean blocked = zone.isBlockSolid(tX, tY) || (oX != 0 && zone.isBlockSolid(tX, y)) || (oY != 0 && zone.isBlockSolid(x, tY)); - if(size.getX() > 1) { - int additionalWidth = size.getX() - 1; + if(sizeX > 1) { + int additionalWidth = sizeX - 1; blocked = blocked || zone.isBlockSolid(tX + additionalWidth, tY) || (oX != 0 && zone.isBlockSolid(tX + additionalWidth, y)) || (oY != 0 && zone.isBlockSolid(x + additionalWidth, tY)); } - if(size.getY() > 1) { - int additionalHeight = size.getY() - 1; + if(sizeY > 1) { + int additionalHeight = sizeY - 1; blocked = blocked || zone.isBlockSolid(tX, tY - additionalHeight) || (oX != 0 && zone.isBlockSolid(tX, y - additionalHeight)) || (oY != 0 && zone.isBlockSolid(x, tY - additionalHeight)); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java index 4c8d615..9871ad3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java @@ -12,7 +12,6 @@ import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.npc.behavior.Behavior; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; -import brainwine.gameserver.util.Vector2i; public class SpawnAttackBehavior extends Behavior { @@ -41,9 +40,8 @@ public class SpawnAttackBehavior extends Behavior { if(npc) { // Spawn child at parent's location - Vector2i size = entity.getSize(); - int spawnX = (int)(entity.getX() + (size.getX() / 2F)); - int spawnY = (int)(entity.getY() + (size.getY() / 2F)); + int spawnX = (int)(entity.getX() + (entity.getSizeX() / 2.0F)); + int spawnY = (int)(entity.getY() + (entity.getSizeX() / 2.0F)); Npc child = new Npc(entity.getZone(), entityConfig); child.setOwner(entity); entity.addChild(child); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java index e1615b0..8c40083 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java @@ -8,7 +8,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.npc.behavior.Behavior; -import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.zone.Zone; public class UnblockBehavior extends Behavior { @@ -23,12 +22,11 @@ public class UnblockBehavior extends Behavior { @Override public boolean behave() { Zone zone = entity.getZone(); - Vector2i size = entity.getSize(); Random random = ThreadLocalRandom.current(); for(int i = 0; i < rate; i++) { - int x = (int)entity.getX() + random.nextInt(size.getX()); - int y = (int)entity.getY() - random.nextInt(size.getY()); + int x = (int)entity.getX() + random.nextInt(entity.getSizeX()); + int y = (int)entity.getY() - random.nextInt(entity.getSizeY()); if(zone.isChunkLoaded(x, y) && zone.getBlock(x, y).getFrontItem().isDiggable()) { zone.digBlock(x, y); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 60f78bc..fc0b058 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -546,7 +546,7 @@ public class Player extends Entity implements CommandExecutor { sendMessage(new PlayerPositionMessage(spawnX, spawnY)); sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.REVIVED)); - zone.sendMessage(new EffectMessage(spawnX, spawnY, "spawn", 20)); + zone.spawnEffect(spawnX, spawnY, "spawn", 20); } /** @@ -561,7 +561,7 @@ public class Player extends Entity implements CommandExecutor { teleportY = y; sendMessage(new TeleportMessage(x, y)); sendMessage(new PlayerPositionMessage(x, y)); - zone.sendMessage(new EffectMessage(x, y, "teleport", 20)); + zone.spawnEffect(x, y, "teleport", 20); } public int getTeleportX() { diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java index 3bf963b..a757504 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -16,11 +16,8 @@ import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; -import brainwine.gameserver.server.messages.BlockMetaMessage; -import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.Vector2i; -import brainwine.gameserver.zone.Chunk; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; @@ -50,7 +47,7 @@ public class SwitchInteraction implements ItemInteraction { if(message != null && !message.isEmpty()) { float effectX = x + item.getBlockWidth() / 2.0F; float effectY = y - item.getBlockHeight() + 1; - zone.sendMessageToChunk(new EffectMessage(effectX, effectY, "emote", message), zone.getChunk(x, y)); + zone.spawnEffect(effectX, effectY, "emote", message); } // Prepare list of targets @@ -116,8 +113,8 @@ public class SwitchInteraction implements ItemInteraction { Entity entity = zone.getEntity(metaBlock.getIntProperty("eid")); if(entity != null && !entity.isDead()) { + entity.spawnEffect("bomb-teleport", 4); entity.setHealth(0); - zone.sendMessageToChunk(new EffectMessage(entity.getX(), entity.getY(), "bomb-teleport", 4), zone.getChunk(metaBlock.getX(), metaBlock.getY())); } } @@ -207,8 +204,7 @@ public class SwitchInteraction implements ItemInteraction { // Send data to players float effectX = metaBlock.getX() + (float)item.getBlockWidth() / 2; float effectY = metaBlock.getY() - (float)item.getBlockHeight() / 2 + 1; - Chunk chunk = zone.getChunk(metaBlock.getX(), metaBlock.getY()); - zone.sendMessageToChunk(new EffectMessage(effectX, effectY, "area steam", 10), chunk); - zone.sendMessageToChunk(new BlockMetaMessage(metaBlock), chunk); + zone.spawnEffect(effectX, effectY, "area steam", 10); + zone.sendBlockMetaUpdate(metaBlock); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java index 3c80eec..349afbf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java @@ -21,7 +21,6 @@ import brainwine.gameserver.item.MiningBonus; import brainwine.gameserver.item.ModType; import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.messages.BlockChangeMessage; -import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.server.messages.InventoryMessage; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MathUtils; @@ -194,8 +193,8 @@ public class BlockMineRequest extends PlayerRequest { // Kill entity if it exists if(entity != null && !entity.isDead()) { + entity.spawnEffect("bomb-teleport", 4); entity.setHealth(0); - zone.sendMessageToChunk(new EffectMessage(entity.getX(), entity.getY(), "bomb-teleport", 4), zone.getChunk(metaBlock.getX(), metaBlock.getY())); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java index 8b7ac85..9e7a099 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -30,7 +30,6 @@ import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.ModType; -import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.server.messages.EntityPositionMessage; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; @@ -270,7 +269,7 @@ public class EntityManager { entity.setPosition(x, y); if(effect && zone.isChunkLoaded(x, y)) { - zone.sendMessageToChunk(new EffectMessage(x + 0.5F, y + 0.5F, "bomb-teleport", 4), zone.getChunk(x, y)); + zone.spawnEffect(x + 0.5F, y + 0.5F, "bomb-teleport", 4); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 0ce28ce..4fc21e6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -186,7 +186,7 @@ public class Zone { * @param message The message to send. * @param chunk The chunk near which players must be. */ - public void sendMessageToChunk(Message message, Chunk chunk) { + public void sendLocalMessage(Message message, Chunk chunk) { for(Player player : getPlayers()) { if(player.isChunkActive(chunk)) { player.sendMessage(message); @@ -194,6 +194,22 @@ public class Zone { } } + public void sendLocalMessage(Message message, float x, float y) { + sendLocalMessage(message, (int)x, (int)y); + } + + public void sendLocalMessage(Message message, int x, int y) { + if(!isChunkLoaded(x, y)) { + return; + } + + sendLocalMessage(message, getChunk(x, y)); + } + + public void sendBlockMetaUpdate(MetaBlock metaBlock) { + sendLocalMessage(new BlockMetaMessage(metaBlock), metaBlock.getX(), metaBlock.getY()); + } + public void sendChatMessage(Player sender, String text) { sendChatMessage(sender, text, ChatType.CHAT); } @@ -210,6 +226,10 @@ public class Zone { GameServer.getInstance().notify(String.format("%s: %s", sender.getName(), text), NotificationType.CHAT); } + public void spawnEffect(float x, float y, String type, Object data) { + sendLocalMessage(new EffectMessage(x, y, type, data), x, y); + } + public boolean isPointVisibleFrom(int x1, int y1, int x2, int y2) { return raycast(x1, y1, x2, y2) == null; } @@ -304,7 +324,7 @@ public class Zone { return; } - sendMessageToChunk(new EffectMessage(x + 0.5f, y + 0.5f, effect, radius), getChunk(x, y)); + spawnEffect(x + 0.5F, y + 0.5F, effect, radius); Player player = cause instanceof Player ? (Player)cause : null; Item item = getBlock(x, y).getFrontItem(); @@ -924,7 +944,7 @@ public class Zone { recalculateSunlight(x, sunlight[x]); } - sendMessageToChunk(new LightMessage(x, getSunlight(x, 1)), chunk); + sendLocalMessage(new LightMessage(x, getSunlight(x, 1)), chunk); } else if(layer == Layer.LIQUID) { if(!item.isAir() && mod > 0) { liquidManager.indexLiquidBlock(x, y); @@ -990,8 +1010,7 @@ public class Zone { switch(meta) { case LOCAL: - sendMessageToChunk(metaBlock == null ? new BlockMetaMessage(x, y) - : new BlockMetaMessage(metaBlock), getChunk(x, y)); + sendLocalMessage(metaBlock == null ? new BlockMetaMessage(x, y) : new BlockMetaMessage(metaBlock), x, y); break; case GLOBAL: sendMessage(new BlockMetaMessage(x, y)); // Send empty one first or it won't work for some reason @@ -1190,7 +1209,7 @@ public class Zone { // Update pending sunlight if(pendingSunlight.contains(x)) { recalculateSunlight(x, sunlight[x]); - sendMessageToChunk(new LightMessage(x, getSunlight(x, 1)), chunk); + sendLocalMessage(new LightMessage(x, getSunlight(x, 1)), chunk); } for(int y = chunkY; y < chunkY + chunk.getHeight(); y++) { From 5928ec5c9819909e633367faa63f7ad133744476 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:58:25 +0100 Subject: [PATCH 21/47] Implement steam system --- .../java/brainwine/gameserver/item/Item.java | 7 + .../server/requests/CraftRequest.java | 39 ++- .../gameserver/zone/SteamIteration.java | 35 +++ .../gameserver/zone/SteamManager.java | 227 ++++++++++++++++++ .../java/brainwine/gameserver/zone/Zone.java | 15 +- .../gameserver/zone/ZoneDataFile.java | 12 +- .../gameserver/zone/ZoneManager.java | 2 +- 7 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java index 2014db1..6e3a7ec 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java @@ -120,6 +120,9 @@ public class Item { @JsonProperty("entity") private boolean entity; + @JsonProperty("steam") + private boolean steam; + @JsonProperty("inventory") private LazyItemGetter inventoryItem; @@ -390,6 +393,10 @@ public class Item { return entity; } + public boolean usesSteam() { + return steam; + } + public Map getSkillBonuses() { return skillBonuses; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java index 4e4f9ea..b5f9454 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java @@ -1,6 +1,7 @@ package brainwine.gameserver.server.requests; import java.util.List; +import java.util.stream.Collectors; import brainwine.gameserver.annotations.OptionalField; import brainwine.gameserver.annotations.RequestInfo; @@ -13,10 +14,8 @@ import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.Pair; import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; -/** - * TODO Account for skills, bonuses etc.. - */ @RequestInfo(id = 19) public class CraftRequest extends PlayerRequest { @@ -54,21 +53,43 @@ public class CraftRequest extends PlayerRequest { return; } } - + // Check if required crafting helpers are nearby if(!player.isGodMode() && item.requiresWorkshop()) { - List workshop = player.getZone().getMetaBlocks(metaBlock - -> MathUtils.inRange(player.getX(), player.getY(), metaBlock.getX(), metaBlock.getY(), 10)); + Zone zone = player.getZone(); + // Fetch list of all meta blocks in the player's vicinity + List workshop = zone.getMetaBlocks(metaBlock -> zone.isChunkLoaded(metaBlock.getX(), metaBlock.getY()) + && MathUtils.inRange(player.getX(), player.getY(), metaBlock.getX(), metaBlock.getY(), 20)); + + // Check for each crafting helper if it is present in the workshop and available for use for(CraftingRequirement craftingHelper : item.getCraftingHelpers()) { - int quantityMissing = craftingHelper.getQuantity() - (int)workshop.stream().filter(metaBlock - -> metaBlock.getItem() == craftingHelper.getItem()).count(); + int quantityRequired = craftingHelper.getQuantity(); + // Fetch list of crafting helpers of this type that are present in the workshop + List presentCraftingHelpers = workshop.stream() + .filter(metaBlock -> metaBlock.getItem() == craftingHelper.getItem()).collect(Collectors.toList()); + int quantityMissing = quantityRequired - presentCraftingHelpers.size(); + + // Check if workshop is still missing crafting helpers of this type and notify the player if this is the case if(quantityMissing > 0) { - player.notify(String.format("You can't craft this item because your workshop is lacking %sx %s.", + player.notify(String.format("You can't craft this item because your workshop is still lacking %sx %s.", quantityMissing, craftingHelper.getItem().getTitle())); return; } + + // Perform additional checks if the crafting helper requires steam to function + if(craftingHelper.getItem().usesSteam()) { + quantityMissing = quantityRequired - (int)presentCraftingHelpers.stream() + .filter(metaBlock -> zone.getBlock(metaBlock.getX(), metaBlock.getY()).getFrontMod() == 1).count(); + + // Notify the player if not enough crafting helpers are powered + if(quantityMissing > 0) { + player.notify(String.format("You can't craft this item because your workshop still needs to provide steam power to %sx %s.", + quantityMissing, craftingHelper.getItem().getTitle())); + return; + } + } } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java b/gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java new file mode 100644 index 0000000..6728043 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java @@ -0,0 +1,35 @@ +package brainwine.gameserver.zone; + +/** + * Used by {@link SteamManager} to keep track of things. + */ +public class SteamIteration { + + private int x; + private int y; + private byte direction; + private short depth; + + public SteamIteration(int x, int y, int direction, int depth) { + this.x = x; + this.y = y; + this.direction = (byte)direction; + this.depth = (short)depth; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public byte getDirection() { + return direction; + } + + public short getDepth() { + return depth; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java new file mode 100644 index 0000000..cc86858 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java @@ -0,0 +1,227 @@ +package brainwine.gameserver.zone; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import brainwine.gameserver.item.Item; + +/** + * Distributes steam through collectors to nearby machines via pipes. + */ +public class SteamManager { + + public static final int STEAM_UPDATE_INTERVAL = 3000; // Update interval in milliseconds + public static final int MAX_ITERATIONS = 300; // Maximum number of iterations before giving up + public static final int MAX_COLLECTOR_DISTANCE = 350; // Collectors that are not within this distance of any players in the zone will be skipped + public static final byte STATE_EMPTY = 0x0; // Nothing or unrelated + public static final byte STATE_PIPE = 0x1; // Pipe + public static final byte STATE_COLLECTOR = 0x2; // Active collector + private final Set collectorIndices = new HashSet<>(); + private final Set steamableIndices = new HashSet<>(); + private final Set processedIndices = new HashSet<>(); + private final List expiredSteamableIndices = new ArrayList<>(); + private final Queue processQueue = new ArrayDeque<>(); + private final Zone zone; + private byte[] data; + private long lastUpdateAt; + + public SteamManager(Zone zone) { + this.zone = zone; + this.data = new byte[(zone.getWidth() * zone.getHeight()) >> 2]; + } + + public void tick(double deltaTime) { + long now = System.currentTimeMillis(); + + // Check if it's time to update steam yet + if(now > lastUpdateAt + STEAM_UPDATE_INTERVAL) { + updateSteam(); + lastUpdateAt = now; + } + } + + private void updateSteam() { + // Do nothing if there are no players in this zone + if(zone.getPlayerCount() == 0) { + return; + } + + // Clear data from previous run + processedIndices.clear(); + expiredSteamableIndices.clear(); + + // Turn off all steam-powered objects + for(int index : steamableIndices) { + int x = index % zone.getWidth(); + int y = index / zone.getWidth(); + + // Skip if chunk isn't loaded + if(!zone.isChunkLoaded(x, y)) { + expiredSteamableIndices.add(index); + continue; + } + + Item item = zone.getBlock(x, y).getFrontItem(); + + // Skip if front item doesn't use steam + if(!item.usesSteam()) { + expiredSteamableIndices.add(index); + continue; + } + + // Update block + zone.updateBlock(x, y, item.getLayer(), item, 0); + } + + // Unindex expired steamables + for(int index : expiredSteamableIndices) { + steamableIndices.remove(index); + } + + // Enqueue blocks at the spouts of all collectors + for(int index : collectorIndices) { + int x = index % zone.getWidth(); + int y = index / zone.getWidth(); + + // Skip if no player is close to this collector + if(zone.getPlayersInRange(x, y, MAX_COLLECTOR_DISTANCE).isEmpty()) { + continue; + } + + // Queue spouts + processQueue.add(new SteamIteration(x + 1, y - 3, 0, 0)); // Top + processQueue.add(new SteamIteration(x + 3, y - 1, 1, 0)); // Right + processQueue.add(new SteamIteration(x + 1, y + 1, 2, 0)); // Bottom + processQueue.add(new SteamIteration(x - 1, y - 1, 3, 0)); // Left + } + + // Travel down the pipeline and power on any machines that are reached by it + while(!processQueue.isEmpty()) { + SteamIteration iteration = processQueue.poll(); + int depth = iteration.getDepth(); + + // Skip if depth limit has been reached + if(depth >= MAX_ITERATIONS) { + continue; + } + + int x = iteration.getX(); + int y = iteration.getY(); + + // Skip if coordinates are out of bounds + if(!zone.areCoordinatesInBounds(x, y)) { + continue; + } + + int index = zone.getBlockIndex(x, y); + + // Skip if block has already been processed + if(processedIndices.contains(index)) { + continue; + } + + processedIndices.add(index); + + // Skip if block is not a pipe + if(getState(x, y) != STATE_PIPE) { + + // ...but activate it first if it uses steam! + if(steamableIndices.contains(index)) { + Item item = zone.getBlock(x, y).getFrontItem(); + zone.updateBlock(x, y, item.getLayer(), item, 1); + } + + continue; + } + + byte direction = iteration.getDirection(); + int nextDepth = depth + 1; + + // Enqueue adjacent blocks for processing + if(direction != 2) processQueue.add(new SteamIteration(x, y - 1, 0, nextDepth)); // Top + if(direction != 3) processQueue.add(new SteamIteration(x + 1, y, 1, nextDepth)); // Right + if(direction != 0) processQueue.add(new SteamIteration(x, y + 1, 2, nextDepth)); // Bottom + if(direction != 1) processQueue.add(new SteamIteration(x - 1, y, 3, nextDepth)); // Left + } + } + + public void indexBlock(int x, int y, Item item) { + int index = zone.getBlockIndex(x, y); + + // Does it use steam? + if(!item.usesSteam()) { + steamableIndices.remove(index); + + // Is it a pipe? + if(!item.hasId("mechanical/pipe")) { + + // Is it a collector and is it on top of a steam vent? + if(!item.hasId("mechanical/collector") || !isCollectorActive(x, y)) { + collectorIndices.remove(index); + setState(index, STATE_EMPTY); + return; + } + + collectorIndices.add(index); + setState(index, STATE_COLLECTOR); + return; + } + + setState(index, STATE_PIPE); + return; + } + + steamableIndices.add(index); + setState(index, STATE_EMPTY); + } + + private boolean isCollectorActive(int x, int y) { + return zone.isChunkLoaded(x + 1, y - 1) && zone.getBlock(x + 1, y - 1).getBaseItem().hasId("base/vent"); + } + + protected void setData(byte[] data) { + // Do nothing if data is null + if(data == null) { + return; + } + + int size = zone.getWidth() * zone.getHeight(); + + // Do nothing if data size is incorrect + if(data.length << 2 != size) { + return; + } + + this.data = data; + + // Index active collectors + for(int i = 0; i < size; i++) { + if(getState(i) == STATE_COLLECTOR) { + collectorIndices.add(i); + } + } + } + + private void setState(int index, byte state) { + int byteOffset = index >> 2; + int bitOffset = (index % 4) << 1; + data[byteOffset] &= ~(0x3 << bitOffset); // Clear bits + data[byteOffset] |= (state & 0x3) << bitOffset; // Set bits + } + + private int getState(int x, int y) { + return getState(zone.getBlockIndex(x, y)); + } + + private int getState(int index) { + return (data[index >> 2] >> ((index % 4) << 1)) & 0x3; + } + + protected byte[] getData() { + return data; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 4fc21e6..5b1eca4 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -76,6 +76,7 @@ public class Zone { private float temperature; private float acidity; private final ChunkManager chunkManager; + private final SteamManager steamManager; private final WeatherManager weatherManager = new WeatherManager(); private final EntityManager entityManager = new EntityManager(this); private final LiquidManager liquidManager = new LiquidManager(this); @@ -98,6 +99,7 @@ public class Zone { this.sunlight = sunlight != null && sunlight.length == width ? sunlight : this.sunlight; this.depths = depths != null && depths.length == 3 ? depths : this.depths; this.chunksExplored = chunksExplored != null && chunksExplored.length == getChunkCount() ? chunksExplored : this.chunksExplored; + steamManager.setData(data.getSteamData()); pendingSunlight.addAll(data.getPendingSunlight()); acidity = biome == Biome.ARCTIC || biome == Biome.SPACE ? 0 : config.getAcidity(); creationDate = config.getCreationDate(); @@ -116,6 +118,7 @@ public class Zone { sunlight = new int[width]; chunksExplored = new boolean[numChunksWidth * numChunksHeight]; chunkManager = new ChunkManager(this); + steamManager = new SteamManager(this); acidity = biome == Biome.ARCTIC || biome == Biome.SPACE ? 0 : 1; Arrays.fill(surface, height); Arrays.fill(sunlight, height); @@ -131,6 +134,7 @@ public class Zone { weatherManager.tick(deltaTime); entityManager.tick(deltaTime); liquidManager.tick(deltaTime); + steamManager.tick(deltaTime); // One full cycle = 1200 seconds = 20 minutes time += deltaTime * (1.0F / 1200.0F); @@ -937,6 +941,7 @@ public class Zone { removeBlockTimer(x, y); entityManager.trySpawnBlockEntity(x, y); + steamManager.indexBlock(x, y, item); if(item.isWhole() && y < sunlight[x]) { sunlight[x] = y; @@ -1215,10 +1220,12 @@ public class Zone { for(int y = chunkY; y < chunkY + chunk.getHeight(); y++) { // Spawn block-related entities entityManager.trySpawnBlockEntity(x, y); - - // Index liquids Block block = chunk.getBlock(x, y); + // Index steam blocks + steamManager.indexBlock(x, y, block.getFrontItem()); + + // Index liquids if(!block.getLiquidItem().isAir() && block.getLiquidMod() > 0) { liquidManager.indexLiquidBlock(x, y); } @@ -1379,6 +1386,10 @@ public class Zone { return areCoordinatesInBounds(x, y) && chunksExplored[getChunkIndex(x, y)]; } + protected byte[] getSteamData() { + return steamManager.getData(); + } + public File getDirectory() { return new File("zones", documentId); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java index 6eeaf4a..1e5fc97 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java @@ -26,16 +26,20 @@ public class ZoneDataFile { @JsonSetter(nulls = Nulls.AS_EMPTY) private boolean[] chunksExplored = {}; + @JsonSetter(nulls = Nulls.AS_EMPTY) + private byte[] steamData = {}; + public ZoneDataFile(Zone zone) { - this(zone.getSurface(), zone.getSunlight(), zone.getDepths(), zone.getPendingSunlight(), zone.getChunksExplored()); + this(zone.getSurface(), zone.getSunlight(), zone.getDepths(), zone.getPendingSunlight(), zone.getChunksExplored(), zone.getSteamData()); } - public ZoneDataFile(int[] surface, int[] sunlight, int[] depths, Collection pendingSunlight, boolean[] chunksExplored) { + public ZoneDataFile(int[] surface, int[] sunlight, int[] depths, Collection pendingSunlight, boolean[] chunksExplored, byte[] steamData) { this.surface = surface; this.sunlight = sunlight; this.depths = depths; this.pendingSunlight = pendingSunlight; this.chunksExplored = chunksExplored; + this.steamData = steamData; } @JsonCreator @@ -60,4 +64,8 @@ public class ZoneDataFile { public boolean[] getChunksExplored() { return chunksExplored; } + + public byte[] getSteamData() { + return steamData; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java index bfcac9d..38608ee 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java @@ -146,7 +146,7 @@ public class ZoneManager { chunksExplored[i] = unpacker.unpackBoolean(); } - ZoneDataFile data = new ZoneDataFile(surface, sunlight, null, pendingSunlight, chunksExplored); + ZoneDataFile data = new ZoneDataFile(surface, sunlight, null, pendingSunlight, chunksExplored, null); Files.write(outputFile.toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(data))); return data; } From e7c48133cc20fc6a3ab4e23365189b49aa103abe Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Tue, 20 Feb 2024 04:05:27 +0100 Subject: [PATCH 22/47] Add hatchet functionality --- .../java/brainwine/gameserver/item/Action.java | 1 + .../server/requests/BlockMineRequest.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Action.java b/gameserver/src/main/java/brainwine/gameserver/item/Action.java index 6c2acf6..543ecee 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Action.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Action.java @@ -28,6 +28,7 @@ public enum Action { REFILL(new RefillConsumable()), SKILL(new SkillConsumable()), SKILL_RESET(new SkillResetConsumable()), + SMASH, STEALTH(new StealthConsumable()), TELEPORT(new TeleportConsumable()), diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java index 349afbf..0d2fc4c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java @@ -95,6 +95,23 @@ public class BlockMineRequest extends PlayerRequest { return; } + // Apply decay if block is being mined with a hatchet + if(item.getMod() == ModType.DECAY && player.getHeldItem().getAction() == Action.SMASH) { + int nextMod = Math.min(4, block.getMod(layer) + 1); + zone.updateBlock(x, y, layer, item, nextMod); + + // Send inventory message for v3 players + if(player.isV3()) { + Item decayItem = item.getDecayInventoryItem(); + + if(!decayItem.isAir()) { + player.sendDelayedMessage(new InventoryMessage(player.getInventory().getClientConfig(decayItem))); + } + } + + return; + } + if(metaBlock != null) { Map metadata = metaBlock.getMetadata(); From c5ff0ba62ec69596ee385d1a09edfb1049097a46 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Tue, 20 Feb 2024 04:21:37 +0100 Subject: [PATCH 23/47] Fix an oopsie --- .../gameserver/server/requests/BlockMineRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java index 0d2fc4c..dd167a7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java @@ -174,10 +174,10 @@ public class BlockMineRequest extends PlayerRequest { } } - zone.updateBlock(x, y, layer, 0, 0, player); - player.getStatistics().trackItemMined(item); Item inventoryItem = item.getMod() == ModType.DECAY && block.getMod(layer) > 0 ? item.getDecayInventoryItem() : item.getInventoryItem(); int quantity = 1; + player.getStatistics().trackItemMined(item); + zone.updateBlock(x, y, layer, 0, 0, player); // Apply mining bonus if there is one if(item.hasMiningBonus()) { From 39f75f83221718396c5c8ee554f994d46add0af0 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:46:27 +0100 Subject: [PATCH 24/47] Player appearance rework --- .../gameserver/entity/EntityConfig.java | 5 ++ .../brainwine/gameserver/entity/npc/Npc.java | 6 ++ .../gameserver/entity/player/Appearance.java | 87 +++++++++++++++++++ .../entity/player/AppearanceSlot.java | 61 +++++++++++++ .../entity/player/ClothingSlot.java | 42 --------- .../gameserver/entity/player/ColorSlot.java | 32 ------- .../gameserver/entity/player/Player.java | 55 +++--------- .../entity/player/PlayerConfigFile.java | 15 +--- .../java/brainwine/gameserver/item/Item.java | 12 +++ .../gameserver/item/ItemRegistry.java | 16 ++++ .../requests/ChangeAppearanceRequest.java | 81 ++++++++++------- 11 files changed, 254 insertions(+), 158 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java index 0da885c..31d21be 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java @@ -30,6 +30,7 @@ public class EntityConfig { private float maxHealth = Entity.DEFAULT_HEALTH; private float baseSpeed = 3; private boolean character; + private boolean human; private boolean named; private Vector2i size = new Vector2i(1, 1); private EntityGroup group = EntityGroup.NONE; @@ -85,6 +86,10 @@ public class EntityConfig { return character; } + public boolean isHuman() { + return human; + } + public boolean isNamed() { return named; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index 1b93189..db53dba 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -17,6 +17,7 @@ import brainwine.gameserver.entity.EntityLoot; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.behavior.SequenceBehavior; +import brainwine.gameserver.entity.player.Appearance; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; @@ -104,6 +105,11 @@ public class Npc extends Entity { this.name = Naming.getRandomEntityName(); } + // Generate random appearance + if(config.isHuman()) { + properties.putAll(Appearance.getRandomAppearance()); + } + this.config = config; this.typeName = config.getName(); this.type = config.getType(); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java new file mode 100644 index 0000000..34684de --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java @@ -0,0 +1,87 @@ +package brainwine.gameserver.entity.player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import brainwine.gameserver.GameConfiguration; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.util.MapHelper; + +/** + * Utility class for player appearance related stuff. + * Ghosts also have a random appearance, which is why it's here instead of in the player class. + */ +public class Appearance { + + public static Map getRandomAppearance() { + return getRandomAppearance(null); + } + + public static Map getRandomAppearance(Player player) { + Map appearance = new HashMap<>(); + + for(AppearanceSlot slot : AppearanceSlot.values()) { + // Skip if slot cannot be changed by players + if(!slot.isChangeable()) { + continue; + } + + String category = slot.getCategory(); + + // Color handling + if(slot.isColor()) { + List colors = getAvailableColors(slot, player); + + // Change appearance to random color + if(!colors.isEmpty()) { + appearance.put(slot.getId(), colors.get((int)(Math.random() * colors.size()))); + } + + continue; + } + + // Fetch list of items in this slot's category that the player owns + List items = ItemRegistry.getItemsByCategory(category).stream() + .filter(item -> item.isBase() || (player != null && player.getInventory().hasItem(item))) + .collect(Collectors.toList()); + + // Change appearance to random clothing item + if(!items.isEmpty()) { + appearance.put(slot.getId(), items.get((int)(Math.random() * items.size())).getCode()); + } + } + + return appearance; + } + + public static List getAvailableColors(AppearanceSlot slot) { + return getAvailableColors(null); + } + + public static List getAvailableColors(AppearanceSlot slot, Player player) { + List colors = new ArrayList<>(); + + // Return empty list if slot is not valid + if(!slot.isColor()) { + return colors; + } + + Map wardrobe = MapHelper.getMap(GameConfiguration.getBaseConfig(), "wardrobe", Collections.emptyMap()); + String category = slot.getCategory(); + + // Add base colors + colors.addAll(MapHelper.getList(wardrobe, category, Collections.emptyList())); + + // Add bonus colors + if(player != null && player.getInventory().hasItem(ItemRegistry.getItem("accessories/makeup"))) { + colors.addAll(MapHelper.getList(wardrobe, String.format("%s-bonus", category), Collections.emptyList())); + } + + return colors; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java new file mode 100644 index 0000000..43ed761 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java @@ -0,0 +1,61 @@ +package brainwine.gameserver.entity.player; + +public enum AppearanceSlot { + + SKIN_COLOR("c*", "skin-color", true), + HAIR_COLOR("h*", "hair-color", true), + HAIR("h", "hair", true), + FACIAL_HAIR("fh", "facialhair", true), + TOPS("t", "tops", true), + BOTTOMS("b", "bottoms", true), + FOOTWEAR("fw", "footwear", true), + HEADGEAR("hg", "headgear", true), + FACIAL_GEAR("fg", "facialgear", true), + FACIAL_GEAR_GLOW("fg*", "facialgear-glow"), + SUIT("u", "suit"), + TOPS_OVERLAY("to", "tops-overlay"), + TOPS_OVERLAY_GLOW("to*", "tops-overlay-glow"), + ARMS_OVERLAY("ao", "arms-overlay"), + LEGS_OVERLAY("lo", "legs-overlay"), + FOOTWEAR_OVERLAY("fo", "footwear-overlay"); + + private final String id; + private final String category; + private final boolean changeable; + + private AppearanceSlot(String id, String category) { + this(id, category, false); + } + + private AppearanceSlot(String id, String category, boolean changeable) { + this.id = id; + this.category = category; + this.changeable = changeable; + } + + public static AppearanceSlot fromId(String id) { + for(AppearanceSlot value : values()) { + if(value.getId().equals(id)) { + return value; + } + } + + return null; + } + + public String getId() { + return id; + } + + public String getCategory() { + return category; + } + + public boolean isChangeable() { + return changeable; + } + + public boolean isColor() { + return id.endsWith("*"); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java deleted file mode 100644 index f0ab70b..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java +++ /dev/null @@ -1,42 +0,0 @@ -package brainwine.gameserver.entity.player; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum ClothingSlot { - - HAIR("h"), - FACIAL_HAIR("fh"), - TOPS("t"), - BOTTOMS("b"), - FOOTWEAR("fw"), - HEADGEAR("hg"), - FACIAL_GEAR("fg"), - SUIT("u"), - TOPS_OVERLAY("to"), - ARMS_OVERLAY("ao"), - LEGS_OVERLAY("lo"), - FOOTWEAR_OVERLAY("fo"); - - private final String id; - - private ClothingSlot(String id) { - this.id = id; - } - - @JsonCreator - public static ClothingSlot fromId(String id) { - for(ClothingSlot value : values()) { - if(value.getId().equals(id)) { - return value; - } - } - - return null; - } - - @JsonValue - public String getId() { - return id; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java deleted file mode 100644 index 8fe4caa..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java +++ /dev/null @@ -1,32 +0,0 @@ -package brainwine.gameserver.entity.player; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum ColorSlot { - - SKIN_COLOR("c*"), - HAIR_COLOR("h*"); - - private final String id; - - private ColorSlot(String id) { - this.id = id; - } - - @JsonCreator - public static ColorSlot fromId(String id) { - for(ColorSlot value : values()) { - if(value.getId().equals(id)) { - return value; - } - } - - return null; - } - - @JsonValue - public String getId() { - return id; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index fc0b058..64edb0f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -10,7 +10,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -103,8 +102,7 @@ public class Player extends Entity implements CommandExecutor { private Map ignoredHints; private Map skills; private Map> bumpedSkills; - private Map equippedClothing; - private Map equippedColors; + private Map appearance; private final Map settings = new HashMap<>(); private final Set activeChunks = new HashSet<>(); private final Map> dialogs = new HashMap<>(); @@ -147,8 +145,7 @@ public class Player extends Entity implements CommandExecutor { this.ignoredHints = config.getIgnoredHints(); this.skills = config.getSkills(); this.bumpedSkills = config.getBumpedSkills(); - this.equippedClothing = config.getEquippedClothing(); - this.equippedColors = config.getEquippedColors(); + this.appearance = config.getAppearance(); health = getMaxHealth(); inventory.setPlayer(this); statistics.setPlayer(this); @@ -169,8 +166,7 @@ public class Player extends Entity implements CommandExecutor { this.ignoredHints = new HashMap<>(); this.skills = new HashMap<>(); this.bumpedSkills = new HashMap<>(); - this.equippedClothing = new HashMap<>(); - this.equippedColors = new HashMap<>(); + this.appearance = Appearance.getRandomAppearance(); } @JsonCreator @@ -271,7 +267,8 @@ public class Player extends Entity implements CommandExecutor { public Map getStatusConfig() { Map config = super.getStatusConfig(); config.put("id", documentId); - config.putAll(getAppearanceConfig()); + config.putAll(appearance); + config.put("u", inventory.findJetpack().getCode()); return config; } @@ -1061,27 +1058,18 @@ public class Player extends Entity implements CommandExecutor { return Collections.unmodifiableSet(achievements); } - public void setClothing(ClothingSlot slot, Item item) { - if(!item.isClothing()) { - return; - } - - equippedClothing.put(slot, item); - zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); + public void randomizeAppearance() { + appearance.putAll(Appearance.getRandomAppearance(this)); + zone.sendMessage(new EntityChangeMessage(id, appearance)); } - public Map getEquippedClothing() { - return Collections.unmodifiableMap(equippedClothing); + public void updateAppearance(Map appearance) { + this.appearance = appearance; + zone.sendMessage(new EntityChangeMessage(id, appearance)); } - public void setColor(ColorSlot slot, String hex) { - // TODO check if the string is actually a valid hex color - equippedColors.put(slot, hex); - zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); - } - - public Map getEquippedColors() { - return Collections.unmodifiableMap(equippedColors); + public Map getAppearance() { + return Collections.unmodifiableMap(appearance); } public void setSkillLevel(Skill skill, int level) { @@ -1309,21 +1297,6 @@ public class Player extends Entity implements CommandExecutor { return connection != null && connection.isOpen(); } - private Map getAppearanceConfig() { - Map appearance = new HashMap<>(); - - for(Entry entry : equippedClothing.entrySet()) { - appearance.put(entry.getKey().getId(), entry.getValue().getCode()); - } - - for(Entry entry : equippedColors.entrySet()) { - appearance.put(entry.getKey().getId(), entry.getValue()); - } - - appearance.put(ClothingSlot.SUIT.getId(), inventory.findJetpack().getCode()); // Jetpack - return appearance; - } - /** * @return A {@link Map} containing all the data necessary for use in {@link ConfigurationMessage}. */ @@ -1345,7 +1318,7 @@ public class Player extends Entity implements CommandExecutor { config.put("items_crafted", statistics.getTotalItemsCrafted()); config.put("play_time", (int)(statistics.getPlayTime())); config.put("deaths", statistics.getDeaths()); - config.put("appearance", getAppearanceConfig()); + config.put("appearance", appearance); config.put("settings", settings); return config; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java index e786466..03fac5d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java @@ -39,8 +39,7 @@ public class PlayerConfigFile { private Map ignoredHints = new HashMap<>(); private Map skills = new HashMap<>(); private Map> bumpedSkills = new HashMap<>(); - private Map equippedClothing = new HashMap<>(); - private Map equippedColors = new HashMap<>(); + private Map appearance = new HashMap<>(); public PlayerConfigFile(Player player) { this.name = player.getName(); @@ -63,8 +62,7 @@ public class PlayerConfigFile { this.ignoredHints = player.getIgnoredHints(); this.skills = player.getSkills(); this.bumpedSkills = player.getBumpedSkills(); - this.equippedClothing = player.getEquippedClothing(); - this.equippedColors = player.getEquippedColors(); + this.appearance = player.getAppearance(); } @JsonCreator @@ -163,12 +161,7 @@ public class PlayerConfigFile { } @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) - public Map getEquippedClothing() { - return equippedClothing; - } - - @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) - public Map getEquippedColors() { - return equippedColors; + public Map getAppearance() { + return appearance; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java index 6e3a7ec..605dcc8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java @@ -30,6 +30,9 @@ public class Item { @JsonProperty("code") private int code; + @JsonProperty("category") + private String category; + @JsonProperty("title") private String title; @@ -237,6 +240,15 @@ public class Item { return code; } + public String getCategory() { + if(category != null) { + return category; + } + + int index = id.indexOf('/'); + return index > 1 ? id.substring(0, index) : null; + } + public String getTitle() { return title; } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java index 4b82d2c..db35ed7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java @@ -2,9 +2,11 @@ package brainwine.gameserver.item; import static brainwine.shared.LogMarkers.SERVER_MARKER; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; @@ -15,6 +17,7 @@ public class ItemRegistry { private static final Logger logger = LogManager.getLogger(); private static final Map items = new HashMap<>(); private static final Map itemsByCode = new HashMap<>(); + private static final Map> itemsByCategory = new HashMap<>(); // TODO maybe just move the registry stuff here public static void clear() { @@ -36,6 +39,15 @@ public class ItemRegistry { return false; } + String category = item.getCategory(); + List categorizedItems = itemsByCategory.get(category); + + if(categorizedItems == null) { + categorizedItems = new ArrayList<>(); + itemsByCategory.put(category, categorizedItems); + } + + categorizedItems.add(item); items.put(id, item); itemsByCode.put(code, item); return true; @@ -52,4 +64,8 @@ public class ItemRegistry { public static Collection getItems() { return Collections.unmodifiableCollection(items.values()); } + + public static List getItemsByCategory(String category) { + return Collections.unmodifiableList(itemsByCategory.getOrDefault(category, Collections.emptyList())); + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java index beb7de2..6c903af 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java @@ -5,29 +5,28 @@ import java.util.Map.Entry; import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.dialog.DialogHelper; -import brainwine.gameserver.entity.player.ClothingSlot; -import brainwine.gameserver.entity.player.ColorSlot; +import brainwine.gameserver.entity.player.Appearance; +import brainwine.gameserver.entity.player.AppearanceSlot; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.messages.EntityChangeMessage; +import brainwine.gameserver.util.MapHelper; -/** - * TODO we should actually check if the sent value is even compatible with the slot. - * We wouldn't want to allow players to equip pants for hats! - */ @RequestInfo(id = 22) public class ChangeAppearanceRequest extends PlayerRequest { - public Map data; + public Map appearance; @Override public void process(Player player) { - if(data.containsKey("meta")) { - String meta = "" + data.get("meta"); + // Handle special cases + if(appearance.containsKey("meta")) { + String meta = MapHelper.getString(appearance, "meta", ""); if(meta.equals("randomize")) { - player.notify("Sorry, you can't randomize your appearance yet."); + player.randomizeAppearance(); } else { player.showDialog(DialogHelper.getWardrobeDialog(meta)); } @@ -35,36 +34,54 @@ public class ChangeAppearanceRequest extends PlayerRequest { return; } - for(Entry entry : data.entrySet()) { - String key = entry.getKey(); + // Validate appearance data + for(Entry entry : appearance.entrySet()) { + AppearanceSlot slot = AppearanceSlot.fromId(entry.getKey()); Object value = entry.getValue(); - if(value instanceof Integer) { - ClothingSlot slot = ClothingSlot.fromId(key); - - if(slot == null) { - continue; - } - - Item item = ItemRegistry.getItem((int)value); - - if(!item.isBase() && !player.getInventory().hasItem(item)) { - player.notify("Sorry, but you do not own this."); + // Fail if slot is not valid + if(slot == null || !slot.isChangeable()) { + fail(player); + return; + } + + // Handle color data + if(slot.isColor()) { + // Fail if color value is not a string + if(!(value instanceof String)) { + fail(player); return; } - player.setClothing(slot, item); - } else if(value instanceof String) { - // TODO check if player owns color - ColorSlot slot = ColorSlot.fromId(key); - String color = (String)value; - - if(slot == null) { - continue; + // Fail if player doesn't own color + if(!Appearance.getAvailableColors(slot, player).contains((String)value)) { + fail(player); + return; } - player.setColor(slot, color); + continue; + } + + // Fail if item value is not an integer (item code) + if(!(value instanceof Integer)) { + fail(player); + return; + } + + Item item = ItemRegistry.getItem((int)value); + + // Do nothing if item isn't valid clothing or player doesn't own it + if(!item.isClothing() || !slot.getCategory().equals(item.getCategory()) || (!item.isBase() && !player.getInventory().hasItem(item))) { + fail(player); + return; } } + + // Update player appearance + player.updateAppearance(appearance); + } + + private void fail(Player player) { + player.sendMessage(new EntityChangeMessage(player.getId(), player.getAppearance())); } } From 34ca131f610d8b963f45d067ae1a83541cb7eff8 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:00:32 +0100 Subject: [PATCH 25/47] Fix zone status compatibility with older game versions --- .../gameserver/entity/player/Player.java | 10 +++++-- .../server/messages/ZoneStatusMessage.java | 6 ++-- .../java/brainwine/gameserver/zone/Zone.java | 28 +++++++++++-------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 64edb0f..6fe19ea 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -66,6 +66,7 @@ import brainwine.gameserver.server.models.EntityStatusData; import brainwine.gameserver.server.pipeline.Connection; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MathUtils; +import brainwine.gameserver.util.VersionUtils; import brainwine.gameserver.zone.Chunk; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; @@ -320,8 +321,7 @@ public class Player extends Entity implements CommandExecutor { } sendMessage(new ConfigurationMessage(id, getClientConfig(), GameConfiguration.getClientConfig(this), zone.getClientConfig(this))); - sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); - sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); + sendMessage(new ZoneStatusMessage(zone.getStatusConfig(this))); sendMessage(new PlayerPositionMessage((int)x, (int)y)); sendMessage(new HealthMessage(health)); @@ -525,8 +525,12 @@ public class Player extends Entity implements CommandExecutor { return clientVersion; } + public boolean hasClientVersion(String version) { + return clientVersion != null && VersionUtils.isGreaterOrEqualTo(clientVersion, version); + } + public boolean isV3() { - return clientVersion != null && clientVersion.startsWith("3"); + return hasClientVersion("3.0.0"); } /** diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java index 635ba75..09c3b3b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java @@ -1,16 +1,14 @@ package brainwine.gameserver.server.messages; -import java.util.Map; - import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; @MessageInfo(id = 17) public class ZoneStatusMessage extends Message { - public Map status; + public Object status; - public ZoneStatusMessage(Map status) { + public ZoneStatusMessage(Object status) { this.status = status; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 5b1eca4..7f44679 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -143,9 +143,13 @@ public class Zone { time -= 1.0F; } + // Send zone status update if(!getPlayers().isEmpty()) { if(now >= lastStatusUpdate + 4000) { - sendMessage(new ZoneStatusMessage(getStatusConfig())); + for(Player player : getPlayers()) { + sendMessage(new ZoneStatusMessage(getStatusConfig(player))); + } + lastStatusUpdate = now; } } @@ -1522,16 +1526,16 @@ public class Zone { /** * @return A {@link Map} containing all the data necessary for use in {@link ZoneStatusMessage}. */ - public Map getStatusConfig() { - Map config = new HashMap<>(); - config.put("w", new int[] { - (int)(time * 10000), - (int)(temperature * 10000), - (int)(weatherManager.getPrecipitation() * 10000), - (int)(weatherManager.getPrecipitation() * 10000), - (int)(weatherManager.getPrecipitation() * 10000), - (int)(acidity * 10000) - }); - return config; + public Object getStatusConfig(Player player) { + int[] status = { + (int)(time * 10000), + (int)(temperature * 10000), + (int)(weatherManager.getPrecipitation() * 10000), + (int)(weatherManager.getPrecipitation() * 10000), + (int)(weatherManager.getPrecipitation() * 10000), + (int)(acidity * 10000) + }; + + return player.hasClientVersion("2.1.0") ? MapHelper.map("w", status) : status; } } From b737d46b1d53260c86fd4d1d20aa8bb9745d965e Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:05:08 +0100 Subject: [PATCH 26/47] Make `/give all` not exclude clothing items --- .../java/brainwine/gameserver/command/commands/GiveCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java index 6102bc8..56fb90b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java @@ -35,7 +35,7 @@ public class GiveCommand extends Command { title = "of every item"; for(Item item : ItemRegistry.getItems()) { - if(!item.isClothing() && !item.isAir()) { + if(!item.isAir()) { items.add(item); } } From 5dbc7555deb89c5135b73107c9b04247b8a41ba3 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:30:47 +0100 Subject: [PATCH 27/47] Fix appearance update oopsie --- .../main/java/brainwine/gameserver/entity/player/Player.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 6fe19ea..cf03310 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -1068,7 +1068,7 @@ public class Player extends Entity implements CommandExecutor { } public void updateAppearance(Map appearance) { - this.appearance = appearance; + this.appearance.putAll(appearance); zone.sendMessage(new EntityChangeMessage(id, appearance)); } From 7a06129e8bad1e7b254fab304a64959c7fea05f2 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:34:53 +0100 Subject: [PATCH 28/47] Prepack `EntityItemUseMessage` --- .../gameserver/entity/player/Player.java | 6 +-- .../server/messages/EntityItemUseMessage.java | 28 +++++++----- .../server/models/EntityItemUseData.java | 43 +++++++++++++++++++ 3 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index cf03310..865f8cf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -338,11 +338,7 @@ public class Player extends Entity implements CommandExecutor { Collection peers = zone.getPlayers(); sendMessage(new EntityStatusMessage(peers, EntityStatus.ENTERING)); sendMessage(new EntityPositionMessage(peers)); - - // TODO prepack this as well - for(Player peer : peers) { - sendMessage(new EntityItemUseMessage(peer.getId(), 0, peer.getHeldItem(), 0)); - } + sendMessage(new EntityItemUseMessage(peers)); // Send achievement data for(Achievement achievement : AchievementManager.getAchievements()) { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java index aec81da..3538198 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java @@ -1,21 +1,29 @@ package brainwine.gameserver.server.messages; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + import brainwine.gameserver.annotations.MessageInfo; +import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.models.EntityItemUseData; -@MessageInfo(id = 10, collection = true) +@MessageInfo(id = 10, prepacked = true) public class EntityItemUseMessage extends Message { - public int entityId; - public int type; - public Item item; - public int status; + public Collection data; - public EntityItemUseMessage(int entityId, int type, Item item, int status) { - this.entityId = entityId; - this.type = type; - this.item = item; - this.status = status; + public EntityItemUseMessage(Collection players) { + this.data = players.stream().map(EntityItemUseData::new).collect(Collectors.toList()); + } + + public EntityItemUseMessage(Player player) { + this.data = Arrays.asList(new EntityItemUseData(player)); + } + + public EntityItemUseMessage(int id, int type, Item item, int status) { + this.data = Arrays.asList(new EntityItemUseData(id, type, item, status)); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java new file mode 100644 index 0000000..9c663da --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java @@ -0,0 +1,43 @@ +package brainwine.gameserver.server.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; + +import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.item.Item; + +@JsonFormat(shape = Shape.ARRAY) +public class EntityItemUseData { + + private final int id; + private final int type; + private final Item item; + private final int status; + + public EntityItemUseData(Player player) { + this(player.getId(), 0, player.getHeldItem(), 0); + } + + public EntityItemUseData(int id, int type, Item item, int status) { + this.id = id; + this.type = type; + this.item = item; + this.status = status; + } + + public int getId() { + return id; + } + + public int getType() { + return type; + } + + public Item getItem() { + return item; + } + + public int getStatus() { + return status; + } +} From 1466ea0176a4e473afb90388a33d0d95075ade7a Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:46:23 +0100 Subject: [PATCH 29/47] Fix player name inconsistency on v2 clients --- api/src/main/java/brainwine/api/DataFetcher.java | 1 + api/src/main/java/brainwine/api/DefaultDataFetcher.java | 5 +++++ api/src/main/java/brainwine/api/GatewayService.java | 2 +- src/main/java/brainwine/DirectDataFetcher.java | 7 +++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/brainwine/api/DataFetcher.java b/api/src/main/java/brainwine/api/DataFetcher.java index a72d6ad..1509e9a 100644 --- a/api/src/main/java/brainwine/api/DataFetcher.java +++ b/api/src/main/java/brainwine/api/DataFetcher.java @@ -9,6 +9,7 @@ public interface DataFetcher { public boolean isPlayerNameTaken(String name); public String registerPlayer(String name); public String login(String name, String password); + public String fetchPlayerName(String name); public boolean verifyAuthToken(String name, String token); public boolean verifyApiToken(String apiToken); public Collection fetchZoneInfo(); diff --git a/api/src/main/java/brainwine/api/DefaultDataFetcher.java b/api/src/main/java/brainwine/api/DefaultDataFetcher.java index 9c6b42c..8a5b724 100644 --- a/api/src/main/java/brainwine/api/DefaultDataFetcher.java +++ b/api/src/main/java/brainwine/api/DefaultDataFetcher.java @@ -23,6 +23,11 @@ public class DefaultDataFetcher implements DataFetcher { throw exception; } + @Override + public String fetchPlayerName(String name) { + throw exception; + } + @Override public boolean verifyAuthToken(String name, String token) { throw exception; diff --git a/api/src/main/java/brainwine/api/GatewayService.java b/api/src/main/java/brainwine/api/GatewayService.java index 2ff42eb..c2927ea 100644 --- a/api/src/main/java/brainwine/api/GatewayService.java +++ b/api/src/main/java/brainwine/api/GatewayService.java @@ -111,7 +111,7 @@ public class GatewayService { return; } - ctx.json(new ServerConnectInfo(api.getGameServerHost(), name, token)); + ctx.json(new ServerConnectInfo(api.getGameServerHost(), dataFetcher.fetchPlayerName(name), token)); } /** diff --git a/src/main/java/brainwine/DirectDataFetcher.java b/src/main/java/brainwine/DirectDataFetcher.java index 76a8bdb..ad6fdaa 100644 --- a/src/main/java/brainwine/DirectDataFetcher.java +++ b/src/main/java/brainwine/DirectDataFetcher.java @@ -6,6 +6,7 @@ import java.util.List; import brainwine.api.DataFetcher; import brainwine.api.models.ZoneInfo; +import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.PlayerManager; import brainwine.gameserver.zone.Zone; import brainwine.gameserver.zone.ZoneManager; @@ -34,6 +35,12 @@ public class DirectDataFetcher implements DataFetcher { public String login(String name, String password) { return playerManager.login(name, password); } + + @Override + public String fetchPlayerName(String name) { + Player player = playerManager.getPlayer(name); + return player == null ? null : player.getName(); + } @Override public boolean verifyAuthToken(String name, String token) { From 882396013ea35b626fba8641b75f1f6d97c8fe6f Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:31:03 +0100 Subject: [PATCH 30/47] Temporary fix for system notifications playing karma warning sound on v2 --- .../java/brainwine/gameserver/entity/player/Player.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 865f8cf..f24e0be 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -213,11 +213,8 @@ public class Player extends Entity implements CommandExecutor { @Override public void notify(Object message, NotificationType type) { - if(type == NotificationType.SYSTEM && isV3()) { - sendMessage(new NotificationMessage(message, NotificationType.PEER_ACCOMPLISHMENT)); - } else { - sendMessage(new NotificationMessage(message, type)); - } + // TODO type SYSTEM (2) apparently plays the karma warning sound on v2 clients, so I guess we'll be mapping all of them to PEER_ACCOMPLISHMENT (11). + sendMessage(new NotificationMessage(message, type == NotificationType.SYSTEM ? NotificationType.PEER_ACCOMPLISHMENT : type)); } @Override From c444371612c56230cb85c989fbbe414d5a237354 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 03:27:56 +0100 Subject: [PATCH 31/47] Send killer entity ID in player death status message --- .../gameserver/entity/player/Player.java | 17 +++++++++++++++-- .../server/messages/EntityStatusMessage.java | 4 ++++ .../server/models/EntityStatusData.java | 6 +++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index f24e0be..72e44ae 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -206,9 +206,22 @@ public class Player extends Entity implements CommandExecutor { @Override public void die(EntityAttack cause) { + Entity killer = cause == null ? null : cause.getAttacker(); + String serverMessage = String.format("%s died.", name); + Map details = new HashMap<>(); + + if(killer != null) { + details.put("<", killer.getId()); + + if(killer.isPlayer()) { + // TODO track kill for killer achievement in pvp zones + serverMessage = String.format("%s killed %s.", killer.getName(), name); + } + } + + sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD, details)); + GameServer.getInstance().notify(serverMessage, NotificationType.CHAT); statistics.trackDeath(); - sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD)); // TODO killer id - GameServer.getInstance().notify(String.format("%s died", name), NotificationType.CHAT); } @Override diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java index accc0d7..cf2c420 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java @@ -28,6 +28,10 @@ public class EntityStatusMessage extends Message { this(Arrays.asList(new EntityStatusData(entity, status))); } + public EntityStatusMessage(Entity entity, EntityStatus status, Map details) { + this(Arrays.asList(new EntityStatusData(entity, status, details))); + } + public EntityStatusMessage(int id, int type, String name, EntityStatus status, Map details) { this(Arrays.asList(new EntityStatusData(id, type, name, status, details))); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java index e753976..ccc9f34 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java @@ -18,7 +18,11 @@ public class EntityStatusData { private final Map details; public EntityStatusData(Entity entity, EntityStatus status) { - this(entity.getId(), entity.getType(), entity.getName(), status, entity.getStatusConfig()); + this(entity, status, entity.getStatusConfig()); + } + + public EntityStatusData(Entity entity, EntityStatus status, Map details) { + this(entity.getId(), entity.getType(), entity.getName(), status, details); } public EntityStatusData(int id, int type, String name, EntityStatus status, Map details) { From 455e63f9277754a023f2d819eca2ec1c7b4122e8 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:06:58 +0100 Subject: [PATCH 32/47] Swap biome & size parameters in `/genzone` command --- .../command/commands/GenerateZoneCommand.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java index 6c316aa..53985f4 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java @@ -18,20 +18,20 @@ public class GenerateZoneCommand extends Command { @Override public void execute(CommandExecutor executor, String[] args) { - Biome biome = Biome.getRandomBiome(); + Biome biome = args.length > 0 ? Biome.fromName(args[0]) : Biome.getRandomBiome(); int width = biome == Biome.DEEP ? 1200 : 2000; int height = biome == Biome.DEEP ? 1000 : 600; int seed = (int)(Math.random() * Integer.MAX_VALUE); - if(args.length > 0 && args.length < 2) { + if(args.length > 1 && args.length < 3) { executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM); return; } - if(args.length >= 2) { + if(args.length >= 3) { try { - width = Integer.parseInt(args[0]); - height = Integer.parseInt(args[1]); + width = Integer.parseInt(args[1]); + height = Integer.parseInt(args[2]); } catch(NumberFormatException e) { executor.notify("Zone width and height must be valid numbers.", SYSTEM); return; @@ -47,10 +47,6 @@ public class GenerateZoneCommand extends Command { } } - if(args.length >= 3) { - biome = Biome.fromName(args[2]); - } - ZoneGenerator generator = null; if(args.length >= 4) { @@ -106,7 +102,7 @@ public class GenerateZoneCommand extends Command { @Override public String getUsage(CommandExecutor executor) { - return "/genzone [ ] [biome] [generator] [seed]"; + return "/genzone [biome] [ ] [generator] [seed]"; } @Override From e330b13a74282eb8141e0b459eefb15e19669b3f Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 21:15:32 +0100 Subject: [PATCH 33/47] Prevent explosions from destroying unlooted containers --- .../main/java/brainwine/gameserver/zone/Zone.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 7f44679..d0be509 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -402,6 +402,18 @@ public class Zone { break; } + // Metadata check + MetaBlock metaBlock = getMetaBlock(positionX, positionY); + + if(metaBlock != null) { + // Do not destroy block if it is a container with loot + if(frontItem.hasUse(ItemUseType.CONTAINER) && metaBlock.hasProperty("$")) { + continue; + } + + // TODO dungeon switch check + } + affectedBlocks.add(position); processed.add(index); } From 94cb2e16f852c2cf83a9fb9355c131a78478d243 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 21:28:47 +0100 Subject: [PATCH 34/47] Don't track explosion triggerer in burst interaction --- .../gameserver/item/interactions/BurstInteraction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java index ecd1b71..a9b8573 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java @@ -57,7 +57,7 @@ public class BurstInteraction implements ItemInteraction { boolean destructive = MapHelper.getBoolean(configMap, "destructive"); // Create explosion and destroy block - zone.explode(x, y, range, player, destructive, damage, damageType, effect); + zone.explode(x, y, range, null, destructive, damage, damageType, effect); zone.updateBlock(x, y, layer, 0); } } From bfeb93be809f1b74988d0d6615f1b5c8c01e5c73 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:30:15 +0100 Subject: [PATCH 35/47] Biome survival skill requirement check --- .../server/requests/ZoneChangeRequest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java index cbf048e..c87919f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java @@ -1,9 +1,13 @@ package brainwine.gameserver.server.requests; +import brainwine.gameserver.GameConfiguration; import brainwine.gameserver.GameServer; import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.Zone; @RequestInfo(id = 24) @@ -23,6 +27,17 @@ public class ZoneChangeRequest extends PlayerRequest { return; } + // Check survival requirement unless player has god mode enabled + if(!player.isGodMode()) { + Biome biome = zone.getBiome(); + int survival = MapHelper.getInt(GameConfiguration.getBaseConfig(), String.format("biomes.%s.survival_requirement", biome.getId())); + + if(player.getTotalSkillLevel(Skill.SURVIVAL) < survival) { + player.notify(String.format("Your survival skill needs to be at least level %s to enter %s worlds.", survival, biome.getId())); + return; + } + } + player.changeZone(zone); } } From 03e96be0a4b3455c45403df5076a78d00b828a3d Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 25 Feb 2024 03:25:31 +0100 Subject: [PATCH 36/47] Add skeleton burying & undertaker achievement type --- .../gameserver/achievements/Achievement.java | 1 + .../achievements/UndertakerAchievement.java | 19 +++++++ .../entity/player/PlayerStatistics.java | 16 ++++++ .../server/requests/BlockPlaceRequest.java | 51 +++++++++++++++++++ .../gameserver/zone/EntityManager.java | 16 ++++++ .../java/brainwine/gameserver/zone/Zone.java | 16 ++++++ 6 files changed, 119 insertions(+) create mode 100644 gameserver/src/main/java/brainwine/gameserver/achievements/UndertakerAchievement.java diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java b/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java index c74fb03..0e9e6e1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java +++ b/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java @@ -28,6 +28,7 @@ import brainwine.gameserver.util.MathUtils; @Type(name = "ScavengingAchievement", value = ScavengingAchievement.class), @Type(name = "DiscoveryAchievement", value = DiscoveryAchievement.class), @Type(name = "SpawnerStoppageAchievement", value = SpawnerStoppageAchievement.class), + @Type(name = "UndertakerAchievement", value = UndertakerAchievement.class), @Type(name = "Journeyman", value = JourneymanAchievement.class) }) @JsonSerialize(using = AchievementSerializer.class) diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/UndertakerAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievements/UndertakerAchievement.java new file mode 100644 index 0000000..591965d --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/achievements/UndertakerAchievement.java @@ -0,0 +1,19 @@ +package brainwine.gameserver.achievements; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; + +import brainwine.gameserver.entity.player.Player; + +public class UndertakerAchievement extends Achievement { + + @JsonCreator + public UndertakerAchievement(@JacksonInject("title") String title) { + super(title); + } + + @Override + public int getProgress(Player player) { + return player.getStatistics().getUndertakings(); + } +} \ No newline at end of file diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java index f028ad8..22dc3b6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java @@ -21,6 +21,7 @@ import brainwine.gameserver.achievements.RaiderAchievement; import brainwine.gameserver.achievements.ScavengingAchievement; import brainwine.gameserver.achievements.SidekickAchievement; import brainwine.gameserver.achievements.SpawnerStoppageAchievement; +import brainwine.gameserver.achievements.UndertakerAchievement; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.item.Item; @@ -39,6 +40,7 @@ public class PlayerStatistics { private int containersLooted; private int dungeonsRaided; private int mawsPlugged; + private int undertakings; private int deaths; @JsonIgnore @@ -309,6 +311,20 @@ public class PlayerStatistics { return mawsPlugged; } + public void trackUndertaking() { + undertakings++; + player.addExperience(25); + player.updateAchievementProgress(UndertakerAchievement.class); + } + + public void setUndertakings(int undertakings) { + this.undertakings = undertakings; + } + + public int getUndertakings() { + return undertakings; + } + public void trackDeath() { deaths++; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java index 8c6688e..d024ee2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java @@ -10,6 +10,7 @@ import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemGroup; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.ModType; import brainwine.gameserver.server.PlayerRequest; @@ -116,6 +117,56 @@ public class BlockPlaceRequest extends PlayerRequest { if(item.hasCustomPlace()) { processCustomPlace(zone, player); } + + // Process burial if item placed is a gravestone + if(item.getGroup() == ItemGroup.GRAVESTONE) { + processBurial(zone, player); + } + } + + private void processBurial(Zone zone, Player player) { + // Check bounds + if(x <= 0 || x + 2 > zone.getWidth() || y + 2 > zone.getHeight()) { + return; + } + + // Do nothing if there is no skeleton underneath the gravestone + if(!zone.getBlock(x, y + 1).getFrontItem().hasId("rubble/skeleton")) { + return; + } + + // Do nothing if the skeleton is obstructed + if(zone.isBlockOccupied(x + 1, y + 1, Layer.FRONT)) { + return; + } + + // Do nothing if the skeleton isn't underground + if(!zone.isUnderground(x, y + 1) || !zone.isUnderground(x + 1, y + 1)) { + return; + } + + // Do nothing if the gravestone isn't above ground + if(zone.isUnderground(x, y) || zone.isUnderground(x + 1, y)) { + return; + } + + // Do nothing if the skeleton isn't surrounded by earth + if(!zone.isBlockEarthy(x - 1, y + 1) || !zone.isBlockEarthy(x + 2, y + 1) || !zone.isBlockEarthy(x, y + 2) || !zone.isBlockEarthy(x + 1, y + 2)) { + return; + } + + // Everything checks out -- fill the grave! + zone.updateBlock(x, y + 1, Layer.FRONT, "ground/earth"); + zone.updateBlock(x + 1, y + 1, Layer.FRONT, "ground/earth"); + zone.spawnEffect(x + 1.0F, y + 0.5F, "expiate", 20); + zone.spawnEffect(x + 1.0F, y + 0.5F, "sparkle up", 20); + + // ~33% chance to spawn a ghost + if(Math.random() < 0.334) { + zone.spawnEntity("ghost", x + 1, y); + } + + player.getStatistics().trackUndertaking(); } private void createBlockTimer(Zone zone, Player player) { diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java index 9e7a099..cfddcec 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -260,6 +260,22 @@ public class EntityManager { } } + public Npc spawnEntity(String type, int x, int y) { + return spawnEntity(type, x, y, false); + } + + public Npc spawnEntity(String type, int x, int y, boolean effect) { + EntityConfig config = EntityRegistry.getEntityConfig(type); + + if(config == null) { + return null; + } + + Npc entity = new Npc(zone, config); + spawnEntity(entity, x, y, effect); + return entity; + } + public void spawnEntity(Entity entity, int x, int y) { spawnEntity(entity, x, y, false); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index d0be509..7d559f9 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -477,6 +477,10 @@ public class Zone { } } + public boolean isBlockEarthy(int x, int y) { + return areCoordinatesInBounds(x, y) && getBlock(x, y).getFrontItem().isEarthy(); + } + public boolean isBlockNatural(int x, int y) { return areCoordinatesInBounds(x, y) && getBlock(x, y).isNatural(); } @@ -1140,6 +1144,14 @@ public class Zone { entityManager.spawnPersistentNpcs(data); } + public Npc spawnEntity(String type, int x, int y) { + return entityManager.spawnEntity(type, x, y); + } + + public Npc spawnEntity(String type, int x, int y, boolean effect) { + return entityManager.spawnEntity(type, x, y, effect); + } + public void spawnEntity(Entity entity, int x, int y) { entityManager.spawnEntity(entity, x, y); } @@ -1305,6 +1317,10 @@ public class Zone { return weatherManager; } + public boolean isUnderground(int x, int y) { + return areCoordinatesInBounds(x, y) && y >= surface[x]; + } + public void setSurface(int x, int surface) { if(areCoordinatesInBounds(x, surface)) { this.surface[x] = surface; From a9ba16815fb04b8fed1a32b5fe7fc87be4ad279b Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 25 Feb 2024 19:15:10 +0100 Subject: [PATCH 37/47] Space biomes should have a higher surface level --- .../gameserver/zone/gen/tasks/TerrainGeneratorTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java index 21e4dc2..281dc86 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java @@ -33,7 +33,7 @@ public class TerrainGeneratorTask implements GeneratorTask { public void generate(GeneratorContext ctx) { int width = ctx.getWidth(); int height = ctx.getHeight(); - int surfaceLevel = height < 600 ? height / 3 : 200; + int surfaceLevel = type == TerrainType.ASTEROIDS ? (height < 600 ? height / 6 : 100) : (height < 600 ? height / 3 : 200); int lowestSurfaceLevel = 0; // Determine surface first, then start placing blocks. From e9a046c8ed0085e4a2890ebf3d14950d4ad8511f Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:50:30 +0100 Subject: [PATCH 38/47] Add creature trapping & implement trapper achievements --- .../gameserver/achievements/Achievement.java | 1 + .../achievements/TrappingAchievement.java | 19 +++++++ .../gameserver/entity/EntityConfig.java | 14 +++++ .../entity/player/PlayerStatistics.java | 26 +++++++++ .../gameserver/item/ItemUseType.java | 1 + .../server/requests/BlockPlaceRequest.java | 53 ++++++++++++++++++- .../java/brainwine/gameserver/zone/Zone.java | 4 ++ 7 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/achievements/TrappingAchievement.java diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java b/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java index 0e9e6e1..00fbfb2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java +++ b/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java @@ -29,6 +29,7 @@ import brainwine.gameserver.util.MathUtils; @Type(name = "DiscoveryAchievement", value = DiscoveryAchievement.class), @Type(name = "SpawnerStoppageAchievement", value = SpawnerStoppageAchievement.class), @Type(name = "UndertakerAchievement", value = UndertakerAchievement.class), + @Type(name = "TrappingAchievement", value = TrappingAchievement.class), @Type(name = "Journeyman", value = JourneymanAchievement.class) }) @JsonSerialize(using = AchievementSerializer.class) diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/TrappingAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievements/TrappingAchievement.java new file mode 100644 index 0000000..72cf2b2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/achievements/TrappingAchievement.java @@ -0,0 +1,19 @@ +package brainwine.gameserver.achievements; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; + +import brainwine.gameserver.entity.player.Player; + +public class TrappingAchievement extends Achievement { + + @JsonCreator + public TrappingAchievement(@JacksonInject("title") String title) { + super(title); + } + + @Override + public int getProgress(Player player) { + return player.getStatistics().getTotalTrappings(); + } +} \ No newline at end of file diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java index 31d21be..fdb43f8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java @@ -32,6 +32,8 @@ public class EntityConfig { private boolean character; private boolean human; private boolean named; + private boolean trappable; + private Item trappablePetItem; private Vector2i size = new Vector2i(1, 1); private EntityGroup group = EntityGroup.NONE; private WeightedMap loot = new WeightedMap<>(); @@ -94,6 +96,18 @@ public class EntityConfig { return named; } + public boolean isTrappable() { + return trappable; + } + + public boolean hasTrappablePetItem() { + return trappablePetItem != null && !trappablePetItem.isAir(); + } + + public Item getTrappablePetItem() { + return trappablePetItem; + } + @JsonSetter(nulls = Nulls.SKIP) private void setSize(Vector2i size) { this.size = size; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java index 22dc3b6..d628a5e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java @@ -21,6 +21,7 @@ import brainwine.gameserver.achievements.RaiderAchievement; import brainwine.gameserver.achievements.ScavengingAchievement; import brainwine.gameserver.achievements.SidekickAchievement; import brainwine.gameserver.achievements.SpawnerStoppageAchievement; +import brainwine.gameserver.achievements.TrappingAchievement; import brainwine.gameserver.achievements.UndertakerAchievement; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.item.Item; @@ -34,6 +35,7 @@ public class PlayerStatistics { private Map discoveries = new HashMap<>(); private Map kills = new HashMap<>(); private Map assists = new HashMap<>(); + private Map trappings = new HashMap<>(); private float playTime; private int itemsPlaced; private int areasExplored; @@ -224,6 +226,30 @@ public class PlayerStatistics { return Collections.unmodifiableMap(assists); } + public void trackTrapping(EntityConfig entity) { + trappings.put(entity, getTrappings(entity) + 1); + player.addExperience(5); + player.updateAchievementProgress(TrappingAchievement.class); + } + + public void setTrappings(Map trappings) { + this.trappings = trappings; + } + + public int getTotalTrappings() { + return trappings.values().stream() + .reduce(Integer::sum) + .orElse(0); + } + + public int getTrappings(EntityConfig entity) { + return trappings.getOrDefault(entity, 0); + } + + public Map getTrappings() { + return Collections.unmodifiableMap(trappings); + } + public void trackPlayTime(float deltaTime) { playTime += deltaTime; } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java index 0042bb5..15282a7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java @@ -33,6 +33,7 @@ public enum ItemUseType { FLY, MULTI, NOTE(new NoteInteraction()), + PET, PLENTY, PROTECTED, PUBLIC, diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java index d024ee2..aec3f4f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java @@ -11,6 +11,7 @@ import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemGroup; +import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.ModType; import brainwine.gameserver.server.PlayerRequest; @@ -118,15 +119,63 @@ public class BlockPlaceRequest extends PlayerRequest { processCustomPlace(zone, player); } - // Process burial if item placed is a gravestone + // Misc processing if(item.getGroup() == ItemGroup.GRAVESTONE) { processBurial(zone, player); + } else if(item.getGroup() == ItemGroup.CAGE) { + processTrapping(zone, player); } } + private void processTrapping(Zone zone, Player player) { + // Check bounds + if(x <= 0 || x + 1 >= zone.getWidth() || y <= 0 || y + 1 >= zone.getHeight()) { + return; + } + + // Do nothing if cage is not surrounded by whole blocks + if(!zone.isBlockWhole(x - 1, y - 1) || !zone.isBlockWhole(x, y - 1) || !zone.isBlockWhole(x + 1, y - 1) || !zone.isBlockWhole(x - 1, y) || !zone.isBlockWhole(x + 1, y) + || !zone.isBlockWhole(x - 1, y + 1) || !zone.isBlockWhole(x, y + 1) || !zone.isBlockWhole(x + 1, y + 1)) { + return; + } + + // Find random trappable entity at this location + // TODO we have to do an isDead() check here because dead NPCs aren't always cleared immediately + Npc entity = zone.getNpcs().stream() + .filter(npc -> !npc.isDead() && !npc.isArtificial() && npc.getBlockX() == x && npc.getBlockY() == y && npc.getConfig().isTrappable()) + .findFirst().orElse(null); + + // Do nothing if no eligible entity was found + if(entity == null) { + return; + } + + EntityConfig config = entity.getConfig(); + + // Try to turn entity into a pet cage + if(item.hasUse(ItemUseType.PET)) { + // Don't waste it if entity has no pet variant + if(!config.hasTrappablePetItem()) { + return; + } + + entity.setHealth(0.0F); + zone.updateBlock(x, y, layer, 0); + player.getInventory().addItem(config.getTrappablePetItem(), true); + player.getStatistics().trackTrapping(config); + return; + } + + // Otherwise, kill the entity and place some fur + // TODO v2 stores the quantity in the mod of "piled" items, but this functionality is not implemented here at all! + entity.attack(player, item, entity.getHealth(), DamageType.ACID, true); + zone.updateBlock(x, y, Layer.FRONT, "ground/fur"); + player.getStatistics().trackTrapping(config); + } + private void processBurial(Zone zone, Player player) { // Check bounds - if(x <= 0 || x + 2 > zone.getWidth() || y + 2 > zone.getHeight()) { + if(x <= 0 || x + 2 >= zone.getWidth() || y + 2 >= zone.getHeight()) { return; } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 7d559f9..a23d040 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -477,6 +477,10 @@ public class Zone { } } + public boolean isBlockWhole(int x, int y) { + return areCoordinatesInBounds(x, y) && getBlock(x, y).getFrontItem().isWhole(); + } + public boolean isBlockEarthy(int x, int y) { return areCoordinatesInBounds(x, y) && getBlock(x, y).getFrontItem().isEarthy(); } From 5de602f0df4c2a33fb72f88a8c308bd9afa194a5 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:35:05 +0100 Subject: [PATCH 39/47] Update chunk data format --- .../server/requests/BlocksRequest.java | 7 + .../java/brainwine/gameserver/zone/Chunk.java | 10 + .../gameserver/zone/ChunkManager.java | 185 +++++++++++------- 3 files changed, 130 insertions(+), 72 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java index 706a8bb..3a00167 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java @@ -47,6 +47,13 @@ public class BlocksRequest extends PlayerRequest { } Chunk chunk = zone.getChunk(index); + + // Kick player if chunk is null (load failure) + if(chunk == null) { + player.kick("Chunk load failure."); + return; + } + chunks.add(chunk); metaBlocks.addAll(zone.getLocalMetaBlocksInChunk(index)); player.addActiveChunk(index); diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java b/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java index 954ddd7..e594e62 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java @@ -16,6 +16,7 @@ public class Chunk { private final int width; private final int height; private final Block[] blocks; + private long saveTime; private boolean modified; @ConstructorProperties({"x", "y", "width", "height", "blocks"}) @@ -83,4 +84,13 @@ public class Chunk { public Block[] getBlocks() { return blocks; } + + public void setSaveTime(long saveTime) { + this.saveTime = saveTime; + } + + @JsonIgnore + public long getSaveTime() { + return saveTime; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java index d299dd3..95d77cb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java @@ -17,11 +17,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.zip.DataFormatException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessageUnpacker; import org.msgpack.jackson.dataformat.MessagePackFactory; import com.fasterxml.jackson.databind.ObjectMapper; @@ -34,66 +33,101 @@ import brainwine.gameserver.util.ZipUtils; public class ChunkManager { + public static final int FILE_SIGNATURE = 0x44574344; + public static final int FILE_HEADER_SIZE = 64; + public static final int LATEST_FILE_VERSION = 0x00000001; + public static final int DEFAULT_CHUNK_ALLOC_SIZE = 2048; + public static final int CHUNK_HEADER_SIZE = 32; + public static final byte[] FILE_HEADER_PADDING = new byte[FILE_HEADER_SIZE - 12]; + public static final byte[] CHUNK_HEADER_PADDING = new byte[CHUNK_HEADER_SIZE - 12]; private static final Logger logger = LogManager.getLogger(); - private static final int allocSize = 2048; private static final ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()) .registerModule(new SimpleModule() .addDeserializer(Block.class, BlockDeserializer.INSTANCE) .addSerializer(BlockSerializer.INSTANCE)); private final Map chunks = new HashMap<>(); private final Zone zone; - private final File blocksFile; private RandomAccessFile file; - private int dataOffset; + private int allocSize; public ChunkManager(Zone zone) { this.zone = zone; - blocksFile = new File(zone.getDirectory(), "blocks.dat"); - File legacyBlocksFile = new File(zone.getDirectory(), "blocks"); + } + + private void initialize() throws IOException, DataFormatException { + // Do nothing if already initialized + if(file != null) { + return; + } - if(!blocksFile.exists() && legacyBlocksFile.exists()) { - logger.info(SERVER_MARKER, "Updating blocks file for zone {} ...", zone.getDocumentId()); - DataInputStream inputStream = null; - DataOutputStream outputStream = null; - - try { - inputStream = new DataInputStream(new FileInputStream(legacyBlocksFile)); - outputStream = new DataOutputStream(new FileOutputStream(blocksFile)); - int chunkCount = zone.getChunkCount(); - - for(int i = 0; i < chunkCount; i++) { - short length = inputStream.readShort(); - byte[] chunkBytes = new byte[length]; - inputStream.read(chunkBytes); - inputStream.skipBytes(2048 - length - 2); - chunkBytes = ZipUtils.inflateBytes(chunkBytes); - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(chunkBytes); - unpacker.unpackArrayHeader(); - int x = unpacker.unpackInt(); - int y = unpacker.unpackInt(); - int width = unpacker.unpackInt(); - int height = unpacker.unpackInt(); - Block[] blocks = new Block[unpacker.unpackArrayHeader() / 3]; - - for(int j = 0; j < blocks.length; j++) { - blocks[j] = new Block(unpacker.unpackInt(), unpacker.unpackInt(), unpacker.unpackInt()); - } - - unpacker.close(); - byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(new Chunk(x, y, width, height, blocks))); - outputStream.writeShort(bytes.length); - outputStream.write(bytes); - outputStream.write(new byte[allocSize - bytes.length - 2]); + File dataDirectory = zone.getDirectory(); + File chunksFileV1 = new File(dataDirectory, "blocks"); // Legacy version (no longer supported) + File chunksFileV2 = new File(dataDirectory, "blocks.dat"); // Previous version + File chunksFile = new File(dataDirectory, "chunks.bin"); // Latest version + + // Check outdated legacy format + if(!chunksFileV2.exists() && chunksFileV1.exists()) { + throw new IOException("Chunk data is outdated. Please try to load this zone with an older server version to update it."); + } + + // Load or initialize header data + if(chunksFile.exists()) { + try(DataInputStream inputStream = new DataInputStream(new FileInputStream(chunksFile))) { + // Check file signature + if(inputStream.readInt() != FILE_SIGNATURE) { + throw new IOException("Invalid file signature"); } - inputStream.close(); - outputStream.close(); - } catch(Exception e) { - logger.error(SERVER_MARKER, "Could not update blocks file for zone {}", zone.getDocumentId(), e); + int fileVersion = inputStream.readInt(); + allocSize = inputStream.readInt(); + inputStream.skip(FILE_HEADER_PADDING.length); + + // Update chunk data if necessary + if(fileVersion != LATEST_FILE_VERSION) { + throw new IOException("Invalid file version"); // Throw exception for now since there is only one version + } } + } else { + allocSize = DEFAULT_CHUNK_ALLOC_SIZE; - legacyBlocksFile.delete(); + try(DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(chunksFile))) { + outputStream.writeInt(FILE_SIGNATURE); + outputStream.writeInt(LATEST_FILE_VERSION); + outputStream.writeInt(allocSize); + outputStream.write(FILE_HEADER_PADDING); + + // Update chunk data from previous version if it is present + if(chunksFileV2.exists()) { + logger.info(SERVER_MARKER, "Updating chunk data for zone {} ...", zone.getDocumentId()); + int chunkCount = zone.getChunkCount(); + + try(DataInputStream inputStream = new DataInputStream(new FileInputStream(chunksFileV2))) { + long now = System.currentTimeMillis(); + + for(int i = 0; i < chunkCount; i++) { + byte[] chunkBytes = inputStream.readNBytes(inputStream.readShort()); + inputStream.skip(DEFAULT_CHUNK_ALLOC_SIZE - chunkBytes.length - 2); // Skip reserved chunk space + + // Write chunk header + outputStream.writeLong(now); // Save time + outputStream.writeInt(chunkBytes.length); + outputStream.write(CHUNK_HEADER_PADDING); + + // Write chunk data + outputStream.write(chunkBytes); + + // Write chunk padding + if(i + 1 < chunkCount) { + outputStream.write(new byte[allocSize - chunkBytes.length - CHUNK_HEADER_SIZE]); + } + } + } + } + } } + + // Create random access file stream + file = new RandomAccessFile(chunksFile, "rw"); } protected void closeStream() { @@ -112,10 +146,7 @@ public class ChunkManager { List inactiveChunks = new ArrayList<>(); for(Chunk chunk : chunks.values()) { - if(chunk.isModified()) { - saveChunk(chunk); - } - + saveChunk(chunk); boolean active = false; for(Player player : zone.getPlayers()) { @@ -140,35 +171,40 @@ public class ChunkManager { int index = zone.getChunkIndex(chunk.getX(), chunk.getY()); try { - if(file == null) { - file = new RandomAccessFile(blocksFile, "rw"); + initialize(); + file.seek(FILE_HEADER_SIZE + index * allocSize); + file.writeLong(System.currentTimeMillis()); // Write save time + + // Write block data if chunk has been modified + if(chunk.isModified()) { + byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(chunk)); + + // TODO reformat entire file with bigger alloc size + if(bytes.length > allocSize - CHUNK_HEADER_SIZE) { + throw new IOException("WARNING: bigger than alloc size: " + bytes.length); + } + + file.writeInt(bytes.length); + file.write(CHUNK_HEADER_PADDING); + file.write(bytes); + chunk.setModified(false); } - - byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(chunk)); - - if(bytes.length > allocSize) { - throw new IOException("WARNING: bigger than alloc size: " + bytes.length); - } - - file.seek(dataOffset + index * allocSize); - file.writeShort(bytes.length); - file.write(bytes); - chunk.setModified(false); - } catch (IOException e) { + } catch(Exception e) { logger.error(SERVER_MARKER, "Could not save chunk {} of zone {}", index, zone.getDocumentId(), e); } } private Chunk loadChunk(int index) { try { - if(file == null) { - file = new RandomAccessFile(blocksFile, "rw"); - } - - file.seek(dataOffset + index * allocSize); - byte[] bytes = new byte[file.readShort()]; + initialize(); + file.seek(FILE_HEADER_SIZE + index * allocSize); + long saveTime = file.readLong(); + byte[] bytes = new byte[file.readInt()]; + file.skipBytes(CHUNK_HEADER_PADDING.length); file.read(bytes); - return mapper.readValue(ZipUtils.inflateBytes(bytes), Chunk.class); + Chunk chunk = mapper.readValue(ZipUtils.inflateBytes(bytes), Chunk.class); + chunk.setSaveTime(saveTime); + return chunk; } catch(Exception e) { logger.error(SERVER_MARKER, "Could not load chunk {} of zone {}", index, zone.getDocumentId(), e); } @@ -237,10 +273,15 @@ public class ChunkManager { Chunk chunk = chunks.get(index); + // Load chunk if it isn't cached if(chunk == null) { chunk = loadChunk(index); - chunks.put(index, chunk); - zone.onChunkLoaded(chunk); + + // Index chunk if it was loaded successfully + if(chunk != null) { + chunks.put(index, chunk); + zone.onChunkLoaded(chunk); + } } return chunk; From 75b12918187641b184646d2724ae653bc4e727bd Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:49:32 +0100 Subject: [PATCH 40/47] Fix oopsie --- .../java/brainwine/gameserver/zone/ChunkManager.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java index 95d77cb..4417c71 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java @@ -100,12 +100,13 @@ public class ChunkManager { if(chunksFileV2.exists()) { logger.info(SERVER_MARKER, "Updating chunk data for zone {} ...", zone.getDocumentId()); int chunkCount = zone.getChunkCount(); + long now = System.currentTimeMillis(); - try(DataInputStream inputStream = new DataInputStream(new FileInputStream(chunksFileV2))) { - long now = System.currentTimeMillis(); - + try(DataInputStream inputStream = new DataInputStream(new FileInputStream(chunksFileV2))) { for(int i = 0; i < chunkCount; i++) { - byte[] chunkBytes = inputStream.readNBytes(inputStream.readShort()); + // Read chunk data + byte[] chunkBytes = new byte[inputStream.readShort()]; + inputStream.read(chunkBytes); inputStream.skip(DEFAULT_CHUNK_ALLOC_SIZE - chunkBytes.length - 2); // Skip reserved chunk space // Write chunk header From 59e321292ab85e0e4c8d782bfaaa2a7c3ce66fa3 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Thu, 7 Mar 2024 00:21:34 +0100 Subject: [PATCH 41/47] Test --- .../java/brainwine/gameserver/entity/player/PlayerManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java index 463ed3f..1b4766b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java @@ -21,7 +21,7 @@ import brainwine.shared.JsonHelper; public class PlayerManager { // TODO check platforms as well - public static final List SUPPORTED_VERSIONS = Arrays.asList("1.12.1", "2.11.0.1", "2.11.1", "3.13.1"); + public static final List SUPPORTED_VERSIONS = Arrays.asList("1.13.3", "2.11.0.1", "2.11.1", "3.13.1"); private static final Logger logger = LogManager.getLogger(); private final Map playersById = new HashMap<>(); private final Map playersByName = new HashMap<>(); From 056e0631ac3c479ea6030fba3b15775a917cda24 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:53:50 +0100 Subject: [PATCH 42/47] Add account lock remover for MacOS --- src/main/java/brainwine/gui/MainView.java | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/brainwine/gui/MainView.java b/src/main/java/brainwine/gui/MainView.java index 8589902..1af4c3d 100644 --- a/src/main/java/brainwine/gui/MainView.java +++ b/src/main/java/brainwine/gui/MainView.java @@ -29,6 +29,7 @@ import brainwine.Bootstrap; import brainwine.util.DesktopUtils; import brainwine.util.OperatingSystem; import brainwine.util.ProcessResult; +import brainwine.util.ProcessUtils; import brainwine.util.RegistryKey; import brainwine.util.RegistryUtils; import brainwine.util.SwingUtils; @@ -65,11 +66,7 @@ public class MainView { // Menu JMenuBar menuBar = new JMenuBar(); JMenu helpMenu = new JMenu("Help"); - - if(OperatingSystem.isWindows()) { - helpMenu.add(SwingUtils.createAction("Clear Account Lock", this::showAccountLockPrompt)); - } - + helpMenu.add(SwingUtils.createAction("Clear Account Lock", this::showAccountLockPrompt)); helpMenu.add(SwingUtils.createAction("GitHub", () -> DesktopUtils.browseUrl(GITHUB_REPOSITORY_URL))); menuBar.add(helpMenu); @@ -135,12 +132,16 @@ public class MainView { if(clearAccountLock()) { JOptionPane.showMessageDialog(frame, "Account lock removed. Register your account next time."); } else { - JOptionPane.showMessageDialog(frame, "Failed to remove account lock.", "Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(frame, "Could not remove account lock.\nEither there is no account lock, or an error has occured.", "Error", JOptionPane.ERROR_MESSAGE); } } } private boolean clearAccountLock() { + return OperatingSystem.isWindows() ? clearAccountLockWindows() : OperatingSystem.isMacOS() ? clearAccountLockMacOS() : false; + } + + private boolean clearAccountLockWindows() { ProcessResult queryResult = RegistryUtils.query(DEEPWORLD_PLAYERPREFS, "playerLock*"); if(queryResult.wasSuccessful()) { @@ -151,11 +152,15 @@ public class MainView { ProcessResult deleteResult = RegistryUtils.delete(DEEPWORLD_PLAYERPREFS, name); return deleteResult.wasSuccessful(); } else { - // Might as well. - return true; + return false; } } return false; } + + // A bit simpler but it should to the trick just fine. + private boolean clearAccountLockMacOS() { + return ProcessUtils.executeCommand("defaults delete com.bytebin.deepworld playerLocked").wasSuccessful(); + } } From 4303c338a55957b43f9507aeaa40db5324cd3631 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:36:46 +0200 Subject: [PATCH 43/47] Automatic default zone sizes --- .../src/main/java/brainwine/gameserver/zone/ZoneManager.java | 2 +- .../java/brainwine/gameserver/zone/gen/ZoneGenerator.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java index 38608ee..f8510c5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java @@ -65,7 +65,7 @@ public class ZoneManager { generator = ZoneGenerator.getDefaultZoneGenerator(); } - Zone zone = generator.generateZone(Biome.PLAIN, 2000, 600); + Zone zone = generator.generateZone(Biome.PLAIN); addZone(zone); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java index 542a526..77a2573 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java @@ -122,7 +122,7 @@ public class ZoneGenerator { } public Zone generateZone(Biome biome) { - return generateZone(biome, 2000, 600); + return generateZone(biome, biome == Biome.DEEP ? 1200 : 2000, biome == Biome.DEEP ? 1000 : 600); } public Zone generateZone(Biome biome, int width, int height) { @@ -165,7 +165,7 @@ public class ZoneGenerator { } public void generateZoneAsync(Biome biome, Consumer callback) { - generateZoneAsync(biome, 2000, 600, callback); + generateZoneAsync(biome, biome == Biome.DEEP ? 1200 : 2000, biome == Biome.DEEP ? 1000 : 600, callback); } public void generateZoneAsync(Biome biome, int width, int height, Consumer callback) { From a58575369bce67cd1cc1aea121e170889c018531 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:59:57 +0200 Subject: [PATCH 44/47] Refactor commands --- .../gameserver/GameConfiguration.java | 2 +- .../java/brainwine/gameserver/GameServer.java | 4 +- .../gameserver/annotations/CommandInfo.java | 15 ++ .../brainwine/gameserver/command/Command.java | 24 --- .../gameserver/command/CommandManager.java | 182 ------------------ .../command/commands/RickrollCommand.java | 56 ------ .../command/commands/StopCommand.java | 33 ---- .../gameserver/commands/Command.java | 11 ++ .../CommandExecutor.java | 2 +- .../gameserver/commands/CommandManager.java | 148 ++++++++++++++ .../{command => }/commands/HelpCommand.java | 34 ++-- .../commands/RegisterCommand.java | 15 +- .../{command => }/commands/SayCommand.java | 16 +- .../{command => }/commands/ThinkCommand.java | 16 +- .../admin}/AcidityCommand.java | 18 +- .../admin}/AdminCommand.java | 18 +- .../admin}/BanCommand.java | 23 +-- .../admin}/BroadcastCommand.java | 23 +-- .../admin}/EntityCommand.java | 18 +- .../admin}/ExperienceCommand.java | 23 +-- .../admin}/ExportCommand.java | 18 +- .../admin}/GenerateZoneCommand.java | 23 +-- .../admin}/GiveCommand.java | 18 +- .../admin}/HealthCommand.java | 23 +-- .../admin}/ImportCommand.java | 18 +- .../admin}/KickCommand.java | 18 +- .../admin}/LevelCommand.java | 23 +-- .../admin}/MuteCommand.java | 23 +-- .../admin}/PlayerIdCommand.java | 18 +- .../admin}/PositionCommand.java | 23 +-- .../admin}/PrefabListCommand.java | 23 +-- .../admin}/SeedCommand.java | 18 +- .../admin}/SettleLiquidsCommand.java | 22 +-- .../admin}/SkillPointsCommand.java | 23 +-- .../commands/admin/StopCommand.java | 25 +++ .../admin}/TeleportCommand.java | 23 +-- .../admin}/TimeCommand.java | 18 +- .../admin}/UnbanCommand.java | 23 +-- .../admin}/UnmuteCommand.java | 18 +- .../admin}/WeatherCommand.java | 18 +- .../admin}/ZoneIdCommand.java | 18 +- .../gameserver/entity/player/Player.java | 2 +- .../server/requests/ChatRequest.java | 2 +- .../server/requests/ConsoleRequest.java | 2 +- src/main/java/brainwine/ServerThread.java | 2 +- 45 files changed, 365 insertions(+), 758 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/annotations/CommandInfo.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/command/Command.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/commands/Command.java rename gameserver/src/main/java/brainwine/gameserver/{command => commands}/CommandExecutor.java (83%) create mode 100644 gameserver/src/main/java/brainwine/gameserver/commands/CommandManager.java rename gameserver/src/main/java/brainwine/gameserver/{command => }/commands/HelpCommand.java (72%) rename gameserver/src/main/java/brainwine/gameserver/{command => }/commands/RegisterCommand.java (85%) rename gameserver/src/main/java/brainwine/gameserver/{command => }/commands/SayCommand.java (75%) rename gameserver/src/main/java/brainwine/gameserver/{command => }/commands/ThinkCommand.java (75%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/AcidityCommand.java (79%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/AdminCommand.java (82%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/BanCommand.java (84%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/BroadcastCommand.java (70%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/EntityCommand.java (80%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/ExperienceCommand.java (81%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/ExportCommand.java (90%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/GenerateZoneCommand.java (89%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/GiveCommand.java (90%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/HealthCommand.java (81%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/ImportCommand.java (83%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/KickCommand.java (80%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/LevelCommand.java (81%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/MuteCommand.java (83%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/PlayerIdCommand.java (78%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/PositionCommand.java (57%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/PrefabListCommand.java (76%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/SeedCommand.java (79%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/SettleLiquidsCommand.java (63%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/SkillPointsCommand.java (81%) create mode 100644 gameserver/src/main/java/brainwine/gameserver/commands/admin/StopCommand.java rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/TeleportCommand.java (73%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/TimeCommand.java (84%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/UnbanCommand.java (75%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/UnmuteCommand.java (79%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/WeatherCommand.java (82%) rename gameserver/src/main/java/brainwine/gameserver/{command/commands => commands/admin}/ZoneIdCommand.java (79%) diff --git a/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java b/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java index 60740db..f356533 100644 --- a/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java +++ b/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java @@ -24,7 +24,7 @@ import org.yaml.snakeyaml.Yaml; import com.fasterxml.jackson.core.JsonProcessingException; -import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.commands.CommandManager; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.item.Item; diff --git a/gameserver/src/main/java/brainwine/gameserver/GameServer.java b/gameserver/src/main/java/brainwine/gameserver/GameServer.java index d7df4f1..a115f66 100644 --- a/gameserver/src/main/java/brainwine/gameserver/GameServer.java +++ b/gameserver/src/main/java/brainwine/gameserver/GameServer.java @@ -9,8 +9,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import brainwine.gameserver.achievements.AchievementManager; -import brainwine.gameserver.command.CommandExecutor; -import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.commands.CommandExecutor; +import brainwine.gameserver.commands.CommandManager; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.PlayerManager; diff --git a/gameserver/src/main/java/brainwine/gameserver/annotations/CommandInfo.java b/gameserver/src/main/java/brainwine/gameserver/annotations/CommandInfo.java new file mode 100644 index 0000000..9dd4d13 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/annotations/CommandInfo.java @@ -0,0 +1,15 @@ +package brainwine.gameserver.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandInfo { + + public String name(); + public String description(); + public String[] aliases() default {}; +} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/Command.java b/gameserver/src/main/java/brainwine/gameserver/command/Command.java deleted file mode 100644 index 3c42d26..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/command/Command.java +++ /dev/null @@ -1,24 +0,0 @@ -package brainwine.gameserver.command; - -public abstract class Command { - - public abstract void execute(CommandExecutor executor, String[] args); - - public abstract String getName(); - - public String[] getAliases() { - return null; - } - - public String getDescription() { - return "No description for this command"; - } - - public String getUsage(CommandExecutor executor) { - return "/" + getName(); - } - - public boolean canExecute(CommandExecutor executor) { - return true; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java b/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java deleted file mode 100644 index 2a731f2..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java +++ /dev/null @@ -1,182 +0,0 @@ -package brainwine.gameserver.command; - -import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import static brainwine.shared.LogMarkers.SERVER_MARKER; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import brainwine.gameserver.command.commands.AcidityCommand; -import brainwine.gameserver.command.commands.AdminCommand; -import brainwine.gameserver.command.commands.BanCommand; -import brainwine.gameserver.command.commands.BroadcastCommand; -import brainwine.gameserver.command.commands.EntityCommand; -import brainwine.gameserver.command.commands.ExperienceCommand; -import brainwine.gameserver.command.commands.ExportCommand; -import brainwine.gameserver.command.commands.GenerateZoneCommand; -import brainwine.gameserver.command.commands.GiveCommand; -import brainwine.gameserver.command.commands.HealthCommand; -import brainwine.gameserver.command.commands.HelpCommand; -import brainwine.gameserver.command.commands.ImportCommand; -import brainwine.gameserver.command.commands.KickCommand; -import brainwine.gameserver.command.commands.LevelCommand; -import brainwine.gameserver.command.commands.MuteCommand; -import brainwine.gameserver.command.commands.PlayerIdCommand; -import brainwine.gameserver.command.commands.PositionCommand; -import brainwine.gameserver.command.commands.PrefabListCommand; -import brainwine.gameserver.command.commands.RegisterCommand; -import brainwine.gameserver.command.commands.RickrollCommand; -import brainwine.gameserver.command.commands.SayCommand; -import brainwine.gameserver.command.commands.SeedCommand; -import brainwine.gameserver.command.commands.SettleLiquidsCommand; -import brainwine.gameserver.command.commands.SkillPointsCommand; -import brainwine.gameserver.command.commands.StopCommand; -import brainwine.gameserver.command.commands.TeleportCommand; -import brainwine.gameserver.command.commands.ThinkCommand; -import brainwine.gameserver.command.commands.TimeCommand; -import brainwine.gameserver.command.commands.UnbanCommand; -import brainwine.gameserver.command.commands.UnmuteCommand; -import brainwine.gameserver.command.commands.WeatherCommand; -import brainwine.gameserver.command.commands.ZoneIdCommand; -import brainwine.gameserver.entity.player.Player; - -public class CommandManager { - - public static final String CUSTOM_COMMAND_PREFIX = "!"; // TODO configurable - private static final Logger logger = LogManager.getLogger(); - private static final Map commands = new HashMap<>(); - private static final Map aliases = new HashMap<>(); - private static boolean initialized = false; - - public static void init() { - if(initialized) { - logger.warn(SERVER_MARKER, "CommandManager is already initialized - skipping!"); - return; - } - - registerCommands(); - initialized = true; - } - - private static void registerCommands() { - logger.info(SERVER_MARKER, "Registering commands ..."); - registerCommand(new StopCommand()); - registerCommand(new RegisterCommand()); - registerCommand(new TeleportCommand()); - registerCommand(new KickCommand()); - registerCommand(new MuteCommand()); - registerCommand(new UnmuteCommand()); - registerCommand(new BanCommand()); - registerCommand(new UnbanCommand()); - registerCommand(new SayCommand()); - registerCommand(new ThinkCommand()); - registerCommand(new BroadcastCommand()); - registerCommand(new PlayerIdCommand()); - registerCommand(new ZoneIdCommand()); - registerCommand(new AdminCommand()); - registerCommand(new HelpCommand()); - registerCommand(new GiveCommand()); - registerCommand(new GenerateZoneCommand()); - registerCommand(new SeedCommand()); - registerCommand(new PrefabListCommand()); - registerCommand(new ExportCommand()); - registerCommand(new ImportCommand()); - registerCommand(new PositionCommand()); - registerCommand(new RickrollCommand()); - registerCommand(new EntityCommand()); - registerCommand(new HealthCommand()); - registerCommand(new ExperienceCommand()); - registerCommand(new LevelCommand()); - registerCommand(new SkillPointsCommand()); - registerCommand(new SettleLiquidsCommand()); - registerCommand(new WeatherCommand()); - registerCommand(new AcidityCommand()); - registerCommand(new TimeCommand()); - } - - public static void executeCommand(CommandExecutor executor, String commandLine) { - if(commandLine.isEmpty()) { - return; - } - - commandLine.trim().replaceAll(" +", " "); - String[] sections = commandLine.split(" ", 2); - - if(sections.length == 0) { - return; - } - - String name = sections[0]; - String[] args = sections.length > 1 ? sections[1].split(" ") : new String[0]; - executeCommand(executor, name, args); - } - - public static void executeCommand(CommandExecutor executor, String commandName, String[] args) { - if(!(executor instanceof Player) && commandName.startsWith(CUSTOM_COMMAND_PREFIX) || commandName.startsWith("/")) { - commandName = commandName.substring(1); - } - - Command command = getCommand(commandName, true); - - if(command == null || !command.canExecute(executor)) { - executor.notify("Unknown command. Type '/help' for a list of commands.", SYSTEM); - return; - } - - if(executor instanceof Player) { - Player player = (Player)executor; - logger.info(SERVER_MARKER, "{} used command '/{}'", player.getName(), commandName + (args.length == 0 ? "" : " " + String.join(" ", args))); - } - - command.execute(executor, args); - } - - public static void registerCommand(Command command) { - String name = command.getName(); - - if(commands.containsKey(name)) { - logger.warn(SERVER_MARKER, "Attempted to register duplicate command {} with name {}", command.getClass(), name); - return; - } - - commands.put(name, command); - String[] aliases = command.getAliases(); - - if(aliases != null) { - for(String alias : aliases) { - if(commands.containsKey(alias) || CommandManager.aliases.containsKey(alias)) { - logger.warn(SERVER_MARKER, "Duplicate alias {} for command {}", alias, command.getClass()); - continue; - } - - CommandManager.aliases.put(alias, command); - } - } - } - - public static Set getCommandNames() { - Set names = new HashSet<>(); - names.addAll(commands.keySet()); - names.addAll(aliases.keySet()); - return names; - } - - public static Command getCommand(String name) { - return getCommand(name, false); - } - - public static Command getCommand(String name, boolean allowAlias) { - return commands.getOrDefault(name, allowAlias ? aliases.get(name) : null); - } - - public static Collection getCommands() { - return Collections.unmodifiableCollection(commands.values()); - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java deleted file mode 100644 index 1113787..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java +++ /dev/null @@ -1,56 +0,0 @@ -package brainwine.gameserver.command.commands; - -import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; - -import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.server.messages.EventMessage; - -public class RickrollCommand extends Command { - - @Override - public void execute(CommandExecutor executor, String[] args) { - if(args.length < 1) { - executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM); - return; - } - - Player player = GameServer.getInstance().getPlayerManager().getPlayer(args[0]); - - if(player == null) { - executor.notify("This player does not exist.", SYSTEM); - return; - } else if(!player.isOnline()) { - executor.notify("This player is offline.", SYSTEM); - return; - } else if(!player.isV3()) { - executor.notify("Cannot open URLs on iOS clients.", SYSTEM); - return; - } - - player.sendMessage(new EventMessage("openUrl", "https://www.youtube.com/watch?v=dQw4w9WgXcQ")); - executor.notify(String.format("Successfully rickrolled %s!", player.getName()), SYSTEM); - } - - @Override - public String getName() { - return "rickroll"; - } - - @Override - public String getDescription() { - return "Makes a player hate you forever."; - } - - @Override - public String getUsage(CommandExecutor executor) { - return "/rickroll "; - } - - @Override - public boolean canExecute(CommandExecutor executor) { - return executor.isAdmin(); - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java deleted file mode 100644 index b7054ad..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java +++ /dev/null @@ -1,33 +0,0 @@ -package brainwine.gameserver.command.commands; - -import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; - -public class StopCommand extends Command { - - @Override - public void execute(CommandExecutor executor, String[] args) { - GameServer.getInstance().stopGracefully(); // YEET!! - } - - @Override - public String getName() { - return "stop"; - } - - @Override - public String[] getAliases() { - return new String[] { "exit", "close", "shutdown" }; - } - - @Override - public String getDescription() { - return "Gracefully shuts down the server after the current tick."; - } - - @Override - public boolean canExecute(CommandExecutor executor) { - return executor.isAdmin(); - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/commands/Command.java b/gameserver/src/main/java/brainwine/gameserver/commands/Command.java new file mode 100644 index 0000000..c655597 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/commands/Command.java @@ -0,0 +1,11 @@ +package brainwine.gameserver.commands; + +public abstract class Command { + + public abstract void execute(CommandExecutor executor, String[] args); + public abstract String getUsage(CommandExecutor executor); + + public boolean canExecute(CommandExecutor executor) { + return true; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java b/gameserver/src/main/java/brainwine/gameserver/commands/CommandExecutor.java similarity index 83% rename from gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java rename to gameserver/src/main/java/brainwine/gameserver/commands/CommandExecutor.java index 9929a1b..cda63e7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/CommandExecutor.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command; +package brainwine.gameserver.commands; import brainwine.gameserver.entity.player.NotificationType; diff --git a/gameserver/src/main/java/brainwine/gameserver/commands/CommandManager.java b/gameserver/src/main/java/brainwine/gameserver/commands/CommandManager.java new file mode 100644 index 0000000..e82da30 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/commands/CommandManager.java @@ -0,0 +1,148 @@ +package brainwine.gameserver.commands; + +import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; +import static brainwine.shared.LogMarkers.SERVER_MARKER; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.reflections.Reflections; + +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.entity.player.Player; + +@SuppressWarnings("unchecked") +public class CommandManager { + + public static final String CUSTOM_COMMAND_PREFIX = "!"; // TODO configurable + private static final Logger logger = LogManager.getLogger(); + private static final Map commands = new HashMap<>(); + private static final Map aliases = new HashMap<>(); + private static boolean initialized = false; + + public static void init() { + if(initialized) { + logger.warn(SERVER_MARKER, "CommandManager is already initialized - skipping!"); + return; + } + + registerCommands(); + initialized = true; + } + + private static void registerCommands() { + logger.info(SERVER_MARKER, "Registering commands ..."); + Reflections reflections = new Reflections("brainwine.gameserver.commands"); + Set> classes = reflections.getTypesAnnotatedWith(CommandInfo.class); + + for(Class clazz : classes) { + if(!Command.class.isAssignableFrom(clazz)) { + logger.warn(SERVER_MARKER, "Attempted to register non-command class {}", clazz.getSimpleName()); + continue; + } + + registerCommand((Class)clazz); + } + } + + public static void executeCommand(CommandExecutor executor, String commandLine) { + if(commandLine.isEmpty()) { + return; + } + + commandLine.trim().replaceAll(" +", " "); + String[] sections = commandLine.split(" ", 2); + + if(sections.length == 0) { + return; + } + + String name = sections[0]; + String[] args = sections.length > 1 ? sections[1].split(" ") : new String[0]; + executeCommand(executor, name, args); + } + + public static void executeCommand(CommandExecutor executor, String commandName, String[] args) { + if(!(executor instanceof Player) && commandName.startsWith(CUSTOM_COMMAND_PREFIX) || commandName.startsWith("/")) { + commandName = commandName.substring(1); + } + + Command command = getCommand(commandName, true); + + if(command == null || !command.canExecute(executor)) { + executor.notify("Unknown command. Type '/help' for a list of commands.", SYSTEM); + return; + } + + if(executor instanceof Player) { + Player player = (Player)executor; + logger.info(SERVER_MARKER, "{} used command '/{}'", player.getName(), commandName + (args.length == 0 ? "" : " " + String.join(" ", args))); + } + + command.execute(executor, args); + } + + public static void registerCommand(Class type) { + CommandInfo info = type.getAnnotation(CommandInfo.class); + + if(info == null) { + logger.warn(SERVER_MARKER, "Cannot register command '{}' because it does not have the CommandInfo annotation", type.getSimpleName()); + return; + } + + String name = info.name(); + String[] aliases = info.aliases(); + + if(commands.containsKey(name)) { + logger.warn(SERVER_MARKER, "Attempted to register duplicate command '{}' with name '{}'", type.getSimpleName(), name); + return; + } + + Command command = null; + + try { + command = type.getConstructor().newInstance(); + } catch(ReflectiveOperationException e) { + logger.error("Failed to not instantiate command '{}'", type.getSimpleName(), e); + return; + } + + commands.put(name, command); + + if(aliases != null) { + for(String alias : aliases) { + if(commands.containsKey(alias) || CommandManager.aliases.containsKey(alias)) { + logger.warn(SERVER_MARKER, "Duplicate alias {} for command {}", alias, command.getClass()); + continue; + } + + CommandManager.aliases.put(alias, command); + } + } + } + + public static Set getCommandNames() { + Set names = new HashSet<>(); + names.addAll(commands.keySet()); + names.addAll(aliases.keySet()); + return names; + } + + public static Command getCommand(String name) { + return getCommand(name, false); + } + + public static Command getCommand(String name, boolean allowAlias) { + return commands.getOrDefault(name, allowAlias ? aliases.get(name) : null); + } + + public static Collection getCommands() { + return Collections.unmodifiableCollection(commands.values()); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/HelpCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/HelpCommand.java similarity index 72% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/HelpCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/HelpCommand.java index 2516d58..8c9fcb3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/HelpCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/HelpCommand.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; @@ -8,16 +8,20 @@ import java.util.List; import org.apache.commons.lang3.math.NumberUtils; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; -import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.annotations.CommandInfo; +@CommandInfo(name = "help", description = "Displays a list of commands.") public class HelpCommand extends Command { @Override public void execute(CommandExecutor executor, String[] args) { List commands = new ArrayList<>(CommandManager.getCommands()); commands.removeIf(command -> !command.canExecute(executor)); + commands.sort((a, b) -> { + CommandInfo info1 = a.getClass().getAnnotation(CommandInfo.class); + CommandInfo info2 = b.getClass().getAnnotation(CommandInfo.class); + return info1.name().compareTo(info2.name()); + }); int pageSize = 8; int pageCount = (int)Math.ceil(commands.size() / (double)pageSize); int page = 1; @@ -41,11 +45,12 @@ public class HelpCommand extends Command { return; } - executor.notify(String.format("========== Information about '/%s' ==========", command.getName()), SYSTEM); - executor.notify(String.format("Description: %s", command.getDescription()), SYSTEM); + CommandInfo info = command.getClass().getAnnotation(CommandInfo.class); + executor.notify(String.format("========== Information about '/%s' ==========", info.name()), SYSTEM); + executor.notify(String.format("Description: %s", info.description()), SYSTEM); executor.notify(String.format("Usage: %s", command.getUsage(executor)), SYSTEM); - executor.notify(String.format("Aliases: %s", command.getAliases() == null ? "None :(" - : Arrays.toString(command.getAliases())), SYSTEM); + executor.notify(String.format("Aliases: %s", info.aliases() == null ? "None :(" + : Arrays.toString(info.aliases())), SYSTEM); return; } } @@ -56,19 +61,10 @@ public class HelpCommand extends Command { executor.notify(String.format("========== Command List (Page %s of %s) ==========", page, pageCount), SYSTEM); for(Command command : commandsToDisplay) { - executor.notify(String.format("%s - %s", command.getUsage(executor), command.getDescription()), SYSTEM); + CommandInfo info = command.getClass().getAnnotation(CommandInfo.class); + executor.notify(String.format("%s - %s", command.getUsage(executor), info.description()), SYSTEM); } } - - @Override - public String getName() { - return "help"; - } - - @Override - public String getDescription() { - return "Displays command information."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/RegisterCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/RegisterCommand.java similarity index 85% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/RegisterCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/RegisterCommand.java index f8ff79d..cab3cf5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/RegisterCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/RegisterCommand.java @@ -1,16 +1,16 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands; import org.apache.commons.validator.routines.EmailValidator; import org.mindrot.jbcrypt.BCrypt; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; import brainwine.gameserver.dialog.DialogHelper; import brainwine.gameserver.entity.player.Player; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; +@CommandInfo(name = "register", description = "Shows a prompt with which you can register your account.") public class RegisterCommand extends Command { @Override @@ -55,13 +55,8 @@ public class RegisterCommand extends Command { } @Override - public String getName() { - return "register"; - } - - @Override - public String getDescription() { - return "Shows a prompt with which you can register your account."; + public String getUsage(CommandExecutor executor) { + return "/register"; } @Override diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SayCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/SayCommand.java similarity index 75% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SayCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/SayCommand.java index e93375b..63f82d5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SayCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/SayCommand.java @@ -1,12 +1,12 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; import brainwine.gameserver.entity.player.ChatType; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "say", description = "Shows a speech bubble to nearby players.") public class SayCommand extends Command { @Override @@ -27,16 +27,6 @@ public class SayCommand extends Command { player.getZone().sendChatMessage(player, text, ChatType.SPEECH); } - @Override - public String getName() { - return "say"; - } - - @Override - public String getDescription() { - return "Shows a speech bubble to nearby players."; - } - @Override public String getUsage(CommandExecutor executor) { return "/say "; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ThinkCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/ThinkCommand.java similarity index 75% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ThinkCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/ThinkCommand.java index e4b5240..599aa0a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ThinkCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/ThinkCommand.java @@ -1,12 +1,12 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; import brainwine.gameserver.entity.player.ChatType; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "think", description = "Shows a thought bubble to nearby players.") public class ThinkCommand extends Command { @Override @@ -27,16 +27,6 @@ public class ThinkCommand extends Command { player.getZone().sendChatMessage(player, text, ChatType.THOUGHT); } - @Override - public String getName() { - return "think"; - } - - @Override - public String getDescription() { - return "Shows a thought bubble to nearby players."; - } - @Override public String getUsage(CommandExecutor executor) { return "/think "; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/AcidityCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/AcidityCommand.java similarity index 79% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/AcidityCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/AcidityCommand.java index 77aa568..2bfc092 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/AcidityCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/AcidityCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "acidity", description = "Displays or changes the acidity in the current zone.") public class AcidityCommand extends Command { @Override @@ -35,16 +37,6 @@ public class AcidityCommand extends Command { zone.setAcidity(value); executor.notify(String.format("Acidity has been set to %s in %s.", value, zone.getName()), SYSTEM); } - - @Override - public String getName() { - return "acidity"; - } - - @Override - public String getDescription() { - return "Displays or changes the acidity in the current zone."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/AdminCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/AdminCommand.java similarity index 82% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/AdminCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/AdminCommand.java index cfce46c..7106f5f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/AdminCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/AdminCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "admin", description = "Grants or revokes administrator rights.") public class AdminCommand extends Command { @Override @@ -38,16 +40,6 @@ public class AdminCommand extends Command { executor.notify(String.format("Changed administrator status of player %s to %s", target.getName(), admin), SYSTEM); } - @Override - public String getName() { - return "admin"; - } - - @Override - public String getDescription() { - return "Allows you to grant or revoke administrator rights."; - } - @Override public String getUsage(CommandExecutor executor) { return "/admin [true|false]"; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/BanCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/BanCommand.java similarity index 84% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/BanCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/BanCommand.java index 2296cd8..d9982db 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/BanCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/BanCommand.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; @@ -7,11 +7,13 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.util.DateTimeUtils; +@CommandInfo(name = "ban", description = "Bans a player from the server.") public class BanCommand extends Command { @Override @@ -58,21 +60,6 @@ public class BanCommand extends Command { target.getName(), endDate.format(DateTimeFormatter.RFC_1123_DATE_TIME), reason), SYSTEM); } - @Override - public String getName() { - return "ban"; - } - - @Override - public String[] getAliases() { - return new String[] { "banish" }; - } - - @Override - public String getDescription() { - return "Bans a player from the server."; - } - @Override public String getUsage(CommandExecutor executor) { return "/ban [reason]"; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/BroadcastCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/BroadcastCommand.java similarity index 70% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/BroadcastCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/BroadcastCommand.java index 08c6f8a..1bb7b25 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/BroadcastCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/BroadcastCommand.java @@ -1,13 +1,15 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.POPUP; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "broadcast", description = "Broadcasts a message to all online players.", aliases = "bc") public class BroadcastCommand extends Command { @Override @@ -26,21 +28,6 @@ public class BroadcastCommand extends Command { executor.notify("Your message has been broadcasted.", POPUP); } - @Override - public String getName() { - return "broadcast"; - } - - @Override - public String[] getAliases() { - return new String[] { "bc" }; - } - - @Override - public String getDescription() { - return "Broadcasts a message to all online players."; - } - @Override public String getUsage(CommandExecutor executor) { return "/broadcast "; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java similarity index 80% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java index f52ad8e..408f782 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java @@ -1,9 +1,10 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.npc.Npc; @@ -11,6 +12,7 @@ import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "entity", description = "Spawns an entity at your current location.") public class EntityCommand extends Command { @Override @@ -32,16 +34,6 @@ public class EntityCommand extends Command { Zone zone = player.getZone(); zone.spawnEntity(new Npc(zone, config), (int)player.getX(), (int)player.getY(), true); } - - @Override - public String getName() { - return "entity"; - } - - @Override - public String getDescription() { - return "Spawns an entity at your current location."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExperienceCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ExperienceCommand.java similarity index 81% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ExperienceCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/ExperienceCommand.java index 3706c4a..fdd84d7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExperienceCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ExperienceCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "experience", description = "Sets the experience of the target player.", aliases = { "xp", "exp" }) public class ExperienceCommand extends Command { @Override @@ -53,21 +55,6 @@ public class ExperienceCommand extends Command { target.setExperience(experience); executor.notify(String.format("Successfully set %s's experience to %s.", target.getName(), experience), SYSTEM); } - - @Override - public String getName() { - return "experience"; - } - - @Override - public String[] getAliases() { - return new String[] { "xp", "exp" }; - } - - @Override - public String getDescription() { - return "Sets the experience of the target player."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExportCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ExportCommand.java similarity index 90% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ExportCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/ExportCommand.java index d4fdb66..399f1f4 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExportCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ExportCommand.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; @@ -6,13 +6,15 @@ import java.util.Arrays; import java.util.regex.Pattern; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.prefab.Prefab; import brainwine.gameserver.prefab.PrefabManager; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "export", description = "Exports a section of a zone to a prefab file.") public class ExportCommand extends Command { public static final Pattern PREFAB_NAME_PATTERN = Pattern.compile("\\w+(/\\w+)*"); @@ -80,16 +82,6 @@ public class ExportCommand extends Command { } } - @Override - public String getName() { - return "export"; - } - - @Override - public String getDescription() { - return "Exports a section of a zone to a prefab file."; - } - @Override public String getUsage(CommandExecutor executor) { return "/export "; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/GenerateZoneCommand.java similarity index 89% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/GenerateZoneCommand.java index 53985f4..0036466 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/GenerateZoneCommand.java @@ -1,14 +1,16 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.Zone; import brainwine.gameserver.zone.gen.ZoneGenerator; +@CommandInfo(name = "genzone", description = "Asynchronously generates a new zone.", aliases = "generate") public class GenerateZoneCommand extends Command { public static final int MIN_WIDTH = 200; @@ -84,21 +86,6 @@ public class GenerateZoneCommand extends Command { } }); } - - @Override - public String getName() { - return "genzone"; - } - - @Override - public String[] getAliases() { - return new String[] { "generate" }; - } - - @Override - public String getDescription() { - return "Asynchronously generates a new zone."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/GiveCommand.java similarity index 90% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/GiveCommand.java index 56fb90b..9a0bd92 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/GiveCommand.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; @@ -6,12 +6,14 @@ import java.util.ArrayList; import java.util.List; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; +@CommandInfo(name = "give", description = "Give or take items from players.") public class GiveCommand extends Command { @Override @@ -84,16 +86,6 @@ public class GiveCommand extends Command { executor.notify(String.format("Took %s %s from %s", -quantity, title, target.getName()), SYSTEM); } } - - @Override - public String getName() { - return "give"; - } - - @Override - public String getDescription() { - return "Adds items to a player's inventory."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/HealthCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/HealthCommand.java similarity index 81% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/HealthCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/HealthCommand.java index 5e84fa3..2e942a8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/HealthCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/HealthCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "health", description = "Sets the target player's health.", aliases = "hp") public class HealthCommand extends Command { @Override @@ -47,21 +49,6 @@ public class HealthCommand extends Command { executor.notify(String.format("Set %s's health to %s", target.getName(), target.getHealth()), SYSTEM); } } - - @Override - public String getName() { - return "health"; - } - - @Override - public String[] getAliases() { - return new String[] { "hp" }; - } - - @Override - public String getDescription() { - return "Sets a player's health."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ImportCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ImportCommand.java similarity index 83% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ImportCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/ImportCommand.java index 7178f86..204ae6c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ImportCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ImportCommand.java @@ -1,13 +1,15 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.prefab.Prefab; +@CommandInfo(name = "import", description = "Places a prefab at the specified location.") public class ImportCommand extends Command { @Override @@ -46,16 +48,6 @@ public class ImportCommand extends Command { player.notify(String.format("Successfully imported '%s' @ [x: %s, y: %s, width: %s, height: %s]", name, x, y, prefab.getWidth(), prefab.getHeight()), SYSTEM); } - - @Override - public String getName() { - return "import"; - } - - @Override - public String getDescription() { - return "Places a prefab at your or a specified location."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/KickCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/KickCommand.java similarity index 80% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/KickCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/KickCommand.java index 1ba646b..63b94ab 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/KickCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/KickCommand.java @@ -1,14 +1,16 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import java.util.Arrays; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "kick", description = "Kicks a player from the server.") public class KickCommand extends Command { @Override @@ -38,16 +40,6 @@ public class KickCommand extends Command { executor.notify("Kicked player " + player.getName() + " for '" + reason + "'", SYSTEM); } - @Override - public String getName() { - return "kick"; - } - - @Override - public String getDescription() { - return "Kicks a player from the server."; - } - @Override public String getUsage(CommandExecutor executor) { return "/kick [reason]"; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/LevelCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/LevelCommand.java similarity index 81% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/LevelCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/LevelCommand.java index 0e1437a..1616b81 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/LevelCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/LevelCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "level", description = "Sets the level of the target player.", aliases = { "lvl", "lv" }) public class LevelCommand extends Command { @Override @@ -55,21 +57,6 @@ public class LevelCommand extends Command { target.setLevel(level); executor.notify(String.format("Successfully set %s's level to %s.", target.getName(), level), SYSTEM); } - - @Override - public String getName() { - return "level"; - } - - @Override - public String[] getAliases() { - return new String[] { "lvl", "lv" }; - } - - @Override - public String getDescription() { - return "Sets the level of the target player."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/MuteCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/MuteCommand.java similarity index 83% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/MuteCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/MuteCommand.java index 9b0a973..f32b0a1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/MuteCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/MuteCommand.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; @@ -7,11 +7,13 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.util.DateTimeUtils; +@CommandInfo(name = "mute", description = "Mutes a player, preventing them from chatting.", aliases = "silence") public class MuteCommand extends Command { @Override @@ -58,21 +60,6 @@ public class MuteCommand extends Command { target.getName(), endDate.format(DateTimeFormatter.RFC_1123_DATE_TIME), reason), SYSTEM); } - @Override - public String getName() { - return "mute"; - } - - @Override - public String[] getAliases() { - return new String[] { "silence", }; - } - - @Override - public String getDescription() { - return "Mutes a player, preventing them from chatting."; - } - @Override public String getUsage(CommandExecutor executor) { return "/mute [reason]"; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/PlayerIdCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/PlayerIdCommand.java similarity index 78% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/PlayerIdCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/PlayerIdCommand.java index 0b0f8de..158b1f3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/PlayerIdCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/PlayerIdCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "pid", description = "Displays the document id of a player.") public class PlayerIdCommand extends Command { @Override @@ -34,16 +36,6 @@ public class PlayerIdCommand extends Command { executor.notify(target.getDocumentId(), SYSTEM); } - @Override - public String getName() { - return "pid"; - } - - @Override - public String getDescription() { - return "Displays the document id of a player."; - } - @Override public String getUsage(CommandExecutor executor) { return String.format("/pid %s", executor instanceof Player ? "[player]" : ""); diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/PositionCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/PositionCommand.java similarity index 57% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/PositionCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/PositionCommand.java index 1ddede5..e05b716 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/PositionCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/PositionCommand.java @@ -1,11 +1,13 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "position", description = "Displays the coordinates of the block you are standing on.", aliases = { "pos", "feet", "coords", "location" }) public class PositionCommand extends Command { @Override @@ -13,21 +15,6 @@ public class PositionCommand extends Command { Player player = (Player)executor; player.notify(String.format("X: %s Y: %s", (int)player.getX(), (int)player.getY() + 1), SYSTEM); } - - @Override - public String getName() { - return "pos"; - } - - @Override - public String[] getAliases() { - return new String[] { "position", "feet", "coords", "location" }; - } - - @Override - public String getDescription() { - return "Displays the coordinates of the block you are standing on."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/PrefabListCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/PrefabListCommand.java similarity index 76% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/PrefabListCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/PrefabListCommand.java index 50ca8e3..dff4e21 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/PrefabListCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/PrefabListCommand.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; @@ -8,10 +8,12 @@ import java.util.List; import org.apache.commons.lang3.math.NumberUtils; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.prefab.Prefab; +@CommandInfo(name = "prefabs", description = "Displays a list of all prefabs.") public class PrefabListCommand extends Command { @Override @@ -34,21 +36,6 @@ public class PrefabListCommand extends Command { executor.notify(prefab.getName(), SYSTEM); } } - - @Override - public String getName() { - return "prefabs"; - } - - @Override - public String[] getAliases() { - return new String[] { "prefablist" }; - } - - @Override - public String getDescription() { - return "Displays a list of all prefabs."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SeedCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/SeedCommand.java similarity index 79% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SeedCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/SeedCommand.java index 5135e79..e5e40a0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SeedCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/SeedCommand.java @@ -1,13 +1,15 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "seed", description = "Displays the seed of a zone.") public class SeedCommand extends Command { @Override @@ -35,16 +37,6 @@ public class SeedCommand extends Command { executor.notify("Seed: " + target.getSeed(), SYSTEM); } - @Override - public String getName() { - return "seed"; - } - - @Override - public String getDescription() { - return "Displays the seed of a zone."; - } - @Override public String getUsage(CommandExecutor executor) { return String.format("/seed %s", executor instanceof Player ? "[zone]" : ""); diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SettleLiquidsCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/SettleLiquidsCommand.java similarity index 63% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SettleLiquidsCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/SettleLiquidsCommand.java index b81d20a..fa6cf38 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SettleLiquidsCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/SettleLiquidsCommand.java @@ -1,11 +1,13 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "settle", description = "Settles all liquids in all active chunks in the current zone. Warning - can cause lag!") public class SettleLiquidsCommand extends Command { @Override @@ -19,18 +21,8 @@ public class SettleLiquidsCommand extends Command { } @Override - public String getName() { - return "settleliquids"; - } - - @Override - public String[] getAliases() { - return new String[] {"settle"}; - } - - @Override - public String getDescription() { - return "Settles all liquids in all active chunks in the current zone. Warning - can cause lag!"; + public String getUsage(CommandExecutor executor) { + return "/settle"; } @Override diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SkillPointsCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/SkillPointsCommand.java similarity index 81% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SkillPointsCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/SkillPointsCommand.java index f6abefc..22ccfa7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SkillPointsCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/SkillPointsCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "skillpoints", description = "Sets the skill points of the target player.", aliases = "points") public class SkillPointsCommand extends Command { @Override @@ -54,21 +56,6 @@ public class SkillPointsCommand extends Command { target.notify(String.format("Your skill point count has been set to %s.", amount), SYSTEM); executor.notify(String.format("Successfully set %s's skill point count to %s.", target.getName(), amount), SYSTEM); } - - @Override - public String getName() { - return "skillpoints"; - } - - @Override - public String[] getAliases() { - return new String[] { "points" }; - } - - @Override - public String getDescription() { - return "Sets the skill points of the target player."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/commands/admin/StopCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/StopCommand.java new file mode 100644 index 0000000..b168555 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/StopCommand.java @@ -0,0 +1,25 @@ +package brainwine.gameserver.commands.admin; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; + +@CommandInfo(name = "stop", description = "Gracefully shuts down the server.", aliases = { "exit", "close", "shutdown" }) +public class StopCommand extends Command { + + @Override + public void execute(CommandExecutor executor, String[] args) { + GameServer.getInstance().stopGracefully(); // YEET!! + } + + @Override + public String getUsage(CommandExecutor executor) { + return "/stop"; + } + + @Override + public boolean canExecute(CommandExecutor executor) { + return executor.isAdmin(); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/TeleportCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/TeleportCommand.java similarity index 73% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/TeleportCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/TeleportCommand.java index 387d0d6..d09952c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/TeleportCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/TeleportCommand.java @@ -1,11 +1,13 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "teleport", description = "Teleports you to the specified position.", aliases = "tp") public class TeleportCommand extends Command { @Override @@ -35,21 +37,6 @@ public class TeleportCommand extends Command { player.teleport(x, y); } - @Override - public String getName() { - return "teleport"; - } - - @Override - public String[] getAliases() { - return new String[] { "tp" }; - } - - @Override - public String getDescription() { - return "Teleports you to the specified position."; - } - @Override public String getUsage(CommandExecutor executor) { return "/teleport "; diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/TimeCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/TimeCommand.java similarity index 84% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/TimeCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/TimeCommand.java index e61c24e..5790328 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/TimeCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/TimeCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "time", description = "Displays or changes the time in the current zone.") public class TimeCommand extends Command { @Override @@ -45,16 +47,6 @@ public class TimeCommand extends Command { zone.setTime(value); executor.notify(String.format("Time has been set to %s in %s.", value, zone.getName()), SYSTEM); } - - @Override - public String getName() { - return "time"; - } - - @Override - public String getDescription() { - return "Displays or changes the time in the current zone."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnbanCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/UnbanCommand.java similarity index 75% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/UnbanCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/UnbanCommand.java index eecb4ed..934f4a2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnbanCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/UnbanCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "unban", description = "Unbans a player.", aliases = "pardon") public class UnbanCommand extends Command { @Override @@ -31,21 +33,6 @@ public class UnbanCommand extends Command { target.unban(executor instanceof Player ? (Player)executor : null); executor.notify(String.format("Player %s has been unbanned.", target.getName()), SYSTEM); } - - @Override - public String getName() { - return "unban"; - } - - @Override - public String[] getAliases() { - return new String[] { "pardon" }; - } - - @Override - public String getDescription() { - return "Unbans a player."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnmuteCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/UnmuteCommand.java similarity index 79% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/UnmuteCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/UnmuteCommand.java index 185a145..23803c8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnmuteCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/UnmuteCommand.java @@ -1,12 +1,14 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; +@CommandInfo(name = "unmute", description = "Unmutes a player.") public class UnmuteCommand extends Command { @Override @@ -31,16 +33,6 @@ public class UnmuteCommand extends Command { target.unmute(executor instanceof Player ? (Player)executor : null); executor.notify(String.format("Player %s has been unmuted.", target.getName()), SYSTEM); } - - @Override - public String getName() { - return "unmute"; - } - - @Override - public String getDescription() { - return "Unmutes a player."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/WeatherCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/WeatherCommand.java similarity index 82% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/WeatherCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/WeatherCommand.java index e7027c7..4b8ea6c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/WeatherCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/WeatherCommand.java @@ -1,14 +1,16 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.WeatherManager; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "weather", description = "Displays or changes the weather in the current zone.") public class WeatherCommand extends Command { @Override @@ -32,16 +34,6 @@ public class WeatherCommand extends Command { zone.getWeatherManager().createRandomRain(dry); executor.notify(String.format("Weather has been %s in %s.", dry ? "cleared" : "made rainy", zone.getName()), SYSTEM); } - - @Override - public String getName() { - return "weather"; - } - - @Override - public String getDescription() { - return "Displays or changes the weather in the current zone."; - } @Override public String getUsage(CommandExecutor executor) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ZoneIdCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ZoneIdCommand.java similarity index 79% rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ZoneIdCommand.java rename to gameserver/src/main/java/brainwine/gameserver/commands/admin/ZoneIdCommand.java index 11bf20a..c674a8a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ZoneIdCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/ZoneIdCommand.java @@ -1,13 +1,15 @@ -package brainwine.gameserver.command.commands; +package brainwine.gameserver.commands.admin; import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.GameServer; -import brainwine.gameserver.command.Command; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.annotations.CommandInfo; +import brainwine.gameserver.commands.Command; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.zone.Zone; +@CommandInfo(name = "zid", description = "Displays the document id of a zone.") public class ZoneIdCommand extends Command { @Override @@ -35,16 +37,6 @@ public class ZoneIdCommand extends Command { executor.notify(target.getDocumentId(), SYSTEM); } - @Override - public String getName() { - return "zid"; - } - - @Override - public String getDescription() { - return "Displays the document id of a zone."; - } - @Override public String getUsage(CommandExecutor executor) { return String.format("/zid %s", executor instanceof Player ? "[zone]" : ""); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 72e44ae..97536a3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -22,7 +22,7 @@ import brainwine.gameserver.Timer; import brainwine.gameserver.achievements.Achievement; import brainwine.gameserver.achievements.AchievementManager; import brainwine.gameserver.achievements.JourneymanAchievement; -import brainwine.gameserver.command.CommandExecutor; +import brainwine.gameserver.commands.CommandExecutor; import brainwine.gameserver.dialog.Dialog; import brainwine.gameserver.dialog.DialogListItem; import brainwine.gameserver.dialog.DialogSection; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java index 267244b..230f932 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java @@ -2,7 +2,7 @@ package brainwine.gameserver.server.requests; import brainwine.gameserver.annotations.OptionalField; import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.commands.CommandManager; import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.server.PlayerRequest; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java index 4ff46b9..022e223 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.requests; import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.commands.CommandManager; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.server.PlayerRequest; diff --git a/src/main/java/brainwine/ServerThread.java b/src/main/java/brainwine/ServerThread.java index 8ca022f..4b60a14 100644 --- a/src/main/java/brainwine/ServerThread.java +++ b/src/main/java/brainwine/ServerThread.java @@ -8,7 +8,7 @@ import org.apache.logging.log4j.Logger; import brainwine.api.Api; import brainwine.gameserver.GameServer; import brainwine.gameserver.TickLoop; -import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.commands.CommandManager; public class ServerThread extends Thread { From ab1147ba89b91c4855380263a5b0a2d12ec1fd2d Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Fri, 26 Apr 2024 23:10:25 +0200 Subject: [PATCH 45/47] Undo bfeb93be809f1b74988d0d6615f1b5c8c01e5c73 --- .../gameserver/server/requests/ZoneChangeRequest.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java index c87919f..ccedb62 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java @@ -1,13 +1,9 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.GameConfiguration; import brainwine.gameserver.GameServer; import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.server.PlayerRequest; -import brainwine.gameserver.util.MapHelper; -import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.Zone; @RequestInfo(id = 24) @@ -28,6 +24,7 @@ public class ZoneChangeRequest extends PlayerRequest { } // Check survival requirement unless player has god mode enabled + /* if(!player.isGodMode()) { Biome biome = zone.getBiome(); int survival = MapHelper.getInt(GameConfiguration.getBaseConfig(), String.format("biomes.%s.survival_requirement", biome.getId())); @@ -36,7 +33,7 @@ public class ZoneChangeRequest extends PlayerRequest { player.notify(String.format("Your survival skill needs to be at least level %s to enter %s worlds.", survival, biome.getId())); return; } - } + }*/ player.changeZone(zone); } From 261ec4e314c48c22e938936885af0beb16d5bbb5 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:21:52 +0200 Subject: [PATCH 46/47] Minor refactors --- .../commands/admin/EntityCommand.java | 16 ++++------------ .../item/interactions/SpawnInteraction.java | 18 +++--------------- .../item/interactions/SwitchInteraction.java | 13 ++++--------- .../server/requests/BlockMineRequest.java | 7 +------ .../server/requests/BlockPlaceRequest.java | 7 +------ .../gameserver/zone/EntityManager.java | 12 ++++-------- 6 files changed, 17 insertions(+), 56 deletions(-) diff --git a/gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java b/gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java index 408f782..701e240 100644 --- a/gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java +++ b/gameserver/src/main/java/brainwine/gameserver/commands/admin/EntityCommand.java @@ -5,12 +5,8 @@ import static brainwine.gameserver.entity.player.NotificationType.SYSTEM; import brainwine.gameserver.annotations.CommandInfo; import brainwine.gameserver.commands.Command; import brainwine.gameserver.commands.CommandExecutor; -import brainwine.gameserver.entity.EntityConfig; -import brainwine.gameserver.entity.EntityRegistry; -import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.entity.player.NotificationType; import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.zone.Zone; @CommandInfo(name = "entity", description = "Spawns an entity at your current location.") public class EntityCommand extends Command { @@ -23,21 +19,17 @@ public class EntityCommand extends Command { } Player player = (Player)executor; - String name = args[0]; - EntityConfig config = EntityRegistry.getEntityConfig(name); + String type = args[0]; - if(config == null) { - executor.notify(String.format("Entity with name '%s' does not exist.", name), NotificationType.SYSTEM); + if(player.getZone().spawnEntity(type, player.getBlockX(), player.getBlockY(), true) == null) { + executor.notify(String.format("Entity type '%s' does not exist.", type), NotificationType.SYSTEM); return; } - - Zone zone = player.getZone(); - zone.spawnEntity(new Npc(zone, config), (int)player.getX(), (int)player.getY(), true); } @Override public String getUsage(CommandExecutor executor) { - return "/entity "; + return "/entity "; } @Override diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java index e7633c7..52e7ae5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java @@ -1,9 +1,6 @@ package brainwine.gameserver.item.interactions; import brainwine.gameserver.entity.Entity; -import brainwine.gameserver.entity.EntityConfig; -import brainwine.gameserver.entity.EntityRegistry; -import brainwine.gameserver.entity.npc.Npc; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.zone.MetaBlock; @@ -22,18 +19,9 @@ public class SpawnInteraction implements ItemInteraction { return; } - EntityConfig entityConfig = EntityRegistry.getEntityConfig(item.getEntitySpawns().next()); - - // Do nothing if type is invalid - if(entityConfig == null) { - return; + // Try to spawn the entity and update block mod + if(zone.spawnEntity(item.getEntitySpawns().next(), x, y) != null) { + zone.updateBlock(x, y, layer, item, 1); } - - // Spawn the entity - Npc npc = new Npc(zone, entityConfig); - zone.spawnEntity(npc, x, y); - - // Update block mod - zone.updateBlock(x, y, layer, item, 1); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java index a757504..15c8229 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -125,21 +125,16 @@ public class SwitchInteraction implements ItemInteraction { return; } - // Determine entity type + // Try to spawn entity String entityType = MapHelper.getString((Map)config, metaBlock.getStringProperty("e")); - EntityConfig entityConfig = EntityRegistry.getEntityConfig(entityType); + Npc npc = zone.spawnEntity(entityType, metaBlock.getX(), metaBlock.getY(), true); - // Do nothing if entity config doesn't exist - if(entityConfig == null) { + // Do nothing if entity failed to spawn + if(npc == null) { return; } - // Create & spawn the entity - Npc npc = new Npc(zone, entityConfig); npc.setArtificial(true); - zone.spawnEntity(npc, metaBlock.getX(), metaBlock.getY(), true); - - // Track entity id in spawner metadata metaBlock.setProperty("eid", npc.getId()); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java index dd167a7..e5888bc 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java @@ -166,12 +166,7 @@ public class BlockMineRequest extends PlayerRequest { // Check for entity spawns if(item.hasEntitySpawns() && block.getMod(layer) == 0 && !item.hasTimer() && !item.hasUse(ItemUseType.SPAWN)) { - EntityConfig type = EntityRegistry.getEntityConfig(item.getEntitySpawns().next()); - - if(type != null) { - Npc npc = new Npc(zone, type); - zone.spawnEntity(npc, x, y); - } + zone.spawnEntity(item.getEntitySpawns().next(), x, y); } Item inventoryItem = item.getMod() == ModType.DECAY && block.getMod(layer) > 0 ? item.getDecayInventoryItem() : item.getInventoryItem(); diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java index aec3f4f..3abbcc3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java @@ -258,12 +258,7 @@ public class BlockPlaceRequest extends PlayerRequest { // Spawn a bunch of entities for(int i = 0; i < value; i++) { - EntityConfig entityType = EntityRegistry.getEntityConfig(item.getEntitySpawns().next()); - - if(entityType != null) { - Npc npc = new Npc(zone, entityType); - zone.spawnEntity(npc, x, y); - } + zone.spawnEntity(item.getEntitySpawns().next(), x, y); } }; break; diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java index cfddcec..a39a046 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -209,12 +209,10 @@ public class EntityManager { List guardians = MapHelper.getList(metaBlock.getMetadata(), "!", Collections.emptyList()); for(String guardian : guardians) { - EntityConfig config = EntityRegistry.getEntityConfig(guardian); + Npc entity = spawnEntity(guardian, x, y); - if(config != null) { - Npc entity = new Npc(zone, config); + if(entity != null) { entity.setGuardBlock(x, y); - spawnEntity(entity, x, y); } } } @@ -230,10 +228,9 @@ public class EntityManager { // Check for mounted entity (turrets & geysers) if(item.isEntity()) { - EntityConfig config = EntityRegistry.getEntityConfig(item.getId()); + Npc entity = spawnEntity(item.getId(), x, y); - if(config != null) { - Npc entity = new Npc(zone, config); + if(entity != null) { MetaBlock metaBlock = zone.getMetaBlock(x, y); // Set owner entity if it has one @@ -242,7 +239,6 @@ public class EntityManager { } entity.setMountBlock(x, y); - spawnEntity(entity, x, y); mountedNpcs.put(index, entity); } } From 1eda98539c6f65565d00aacbd5b186a836e47f1d Mon Sep 17 00:00:00 2001 From: Kuroppoi Date: Wed, 1 May 2024 16:16:17 +0200 Subject: [PATCH 47/47] Custom build logic (#49) --- .gitattributes | 2 + .github/workflows/build.yml | 2 +- .gitignore | 3 +- README.md | 72 +++++----- api/build.gradle | 6 +- build-logic/build.gradle | 28 ++++ .../java/brainwine/bootstrap/Bootstrap.java | 84 ++++++++++++ .../java/brainwine/bootstrap/Constants.java | 12 ++ .../brainwine/build/DistributionPlugin.java | 14 ++ .../brainwine/build/DistributionTask.java | 128 ++++++++++++++++++ build.gradle | 28 ++-- gameserver/build.gradle | 6 +- settings.gradle | 4 + shared/build.gradle | 4 - .../brainwine/{Bootstrap.java => Main.java} | 6 +- src/main/java/brainwine/ServerThread.java | 10 +- src/main/java/brainwine/gui/MainView.java | 8 +- src/main/java/brainwine/gui/ServerPanel.java | 20 +-- 18 files changed, 341 insertions(+), 96 deletions(-) create mode 100644 build-logic/build.gradle create mode 100644 build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java create mode 100644 build-logic/src/boot/java/brainwine/bootstrap/Constants.java create mode 100644 build-logic/src/main/java/brainwine/build/DistributionPlugin.java create mode 100644 build-logic/src/main/java/brainwine/build/DistributionTask.java rename src/main/java/brainwine/{Bootstrap.java => Main.java} (98%) diff --git a/.gitattributes b/.gitattributes index f7fa1f5..5064e20 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ +/gradlew text eol=lf + # These are explicitly windows files and should use crlf *.bat text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3c24f1..774dc16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,5 +32,5 @@ jobs: uses: actions/upload-artifact@v3.1.1 with: name: brainwine - path: build/libs/brainwine.jar + path: build/dist/brainwine.jar retention-days: 7 diff --git a/.gitignore b/.gitignore index 3e0742f..8017793 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Gradle .gradle build +!**/src/**/build # Eclipse *.launch @@ -11,4 +12,4 @@ build bin # Misc -run \ No newline at end of file +run diff --git a/README.md b/README.md index b57da34..7250b78 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,49 @@ -# Brainwine -[![build](https://github.com/kuroppoi/brainwine/actions/workflows/build.yml/badge.svg)](https://github.com/kuroppoi/brainwine/actions) +

Brainwine

+

+ build + release +

-Brainwine is a Deepworld private server written in Java, made with user-friendliness and portability in mind. -Due to the time it will take for this project to be complete (and my inconsistent working on it), brainwine has been prematurely open-sourced -and is free for all to use.\ -Keep in mind, though, that this server is not finished yet. Expect to encounter bad code, bugs and missing features!\ -Brainwine is currently compatible with the following versions of Deepworld: -- Steam: `v3.13.1` +Brainwine is a Deepworld private server written in Java, designed to be portable and easy to use.\ +It's still a work in progress, so keep in mind that it's not yet feature-complete. (A to-do list can be found [here](https://github.com/kuroppoi/brainwine/projects/1).)\ +Brainwine currently supports the following versions of Deepworld: + +- Windows: `v3.13.1` - iOS: `v2.11.0.1` - MacOS: `v2.11.1` -## Features -A list of all planned, in-progress and finished features can be found [here.](https://github.com/kuroppoi/brainwine/projects/1) +## Quick Local Setup -## Setup +- Install [Java 8](https://adoptium.net/temurin/releases/?package=jdk&version=8). +- Download the [latest Brainwine release](https://github.com/kuroppoi/brainwine/releases/latest). +- Run Brainwine, go to the server tab and start the server. +- Go to the game tab and start the game. + - If this isn't available for you, download a [patching kit](https://github.com/kuroppoi/brainwine/releases/tag/patching-kits-1.0) for your platform and follow the instructions there. +- Register a new account and play the game. -### Setting up the client - -Before you can connect to a server, a few modifications need to be made to the Deepworld game client.\ -The exact process of this differs per platform.\ -You may download an installation package for your desired platform [here.](https://github.com/kuroppoi/brainwine/releases/tag/patching-kits-1.0) - -### Setting up the server +## Building #### Prerequisites -- Java 8 or newer +- Java 8 Development Kit -You can download the latest release [here.](https://github.com/kuroppoi/brainwine/releases/latest)\ -Alternatively, if you wish to build from source, clone this repository with the `--recurse-submodules` flag\ -and run `gradlew dist` in the root directory of the repository.\ -After the build has finished, the output jar will be located in `build/libs`.\ -You may then start the server through the gui, or start it directly by running the jar with the `disablegui` flag. +```sh +git clone --recurse-submodules https://github.com/kuroppoi/brainwine.git +cd brainwine +./gradlew dist +``` -#### Configurations +The output will be located in the `/build/dist` directory. -On first-time startup, configuration files will be generated which you may modify however you like: -- `api.json` Configuration file for news & API connectivity information. -- `loottables.json` Configuration file for which loot may be obtained from containers. -- `spawning.json` Configuration file for entity spawns per biome. -- `generators` Folder containing configuration files for zone generators. +## Usage -## Contributions +Execute `brainwine.jar` to start the program. Navigate to the server tab and press the button to start the server.\ +It is also possible to start the server immediately with no user interface: -Disagree with how I did something? Found a potential error? See some room for improvement? Or just want to add a feature? -Glad to hear it! Feel free to make a pull request anytime. Just make sure you follow the code style! -And, apologies in advance for the lack of documentation. Haven't gotten around to do it yet. Sorry! +```sh +# This behavior is the default on platforms that do not support Java's Desktop API. +java -jar brainwine.jar disablegui +``` -## Issues - -Found a bug? Before posting an issue, make sure your build is up-to-date and your issue has not already been posted before. -Provide a detailed explanation of the issue, and how to reproduce it. I'll get to it ASAP! +To connect to a local or remote server, download a [patching kit](https://github.com/kuroppoi/brainwine/releases/tag/patching-kits-1.0) for your desired platform.\ +Alternatively, Windows users may use the program's user interface to configure the host settings and start the game. diff --git a/api/build.gradle b/api/build.gradle index f441ade..84dcbf4 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -8,9 +8,5 @@ repositories { dependencies { implementation 'io.javalin:javalin:4.6.8' - implementation project(':shared') -} - -jar { - archiveBaseName = 'brainwine-api' + implementation project(':brainwine-shared') } diff --git a/build-logic/build.gradle b/build-logic/build.gradle new file mode 100644 index 0000000..513b583 --- /dev/null +++ b/build-logic/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java-gradle-plugin' +} + +sourceSets { + boot { + + } + main { + compileClasspath += sourceSets.boot.output + runtimeClasspath += sourceSets.boot.output + } +} + +gradlePlugin { + plugins { + distributionPlugin { + id = "brainwine.distribution" + implementationClass = "brainwine.build.DistributionPlugin" + } + } +} + +jar { + from sourceSets.boot.output +} + +version = '1.0.0-SNAPSHOT' diff --git a/build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java b/build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java new file mode 100644 index 0000000..e2553f4 --- /dev/null +++ b/build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java @@ -0,0 +1,84 @@ +package brainwine.bootstrap; + +import static brainwine.bootstrap.Constants.BOOT_CLASS_KEY; +import static brainwine.bootstrap.Constants.CLASS_PATH_KEY; +import static brainwine.bootstrap.Constants.LIBRARY_PATH; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public class Bootstrap { + + public static void main(String[] args) { + new Bootstrap().run(args); + } + + private void run(String[] args) { + Attributes attributes = null; + + try { + Enumeration resources = getClass().getClassLoader().getResources(JarFile.MANIFEST_NAME); + + while(resources.hasMoreElements()) { + try(InputStream inputStream = resources.nextElement().openStream()) { + Manifest manifest = new Manifest(inputStream); + if(getClass().getName().equals(manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS))) { + attributes = manifest.getMainAttributes(); + break; + } + } + } + } catch(IOException e) { + System.err.println("Could not load manifest file"); + e.printStackTrace(); + System.exit(-1); + } + + String[] libraryNames = attributes.getValue(CLASS_PATH_KEY).split(";"); + URL[] libraryUrls = new URL[libraryNames.length]; + + try { + for(int i = 0; i < libraryNames.length; i++) { + String libraryName = libraryNames[i]; + + try(InputStream inputStream = getClass().getResourceAsStream(String.format("/%s/%s", LIBRARY_PATH, libraryName))) { + File outputFile = new File("libraries", libraryName); + libraryUrls[i] = outputFile.toURI().toURL(); + + if(outputFile.exists()) { + continue; + } + + outputFile.getParentFile().mkdirs(); + Files.copy(inputStream, outputFile.toPath()); + } + } + } catch(Exception e) { + System.err.println("Could not extract library JARs"); + e.printStackTrace(); + System.exit(-1); + } + + URLClassLoader classLoader = new URLClassLoader(libraryUrls, getClass().getClassLoader().getParent()); + Thread.currentThread().setContextClassLoader(classLoader); + + try { + Class mainClass = Class.forName(attributes.getValue(BOOT_CLASS_KEY), true, classLoader); + Method method = mainClass.getMethod("main", String[].class); + method.invoke(null, (Object)args); + } catch(ReflectiveOperationException e) { + System.err.println("Could not invoke entry point"); + e.printStackTrace(); + System.exit(-1); + } + } +} diff --git a/build-logic/src/boot/java/brainwine/bootstrap/Constants.java b/build-logic/src/boot/java/brainwine/bootstrap/Constants.java new file mode 100644 index 0000000..560c195 --- /dev/null +++ b/build-logic/src/boot/java/brainwine/bootstrap/Constants.java @@ -0,0 +1,12 @@ +package brainwine.bootstrap; + +import java.util.jar.Attributes; + +public class Constants { + + public static final Attributes.Name BOOT_CLASS_KEY = new Attributes.Name("Dist-Boot-Class"); + public static final Attributes.Name CLASS_PATH_KEY = new Attributes.Name("Dist-Class-Path"); + public static final String LICENSE_PATH = "META-INF/LICENSE"; + public static final String LIBRARY_PATH = "META-INF/libraries"; + public static final Class MAIN_CLASS = Bootstrap.class; +} diff --git a/build-logic/src/main/java/brainwine/build/DistributionPlugin.java b/build-logic/src/main/java/brainwine/build/DistributionPlugin.java new file mode 100644 index 0000000..c1580d9 --- /dev/null +++ b/build-logic/src/main/java/brainwine/build/DistributionPlugin.java @@ -0,0 +1,14 @@ +package brainwine.build; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPlugin; + +public class DistributionPlugin implements Plugin { + + @Override + public void apply(Project project) { + project.getPlugins().apply(JavaPlugin.class); + project.getTasks().register("dist", DistributionTask.class, task -> task.dependsOn("build")); + } +} diff --git a/build-logic/src/main/java/brainwine/build/DistributionTask.java b/build-logic/src/main/java/brainwine/build/DistributionTask.java new file mode 100644 index 0000000..2851326 --- /dev/null +++ b/build-logic/src/main/java/brainwine/build/DistributionTask.java @@ -0,0 +1,128 @@ +package brainwine.build; + +import static brainwine.bootstrap.Constants.BOOT_CLASS_KEY; +import static brainwine.bootstrap.Constants.CLASS_PATH_KEY; +import static brainwine.bootstrap.Constants.LIBRARY_PATH; +import static brainwine.bootstrap.Constants.LICENSE_PATH; +import static brainwine.bootstrap.Constants.MAIN_CLASS; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.FileTree; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.initialization.IncludedBuild; +import org.gradle.api.invocation.Gradle; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.bundling.Jar; + +public abstract class DistributionTask extends DefaultTask { + + private final FileTree bootCodeTree; + + @Input + public abstract Property getMainClass(); + + @Input + @Optional + public abstract Property getArchiveFileName(); + + @Input + @Optional + public abstract RegularFileProperty getLicenseFile(); + + @Inject + public DistributionTask(Gradle gradle) { + IncludedBuild build = gradle.getIncludedBuilds().stream().filter(x -> x.getName().equals("build-logic")).findFirst().get(); + bootCodeTree = getProject().fileTree(new File(build.getProjectDir(), "build/classes/java/boot")); + } + + @TaskAction + public void createDistributionArchive() throws IOException { + Configuration config = getProject().getConfigurations().getByName("runtimeClasspath"); + Jar jarTask = (Jar)getProject().getTasks().getByName("jar"); + String archiveFileName = getArchiveFileName().getOrElse(jarTask.getArchiveFileName().get()); + File outputDirectory = new File(getProject().getBuildDir(), "dist"); + outputDirectory.mkdirs(); + File outputFile = new File(outputDirectory, archiveFileName); + + // Fetch libraries + List classpath = new ArrayList<>(); + config.getResolvedConfiguration().getResolvedArtifacts().forEach(artifact -> classpath.add(artifact.getFile())); + jarTask.getOutputs().getFiles().forEach(classpath::add); + classpath.sort((a, b) -> a.getName().compareTo(b.getName())); // Guarantee file order + + // Create jar manifest + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, MAIN_CLASS.getName()); + manifest.getMainAttributes().put(BOOT_CLASS_KEY, getMainClass().get()); + manifest.getMainAttributes().put(CLASS_PATH_KEY, String.join(";", classpath.stream().map(File::getName).collect(Collectors.toList()))); + manifest.getMainAttributes().putValue("Multi-Release", "true"); + + // Create jar file + try(JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(outputFile))) { + // Add manifest + addJarManifest(outputStream, manifest); + + // Add libraries + for(File file : classpath) { + addFileToJar(outputStream, file, String.format("%s/%s", LIBRARY_PATH, file.getName())); + } + + // Add boot code + bootCodeTree.visit(details -> { + if(!details.isDirectory()) { + try { + addFileToJar(outputStream, details.getFile(), details.getPath()); + } catch(IOException e) { + throw new GradleException(e.getMessage(), e); + } + } + }); + + // Add license + RegularFileProperty licenseFile = getLicenseFile(); + + if(licenseFile.isPresent()) { + addFileToJar(outputStream, licenseFile.get().getAsFile(), LICENSE_PATH); + } + } + } + + private void addJarManifest(JarOutputStream outputStream, Manifest manifest) throws IOException { + JarEntry entry = new JarEntry(JarFile.MANIFEST_NAME); + entry.setTime(0); + outputStream.putNextEntry(entry); + manifest.write(outputStream); + outputStream.closeEntry(); + } + + private void addFileToJar(JarOutputStream outputStream, File file, String targetPath) throws IOException { + byte[] bytes = Files.readAllBytes(file.toPath()); + JarEntry entry = new JarEntry(targetPath); + entry.setTime(0); + entry.setSize(bytes.length); + outputStream.putNextEntry(entry); + outputStream.write(bytes); + outputStream.closeEntry(); + } +} diff --git a/build.gradle b/build.gradle index fcf038c..c47d584 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ plugins { - id 'java' + id 'brainwine.distribution' } ext { - mainClass = 'brainwine.Bootstrap' + mainClass = 'brainwine.Main' workingDirectory = 'run' } @@ -15,26 +15,14 @@ dependencies { implementation 'com.formdev:flatlaf-intellij-themes:3.0' implementation 'com.formdev:flatlaf-extras:3.0' implementation 'com.formdev:flatlaf:3.0' - implementation project(':api') - implementation project(':gameserver') - implementation project(':shared') + implementation project(':brainwine-api') + implementation project(':brainwine-gameserver') + implementation project(':brainwine-shared') } -task dist(type: Jar) { - manifest { - attributes 'Multi-Release': 'true', - 'Main-Class': project.ext.mainClass - } - - from { - configurations.runtimeClasspath.collect { - it.isDirectory() ? it : zipTree(it) - } - } - - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - dependsOn configurations.runtimeClasspath - with jar +dist { + mainClass = project.ext.mainClass + licenseFile = file("${project.rootDir}/LICENSE.md") } task run(type: JavaExec) { diff --git a/gameserver/build.gradle b/gameserver/build.gradle index 847e487..b707887 100644 --- a/gameserver/build.gradle +++ b/gameserver/build.gradle @@ -24,11 +24,7 @@ dependencies { implementation 'org.reflections:reflections:0.10.2' implementation 'io.netty:netty-all:4.1.79.Final' implementation 'org.mindrot:jbcrypt:0.4' - implementation project(':shared') -} - -jar { - archiveBaseName = 'brainwine-gameserver' + implementation project(':brainwine-shared') } processResources.includeEmptyDirs = false diff --git a/settings.gradle b/settings.gradle index 38583d4..566f3e2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,6 @@ rootProject.name = 'brainwine' +includeBuild 'build-logic' include('api', 'gameserver', 'shared') +project(":api").name = 'brainwine-api' +project(':gameserver').name = 'brainwine-gameserver' +project(':shared').name = 'brainwine-shared' diff --git a/shared/build.gradle b/shared/build.gradle index 451764c..0782595 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -15,7 +15,3 @@ dependencies { api 'commons-validator:commons-validator:1.7' api 'org.apache.commons:commons-text:1.9' } - -jar { - archiveBaseName = 'brainwine-shared' -} diff --git a/src/main/java/brainwine/Bootstrap.java b/src/main/java/brainwine/Main.java similarity index 98% rename from src/main/java/brainwine/Bootstrap.java rename to src/main/java/brainwine/Main.java index bb1969f..9f09f3f 100644 --- a/src/main/java/brainwine/Bootstrap.java +++ b/src/main/java/brainwine/Main.java @@ -21,7 +21,7 @@ import brainwine.gui.MainView; import brainwine.gui.theme.ThemeManager; import brainwine.util.SwingUtils; -public class Bootstrap { +public class Main { private static Logger logger = LogManager.getLogger(); private static boolean disableGui = false; @@ -41,10 +41,10 @@ public class Bootstrap { } } - new Bootstrap(); + new Main(); } - public Bootstrap() { + public Main() { // Create gui or directly start server if gui is disabled or not supported if(!disableGui && (Desktop.isDesktopSupported() || forceGui)) { try { diff --git a/src/main/java/brainwine/ServerThread.java b/src/main/java/brainwine/ServerThread.java index 4b60a14..47994ed 100644 --- a/src/main/java/brainwine/ServerThread.java +++ b/src/main/java/brainwine/ServerThread.java @@ -13,14 +13,14 @@ import brainwine.gameserver.commands.CommandManager; public class ServerThread extends Thread { private static Logger logger = LogManager.getLogger(); - private final Bootstrap bootstrap; + private final Main main; private GameServer gameServer; private Api api; private boolean running; - public ServerThread(Bootstrap bootstrap) { + public ServerThread(Main main) { super("server"); - this.bootstrap = bootstrap; + this.main = main; } @Override @@ -36,7 +36,7 @@ public class ServerThread extends Thread { logger.info(SERVER_MARKER, "Server has started"); running = true; - bootstrap.onServerStarted(); + main.onServerStarted(); while(!gameServer.shouldStop()) { tickLoop.update(); @@ -80,7 +80,7 @@ public class ServerThread extends Thread { logger.error(SERVER_MARKER, "An unexpected exception occured whilst shutting down", e); } finally { running = false; - bootstrap.onServerStopped(); + main.onServerStopped(); } } diff --git a/src/main/java/brainwine/gui/MainView.java b/src/main/java/brainwine/gui/MainView.java index 1af4c3d..13ac904 100644 --- a/src/main/java/brainwine/gui/MainView.java +++ b/src/main/java/brainwine/gui/MainView.java @@ -25,7 +25,7 @@ import com.formdev.flatlaf.extras.FlatAnimatedLafChange; import com.formdev.flatlaf.extras.components.FlatTabbedPane; import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabAlignment; -import brainwine.Bootstrap; +import brainwine.Main; import brainwine.util.DesktopUtils; import brainwine.util.OperatingSystem; import brainwine.util.ProcessResult; @@ -43,7 +43,7 @@ public class MainView { private final ServerPanel serverPanel; private final SettingsPanel settingsPanel; - public MainView(Bootstrap bootstrap) { + public MainView(Main main) { logger.info(GUI_MARKER, "Creating main view ..."); // Panel @@ -59,7 +59,7 @@ public class MainView { tabbedPane.addTab("Play Game", UIManager.getIcon("Brainwine.playIcon"), new GamePanel(this)); } - tabbedPane.addTab("Server", UIManager.getIcon("Brainwine.serverIcon"), serverPanel = new ServerPanel(bootstrap)); + tabbedPane.addTab("Server", UIManager.getIcon("Brainwine.serverIcon"), serverPanel = new ServerPanel(main)); tabbedPane.addTab("Settings", UIManager.getIcon("Brainwine.settingsIcon"), settingsPanel = new SettingsPanel(this)); panel.add(tabbedPane); @@ -79,7 +79,7 @@ public class MainView { frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent event) { - bootstrap.closeApplication(); + main.closeApplication(); } }); frame.setJMenuBar(menuBar); diff --git a/src/main/java/brainwine/gui/ServerPanel.java b/src/main/java/brainwine/gui/ServerPanel.java index 50a48df..efc1e7d 100644 --- a/src/main/java/brainwine/gui/ServerPanel.java +++ b/src/main/java/brainwine/gui/ServerPanel.java @@ -27,20 +27,20 @@ import org.apache.logging.log4j.Level; import com.formdev.flatlaf.extras.components.FlatScrollPane; import com.formdev.flatlaf.extras.components.FlatTextField; -import brainwine.Bootstrap; +import brainwine.Main; import brainwine.ListenableAppender; import brainwine.gui.event.AutoScrollAdjustmentListener; @SuppressWarnings("serial") public class ServerPanel extends JPanel { - private final Bootstrap bootstrap; + private final Main main; private final JTextPane consoleOutput; private final FlatTextField consoleInput; private final JButton serverButton; - public ServerPanel(Bootstrap bootstrap) { - this.bootstrap = bootstrap; + public ServerPanel(Main main) { + this.main = main; setLayout(new BorderLayout()); setBorder(BorderFactory.createEmptyBorder(0, 3, 3, 3)); @@ -114,32 +114,32 @@ public class ServerPanel extends JPanel { if(!commandLine.isEmpty()) { appendConsoleOutput(String.format("> %s\n", commandLine), Color.GRAY); - bootstrap.executeCommand(commandLine); + main.executeCommand(commandLine); consoleInput.setText(null); } } private void toggleServer() { - if(bootstrap.isServerRunning()) { + if(main.isServerRunning()) { serverButton.setEnabled(false); consoleInput.setEditable(false); consoleInput.setText(null); - bootstrap.stopServer(); + main.stopServer(); } else { serverButton.setEnabled(false); consoleInput.setEditable(true); consoleOutput.setText(null); - bootstrap.startServer(); + main.startServer(); } } public void enableServerButton() { - if(!bootstrap.isServerRunning()) { + if(!main.isServerRunning()) { consoleInput.setEditable(false); consoleInput.setText(null); } - serverButton.setText(bootstrap.isServerRunning() ? "Stop Server" : "Start Server"); + serverButton.setText(main.isServerRunning() ? "Stop Server" : "Start Server"); serverButton.setEnabled(true); } }