Skip to content

Commit

Permalink
feat: MatchZy demos support
Browse files Browse the repository at this point in the history
- fix incorrect round end reason when bomb exploded after round end akiver/cs-demo-manager#896

ref akiver/cs-demo-manager#751
  • Loading branch information
akiver committed Dec 2, 2024
1 parent 3bb55c5 commit 459d639
Show file tree
Hide file tree
Showing 19 changed files with 2,457 additions and 104 deletions.
8 changes: 8 additions & 0 deletions internal/demo/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ var faceItDemoNameRegex = regexp.MustCompile(`/[0-9]+_team[a-z0-9-]+-Team[a-z0-9
var ebotDemoNameRegex = regexp.MustCompile(`/([0-9]*)_(.*?)-(.*?)_(.*?)(.dem)/`)
var fiveEPlayDemoNameRegex = regexp.MustCompile(`^g\d+-(.*)[a-zA-Z0-9_]*$`)

// Default format: {TIME}_{MATCH_ID}_{MAP}_{TEAM1}_vs_{TEAM2}
// https://shobhit-pathak.github.io/MatchZy/gotv/#recording-demos
var matchZyDemoNameRegex = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})_(\d+)_([a-zA-Z0-9_]+)_(.+?)_vs_(.+)$`)

// Reads the .info file associated with a demo if it exists and returns its content as bytes.
func getMatchInfoProtoBytes(demoFilePath string) []byte {
infoFilePath := demoFilePath + ".info"
Expand Down Expand Up @@ -295,6 +299,10 @@ func GetDemoSource(demo *Demo) constants.DemoSource {
return constants.DemoSourceGamersclub
}

if strings.Contains(serverName, "matchzy") || matchZyDemoNameRegex.MatchString(demoName) {
return constants.DemoSourceMatchZy
}

if strings.Contains(serverName, "valve") {
return constants.DemoSourceValve
}
Expand Down
11 changes: 11 additions & 0 deletions internal/slice/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,14 @@ func Filter[T comparable](s []T, predicate func(item T, index int) bool) []T {

return result
}

func Find[T comparable](collection []T, predicate func(item T) bool) (T, bool) {
for i := range collection {
if predicate(collection[i]) {
return collection[i], true
}
}

var result T
return result, false
}
1 change: 1 addition & 0 deletions js/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const DemoSource = {
Fastcup: 'fastcup',
FiveEPlay: '5eplay',
Gamersclub: 'gamersclub',
MatchZy: 'matchzy',
PerfectWorld: 'perfectworld',
Popflash: 'popflash',
Unknown: 'unknown',
Expand Down
31 changes: 27 additions & 4 deletions pkg/api/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ func analyzeDemo(demoPath string, options AnalyzeDemoOptions) (*Match, error) {
case constants.DemoSourceGamersclub:
// Looks like they use an eBot fork but rounds are not detected properly.
return nil, errors.New("gamersclub demos are not supported (GamersClubNotSupported)")
case constants.DemoSourceMatchZy:
createMatchZyAnalyzer(analyzer)
case constants.DemoSourcePopFlash:
// TODO notImplemented Unsure how demos from PopFlash are nowadays, need to find some to test.
// Even latest CSGO demos from PopFlash may not really work because their recording system has probably changed
Expand All @@ -204,11 +206,25 @@ func analyzeDemo(demoPath string, options AnalyzeDemoOptions) (*Match, error) {
}

// Required for CS2 demos, the following data are available only at the end of the parsing in the CDemoFileInfo message.
// The parser updates the header values when the CDemoFileInfo message is parsed.
// Unfortunately, some CS2 demos may not contain this message and so the header values are not updated.
// We fallback to the parser's internal values in such cases.
header := parser.Header()
match.TickCount = header.PlaybackTicks
if match.TickCount == 0 {
match.TickCount = analyzer.currentTick()
}

match.Duration = header.PlaybackTime
if match.Duration == 0 {
match.Duration = parser.CurrentTime()
}
if match.Duration > 0 {
match.FrameRate = float64(header.PlaybackFrames) / header.PlaybackTime.Seconds()
frames := int(header.PlaybackFrames)
if frames == 0 {
frames = parser.CurrentFrame()
}
match.FrameRate = float64(frames) / match.Duration.Seconds()
}

analyzer.postProcess(analyzer)
Expand Down Expand Up @@ -292,6 +308,12 @@ func (analyzer *Analyzer) reset() {
TeamBName: analyzer.match.TeamB.Name,
weaponsBoughtUniqueIds: nil,
}
analyzer.createPlayersEconomies()
}

func (analyzer *Analyzer) resetCurrentRound() {
analyzer.match.resetRound(analyzer.currentRound.Number)
analyzer.createPlayersEconomies()
}

func (analyzer *Analyzer) registerPlayer(player *common.Player, teamState *common.TeamState) {
Expand Down Expand Up @@ -560,6 +582,10 @@ func (analyzer *Analyzer) defaultRoundEndOfficiallyHandler(event events.RoundEnd
analyzer.match.Rounds = append(analyzer.match.Rounds, analyzer.currentRound)
}

func (analyzer *Analyzer) defaultAnnouncementWinPanelMatchHandler(event events.AnnouncementWinPanelMatch) {
analyzer.updatePlayersScores()
}

func (analyzer *Analyzer) registerCommonHandlers(includePositions bool) {
parser := analyzer.parser
match := analyzer.match
Expand Down Expand Up @@ -885,9 +911,6 @@ func (analyzer *Analyzer) registerCommonHandlers(includePositions bool) {

bombExploded := newBombExploded(analyzer, event)
match.BombsExploded = append(match.BombsExploded, bombExploded)
// Update the round end reason because sometimes when the bomb exploded, the round end event indicates
// RoundEndReasonTerroristsWin instead of RoundEndReasonTargetBombed.
analyzer.currentRound.EndReason = events.RoundEndReasonTargetBombed
})

parser.RegisterEventHandler(func(event events.BombPlantBegin) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/constants/demo_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
DemoSourceFiveEPlay DemoSource = "5eplay"
DemoSourceGamersclub DemoSource = "gamersclub"
// "Perfect World" (完美世界) is a Chinese company that Valve partnered with to release CS:GO in China.
DemoSourceMatchZy DemoSource = "matchzy"
DemoSourcePerfectWorld DemoSource = "perfectworld"
DemoSourcePopFlash DemoSource = "popflash"
DemoSourceUnknown DemoSource = "unknown"
Expand All @@ -36,4 +37,5 @@ var SupportedDemoSources = []DemoSource{
DemoSourcePerfectWorld,
DemoSourcePopFlash,
DemoSourceValve,
DemoSourceMatchZy,
}
4 changes: 1 addition & 3 deletions pkg/api/ebot.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,5 @@ func createEbotAnalyzer(analyzer *Analyzer) {
match.Rounds = append(match.Rounds, analyzer.currentRound)
})

parser.RegisterEventHandler(func(event events.AnnouncementWinPanelMatch) {
analyzer.updatePlayersScores()
})
parser.RegisterEventHandler(analyzer.defaultAnnouncementWinPanelMatchHandler)
}
4 changes: 1 addition & 3 deletions pkg/api/esportal.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,5 @@ func createEsportalAnalyzer(analyzer *Analyzer) {

parser.RegisterEventHandler(analyzer.defaultRoundEndOfficiallyHandler)

parser.RegisterEventHandler(func(event events.AnnouncementWinPanelMatch) {
analyzer.updatePlayersScores()
})
parser.RegisterEventHandler(analyzer.defaultAnnouncementWinPanelMatchHandler)
}
172 changes: 88 additions & 84 deletions pkg/api/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,93 @@ func (match *Match) reset() {
match.initTeams()
}

func (match *Match) resetRound(roundNumber int) {
match.BombsPlantStart = slice.Filter(match.BombsPlantStart, func(event *BombPlantStart, index int) bool {
return event.RoundNumber != roundNumber
})
match.BombsPlanted = slice.Filter(match.BombsPlanted, func(event *BombPlanted, index int) bool {
return event.RoundNumber != roundNumber
})
match.BombsDefuseStart = slice.Filter(match.BombsDefuseStart, func(event *BombDefuseStart, index int) bool {
return event.RoundNumber != roundNumber
})
match.BombsDefused = slice.Filter(match.BombsDefused, func(event *BombDefused, index int) bool {
return event.RoundNumber != roundNumber
})
match.BombsExploded = slice.Filter(match.BombsExploded, func(event *BombExploded, index int) bool {
return event.RoundNumber != roundNumber
})
match.ChatMessages = slice.Filter(match.ChatMessages, func(msg *ChatMessage, index int) bool {
return msg.RoundNumber != roundNumber
})
match.ChickenDeaths = slice.Filter(match.ChickenDeaths, func(death *ChickenDeath, index int) bool {
return death.RoundNumber != roundNumber
})
match.ChickenPositions = slice.Filter(match.ChickenPositions, func(position *ChickenPosition, index int) bool {
return position.RoundNumber != roundNumber
})
match.Clutches = slice.Filter(match.Clutches, func(clutch *Clutch, index int) bool {
return clutch.RoundNumber != roundNumber
})
match.Damages = slice.Filter(match.Damages, func(damage *Damage, index int) bool {
return damage.RoundNumber != roundNumber
})
match.DecoysStart = slice.Filter(match.DecoysStart, func(decoy *DecoyStart, index int) bool {
return decoy.RoundNumber != roundNumber
})
match.FlashbangsExplode = slice.Filter(match.FlashbangsExplode, func(flash *FlashbangExplode, index int) bool {
return flash.RoundNumber != roundNumber
})
match.GrenadeBounces = slice.Filter(match.GrenadeBounces, func(grenade *GrenadeBounce, index int) bool {
return grenade.RoundNumber != roundNumber
})
match.GrenadePositions = slice.Filter(match.GrenadePositions, func(position *GrenadePosition, index int) bool {
return position.RoundNumber != roundNumber
})
match.GrenadeProjectilesDestroy = slice.Filter(match.GrenadeProjectilesDestroy, func(grenade *GrenadeProjectileDestroy, index int) bool {
return grenade.RoundNumber != roundNumber
})
match.HeGrenadesExplode = slice.Filter(match.HeGrenadesExplode, func(grenade *HeGrenadeExplode, index int) bool {
return grenade.RoundNumber != roundNumber
})
match.HostagePickUpStart = slice.Filter(match.HostagePickUpStart, func(hostage *HostagePickUpStart, index int) bool {
return hostage.RoundNumber != roundNumber
})
match.HostagePickedUp = slice.Filter(match.HostagePickedUp, func(hostage *HostagePickedUp, index int) bool {
return hostage.RoundNumber != roundNumber
})
match.HostagePositions = slice.Filter(match.HostagePositions, func(position *HostagePosition, index int) bool {
return position.RoundNumber != roundNumber
})
match.HostageRescued = slice.Filter(match.HostageRescued, func(hostage *HostageRescued, index int) bool {
return hostage.RoundNumber != roundNumber
})
match.InfernoPositions = slice.Filter(match.InfernoPositions, func(position *InfernoPosition, index int) bool {
return position.RoundNumber != roundNumber
})
match.Kills = slice.Filter(match.Kills, func(kill *Kill, index int) bool {
return kill.RoundNumber != roundNumber
})
match.PlayerEconomies = slice.Filter(match.PlayerEconomies, func(eco *PlayerEconomy, index int) bool {
return eco.RoundNumber != roundNumber
})
match.PlayerPositions = slice.Filter(match.PlayerPositions, func(position *PlayerPosition, index int) bool {
return position.RoundNumber != roundNumber
})
match.PlayersBuy = slice.Filter(match.PlayersBuy, func(event *PlayerBuy, index int) bool {
return event.RoundNumber != roundNumber
})
match.PlayersFlashed = slice.Filter(match.PlayersFlashed, func(event *PlayerFlashed, index int) bool {
return event.RoundNumber != roundNumber
})
match.Shots = slice.Filter(match.Shots, func(event *Shot, index int) bool {
return event.RoundNumber != roundNumber
})
match.SmokesStart = slice.Filter(match.SmokesStart, func(event *SmokeStart, index int) bool {
return event.RoundNumber != roundNumber
})
}

func (match *Match) deleteIncompleteRounds() {
for i := len(match.Rounds) - 1; i >= 0; i-- {
round := match.Rounds[i]
Expand All @@ -370,89 +457,6 @@ func (match *Match) deleteIncompleteRounds() {

match.Rounds = append(match.Rounds[:i], match.Rounds[i+1:]...)

match.BombsPlantStart = slice.Filter(match.BombsPlantStart, func(event *BombPlantStart, index int) bool {
return event.RoundNumber != round.Number
})
match.BombsPlanted = slice.Filter(match.BombsPlanted, func(event *BombPlanted, index int) bool {
return event.RoundNumber != round.Number
})
match.BombsDefuseStart = slice.Filter(match.BombsDefuseStart, func(event *BombDefuseStart, index int) bool {
return event.RoundNumber != round.Number
})
match.BombsDefused = slice.Filter(match.BombsDefused, func(event *BombDefused, index int) bool {
return event.RoundNumber != round.Number
})
match.BombsExploded = slice.Filter(match.BombsExploded, func(event *BombExploded, index int) bool {
return event.RoundNumber != round.Number
})
match.ChatMessages = slice.Filter(match.ChatMessages, func(msg *ChatMessage, index int) bool {
return msg.RoundNumber != round.Number
})
match.ChickenDeaths = slice.Filter(match.ChickenDeaths, func(death *ChickenDeath, index int) bool {
return death.RoundNumber != round.Number
})
match.ChickenPositions = slice.Filter(match.ChickenPositions, func(position *ChickenPosition, index int) bool {
return position.RoundNumber != round.Number
})
match.Clutches = slice.Filter(match.Clutches, func(clutch *Clutch, index int) bool {
return clutch.RoundNumber != round.Number
})
match.Damages = slice.Filter(match.Damages, func(damage *Damage, index int) bool {
return damage.RoundNumber != round.Number
})
match.DecoysStart = slice.Filter(match.DecoysStart, func(decoy *DecoyStart, index int) bool {
return decoy.RoundNumber != round.Number
})
match.FlashbangsExplode = slice.Filter(match.FlashbangsExplode, func(flash *FlashbangExplode, index int) bool {
return flash.RoundNumber != round.Number
})
match.GrenadeBounces = slice.Filter(match.GrenadeBounces, func(grenade *GrenadeBounce, index int) bool {
return grenade.RoundNumber != round.Number
})
match.GrenadePositions = slice.Filter(match.GrenadePositions, func(position *GrenadePosition, index int) bool {
return position.RoundNumber != round.Number
})
match.GrenadeProjectilesDestroy = slice.Filter(match.GrenadeProjectilesDestroy, func(grenade *GrenadeProjectileDestroy, index int) bool {
return grenade.RoundNumber != round.Number
})
match.HeGrenadesExplode = slice.Filter(match.HeGrenadesExplode, func(grenade *HeGrenadeExplode, index int) bool {
return grenade.RoundNumber != round.Number
})
match.HostagePickUpStart = slice.Filter(match.HostagePickUpStart, func(hostage *HostagePickUpStart, index int) bool {
return hostage.RoundNumber != round.Number
})
match.HostagePickedUp = slice.Filter(match.HostagePickedUp, func(hostage *HostagePickedUp, index int) bool {
return hostage.RoundNumber != round.Number
})
match.HostagePositions = slice.Filter(match.HostagePositions, func(position *HostagePosition, index int) bool {
return position.RoundNumber != round.Number
})
match.HostageRescued = slice.Filter(match.HostageRescued, func(hostage *HostageRescued, index int) bool {
return hostage.RoundNumber != round.Number
})
match.InfernoPositions = slice.Filter(match.InfernoPositions, func(position *InfernoPosition, index int) bool {
return position.RoundNumber != round.Number
})
match.Kills = slice.Filter(match.Kills, func(kill *Kill, index int) bool {
return kill.RoundNumber != round.Number
})
match.PlayerEconomies = slice.Filter(match.PlayerEconomies, func(eco *PlayerEconomy, index int) bool {
return eco.RoundNumber != round.Number
})
match.PlayerPositions = slice.Filter(match.PlayerPositions, func(position *PlayerPosition, index int) bool {
return position.RoundNumber != round.Number
})
match.PlayersBuy = slice.Filter(match.PlayersBuy, func(event *PlayerBuy, index int) bool {
return event.RoundNumber != round.Number
})
match.PlayersFlashed = slice.Filter(match.PlayersFlashed, func(event *PlayerFlashed, index int) bool {
return event.RoundNumber != round.Number
})
match.Shots = slice.Filter(match.Shots, func(event *Shot, index int) bool {
return event.RoundNumber != round.Number
})
match.SmokesStart = slice.Filter(match.SmokesStart, func(event *SmokeStart, index int) bool {
return event.RoundNumber != round.Number
})
match.resetRound(round.Number)
}
}
Loading

0 comments on commit 459d639

Please sign in to comment.