Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better SceneTags #2361

Merged
merged 11 commits into from
Sep 16, 2023
101 changes: 101 additions & 0 deletions src/main/java/emu/grasscutter/command/commands/SetSceneTagCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package emu.grasscutter.command.commands;

import emu.grasscutter.command.*;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.scene.SceneTagData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;

import java.util.*;

@Command(label = "setSceneTag", aliases = { "tag" }, usage = {
"<add|remove|unlockall> <sceneTagId>" }, permission = "player.setscenetag", permissionTargeted = "player.setscenetag.others")
public final class SetSceneTagCommand implements CommandHandler {
private final Int2ObjectMap<SceneTagData> sceneTagData = GameData.getSceneTagDataMap();

@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.size() == 0) {
sendUsageMessage(sender);
return;
}

String actionStr = args.get(0).toLowerCase();
int value = -1;
NotThorny marked this conversation as resolved.
Show resolved Hide resolved

if (args.size() > 1) {
try {
value = Integer.parseInt(args.get(1));
} catch (NumberFormatException ignored) {
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
return;
}
} else {
if (actionStr.equals("unlockall")) {
unlockAllSceneTags(targetPlayer);
return;
} else {
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
return;
}
}

final int userVal = value;
NotThorny marked this conversation as resolved.
Show resolved Hide resolved

var sceneData = sceneTagData.values().stream().filter(sceneTag -> sceneTag.getId() == userVal).findFirst();
if (sceneData == null) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id");
return;
}
int scene = sceneData.get().getSceneId();

switch (actionStr) {
case "add", "set" -> addSceneTag(targetPlayer, scene, value);
case "remove", "del" -> removeSceneTag(targetPlayer, scene, value);
default -> CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
}

}

private void addSceneTag(Player targetPlayer, int scene, int value) {
// Ensure key exists
if (targetPlayer.getSceneTags().get(scene) == null) {
targetPlayer.getSceneTags().put(scene, new HashSet<>());
}
targetPlayer.getSceneTags().get(scene).add(value);
setSceneTags(targetPlayer);
}

private void removeSceneTag(Player targetPlayer, int scene, int value) {
targetPlayer.getSceneTags().get(scene).remove(value);
setSceneTags(targetPlayer);
}

private void unlockAllSceneTags(Player targetPlayer) {
var allData = sceneTagData.values();

// Add all SceneTags
allData.stream().toList().forEach(sceneTag -> {
if (targetPlayer.getSceneTags().get(sceneTag.getSceneId()) == null) {
targetPlayer.getSceneTags().put(sceneTag.getSceneId(), new HashSet<>());
}
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
});

// Remove default SceneTags, as most are "before" or "locked" states
allData.stream().filter(sceneTag -> sceneTag.isDefaultValid())
// Only remove for big world as some other scenes only have defaults
.filter(sceneTag -> sceneTag.getSceneId() == 3)
.forEach(sceneTag -> {
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).remove(sceneTag.getId());
});

setSceneTags(targetPlayer);
NotThorny marked this conversation as resolved.
Show resolved Hide resolved
}

private void setSceneTags(Player targetPlayer) {
targetPlayer.sendPacket(new PacketPlayerWorldSceneInfoListNotify(targetPlayer));
}

}
21 changes: 21 additions & 0 deletions src/main/java/emu/grasscutter/game/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public class Player implements PlayerHook, FieldFetch {
@Getter private Map<Integer, ActiveCookCompoundData> activeCookCompounds;
@Getter private Map<Integer, Integer> questGlobalVariables;
@Getter private Map<Integer, Integer> openStates;
@Getter private Map<Integer, Set<Integer>> sceneTags;
@Getter @Setter private Map<Integer, Set<Integer>> unlockedSceneAreas;
@Getter @Setter private Map<Integer, Set<Integer>> unlockedScenePoints;
@Getter @Setter private List<Integer> chatEmojiIdList;
Expand Down Expand Up @@ -244,6 +245,7 @@ public Player() {
this.unlockedRecipies = new HashMap<>();
this.questGlobalVariables = new HashMap<>();
this.openStates = new HashMap<>();
this.sceneTags = new HashMap<>();
this.unlockedSceneAreas = new HashMap<>();
this.unlockedScenePoints = new HashMap<>();
this.chatEmojiIdList = new ArrayList<>();
Expand Down Expand Up @@ -295,6 +297,7 @@ public Player(GameSession session) {
this.codex = new PlayerCodex(this);

this.applyProperties();
this.applyStartingSceneTags();
this.getFlyCloakList().add(140001);
this.getNameCardList().add(210001);

Expand Down Expand Up @@ -587,6 +590,20 @@ private void applyProperties() {
this.getProperty(PlayerProperty.PROP_DIVE_MAX_STAMINA));
}

/**
* Applies all default scenetags to the player.
*/
private void applyStartingSceneTags() {
GameData.getSceneTagDataMap().values().stream()
.filter(sceneTag -> sceneTag.isDefaultValid())
.forEach(sceneTag -> {
if (this.getSceneTags().get(sceneTag.getSceneId()) == null) {
this.getSceneTags().put(sceneTag.getSceneId(), new HashSet<>());
}
this.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
});
}

/**
* Applies a property to the player if it doesn't exist in the database.
*
Expand Down Expand Up @@ -1384,6 +1401,10 @@ public void onLogin() {
}
*/

// Ensure the player has valid scenetags, allows old accounts to work
if (this.getSceneTags().isEmpty() || this.getSceneTags() == null) {
this.applyStartingSceneTags();
}

if (GameHome.HOME_SCENE_IDS.contains(this.getSceneId())) {
this.setSceneId(this.prevScene <= 0 ? 3 : this.prevScene); // if the player in home, make the player go back.
Expand Down
30 changes: 23 additions & 7 deletions src/main/java/emu/grasscutter/scripts/ScriptLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -1013,22 +1013,38 @@ public int ChangeToTargetLevelTag(int var1){
}

public int AddSceneTag(int sceneId, int sceneTagId){
logger.warn("[LUA] Call unimplemented AddSceneTag with {}, {}", sceneId, sceneTagId);
//TODO implement
logger.debug("[LUA] Call AddSceneTag with {}, {}", sceneId, sceneTagId);
// Ensure key exists for given scene
if (sceneScriptManager.get().getScene().getHost().getSceneTags().get(sceneId) == null) {
sceneScriptManager.get().getScene().getHost().getSceneTags().put(sceneId, new HashSet<>());
}
sceneScriptManager.get().getScene().getHost().getSceneTags().get(sceneId).add(sceneTagId);
return 0;
}

public int DelSceneTag(int sceneId, int sceneTagId){
logger.warn("[LUA] Call unimplemented DelSceneTag with {}, {}", sceneId, sceneTagId);
//TODO implement
logger.debug("[LUA] Call DelSceneTag with {}, {}", sceneId, sceneTagId);
var sceneTags = sceneScriptManager.get().getScene().getHost().getSceneTags();
// Ensure key exists for given scene
if (sceneTags.get(sceneId) == null) {
// Can't delete something that doesn't exist
return 0;
}
sceneScriptManager.get().getScene().getHost().getSceneTags().get(sceneId).remove(sceneTagId);
NotThorny marked this conversation as resolved.
Show resolved Hide resolved
return 0;
}

public boolean CheckSceneTag(int sceneId, int sceneTagId){
logger.warn("[LUA] Call unimplemented CheckSceneTag with {}, {}", sceneId, sceneTagId);
//TODO implement
return false;
logger.debug("[LUA] Call CheckSceneTag with {}, {}", sceneId, sceneTagId);
var sceneTags = sceneScriptManager.get().getScene().getHost().getSceneTags();
// Ensure key exists for given scene
if (sceneTags.get(sceneId) == null) {
// No point checking if it is null
return false;
}
return sceneScriptManager.get().getScene().getHost().getSceneTags().get(sceneId).contains(sceneTagId);
}

public int StartHomeGallery(int galleryId, int uid){
logger.warn("[LUA] Call unimplemented StartHomeGallery with {} {}", galleryId, uid);
//TODO implement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void handle(GameSession session, byte[] header, byte[] payload) throws Ex
session.send(new PacketServerTimeNotify());
session.send(new PacketWorldPlayerInfoNotify(world));
session.send(new PacketWorldDataNotify(world));
session.send(new PacketPlayerWorldSceneInfoListNotify());
session.send(new PacketPlayerWorldSceneInfoListNotify(player));
session.send(new BasePacket(PacketOpcodes.SceneForceUnlockNotify));
session.send(new PacketHostPlayerNotify(world));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
package emu.grasscutter.server.packet.send;

import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.scene.SceneTagData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.MapLayerInfoOuterClass;
import emu.grasscutter.net.proto.PlayerWorldSceneInfoListNotifyOuterClass.PlayerWorldSceneInfoListNotify;
import emu.grasscutter.net.proto.PlayerWorldSceneInfoOuterClass.PlayerWorldSceneInfo;
import java.util.stream.IntStream;

import java.util.Map;

public class PacketPlayerWorldSceneInfoListNotify extends BasePacket {

public PacketPlayerWorldSceneInfoListNotify() {
public PacketPlayerWorldSceneInfoListNotify(Player player) {
super(PacketOpcodes.PlayerWorldSceneInfoListNotify); // Rename opcode later

var sceneTags = player.getSceneTags();

PlayerWorldSceneInfoListNotify.Builder proto =
PlayerWorldSceneInfoListNotify.newBuilder()
.addInfoList(PlayerWorldSceneInfo.newBuilder().setSceneId(1).setIsLocked(false).build())
.addInfoList(
PlayerWorldSceneInfo.newBuilder()
.setSceneId(3)
.addInfoList(PlayerWorldSceneInfo.newBuilder().setSceneId(1).setIsLocked(false).build());

// Iterate over all scenes the user has tags for
for (int scene : sceneTags.keySet()) {
NotThorny marked this conversation as resolved.
Show resolved Hide resolved
var worldInfoBuilder = PlayerWorldSceneInfo.newBuilder();

// Add all scene tags for the given scene
proto.addInfoList(
worldInfoBuilder
.setSceneId(scene)
.setIsLocked(false)
.addAllSceneTagIdList(
GameData.getSceneTagDataMap().values().stream()
.filter(sceneTag -> sceneTag.getSceneId() == 3)
.filter(
sceneTag ->
sceneTag.isDefaultValid()
|| sceneTag.getCond().get(0).getCondType() != null)
.map(SceneTagData::getId)
.toList())
// .addSceneTagIdList(102) // Jade chamber (alr added)
// .addSceneTagIdList(113)
// .addSceneTagIdList(117)
// .addSceneTagIdList(1093) // 3.0 Vana_real
.addSceneTagIdList(1094) // 3.0 Vana_dream
// .addSceneTagIdList(1095) // 3.0 Vana_first
// .addSceneTagIdList(1096) // 3.0 Vana_festival
.addSceneTagIdList(152) // 3.1 event
.addSceneTagIdList(153) // 3.1 event
.addSceneTagIdList(1164) // Desert Arena (XMSM_CWLTop)
.addSceneTagIdList(1166) // Desert Pyramid (CWL_Trans_02)
.setMapLayerInfo(
sceneTags.entrySet().stream()
.filter(e -> e.getKey().equals(scene))
.map(Map.Entry::getValue)
.toList()
.get(0)
)
);

// Add map layer information for big world
if (scene == 3) {
worldInfoBuilder.setMapLayerInfo(
MapLayerInfoOuterClass.MapLayerInfo.newBuilder()
.addAllUnlockedMapLayerIdList(
GameData.getMapLayerDataMap().keySet()) // MapLayer Ids
Expand All @@ -49,31 +49,11 @@ public PacketPlayerWorldSceneInfoListNotify() {
.addAllUnlockedMapLayerGroupIdList(
GameData.getMapLayerGroupDataMap()
.keySet()) // will show MapLayer options when hovered over
.build()) // map layer test
.build())
.addInfoList(
PlayerWorldSceneInfo.newBuilder()
.setSceneId(4)
.setIsLocked(false)
.addSceneTagIdList(106)
.addSceneTagIdList(109)
.addSceneTagIdList(117)
.build())
.addInfoList(PlayerWorldSceneInfo.newBuilder().setSceneId(5).setIsLocked(false).build())
.addInfoList(PlayerWorldSceneInfo.newBuilder().setSceneId(6).setIsLocked(false).build())
.addInfoList(PlayerWorldSceneInfo.newBuilder().setSceneId(7).setIsLocked(false).build())
.addInfoList(
PlayerWorldSceneInfo.newBuilder()
.setSceneId(9)
.setIsLocked(false)
.addAllSceneTagIdList(IntStream.range(0, 3000).boxed().toList())
.build())
.addInfoList(
PlayerWorldSceneInfo.newBuilder()
.setSceneId(10)
.setIsLocked(false)
.addAllSceneTagIdList(IntStream.range(0, 3000).boxed().toList())
.build()); // 3.8
.build()); // map layer test
}

worldInfoBuilder.build();
}

this.setData(proto);
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/languages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@
"setProp": {
"description": "Sets accountwide properties. Things like godmode can be enabled this way, as well as changing things like unlocked abyss floor and battle pass progress.\n\tValues for <prop> (case-insensitive): GodMode | UnlimitedStamina | UnlimitedEnergy | TowerLevel | WorldLevel | BPLevel | SetOpenState | UnsetOpenState | UnlockMap\n\t(cont.) see PlayerProperty enum for other possible values, of the form PROP_MAX_SPRING_VOLUME -> max_spring_volume"
},
"setSceneTag":{
"description": "Sets account-specific scene tags. This controls things like rocks blocking doors, buildings being visible, and other (usually quest-related) things that affect what is visible in your world."
},
"setStats": {
"description": "Sets fight property for your current active character\n\tValues for <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Elemental DMG Bonus: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Elemental RES: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
"locked_to": "%s locked to %s.",
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/languages/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@
"setProp": {
"description": "Establece propiedades de la cuenta. Cosas como el modo Dios pueden ser establecidos con este comando, además de cambiar cosas como desbloquear pisos del abismo o progreso del pase de batalla.\n\tValores para <prop>: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel\n\t(cont.) Observa PlayerProperty enum para ver otros posibles valores, de la forma PROP_MAX_SPRING_VOLUME -> max_spring_volume"
},
"setSceneTag":{
"description": "Establece etiquetas de escena específicas de la cuenta. Esto controla cosas como rocas bloqueando puertas, edificios visibles y otras cosas (normalmente relacionadas con misiones) que afectan a lo que es visible en tu mundo."
},
"setStats": {
"description": "Establece propiedades de combate para tu personaje actual\n\tValores para <estado>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus de daño elemental: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Resistencia elemental: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
"locked_to": "%s fijado a %s.",
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/languages/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@
"setProp": {
"description": "Définit des propriétes pour votre compte. Des choses comme le godemode peuvent être activés avec cette commande, et le déblocage de l'abysse ainsi que l'avancement du PB.\n\tValues for <prop>: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel\n\t(cont.) see PlayerProperty enum for other possible values, of form PROP_MAX_SPRING_VOLUME -> max_spring_volume"
},
"setSceneTag":{
"description": "Définit les balises de scène spécifiques au compte. Cela permet de contrôler des choses comme les rochers qui bloquent les portes, les bâtiments qui sont visibles, et d'autres choses (généralement liées aux quêtes) qui affectent ce qui est visible dans votre monde."
},
"setStats": {
"description": "Définit les propriétés de combat de votre personnage actif\n\tValeurs pour <stat>: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus de dégât élémentaire: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) Résistance élémentaire: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
"locked_to": "%s verrouillé à %s.",
Expand Down