diff --git a/game/packet/client.go b/game/packet/client.go index 4529fd3..2016569 100755 --- a/game/packet/client.go +++ b/game/packet/client.go @@ -70,6 +70,7 @@ var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ 0x0082: &ClientMultiplayerLeave{}, 0x0088: &Client0088{}, 0x008B: &ClientRequestMessengerList{}, + 0x0098: &ClientRareShopOpen{}, 0x009C: &ClientRequestPlayerHistory{}, 0x00AE: &ClientTutorialClear{}, 0x00B5: &ClientEnterMyRoom{}, @@ -81,10 +82,13 @@ var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ 0x0140: &ClientShopJoin{}, 0x0143: &ClientRequestInboxList{}, 0x0144: &ClientRequestInboxMessage{}, + 0x014B: &ClientBlackPapelPlay{}, + 0x0157: &ClientAchievementStatusRequest{}, 0x016E: &ClientRequestDailyReward{}, 0x0176: &ClientEventLobbyJoin{}, 0x0177: &ClientEventLobbyLeave{}, 0x0184: &ClientAssistModeToggle{}, + 0x0186: &ClientBigPapelPlay{}, }) // ClientAuth is a message sent to authenticate a session. @@ -112,6 +116,11 @@ type ClientGetUserOnlineStatus struct { Username common.PString } +// ClientRareShopOpen notifies the server if a user opens the rare shop menu. +type ClientRareShopOpen struct { + ClientMessage_ +} + // ClientRoomEdit is sent when the client changes room settings. type ClientRoomEdit struct { ClientMessage_ @@ -383,6 +392,12 @@ type ClientRequestMessengerList struct { ClientMessage_ } +// ClientAchievementStatusRequest requests Achievement Status for a user. +type ClientAchievementStatusRequest struct { + ClientMessage_ + UserID uint32 +} + // ClientGetUserData is a message sent by the client to request // the client state. type ClientGetUserData struct { @@ -517,3 +532,11 @@ type ClientLockerInventoryRequest struct { type Client00FE struct { ClientMessage_ } + +type ClientBlackPapelPlay struct { + ClientMessage_ +} + +type ClientBigPapelPlay struct { + ClientMessage_ +} diff --git a/game/packet/server.go b/game/packet/server.go index c1f207d..891c56d 100755 --- a/game/packet/server.go +++ b/game/packet/server.go @@ -73,6 +73,8 @@ var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ 0x00F1: &ServerMessageConnect{}, 0x00F5: &ServerMultiplayerJoined{}, 0x00F6: &ServerMultiplayerLeft{}, + 0x00FB: &ServerBlackPapelResponse{}, + 0x010B: &ServerRareShopOpen{}, 0x010E: &ServerPlayerHistory{}, 0x011F: &ServerTutorialStatus{}, 0x012B: &ServerMyRoomEntered{}, @@ -89,13 +91,17 @@ var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ 0x0211: &ServerInboxList{}, 0x0212: &ServerMailMessage{}, 0x0216: &ServerUserStatusUpdate{}, + 0x021B: &ServerBlackPapelWinnings{}, 0x021D: &ServerAchievementProgress{}, + 0x022C: &ServerAchievementUnknownResponse{}, + 0x022D: &ServerAchievementStatusResponse{}, 0x0230: &Server0230{}, 0x0231: &Server0231{}, 0x0248: &ServerLoginBonusStatus{}, 0x0250: &ServerEventLobbyJoined{}, 0x0251: &ServerEventLobbyLeft{}, 0x026A: &ServerAssistModeToggled{}, + 0x026C: &ServerBigPapelWinnings{}, }) // ConnectMessage is the message sent upon connecting. @@ -250,6 +256,39 @@ type GamePlayer struct { NumCards uint8 } +type ServerRareShopOpen struct { + ServerMessage_ + UnknownA int32 + UnknownB int32 + UnknownC uint32 +} + +type Achievement struct { + ID uint32 + Value uint32 + Timestamp uint32 +} + +type AchievementGroup struct { + GroupID uint32 + ID uint32 + Count uint32 `struct:"sizeof=Achievements"` + Achievements []Achievement +} + +type ServerAchievementStatusResponse struct { + ServerMessage_ + Status uint32 + Remaining uint32 + Count uint32 `struct:"sizeof=Groups"` + Groups []AchievementGroup +} + +type ServerAchievementUnknownResponse struct { + ServerMessage_ + UnknownA [4]byte +} + type GameInitFull struct { NumPlayers byte `struct:"sizeof=Players"` Players []GamePlayer @@ -299,7 +338,7 @@ type ServerRoomAction struct { gamemodel.RoomAction } -// ServerPangBalanceData is sent after a pang purchase succeeds. +// ServerPangBalanceData is sent after a pang purchase succeeds. Sometimes Pang Spent is not set like on Black Papel Transactions. type ServerPangBalanceData struct { ServerMessage_ PangsRemaining uint64 @@ -878,3 +917,37 @@ type ServerAssistModeToggled struct { ServerMessage_ Unknown uint32 } + +type ServerBlackPapelWinnings struct { + ServerMessage_ + Status uint32 + BlackPapelInvTicketSlot uint32 + UniqueItemsWon uint32 `struct:"sizeof=Items"` + Items []ServerBlackPapelItems + PangsRemaining uint64 + CookiesRemaining uint64 //not filled in but seems correct +} + +type ServerBlackPapelResponse struct { + ServerMessage_ + RemainingTurns int32 + UnknownA int32 +} + +type ServerBlackPapelItems struct { + DolfiniBallColor uint32 + ItemTypeID uint32 + InventorySlot uint32 + Quantity uint32 + Rarity uint32 +} + +type ServerBigPapelWinnings struct { + ServerMessage_ + Status uint32 + BlackPapelInvTicketSlot uint32 + UniqueItemsWon uint32 `struct:"sizeof=Items"` + Items []ServerBlackPapelItems + PangsRemaining uint64 + CookiesRemaining uint64 +} diff --git a/game/server/conn.go b/game/server/conn.go index a70ba8a..41a9adc 100755 --- a/game/server/conn.go +++ b/game/server/conn.go @@ -21,6 +21,8 @@ import ( "context" "errors" "fmt" + "math/rand" + "time" "github.com/pangbox/server/common" "github.com/pangbox/server/database/accounts" @@ -416,6 +418,120 @@ func (c *Conn) Handle(ctx context.Context) error { }) case *gamepacket.Client0088: // Unknown tutorial-related message. + case *gamepacket.ClientRareShopOpen: + c.SendMessage(ctx, &gamepacket.ServerRareShopOpen{ + UnknownA: 0, + UnknownB: 0, + UnknownC: 0, + }) + case *gamepacket.ClientAchievementStatusRequest: + c.SendMessage(ctx, &gamepacket.ServerAchievementStatusResponse{ + Remaining: 1, + Count: 1, + Groups: []gamepacket.AchievementGroup{ + { + GroupID: 0x4c80002d, + ID: 248388034, + Count: 5, + Achievements: []gamepacket.Achievement{ + { + ID: 0x7480087c, + Value: 1, + Timestamp: uint32(time.Date(2018, 11, 3, 0, 25, 10, 0, time.UTC).Unix()), + }, + { + ID: 0x7480087d, + Value: 3, + Timestamp: uint32(time.Date(2018, 11, 7, 2, 5, 26, 0, time.UTC).Unix()), + }, + { + ID: 0x7480087e, + Value: 5, + Timestamp: uint32(time.Date(2018, 11, 7, 3, 3, 45, 0, time.UTC).Unix()), + }, + { + ID: 0x7480087f, + Value: 7, + Timestamp: uint32(time.Date(2018, 11, 8, 23, 27, 19, 0, time.UTC).Unix()), + }, + { + ID: 0x74800880, + Value: 10, + Timestamp: uint32(time.Date(2019, 8, 25, 19, 39, 45, 0, time.UTC).Unix()), + }, + }, + }, + }, + }) + c.SendMessage(ctx, &gamepacket.ServerAchievementUnknownResponse{ + UnknownA: [4]byte{0x00, 0x00, 0x00, 0x00}, + }) + case *gamepacket.ClientBigPapelPlay: + c.SendMessage(ctx, &gamepacket.ServerBlackPapelResponse{ + RemainingTurns: 50, //Displays as remaining turns in the box. + UnknownA: -1, + }) + // TODO make Pang interaction transactional + // TODO make items show up in inventory + // TODO make sure not to subtract pang if a ticket is used. + // TODO Fix pang able to go negative. + c.player.Pang, err = c.s.accountsService.AddPang(ctx, c.player.PlayerID, -10000) + if err != nil { + return err + } + packet := &gamepacket.ServerBigPapelWinnings{} + const minBigPapelItems = 10 + const maxBigPapelItems = 20 + itemQuantities := make(map[uint32]int) + numItems := rand.Intn(maxBigPapelItems-minBigPapelItems) + minBigPapelItems + for i := 0; i < numItems; i++ { + typeID := c.s.papelShop.Choose() + itemQuantities[typeID]++ + } + for typeID, quantity := range itemQuantities { + item := gamepacket.ServerBlackPapelItems{} + item.ItemTypeID = typeID + item.Quantity = uint32(quantity) + packet.Items = append(packet.Items, item) + } + packet.PangsRemaining = uint64(c.player.Pang) //This should be calculated by player data + packet.CookiesRemaining = uint64(c.player.Points) //Cookies remaining even if the action doesn't cost cookies + c.SendMessage(ctx, packet) + case *gamepacket.ClientBlackPapelPlay: + // TODO make Pang interaction transactional. + // TODO make items show up in inventory. + // TODO make sure not to subtract pang if a ticket is used. + c.player.Pang, err = c.s.accountsService.AddPang(ctx, c.player.PlayerID, -500) + if err != nil { + return err + } + packet := &gamepacket.ServerBlackPapelWinnings{} + drawnItemsSet := make(map[uint32]struct{}) + for i := rand.Intn(4) + 1; i > 0; { + item := gamepacket.ServerBlackPapelItems{} + var typeID uint32 + for { + typeID = c.s.papelShop.Choose() + if _, ok := drawnItemsSet[typeID]; !ok { + drawnItemsSet[typeID] = struct{}{} + break + } + } + item.ItemTypeID = typeID + item.DolfiniBallColor = uint32(rand.Intn(3)) + item.Rarity = c.s.papelRarity[typeID] + if item.Rarity == 2 { + item.Quantity = 1 + } else { + item.Quantity = uint32(rand.Intn(3) + 1) + } + packet.UniqueItemsWon += 1 + packet.Items = append(packet.Items, item) + i-- + } + packet.PangsRemaining = uint64(c.player.Pang) + packet.CookiesRemaining = uint64(c.player.Points) + c.SendMessage(ctx, packet) case *gamepacket.ClientRoomUserEquipmentChange: // TODO case *gamepacket.ClientTutorialStart: diff --git a/game/server/playerdata.go b/game/server/playerdata.go index 4ac9481..c2e3a9c 100644 --- a/game/server/playerdata.go +++ b/game/server/playerdata.go @@ -32,6 +32,7 @@ func (c *Conn) getPlayerInfo() pangya.PlayerInfo { func (c *Conn) getPlayerStats() pangya.PlayerStats { return pangya.PlayerStats{ Pang: uint64(c.player.Pang), + Rank: byte(c.player.Rank), // TODO } } diff --git a/game/server/server.go b/game/server/server.go index 02ca663..d396075 100755 --- a/game/server/server.go +++ b/game/server/server.go @@ -52,11 +52,19 @@ type Server struct { configProvider gameconfig.Provider lobby *room.Lobby logger *log.Entry + papelShop *WeightedRand + papelRarity map[uint32]uint32 } // New creates a new instance of the game server. func New(opts Options) *Server { logger := log.WithField("server", "GameServer") + papelShop := NewWeightedRand() + papelRarity := make(map[uint32]uint32) + for _, item := range opts.ConfigProvider.GetPapelShopOdds() { + papelShop.Add(item.TypeID, item.Weight) + papelRarity[item.TypeID] = uint32(item.Rarity) + } return &Server{ baseServer: &common.BaseServer{}, topologyClient: opts.TopologyClient, @@ -66,6 +74,8 @@ func New(opts Options) *Server { channelName: opts.ChannelName, configProvider: opts.ConfigProvider, logger: logger, + papelShop: papelShop, + papelRarity: papelRarity, } } diff --git a/game/server/weightedrandom.go b/game/server/weightedrandom.go new file mode 100644 index 0000000..5ddbae7 --- /dev/null +++ b/game/server/weightedrandom.go @@ -0,0 +1,52 @@ +package gameserver + +import ( + "errors" + "math/rand" + "sort" +) + +type WeightedRand struct { + cumulativeWeights []int64 + values []uint32 + totalWeight int64 +} + +func NewWeightedRand() *WeightedRand { + return &WeightedRand{} +} + +func (w *WeightedRand) Add(value uint32, weight int64) error { + // Detect if the total weight is going to overflow + if w.totalWeight+weight < w.totalWeight { + return errors.New("total weight will overflow") + } + + // Update total weight + w.totalWeight += weight + + // Append the value + w.values = append(w.values, value) + + // Compute cumulative weight + var cumulativeWeight int64 + if len(w.cumulativeWeights) > 0 { + cumulativeWeight = w.cumulativeWeights[len(w.cumulativeWeights)-1] + weight + } else { + cumulativeWeight = weight + } + w.cumulativeWeights = append(w.cumulativeWeights, cumulativeWeight) + + return nil +} + +func (w *WeightedRand) Choose() uint32 { + // Generate a random number in the range [0, totalWeight) + r := rand.Int63n(w.totalWeight) + + // Use binary search to find the index where our random number fits in + index := sort.Search(len(w.cumulativeWeights), func(i int) bool { return w.cumulativeWeights[i] > r }) + + // Return the corresponding value + return w.values[index] +} diff --git a/gameconfig/config.go b/gameconfig/config.go index fa7b6c4..c85ac7e 100644 --- a/gameconfig/config.go +++ b/gameconfig/config.go @@ -26,6 +26,7 @@ type Provider interface { GetCharacterDefaults(id uint8) CharacterDefaults GetDefaultClubSetTypeID() uint32 GetDefaultPang() uint64 + GetPapelShopOdds() []ItemProbability } type CharacterDefaults struct { @@ -37,12 +38,20 @@ type Manifest struct { CharacterDefaults []CharacterDefaults `json:"CharacterDefaults"` DefaultClubSetTypeID uint32 `json:"DefaultClubSetTypeID"` DefaultPang uint64 `json:"DefaultPang"` + PapelShopOdds []ItemProbability `json:"PapelShopOdds"` } type configFileProvider struct { characterDefaults map[uint8]CharacterDefaults defaultClubSetTypeID uint32 defaultPang uint64 + papelShopOdds []ItemProbability +} + +type ItemProbability struct { + TypeID uint32 + Weight int64 + Rarity uint32 } func Default() Provider { @@ -78,6 +87,7 @@ func FromManifest(manifest Manifest) Provider { characterDefaults: make(map[uint8]CharacterDefaults), defaultClubSetTypeID: manifest.DefaultClubSetTypeID, defaultPang: manifest.DefaultPang, + papelShopOdds: manifest.PapelShopOdds, } for _, defaults := range manifest.CharacterDefaults { provider.characterDefaults[defaults.CharacterID] = defaults @@ -96,3 +106,7 @@ func (c *configFileProvider) GetDefaultClubSetTypeID() uint32 { func (c *configFileProvider) GetDefaultPang() uint64 { return c.defaultPang } + +func (c *configFileProvider) GetPapelShopOdds() []ItemProbability { + return c.papelShopOdds +} diff --git a/gameconfig/default.json b/gameconfig/default.json index 26e4efa..968e1fc 100644 --- a/gameconfig/default.json +++ b/gameconfig/default.json @@ -1,5 +1,24 @@ { "DefaultPang": 20000, "DefaultClubSetTypeID": 268435553, - "CharacterDefaults": [] + "CharacterDefaults": [], + "PapelShopOdds": [ + {"TypeID":402653184,"Weight":100, "Rarity":0}, + {"TypeID":402653185,"Weight":100, "Rarity":0}, + {"TypeID":402653188,"Weight":90, "Rarity":0}, + {"TypeID":402653188,"Weight":90, "Rarity":0}, + {"TypeID":402653192,"Weight":150, "Rarity":0}, + {"TypeID":402653191,"Weight":150, "Rarity":0}, + {"TypeID":436207657,"Weight":20, "Rarity":0}, + {"TypeID":402653190,"Weight":20, "Rarity":1}, + {"TypeID":335544321,"Weight":20, "Rarity":1}, + {"TypeID":402653193,"Weight":20, "Rarity":1}, + {"TypeID":335544322,"Weight":20, "Rarity":1}, + {"TypeID":335544323,"Weight":20, "Rarity":1}, + {"TypeID":335544325,"Weight":20, "Rarity":1}, + {"TypeID":436207616,"Weight":35, "Rarity":1}, + {"TypeID":402653194,"Weight":20, "Rarity":1}, + {"TypeID":402653195,"Weight":20, "Rarity":1}, + {"TypeID":135544902,"Weight":2, "Rarity":2} + ] } diff --git a/pangya/player.go b/pangya/player.go index 02c0018..a207ad1 100755 --- a/pangya/player.go +++ b/pangya/player.go @@ -54,7 +54,7 @@ type PlayerStats struct { LongestPutt float32 LongestChip float32 TotalXP uint32 - Level byte + Rank byte Pang uint64 TotalScore int32 DifficultyScore [5]uint8