From b6ba7007e447f0f54600e8078daab70506f0192a Mon Sep 17 00:00:00 2001 From: autumnshine Date: Tue, 13 Jan 2026 19:03:25 +0800 Subject: [PATCH 1/4] Implemented packets for pin code. --- .../java/kinoko/database/AccountAccessor.java | 4 + .../cassandra/CassandraAccountAccessor.java | 30 +++++++ .../cassandra/table/AccountTable.java | 2 + .../kinoko/handler/stage/LoginHandler.java | 89 ++++++++++++++++++- .../packet/stage/CheckPinCodeResultType.java | 23 +++++ .../java/kinoko/packet/stage/LoginPacket.java | 14 ++- .../kinoko/packet/stage/PinCodeModalOpt.java | 17 ++++ .../packet/stage/PinCodeUpdateModalOpt.java | 16 ++++ src/main/java/kinoko/server/ServerConfig.java | 1 + src/main/java/kinoko/server/node/Client.java | 10 +++ 10 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java create mode 100644 src/main/java/kinoko/packet/stage/PinCodeModalOpt.java create mode 100644 src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java diff --git a/src/main/java/kinoko/database/AccountAccessor.java b/src/main/java/kinoko/database/AccountAccessor.java index 39af1563..3b0b60af 100644 --- a/src/main/java/kinoko/database/AccountAccessor.java +++ b/src/main/java/kinoko/database/AccountAccessor.java @@ -16,4 +16,8 @@ public interface AccountAccessor { boolean newAccount(String username, String password); boolean saveAccount(Account account); + + Optional getPinCode(int accountId); + + boolean savePinCode(int accountId, String pinCode); } diff --git a/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java b/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java index 32efdb43..820c0452 100644 --- a/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java +++ b/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java @@ -195,4 +195,34 @@ public boolean saveAccount(Account account) { ); return updateResult.wasApplied(); } + + @Override + public Optional getPinCode(int accountId) { + final ResultSet selectResult = getSession().execute( + selectFrom(getKeyspace(), AccountTable.getTableName()).all() + .column(AccountTable.PIN_CODE) + .whereColumn(AccountTable.ACCOUNT_ID).isEqualTo(literal(accountId)) + .build() + .setExecutionProfileName(CassandraConnector.PROFILE_ONE) + ); + final Row row = selectResult.one(); + if (row != null) { + final String pinCode = row.getString(AccountTable.PIN_CODE); + if (pinCode != null && !pinCode.isBlank()) { + return Optional.of(pinCode); + } + } + return Optional.empty(); + } + + @Override + public boolean savePinCode(int accountId, String pinCode) { + final ResultSet updateResult = getSession().execute( + update(getKeyspace(), AccountTable.getTableName()) + .setColumn(AccountTable.PIN_CODE, literal(pinCode)) + .whereColumn(AccountTable.ACCOUNT_ID).isEqualTo(literal(accountId)) + .build() + ); + return updateResult.wasApplied(); + } } diff --git a/src/main/java/kinoko/database/cassandra/table/AccountTable.java b/src/main/java/kinoko/database/cassandra/table/AccountTable.java index 18b0bf32..6f6fea2f 100644 --- a/src/main/java/kinoko/database/cassandra/table/AccountTable.java +++ b/src/main/java/kinoko/database/cassandra/table/AccountTable.java @@ -20,6 +20,7 @@ public final class AccountTable { public static final String TRUNK_MONEY = "trunk_money"; public static final String LOCKER_ITEMS = "locker_items"; public static final String WISHLIST = "wishlist"; + public static final String PIN_CODE = "pin_code"; private static final String tableName = "account_table"; @@ -44,6 +45,7 @@ public static void createTable(CqlSession session, String keyspace) { .withColumn(TRUNK_MONEY, DataTypes.INT) .withColumn(LOCKER_ITEMS, DataTypes.frozenListOf(SchemaBuilder.udt(CashItemInfoUDT.getTypeName(), true))) .withColumn(WISHLIST, DataTypes.frozenListOf(DataTypes.INT)) + .withColumn(PIN_CODE, DataTypes.TEXT) .build() ); session.execute( diff --git a/src/main/java/kinoko/handler/stage/LoginHandler.java b/src/main/java/kinoko/handler/stage/LoginHandler.java index ca425ef3..a1ac3e71 100644 --- a/src/main/java/kinoko/handler/stage/LoginHandler.java +++ b/src/main/java/kinoko/handler/stage/LoginHandler.java @@ -2,8 +2,7 @@ import kinoko.database.DatabaseManager; import kinoko.handler.Handler; -import kinoko.packet.stage.LoginPacket; -import kinoko.packet.stage.LoginResultType; +import kinoko.packet.stage.*; import kinoko.provider.EtcProvider; import kinoko.provider.ItemProvider; import kinoko.provider.SkillProvider; @@ -82,7 +81,9 @@ public static void handleCheckPassword(Client c, InPacket inPacket) { c.setAccount(account); c.setMachineId(machineId); - c.getServerNode().addClient(c); + if (!ServerConfig.ENABLE_PIN_CODE) { + doAddClient(c); + } c.write(LoginPacket.checkPasswordResultSuccess(account, c.getClientKey())); }); } @@ -415,6 +416,84 @@ public static void handleCheckSpwRequest(Client c, InPacket inPacket) { handleMigration(c, account, characterId); } + @Handler(InHeader.CheckPinCode) + public static void handleCheckPinCode(Client c, InPacket inPacket) { + final byte pinCodeModalOpt = inPacket.decodeByte(); + if (pinCodeModalOpt == PinCodeModalOpt.CANCEL.getValue()) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.Cancel)); + return; + } + + final byte pinCodeAttemptMaxCount = 5; + + final boolean pinCodeShouldRegisterOrEnterInLoginOpt = inPacket.decodeBoolean(); + + final String inputPinCode = inPacket.decodeString(); + if (pinCodeModalOpt == PinCodeModalOpt.LOGIN.getValue()) { + final Optional pinCodeOptional = DatabaseManager.accountAccessor().getPinCode(c.getAccount().getId()); + if (pinCodeShouldRegisterOrEnterInLoginOpt) { + if (pinCodeOptional.isEmpty() || pinCodeOptional.get().isBlank()) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CreateOrUpdate)); + } else { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.RequestToEnter)); + } + } else { + if (pinCodeOptional.isEmpty() || pinCodeOptional.get().isBlank()) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CreateOrUpdate)); + } else { + if (inputPinCode.equals(pinCodeOptional.get())) { + c.setPinCodeAttemptCount(0); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.Done)); + } else { + final int pinCodeAttemptCount = c.getPinCodeAttemptCount(); + if (pinCodeAttemptCount >= pinCodeAttemptMaxCount) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckTooMuchInvalid)); + } else { + c.setPinCodeAttemptCount(pinCodeAttemptCount + 1); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckInvalid)); + } + } + } + } + } else if (pinCodeModalOpt == PinCodeModalOpt.CHANGE_PIN.getValue()) { + final Optional pinCodeOptional = DatabaseManager.accountAccessor().getPinCode(c.getAccount().getId()); + if (pinCodeOptional.isPresent() && inputPinCode.equals(pinCodeOptional.get())) { + c.setPinCodeAttemptCount(0); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CreateOrUpdate)); + } else { + final int pinCodeAttemptCount = c.getPinCodeAttemptCount(); + if (pinCodeAttemptCount >= pinCodeAttemptMaxCount) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckTooMuchInvalid)); + } else { + c.setPinCodeAttemptCount(pinCodeAttemptCount + 1); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckInvalid)); + } + } + } + } + + @Handler(InHeader.UpdatePinCode) + public static void handleUpdatePinCode(Client c, InPacket inPacket) { +// void __thiscall CLogin::OnUpdatePinCodeResult(CLogin *this, CInPacket *iPacket) +// { +// if ( CInPacket::Decode1(iPacket) ) +// CLoginUtilDlg::Error(15, 0); +// else +// CPinCodeDlg::Notice(8); +// CUITitle::EnableLoginCtrl((CUITitle *)TSingleton::ms_pInstance._m_pStr, 1); +// } + final byte pinCodeUpdateModalOpt = inPacket.decodeByte(); + boolean hasErrorInUpdate = false; + if (pinCodeUpdateModalOpt == PinCodeUpdateModalOpt.CANCEL.getValue()) { + // If the user clicks the cancel button, then also set hasErrorInUpdate to true. + hasErrorInUpdate = true; + } else { + final String pinCode = inPacket.decodeString(); + hasErrorInUpdate = !DatabaseManager.accountAccessor().savePinCode(c.getAccount().getId(), pinCode); + } + c.write(LoginPacket.updatePinCodeResult(hasErrorInUpdate)); + } + private static void loadCharacterList(Client c) { // Resolve character list for account, sorted by highest level final Account account = c.getAccount(); @@ -452,4 +531,8 @@ private static void handleMigration(Client c, Account account, int characterId) c.write(LoginPacket.selectCharacterResultSuccess(transferInfo.getChannelHost(), transferInfo.getChannelPort(), characterId)); }); } + + public static void doAddClient(Client c) { + c.getServerNode().addClient(c); + } } diff --git a/src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java b/src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java new file mode 100644 index 00000000..0700441b --- /dev/null +++ b/src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java @@ -0,0 +1,23 @@ +package kinoko.packet.stage; + +public enum CheckPinCodeResultType { + Cancel(-1), // This value does not exist on the client side; it is only used on the server side to indicate a cancellation operation. + Done(0), // In the client, when CheckPinCodeResultType =0 and m_nGameStartMode != 1, a WorldRequest packet will be sent. + CreateOrUpdate(1), + CheckInvalid(2), + CheckTooMuchInvalid(3), + RequestToEnter(4), + // no have 5,6 value + AccountAlreadyLogged(7), + ; + + private final int value; + + CheckPinCodeResultType(int value) { + this.value = value; + } + + public final int getValue() { + return value; + } +} diff --git a/src/main/java/kinoko/packet/stage/LoginPacket.java b/src/main/java/kinoko/packet/stage/LoginPacket.java index bd921fe7..258b2171 100644 --- a/src/main/java/kinoko/packet/stage/LoginPacket.java +++ b/src/main/java/kinoko/packet/stage/LoginPacket.java @@ -46,7 +46,7 @@ public static OutPacket checkPasswordResultSuccess(Account account, byte[] clien outPacket.encodeLong(0); // dtRegisterDate outPacket.encodeInt(account.getSlotCount()); // nNumOfCharacter - outPacket.encodeByte(true); // true ? VIEW_WORLD_SELECT : CHECK_PIN_CODE + outPacket.encodeByte(!ServerConfig.ENABLE_PIN_CODE); // true ? WORLD_REQUEST : CHECK_PIN_CODE outPacket.encodeByte(LoginOpt.getLoginOpt(account).getValue()); // bLoginOpt outPacket.encodeArray(clientKey); return outPacket; @@ -206,6 +206,18 @@ public static OutPacket checkSecondaryPasswordResult() { return outPacket; } + public static OutPacket checkPinCodeResult(CheckPinCodeResultType resultType) { + final OutPacket outPacket = OutPacket.of(OutHeader.CheckPinCodeResult); + outPacket.encodeByte(resultType.getValue()); + return outPacket; + } + + public static OutPacket updatePinCodeResult(boolean updateResult) { + final OutPacket outPacket = OutPacket.of(OutHeader.UpdatePinCodeResult); + outPacket.encodeByte(updateResult ? 1 : 0); + return outPacket; + } + private enum LoginOpt { INITIALIZE_SECONDARY_PASSWORD(0), CHECK_SECONDARY_PASSWORD(1), diff --git a/src/main/java/kinoko/packet/stage/PinCodeModalOpt.java b/src/main/java/kinoko/packet/stage/PinCodeModalOpt.java new file mode 100644 index 00000000..ff3312a3 --- /dev/null +++ b/src/main/java/kinoko/packet/stage/PinCodeModalOpt.java @@ -0,0 +1,17 @@ +package kinoko.packet.stage; + +public enum PinCodeModalOpt { + CANCEL(0), + LOGIN(1), + CHANGE_PIN(2), + ; + private final int value; + + PinCodeModalOpt(int value) { + this.value = value; + } + + public final int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java b/src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java new file mode 100644 index 00000000..ac9c29c7 --- /dev/null +++ b/src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java @@ -0,0 +1,16 @@ +package kinoko.packet.stage; + +public enum PinCodeUpdateModalOpt { + CANCEL(0), + OK(1), + ; + private final int value; + + PinCodeUpdateModalOpt(int value) { + this.value = value; + } + + public final int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/kinoko/server/ServerConfig.java b/src/main/java/kinoko/server/ServerConfig.java index e67eaf70..415a9127 100644 --- a/src/main/java/kinoko/server/ServerConfig.java +++ b/src/main/java/kinoko/server/ServerConfig.java @@ -13,6 +13,7 @@ public final class ServerConfig { public static final boolean AUTO_CREATE_ACCOUNT = Util.getEnv("AUTO_CREATE_ACCOUNT", true); public static final boolean REQUIRE_SECONDARY_PASSWORD = Util.getEnv("REQUIRE_SECONDARY_PASSWORD", true); + public static final boolean ENABLE_PIN_CODE = Util.getEnv("ENABLE_PIN_CODE", true); public static final String WZ_DIRECTORY = Util.getEnv("WZ_DIRECTORY", "wz"); public static final String DATA_DIRECTORY = Util.getEnv("DATA_DIRECTORY", "data"); diff --git a/src/main/java/kinoko/server/node/Client.java b/src/main/java/kinoko/server/node/Client.java index 8d70dd4b..5b962e8f 100644 --- a/src/main/java/kinoko/server/node/Client.java +++ b/src/main/java/kinoko/server/node/Client.java @@ -12,9 +12,19 @@ public final class Client extends NettyClient { private User user; private byte[] machineId; private byte[] clientKey; + private int pinCodeAttemptCount; public Client(ServerNode serverNode, SocketChannel socketChannel) { super(serverNode, socketChannel); + this.pinCodeAttemptCount = 0; + } + + public int getPinCodeAttemptCount() { + return pinCodeAttemptCount; + } + + public void setPinCodeAttemptCount(int pinCodeAttemptCount) { + this.pinCodeAttemptCount = pinCodeAttemptCount; } public Account getAccount() { From e6829e759c70bacf14880e51e28d03ab0d79c55d Mon Sep 17 00:00:00 2001 From: autumnshine Date: Tue, 13 Jan 2026 19:23:16 +0800 Subject: [PATCH 2/4] After successfully verifying the pin code, add the client to the ServerNode. --- src/main/java/kinoko/handler/stage/LoginHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kinoko/handler/stage/LoginHandler.java b/src/main/java/kinoko/handler/stage/LoginHandler.java index a1ac3e71..d9ced592 100644 --- a/src/main/java/kinoko/handler/stage/LoginHandler.java +++ b/src/main/java/kinoko/handler/stage/LoginHandler.java @@ -443,6 +443,7 @@ public static void handleCheckPinCode(Client c, InPacket inPacket) { } else { if (inputPinCode.equals(pinCodeOptional.get())) { c.setPinCodeAttemptCount(0); + doAddClient(c); c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.Done)); } else { final int pinCodeAttemptCount = c.getPinCodeAttemptCount(); From 18f3c3fceb53069870189a70ee2bea984065ba4d Mon Sep 17 00:00:00 2001 From: autumnshine Date: Tue, 13 Jan 2026 19:27:48 +0800 Subject: [PATCH 3/4] Rename doAddClient to addClientToServerNode --- src/main/java/kinoko/handler/stage/LoginHandler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/kinoko/handler/stage/LoginHandler.java b/src/main/java/kinoko/handler/stage/LoginHandler.java index d9ced592..bad0e10d 100644 --- a/src/main/java/kinoko/handler/stage/LoginHandler.java +++ b/src/main/java/kinoko/handler/stage/LoginHandler.java @@ -82,7 +82,7 @@ public static void handleCheckPassword(Client c, InPacket inPacket) { c.setAccount(account); c.setMachineId(machineId); if (!ServerConfig.ENABLE_PIN_CODE) { - doAddClient(c); + addClientToServerNode(c); } c.write(LoginPacket.checkPasswordResultSuccess(account, c.getClientKey())); }); @@ -443,7 +443,7 @@ public static void handleCheckPinCode(Client c, InPacket inPacket) { } else { if (inputPinCode.equals(pinCodeOptional.get())) { c.setPinCodeAttemptCount(0); - doAddClient(c); + addClientToServerNode(c); c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.Done)); } else { final int pinCodeAttemptCount = c.getPinCodeAttemptCount(); @@ -533,7 +533,7 @@ private static void handleMigration(Client c, Account account, int characterId) }); } - public static void doAddClient(Client c) { + public static void addClientToServerNode(Client c) { c.getServerNode().addClient(c); } } From 5dd62a98bbc5074ba4b533519c3706f75a0e097c Mon Sep 17 00:00:00 2001 From: autumnshine Date: Mon, 26 Jan 2026 13:36:09 +0800 Subject: [PATCH 4/4] Use outPacket.encodeByte(boolean) instead of outPacket.encodeByte(byte) for updatePinCodeResult --- src/main/java/kinoko/packet/stage/LoginPacket.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kinoko/packet/stage/LoginPacket.java b/src/main/java/kinoko/packet/stage/LoginPacket.java index 258b2171..55d526ef 100644 --- a/src/main/java/kinoko/packet/stage/LoginPacket.java +++ b/src/main/java/kinoko/packet/stage/LoginPacket.java @@ -214,7 +214,7 @@ public static OutPacket checkPinCodeResult(CheckPinCodeResultType resultType) { public static OutPacket updatePinCodeResult(boolean updateResult) { final OutPacket outPacket = OutPacket.of(OutHeader.UpdatePinCodeResult); - outPacket.encodeByte(updateResult ? 1 : 0); + outPacket.encodeByte(updateResult); return outPacket; }