'OHRRPGCE GAME - in-game support for achievements
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.
'
#include "achievements_runtime.bi"
#include "achievements.bi"
#include "moresubs.bi"
#include "purchase.bi"
#include "steam.bi"
#include "vbcompat.bi" 'for NOW

' -- achievements_runtime
' This file contains functionality related to implementing achievements during gameplay. This includes
' keeping track of seen tags, progress values, etc, as well as logic to reward achievements if
' appropriate.
'
' Assumptions:
' * achievement definitions (achievements.rbas) have been loaded
' * the indexes in achievement_progress match the indexes in the achievement definitions
' 
' IDs (in the type-member sense) are _not_ used to match achievements. These IDs are only for save data


' uncomment to enable spammy debug output
' #define DEBUG_ACHIEVEMENTS

using Reload

namespace Achievements

  declare function evaluate_a_tag(index as integer, tag as integer) as boolean
  declare function needs_rewarding(index as integer) as boolean
  declare function needs_progress_update(index as integer) as boolean
  declare sub reward_achievement(index as integer)

  ' -- globals --
  dim shared achievement_progress() as AchievementProgress
  dim shared save_loaded as boolean = false

  ' -- public api --

  sub evaluate_tags()
    ach_verbose("achievements_evaluate_tags")

    if save_loaded = false then
      ach_verbose("save is not loaded, aborting evaluation")
      return 'don't evaluate anything before the save has actually been loaded
    end if

    for index as integer = 0 to ubound(achievement_progress)
      if achievement_progress(index).rewarded then continue for ' ignore rewarded achievements

      with get_definition_by_index(index)
        ach_verbose("evaluating idx=" & index & " id=" & .id & " '" & .name & "'")
        for t as integer = 0 to v_len(.tags) - 1
          dim tag as integer = .tags[t]

          if evaluate_a_tag(index, tag) then
            if needs_rewarding(index) then
              reward_achievement index
            elseif needs_progress_update(index) then
              ' ping user with progress
              ach_debug("Steam: notifying progress on '" & .name & "'")
              Steam.notify_achievement_progress .steam_id, achievement_progress(index).value, .max_value
            end if
          end if
        next
      end with
    next
  end sub

  ' Disables achievement evaluation
  sub runtime_reset
    dim count as integer = definitions_count()
    ach_verbose("resetting achievement data to " & count)

    if count = 0 then
      erase achievement_progress
    else
      redim achievement_progress(count - 1)
    end if
    
    save_loaded = false
  end sub

  ' node may be null.
  sub runtime_load(node as Reload.NodePtr)
    runtime_reset

    ach_debug("loading save game")

    for index as integer = 0 to ubound(achievement_progress)
      achievement_progress(index).id = get_definition_by_index(index).id
    next

    if node <> null then
      READNODE node
        WITHNODE node."achievement" as achievement
          dim progress as AchievementProgress ptr = null
          dim id as integer = GetInteger(achievement)

          for index as integer = 0 to ubound(achievement_progress)
            if achievement_progress(index).id = id then
              progress = @achievement_progress(index)
              exit for
            end if
          next

          if progress <> null then
            progress->value = achievement."value".integer
            progress->rewarded = achievement."rewarded".exists
            progress->rewarded_date = achievement."rewarded_date".double

            ach_debug("loading progress of #" & id & ": value=" & progress->value & " rewarded=" & progress->rewarded)
            dim tag as Nodeptr = FirstChild(achievement."tags".ptr, "tag")
            while tag <> null
              dim t as integer = GetInteger(tag)
              ach_debug("  seen tag " & t)
              v_append progress->seen_tags, t
              tag = NextSibling(tag, "tag")
            wend
          else
            ach_debug("Encountered data for missing achievement with id " & id & ". Discarding")
          end if
        END WITHNODE
      END READNODE
    end if

    save_loaded = true

    #ifndef __FB_BLACKBOX__
      ' Update awarded status in Steam, in case we previously couldn't (e.g. a new achievement that's
      ' been added to a game, or not previously running under Steam).
      ' However we mustn't resend achievements on Blackbox (unless they're actually regained, in a new game).
      if Steam.available then
        for index as integer = 0 to ubound(achievement_progress)
          if achievement_progress(index).rewarded then
            with get_definition_by_index(index)
              if .steam_id <> "" then
                ach_debug("Steam: rewarding #" & .id & " '" & .name & "' Steam ID '" & .steam_id & "'")
                Steam.reward_achievement .steam_id
              end if
            end with
          end if
        next
      end if
    #endif

    ach_debug("done loading save game")
  end sub

  sub runtime_save(node as NodePtr)
    ach_debug("saving achievement data")

    if ubound(achievement_progress) < 0 then
      ach_debug("no achievements to save")
      return
    end if

    dim achievements_node as NodePtr = AppendChildNode(node, "achievements")

    for index as integer = 0 to ubound(achievement_progress)
      with achievement_progress(index)
        ach_debug("State of achievement id " & .id & ": value=" & .value & ", tags=" & v_str(.seen_tags))

        if .value <= 0 andalso .rewarded = false andalso v_len(.seen_tags) <= 0 then continue for

        dim achievement as NodePtr = AppendChildNode(achievements_node, "achievement", .id)
        AppendChildNode(achievement, "value", .value)
        if .rewarded then
          SetChildNodeDate(achievement, "rewarded", NOW())
        end if

        dim tags as NodePtr = AppendChildNode(achievement, "tags")
        for t as integer = 0 to v_len(.seen_tags) - 1
          AppendChildNode(tags, "tag", .seen_tags[t])
        next
      end with
    next
  end sub

  ' -- members for AchievementProgress --

  constructor AchievementProgress
    v_new seen_tags
  end constructor

  destructor AchievementProgress
    v_free seen_tags
  end destructor

  ' -- internal functions --

  private function evaluate_a_tag(index as integer, tag as integer) as boolean
    ach_verbose("evaluate_a_tag(" & index & ", " & tag & ")")

    dim byref progress as AchievementProgress = achievement_progress(index)

    if progress.rewarded then return false ' ignore rewarded achievements

    with get_definition_by_index(index)
      dim is_triggered as boolean = istag(tag)
      dim ix as integer = v_find(progress.seen_tags, tag)
      ach_verbose("Relevant tag " & tag & " is " & is_triggered)
      if is_triggered and ix = -1 then
        v_append progress.seen_tags, tag
        if .achievement_type = AchievementType.count andalso progress.value < .max_value then
          progress.value += 1
        end if
        ach_debug(.id & ": tag " & tag & " turned on, new value=" & progress.value & " of max=" & .max_value)
        return true
      elseif .latching = false andalso is_triggered = false andalso ix <> -1 then
        v_remove progress.seen_tags, tag
        ' note: disabling a tag does _not_ decrement the value
        ' for this reason, we return false since the achievement state didn't really change
        ach_debug(.id & ": tag " & tag & " turned off")
        return false
      end if
    end with

    return false
  end function

  private function needs_rewarding(index as integer) as boolean
    ach_verbose("needs_rewarding(" & index & ")")
    dim byref achievement as AchievementDefinition = get_definition_by_index(index)
    dim byref progress as AchievementProgress = achievement_progress(index)

    if progress.rewarded then return false ' ignore rewarded achievements

    select case achievement.achievement_type
      case AchievementType.flag
        for ix as integer = 0 to v_len(achievement.tags) - 1
          dim tag as integer = achievement.tags[ix]
          if v_find(progress.seen_tags, tag) = -1 then
            ach_verbose(":( " & achievement.id & " is not complete because tag " & tag & " is not set")
            return false
          end if
        next
      case AchievementType.count
        if progress.value < achievement.max_value then
          ach_verbose(":( " & achievement.id & " is not complete because value " & progress.value & " is less than " & achievement.max_value)
          return false
        end if
    end select
    ach_debug(achievement.id & " '" & achievement.name & "' is complete!")
    return true
  end function

  ' call this _after_ checking is_complete!
  private function needs_progress_update(index as integer) as boolean
    ach_verbose("needs_progress_update(" & index & ")")
    dim byref achievement as AchievementDefinition = get_definition_by_index(index)
    dim byref progress as AchievementProgress = achievement_progress(index)

    ach_verbose((achievement.achievement_type = AchievementType.count) & _
      ", " & (achievement.progress_interval > 0) & _
      ", " & (progress.value > 0) & _
      ", " & (progress.value mod achievement.progress_interval) _
    )
    return achievement.achievement_type = AchievementType.count _
      andalso achievement.progress_interval > 0 _
      andalso progress.value > 0 _
      andalso (progress.value mod achievement.progress_interval) = 0
  end function

  private sub reward_achievement(index as integer)
    dim needs_persist as boolean = false

    with get_definition_by_index(index)
      ach_debug("Rewarding #" & .id & " '" & .name & "' Steam ID '" & .steam_id & "'")
      if .steam_id <> "" then
        Steam.reward_achievement .steam_id
      end if
    end with

    with achievement_progress(index)
      .rewarded = true
      .rewarded_date = NOW

      ' if permanent, persist in global store
      if is_permanent() then
        dim persist as NodePtr = get_persist_reld()
        dim path as string = "/achievements/achievement[" & .id & "]"
        if NodeByPath(persist, path) = 0 then
          ach_debug("...not in persist.reld... adding now!")
          dim achievement as NodePtr = NodeByPath(persist, path, true)
          SetChildNodeDate(achievement, "rewarded_date", NOW())
          write_persist_reld
        else
          ach_debug("...already in persist.reld, skip adding")
        end if
      else
        ach_debug("...is per-save, not adding to persist")
      end if
    end with
  end sub

end namespace
