Little Achievements

From The Official Visionaire Studio: Adventure Game Engine Wiki
Name Type By
Little Achievements Definition Einzelkämpfer

This script saves and loads condition states to/from an external file and thus allows the implementation of a basic local achievement system where condition states are available independent from a particular game session.

Achievements are only valid on the device they were obtained and cannot be shared (at least I tried to make cheating difficult enough for the ordinary user). If you release your game on a proper game platform like Steam or GOG, you better make use of their advanced achievement systems.


Instructions

  1. Add the main script to the Visionaire Studio Script Editor and set the script as a definition script.
  2. Add achievement conditions with an initial value of false in the editor (no combined conditions). Make sure their names are unique throughout the project.
  3. Add the names of these conditions to the achvm_conditions table of the script.
  4. Change the secret key.
  5. Call "save_achievements()" whenever you changed one of the achievement conditions in your game.
  6. Call "load_achievements()" after loading a savegame. To do this, add the GameLoaded script as a definition script, too.
    You don't have to set up a new definition script; you can just add the code to the main script instead. You may however want to use the "engineEvent" event handler for other functions as well - that's why I kept it separate. If you already have this event handler defined in your project, just add the "load_achievements()" function to it.


If you want to save the achievements in the config.ini file instead of in a separate file, enable the INI mode. You would have to handle config.ini writing and reading through your own script though.

  1. Call "get_achievements()" from your config.ini script and write the string you get to the .ini file. Rewrite the config.ini whenever you change one of the achievement conditions in your game.
  2. Pass the achievement string read from the config.ini to the "set_achievements()" function. Read the config.ini at game start and after loading a savegame.


Main Script

--[[

Little Achievements
-------------------
Script for saving and loading condition states to/from an external file. It allows implementation of a basic local achievement system where condition states are available independent from a particular game session. Achievements are only valid on the device they were obtained and cannot be shared (at least I tried to make cheating difficult enough for the ordinary user). If you release your game on a proper game platform like Steam or GOG, you better make use of their advanced achievement systems. Enable the INI mode, if you want to save the achievements in the config.ini file instead of in a separate file. You would have to handle config.ini writing and reading through your own script though.

Author:       Einzelkämpfer
Version:      1.2
Date:         2023-05-21
Requirements: Visionaire Studio 5.2 or higher
License:      MIT License (details at the bottom of the script)

How to use (A: Regular mode / B: INI mode):
 1. Add your achievement conditions to the "achvm_conditions" table below and change the "secret_key".
A2. Call "save_achievements()" whenever you change one of the achievement conditions in your game.
A3. Call "load_achievements()" after loading a savegame (see the "GameLoaded" script).
B2. Call "get_achievements()" from your config.ini script and write the string you get to the .ini file.
    Rewrite the config.ini whenever you change one of the achievement conditions in your game.
B3. Pass the achievement string read from the config.ini to the "set_achievements()" function.
    Read the config.ini at game start and after loading a savegame.

ATTENTION: THIS VERSION OF THE SCRIPT HAS ONLY BEEN TESTED ON A SINGLE WINDOWS 10 MACHINE SO FAR, SO IT MAY WORK OR NOT WORK AS INTENDED ON OTHER OPERATING SYSTEMS.
]]



-------------------------------
-- DEFINE YOUR SETTINGS HERE --
-------------------------------

-- Add all your achievement conditions to this table. The conditions have to be set up in the editor with an
-- initial value of false (no combined conditions). Make sure their names are unique throughout the project.
local achvm_conditions = {
  "achievement_condition_1",
  "achievement_condition_2",
  "achievement_condition_3"
}

-- Enter a random string of any length
local secret_key = "gKEA82cajZS5Z8vd"

-- You may change the filename to store the achievements
local achvm_file = "achvm.dat"

-- If you would like to save the achievements in the config.ini instead of in a separate file,
-- set the INI mode to true. In that case you have to read and write the config.ini by yourself
-- and use the set_achievements() and get_achievements() functions in your config.ini script.
local ini_mode = false



--------------------------------------------------------
-- USUALLY NO NEED TO CHANGE ANYTHING BELOW THIS LINE --
--------------------------------------------------------

local achvm_hashed_conds = {}


-- Regular mode: Call this function after loading a game
function load_achievements()
  if not ini_mode then
    local read_file = io.open(localAppDir .. achvm_file, "r")
    local saved_achvm = {}
  
    if read_file then
      for line in read_file:lines() do
        line = string.gsub(line, '\r', '')
        saved_achvm[line] = line
      end

      read_file:close()
    
      for k,v in pairs(achvm_hashed_conds) do
        if(saved_achvm[k] ~= nil) then
          Conditions[v].Value = true
        else
          Conditions[v].Value = false
        end
      end
    end
  end
end


-- Regular mode: Call this function after you changed one of the achievement conditions
function save_achievements()
  if not ini_mode then
    local write_file = io.open(localAppDir .. achvm_file, "w")

    if write_file then
      for k,v in pairs(achvm_hashed_conds) do
        if(Conditions[v].Value) then
          write_file:write(k .. "\n")
        end
      end
    
      write_file:close()
    end
  end
end


-- INI mode: Pass the achievements string from the config.ini file to this function to set the achievement conditions accordingly
function set_achievements(ini_str)
  if ini_mode then
    ini_str = string.lower(ini_str)
  
    if string.sub(ini_str, 1, 15) == "achievements = " then
      local achvm_str = string.sub(ini_str, 16)
      local saved_achvm = {}
    
      if achvm_str ~= "" then
        for i in string.gmatch(achvm_str, "([^|]+)") do
          saved_achvm[i] = i
          print(i)
        end
      end
  
      for k,v in pairs(achvm_hashed_conds) do
        if(saved_achvm[k] ~= nil) then
          Conditions[v].Value = true
        else
          Conditions[v].Value = false
        end
      end
    end
  end
end


-- INI mode: Call this function to get the achievements string to save in the config.ini file
function get_achievements()
  if ini_mode then
    local get_achvm = {}
  
    for k,v in pairs(achvm_hashed_conds) do
      if(Conditions[v].Value) then
        table.insert(get_achvm, k)
      end
    end

    if get_achvm ~= nil then
      return "# Little achievements\nAchievements = " .. table.concat(get_achvm, "|")
    end
  
    return ""
  end
end


-- Try to create a unique hash to avoid cheating
function obfuscate_cond(cond)
  local system_info = system.systemInfo()
  local string = secret_key

  if system_info.cpuModel ~= nil then string = string .. system_info.cpuModel end
  if system_info.gpu ~= nil then string = string .. system_info.gpu end
  if system_info.os ~= nil then string = string .. system_info.os end
  if system_info.ram ~= nil then string = string .. system_info.ram end
  string = string .. localAppDir .. cond

  return sha1(string)
end


-- Initialize the achievements
function achievements_init()
  for k,v in pairs(achvm_conditions) do
    achvm_hashed_conds[ obfuscate_cond(v) ] = v
  end
end


achievements_init()

if not ini_mode then
  load_achievements()
end



--[[

MIT License

Copyright 2023 Einzelkämpfer

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

]]


GameLoaded Script

-- Gets executed right after loading a savegame
function onEngineEvent(event, path) 
  if event == "LoadingSavegameSuccess" then 
    load_achievements()
  end 
end 

registerEventHandler("engineEvent", "onEngineEvent")


Resources

Name Description
Little Achievements 1.2.zip A working example of the script in action. Visionaire Studio 5.2+ is required to run the included .ved file(s).