From 29a1b97b3849daaf07e2d3789b54817101e9061c Mon Sep 17 00:00:00 2001
From: rubenwardy <rw@rubenwardy.com>
Date: Thu, 13 Sep 2018 17:31:58 +0100
Subject: [PATCH] Improve code structure and awards formspec size

---
 api.lua                                    | 529 ---------------------
 init.lua                                   |  18 +-
 src/api_awards.lua                         | 179 +++++++
 src/api_triggers.lua                       | 215 +++++++++
 awards.lua => src/awards.lua               |   0
 chat_commands.lua => src/chat_commands.lua |   0
 src/data.lua                               | 111 +++++
 gui.lua => src/gui.lua                     |  40 +-
 intllib.lua => src/intllib.lua             |   0
 triggers.lua => src/triggers.lua           |   0
 10 files changed, 547 insertions(+), 545 deletions(-)
 delete mode 100644 api.lua
 create mode 100644 src/api_awards.lua
 create mode 100644 src/api_triggers.lua
 rename awards.lua => src/awards.lua (100%)
 rename chat_commands.lua => src/chat_commands.lua (100%)
 create mode 100644 src/data.lua
 rename gui.lua => src/gui.lua (84%)
 rename intllib.lua => src/intllib.lua (100%)
 rename triggers.lua => src/triggers.lua (100%)

diff --git a/api.lua b/api.lua
deleted file mode 100644
index e625785..0000000
--- a/api.lua
+++ /dev/null
@@ -1,529 +0,0 @@
--- Copyright (c) 2013-18 rubenwardy. MIT.
-
-local S, NS = awards.gettext, awards.ngettext
-
-awards.registered_awards = {}
-awards.on = {}
-awards.on_unlock = {}
-
-local storage = minetest.get_mod_storage()
-
--- Table Save Load Functions
-function awards.save()
-	storage:set_string("player_data", minetest.write_json(awards.players))
-end
-
-local function convert_data()
-	minetest.log("warning", "Importing awards data from previous version")
-
-	local old_players = awards.players
-	awards.players = {}
-	for name, data in pairs(old_players) do
-		while name.name do
-			name = name.name
-		end
-		data.name = name
-		print("Converting data for " .. name)
-
-		-- Just rename counted
-		local counted = {
-			chats  = "chat",
-			deaths = "death",
-			joins  = "join",
-		}
-		for from, to in pairs(counted) do
-			data[to]   = data[from]
-			data[from] = nil
-		end
-
-		data.death = {
-			unknown = data.death,
-			__total = data.death,
-		}
-
-		-- Convert item db to new format
-		local counted_items = {
-			count = "dig",
-			place = "place",
-			craft = "craft",
-		}
-		for from, to in pairs(counted_items) do
-			local ret = {}
-
-			local count = 0
-			if data[from] then
-				for modname, items in pairs(data[from]) do
-					for itemname, value in pairs(items) do
-						itemname = modname .. ":" .. itemname
-						local key = minetest.registered_aliases[itemname] or itemname
-						ret[key] = value
-						count = count + value
-					end
-				end
-			end
-
-			ret.__total = count
-			data[from] = nil
-			data[to] = ret
-		end
-
-		awards.players[name] = data
-	end
-end
-
-function awards.load()
-	local old_save_path = minetest.get_worldpath().."/awards.txt"
-	local file = io.open(old_save_path, "r")
-	if file then
-		local table = minetest.deserialize(file:read("*all"))
-		if type(table) == "table" then
-			awards.players = table
-			convert_data()
-		else
-			awards.players = {}
-		end
-		file:close()
-		os.rename(old_save_path, minetest.get_worldpath().."/awards.bk.txt")
-		awards.save()
-	else
-		awards.players = minetest.parse_json(storage:get_string("player_data")) or {}
-	end
-end
-
-function awards.player(name)
-	assert(type(name) == "string")
-	local data = awards.players[name] or {}
-	awards.players[name] = data
-	data.name = data.name or name
-	data.unlocked = data.unlocked or {}
-	return data
-end
-
-function awards.player_or_nil(name)
-	return awards.players[name]
-end
-
-local default_def = {}
-
-function default_def:run_callbacks(player, data, table_func)
-	for i = 1, #self.on do
-		local res = nil
-		local entry = self.on[i]
-		if type(entry) == "function" then
-			res = entry(player, data)
-		elseif type(entry) == "table" and entry.award then
-			res = table_func(entry)
-		end
-
-		if res then
-			awards.unlock(player:get_player_name(), res)
-		end
-	end
-end
-
-function awards.register_trigger(tname, tdef)
-	assert(type(tdef) == "table",
-			"Passing a callback to register_trigger is not supported in 3.0")
-
-	tdef.name = tname
-	for key, value in pairs(default_def) do
-		tdef[key] = value
-	end
-
-	if tdef.type == "counted" then
-		local old_reg = tdef.on_register
-
-		function tdef:on_register(def)
-			local tmp = {
-				award  = def.name,
-				target = def.trigger.target,
-			}
-			tdef.register(tmp)
-
-			function def.getProgress(_, data)
-				local done = data[tname] or 0
-				return {
-					perc = done / tmp.target,
-					label = S(tdef.progress, done, tmp.target),
-				}
-			end
-
-			function def.getDefaultDescription(_)
-				local n = def.trigger.target
-				return NS(tdef.auto_description[1], tdef.auto_description[2], n, n)
-			end
-
-			if old_reg then
-				return old_reg(tdef, def)
-			end
-		end
-
-		function tdef.notify(player)
-			assert(player and player.is_player and player:is_player())
-			local name = player:get_player_name()
-			local data = awards.player(name)
-
-			-- Increment counter
-			local currentVal = (data[tname] or 0) + 1
-			data[tname] = currentVal
-
-			tdef:run_callbacks(player, data, function(entry)
-				if entry.target and entry.award and currentVal and
-						currentVal >= entry.target then
-					return entry.award
-				end
-			end)
-		end
-
-		awards["notify_" .. tname] = tdef.notify
-
-	elseif tdef.type == "counted_key" then
-		if tdef.key_is_item then
-			tdef.watched_groups = {}
-		end
-
-		-- On award register
-		local old_reg = tdef.on_register
-		function tdef:on_register(def)
-			-- Register trigger
-			local tmp = {
-				award  = def.name,
-				key    = tdef:get_key(def),
-				target = def.trigger.target,
-			}
-			tdef.register(tmp)
-
-			-- If group, add it to watch list
-			if tdef.key_is_item and tmp.key and tmp.key:sub(1, 6) == "group:" then
-				tdef.watched_groups[tmp.key:sub(7, #tmp.key)] = true
-			end
-
-			-- Called to get progress values and labels
-			function def.getProgress(_, data)
-				local done
-				data[tname] = data[tname] or {}
-				if tmp.key then
-					done = data[tname][tmp.key] or 0
-				else
-					done = data[tname].__total or 0
-				end
-				return {
-					perc = done / tmp.target,
-					label = S(tdef.progress, done, tmp.target),
-				}
-			end
-
-			-- Build description if none is specificed by the award
-			function def.getDefaultDescription(_)
-				local n = def.trigger.target
-				if tmp.key then
-					local nname = tmp.key
-					return NS(tdef.auto_description[1],
-							tdef.auto_description[2], n, n, nname)
-				else
-					return NS(tdef.auto_description_total[1],
-							tdef.auto_description_total[2], n, n)
-				end
-			end
-
-			-- Call on_register in trigger type definition
-			if old_reg then
-				return old_reg(tdef, def)
-			end
-		end
-
-		function tdef.notify(player, key, n)
-			n = n or 1
-
-			if tdef.key_is_item and key:sub(1, 6) ~= "group:" then
-				local itemdef = minetest.registered_items[key]
-				if itemdef then
-					for groupname, _ in pairs(itemdef.groups or {}) do
-						if tdef.watched_groups[groupname] then
-							tdef.notify(player, "group:" .. groupname, n)
-						end
-					end
-				end
-			end
-
-			assert(player and player.is_player and player:is_player() and key)
-			local name = player:get_player_name()
-			local data = awards.player(name)
-
-			-- Increment counter
-			data[tname] = data[tname] or {}
-			local currentVal = (data[tname][key] or 0) + n
-			data[tname][key] = currentVal
-			if key:sub(1, 6) ~= "group:" then
-				data[tname].__total = (data[tname].__total or 0) + n
-			end
-
-			tdef:run_callbacks(player, data, function(entry)
-				local current
-				if entry.key == key then
-					current = currentVal
-				elseif entry.key == nil then
-					current = data[tname].__total
-				else
-					return
-				end
-
-				if current >= entry.target then
-					return entry.award
-				end
-			end)
-		end
-
-		awards["notify_" .. tname] = tdef.notify
-
-	elseif tdef.type and tdef.type ~= "custom" then
-		error("Unrecognised trigger type " .. tdef.type)
-	end
-
-	awards.registered_triggers[tname] = tdef
-
-	tdef.on = {}
-	tdef.register = function(func)
-		table.insert(tdef.on, func)
-	end
-
-	-- Backwards compat
-	awards.on[tname] = tdef.on
-	awards['register_on_' .. tname] = tdef.register
-	return tdef
-end
-
-function awards.increment_item_counter(data, field, itemname, count)
-	itemname = minetest.registered_aliases[itemname] or itemname
-	data[field][itemname] = (data[field][itemname] or 0) + 1
-end
-
-function awards.get_item_count(data, field, itemname)
-	itemname = minetest.registered_aliases[itemname] or itemname
-	return data[field][itemname] or 0
-end
-
-function awards.get_total_keyed_count(data, field)
-	return data[field].__total or 0
-end
-
-function awards.register_on_unlock(func)
-	table.insert(awards.on_unlock, func)
-end
-
-function awards.register_award(name, def)
-	def.name = name
-
-	-- Add Triggers
-	if def.trigger and def.trigger.type then
-		local tdef = awards.registered_triggers[def.trigger.type]
-		assert(tdef, "Trigger not found: " .. def.trigger.type)
-		tdef:on_register(def)
-	end
-
-	function def:can_unlock(data)
-		if not self.requires then
-			return true
-		end
-
-		for i=1, #self.requires do
-			if not data.unlocked[self.requires[i]] then
-				return false
-			end
-		end
-		return true
-	end
-
-	-- Add Award
-	awards.registered_awards[name] = def
-
-	local tdef = awards.registered_awards[name]
-	if def.description == nil and tdef.getDefaultDescription then
-		def.description = tdef:getDefaultDescription()
-	end
-end
-
-function awards.enable(name)
-	local data = awards.player(name)
-	if data then
-		data.disabled = nil
-	end
-end
-
-function awards.disable(name)
-	local data = awards.player(name)
-	if data then
-		data.disabled = true
-	end
-end
-
-function awards.clear_player(name)
-	awards.players[name] = {}
-end
-
--- This function is called whenever a target condition is met.
--- It checks if a player already has that award, and if they do not,
--- it gives it to them
-----------------------------------------------
---awards.unlock(name, award)
--- name - the name of the player
--- award - the name of the award to give
-function awards.unlock(name, award)
-	-- Access Player Data
-	local data  = awards.player(name)
-	local awdef = awards.registered_awards[award]
-	assert(awdef, "Unable to unlock an award which doesn't exist!")
-
-	if data.disabled or
-			(data.unlocked[award] and data.unlocked[award] == award) then
-		return
-	end
-
-	if not awdef:can_unlock(data) then
-		minetest.log("warning", "can_unlock returned false in unlock of " ..
-				award .. " for " .. name)
-		return
-	end
-
-	-- Unlock Award
-	minetest.log("action", name.." has unlocked award "..name)
-	data.unlocked[award] = award
-	awards.save()
-
-	-- Give Prizes
-	if awdef and awdef.prizes then
-		for i = 1, #awdef.prizes do
-			local itemstack = ItemStack(awdef.prizes[i])
-			if not itemstack:is_empty() then
-				local receiverref = minetest.get_player_by_name(name)
-				if receiverref then
-					receiverref:get_inventory():add_item("main", itemstack)
-				end
-			end
-		end
-	end
-
-	-- Run callbacks
-	if awdef.on_unlock and awdef.on_unlock(name, awdef) then
-		return
-	end
-	for _, callback in pairs(awards.on_unlock) do
-		if callback(name, awdef) then
-			return
-		end
-	end
-
-	-- Get Notification Settings
-	local title = awdef.title or award
-	local desc = awdef.description or ""
-	local background = awdef.background or "awards_bg_default.png"
-	local icon = awdef.icon or "awards_unknown.png"
-	local sound = awdef.sound
-	if sound == nil then
-		-- Explicit check for nil because sound could be `false` to disable it
-		sound = {name="awards_got_generic", gain=0.25}
-	end
-
-	-- Do Notification
-	if sound then
-		-- Enforce sound delay to prevent sound spamming
-		local lastsound = data.lastsound
-		if lastsound == nil or os.difftime(os.time(), lastsound) >= 1 then
-			minetest.sound_play(sound, {to_player=name})
-			data.lastsound = os.time()
-		end
-	end
-
-	if awards.show_mode == "chat" then
-		local chat_announce
-		if awdef.secret then
-			chat_announce = S("Secret Award Unlocked: %s")
-		else
-			chat_announce = S("Award Unlocked: %s")
-		end
-		-- use the chat console to send it
-		minetest.chat_send_player(name, string.format(chat_announce, title))
-		if desc~="" then
-			minetest.chat_send_player(name, desc)
-		end
-	else
-		local player = minetest.get_player_by_name(name)
-		local one = player:hud_add({
-			hud_elem_type = "image",
-			name = "award_bg",
-			scale = {x = 2, y = 1},
-			text = background,
-			position = {x = 0.5, y = 0.05},
-			offset = {x = 0, y = 138},
-			alignment = {x = 0, y = -1}
-		})
-		local hud_announce
-		if awdef.secret then
-			hud_announce = S("Secret Award Unlocked!")
-		else
-			hud_announce = S("Award Unlocked!")
-		end
-		local two = player:hud_add({
-			hud_elem_type = "text",
-			name = "award_au",
-			number = 0xFFFFFF,
-			scale = {x = 100, y = 20},
-			text = hud_announce,
-			position = {x = 0.5, y = 0.05},
-			offset = {x = 0, y = 45},
-			alignment = {x = 0, y = -1}
-		})
-		local three = player:hud_add({
-			hud_elem_type = "text",
-			name = "award_title",
-			number = 0xFFFFFF,
-			scale = {x = 100, y = 20},
-			text = title,
-			position = {x = 0.5, y = 0.05},
-			offset = {x = 0, y = 100},
-			alignment = {x = 0, y = -1}
-		})
-		local four = player:hud_add({
-			hud_elem_type = "image",
-			name = "award_icon",
-			scale = {x = 4, y = 4},
-			text = icon,
-			position = {x = 0.5, y = 0.05},
-			offset = {x = -200.5, y = 126},
-			alignment = {x = 0, y = -1}
-		})
-		minetest.after(4, function()
-			local player2 = minetest.get_player_by_name(name)
-			if player2 then
-				player2:hud_remove(one)
-				player2:hud_remove(two)
-				player2:hud_remove(three)
-				player2:hud_remove(four)
-			end
-		end)
-	end
-end
-
-minetest.register_on_player_receive_fields(function(player, formname, fields)
-	if formname ~= "awards:awards" then
-		return false
-	end
-	if fields.quit then
-		return true
-	end
-	local name = player:get_player_name()
-	if fields.awards then
-		local event = minetest.explode_textlist_event(fields.awards)
-		if event.type == "CHG" then
-			awards.show_to(name, name, event.index, false)
-		end
-	end
-
-	return true
-end)
-
-awards.load()
-
-minetest.register_on_shutdown(function()
-	awards.save()
-end)
diff --git a/init.lua b/init.lua
index 0e96327..98f054a 100644
--- a/init.lua
+++ b/init.lua
@@ -7,14 +7,20 @@ awards = {
 }
 
 -- Internationalization support.
-awards.gettext, awards.ngettext = dofile(minetest.get_modpath("awards").."/intllib.lua")
+awards.gettext, awards.ngettext = dofile(minetest.get_modpath("awards").."/src/intllib.lua")
 
 -- Load files
-dofile(minetest.get_modpath("awards").."/api.lua")
-dofile(minetest.get_modpath("awards").."/chat_commands.lua")
-dofile(minetest.get_modpath("awards").."/gui.lua")
-dofile(minetest.get_modpath("awards").."/triggers.lua")
-dofile(minetest.get_modpath("awards").."/awards.lua")
+dofile(minetest.get_modpath("awards").."/src/data.lua")
+dofile(minetest.get_modpath("awards").."/src/api_awards.lua")
+dofile(minetest.get_modpath("awards").."/src/api_triggers.lua")
+dofile(minetest.get_modpath("awards").."/src/chat_commands.lua")
+dofile(minetest.get_modpath("awards").."/src/gui.lua")
+dofile(minetest.get_modpath("awards").."/src/triggers.lua")
+dofile(minetest.get_modpath("awards").."/src/awards.lua")
+
+awards.load()
+minetest.register_on_shutdown(awards.save)
+
 
 -- Backwards compatibility
 awards.give_achievement     = awards.unlock
diff --git a/src/api_awards.lua b/src/api_awards.lua
new file mode 100644
index 0000000..4147d43
--- /dev/null
+++ b/src/api_awards.lua
@@ -0,0 +1,179 @@
+-- Copyright (c) 2013-18 rubenwardy. MIT.
+
+local S = awards.gettext
+
+function awards.register_award(name, def)
+	def.name = name
+
+	-- Add Triggers
+	if def.trigger and def.trigger.type then
+		local tdef = awards.registered_triggers[def.trigger.type]
+		assert(tdef, "Trigger not found: " .. def.trigger.type)
+		tdef:on_register(def)
+	end
+
+	function def:can_unlock(data)
+		if not self.requires then
+			return true
+		end
+
+		for i=1, #self.requires do
+			if not data.unlocked[self.requires[i]] then
+				return false
+			end
+		end
+		return true
+	end
+
+	-- Add Award
+	awards.registered_awards[name] = def
+
+	local tdef = awards.registered_awards[name]
+	if def.description == nil and tdef.getDefaultDescription then
+		def.description = tdef:getDefaultDescription()
+	end
+end
+
+
+-- This function is called whenever a target condition is met.
+-- It checks if a player already has that award, and if they do not,
+-- it gives it to them
+----------------------------------------------
+--awards.unlock(name, award)
+-- name - the name of the player
+-- award - the name of the award to give
+function awards.unlock(name, award)
+	-- Access Player Data
+	local data  = awards.player(name)
+	local awdef = awards.registered_awards[award]
+	assert(awdef, "Unable to unlock an award which doesn't exist!")
+
+	if data.disabled or
+			(data.unlocked[award] and data.unlocked[award] == award) then
+		return
+	end
+
+	if not awdef:can_unlock(data) then
+		minetest.log("warning", "can_unlock returned false in unlock of " ..
+				award .. " for " .. name)
+		return
+	end
+
+	-- Unlock Award
+	minetest.log("action", name.." has unlocked award "..name)
+	data.unlocked[award] = award
+	awards.save()
+
+	-- Give Prizes
+	if awdef and awdef.prizes then
+		for i = 1, #awdef.prizes do
+			local itemstack = ItemStack(awdef.prizes[i])
+			if not itemstack:is_empty() then
+				local receiverref = minetest.get_player_by_name(name)
+				if receiverref then
+					receiverref:get_inventory():add_item("main", itemstack)
+				end
+			end
+		end
+	end
+
+	-- Run callbacks
+	if awdef.on_unlock and awdef.on_unlock(name, awdef) then
+		return
+	end
+	for _, callback in pairs(awards.on_unlock) do
+		if callback(name, awdef) then
+			return
+		end
+	end
+
+	-- Get Notification Settings
+	local title = awdef.title or award
+	local desc = awdef.description or ""
+	local background = awdef.background or "awards_bg_default.png"
+	local icon = awdef.icon or "awards_unknown.png"
+	local sound = awdef.sound
+	if sound == nil then
+		-- Explicit check for nil because sound could be `false` to disable it
+		sound = {name="awards_got_generic", gain=0.25}
+	end
+
+	-- Do Notification
+	if sound then
+		-- Enforce sound delay to prevent sound spamming
+		local lastsound = data.lastsound
+		if lastsound == nil or os.difftime(os.time(), lastsound) >= 1 then
+			minetest.sound_play(sound, {to_player=name})
+			data.lastsound = os.time()
+		end
+	end
+
+	if awards.show_mode == "chat" then
+		local chat_announce
+		if awdef.secret then
+			chat_announce = S("Secret Award Unlocked: %s")
+		else
+			chat_announce = S("Award Unlocked: %s")
+		end
+		-- use the chat console to send it
+		minetest.chat_send_player(name, string.format(chat_announce, title))
+		if desc~="" then
+			minetest.chat_send_player(name, desc)
+		end
+	else
+		local player = minetest.get_player_by_name(name)
+		local one = player:hud_add({
+			hud_elem_type = "image",
+			name = "award_bg",
+			scale = {x = 2, y = 1},
+			text = background,
+			position = {x = 0.5, y = 0.05},
+			offset = {x = 0, y = 138},
+			alignment = {x = 0, y = -1}
+		})
+		local hud_announce
+		if awdef.secret then
+			hud_announce = S("Secret Award Unlocked!")
+		else
+			hud_announce = S("Award Unlocked!")
+		end
+		local two = player:hud_add({
+			hud_elem_type = "text",
+			name = "award_au",
+			number = 0xFFFFFF,
+			scale = {x = 100, y = 20},
+			text = hud_announce,
+			position = {x = 0.5, y = 0.05},
+			offset = {x = 0, y = 45},
+			alignment = {x = 0, y = -1}
+		})
+		local three = player:hud_add({
+			hud_elem_type = "text",
+			name = "award_title",
+			number = 0xFFFFFF,
+			scale = {x = 100, y = 20},
+			text = title,
+			position = {x = 0.5, y = 0.05},
+			offset = {x = 0, y = 100},
+			alignment = {x = 0, y = -1}
+		})
+		local four = player:hud_add({
+			hud_elem_type = "image",
+			name = "award_icon",
+			scale = {x = 4, y = 4},
+			text = icon,
+			position = {x = 0.5, y = 0.05},
+			offset = {x = -200.5, y = 126},
+			alignment = {x = 0, y = -1}
+		})
+		minetest.after(4, function()
+			local player2 = minetest.get_player_by_name(name)
+			if player2 then
+				player2:hud_remove(one)
+				player2:hud_remove(two)
+				player2:hud_remove(three)
+				player2:hud_remove(four)
+			end
+		end)
+	end
+end
diff --git a/src/api_triggers.lua b/src/api_triggers.lua
new file mode 100644
index 0000000..9311bcf
--- /dev/null
+++ b/src/api_triggers.lua
@@ -0,0 +1,215 @@
+-- Copyright (c) 2013-18 rubenwardy. MIT.
+
+local S, NS = awards.gettext, awards.ngettext
+
+awards.registered_awards = {}
+awards.on = {}
+awards.on_unlock = {}
+
+local default_def = {}
+
+function default_def:run_callbacks(player, data, table_func)
+	for i = 1, #self.on do
+		local res = nil
+		local entry = self.on[i]
+		if type(entry) == "function" then
+			res = entry(player, data)
+		elseif type(entry) == "table" and entry.award then
+			res = table_func(entry)
+		end
+
+		if res then
+			awards.unlock(player:get_player_name(), res)
+		end
+	end
+end
+
+function awards.register_trigger(tname, tdef)
+	assert(type(tdef) == "table",
+			"Passing a callback to register_trigger is not supported in 3.0")
+
+	tdef.name = tname
+	for key, value in pairs(default_def) do
+		tdef[key] = value
+	end
+
+	if tdef.type == "counted" then
+		local old_reg = tdef.on_register
+
+		function tdef:on_register(def)
+			local tmp = {
+				award  = def.name,
+				target = def.trigger.target,
+			}
+			tdef.register(tmp)
+
+			function def.getProgress(_, data)
+				local done = data[tname] or 0
+				return {
+					perc = done / tmp.target,
+					label = S(tdef.progress, done, tmp.target),
+				}
+			end
+
+			function def.getDefaultDescription(_)
+				local n = def.trigger.target
+				return NS(tdef.auto_description[1], tdef.auto_description[2], n, n)
+			end
+
+			if old_reg then
+				return old_reg(tdef, def)
+			end
+		end
+
+		function tdef.notify(player)
+			assert(player and player.is_player and player:is_player())
+			local name = player:get_player_name()
+			local data = awards.player(name)
+
+			-- Increment counter
+			local currentVal = (data[tname] or 0) + 1
+			data[tname] = currentVal
+
+			tdef:run_callbacks(player, data, function(entry)
+				if entry.target and entry.award and currentVal and
+						currentVal >= entry.target then
+					return entry.award
+				end
+			end)
+		end
+
+		awards["notify_" .. tname] = tdef.notify
+
+	elseif tdef.type == "counted_key" then
+		if tdef.key_is_item then
+			tdef.watched_groups = {}
+		end
+
+		-- On award register
+		local old_reg = tdef.on_register
+		function tdef:on_register(def)
+			-- Register trigger
+			local tmp = {
+				award  = def.name,
+				key    = tdef:get_key(def),
+				target = def.trigger.target,
+			}
+			tdef.register(tmp)
+
+			-- If group, add it to watch list
+			if tdef.key_is_item and tmp.key and tmp.key:sub(1, 6) == "group:" then
+				tdef.watched_groups[tmp.key:sub(7, #tmp.key)] = true
+			end
+
+			-- Called to get progress values and labels
+			function def.getProgress(_, data)
+				local done
+				data[tname] = data[tname] or {}
+				if tmp.key then
+					done = data[tname][tmp.key] or 0
+				else
+					done = data[tname].__total or 0
+				end
+				return {
+					perc = done / tmp.target,
+					label = S(tdef.progress, done, tmp.target),
+				}
+			end
+
+			-- Build description if none is specificed by the award
+			function def.getDefaultDescription(_)
+				local n = def.trigger.target
+				if tmp.key then
+					local nname = tmp.key
+					return NS(tdef.auto_description[1],
+							tdef.auto_description[2], n, n, nname)
+				else
+					return NS(tdef.auto_description_total[1],
+							tdef.auto_description_total[2], n, n)
+				end
+			end
+
+			-- Call on_register in trigger type definition
+			if old_reg then
+				return old_reg(tdef, def)
+			end
+		end
+
+		function tdef.notify(player, key, n)
+			n = n or 1
+
+			if tdef.key_is_item and key:sub(1, 6) ~= "group:" then
+				local itemdef = minetest.registered_items[key]
+				if itemdef then
+					for groupname, _ in pairs(itemdef.groups or {}) do
+						if tdef.watched_groups[groupname] then
+							tdef.notify(player, "group:" .. groupname, n)
+						end
+					end
+				end
+			end
+
+			assert(player and player.is_player and player:is_player() and key)
+			local name = player:get_player_name()
+			local data = awards.player(name)
+
+			-- Increment counter
+			data[tname] = data[tname] or {}
+			local currentVal = (data[tname][key] or 0) + n
+			data[tname][key] = currentVal
+			if key:sub(1, 6) ~= "group:" then
+				data[tname].__total = (data[tname].__total or 0) + n
+			end
+
+			tdef:run_callbacks(player, data, function(entry)
+				local current
+				if entry.key == key then
+					current = currentVal
+				elseif entry.key == nil then
+					current = data[tname].__total
+				else
+					return
+				end
+
+				if current >= entry.target then
+					return entry.award
+				end
+			end)
+		end
+
+		awards["notify_" .. tname] = tdef.notify
+
+	elseif tdef.type and tdef.type ~= "custom" then
+		error("Unrecognised trigger type " .. tdef.type)
+	end
+
+	awards.registered_triggers[tname] = tdef
+
+	tdef.on = {}
+	tdef.register = function(func)
+		table.insert(tdef.on, func)
+	end
+
+	-- Backwards compat
+	awards.on[tname] = tdef.on
+	awards['register_on_' .. tname] = tdef.register
+	return tdef
+end
+
+function awards.increment_item_counter(data, field, itemname, count)
+	itemname = minetest.registered_aliases[itemname] or itemname
+	data[field][itemname] = (data[field][itemname] or 0) + 1
+end
+
+function awards.get_item_count(data, field, itemname)
+	itemname = minetest.registered_aliases[itemname] or itemname
+	return data[field][itemname] or 0
+end
+
+function awards.get_total_keyed_count(data, field)
+	return data[field].__total or 0
+end
+
+function awards.register_on_unlock(func)
+	table.insert(awards.on_unlock, func)
+end
diff --git a/awards.lua b/src/awards.lua
similarity index 100%
rename from awards.lua
rename to src/awards.lua
diff --git a/chat_commands.lua b/src/chat_commands.lua
similarity index 100%
rename from chat_commands.lua
rename to src/chat_commands.lua
diff --git a/src/data.lua b/src/data.lua
new file mode 100644
index 0000000..27ae5e3
--- /dev/null
+++ b/src/data.lua
@@ -0,0 +1,111 @@
+
+local storage = minetest.get_mod_storage()
+local __player_data
+
+-- Table Save Load Functions
+function awards.save()
+	storage:set_string("player_data", minetest.write_json(__player_data))
+end
+
+local function convert_data()
+	minetest.log("warning", "Importing awards data from previous version")
+
+	local old_players = __player_data
+	__player_data = {}
+	for name, data in pairs(old_players) do
+		while name.name do
+			name = name.name
+		end
+		data.name = name
+		print("Converting data for " .. name)
+
+		-- Just rename counted
+		local counted = {
+			chats  = "chat",
+			deaths = "death",
+			joins  = "join",
+		}
+		for from, to in pairs(counted) do
+			data[to]   = data[from]
+			data[from] = nil
+		end
+
+		data.death = {
+			unknown = data.death,
+			__total = data.death,
+		}
+
+		-- Convert item db to new format
+		local counted_items = {
+			count = "dig",
+			place = "place",
+			craft = "craft",
+		}
+		for from, to in pairs(counted_items) do
+			local ret = {}
+
+			local count = 0
+			if data[from] then
+				for modname, items in pairs(data[from]) do
+					for itemname, value in pairs(items) do
+						itemname = modname .. ":" .. itemname
+						local key = minetest.registered_aliases[itemname] or itemname
+						ret[key] = value
+						count = count + value
+					end
+				end
+			end
+
+			ret.__total = count
+			data[from] = nil
+			data[to] = ret
+		end
+
+		__player_data[name] = data
+	end
+end
+
+function awards.load()
+	local old_save_path = minetest.get_worldpath().."/awards.txt"
+	local file = io.open(old_save_path, "r")
+	if file then
+		local table = minetest.deserialize(file:read("*all"))
+		if type(table) == "table" then
+			__player_data = table
+			convert_data()
+		else
+			__player_data = {}
+		end
+		file:close()
+		os.rename(old_save_path, minetest.get_worldpath().."/awards.bk.txt")
+		awards.save()
+	else
+		__player_data = minetest.parse_json(storage:get_string("player_data")) or {}
+	end
+end
+
+function awards.player(name)
+	assert(type(name) == "string")
+	local data = __player_data[name] or {}
+	__player_data[name] = data
+
+	data.name     = data.name or name
+	data.unlocked = data.unlocked or {}
+	return data
+end
+
+function awards.player_or_nil(name)
+	return __player_data[name]
+end
+
+function awards.enable(name)
+	awards.player(name).disabled = nil
+end
+
+function awards.disable(name)
+	awards.player(name).disabled = true
+end
+
+function awards.clear_player(name)
+	__player_data[name] = {}
+end
diff --git a/gui.lua b/src/gui.lua
similarity index 84%
rename from gui.lua
rename to src/gui.lua
index 19da07d..cfc82af 100644
--- a/gui.lua
+++ b/src/gui.lua
@@ -99,14 +99,14 @@ function awards.get_formspec(name, to, sid)
 			status = S("%s (unlocked)")
 		end
 
-		formspec = formspec .. "textarea[0.5,2.7;4.8,1.45;;" ..
+		formspec = formspec .. "textarea[0.5,3.1;4.8,1.45;;" ..
 			string.format(status, minetest.formspec_escape(title)) ..
 			";]"
 
 		if sdef and sdef.icon then
-			formspec = formspec .. "image[1,0;3,3;" .. sdef.icon .. "]"
+			formspec = formspec .. "image[0.6,0;3,3;" .. sdef.icon .. "]"
 		end
-		local barwidth = 4.6
+		local barwidth = 3.95
 		local perc = nil
 		local label = nil
 		if sdef.getProgress and data then
@@ -118,19 +118,21 @@ function awards.get_formspec(name, to, sid)
 			if perc > 1 then
 				perc = 1
 			end
-			formspec = formspec .. "background[0,4.80;" .. barwidth ..",0.25;awards_progress_gray.png;false]"
-			formspec = formspec .. "background[0,4.80;" .. (barwidth * perc) ..",0.25;awards_progress_green.png;false]"
+			formspec = formspec .. "background[0,8.24;" .. barwidth ..",0.4;awards_progress_gray.png;false]"
+			formspec = formspec .. "background[0,8.24;" .. (barwidth * perc) ..",0.4;awards_progress_green.png;false]"
 			if label then
-				formspec = formspec .. "label[1.75,4.63;" .. minetest.formspec_escape(label) .. "]"
+				formspec = formspec .. "label[1.6,8.15;" .. minetest.formspec_escape(label) .. "]"
 			end
 		end
 		if sdef and sdef.description then
-			formspec = formspec	.. "textarea[0.25,3.75;4.8,1.7;;"..minetest.formspec_escape(sdef.description)..";]"
+			formspec = formspec .. "box[-0.05,3.75;3.9,4.2;#000]"
+			formspec = formspec	.. "textarea[0.25,3.75;3.9,4.2;;" ..
+					minetest.formspec_escape(sdef.description) .. ";]"
 		end
 	end
 
 	-- Create list box
-	formspec = formspec .. "textlist[4.75,0;6,5;awards;"
+	formspec = formspec .. "textlist[4,0;3.8,8.6;awards;"
 	local first = true
 	for _, award in pairs(awards_list) do
 		local def = award.def
@@ -203,11 +205,29 @@ function awards.show_to(name, to, sid, text)
 		end
 		-- Show formspec to user
 		minetest.show_formspec(to,"awards:awards",
-			"size[11,5]" .. deco ..
+			"size[8,8.6]" .. deco ..
 			awards.get_formspec(name, to, sid))
 	end
 end
 
+minetest.register_on_player_receive_fields(function(player, formname, fields)
+	if formname ~= "awards:awards" then
+		return false
+	end
+	if fields.quit then
+		return true
+	end
+	local name = player:get_player_name()
+	if fields.awards then
+		local event = minetest.explode_textlist_event(fields.awards)
+		if event.type == "CHG" then
+			awards.show_to(name, name, event.index, false)
+		end
+	end
+
+	return true
+end)
+
 if minetest.get_modpath("sfinv") then
 	sfinv.register_page("awards:awards", {
 		title = S("Awards"),
@@ -222,7 +242,7 @@ if minetest.get_modpath("sfinv") then
 			local name = player:get_player_name()
 			return sfinv.make_formspec(player, context,
 				awards.get_formspec(name, name, context.awards_idx),
-				false, "size[11,5]")
+				false)
 		end,
 		on_player_receive_fields = function(self, player, context, fields)
 			if fields.awards then
diff --git a/intllib.lua b/src/intllib.lua
similarity index 100%
rename from intllib.lua
rename to src/intllib.lua
diff --git a/triggers.lua b/src/triggers.lua
similarity index 100%
rename from triggers.lua
rename to src/triggers.lua