<commit_before>package demoinfocs

import (
	"errors"
	"fmt"

	"github.com/golang/geo/r3"
	"github.com/markus-wa/go-unassert"

	common "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/common"
	events "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/events"
	msg "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/msg"
)

func (p *parser) handleGameEventList(gel *msg.CSVCMsg_GameEventList) {
	p.gameEventDescs = make(map[int32]*msg.CSVCMsg_GameEventListDescriptorT)
	for _, d := range gel.GetDescriptors() {
		p.gameEventDescs[d.GetEventid()] = d
	}
}

func (p *parser) handleGameEvent(ge *msg.CSVCMsg_GameEvent) {
	if p.gameEventDescs == nil {
		p.eventDispatcher.Dispatch(events.ParserWarn{Message: "received GameEvent but event descriptors are missing"})
		unassert.Error("received GameEvent but event descriptors are missing")

		return
	}

	desc := p.gameEventDescs[ge.Eventid]

	debugGameEvent(desc, ge)

	data := mapGameEventData(desc, ge)

	if handler, eventKnown := p.gameEventHandler.gameEventNameToHandler[desc.Name]; eventKnown {
		// some events are known but have no handler
		// these will just be dispatched as GenericGameEvent
		if handler != nil {
			handler(data)
		}
	} else {
		p.eventDispatcher.Dispatch(events.ParserWarn{Message: fmt.Sprintf("unknown event %q", desc.Name)})
		unassert.Error("unknown event %q", desc.Name)
	}

	p.eventDispatcher.Dispatch(events.GenericGameEvent{
		Name: desc.Name,
		Data: data,
	})
}

type gameEventHandler struct {
	parser                  *parser
	gameEventNameToHandler  map[string]gameEventHandlerFunc
	userIDToFallDamageFrame map[int32]int
	frameToRoundEndReason   map[int]events.RoundEndReason
}

func (geh gameEventHandler) dispatch(event interface{}) {
	geh.parser.eventDispatcher.Dispatch(event)
}

func (geh gameEventHandler) gameState() *gameState {
	return geh.parser.gameState
}

func (geh gameEventHandler) playerByUserID(userID int) *common.Player {
	return geh.gameState().playersByUserID[userID]
}

func (geh gameEventHandler) playerByUserID32(userID int32) *common.Player {
	return geh.playerByUserID(int(userID))
}

type gameEventHandlerFunc func(map[string]*msg.CSVCMsg_GameEventKeyT)

//nolint:funlen
func newGameEventHandler(parser *parser) gameEventHandler {
	geh := gameEventHandler{
		parser:                  parser,
		userIDToFallDamageFrame: make(map[int32]int),
		frameToRoundEndReason:   make(map[int]events.RoundEndReason),
	}

	// some events need to be delayed until their data is available
	// some events can't be delayed because the required state is lost by the end of the tick
	// TODO: maybe we're supposed to delay all of them and store the data we need until the end of the tick
	delay := func(f gameEventHandlerFunc) gameEventHandlerFunc {
		return func(data map[string]*msg.CSVCMsg_GameEventKeyT) {
			parser.delayedEventHandlers = append(parser.delayedEventHandlers, func() {
				f(data)
			})
		}
	}

	// some events only need to be delayed at the start of the demo until players are connected
	delayIfNoPlayers := func(f gameEventHandlerFunc) gameEventHandlerFunc {
		return func(data map[string]*msg.CSVCMsg_GameEventKeyT) {
			if len(parser.gameState.playersByUserID) == 0 {
				delay(f)
			} else {
				f(data)
			}
		}
	}

	geh.gameEventNameToHandler = map[string]gameEventHandlerFunc{
		// sorted alphabetically
		"announce_phase_end":              nil,                                   // Dunno
		"begin_new_match":                 geh.beginNewMatch,                     // Match started
		"bomb_beep":                       nil,                                   // Bomb beep
		"bomb_begindefuse":                delayIfNoPlayers(geh.bombBeginDefuse), // Defuse started
		"bomb_beginplant":                 delayIfNoPlayers(geh.bombBeginPlant),  // Plant started
		"bomb_defused":                    delayIfNoPlayers(geh.bombDefused),     // Defuse finished
		"bomb_dropped":                    delayIfNoPlayers(geh.bombDropped),     // Bomb dropped
		"bomb_exploded":                   delayIfNoPlayers(geh.bombExploded),    // Bomb exploded
		"bomb_pickup":                     delayIfNoPlayers(geh.bombPickup),      // Bomb picked up
		"bomb_planted":                    delayIfNoPlayers(geh.bombPlanted),     // Plant finished
		"bot_takeover":                    delay(geh.botTakeover),                // Bot got taken over
		"buytime_ended":                   nil,                                   // Not actually end of buy time, seems to only be sent once per game at the start
		"cs_match_end_restart":            nil,                                   // Yawn
		"cs_pre_restart":                  nil,                                   // Not sure, doesn't seem to be important
		"cs_round_final_beep":             nil,                                   // Final beep
		"cs_round_start_beep":             nil,                                   // Round start beeps
		"cs_win_panel_match":              geh.csWinPanelMatch,                   // Not sure, maybe match end event???
		"cs_win_panel_round":              nil,                                   // Win panel, (==end of match?)
		"decoy_detonate":                  geh.decoyDetonate,                     // Decoy exploded/expired
		"decoy_started":                   delay(geh.decoyStarted),               // Decoy started. Delayed because projectile entity is not yet created
		"endmatch_cmm_start_reveal_items": nil,                                   // Drops
		"entity_visible":                  nil,                                   // Dunno, only in locally recorded demo
		"enter_bombzone":                  nil,                                   // Dunno, only in locally recorded demo
		"exit_bombzone":                   nil,                                   // Dunno, only in locally recorded demo
		"enter_buyzone":                   nil,                                   // Dunno, only in locally recorded demo
		"exit_buyzone":                    nil,                                   // Dunno, only in locally recorded demo
		"flashbang_detonate":              geh.flashBangDetonate,                 // Flash exploded
		"hegrenade_detonate":              geh.heGrenadeDetonate,                 // HE exploded
		"hltv_chase":                      nil,                                   // Don't care
		"hltv_fixed":                      nil,                                   // Dunno
		"hltv_message":                    nil,                                   // No clue
		"hltv_status":                     nil,                                   // Don't know
		"inferno_expire":                  geh.infernoExpire,                     // Incendiary expired
		"inferno_startburn":               delay(geh.infernoStartBurn),           // Incendiary exploded/started. Delayed because inferno entity is not yet created
		"inspect_weapon":                  nil,                                   // Dunno, only in locally recorded demo
		"item_equip":                      delay(geh.itemEquip),                  // Equipped / weapon swap, I think. Delayed because of #142 - Bot entity possibly not yet created
		"item_pickup":                     delay(geh.itemPickup),                 // Picked up or bought? Delayed because of #119 - Equipment.UniqueID()
		"item_remove":                     geh.itemRemove,                        // Dropped?
		"jointeam_failed":                 nil,                                   // Dunno, only in locally recorded demo
		"other_death":                     nil,                                   // Dunno
		"player_blind":                    delay(geh.playerBlind),                // Player got blinded by a flash. Delayed because Player.FlashDuration hasn't been updated yet
		"player_changename":               nil,                                   // Name change
		"player_connect":                  geh.playerConnect,                     // Bot connected or player reconnected, players normally come in via string tables & data tables
		"player_connect_full":             nil,                                   // Connecting finished
		"player_death":                    delayIfNoPlayers(geh.playerDeath),     // Player died
		"player_disconnect":               geh.playerDisconnect,                  // Player disconnected (kicked, quit, timed out etc.)
		"player_falldamage":               geh.playerFallDamage,                  // Falldamage
		"player_footstep":                 delayIfNoPlayers(geh.playerFootstep),  // Footstep sound.- Delayed because otherwise Player might be nil
		"player_hurt":                     geh.playerHurt,                        // Player got hurt
		"player_jump":                     geh.playerJump,                        // Player jumped
		"player_spawn":                    nil,                                   // Player spawn
		"player_given_c4":                 nil,                                   // Dunno, only present in POV demos

		// Player changed team. Delayed for two reasons
		// - team IDs of other players changing teams in the same tick might not have changed yet
		// - player entities might not have been re-created yet after a reconnect
		"player_team":                    delay(geh.playerTeam),
		"round_announce_final":           geh.roundAnnounceFinal,           // 30th round for normal de_, not necessarily matchpoint
		"round_announce_last_round_half": geh.roundAnnounceLastRoundHalf,   // Last round of the half
		"round_announce_match_point":     nil,                              // Match point announcement
		"round_announce_match_start":     nil,                              // Special match start announcement
		"round_announce_warmup":          nil,                              // Dunno
		"round_end":                      geh.roundEnd,                     // Round ended and the winner was announced
		"round_end_upload_stats":         nil,                              // Dunno, only present in POV demos
		"round_freeze_end":               geh.roundFreezeEnd,               // Round start freeze ended
		"round_mvp":                      geh.roundMVP,                     // Round MVP was announced
		"round_officially_ended":         geh.roundOfficiallyEnded,         // The event after which you get teleported to the spawn (=> You can still walk around between round_end and this event)
		"round_poststart":                nil,                              // Ditto
		"round_prestart":                 nil,                              // Ditto
		"round_start":                    geh.roundStart,                   // Round started
		"round_time_warning":             nil,                              // Round time warning
		"server_cvar":                    nil,                              // Dunno
		"smokegrenade_detonate":          geh.smokeGrenadeDetonate,         // Smoke popped
		"smokegrenade_expired":           geh.smokeGrenadeExpired,          // Smoke expired
		"switch_team":                    nil,                              // Dunno, only present in POV demos
		"tournament_reward":              nil,                              // Dunno
		"weapon_fire":                    delayIfNoPlayers(geh.weaponFire), // Weapon was fired
		"weapon_fire_on_empty":           nil,                              // Sounds boring
		"weapon_reload":                  geh.weaponReload,                 // Weapon reloaded
		"weapon_zoom":                    nil,                              // Zooming in
		"weapon_zoom_rifle":              nil,                              // Dunno, only in locally recorded demo
	}

	return geh
}

func (geh gameEventHandler) roundStart(data map[string]*msg.CSVCMsg_GameEventKeyT) {
	geh.dispatch(events.RoundStart{
		TimeLimit: int(data["timelimit"].GetValLong()),
		FragLimit: int(data["fraglimit"].GetValLong()),
		Objective: data["objective"].GetValString(),
	})<commit_msg>game events: add POV-only game-events as known events<commit_after>package demoinfocs

import (
	"errors"
	"fmt"

	"github.com/golang/geo/r3"
	"github.com/markus-wa/go-unassert"

	common "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/common"
	events "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/events"
	msg "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/msg"
)

func (p *parser) handleGameEventList(gel *msg.CSVCMsg_GameEventList) {
	p.gameEventDescs = make(map[int32]*msg.CSVCMsg_GameEventListDescriptorT)
	for _, d := range gel.GetDescriptors() {
		p.gameEventDescs[d.GetEventid()] = d
	}
}

func (p *parser) handleGameEvent(ge *msg.CSVCMsg_GameEvent) {
	if p.gameEventDescs == nil {
		p.eventDispatcher.Dispatch(events.ParserWarn{Message: "received GameEvent but event descriptors are missing"})
		unassert.Error("received GameEvent but event descriptors are missing")

		return
	}

	desc := p.gameEventDescs[ge.Eventid]

	debugGameEvent(desc, ge)

	data := mapGameEventData(desc, ge)

	if handler, eventKnown := p.gameEventHandler.gameEventNameToHandler[desc.Name]; eventKnown {
		// some events are known but have no handler
		// these will just be dispatched as GenericGameEvent
		if handler != nil {
			handler(data)
		}
	} else {
		p.eventDispatcher.Dispatch(events.ParserWarn{Message: fmt.Sprintf("unknown event %q", desc.Name)})
		unassert.Error("unknown event %q", desc.Name)
	}

	p.eventDispatcher.Dispatch(events.GenericGameEvent{
		Name: desc.Name,
		Data: data,
	})
}

type gameEventHandler struct {
	parser                  *parser
	gameEventNameToHandler  map[string]gameEventHandlerFunc
	userIDToFallDamageFrame map[int32]int
	frameToRoundEndReason   map[int]events.RoundEndReason
}

func (geh gameEventHandler) dispatch(event interface{}) {
	geh.parser.eventDispatcher.Dispatch(event)
}

func (geh gameEventHandler) gameState() *gameState {
	return geh.parser.gameState
}

func (geh gameEventHandler) playerByUserID(userID int) *common.Player {
	return geh.gameState().playersByUserID[userID]
}

func (geh gameEventHandler) playerByUserID32(userID int32) *common.Player {
	return geh.playerByUserID(int(userID))
}

type gameEventHandlerFunc func(map[string]*msg.CSVCMsg_GameEventKeyT)

//nolint:funlen
func newGameEventHandler(parser *parser) gameEventHandler {
	geh := gameEventHandler{
		parser:                  parser,
		userIDToFallDamageFrame: make(map[int32]int),
		frameToRoundEndReason:   make(map[int]events.RoundEndReason),
	}

	// some events need to be delayed until their data is available
	// some events can't be delayed because the required state is lost by the end of the tick
	// TODO: maybe we're supposed to delay all of them and store the data we need until the end of the tick
	delay := func(f gameEventHandlerFunc) gameEventHandlerFunc {
		return func(data map[string]*msg.CSVCMsg_GameEventKeyT) {
			parser.delayedEventHandlers = append(parser.delayedEventHandlers, func() {
				f(data)
			})
		}
	}

	// some events only need to be delayed at the start of the demo until players are connected
	delayIfNoPlayers := func(f gameEventHandlerFunc) gameEventHandlerFunc {
		return func(data map[string]*msg.CSVCMsg_GameEventKeyT) {
			if len(parser.gameState.playersByUserID) == 0 {
				delay(f)
			} else {
				f(data)
			}
		}
	}

	geh.gameEventNameToHandler = map[string]gameEventHandlerFunc{
		// sorted alphabetically
		"ammo_pickup":                     nil,                                   // Dunno, only in locally recorded (POV) demo
		"announce_phase_end":              nil,                                   // Dunno
		"begin_new_match":                 geh.beginNewMatch,                     // Match started
		"bomb_beep":                       nil,                                   // Bomb beep
		"bomb_begindefuse":                delayIfNoPlayers(geh.bombBeginDefuse), // Defuse started
		"bomb_beginplant":                 delayIfNoPlayers(geh.bombBeginPlant),  // Plant started
		"bomb_defused":                    delayIfNoPlayers(geh.bombDefused),     // Defuse finished
		"bomb_dropped":                    delayIfNoPlayers(geh.bombDropped),     // Bomb dropped
		"bomb_exploded":                   delayIfNoPlayers(geh.bombExploded),    // Bomb exploded
		"bomb_pickup":                     delayIfNoPlayers(geh.bombPickup),      // Bomb picked up
		"bomb_planted":                    delayIfNoPlayers(geh.bombPlanted),     // Plant finished
		"bot_takeover":                    delay(geh.botTakeover),                // Bot got taken over
		"buytime_ended":                   nil,                                   // Not actually end of buy time, seems to only be sent once per game at the start
		"cs_intermission":                 nil,                                   // Dunno, only in locally recorded (POV) demo
		"cs_match_end_restart":            nil,                                   // Yawn
		"cs_pre_restart":                  nil,                                   // Not sure, doesn't seem to be important
		"cs_round_final_beep":             nil,                                   // Final beep
		"cs_round_start_beep":             nil,                                   // Round start beeps
		"cs_win_panel_match":              geh.csWinPanelMatch,                   // Not sure, maybe match end event???
		"cs_win_panel_round":              nil,                                   // Win panel, (==end of match?)
		"decoy_detonate":                  geh.decoyDetonate,                     // Decoy exploded/expired
		"decoy_started":                   delay(geh.decoyStarted),               // Decoy started. Delayed because projectile entity is not yet created
		"endmatch_cmm_start_reveal_items": nil,                                   // Drops
		"entity_visible":                  nil,                                   // Dunno, only in locally recorded (POV) demo
		"enter_bombzone":                  nil,                                   // Dunno, only in locally recorded (POV) demo
		"exit_bombzone":                   nil,                                   // Dunno, only in locally recorded (POV) demo
		"enter_buyzone":                   nil,                                   // Dunno, only in locally recorded (POV) demo
		"exit_buyzone":                    nil,                                   // Dunno, only in locally recorded (POV) demo
		"flashbang_detonate":              geh.flashBangDetonate,                 // Flash exploded
		"hegrenade_detonate":              geh.heGrenadeDetonate,                 // HE exploded
		"hltv_chase":                      nil,                                   // Don't care
		"hltv_fixed":                      nil,                                   // Dunno
		"hltv_message":                    nil,                                   // No clue
		"hltv_status":                     nil,                                   // Don't know
		"hostname_changed":                nil,                                   // Only present in locally recorded (POV) demos
		"inferno_expire":                  geh.infernoExpire,                     // Incendiary expired
		"inferno_startburn":               delay(geh.infernoStartBurn),           // Incendiary exploded/started. Delayed because inferno entity is not yet created
		"inspect_weapon":                  nil,                                   // Dunno, only in locally recorded (POV) demos
		"item_equip":                      delay(geh.itemEquip),                  // Equipped / weapon swap, I think. Delayed because of #142 - Bot entity possibly not yet created
		"item_pickup":                     delay(geh.itemPickup),                 // Picked up or bought? Delayed because of #119 - Equipment.UniqueID()
		"item_pickup_slerp":               nil,                                   // Not sure, only in locally recorded (POV) demos
		"item_remove":                     geh.itemRemove,                        // Dropped?
		"jointeam_failed":                 nil,                                   // Dunno, only in locally recorded (POV) demos
		"other_death":                     nil,                                   // Dunno
		"player_blind":                    delay(geh.playerBlind),                // Player got blinded by a flash. Delayed because Player.FlashDuration hasn't been updated yet
		"player_changename":               nil,                                   // Name change
		"player_connect":                  geh.playerConnect,                     // Bot connected or player reconnected, players normally come in via string tables & data tables
		"player_connect_full":             nil,                                   // Connecting finished
		"player_death":                    delayIfNoPlayers(geh.playerDeath),     // Player died
		"player_disconnect":               geh.playerDisconnect,                  // Player disconnected (kicked, quit, timed out etc.)
		"player_falldamage":               geh.playerFallDamage,                  // Falldamage
		"player_footstep":                 delayIfNoPlayers(geh.playerFootstep),  // Footstep sound.- Delayed because otherwise Player might be nil
		"player_hurt":                     geh.playerHurt,                        // Player got hurt
		"player_jump":                     geh.playerJump,                        // Player jumped
		"player_spawn":                    nil,                                   // Player spawn
		"player_spawned":                  nil,                                   // Only present in locally recorded (POV) demos
		"player_given_c4":                 nil,                                   // Dunno, only present in locally recorded (POV) demos

		// Player changed team. Delayed for two reasons
		// - team IDs of other players changing teams in the same tick might not have changed yet
		// - player entities might not have been re-created yet after a reconnect
		"player_team":                    delay(geh.playerTeam),
		"round_announce_final":           geh.roundAnnounceFinal,           // 30th round for normal de_, not necessarily matchpoint
		"round_announce_last_round_half": geh.roundAnnounceLastRoundHalf,   // Last round of the half
		"round_announce_match_point":     nil,                              // Match point announcement
		"round_announce_match_start":     nil,                              // Special match start announcement
		"round_announce_warmup":          nil,                              // Dunno
		"round_end":                      geh.roundEnd,                     // Round ended and the winner was announced
		"round_end_upload_stats":         nil,                              // Dunno, only present in POV demos
		"round_freeze_end":               geh.roundFreezeEnd,               // Round start freeze ended
		"round_mvp":                      geh.roundMVP,                     // Round MVP was announced
		"round_officially_ended":         geh.roundOfficiallyEnded,         // The event after which you get teleported to the spawn (=> You can still walk around between round_end and this event)
		"round_poststart":                nil,                              // Ditto
		"round_prestart":                 nil,                              // Ditto
		"round_start":                    geh.roundStart,                   // Round started
		"round_time_warning":             nil,                              // Round time warning
		"server_cvar":                    nil,                              // Dunno
		"smokegrenade_detonate":          geh.smokeGrenadeDetonate,         // Smoke popped
		"smokegrenade_expired":           geh.smokeGrenadeExpired,          // Smoke expired
		"switch_team":                    nil,                              // Dunno, only present in POV demos
		"tournament_reward":              nil,                              // Dunno
		"weapon_fire":                    delayIfNoPlayers(geh.weaponFire), // Weapon was fired
		"weapon_fire_on_empty":           nil,                              // Sounds boring
		"weapon_reload":                  geh.weaponReload,                 // Weapon reloaded
		"weapon_zoom":                    nil,                              // Zooming in
		"weapon_zoom_rifle":              nil,                              // Dunno, only in locally recorded (POV) demo
	}

	return geh
}

func (geh gameEventHandler) roundStart(data map[string]*msg.CSVCMsg_GameEventKeyT) {
	geh.dispatch(events.RoundStart{
		TimeLimit: int(data["timelimit"].GetValLong()),
		FragLimit: int(data["fraglimit"].GetValLong()),
		Objective: data["objective"].GetValString(),
	})
{'fraction_non_alphanumeric': 0.08452200923693137, 'fraction_numerical': 0.001961826133158949, 'mean_word_length': 1.6261672212085436, 'pattern_counts': {'":': 159, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 152, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3803689', 'n_tokens_mistral': 7117, 'n_tokens_neox': 6470, 'n_words': 1615}