Creating New Scripts Dynamically in Lua
- by bazola
Right now this is just a crazy idea that I had, but I was able to implement the code and get it working properly. I am not entirely sure of what the use cases would be just yet.
What this code does is create a new Lua script file in the project directory. The ScriptWriter takes as arguments the file name, a table containing any arguments that the script should take when created, and a table containing any instance variables to create by default. My plan is to extend this code to create new functions based on inputs sent in during its creation as well.
What makes this cool is that the new file is both generated and loaded dynamically on the fly. Theoretically you could get this code to generate and load any script imaginable. One use case I can think of is an AI that creates scripts to map out it's functions, and creates new scripts for new situations or environments. At this point, this is all theoretical, though.
Here is the test code that is creating the new script and then immediately loading it and calling functions from it:
function Card:doScriptWriterThing()
local scriptName = "ScriptIAmMaking"
local scripter = scriptWriter:new(scriptName, {"argumentName"}, {name = "'test'", one = 1})
scripter:makeFileForLoadedSettings()
local loadedScript = require (scriptName)
local scriptInstance = loadedScript:new("sayThis")
print(scriptInstance:get_name()) --will print test
print(scriptInstance:get_one()) -- will print 1
scriptInstance:set_one(10000)
print(scriptInstance:get_one()) -- will print 10000
print(scriptInstance:get_argumentName()) -- will print sayThis
scriptInstance:set_argumentName("saySomethingElse")
print(scriptInstance:get_argumentName()) --will print saySomethingElse
end
Here is ScriptWriter.lua
local ScriptWriter = {}
local twoSpaceIndent = " "
local equalsWithSpaces = " = "
local newLine = "\n"
--scriptNameToCreate must be a string
--argumentsForNew and instanceVariablesToCreate must be tables and not nil
function ScriptWriter:new(scriptNameToCreate, argumentsForNew, instanceVariablesToCreate)
local instance = setmetatable({}, { __index = self })
instance.name = scriptNameToCreate
instance.newArguments = argumentsForNew
instance.instanceVariables = instanceVariablesToCreate
instance.stringList = {}
return instance
end
function ScriptWriter:makeFileForLoadedSettings()
self:buildInstanceMetatable()
self:buildInstanceCreationMethod()
self:buildSettersAndGetters()
self:buildReturn()
self:writeStringsToFile()
end
--very first line of any script that will have instances
function ScriptWriter:buildInstanceMetatable()
table.insert(self.stringList, "local " .. self.name .. " = {}" .. newLine)
table.insert(self.stringList, newLine)
end
--every script made this way needs a new method to create its instances
function ScriptWriter:buildInstanceCreationMethod()
--new() function declaration
table.insert(self.stringList, ("function " .. self.name .. ":new("))
self:buildNewArguments()
table.insert(self.stringList, ")" .. newLine)
--first line inside :new() function
table.insert(self.stringList, twoSpaceIndent .. "local instance = setmetatable({}, { __index = self })" .. newLine)
--add designated arguments inside :new()
self:buildNewArgumentVariables()
--create the instance variables with the loaded values
for key,value in pairs(self.instanceVariables) do
table.insert(self.stringList, twoSpaceIndent .. "instance." .. key .. equalsWithSpaces .. value .. newLine)
end
--close the :new() function
table.insert(self.stringList, twoSpaceIndent .. "return instance" .. newLine)
table.insert(self.stringList, "end" .. newLine)
table.insert(self.stringList, newLine)
end
function ScriptWriter:buildNewArguments()
--if there are arguments for :new(), add them
for key,value in ipairs(self.newArguments) do
table.insert(self.stringList, value)
table.insert(self.stringList, ", ")
end
if next(self.newArguments) ~= nil then --makes sure the table is not empty first
table.remove(self.stringList) --remove the very last element, which will be the extra ", "
end
end
function ScriptWriter:buildNewArgumentVariables()
--add the designated arguments to :new()
for key, value in ipairs(self.newArguments) do
table.insert(self.stringList, twoSpaceIndent .. "instance." .. value .. equalsWithSpaces .. value .. newLine)
end
end
--the instance variables need separate code because their names have to be the key and not the argument name
function ScriptWriter:buildSettersAndGetters()
for key,value in ipairs(self.newArguments) do
self:buildArgumentSetter(value)
self:buildArgumentGetter(value)
table.insert(self.stringList, newLine)
end
for key,value in pairs(self.instanceVariables) do
self:buildInstanceVariableSetter(key, value)
self:buildInstanceVariableGetter(key, value)
table.insert(self.stringList, newLine)
end
end
--code for arguments passed in
function ScriptWriter:buildArgumentSetter(variable)
table.insert(self.stringList, "function " .. self.name .. ":set_" .. variable .. "(newValue)" .. newLine)
table.insert(self.stringList, twoSpaceIndent .. "self." .. variable .. equalsWithSpaces .. "newValue" .. newLine)
table.insert(self.stringList, "end" .. newLine)
end
function ScriptWriter:buildArgumentGetter(variable)
table.insert(self.stringList, "function " .. self.name .. ":get_" .. variable .. "()" .. newLine)
table.insert(self.stringList, twoSpaceIndent .. "return " .. "self." .. variable .. newLine)
table.insert(self.stringList, "end" .. newLine)
end
--code for instance variable values passed in
function ScriptWriter:buildInstanceVariableSetter(key, variable)
table.insert(self.stringList, "function " .. self.name .. ":set_" .. key .. "(newValue)" .. newLine)
table.insert(self.stringList, twoSpaceIndent .. "self." .. key .. equalsWithSpaces .. "newValue" .. newLine)
table.insert(self.stringList, "end" .. newLine)
end
function ScriptWriter:buildInstanceVariableGetter(key, variable)
table.insert(self.stringList, "function " .. self.name .. ":get_" .. key .. "()" .. newLine)
table.insert(self.stringList, twoSpaceIndent .. "return " .. "self." .. key .. newLine)
table.insert(self.stringList, "end" .. newLine)
end
--last line of any script that will have instances
function ScriptWriter:buildReturn()
table.insert(self.stringList, "return " .. self.name)
end
function ScriptWriter:writeStringsToFile()
local fileName = (self.name .. ".lua")
file = io.open(fileName, 'w')
for key,value in ipairs(self.stringList) do
file:write(value)
end
file:close()
end
return ScriptWriter
And here is what the code provided will generate:
local ScriptIAmMaking = {}
function ScriptIAmMaking:new(argumentName)
local instance = setmetatable({}, { __index = self })
instance.argumentName = argumentName
instance.name = 'test'
instance.one = 1
return instance
end
function ScriptIAmMaking:set_argumentName(newValue)
self.argumentName = newValue
end
function ScriptIAmMaking:get_argumentName()
return self.argumentName
end
function ScriptIAmMaking:set_name(newValue)
self.name = newValue
end
function ScriptIAmMaking:get_name()
return self.name
end
function ScriptIAmMaking:set_one(newValue)
self.one = newValue
end
function ScriptIAmMaking:get_one()
return self.one
end
return ScriptIAmMaking
All of this is generated with these calls:
local scripter = scriptWriter:new(scriptName, {"argumentName"}, {name = "'test'", one = 1})
scripter:makeFileForLoadedSettings()
I am not sure if I am correct that this could be useful in certain situations. What I am looking for is feedback on the readability of the code, and following Lua best practices. I would also love to hear whether this approach is a valid one, and whether the way that I have done things will be extensible.