'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