Inventory Access Rules - rebel1324/NutScript GitHub Wiki

Access Rules

Inventories typically have some constraints. For example, inventories usually have some sort of maximum number of items it can hold. The inventory class we made so far does not have any constraints, so it can hold an arbitrary number of items.

Recall in the beginning of the tutorial, the end result would be a simple inventory with a maximum capacity. In this part, we will go over how to make sure that MyInventory inventories can only hold a finite number of items using access rules. This will build on the code from the previous part.

Definition

An access rule is a function that takes as input an inventory instance, a string describing an inventory action, and a context. A context is a table which will hold some information about the action. This function then returns one of the following:

  1. true
  2. false
  3. nothing (nil)

If an access rule returns true, then the action is allowed. However, if the access rule returns false, then the action is not allowed. If the action does not return anything (nil), then the decision is left to some other code (e.g. other access rules, or the code responsible for running the action rules).

If an access rule returns false, the function may also return a string containing the reason why the action was denied as a second return value.

Example: Since we want to limit the number of items in our MyInventory instances, we should have a rule that stops items from being added if the inventory is at capacity. So, when an item is being added (i.e. action is "add"), then we should return false if there are too many items. Otherwise, we can return true since the inventory is not at capacity.

MAX_ITEMS = 10

function CanAddItemIfNotAtCapacity(inventory, action, context)
  if (action == "add") then
    return table.Count(inventory:getItems()) < MAX_ITEMS
  end
end

Adding Access Rules to an Inventory

To a Single Instance

Given an inventory instance inventory and an access rule rule, we can add the access rule to inventory using the addAccessRule method. So, we would write inventory:addAccessRule(rule) to make it so the next time an access check if being done, rule will run.

Access rules are ran in the order they are added.

By default, the addAccessRule method will add the given access rule to the end of the list of existing access rules. If you wish to change the order, the second argument to addAccessRule is a positive integer. The smaller the number, the higher priority the access rule. So, if the inventory had existing access rules, inventory:addAccessRule(rule) will cause rule to run after all the existing access rules. But, inventory:addAccessRule(rule, 1) will make it so rule runs first, before all the other access rules.

To all Instances of a Class

If you wish to add the access rule to all inventory instances of a certain class, you can use the configure method on the inventory class. From the previous parts of this tutorial, we have our MyInventory class. So, if we wanted to add the CanMaybeAddItem access rule from above to the MyInventory class, we would add the following code to our class:

function MyInventory:configure()
  self:addAccessRule(CanAddItemIfNotAtCapacity)
end

Checking for Access

Now that we have an access rule, we need to actually call it. Since we want to control when items are being added to our inventory, we should somehow call the access rules in our add method. From the last part, we have

function MyInventory:add(itemType)
  return nut.item.instance(itemType)
    :next(function(item)
      -- Note we do not use the colon syntax since we do not want to
      -- pass in self.BaseClass as the first argument!
      return self.BaseClass.add(self, item)
    end)
end

To call access rules, we use the canAccess method. Since our access rule checked if action is "add", we will pass in "add" as the first argument. Then, we pass in a table as the second argument. This table is the context for the access rules.

The canAccess method returns

  1. true if an access rule returns true
  2. false if an access rule returns false
  3. nil otherwise (either no access rules exist or none of them returned anything)

The second return value may be a string containing the reason why an access rule returned false, if the first return value is false.

So, if we cannot access the inventory, we should not add the item. Thus, we have:

function MyInventory:add(itemInstanceOrType)
  -- If the first argument is an item, just delegate to the original `add` method.
  if (nut.item.isItem(itemInstanceOrType)) then
    return self.BaseClass.add(self, itemInstanceOrType)
  end
  local itemType = itemInstanceOrType

  -- If we cannot add the item, just return an error.
  local itemTable = nut.item.list[itemType]
  assert(itemTable, tostring(itemType).." is not a valid item")

  local context = { item = itemTable }
  local canAccess, reason = self:canAccess("add", context)
  if (not canAccess) then
    return deferred.resolve({ err = reason })
  end

  return nut.item.instance(itemType)
    :next(function(item)
      -- Note we do not use the colon syntax since we do not want to
      -- pass in self.BaseClass as the first argument!
      return self.BaseClass.add(self, item)
    end)
end

Now, we have an inventory that can only hold a limited number of items! As an exercise, you should test this.

Standard Actions

The framework itself has some standard access rules that you should support.

add

The "add" action is for when an item is being added to the inventory using the add method. The context contains at least the following keys:

Key Description
item An item table (either an instance or the of the values in nut.item.list) corresponding to the item being added.

repl

The "repl" action is used to determine which players are allowed to see inventory changes on the client side. For example, if an item is added to the inventory, only players who pass the "repl" check may receive the event client-side. The context contains at least the following keys:

Key Description
client The player that is being checked. Returning true means the player will have state changes replicated client-side.

Removing Access Rules

You may want some of your access rules to be temporary. You can use the Inventory:removeAccessRule(rule) method. Given a reference to an access rule added before, it removes the access rule from the inventory. For example, you may add an access rule somewhere like:

function AllowEverything(inventory, action, context)
    return true
end

-- With some inventory instance:
someInventory:addAccessRule(AllowEverything)

Later on, you can remove the access rule using:

someInventory:removeAccessRule(AllowEverything)

If you are going to create a temporary function for your access rule, it is important you save a reference to it somewhere so you can remove the access rules later on. Access rules are removed by value, so the argument for removeAccessRule must be the same value as the one passed into addAccessRule.