Merge branch 'development' into dev-quests

This commit is contained in:
Melledy 2022-05-12 02:31:28 -07:00
commit 0df4a076d7
65 changed files with 1337 additions and 413 deletions

View File

@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message AbilityMixinWidgetMpSupport {
uint32 target_entity_id = 1;
}

View File

@ -0,0 +1,29 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "LunchBoxData.proto";
import "AnchorPointData.proto";
import "OneoffGatherPointDetectorData.proto";
import "ClientCollectorData.proto";
import "WidgetCoolDownData.proto";
import "WidgetSlotData.proto";
message AllWidgetDataNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4260;
}
repeated AnchorPointData anchor_point_list = 2;
uint32 next_anchor_point_usable_time = 3;
LunchBoxData lunch_box_data = 5;
repeated OneoffGatherPointDetectorData oneoff_gather_point_detector_data_list = 6;
repeated ClientCollectorData client_collector_data_list = 7;
repeated WidgetCoolDownData cool_down_group_data_list = 8;
repeated WidgetCoolDownData normal_cool_down_data_list = 9;
repeated WidgetSlotData slot_list = 11;
}

View File

@ -0,0 +1,12 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "Vector.proto";
message AnchorPointData {
uint32 anchor_point_id = 1;
Vector pos = 2;
Vector rot = 3;
uint32 end_time = 4;
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "AnchorPointData.proto";
message AnchorPointDataNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4253;
}
repeated AnchorPointData anchor_point_list = 1;
uint32 next_usable_time = 2;
}

View File

@ -0,0 +1,24 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message AnchorPointOpReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4255;
}
enum AnchorPointOpType {
ANCHOR_POINT_OP_NONE = 0;
ANCHOR_POINT_OP_TELEPORT = 1;
ANCHOR_POINT_OP_REMOVE = 2;
}
uint32 anchor_point_op_type = 1;
uint32 anchor_point_id = 2;
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message AnchorPointOpRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4274;
}
int32 retcode = 1;
uint32 anchor_point_op_type = 2;
uint32 anchor_point_id = 3;
}

View File

@ -0,0 +1,10 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message ClientCollectorData {
uint32 material_id = 1;
uint32 max_points = 2;
uint32 curr_points = 3;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "ClientCollectorData.proto";
message ClientCollectorDataNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4280;
}
repeated ClientCollectorData client_collector_data_list = 1;
}

View File

@ -0,0 +1,16 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message GetWidgetSlotReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4285;
}
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetSlotData.proto";
message GetWidgetSlotRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4291;
}
int32 retcode = 1;
repeated WidgetSlotData slot_list = 2;
}

8
proto/LunchBoxData.proto Normal file
View File

@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message LunchBoxData {
map<uint32, uint32> slot_material_map = 1;
}

View File

@ -0,0 +1,9 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
enum LunchBoxSlotType {
LUNCH_BOX_SLOT_NONE = 0;
LUNCH_BOX_SLOT_REVIVE = 1;
LUNCH_BOX_SLOT_HEAL = 2;
}

View File

@ -0,0 +1,15 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "Vector.proto";
message OneoffGatherPointDetectorData {
uint32 material_id = 1;
bool is_all_collected = 2;
bool is_hint_valid = 3;
Vector hint_center_pos = 4;
uint32 hint_radius = 5;
uint32 group_id = 6;
uint32 config_id = 7;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "OneoffGatherPointDetectorData.proto";
message OneoffGatherPointDetectorDataNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4288;
}
repeated OneoffGatherPointDetectorData oneoff_gather_point_detector_data_list = 1;
}

View File

@ -0,0 +1,26 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetCreateLocationInfo.proto";
import "WidgetCameraInfo.proto";
import "WidgetCreatorInfo.proto";
import "WidgetThunderBirdFeatherInfo.proto";
message QuickUseWidgetReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4300;
}
oneof Param {
WidgetCreateLocationInfo location_info = 20;
WidgetCameraInfo camera_info = 21;
WidgetCreatorInfo creator_info = 22;
WidgetThunderBirdFeatherInfo thunder_bird_feather_info = 23;
}
}

View File

@ -0,0 +1,21 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "OneoffGatherPointDetectorData.proto";
import "ClientCollectorData.proto";
message QuickUseWidgetRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4258;
}
int32 retcode = 1;
uint32 material_id = 2;
OneoffGatherPointDetectorData detector_data = 3;
ClientCollectorData client_collector_data = 4;
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "LunchBoxData.proto";
message SetUpLunchBoxWidgetReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4265;
}
LunchBoxData lunch_box_data = 1;
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "LunchBoxData.proto";
message SetUpLunchBoxWidgetRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4297;
}
int32 retcode = 1;
LunchBoxData lunch_box_data = 2;
}

View File

@ -0,0 +1,21 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetSlotOp.proto";
import "WidgetSlotTag.proto";
message SetWidgetSlotReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4269;
}
WidgetSlotOp op = 2;
repeated WidgetSlotTag tag_list = 3;
uint32 material_id = 4;
}

View File

@ -0,0 +1,21 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetSlotOp.proto";
import "WidgetSlotTag.proto";
message SetWidgetSlotRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4279;
}
int32 retcode = 1;
WidgetSlotOp op = 2;
repeated WidgetSlotTag tag_list = 3;
uint32 material_id = 4;
}

View File

@ -0,0 +1,20 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "Vector.proto";
message UseWidgetCreateGadgetReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4276;
}
uint32 material_id = 1;
Vector pos = 2;
Vector rot = 3;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message UseWidgetCreateGadgetRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4270;
}
int32 retcode = 1;
uint32 material_id = 2;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message UseWidgetRetractGadgetReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4273;
}
uint32 entity_id = 1;
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message UseWidgetRetractGadgetRsp {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4266;
}
int32 retcode = 1;
uint32 entity_id = 2;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetSlotData.proto";
message WidgetActiveChangeNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4264;
}
repeated WidgetSlotData widget_data_list = 1;
}

View File

@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetCameraInfo {
uint32 target_entity_id = 1;
}

View File

@ -0,0 +1,10 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetCoolDownData {
uint32 id = 1;
uint64 cool_down_time = 2;
bool is_success = 3;
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetCoolDownData.proto";
message WidgetCoolDownNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4298;
}
repeated WidgetCoolDownData group_cool_down_data_list = 1;
repeated WidgetCoolDownData normal_cool_down_data_list = 2;
}

View File

@ -0,0 +1,10 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "Vector.proto";
message WidgetCreateLocationInfo {
Vector pos = 1;
Vector rot = 2;
}

View File

@ -0,0 +1,12 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetCreatorOpType.proto";
import "WidgetCreateLocationInfo.proto";
message WidgetCreatorInfo {
WidgetCreatorOpType op_type = 1;
uint32 entity_id = 2;
WidgetCreateLocationInfo location_info = 3;
}

View File

@ -0,0 +1,9 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
enum WidgetCreatorOpType {
WIDGET_CREATOR_TYPE_NONE = 0;
WIDGET_CREATOR_TYPE_RETRACT = 1;
WIDGET_CREATOR_TYPE_RETRACT_AND_CREATE = 2;
}

View File

@ -0,0 +1,23 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetCreateLocationInfo.proto";
import "WidgetCreatorInfo.proto";
message WidgetDoBagReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4290;
}
oneof OpInfo {
WidgetCreateLocationInfo location_info = 20;
WidgetCreatorInfo widget_creator_info = 21;
}
uint32 material_id = 1;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetDoBagRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4271;
}
int32 retcode = 1;
uint32 material_id = 2;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetGadgetData.proto";
message WidgetGadgetAllDataNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4262;
}
repeated WidgetGadgetData widget_gadget_data = 1;
}

View File

@ -0,0 +1,9 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetGadgetData {
uint32 gadget_id = 1;
repeated uint32 gadget_entity_id_list = 3;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetGadgetData.proto";
message WidgetGadgetDataNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4256;
}
WidgetGadgetData widget_gadget_data = 1;
}

View File

@ -0,0 +1,16 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetGadgetDestroyNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4268;
}
uint32 entity_id = 1;
}

View File

@ -0,0 +1,19 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetReportReq {
enum CmdId {
option allow_alias = true;
ENET_CHANNEL_ID = 0;
NONE = 0;
ENET_IS_RELIABLE = 1;
IS_ALLOW_CLIENT = 1;
CMD_ID = 4294;
}
uint32 material_id = 1;
bool is_clear_hint = 2;
bool is_client_collect = 3;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetReportRsp {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4259;
}
int32 retcode = 1;
uint32 material_id = 2;
}

View File

@ -0,0 +1,19 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetSlotOp.proto";
import "WidgetSlotData.proto";
message WidgetSlotChangeNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4289;
}
WidgetSlotOp op = 1;
WidgetSlotData slot = 2;
}

View File

@ -0,0 +1,12 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
import "WidgetSlotTag.proto";
message WidgetSlotData {
WidgetSlotTag tag = 1;
uint32 material_id = 2;
uint32 cd_over_time = 3;
bool is_active = 4;
}

8
proto/WidgetSlotOp.proto Normal file
View File

@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
enum WidgetSlotOp {
ATTACH = 0;
DETACH = 1;
}

View File

@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
enum WidgetSlotTag {
WIDGET_SLOT_QUICK_USE = 0;
WIDGET_SLOT_ATTACH_AVATAR = 1;
}

View File

@ -0,0 +1,7 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetSlotTagComparer {
}

View File

@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetThunderBirdFeatherInfo {
repeated uint32 entity_id_list = 1;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "emu.grasscutter.net.proto";
message WidgetUseAttachAbilityGroupChangeNotify {
enum CmdId {
option allow_alias = true;
NONE = 0;
ENET_CHANNEL_ID = 0;
ENET_IS_RELIABLE = 1;
CMD_ID = 4292;
}
uint32 material_id = 1;
bool is_attach = 2;
}

View File

@ -4,6 +4,7 @@ import java.io.*;
import java.util.Calendar; import java.util.Calendar;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.PluginManager;
import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.scripts.ScriptLoader;
@ -111,6 +112,9 @@ public final class Grasscutter {
// Create plugin manager instance. // Create plugin manager instance.
pluginManager = new PluginManager(); pluginManager = new PluginManager();
// TODO: find a better place?
StaminaManager.initialize();
// Start servers. // Start servers.
var runMode = SERVER.runMode; var runMode = SERVER.runMode;
if (runMode == ServerRunMode.HYBRID) { if (runMode == ServerRunMode.HYBRID) {

View File

@ -13,6 +13,7 @@ import com.google.gson.reflect.TypeToken;
import com.sun.nio.file.SensitivityWatchEventModifier; import com.sun.nio.file.SensitivityWatchEventModifier;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
@ -127,15 +128,10 @@ public class GachaManager {
} }
// Spend currency // Spend currency
if (banner.getCostItem() > 0) { if (banner.getCostItem() > 0 && !player.getInventory().payItem(banner.getCostItem(), times)) {
GameItem costItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(banner.getCostItem());
if (costItem == null || costItem.getCount() < times) {
return; return;
} }
player.getInventory().removeItem(costItem, times);
}
// Roll // Roll
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner); PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
IntList wonItems = new IntArrayList(times); IntList wonItems = new IntArrayList(times);

View File

@ -7,6 +7,7 @@ import java.util.List;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.def.AvatarCostumeData; import emu.grasscutter.data.def.AvatarCostumeData;
import emu.grasscutter.data.def.AvatarData; import emu.grasscutter.data.def.AvatarData;
import emu.grasscutter.data.def.AvatarFlycloakData; import emu.grasscutter.data.def.AvatarFlycloakData;
@ -257,6 +258,64 @@ public class Inventory implements Iterable<GameItem> {
} }
} }
private int getVirtualItemCount(int itemId) {
switch (itemId) {
case 201: // Primogem
return player.getPrimogems();
case 202: // Mora
return player.getMora();
case 203: // Genesis Crystals
return player.getCrystals();
default:
GameItem item = getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId); // What if we ever want to operate on weapons/relics/furniture? :S
return (item == null) ? 0 : item.getCount();
}
}
public boolean payItem(int id, int count) {
return payItem(new ItemParamData(id, count));
}
public boolean payItem(ItemParamData costItem) {
return payItems(new ItemParamData[] {costItem}, 1, null);
}
public boolean payItems(ItemParamData[] costItems) {
return payItems(costItems, 1, null);
}
public boolean payItems(ItemParamData[] costItems, int quantity) {
return payItems(costItems, quantity, null);
}
public synchronized boolean payItems(ItemParamData[] costItems, int quantity, ActionReason reason) {
// Make sure player has requisite items
for (ItemParamData cost : costItems) {
if (getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) {
return false;
}
}
// All costs are satisfied, now remove them all
for (ItemParamData cost : costItems) {
switch (cost.getId()) {
case 201 -> // Primogem
player.setPrimogems(player.getPrimogems() - (cost.getCount() * quantity));
case 202 -> // Mora
player.setMora(player.getMora() - (cost.getCount() * quantity));
case 203 -> // Genesis Crystals
player.setCrystals(player.getCrystals() - (cost.getCount() * quantity));
default ->
removeItem(getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()), cost.getCount() * quantity);
}
}
if (reason != null) { // Do we need these?
// getPlayer().sendPacket(new PacketItemAddHintNotify(changedItems, reason));
}
// getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems));
return true;
}
public void removeItems(List<GameItem> items) { public void removeItems(List<GameItem> items) {
// TODO Bulk delete // TODO Bulk delete
for (GameItem item : items) { for (GameItem item : items) {

View File

@ -1,6 +1,7 @@
package emu.grasscutter.game.managers; package emu.grasscutter.game.managers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -38,6 +39,8 @@ public class InventoryManager {
private final static int RELIC_MATERIAL_1 = 105002; // Sanctifying Unction private final static int RELIC_MATERIAL_1 = 105002; // Sanctifying Unction
private final static int RELIC_MATERIAL_2 = 105003; // Sanctifying Essence private final static int RELIC_MATERIAL_2 = 105003; // Sanctifying Essence
private final static int RELIC_MATERIAL_EXP_1 = 2500; // Sanctifying Unction
private final static int RELIC_MATERIAL_EXP_2 = 10000; // Sanctifying Essence
private final static int WEAPON_ORE_1 = 104011; // Enhancement Ore private final static int WEAPON_ORE_1 = 104011; // Enhancement Ore
private final static int WEAPON_ORE_2 = 104012; // Fine Enhancement Ore private final static int WEAPON_ORE_2 = 104012; // Fine Enhancement Ore
@ -85,6 +88,7 @@ public class InventoryManager {
int moraCost = 0; int moraCost = 0;
int expGain = 0; int expGain = 0;
List<GameItem> foodRelics = new ArrayList<GameItem>();
for (long guid : foodRelicList) { for (long guid : foodRelicList) {
// Add to delete queue // Add to delete queue
GameItem food = player.getInventory().getItemByGuid(guid); GameItem food = player.getInventory().getItemByGuid(guid);
@ -96,23 +100,21 @@ public class InventoryManager {
expGain += food.getItemData().getBaseConvExp(); expGain += food.getItemData().getBaseConvExp();
// Feeding artifact with exp already // Feeding artifact with exp already
if (food.getTotalExp() > 0) { if (food.getTotalExp() > 0) {
expGain += (int) Math.floor(food.getTotalExp() * .8f); expGain += (food.getTotalExp() * 4) / 5;
} }
foodRelics.add(food);
} }
List<ItemParamData> payList = new ArrayList<ItemParamData>();
for (ItemParam itemParam : list) { for (ItemParam itemParam : list) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemParam.getItemId()); int amount = itemParam.getCount(); // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_RELIQUARY_MATERIAL) { int gain = amount * switch(itemParam.getItemId()) {
continue; case RELIC_MATERIAL_1 -> RELIC_MATERIAL_EXP_1;
} case RELIC_MATERIAL_2 -> RELIC_MATERIAL_EXP_2;
int amount = Math.min(food.getCount(), itemParam.getCount()); default -> 0;
int gain = 0; };
if (food.getItemId() == RELIC_MATERIAL_2) {
gain = 10000 * amount;
} else if (food.getItemId() == RELIC_MATERIAL_1) {
gain = 2500 * amount;
}
expGain += gain; expGain += gain;
moraCost += gain; moraCost += gain;
payList.add(new ItemParamData(itemParam.getItemId(), itemParam.getCount()));
} }
// Make sure exp gain is valid // Make sure exp gain is valid
@ -120,28 +122,14 @@ public class InventoryManager {
return; return;
} }
// Check mora // Confirm payment of materials and mora (assume food relics are payable afterwards)
if (player.getMora() < moraCost) { payList.add(new ItemParamData(202, moraCost));
if (!player.getInventory().payItems(payList.toArray(new ItemParamData[0]))) {
return; return;
} }
player.setMora(player.getMora() - moraCost);
// Consume food items // Consume food relics
for (long guid : foodRelicList) { player.getInventory().removeItems(foodRelics);
GameItem food = player.getInventory().getItemByGuid(guid);
if (food == null || !food.isDestroyable()) {
continue;
}
player.getInventory().removeItem(food);
}
for (ItemParam itemParam : list) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemParam.getItemId());
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_RELIQUARY_MATERIAL) {
continue;
}
int amount = Math.min(food.getCount(), itemParam.getCount());
player.getInventory().removeItem(food, amount);
}
// Implement random rate boost // Implement random rate boost
int rate = 1; int rate = 1;
@ -231,22 +219,16 @@ public class InventoryManager {
} }
expGain += food.getItemData().getWeaponBaseExp(); expGain += food.getItemData().getWeaponBaseExp();
if (food.getTotalExp() > 0) { if (food.getTotalExp() > 0) {
expGain += (int) Math.floor(food.getTotalExp() * .8f); expGain += (food.getTotalExp() * 4) / 5;
} }
} }
for (ItemParam param : itemParamList) { for (ItemParam param : itemParamList) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId()); expGain += param.getCount() * switch(param.getItemId()) {
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) { case WEAPON_ORE_1 -> WEAPON_ORE_EXP_1;
continue; case WEAPON_ORE_2 -> WEAPON_ORE_EXP_2;
} case WEAPON_ORE_3 -> WEAPON_ORE_EXP_3;
int amount = Math.min(param.getCount(), food.getCount()); default -> 0;
if (food.getItemId() == WEAPON_ORE_3) { };
expGain += 10000 * amount;
} else if (food.getItemId() == WEAPON_ORE_2) {
expGain += 2000 * amount;
} else if (food.getItemId() == WEAPON_ORE_1) {
expGain += 400 * amount;
}
} }
// Try // Try
@ -288,65 +270,45 @@ public class InventoryManager {
} }
// Get exp gain // Get exp gain
int expGain = 0, moraCost = 0; int expGain = 0, expGainFree = 0;
List<GameItem> foodWeapons = new ArrayList<GameItem>();
for (long guid : foodWeaponGuidList) { for (long guid : foodWeaponGuidList) {
GameItem food = player.getInventory().getItemByGuid(guid); GameItem food = player.getInventory().getItemByGuid(guid);
if (food == null || !food.isDestroyable()) { if (food == null || !food.isDestroyable()) {
continue; continue;
} }
expGain += food.getItemData().getWeaponBaseExp(); expGain += food.getItemData().getWeaponBaseExp();
moraCost += (int) Math.floor(food.getItemData().getWeaponBaseExp() * .1f);
if (food.getTotalExp() > 0) { if (food.getTotalExp() > 0) {
expGain += (int) Math.floor(food.getTotalExp() * .8f); expGainFree += (food.getTotalExp() * 4) / 5; // No tax :D
} }
foodWeapons.add(food);
} }
List<ItemParamData> payList = new ArrayList<ItemParamData>();
for (ItemParam param : itemParamList) { for (ItemParam param : itemParamList) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId()); int amount = param.getCount(); // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) { int gain = amount * switch(param.getItemId()) {
continue; case WEAPON_ORE_1 -> WEAPON_ORE_EXP_1;
} case WEAPON_ORE_2 -> WEAPON_ORE_EXP_2;
int amount = Math.min(param.getCount(), food.getCount()); case WEAPON_ORE_3 -> WEAPON_ORE_EXP_3;
int gain = 0; default -> 0;
if (food.getItemId() == WEAPON_ORE_3) { };
gain = 10000 * amount;
} else if (food.getItemId() == WEAPON_ORE_2) {
gain = 2000 * amount;
} else if (food.getItemId() == WEAPON_ORE_1) {
gain = 400 * amount;
}
expGain += gain; expGain += gain;
moraCost += (int) Math.floor(gain * .1f); payList.add(new ItemParamData(param.getItemId(), amount));
} }
// Make sure exp gain is valid // Make sure exp gain is valid
int moraCost = expGain / 10;
expGain += expGainFree;
if (expGain <= 0) { if (expGain <= 0) {
return; return;
} }
// Mora check // Confirm payment of materials and mora (assume food weapons are payable afterwards)
if (player.getMora() >= moraCost) { payList.add(new ItemParamData(202, moraCost));
player.setMora(player.getMora() - moraCost); if (!player.getInventory().payItems(payList.toArray(new ItemParamData[0]))) {
} else {
return; return;
} }
player.getInventory().removeItems(foodWeapons);
// Consume weapon/items used to feed
for (long guid : foodWeaponGuidList) {
GameItem food = player.getInventory().getItemByGuid(guid);
if (food == null || !food.isDestroyable()) {
continue;
}
player.getInventory().removeItem(food);
}
for (ItemParam param : itemParamList) {
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId());
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) {
continue;
}
int amount = Math.min(param.getCount(), food.getCount());
player.getInventory().removeItem(food, amount);
}
// Level up // Level up
int maxLevel = promoteData.getUnlockMaxLevel(); int maxLevel = promoteData.getUnlockMaxLevel();
@ -393,7 +355,7 @@ public class InventoryManager {
player.sendPacket(new PacketWeaponUpgradeRsp(weapon, oldLevel, leftovers)); player.sendPacket(new PacketWeaponUpgradeRsp(weapon, oldLevel, leftovers));
} }
private List<ItemParam> getLeftoverOres(float leftover) { private List<ItemParam> getLeftoverOres(int leftover) {
List<ItemParam> leftoverOreList = new ArrayList<>(3); List<ItemParam> leftoverOreList = new ArrayList<>(3);
if (leftover < WEAPON_ORE_EXP_1) { if (leftover < WEAPON_ORE_EXP_1) {
@ -401,11 +363,11 @@ public class InventoryManager {
} }
// Get leftovers // Get leftovers
int ore3 = (int) Math.floor(leftover / WEAPON_ORE_EXP_3); int ore3 = leftover / WEAPON_ORE_EXP_3;
leftover = leftover % WEAPON_ORE_EXP_3; leftover = leftover % WEAPON_ORE_EXP_3;
int ore2 = (int) Math.floor(leftover / WEAPON_ORE_EXP_2); int ore2 = leftover / WEAPON_ORE_EXP_2;
leftover = leftover % WEAPON_ORE_EXP_2; leftover = leftover % WEAPON_ORE_EXP_2;
int ore1 = (int) Math.floor(leftover / WEAPON_ORE_EXP_1); int ore1 = leftover / WEAPON_ORE_EXP_1;
if (ore3 > 0) { if (ore3 > 0) {
leftoverOreList.add(ItemParam.newBuilder().setItemId(WEAPON_ORE_3).setCount(ore3).build()); leftoverOreList.add(ItemParam.newBuilder().setItemId(WEAPON_ORE_3).setCount(ore3).build());
@ -496,26 +458,15 @@ public class InventoryManager {
return; return;
} }
// Make sure player has promote items // Pay materials and mora if possible
for (ItemParamData cost : nextPromoteData.getCostItems()) { ItemParamData[] costs = nextPromoteData.getCostItems(); // Can this be null?
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()); if (nextPromoteData.getCoinCost() > 0) {
if (feedItem == null || feedItem.getCount() < cost.getCount()) { costs = Arrays.copyOf(costs, costs.length + 1);
costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
}
if (!player.getInventory().payItems(costs)) {
return; return;
} }
}
// Mora check
if (player.getMora() >= nextPromoteData.getCoinCost()) {
player.setMora(player.getMora() - nextPromoteData.getCoinCost());
} else {
return;
}
// Consume promote filler items
for (ItemParamData cost : nextPromoteData.getCostItems()) {
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
player.getInventory().removeItem(feedItem, cost.getCount());
}
int oldPromoteLevel = weapon.getPromoteLevel(); int oldPromoteLevel = weapon.getPromoteLevel();
weapon.setPromoteLevel(nextPromoteLevel); weapon.setPromoteLevel(nextPromoteLevel);
@ -552,26 +503,15 @@ public class InventoryManager {
return; return;
} }
// Make sure player has cost items // Pay materials and mora if possible
for (ItemParamData cost : nextPromoteData.getCostItems()) { ItemParamData[] costs = nextPromoteData.getCostItems(); // Can this be null?
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()); if (nextPromoteData.getCoinCost() > 0) {
if (feedItem == null || feedItem.getCount() < cost.getCount()) { costs = Arrays.copyOf(costs, costs.length + 1);
costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
}
if (!player.getInventory().payItems(costs)) {
return; return;
} }
}
// Mora check
if (player.getMora() >= nextPromoteData.getCoinCost()) {
player.setMora(player.getMora() - nextPromoteData.getCoinCost());
} else {
return;
}
// Consume promote filler items
for (ItemParamData cost : nextPromoteData.getCostItems()) {
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
player.getInventory().removeItem(feedItem, cost.getCount());
}
// Update promote level // Update promote level
avatar.setPromoteLevel(nextPromoteLevel); avatar.setPromoteLevel(nextPromoteLevel);
@ -616,34 +556,25 @@ public class InventoryManager {
return; return;
} }
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId);
if (feedItem == null || feedItem.getItemData().getMaterialType() != MaterialType.MATERIAL_EXP_FRUIT || feedItem.getCount() < count) {
return;
}
// Calc exp // Calc exp
int expGain = 0, moraCost = 0; int expGain = switch(itemId) {
case AVATAR_BOOK_1 -> AVATAR_BOOK_EXP_1 * count;
case AVATAR_BOOK_2 -> AVATAR_BOOK_EXP_2 * count;
case AVATAR_BOOK_3 -> AVATAR_BOOK_EXP_3 * count;
default -> 0;
};
// TODO clean up // Sanity check
if (itemId == AVATAR_BOOK_3) { if (expGain <= 0) {
expGain = AVATAR_BOOK_EXP_3 * count;
} else if (itemId == AVATAR_BOOK_2) {
expGain = AVATAR_BOOK_EXP_2 * count;
} else if (itemId == AVATAR_BOOK_1) {
expGain = AVATAR_BOOK_EXP_1 * count;
}
moraCost = (int) Math.floor(expGain * .2f);
// Mora check
if (player.getMora() >= moraCost) {
player.setMora(player.getMora() - moraCost);
} else {
return; return;
} }
// Consume items // Payment check
player.getInventory().removeItem(feedItem, count); int moraCost = expGain / 5;
ItemParamData[] costItems = new ItemParamData[] {new ItemParamData(itemId, count), new ItemParamData(202, moraCost)};
if (!player.getInventory().payItems(costItems)) {
return;
}
// Level up // Level up
upgradeAvatar(player, avatar, promoteData, expGain); upgradeAvatar(player, avatar, promoteData, expGain);
@ -764,32 +695,14 @@ public class InventoryManager {
return; return;
} }
// Make sure player has cost items // Pay materials and mora if possible
for (ItemParamData cost : proudSkill.getCostItems()) { List<ItemParamData> costs = new ArrayList<ItemParamData>(proudSkill.getCostItems()); // Can this be null?
if (cost.getId() == 0) { if (proudSkill.getCoinCost() > 0) {
continue; costs.add(new ItemParamData(202, proudSkill.getCoinCost()));
} }
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()); if (!player.getInventory().payItems(costs.toArray(new ItemParamData[0]))) {
if (feedItem == null || feedItem.getCount() < cost.getCount()) {
return; return;
} }
}
// Mora check
if (player.getMora() >= proudSkill.getCoinCost()) {
player.setMora(player.getMora() - proudSkill.getCoinCost());
} else {
return;
}
// Consume promote filler items
for (ItemParamData cost : proudSkill.getCostItems()) {
if (cost.getId() == 0) {
continue;
}
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
player.getInventory().removeItem(feedItem, cost.getCount());
}
// Upgrade skill // Upgrade skill
avatar.getSkillLevelMap().put(skillId, nextLevel); avatar.getSkillLevelMap().put(skillId, nextLevel);
@ -822,14 +735,11 @@ public class InventoryManager {
return; return;
} }
GameItem costItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(talentData.getMainCostItemId()); // Pay constellation item if possible
if (costItem == null || costItem.getCount() < talentData.getMainCostItemCount()) { if (!player.getInventory().payItem(talentData.getMainCostItemId(), 1)) {
return; return;
} }
// Consume item
player.getInventory().removeItem(costItem, talentData.getMainCostItemCount());
// Apply + recalc // Apply + recalc
avatar.getTalentIdList().add(talentData.getId()); avatar.getTalentIdList().add(talentData.getId());
avatar.setCoreProudSkillLevel(currentTalentLevel + 1); avatar.setCoreProudSkillLevel(currentTalentLevel + 1);

View File

@ -8,5 +8,5 @@ public interface AfterUpdateStaminaListener {
* @param reason Why updating stamina. * @param reason Why updating stamina.
* @param newStamina New Stamina value. * @param newStamina New Stamina value.
*/ */
void onAfterUpdateStamina(String reason, int newStamina); void onAfterUpdateStamina(String reason, int newStamina, boolean isCharacterStamina);
} }

View File

@ -8,7 +8,7 @@ public interface BeforeUpdateStaminaListener {
* @param newStamina New ABSOLUTE stamina value. * @param newStamina New ABSOLUTE stamina value.
* @return true if you want to cancel this update, otherwise false. * @return true if you want to cancel this update, otherwise false.
*/ */
int onBeforeUpdateStamina(String reason, int newStamina); int onBeforeUpdateStamina(String reason, int newStamina, boolean isCharacterStamina);
/** /**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina. * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update. * This gives listeners a chance to intercept this update.
@ -16,5 +16,5 @@ public interface BeforeUpdateStaminaListener {
* @param consumption ConsumptionType and RELATIVE stamina change amount. * @param consumption ConsumptionType and RELATIVE stamina change amount.
* @return true if you want to cancel this update, otherwise false. * @return true if you want to cancel this update, otherwise false.
*/ */
Consumption onBeforeUpdateStamina(String reason, Consumption consumption); Consumption onBeforeUpdateStamina(String reason, Consumption consumption, boolean isCharacterStamina);
} }

View File

@ -13,18 +13,19 @@ public enum ConsumptionType {
// Slow swimming is handled per movement, not per second. // Slow swimming is handled per movement, not per second.
// Arm movement frequency depends on gender/age/height. // Arm movement frequency depends on gender/age/height.
// TODO: Instead of cost -80 per tick, find a proper way to calculate cost. // TODO: Instead of cost -80 per tick, find a proper way to calculate cost.
SKIFF(-300), // TODO: Get real value SKIFF_DASH(-204),
SPRINT(-1800), SPRINT(-1800),
SWIM_DASH_START(-20), SWIM_DASH_START(-2000),
SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick
SWIMMING(-80), SWIMMING(-80),
TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick
TALENT_DASH_START(-1000), TALENT_DASH_START(-1000),
// restore // restore
POWERED_FLY(500), // TODO: Get real value POWERED_FLY(500),
POWERED_SKIFF(2000), // TODO: Get real value POWERED_SKIFF(500),
RUN(500), RUN(500),
SKIFF(500),
STANDBY(500), STANDBY(500),
WALK(500); WALK(500);

View File

@ -2,6 +2,7 @@ package emu.grasscutter.game.managers.StaminaManager;
import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.Logger;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
@ -13,21 +14,21 @@ import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
import emu.grasscutter.net.proto.VectorOuterClass.Vector; import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.Math;
import java.util.*; import java.util.*;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.GAME_OPTIONS;
public class StaminaManager { public class StaminaManager {
// TODO: Skiff state detection? // TODO: Skiff state detection?
private final Player player; private final Player player;
private final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{ private static final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{
put("CLIMB", new HashSet<>(List.of( put("CLIMB", new HashSet<>(List.of(
MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover
MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY
@ -48,7 +49,7 @@ public class StaminaManager {
))); )));
put("SKIFF", new HashSet<>(List.of( put("SKIFF", new HashSet<>(List.of(
MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding
MotionState.MOTION_SKIFF_DASH, // NOT OBSERVED even when dashing MotionState.MOTION_SKIFF_DASH, // sustained, observed with waverider entity ID.
MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing
MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover
))); )));
@ -108,7 +109,8 @@ public class StaminaManager {
}}; }};
private final Logger logger = Grasscutter.getLogger(); private final Logger logger = Grasscutter.getLogger();
public final static int GlobalMaximumStamina = 24000; public final static int GlobalCharacterMaximumStamina = 24000;
public final static int GlobalVehicleMaxStamina = 24000;
private Position currentCoordinates = new Position(0, 0, 0); private Position currentCoordinates = new Position(0, 0, 0);
private Position previousCoordinates = new Position(0, 0, 0); private Position previousCoordinates = new Position(0, 0, 0);
private MotionState currentState = MotionState.MOTION_STANDBY; private MotionState currentState = MotionState.MOTION_STANDBY;
@ -122,74 +124,58 @@ public class StaminaManager {
private int lastSkillId = 0; private int lastSkillId = 0;
private int lastSkillCasterId = 0; private int lastSkillCasterId = 0;
private boolean lastSkillFirstTick = true; private boolean lastSkillFirstTick = true;
public static final HashSet<Integer> TalentMovements = new HashSet<>(List.of( private int vehicleId = -1;
10013, // Kamisato Ayaka private int vehicleStamina = GlobalVehicleMaxStamina;
10413 // Mona private static final HashSet<Integer> TalentMovements = new HashSet<>(List.of(
10013, 10413
)); ));
private static final HashMap<Integer, Float> ClimbFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> DashFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> FlyFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> SwimFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> ClimbTalentReductionMap = new HashMap<>() {{
put(262301, 0.8f);
}};
private static final HashMap<Integer, Float> FlyTalentReductionMap = new HashMap<>() {{
put(212301, 0.8f);
put(222301, 0.8f);
}};
private static final HashMap<Integer, Float> SwimTalentReductionMap = new HashMap<>() {{
put(242301, 0.8f);
put(542301, 0.8f);
}};
// TODO: Get from somewhere else, instead of hard-coded here? public static final HashSet<Integer> BowAvatars = new HashSet<>();
public static final HashSet<Integer> ClaymoreSkills = new HashSet<>(List.of( public static final HashSet<Integer> CatalystAvatars = new HashSet<>();
10160, // Diluc, /=2 public static final HashSet<Integer> ClaymoreAvatars = new HashSet<>();
10201, // Razor public static final HashSet<Integer> PolearmAvatars = new HashSet<>();
10241, // Beidou public static final HashSet<Integer> SwordAvatars = new HashSet<>();
10341, // Noelle
10401, // Chongyun
10441, // Xinyan
10511, // Eula
10531, // Sayu
10571 // Arataki Itto, = 0
));
public static final HashSet<Integer> CatalystSkills = new HashSet<>(List.of(
10060, // Lisa
10070, // Barbara
10271, // Ningguang
10291, // Klee
10411, // Mona
10431, // Sucrose
10481, // Yanfei
10541, // Sangonomoiya Kokomi
10581 // Yae Miko
));
public static final HashSet<Integer> PolearmSkills = new HashSet<>(List.of(
10231, // Xiangling
10261, // Xiao
10301, // Zhongli
10451, // Rosaria
10461, // Hu Tao
10501, // Thoma
10521, // Raiden Shogun
10631, // Shenhe
10641 // Yunjin
));
public static final HashSet<Integer> SwordSkills = new HashSet<>(List.of(
10024, // Kamisato Ayaka
10031, // Jean
10073, // Kaeya
10321, // Bennett
10337, // Tartaglia, melee stance (10332 switch to melee, 10336 switch to ranged stance)
10351, // Qiqi
10381, // Xingqiu
10386, // Albedo
10421, // Keqing, =-2500
10471, // Kaedehara Kazuha
10661, // Kamisato Ayato
100553, // Lumine
100540 // Aether
));
public static final HashSet<Integer> BowSkills = new HashSet<>(List.of(
10041, 10043, // Amber
10221, 10223,// Venti
10311, 10315, // Fischl
10331, 10335, // Tartaglia, ranged stance
10371, // Ganyu
10391, 10394, // Diona
10491, // Yoimiya
10551, 10554, // Gorou
10561, 10564, // Kojou Sara
10621, // Aloy
99998, 99999 // Yelan // TODO: get real values
));
public static void initialize() {
// Initialize skill categories
GameData.getAvatarDataMap().forEach((avatarId, avatarData) -> {
switch (avatarData.getWeaponType()) {
case "WEAPON_BOW" -> BowAvatars.add(avatarId);
case "WEAPON_CLAYMORE" -> ClaymoreAvatars.add(avatarId);
case "WEAPON_CATALYST" -> CatalystAvatars.add(avatarId);
case "WEAPON_POLE" -> PolearmAvatars.add(avatarId);
case "WEAPON_SWORD_ONE_HAND" -> SwordAvatars.add(avatarId);
}
});
// TODO: Initialize foods etc.
}
public StaminaManager(Player player) { public StaminaManager(Player player) {
this.player = player; this.player = player;
@ -203,6 +189,22 @@ public class StaminaManager {
lastSkillCasterId = skillCasterId; lastSkillCasterId = skillCasterId;
} }
public int getMaxCharacterStamina() {
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
}
public int getCurrentCharacterStamina() {
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
}
public int getMaxVehicleStamina() {
return GlobalVehicleMaxStamina;
}
public int getCurrentVehicleStamina() {
return vehicleStamina;
}
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) { public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
if (beforeUpdateStaminaListeners.containsKey(listenerName)) { if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
return false; return false;
@ -244,67 +246,71 @@ public class StaminaManager {
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3; return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
} }
public int updateStaminaRelative(GameSession session, Consumption consumption) { public int updateStaminaRelative(GameSession session, Consumption consumption, boolean isCharacterStamina) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
if (consumption.amount == 0) { if (consumption.amount == 0) {
return currentStamina; return currentStamina;
} }
// notify will update // notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption); Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption, isCharacterStamina);
if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) { if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) {
logger.debug("[StaminaManager] Stamina update relative(" + logger.debug("Stamina update relative(" +
consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" + consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" +
consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
return currentStamina; return currentStamina;
} }
} }
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
logger.trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + logger.trace((isCharacterStamina ? "C " : "V ") + currentStamina + "/" + maxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," +
consumption.amount + ")"); consumption.amount + ")");
int newStamina = currentStamina + consumption.amount; int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} else if (newStamina > playerMaxStamina) { } else if (newStamina > maxStamina) {
newStamina = playerMaxStamina; newStamina = maxStamina;
} }
return setStamina(session, consumption.type.toString(), newStamina); return setStamina(session, consumption.type.toString(), newStamina, isCharacterStamina);
} }
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) { public int updateStaminaAbsolute(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
// notify will update // notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina); int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina, isCharacterStamina);
if (overriddenNewStamina != newStamina) { if (overriddenNewStamina != newStamina) {
logger.debug("[StaminaManager] Stamina update absolute(" + logger.debug("Stamina update absolute(" +
reason + ", " + newStamina + ") overridden to absolute(" + reason + ", " + newStamina + ") overridden to absolute(" +
reason + ", " + newStamina + ") by: " + listener.getKey()); reason + ", " + newStamina + ") by: " + listener.getKey());
return currentStamina; return currentStamina;
} }
} }
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} else if (newStamina > playerMaxStamina) { } else if (newStamina > maxStamina) {
newStamina = playerMaxStamina; newStamina = maxStamina;
} }
return setStamina(session, reason, newStamina); return setStamina(session, reason, newStamina, isCharacterStamina);
} }
// Returns new stamina and sends PlayerPropNotify // Returns new stamina and sends PlayerPropNotify or VehicleStaminaNotify
public int setStamina(GameSession session, String reason, int newStamina) { public int setStamina(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
if (!GAME_OPTIONS.staminaUsage) { if (!GAME_OPTIONS.staminaUsage) {
newStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); newStamina = getMaxCharacterStamina();
} }
// set stamina if is character stamina
// set stamina if (isCharacterStamina) {
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
} else {
vehicleStamina = newStamina;
session.send(new PacketVehicleStaminaNotify(vehicleId, ((float) newStamina) / 100));
}
// notify updated // notify updated
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
listener.getValue().onAfterUpdateStamina(reason, newStamina); listener.getValue().onAfterUpdateStamina(reason, newStamina, isCharacterStamina);
} }
return newStamina; return newStamina;
} }
@ -343,22 +349,23 @@ public class StaminaManager {
// External trigger handler // External trigger handler
public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
// Ignore if skill not cast by not current active // Ignore if skill not cast by not current active avatar
if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) { if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) {
return; return;
} }
setSkillCast(skillId, casterId); setSkillCast(skillId, casterId);
// Handle immediate stamina cost // Handle immediate stamina cost
if (ClaymoreSkills.contains(skillId)) { int currentAvatarId = player.getTeamManager().getCurrentAvatarEntity().getAvatar().getAvatarId();
if (ClaymoreAvatars.contains(currentAvatarId)) {
// Exclude claymore as their stamina cost starts when MixinStaminaCost gets in // Exclude claymore as their stamina cost starts when MixinStaminaCost gets in
return; return;
} }
// TODO: Differentiate normal attacks from charged attacks and exclude // TODO: Differentiate normal attacks from charged attacks and exclude
// TODO: Temporary: Exclude non-claymore attacks for now // TODO: Temporary: Exclude non-claymore attacks for now
if (BowSkills.contains(skillId) if (BowAvatars.contains(currentAvatarId)
|| SwordSkills.contains(skillId) || SwordAvatars.contains(currentAvatarId)
|| PolearmSkills.contains(skillId) || PolearmAvatars.contains(currentAvatarId)
|| CatalystSkills.contains(skillId) || CatalystAvatars.contains(currentAvatarId)
) { ) {
return; return;
} }
@ -367,7 +374,7 @@ public class StaminaManager {
public void handleMixinCostStamina(boolean isSwim) { public void handleMixinCostStamina(boolean isSwim) {
// Talent moving and claymore avatar charged attack duration // Talent moving and claymore avatar charged attack duration
// logger.trace("abilityMixinCostStamina: isSwim: " + isSwim); // logger.trace("abilityMixinCostStamina: isSwim: " + isSwim + "\tlastSkill: " + lastSkillId);
if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) { if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) {
handleImmediateStamina(cachedSession, lastSkillId); handleImmediateStamina(cachedSession, lastSkillId);
} }
@ -381,11 +388,11 @@ public class StaminaManager {
MotionState motionState = motionInfo.getState(); MotionState motionState = motionInfo.getState();
int notifyEntityId = entity.getId(); int notifyEntityId = entity.getId();
int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId(); int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId();
if (notifyEntityId != currentAvatarEntityId) { if (notifyEntityId != currentAvatarEntityId && notifyEntityId != vehicleId) {
return; return;
} }
currentState = motionState; currentState = motionState;
// logger.trace("" + currentState); // logger.trace(currentState + "\t" + (notifyEntityId == currentAvatarEntityId ? "character" : "vehicle"));
Vector posVector = motionInfo.getPos(); Vector posVector = motionInfo.getPos();
Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
@ -395,28 +402,40 @@ public class StaminaManager {
handleImmediateStamina(session, motionState); handleImmediateStamina(session, motionState);
} }
public void handleVehicleInteractReq(GameSession session, int vehicleId, VehicleInteractType vehicleInteractType) {
if (vehicleInteractType == VehicleInteractType.VEHICLE_INTERACT_IN) {
this.vehicleId = vehicleId;
// Reset character stamina here to prevent falling into water immediately on ejection if char stamina is
// close to empty when boarding.
updateStaminaAbsolute(session, "board vehicle", getMaxCharacterStamina(), true);
updateStaminaAbsolute(session, "board vehicle", getMaxVehicleStamina(), false);
} else {
this.vehicleId = -1;
}
}
// Internal handler // Internal handler
private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) { private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) {
switch (motionState) { switch (motionState) {
case MOTION_CLIMB: case MOTION_CLIMB:
if (currentState != MotionState.MOTION_CLIMB) { if (currentState != MotionState.MOTION_CLIMB) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START)); updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START), true);
} }
break; break;
case MOTION_DASH_BEFORE_SHAKE: case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT)); updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT), true);
} }
break; break;
case MOTION_CLIMB_JUMP: case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) { if (previousState != MotionState.MOTION_CLIMB_JUMP) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP)); updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP), true);
} }
break; break;
case MOTION_SWIM_DASH: case MOTION_SWIM_DASH:
if (previousState != MotionState.MOTION_SWIM_DASH) { if (previousState != MotionState.MOTION_SWIM_DASH) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START)); updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START), true);
} }
break; break;
} }
@ -424,18 +443,20 @@ public class StaminaManager {
private void handleImmediateStamina(GameSession session, int skillId) { private void handleImmediateStamina(GameSession session, int skillId) {
Consumption consumption = getFightConsumption(skillId); Consumption consumption = getFightConsumption(skillId);
updateStaminaRelative(session, consumption); updateStaminaRelative(session, consumption, true);
} }
private class SustainedStaminaHandler extends TimerTask { private class SustainedStaminaHandler extends TimerTask {
public void run() { public void run() {
boolean moving = isPlayerMoving(); boolean moving = isPlayerMoving();
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentCharacterStamina = getCurrentCharacterStamina();
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxCharacterStamina = getMaxCharacterStamina();
if (moving || (currentStamina < maxStamina)) { int currentVehicleStamina = getCurrentVehicleStamina();
int maxVehicleStamina = getMaxVehicleStamina();
if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) {
logger.trace("Player moving: " + moving + ", stamina full: " + logger.trace("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina"); (currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina");
boolean isCharacterStamina = true;
Consumption consumption; Consumption consumption;
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbConsumption(); consumption = getClimbConsumption();
@ -447,43 +468,44 @@ public class StaminaManager {
consumption = new Consumption(ConsumptionType.RUN); consumption = new Consumption(ConsumptionType.RUN);
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) { } else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
consumption = getSkiffConsumption(); consumption = getSkiffConsumption();
isCharacterStamina = false;
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = new Consumption(ConsumptionType.STANDBY); consumption = new Consumption(ConsumptionType.STANDBY);
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { } else if (MotionStatesCategorized.get("SWIM").contains(currentState)) {
consumption = getSwimConsumptions(); consumption = getSwimConsumptions();
} else if (MotionStatesCategorized.get("WALK").contains((currentState))) { } else if (MotionStatesCategorized.get("WALK").contains(currentState)) {
consumption = new Consumption(ConsumptionType.WALK); consumption = new Consumption(ConsumptionType.WALK);
} else if (MotionStatesCategorized.get("OTHER").contains((currentState))) { } else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) {
consumption = new Consumption();
} else if (MotionStatesCategorized.get("OTHER").contains(currentState)) {
consumption = getOtherConsumptions(); consumption = getOtherConsumptions();
} else { } else { // ignore
// ignore
return; return;
} }
if (consumption.amount < 0) {
/* Do not apply reduction factor when recovering stamina if (consumption.amount < 0 && isCharacterStamina) {
TODO: Reductions that apply to all motion types: // Do not apply reduction factor when recovering stamina
Elemental Resonance if (player.getTeamManager().getTeamResonances().contains(10301)) {
Wind: -15% consumption.amount *= 0.85f;
Skills
Diona E: -10% while shield lasts - applies to SP+MP
Barbara E: -12% while lasts - applies to SP+MP
*/
} }
// Delay 2 seconds before starts recovering stamina }
if (cachedSession != null) { // Delay 1 seconds before starts recovering stamina
if (consumption.amount != 0 && cachedSession != null) {
if (consumption.amount < 0) { if (consumption.amount < 0) {
staminaRecoverDelay = 0; staminaRecoverDelay = 0;
} }
if (consumption.amount > 0 && consumption.type != ConsumptionType.POWERED_FLY) { if (consumption.amount > 0
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. && consumption.type != ConsumptionType.POWERED_FLY
if (staminaRecoverDelay < 10) { && consumption.type != ConsumptionType.POWERED_SKIFF) {
// For others recover after 2 seconds (10 ticks) - as official server does. // For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this.
if (staminaRecoverDelay < 5) {
// For others recover after 1 seconds (5 ticks) - as official server does.
staminaRecoverDelay++; staminaRecoverDelay++;
consumption.amount = 0; consumption.amount = 0;
logger.trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay); logger.trace("Delaying recovery: " + staminaRecoverDelay);
} }
} }
updateStaminaRelative(cachedSession, consumption); updateStaminaRelative(cachedSession, consumption, isCharacterStamina);
} }
} }
previousState = currentState; previousState = currentState;
@ -496,10 +518,11 @@ public class StaminaManager {
} }
private void handleDrowning() { private void handleDrowning() {
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); // TODO: fix drowning waverider entity
int stamina = getCurrentCharacterStamina();
if (stamina < 10) { if (stamina < 10) {
logger.trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + logger.trace(getCurrentCharacterStamina() + "/" +
player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState); getMaxCharacterStamina() + "\t" + currentState);
if (currentState != MotionState.MOTION_SWIM_IDLE) { if (currentState != MotionState.MOTION_SWIM_IDLE) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
} }
@ -517,24 +540,25 @@ public class StaminaManager {
return getTalentMovingSustainedCost(skillCasting); return getTalentMovingSustainedCost(skillCasting);
} }
// Bow avatar charged attack // Bow avatar charged attack
if (BowSkills.contains(skillCasting)) { int currentAvatarId = player.getTeamManager().getCurrentAvatarEntity().getAvatar().getAvatarId();
if (BowAvatars.contains(currentAvatarId)) {
return getBowSustainedCost(skillCasting); return getBowSustainedCost(skillCasting);
} }
// Claymore avatar charged attack // Claymore avatar charged attack
if (ClaymoreSkills.contains(skillCasting)) { if (ClaymoreAvatars.contains(currentAvatarId)) {
return getClaymoreSustainedCost(skillCasting); return getClaymoreSustainedCost(skillCasting);
} }
// Catalyst avatar charged attack // Catalyst avatar charged attack
if (CatalystSkills.contains(skillCasting)) { if (CatalystAvatars.contains(currentAvatarId)) {
return getCatalystSustainedCost(skillCasting); return getCatalystCost(skillCasting);
} }
// Polearm avatar charged attack // Polearm avatar charged attack
if (PolearmSkills.contains(skillCasting)) { if (PolearmAvatars.contains(currentAvatarId)) {
return getPolearmSustainedCost(skillCasting); return getPolearmCost(skillCasting);
} }
// Sword avatar charged attack // Sword avatar charged attack
if (SwordSkills.contains(skillCasting)) { if (SwordAvatars.contains(skillCasting)) {
return getSwordSustainedCost(skillCasting); return getSwordCost(skillCasting);
} }
return new Consumption(); return new Consumption();
} }
@ -546,18 +570,8 @@ public class StaminaManager {
consumption.amount = ConsumptionType.CLIMBING.amount; consumption.amount = ConsumptionType.CLIMBING.amount;
} }
// Climbing specific reductions // Climbing specific reductions
// TODO: create a food cost reduction map consumption.amount *= getFoodCostReductionFactor(ClimbFoodReductionMap);
HashMap<Integer, Float> foodReductionMap = new HashMap<>() {{ consumption.amount *= getTalentCostReductionFactor(ClimbTalentReductionMap);
// TODO: get real talent id
put(0, 0.8f); // Sample food
}};
consumption.amount *= getFoodCostReductionFactor(foodReductionMap);
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
// TODO: get real talent id
put(0, 0.8f); // Xiao
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
return consumption; return consumption;
} }
@ -572,13 +586,9 @@ public class StaminaManager {
consumption.type = ConsumptionType.SWIM_DASH; consumption.type = ConsumptionType.SWIM_DASH;
consumption.amount = ConsumptionType.SWIM_DASH.amount; consumption.amount = ConsumptionType.SWIM_DASH.amount;
} }
// Reductions // Swimming specific reductions
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ consumption.amount *= getFoodCostReductionFactor(SwimFoodReductionMap);
// TODO: get real talent id consumption.amount *= getTalentCostReductionFactor(SwimTalentReductionMap);
put(0, 0.8f); // Beidou
put(1, 0.8f); // Sangonomiya Kokomi
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
return consumption; return consumption;
} }
@ -587,8 +597,8 @@ public class StaminaManager {
if (currentState == MotionState.MOTION_DASH) { if (currentState == MotionState.MOTION_DASH) {
consumption.type = ConsumptionType.DASH; consumption.type = ConsumptionType.DASH;
consumption.amount = ConsumptionType.DASH.amount; consumption.amount = ConsumptionType.DASH.amount;
// TODO: Dashing specific reductions // Dashing specific reductions
// Foods: consumption.amount *= getFoodCostReductionFactor(DashFoodReductionMap);
} }
return consumption; return consumption;
} }
@ -599,32 +609,34 @@ public class StaminaManager {
return new Consumption(ConsumptionType.POWERED_FLY); return new Consumption(ConsumptionType.POWERED_FLY);
} }
Consumption consumption = new Consumption(ConsumptionType.FLY); Consumption consumption = new Consumption(ConsumptionType.FLY);
// Passive Talents // Flying specific reductions
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{ consumption.amount *= getFoodCostReductionFactor(FlyFoodReductionMap);
put(212301, 0.8f); // Amber consumption.amount *= getTalentCostReductionFactor(FlyTalentReductionMap);
put(222301, 0.8f); // Venti
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
// TODO: Foods
return consumption; return consumption;
} }
private Consumption getSkiffConsumption() { private Consumption getSkiffConsumption() {
// POWERED_SKIFF, e.g. wind tunnel
if (currentState == MotionState.MOTION_SKIFF_POWERED_DASH) {
return new Consumption(ConsumptionType.POWERED_SKIFF);
}
// No known reduction for skiffing. // No known reduction for skiffing.
return new Consumption(ConsumptionType.SKIFF); return switch (currentState) {
case MOTION_SKIFF_DASH -> new Consumption(ConsumptionType.SKIFF_DASH);
case MOTION_SKIFF_POWERED_DASH -> new Consumption(ConsumptionType.POWERED_SKIFF);
case MOTION_SKIFF_NORMAL -> new Consumption(ConsumptionType.SKIFF);
default -> new Consumption();
};
} }
private Consumption getOtherConsumptions() { private Consumption getOtherConsumptions() {
if (currentState == MotionState.MOTION_NOTIFY) { switch (currentState) {
if (BowSkills.contains(lastSkillId)) { case MOTION_NOTIFY:
// if (BowSkills.contains(lastSkillId)) {
// return new Consumption(ConsumptionType.FIGHT, 500);
// }
break;
case MOTION_FIGHT:
// TODO: what if charged attack
return new Consumption(ConsumptionType.FIGHT, 500); return new Consumption(ConsumptionType.FIGHT, 500);
} }
}
// TODO: Add other logic
return new Consumption(); return new Consumption();
} }
@ -671,11 +683,11 @@ public class StaminaManager {
return new Consumption(ConsumptionType.FIGHT, +500); return new Consumption(ConsumptionType.FIGHT, +500);
} }
private Consumption getCatalystSustainedCost(int skillId) { private Consumption getCatalystCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000); Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000);
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
// TODO: Yanfei // TODO:
} }
return consumption; return consumption;
} }
@ -684,18 +696,20 @@ public class StaminaManager {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333 Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
case 10571: // Arataki Itto, does not consume stamina at all. case 10571:
case 10532:
consumption.amount = 0; consumption.amount = 0;
break; break;
case 10160: // Diluc, with talent "Relentless" stamina cost is decreased by 50% case 10160:
// TODO: How to get talent status? if (player.getTeamManager().getCurrentAvatarEntity().getAvatar().getProudSkillList().contains(162101)) {
consumption.amount /= 2; consumption.amount /= 2;
}
break; break;
} }
return consumption; return consumption;
} }
private Consumption getPolearmSustainedCost(int skillId) { private Consumption getPolearmCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500); Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500);
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
@ -704,11 +718,11 @@ public class StaminaManager {
return consumption; return consumption;
} }
private Consumption getSwordSustainedCost(int skillId) { private Consumption getSwordCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000); Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000);
// Character specific handling // Character specific handling
switch (skillId) { switch (skillId) {
case 10421: // Keqing, -2500 case 10421:
consumption.amount = -2500; consumption.amount = -2500;
break; break;
} }

View File

@ -85,6 +85,8 @@ public class Player {
private Set<Integer> flyCloakList; private Set<Integer> flyCloakList;
private Set<Integer> costumeList; private Set<Integer> costumeList;
private Integer widgetId;
@Transient private long nextGuid = 0; @Transient private long nextGuid = 0;
@Transient private int peerId; @Transient private int peerId;
@Transient private World world; @Transient private World world;
@ -302,6 +304,14 @@ public class Player {
this.updateProfile(); this.updateProfile();
} }
public Integer getWidgetId() {
return widgetId;
}
public void setWidgetId(Integer widgetId) {
this.widgetId = widgetId;
}
public Position getPos() { public Position getPos() {
return pos; return pos;
} }
@ -1170,6 +1180,8 @@ public class Player {
session.send(new PacketFinishedParentQuestNotify(this)); session.send(new PacketFinishedParentQuestNotify(this));
session.send(new PacketQuestListNotify(this)); session.send(new PacketQuestListNotify(this));
session.send(new PacketServerCondMeetQuestListUpdateNotify(this)); session.send(new PacketServerCondMeetQuestListUpdateNotify(this));
session.send(new PacketAllWidgetDataNotify(this));
session.send(new PacketWidgetGadgetAllDataNotify());
getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward. getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward.
@ -1267,7 +1279,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009 } else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009
if (!(0 <= value && value <= 1)) { return false; } if (!(0 <= value && value <= 1)) { return false; }
} else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010 } else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010
if (!(value >= 0 && value <= StaminaManager.GlobalMaximumStamina)) { return false; } if (!(value >= 0 && value <= StaminaManager.GlobalCharacterMaximumStamina)) { return false; }
} else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011 } else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011
int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA); int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (!(value >= 0 && value <= playerMaximumStamina)) { return false; } if (!(value >= 0 && value <= playerMaximumStamina)) { return false; }

View File

@ -18,7 +18,7 @@ import emu.grasscutter.server.packet.send.PacketBuyGoodsRsp;
import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify; import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import java.util.HashMap; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -56,36 +56,13 @@ public class HandlerBuyGoodsReq extends PacketHandler {
return; return;
} }
if (sg.getScoin() > 0 && session.getPlayer().getMora() < buyGoodsReq.getBoughtNum() * sg.getScoin()) { List<ItemParamData> costs = new ArrayList<ItemParamData>(sg.getCostItemList()); // Can this even be null?
costs.add(new ItemParamData(202, sg.getScoin()));
costs.add(new ItemParamData(201, sg.getHcoin()));
costs.add(new ItemParamData(203, sg.getMcoin()));
if (!session.getPlayer().getInventory().payItems(costs.toArray(new ItemParamData[0]), buyGoodsReq.getBoughtNum())) {
return; return;
} }
if (sg.getHcoin() > 0 && session.getPlayer().getPrimogems() < buyGoodsReq.getBoughtNum() * sg.getHcoin()) {
return;
}
if (sg.getMcoin() > 0 && session.getPlayer().getCrystals() < buyGoodsReq.getBoughtNum() * sg.getMcoin()) {
return;
}
HashMap<GameItem, Integer> itemsCache = new HashMap<>();
if (sg.getCostItemList() != null) {
for (ItemParamData p : sg.getCostItemList()) {
Optional<GameItem> invItem = session.getPlayer().getInventory().getItems().values().stream().filter(x -> x.getItemId() == p.getId()).findFirst();
if (invItem.isEmpty() || invItem.get().getCount() < p.getCount())
return;
itemsCache.put(invItem.get(), p.getCount() * buyGoodsReq.getBoughtNum());
}
}
session.getPlayer().setMora(session.getPlayer().getMora() - buyGoodsReq.getBoughtNum() * sg.getScoin());
session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() - buyGoodsReq.getBoughtNum() * sg.getHcoin());
session.getPlayer().setCrystals(session.getPlayer().getCrystals() - buyGoodsReq.getBoughtNum() * sg.getMcoin());
if (!itemsCache.isEmpty()) {
for (GameItem gi : itemsCache.keySet()) {
session.getPlayer().getInventory().removeItem(gi, itemsCache.get(gi));
}
itemsCache.clear();
}
session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum(), ShopManager.getShopNextRefreshTime(sg)); session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum(), ShopManager.getShopNextRefreshTime(sg));
GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getId())); GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getId()));

View File

@ -1,16 +1,20 @@
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketGetShopRsp;
import emu.grasscutter.server.packet.send.PacketGetWidgetSlotRsp;
@Opcodes(PacketOpcodes.GetWidgetSlotReq) @Opcodes(PacketOpcodes.GetWidgetSlotReq)
public class HandlerGetWidgetSlotReq extends PacketHandler { public class HandlerGetWidgetSlotReq extends PacketHandler {
@Override @Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
// Unhandled Player player = session.getPlayer();
session.send(new PacketGetWidgetSlotRsp(player));
} }
} }

View File

@ -0,0 +1,33 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.SetWidgetSlotReqOuterClass;
import emu.grasscutter.net.proto.WidgetSlotOpOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketSetWidgetSlotRsp;
import emu.grasscutter.server.packet.send.PacketWidgetSlotChangeNotify;
@Opcodes(PacketOpcodes.SetWidgetSlotReq)
public class HandlerSetWidgetSlotReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
SetWidgetSlotReqOuterClass.SetWidgetSlotReq req = SetWidgetSlotReqOuterClass.SetWidgetSlotReq.parseFrom(payload);
Player player = session.getPlayer();
player.setWidgetId(req.getMaterialId());
// WidgetSlotChangeNotify op & slot key
session.send(new PacketWidgetSlotChangeNotify(WidgetSlotOpOuterClass.WidgetSlotOp.DETACH));
// WidgetSlotChangeNotify slot
session.send(new PacketWidgetSlotChangeNotify(req.getMaterialId()));
// SetWidgetSlotRsp
session.send(new PacketSetWidgetSlotRsp(req.getMaterialId()));
}
}

View File

@ -14,6 +14,7 @@ public class HandlerVehicleInteractReq extends PacketHandler {
@Override @Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
VehicleInteractReqOuterClass.VehicleInteractReq req = VehicleInteractReqOuterClass.VehicleInteractReq.parseFrom(payload); VehicleInteractReqOuterClass.VehicleInteractReq req = VehicleInteractReqOuterClass.VehicleInteractReq.parseFrom(payload);
session.getPlayer().getStaminaManager().handleVehicleInteractReq(session, req.getEntityId(), req.getInteractType());
session.send(new PacketVehicleInteractRsp(session.getPlayer(), req.getEntityId(), req.getInteractType())); session.send(new PacketVehicleInteractRsp(session.getPlayer(), req.getEntityId(), req.getInteractType()));
} }
} }

View File

@ -0,0 +1,59 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.AllWidgetDataNotifyOuterClass.AllWidgetDataNotify;
import emu.grasscutter.net.proto.LunchBoxDataOuterClass;
import emu.grasscutter.net.proto.WidgetSlotDataOuterClass;
import emu.grasscutter.net.proto.WidgetSlotTagOuterClass;
import java.util.List;
import java.util.Map;
public class PacketAllWidgetDataNotify extends BasePacket {
public PacketAllWidgetDataNotify(Player player) {
super(PacketOpcodes.AllWidgetDataNotify);
// TODO: Implement this
AllWidgetDataNotify.Builder proto = AllWidgetDataNotify.newBuilder()
// If you want to implement this, feel free to do so. :)
.setLunchBoxData(
LunchBoxDataOuterClass.LunchBoxData.newBuilder().build()
)
// Maybe it's a little difficult, or it makes you upset :(
.addAllOneoffGatherPointDetectorDataList(List.of())
// So, goodbye, and hopefully sometime in the future o(**)
.addAllCoolDownGroupDataList(List.of())
// I'll see your PR with a title that says (((*)
.addAllAnchorPointList(List.of())
// "Complete implementation of widget functionality" bd 
.addAllClientCollectorDataList(List.of())
// Good luck, my boy.
.addAllNormalCoolDownDataList(List.of());
if (player.getWidgetId() == null) {
proto.addAllSlotList(List.of());
} else {
proto.addSlotList(
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
.setIsActive(true)
.setMaterialId(player.getWidgetId())
.build()
);
proto.addSlotList(
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
.setTag(WidgetSlotTagOuterClass.WidgetSlotTag.WIDGET_SLOT_ATTACH_AVATAR)
.build()
);
}
AllWidgetDataNotify protoData = proto.build();
this.setData(protoData);
}
}

View File

@ -0,0 +1,41 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.GetWidgetSlotRspOuterClass;
import emu.grasscutter.net.proto.WidgetSlotDataOuterClass;
import emu.grasscutter.net.proto.WidgetSlotTagOuterClass;
import java.util.List;
public class PacketGetWidgetSlotRsp extends BasePacket {
public PacketGetWidgetSlotRsp(Player player) {
super(PacketOpcodes.GetWidgetSlotRsp);
GetWidgetSlotRspOuterClass.GetWidgetSlotRsp.Builder proto =
GetWidgetSlotRspOuterClass.GetWidgetSlotRsp.newBuilder();
if (player.getWidgetId() == null) {
proto.addAllSlotList(List.of());
} else {
proto.addSlotList(
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
.setIsActive(true)
.setMaterialId(player.getWidgetId())
.build()
);
proto.addSlotList(
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
.setTag(WidgetSlotTagOuterClass.WidgetSlotTag.WIDGET_SLOT_ATTACH_AVATAR)
.build()
);
}
GetWidgetSlotRspOuterClass.GetWidgetSlotRsp protoData = proto.build();
this.setData(protoData);
}
}

View File

@ -0,0 +1,18 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.SetWidgetSlotRspOuterClass;
public class PacketSetWidgetSlotRsp extends BasePacket {
public PacketSetWidgetSlotRsp(int materialId) {
super(PacketOpcodes.SetWidgetSlotRsp);
SetWidgetSlotRspOuterClass.SetWidgetSlotRsp proto = SetWidgetSlotRspOuterClass.SetWidgetSlotRsp.newBuilder()
.setMaterialId(materialId)
.build();
this.setData(proto);
}
}

View File

@ -0,0 +1,18 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.VehicleStaminaNotifyOuterClass.VehicleStaminaNotify;
public class PacketVehicleStaminaNotify extends BasePacket {
public PacketVehicleStaminaNotify(int vehicleId, float newStamina) {
super(PacketOpcodes.VehicleStaminaNotify);
VehicleStaminaNotify.Builder proto = VehicleStaminaNotify.newBuilder();
proto.setEntityId(vehicleId);
proto.setCurStamina(newStamina);
this.setData(proto.build());
}
}

View File

@ -0,0 +1,16 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.WidgetGadgetAllDataNotifyOuterClass.WidgetGadgetAllDataNotify;
public class PacketWidgetGadgetAllDataNotify extends BasePacket {
public PacketWidgetGadgetAllDataNotify() {
super(PacketOpcodes.AllWidgetDataNotify);
WidgetGadgetAllDataNotify proto = WidgetGadgetAllDataNotify.newBuilder().build();
this.setData(proto);
}
}

View File

@ -0,0 +1,47 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.WidgetSlotChangeNotifyOuterClass;
import emu.grasscutter.net.proto.WidgetSlotDataOuterClass;
import emu.grasscutter.net.proto.WidgetSlotOpOuterClass;
public class PacketWidgetSlotChangeNotify extends BasePacket {
public PacketWidgetSlotChangeNotify(WidgetSlotChangeNotifyOuterClass.WidgetSlotChangeNotify proto) {
super(PacketOpcodes.WidgetSlotChangeNotify);
this.setData(proto);
}
public PacketWidgetSlotChangeNotify(WidgetSlotOpOuterClass.WidgetSlotOp op) {
super(PacketOpcodes.WidgetSlotChangeNotify);
WidgetSlotChangeNotifyOuterClass.WidgetSlotChangeNotify proto = WidgetSlotChangeNotifyOuterClass.WidgetSlotChangeNotify.newBuilder()
.setOp(op)
.setSlot(
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
.setIsActive(true)
.build()
)
.build();
this.setData(proto);
}
public PacketWidgetSlotChangeNotify(int materialId) {
super(PacketOpcodes.WidgetSlotChangeNotify);
WidgetSlotChangeNotifyOuterClass.WidgetSlotChangeNotify proto = WidgetSlotChangeNotifyOuterClass.WidgetSlotChangeNotify.newBuilder()
.setSlot(
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
.setIsActive(true)
.setMaterialId(materialId)
.build()
)
.build();
this.setData(proto);
}
}