Strings "true" and "false" converted to booleans in plugin settings

Run this code with the command bar:

local value = "true"
print(type(value))
--> string

local plugin = PluginManager():CreatePlugin()
plugin:SetSetting("key", value)
value = plugin:GetSetting("key")
print(type(value))
--> boolean
2 Likes

Isn’t that behavior due to using strings for all values?

Here are some other behaviors that would be interesting to note:

if saving “1” returns a number
if saving “” returns nil

EDIT: those were questions

EDIT 2: and yet my presumptions were correct

Things I am never using again:
[âś“] Plugin Settings

2 Likes

[quote] Things I am never using again:
[âś“] Plugin Settings [/quote]

I decided to make a module to help this.

Basically, it wraps the Plugin object so that GetSetting and SetSetting will use the HTTP JSON encoder, which is a lot more stable. No other changes should be required. In case they someday fix the plugin settings encoder, there’s also an option to enable “transition mode”, which will convert data encoded with BetterSettings to normal plugin settings.

--[[ BetterSettings

BetterSettings is a module that lets your plugin save settings properly.

This module returns a function that wraps around a Plugin object, replacing
the GetSetting and SetSetting functions with equivalents that use Roblox's
HTTP JSON encoder instead of the plugin settings JSON encoder, which does not
work properly.

This function receives three arguments:
	- plugin: A Plugin object (e.g. the `plugin` variable).
	- encodeKey: A string indicating the key where all settings will be stored.
	- transitionMode: A bool indicating whether to transition from old settings to new.

This function returns an object that wraps the Plugin object. You should be
able to access all members normally.

To start using this module, the following line should be sufficient:

	plugin = require(script.BetterSettings)(plugin, 'better_settings_key')

If Roblox someday fixes their JSON encoder, and you want to start using it,
then all you have to do is set transitionMode to `true`. This will change the
behavior of GetSetting and SetSetting: GetSetting will decode encodeKey if it
is present in the settings, and decode normally otherwise. Likewise,
SetSetting will encode data normally, and delete the old data in encodeKey if
it is present. For this reason, you should choose an encodeKey that you will
never use in your actual settings, so that there are no conflicts.

	plugin = require(script.BetterSettings)(plugin, 'better_settings_key', true)

Once you have given enough time for the settings of the users of your plugin
to transition, then you should be able to stop using BetterSettings altogether
without any problems.

]]

-- Base64
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

function base64Encode(data)
    return ((data:gsub('.', function(x)
        local r,b='',x:byte()
        for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
        return r;
    end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if (#x < 6) then return '' end
        local c=0
        for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
        return b:sub(c+1,c+1)
    end)..({ '', '==', '=' })[#data%3+1])
end

function base64Decode(data)
    data = string.gsub(data, '[^'..b..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(b:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return string.char(c)
    end))
end

-- JSON
function jsonEncode(...)
	local http = game:GetService('HttpService')
	return pcall(http.JSONEncode, http, ...)
end

function jsonDecode(...)
	local http = game:GetService('HttpService')
	return pcall(http.JSONDecode, http, ...)
end

-- Wrapper
function instanceWrapper(instance, wrapper)
	local proxy = newproxy(true)
	local mt = getmetatable(proxy)

	function mt.__index(t, k)
		if wrapper[k] ~= nil then return wrapper[k] end
		return instance[k]
	end

	function mt.__newindex(t, k, v)
		instance[k] = v
	end

	function mt.__tostring(t)
		return tostring(instance)
	end

	mt.__metatable = "The metatable is locked"

	return proxy
end

return function(plugin, encodeKey, transitionMode)
	if type(encodeKey) ~= 'string' then
		error('encodeKey must be a string',2)
	end

	local wrapper = {}

	function wrapper:GetSetting(key)
		if key == nil then
			error('Argument 1 missing or nil')
		end
		if type(key) ~= 'string' then
			error(string.format('bad argument #1 to GetSetting (string expected, got %s)',type(key)),2)
		end

		local data = plugin:GetSetting(encodeKey)
		if type(data) == 'string' then
			data = base64Decode(data)

			local success, settings = jsonDecode(data)
			if success then
				return settings[key]
			end

		elseif not transitionMode then
			return plugin:GetSetting(key)
		end

		return nil
	end

	function wrapper:SetSetting(key, value)
		if key == nil then
			error('Argument 1 missing or nil')
		end
		if type(key) ~= 'string' then
			error(string.format('bad argument #1 to GetSetting (string expected, got %s)',type(key)),2)
		end

		if transitionMode then
			local settings
			local data = plugin:GetSetting(encodeKey)
			if type(data) == 'string' then
				data = base64Decode(data)

				local success, data = jsonDecode(data)
				if success then
					settings = data
				else
					settings = {}
				end
			else
				settings = {}
			end

			settings[key] = value
			local success, data = jsonEncode(settings)
			if success then
				data = base64Encode(data)
				plugin:SetSetting(encodeKey, data)
			end
		else
			-- Convert old settings to new
			local data = plugin:GetSetting(encodeKey)
			if type(data) == 'string' then
				data = base64Decode(data)

				local success, settings = jsonDecode(data)
				if success then
					for k,v in pairs(settings) do
						plugin:SetSetting(k, v)
					end

					-- API requires a non-nil value, but that might change, so
					-- try setting it to nil first.
					if not pcall(plugin.SetSetting, plugin, encodeKey, nil) then
						plugin:SetSetting(encodeKey, false)
					end
				end
			end

			plugin:SetSetting(key, value)
		end

		local settings = plugin:GetSetting(encodeKey)
		if type(settings) == 'string' then
			settings = base64Decode(settings)

			local success, settings = jsonDecode(settings)
			if success then
				return settings[key]
			end

		elseif not transitionMode then
			return plugin:GetSetting(key)
		end

		return nil
	end

	return instanceWrapper(plugin, wrapper)
end
2 Likes