diff --git a/docs/mister.md b/docs/mister.md index 8141c5db..cf498d7e 100644 --- a/docs/mister.md +++ b/docs/mister.md @@ -159,6 +159,21 @@ With this configuration, removing a token will not exit the game when using the The core name is the same as the name that shows on the left sidebar of the OSD when in a core. +### Exit Game Delay + +| Key | Default Value | +|-----------------------|---------------| +| `exit_game_delay` | 0 | + +A number, in seconds, that TapTo will wait between the removal of the card and reloading the menu core. Requires `exit_game` set to yes. This parameter is useful if you want to swap games without reloading the menu core. +If a new card is tapped before the menu core is loaded, the command on the card will be executed immediately and the menu core loading will be cancelled. + +```ini +[tapto] +exit_game=yes +exit_game_delay=6 +``` + ## Mappings Database TapTo supports an `nfc.csv` file in the top of the SD card. This file can be used to override the text read from a tag and map it to a different text value. This is useful for mapping Amiibos which are read-only, testing text values before actually writing them, and is necessary for using the `command` custom command by default. diff --git a/pkg/config/user.go b/pkg/config/user.go index b0e29e1f..efe64c3a 100644 --- a/pkg/config/user.go +++ b/pkg/config/user.go @@ -40,6 +40,7 @@ type TapToConfig struct { ProbeDevice bool `ini:"probe_device,omitempty"` ExitGame bool `ini:"exit_game,omitempty"` ExitGameBlocklist []string `ini:"exit_game_blocklist,omitempty"` + ExitGameDelay int8 `ini:"exit_game_delay"` Debug bool `ini:"debug,omitempty"` } @@ -122,6 +123,12 @@ func (c *UserConfig) GetExitGameBlocklist() []string { return c.TapTo.ExitGameBlocklist } +func (c *UserConfig) GetExitGameDelay() int8 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.TapTo.ExitGameDelay +} + func (c *UserConfig) SetExitGameBlocklist(exitGameBlocklist []string) { c.mu.Lock() defer c.mu.Unlock() diff --git a/pkg/daemon/reader.go b/pkg/daemon/reader.go index 8c4bc86c..f05fcbc7 100644 --- a/pkg/daemon/reader.go +++ b/pkg/daemon/reader.go @@ -9,6 +9,7 @@ import ( "github.com/clausecker/nfc/v2" "github.com/rs/zerolog/log" + mrextConfig "github.com/wizzomafizzo/mrext/pkg/config" "github.com/wizzomafizzo/mrext/pkg/input" "github.com/wizzomafizzo/tapto/pkg/config" "github.com/wizzomafizzo/tapto/pkg/daemon/state" @@ -163,15 +164,27 @@ func OpenDeviceWithRetries(cfg *config.UserConfig, st *state.State, quiet bool) } func shouldExit( - removed bool, + candidateForRemove bool, cfg *config.UserConfig, st *state.State, ) bool { - if !removed || st.GetLastScanned().FromApi || st.IsLauncherDisabled() { + // do not exit from menu, there is nowhere to go anyway + if mister.GetActiveCoreName() == mrextConfig.MenuCore { return false } - if cfg.GetExitGame() && !inExitGameBlocklist(cfg) { + // candidateForRemove is true from the moment in which we remove a card + if !candidateForRemove || st.GetLastScanned().FromApi || st.IsLauncherDisabled() { + return false + } + + var hasTimePassed bool = false + var removalTime = st.GetCardRemovalTime() + if !removalTime.IsZero() { + hasTimePassed = int8(time.Since(removalTime).Seconds()) >= cfg.GetExitGameDelay() + } + + if hasTimePassed && cfg.GetExitGame() && !inExitGameBlocklist(cfg) { return true } else { return false @@ -189,8 +202,9 @@ func readerPollLoop( ttp := TimesToPoll pbp := PeriodBetweenPolls - var lastError time.Time + var candidateForRemove bool + var currentlyLoadedCard state.Token playFail := func() { if time.Since(lastError) > 1*time.Second { mister.PlayFail(cfg) @@ -237,6 +251,8 @@ func readerPollLoop( log.Info().Msgf("opened connection: %s %s", pnd, pnd.Connection()) } + // activeCard is the card that sat on the scanner at the previous poll loop. + // is not the card representing the current loaded core activeCard := st.GetActiveCard() writeRequest := st.GetWriteRequest() @@ -301,6 +317,18 @@ func readerPollLoop( newScanned, removed, err := pollDevice(cfg, &pnd, activeCard, ttp, pbp) + // if we removed but we weren't removing already, start the remove countdown + if removed && candidateForRemove == false { + st.SetCardRemovalTime(time.Now()) + candidateForRemove = true + // if we were removing but we put back the card we had before + // then we are ok blocking the exit process + } else if candidateForRemove && (newScanned.UID == currentlyLoadedCard.UID) { + log.Info().Msgf("Card was removed but inserted back") + st.SetCardRemovalTime(time.Time{}) + candidateForRemove = false + } + if errors.Is(err, nfc.Error(nfc.EIO)) { st.SetReaderDisconnected() log.Error().Msgf("error during poll: %s", err) @@ -317,21 +345,33 @@ func readerPollLoop( st.SetActiveCard(newScanned) - if shouldExit(removed, cfg, st) { + if shouldExit(candidateForRemove, cfg, st) { + candidateForRemove = false + st.SetCardRemovalTime(time.Time{}) mister.ExitGame() + currentlyLoadedCard = state.Token{} continue } - if newScanned.UID == "" || activeCard.UID == newScanned.UID { + // if there is no card (newScanned.UID == "") + // if the card is the same as the one we have scanned before ( activeCard.UID == newScanned.UID) + // if the card has no text a real card but empty (newScanned.Text == "") + // if the card is the same that has been loaded last time (newScanned.UID == currentlyLoadedCard.UID) + if newScanned.UID == "" || activeCard.UID == newScanned.UID || newScanned.Text == "" || newScanned.UID == currentlyLoadedCard.UID { continue } + // should we play success if launcher is disabled? mister.PlaySuccess(cfg) if st.IsLauncherDisabled() { continue } + // we are about to change card, so we also stop the exit process + st.SetCardRemovalTime(time.Time{}) + candidateForRemove = false + currentlyLoadedCard = newScanned tq.Enqueue(newScanned) } diff --git a/pkg/daemon/state/state.go b/pkg/daemon/state/state.go index 295f3716..a1a228b6 100644 --- a/pkg/daemon/state/state.go +++ b/pkg/daemon/state/state.go @@ -36,6 +36,7 @@ type State struct { dbLoadTime time.Time uidMap map[string]string textMap map[string]string + cardRemovalTime time.Time } func (s *State) SetUpdateHook(hook *func(st *State)) { @@ -65,6 +66,12 @@ func (s *State) SetActiveCard(card Token) { } } +func (s *State) SetCardRemovalTime(removalTime time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + s.cardRemovalTime = removalTime +} + func (s *State) GetActiveCard() Token { s.mu.RLock() defer s.mu.RUnlock() @@ -77,6 +84,12 @@ func (s *State) GetLastScanned() Token { return s.lastScanned } +func (s *State) GetCardRemovalTime() time.Time { + s.mu.RLock() + defer s.mu.RUnlock() + return s.cardRemovalTime +} + func (s *State) StopService() { s.mu.Lock() s.stopService = true diff --git a/scripts/taptui/taptui.sh b/scripts/taptui/taptui.sh index 508d2a27..6dd9796d 100755 --- a/scripts/taptui/taptui.sh +++ b/scripts/taptui/taptui.sh @@ -783,22 +783,28 @@ _EOF_ _Settings() { local menuOptions selected menuOptions=( - "Service" "Start/stop the TapTo service" - "Commands" "Toggles the ability to run Linux commands from NFC tags" - "Sounds" "Toggles sounds played when a tag is scanned" - "Connection" "Hardware configuration for certain NFC readers" - "Probe" "Auto detection of a serial based reader device" + "Service" "Start/stop the TapTo service" + "Commands" "Toggles the ability to run Linux commands from NFC tags" + "Sounds" "Toggles sounds played when a tag is scanned" + "Connection" "Hardware configuration for certain NFC readers" + "Probe" "Auto detection of a serial based reader device" + "Exit Game" "Exit Game When Token Is Removed" + "Exit Blocklist" "Exit Game Core Blocklist" + "Exit Delay" "Exit Game Delay" ) while true; do selected="$(_menu --cancel-label "Back" -- "${menuOptions[@]}")" exitcode="${?}"; [[ "${exitcode}" -ge 1 ]] && return "${exitcode}" case "${selected}" in - Service) _serviceSetting ;; - Commands) _commandSetting ;; - Sounds) _soundSetting ;; - Connection) _connectionSetting ;; - Probe) _probeSetting ;; + "Service") _serviceSetting ;; + "Commands") _commandSetting ;; + "Sounds") _soundSetting ;; + "Connection") _connectionSetting ;; + "Probe") _probeSetting ;; + "Exit Game") _exitGameSetting ;; + "Exit Blocklist") _exitGameBlocklistSetting ;; + "Exit Delay") _exitGameDelaySetting ;; esac done }