Wait, I don't understand how "require" works

Started by Max, October 04, 2018, 04:10:21 AM

Previous topic - Next topic
So, I've got a few towns in my games and I hate copying/pasting the blacksmiths' and shopkeepers' code from one place to another, because they all function the same. I think this is a situation I can make a script which all will use, but I'm not sure how the syntax for this works. Does the "require" syntax (is this an operator?) return... anything? I'm not quite sure how to use require properly.

But anyway, here's my (third) attempt:

In a town's map:
Code (lua) Select

function blacksmith:on_interaction()
  local shop = require("scripts/shops/blacksmith")
  shop:blacksmith()
end


The blacksmith() function in the script is pretty simple and tested, just dialogues and choices which will take money and increase attack power, basically, but here is it anyway. I don't think this is where the problem is.
Code (lua) Select

function blacksmith()

game:start_dialog("_yarrowmouth.npcs.blacksmith", function(answer)
    --sword
    if answer == 2 then
      --have required items
      if game:has_item("sword") == true and game:get_item("coral_ore"):get_amount() >= 1 and game:get_money() >= 50 then
        game:set_value("sword_damage", game:get_value("sword_damage") + 1)
        game:remove_money(50)
        game:get_item("coral_ore"):remove_amount(1)
        game:start_dialog("_goatshead.npcs.palladio.sword_improved")
      else --don't have required items
        game:start_dialog("_game.insufficient_items")
      end

    --bow
    elseif answer == 3 then
      --have required items
      if game:has_item("bow") == true and game:get_item("coral_ore"):get_amount() >= 1 and game:get_money() >= 50 then
        game:set_value("bow_damage", game:get_value("bow_damage") + 1)
        game:remove_money(50)
        game:get_item("coral_ore"):remove_amount(1)
        game:start_dialog("_goatshead.npcs.palladio.bow_improved")
      else --don't have required items
        game:start_dialog("_game.insufficient_items")
      end

    end -- which answer end

  end) --dialog end

end

October 04, 2018, 07:02:56 AM #1 Last Edit: October 04, 2018, 07:04:44 AM by llamazing
local shop in your map is a table. backsmith() is a function in that table.

When using require, you need to have the script being required return a table. Anywhere that that script is required, it will get a reference to the table, the advantage being that the script only has to be loaded once.
EDIT: the script that gets required does not have to return a table if instead it simply defines global variables/functions

You'll want to do something like the following for your blacksmith script:
Code (lua) Select
local blacksmith = {}

function blacksmith:start_dialog()
  --do stuff
end

function blacksmith:function2()
  --some other function
end

return blacksmith


The problem with this, though, is that the blacksmith script here won't be able to use any references to game since it doesn't know what that is. If the same game instance will be used everywhere you use the script then you can get away with something like the following: (be warned that every time you require the script the game will get set, and only the last one will stick, but in the case of a map it will be set every time the map loads)
Code (lua) Select
local blacksmith = {}

local game

function blacksmith:set_game(current_game)
  game = current_game
end

function blacksmith:start_dialog()
  game:start_dialog("_yarrowmouth.npcs.blacksmith", function(answer)
    --blah blah
  end)
end

function blacksmith:function2()
  --some other function
end

return blacksmith


Then your map script becomes:
Code (lua) Select

local blacksmith_funcs = require("scripts/shops/blacksmith")
blacksmith_funcs:set_game(game)

function blacksmith:on_interaction()
  blacksmith_funcs:start_dialog()
end


But if you're going to have multiple game instances then this solution is no good and you'll want to do something like the following instead:
Code (lua) Select
local blacksmith = {}

function blacksmith:start_dialog(game)
  game:start_dialog("_yarrowmouth.npcs.blacksmith", function(answer)
    --blah blah
  end)
end

function blacksmith:function2(game)
  --some other function
end

return blacksmith


And map script:
Code (lua) Select

local blacksmith_funcs = require("scripts/shops/blacksmith")

function blacksmith:on_interaction()
  blacksmith_funcs:start_dialog(game)
end

I think I understand it all now! The require command will just run the script, which might return a table, if you tell it to. In this case, you should write the script to return a table. Got it! I messed around with this for a long time before I realized I wasn't returning the table, hahaha. All working now!

Thanks, llamazing!

Quote from: Max on October 07, 2018, 08:49:37 PM
The require command will just run the script, which might return a table, if you tell it to.
There's one subtlety that I'm not sure if you picked up on, and that's that the require command will only run the script once. So if the script is also required somewhere else, then instead of running the script a second time, the return values saved from the first time the require command was used will simply be passed along to wherever else the script gets required.

To see an example of this, imagine a script (print_script.lua) to be required that simply contains the line print("some text").

Now if you add require("/scripts/print_script.lua") to both main.lua and game_manager.lua then you'll only see one print statement when the game is loaded.

If instead you change it to sol.main.do_file("scripts/print_script.lua"), then this time the script will actually be run twice and you'll see two print statements. Put the sol.main.do_file statement in a map script and you'll actually see the line get printed every time the map loads, which is not the case if using require.

If the script were to return a table, then using require would result in the same table reference being returned at both places. If using do_file, then two separate table instances (that probably look identical) would be returned. Generally, using require is what you are going to want to use the majority of the time, but there may be instances where sol.main.do_file is better for what you want to accomplish.


As far as I know, requires are usually written at the top of your script, and I recommend to do so (so that others can see the dependencies and avoid errors if some required file is missing). Think on them as "imports", since they are just that.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Awesome, great advice from the both of you : )
I hadn't quite picked up on that subtlty, llamazing, and I can easily imagine myself trying to clean things up by exporting duplicate code to scripts and then being lost when it didn't work as expected. Thanks!

October 11, 2018, 07:56:02 PM #6 Last Edit: October 11, 2018, 07:59:36 PM by alexgleason
Quote from: llamazing on October 07, 2018, 11:10:50 PM
So if the script is also required somewhere else, then instead of running the script a second time, the return values saved from the first time the require command was used will simply be passed along to wherever else the script gets required.

Thank you for clarifying this! I found this thread very helpful.

Variable scope still trips me up. For instance, in this menu script I don't understand why this code doesn't work:

Code ( lua) Select
local menu = {}
local game = sol.main.game -- Use `game` as a shorthand for `sol.main.game`

function menu:on_started()
  print(game:get_item_assigned(1):get_name())
end

return menu


Error: In on_started: [string "scripts/menus/my_menu.lua"]:5: attempt to index upvalue 'game' (a nil value)

Yet this code works just fine:

Code ( lua) Select
local menu = {}

function menu:on_started()
  -- Use sol.main.game directly
  print(sol.main.game:get_item_assigned(1):get_name())
end

return menu


Using sol.main.game works great. 👍 (I'm using the boilerplate code which sets sol.main.game within main.lua)

More confusingly, this code also doesn't work.

Code ( lua) Select
local menu = {}
game = sol.main.game -- Use a global variable this time

function menu:on_started()
  print(game:get_item_assigned(1):get_name())
end

return menu


Error: In on_started: [string "scripts/menus/item_menu.lua"]:5: attempt to index global 'game' (a nil value)

I thought when you require() a Lua script, any global variables created by the script persist throughout the whole codebase. I'm super confused why these don't work. I thought variable scope was the problem, but maybe not?
RIP Aaron Swartz

sol.main.game does not exist yet (i.e. is nil) at the time your script is executed. And your script is executed the first time you require() it, so most likely long before you assign something to sol.main.game.

Quote from: Christopho on October 11, 2018, 08:27:38 PM
sol.main.game does not exist yet (i.e. is nil) at the time your script is executed. And your script is executed the first time you require() it, so most likely long before you assign something to sol.main.game.

OMG thank you! It took me a minute to understand what you meant. The menu script itself is executed before sol.main.game is set, but the function call happens after, so it has access to it. Thank you for bringing me clarity. ;D
RIP Aaron Swartz

The other thing to consider is that even if assigning sol.main.game to local game did work, you'd still have problems for the case where the player exits the game to the title screen and starts a different game, in which case the local game would have a reference to the wrong instance of the game.