fix(server): login and map stuff

This commit is contained in:
capyb2222 committed 2026-09-24 19:39:42 -07:00
1 parent e05122031d
commit 3839761c34
23 files changed
+234 -136

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
java -jar LunaGC-7.0.0.jar
java -jar LunaGC-7.1.0.jar
pause
+1 -1
View File
@@ -65,7 +65,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
group = 'io.grasscutter'
version = '7.0.0'
version = '7.1.0'
java {
withJavadocJar()
@@ -7,8 +7,8 @@ import emu.grasscutter.utils.objects.SparseSet;
import java.util.Arrays;
public final class GameConstants {
public static String VERSION = "7.0.0";
public static int[] VERSION_PARTS = {7, 0, 0};
public static String VERSION = "7.1.0";
public static int[] VERSION_PARTS = {7, 1, 0};
public static boolean DEBUG = false;
public static final int ENTITY_ID_BIT_SHIFT = 21;
@@ -106,7 +106,7 @@ public final class Grasscutter {
// Initialize server.
logger.info(translate("messages.status.starting"));
logger.info(translate("messages.status.game_version", GameConstants.VERSION));
logger.info(translate("messages.status.version", "7.0.0", "capy"));
logger.info(translate("messages.status.version", GameConstants.VERSION, "capy"));
// Initialize database.
DatabaseManager.initialize();
@@ -242,7 +242,7 @@ public final class SetPropCommand implements CommandHandler {
boolean forbidSimpleUnlock = pointData.isForbidSimpleUnlock();
boolean sceneBuildingPointLocked =
pointData.getType().equals("SceneBuildingPoint") && !pointData.isUnlocked();
"SceneBuildingPoint".equals(pointData.getType()) && !pointData.isUnlocked();
if (forbidSimpleUnlock || sceneBuildingPointLocked) scenePointsBackup.remove(p);
}
@@ -362,7 +362,6 @@ public class ConfigContainer {
/* Protobuf field number the Lua payload is written to, flat at the top level.
* 0 uses the built-in default (6, which is `payload` on 7.0's message). */
public int payloadField = 0;
public String[] sweep = {};
}
public static class BirthdayMailOptions {
@@ -438,7 +437,7 @@ public class ConfigContainer {
public static class JoinOptions {
public int[] welcomeEmotes = {2007, 1002, 4010};
public String welcomeMessage = "Welcome to LunaGC 7.0.0";
public String welcomeMessage = "Welcome to LunaGC " + emu.grasscutter.GameConstants.VERSION;
public JoinOptions.Mail welcomeMail = new JoinOptions.Mail();
public static class Mail {
@@ -1,50 +1,51 @@
package emu.grasscutter.data.excels;
import emu.grasscutter.data.*;
import lombok.Getter;
import java.util.*;
@ResourceType(name = "OpenStateConfigData.json", loadPriority = ResourceType.LoadPriority.HIGHEST)
public class OpenStateData extends GameResource {
@Getter(onMethod_ = @Override)
private int id;
@Getter
private boolean defaultState;
@Getter
private boolean allowClientOpen;
@Getter
private int systemOpenUiId;
@Getter
private List<OpenStateCond> cond;
@Override
public void onLoad() {
// Add this open state to the global list.
GameData.getOpenStateList().add(this);
// Remove any empty conditions
if (this.cond != null) {
this.cond.removeIf(c -> c.getCondType() == null);
} else {
this.cond = new ArrayList<>();
}
}
public enum OpenStateCondType {
OPEN_STATE_COND_PLAYER_LEVEL,
OPEN_STATE_COND_QUEST,
OPEN_STATE_OFFERING_LEVEL,
OPEN_STATE_CITY_REPUTATION_LEVEL,
OPEN_STATE_COND_PARENT_QUEST
}
public static class OpenStateCond {
@Getter
private OpenStateCondType condType;
@Getter
private int param;
@Getter
private int param2;
}
}
package emu.grasscutter.data.excels;
import emu.grasscutter.data.*;
import lombok.Getter;
import java.util.*;
@ResourceType(name = "OpenStateConfigData.json", loadPriority = ResourceType.LoadPriority.HIGHEST)
public class OpenStateData extends GameResource {
@Getter(onMethod_ = @Override)
private int id;
@Getter
private boolean defaultState;
@Getter
private boolean allowClientOpen;
@Getter
private int systemOpenUiId;
@Getter
private List<OpenStateCond> cond;
@Override
public void onLoad() {
// Add this open state to the global list.
GameData.getOpenStateList().add(this);
// Remove any empty conditions
if (this.cond != null) {
this.cond.removeIf(c -> c.getCondType() == null);
} else {
this.cond = new ArrayList<>();
}
}
public enum OpenStateCondType {
OPEN_STATE_COND_PLAYER_LEVEL,
OPEN_STATE_COND_QUEST,
OPEN_STATE_OFFERING_LEVEL,
OPEN_STATE_CITY_REPUTATION_LEVEL,
OPEN_STATE_COND_PARENT_QUEST
}
public static class OpenStateCond {
@Getter
private OpenStateCondType condType;
@Getter
private int param;
@Getter
private int param2;
}
}
␍
@@ -24,6 +24,10 @@ public class ActivityData extends GameResource {
@Override
public void onLoad() {
if (watcherId == null) {
this.watcherDataList = List.of();
return;
}
this.watcherDataList =
watcherId.stream()
.map(item -> GameData.getActivityWatcherDataMap().get(item.intValue()))
@@ -20,8 +20,14 @@ public class TowerScheduleData extends GameResource {
@Override
public void onLoad() {
super.onLoad();
if (this.schedules == null) {
this.schedules = List.of();
return;
}
this.schedules =
this.schedules.stream().filter(item -> item.getFloorList().size() > 0).toList();
this.schedules.stream()
.filter(item -> item.getFloorList() != null && !item.getFloorList().isEmpty())
.toList();
}
public int getScheduleId() {
@@ -235,6 +235,8 @@ public class ChatSystem implements ChatSystemHandler {
private void sendServerWelcomeMessages(Player player) {
var joinOptions = GAME_INFO.joinOptions;
player.sendPacket(new PacketGetPlayerFriendListRsp(player));
if (joinOptions.welcomeEmotes != null && joinOptions.welcomeEmotes.length > 0) {
this.sendPrivateMessageFromServer(
player.getUid(),
@@ -0,0 +1,99 @@
package emu.grasscutter.server.game;
import com.google.gson.reflect.TypeToken;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.UnknownFieldSet;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.JsonUtils;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class FanOut {
public static final class Target {
public int cmdId;
public String symbol;
public Map<String, Integer> fields = new HashMap<>();
}
private static Map<String, List<Target>> table;
private static final Map<String, Map<Integer, String>> numberNames = new ConcurrentHashMap<>();
private static final Set<String> announced = ConcurrentHashMap.newKeySet();
private static synchronized Map<String, List<Target>> table() {
if (table != null) return table;
table = Map.of();
var path = FileUtils.getDataUserPath("proto-fanout.json");
if (!Files.exists(path)) return table;
try {
Map<String, List<Target>> loaded =
JsonUtils.decode(
Files.readString(path), new TypeToken<Map<String, List<Target>>>() {}.getType());
if (loaded != null) table = loaded;
Grasscutter.getLogger().info("[fan-out] {} packets resolved by family", table.size());
} catch (Exception e) {
Grasscutter.getLogger().warn("[fan-out] could not read {}: {}", path, e.getMessage());
}
return table;
}
public static boolean send(GameSession session, BasePacket packet) {
var simple = packet.getClass().getSimpleName();
if (!simple.startsWith("Packet")) return false;
var name = simple.substring("Packet".length());
var targets = table().get(name);
if (targets == null || targets.isEmpty() || packet.getData() == null) return false;
var names = numberNames.computeIfAbsent(name, FanOut::numberNames);
if (names.isEmpty()) return false;
UnknownFieldSet source;
try {
source = UnknownFieldSet.parseFrom(packet.getData());
} catch (Exception e) {
return false;
}
if (announced.add(name)) {
Grasscutter.getLogger()
.info("[fan-out] {} goes to {} candidate(s)", name, targets.size());
}
for (var target : targets) {
var out = UnknownFieldSet.newBuilder();
for (var entry : source.asMap().entrySet()) {
var field = names.get(entry.getKey());
var number = field == null ? null : target.fields.get(field);
if (number != null) out.mergeField(number, entry.getValue());
}
var copy =
packet.shouldBuildHeader()
? new BasePacket(target.cmdId, true)
: new BasePacket(target.cmdId);
if (!packet.shouldBuildHeader() && packet.getHeader() != null) {
copy.setHeader(packet.getHeader());
}
copy.shouldEncrypt = packet.shouldEncrypt;
copy.setUseDispatchKey(packet.useDispatchKey());
copy.setData(out.build().toByteArray());
session.send(copy);
}
return true;
}
private static Map<Integer, String> numberNames(String name) {
var out = new HashMap<Integer, String>();
try {
var cls = Class.forName("emu.grasscutter.net.proto." + name + "OuterClass$" + name);
var descriptor = (Descriptor) cls.getMethod("getDescriptor").invoke(null);
for (var field : descriptor.getFields()) out.put(field.getNumber(), field.getName());
} catch (Exception ignored) {
}
return out;
}
}
@@ -49,6 +49,7 @@ public final class GameServerPacketHandler {
public void handle(GameSession session, int opcode, byte[] header, byte[] payload) {
PacketHandler handler = this.handlers.get(opcode);
if (handler == null && unannounced.add(opcode)) {
String fields;
try {
@@ -121,11 +121,15 @@ public class GameSession implements GameSessionManager.KcpChannel {
return;
}
if (packet.getOpcode() <= 0 && FanOut.send(this, packet)) {
return;
}
if (packet.getOpcode() <= 0) {
if (missingCmdIdReported.add(packet.getClass().getSimpleName())) {
Grasscutter.getLogger()
.warn(
"{} has no 7.0 CmdId, so it is not being sent.",
"{} has no CmdId in this version, so it is not being sent.",
packet.getClass().getSimpleName());
}
return;
@@ -6,6 +6,7 @@ import emu.grasscutter.*;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.GetPlayerTokenReqOuterClass.GetPlayerTokenReq;
import emu.grasscutter.server.event.game.PlayerCreationEvent;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.game.GameSession.SessionState;
@@ -19,17 +20,13 @@ import javax.crypto.Cipher;
@Opcodes(PacketOpcodes.GetPlayerTokenReq)
public class HandlerGetPlayerTokenReq extends PacketHandler {
private static final int F_ACCOUNT_UID = 2; // 6.7: 2
private static final int F_ACCOUNT_TOKEN = 6; // 6.7: 3
private static final int F_KEY_ID = 588; // 6.7: 41
private static final int F_CLIENT_RAND_KEY = 932; // 6.7: 1475
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
var accountId = ProtoRead.string(payload, F_ACCOUNT_UID);
var accountToken = ProtoRead.string(payload, F_ACCOUNT_TOKEN);
var clientRandKey = ProtoRead.string(payload, F_CLIENT_RAND_KEY);
var keyId = (int) ProtoRead.varint(payload, F_KEY_ID);
var req = GetPlayerTokenReq.parseFrom(payload);
var accountId = req.getAccountUid();
var accountToken = req.getAccountToken();
var clientRandKey = req.getClientRandKey();
var keyId = req.getKeyId();
var account = DispatchUtils.authenticate(accountId, accountToken);
@@ -2,22 +2,19 @@ package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.PacketHeadOuterClass.PacketHead;
import emu.grasscutter.net.proto.PingReqOuterClass.PingReq;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.utils.ProtoRead;
import emu.grasscutter.server.packet.send.PacketPingRsp;
@Opcodes(PacketOpcodes.PingReq)
public class HandlerPingReq extends PacketHandler {
// read by field number so a renumber cannot silently zero the ping
private static final int F_CLIENT_TIME = 1;
private static final int F_SEQ = 15;
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
PacketHead head = PacketHead.parseFrom(header);
var clientTime = (int) ProtoRead.varint(payload, F_CLIENT_TIME);
var seq = (int) ProtoRead.varint(payload, F_SEQ);
var req = PingReq.parseFrom(payload);
var clientTime = req.getClientTime();
var seq = req.getSeq();
session.updateLastPingTime(clientTime);
@@ -48,11 +48,8 @@ public class HandlerSceneInitFinishReq extends PacketHandler {
// The wind seed CmdId is unconfirmed for 6.7; a wrong one can hang the client here, so
// cmdId=0 suppresses the packet entirely rather than falling back to the UID watermark.
var sweep = Configuration.GAME_OPTIONS.watermark.sweep;
if (PacketWindSeedClientNotify.disabled()) {
Grasscutter.getLogger().info("[watermark] suppressed (cmdId=0)");
} else if (sweep != null && sweep.length > 0) {
sendSweep(session, sweep);
} else {
var packet = buildWatermarkPacket();
Grasscutter.getLogger()
@@ -69,50 +66,6 @@ public class HandlerSceneInitFinishReq extends PacketHandler {
player.getScene().playerSceneInitialized(player);
}
private static void sendSweep(GameSession session, String[] sweep) {
var options = Configuration.GAME_OPTIONS.watermark;
if (!options.enabled || options.text == null || options.text.isBlank()) {
Grasscutter.getLogger().warn("[watermark] sweep configured but the watermark is disabled or has no text.");
return;
}
for (var entry : sweep) {
var parts = entry.split(":");
int cmdId, field;
try {
cmdId = Integer.parseInt(parts[0].trim());
field = parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 1;
} catch (Exception e) {
Grasscutter.getLogger().warn("[watermark] ignoring malformed sweep entry '{}'", entry);
continue;
}
// These two are PlayerLoginRsp and GetPlayerTokenRsp. A Lua payload under either breaks
// the login handshake instead of the watermark, which is a far worse failure to debug.
if (cmdId == 8191 || cmdId == 9250) {
Grasscutter.getLogger().warn("[watermark] refusing to sweep {}, it is a login packet", cmdId);
continue;
}
// Each candidate writes its own CmdId as the watermark text, so whichever one the
// client actually accepts identifies itself on screen - one login instead of a bisect.
var text = String.valueOf(cmdId);
int from = RichTextUtils.parseColor(options.color);
int to = RichTextUtils.parseColor(options.gradientTo);
var payload =
from < 0
? text
: (to < 0
? RichTextUtils.colorize(text, from)
: RichTextUtils.gradient(text, from, to));
if (!WatermarkUtils.fits(payload)) payload = text;
Grasscutter.getLogger().info("[watermark] sweep -> cmdId={} payloadField={}", cmdId, field);
session.send(
new PacketWindSeedClientNotify(WatermarkUtils.buildLuac(payload), cmdId, field));
}
}
private static BasePacket buildWatermarkPacket() {
var options = Configuration.GAME_OPTIONS.watermark;
@@ -18,6 +18,6 @@ public class PacketEnterSceneReadyRsp extends BasePacket {
.setRetcode(maskedRetcode)
.build();
this.setData(p.toByteArray());
this.setData(p);
}
}
@@ -4,6 +4,8 @@ import emu.grasscutter.data.GameData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.GetScenePointRspOuterClass.GetScenePointRsp;
import java.util.Collection;
import java.util.TreeSet;
public class PacketGetScenePointRsp extends BasePacket {
@@ -23,10 +25,18 @@ public class PacketGetScenePointRsp extends BasePacket {
p.addAllUnhidePointList(unlockedPoints);
}
for (int i = 1; i < 9; i++) {
p.addUnlockAreaList(i);
}
p.addAllUnlockAreaList(areas(player, sceneId));
this.setData(p);
}
private static Collection<Integer> areas(Player player, int sceneId) {
var unlocked = player.getUnlockedSceneAreas(sceneId);
if (!unlocked.isEmpty()) return unlocked;
var all = new TreeSet<Integer>();
for (var area : GameData.getWorldAreaDataMap().values()) {
if (area.getSceneId() == sceneId && area.getChildArea() == 0) all.add(area.getParentArea());
}
return all;
}
}
@@ -1 +1,2 @@
// removed due to proto not existing
// removed due to proto not existing
␍
@@ -18,6 +18,6 @@ public class PacketSceneInitFinishRsp extends BasePacket {
.setRetcode(maskedRetcode)
.build();
this.setData(p.toByteArray());
this.setData(p);
}
}
@@ -20,7 +20,10 @@ public class PacketWindSeedClientNotify extends BasePacket {
}
/** `payload` on 7.0's _PlayerNormalLuaShellNotify. 6.7 had it at 11, 6.6 at 8. */
private static final int DEFAULT_PAYLOAD_FIELD = 6;
private static final int DEFAULT_PAYLOAD_FIELD =
emu.grasscutter.net.proto.WindSeedType1NotifyOuterClass.WindSeedType1Notify.getDescriptor()
.findFieldByName("_payload")
.getNumber();
public static boolean disabled() {
return Configuration.GAME_OPTIONS.watermark.cmdId < 0;
@@ -64,10 +67,4 @@ public class PacketWindSeedClientNotify extends BasePacket {
super(cmdId());
this.setData(encode(data));
}
/** One candidate of a sweep: an explicit CmdId and payload field rather than the configured pair. */
public PacketWindSeedClientNotify(byte[] data, int cmdId, int payloadField) {
super(cmdId);
this.setData(encode(data, payloadField));
}
}
@@ -148,6 +148,31 @@ public interface JsonAdapters {
}
}
class UnsignedLongAdapter extends TypeAdapter<Long> {
@Override
public Long read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
val value = reader.nextString();
try {
return Long.parseLong(value);
} catch (NumberFormatException signed) {
try {
return Long.parseUnsignedLong(value);
} catch (NumberFormatException unsigned) {
throw new IOException("Invalid long - " + value);
}
}
}
@Override
public void write(JsonWriter writer, Long value) throws IOException {
writer.value(value);
}
}
class IntListAdapter extends TypeAdapter<IntList> {
@Override
public IntList read(JsonReader reader) throws IOException {
@@ -19,6 +19,8 @@ public final class JsonUtils {
.setPrettyPrinting()
.registerTypeAdapter(DynamicFloat.class, new DynamicFloatAdapter())
.registerTypeAdapter(IntList.class, new IntListAdapter())
.registerTypeAdapter(long.class, new UnsignedLongAdapter())
.registerTypeAdapter(Long.class, new UnsignedLongAdapter())
.registerTypeAdapter(Position.class, new PositionAdapter())
.registerTypeAdapter(GridPosition.class, new GridPositionAdapter())
.registerTypeAdapter(byte[].class, new ByteArrayAdapter())