aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Pickering <alexandermpickering@gmail.com>2017-11-26 21:07:54 -0500
committerAlexander Pickering <alexandermpickering@gmail.com>2017-11-26 21:07:54 -0500
commit83af51534bf16bf048aea1cd3b74a0308ed9dd71 (patch)
treeff82f3e6dd841633b1355b73181bcae607ee1138
parent25e4d04a331a6a0b9d897d4f721757730771ff97 (diff)
downloadartery-83af51534bf16bf048aea1cd3b74a0308ed9dd71.tar.gz
artery-83af51534bf16bf048aea1cd3b74a0308ed9dd71.tar.bz2
artery-83af51534bf16bf048aea1cd3b74a0308ed9dd71.zip
Started work on writing tutorials
Wrote tutorials for * Setup * Addon structure * Inventories * Items
-rw-r--r--README.md15
-rw-r--r--config.ld20
-rw-r--r--tutorials/tut000_setup.md (renamed from doc/examples/setup.md)13
-rw-r--r--tutorials/tut010_first_addon.md (renamed from doc/examples/babysfirstaddon.md)12
-rw-r--r--tutorials/tut020_nrequire.md111
-rw-r--r--tutorials/tut021_detouring.md29
-rw-r--r--tutorials/tut030_inventories.md144
-rw-r--r--tutorials/tut031_metatables.md30
-rw-r--r--tutorials/tut032_more_fields.md5
-rw-r--r--tutorials/tut040_items.md25
-rw-r--r--tutorials/tut041_not_enough_items.md27
-rw-r--r--tutorials/tut042_too_many_items.md43
12 files changed, 463 insertions, 11 deletions
diff --git a/README.md b/README.md
index a532f63..a615ba6 100644
--- a/README.md
+++ b/README.md
@@ -30,3 +30,18 @@ In order to build your world, you will probably need to write a lot of your own
* To add randomly spawning monsters, place art_huntablespawn points around the map, and then create zones with bobbleheadbob's Zones tool. Configure the zone with the monsters, and chance of spawning you want.
* To add new items, see the items tutorial on [the documentation](https://cogarr.net/artery/)
* To stich servers together into a logical world, use the zones tool to place art_serverchagne volumes, configure the volume with the server to go to (ipaddress:port) and the location on the server to spawn the player.
+
+Server owner? See @{tut000_setup.md}
+
+Developer looking to use Artery? See the tutorials below!
+
+* @{tut000_setup.md}
+* @{tut010_first_addon.md}
+* @{tut020_nrequire.md}
+* @{tut021_detouring.md}
+* @{tut030_inventories.md}
+* @{tut031_metatables.md}
+* @{tut032_more_fields.md}
+* @{tut040_items.md}
+* @{tut041_not_enough_items.md}
+* @{tut042_too_many_items.md}
diff --git a/config.ld b/config.ld
index e6eb32f..66884e2 100644
--- a/config.ld
+++ b/config.ld
@@ -5,7 +5,8 @@ title = 'Artery Documentation'
style = '!fixed'
use_markdown_titles = true
file = {
- ".",
+ "./gamemode",
+ "./tutorials",
exclude={
"gamemode/core/database/sv_mysqlite.lua"
}
@@ -17,6 +18,7 @@ new_type("metamethod","Meta Methods", false)
new_type("server","Server Modules", true)
new_type("client","Client Modules", true)
new_type("shared","Shared Modules", true)
+new_type("tutorial","Tutorials",true)
custom_tags = {
{'reqadmin', hidden=false},
}
@@ -38,8 +40,16 @@ custom_display_name_handler = function(item,default_handeler)
return default_handeler(item)
end
-readme = "README.md"
-
-examples = {
- "doc/examples"
+readme = {
+ "README.md",
+ "tutorials/tut000_setup.md",
+ "tutorials/tut010_first_addon.md",
+ "tutorials/tut020_nrequire.md",
+ "tutorials/tut021_detouring.md",
+ "tutorials/tut030_inventories.md",
+ "tutorials/tut031_metatables.md",
+ "tutorials/tut032_more_fields.md",
+ "tutorials/tut040_items.md",
+ "tutorials/tut041_not_enough_items.md",
+ "tutorials/tut042_too_many_items.md",
}
diff --git a/doc/examples/setup.md b/tutorials/tut000_setup.md
index 2f9e0ce..d71455f 100644
--- a/doc/examples/setup.md
+++ b/tutorials/tut000_setup.md
@@ -1,4 +1,6 @@
-# Tut 0x00 Setup & Installation
+# Tut 0x000
+
+## Setup and installation
This tutorial covers how to set up Artery so you can begin developing.
@@ -20,3 +22,12 @@ First, you'll need to download the Artery gamemode base. This can be done for up
## Non-git setup
You'll be doing almost the same thing, only downloading zips instead of using the `git clone` command
+
+1. Go to [cogarr.net/source/cgit.cgi/artery](cogarr.net/source/cgit.cgi/artery) and click the latest version of the zip
+2. Find your garrysmod folder, it'll useually be in `C:/Program Files (x86)/Steam/steamapps/common/GarrysMod/garrysmod`
+3. Extract the folder into `garrysmod/gamemodes/` and rename the folder to `artery` so that `garrysmod/gamemodes/artery/README.md` is a valid file.
+3. Downlaod the latest versions of [cogarr.net/source/cgit.cgi/artery_editor](cogarr.net/source/cgit.cgi/artery_editor) and [cogarr.net/source/cgit.cgi/zones](cogarr.net/source/cgit.cgi/zones), and extract them both into the `garrysmod/addons` folder so that `garrysmod/addons/artery_editor/README.txt` and `garrysmod/addons/zones/README.md` are both valid files.
+4. Launch garry's mod (Re-launch if you had it up before) and select the "artery" gamemode, start a server ***with 2 players*** and open console to look for errors.
+
+
+Next tutorial: @{tut010_first_addon.md}
diff --git a/doc/examples/babysfirstaddon.md b/tutorials/tut010_first_addon.md
index 595ad77..d4f8cb1 100644
--- a/doc/examples/babysfirstaddon.md
+++ b/tutorials/tut010_first_addon.md
@@ -1,8 +1,10 @@
-# Tut 0x01 - Baby's First Addon
+# Tut 0x010
+
+## Baby's first addon
A quick primer on addons for Garry's Mod, and Artery.
-You may have seen on the gmod wiki, instructions on running lua code. One way is to place files in your `garrysmod/lua/autorun` directory, and they will be run automatically when you enter single player. This can get messy though, another way to run lua is by making a folder in `garrysmod/addons/<addon_name>/lua/autorun`, and placing your scripts in that (i.e. so that `garrysmod/addons/my_first_addon/lua/autorun/hello.lua` is a valid file). As it turns out, all folders under `garrysmod/addons/<addon_name>` get reflected! This means that if you have a file `garrysmod/addons/data/pac3/my_pac.txt` it will appear in the pac editor and a pac you can load!
+You may have seen on the gmod wiki, instructions on running lua code. One way is to place files in your `garrysmod/lua/autorun` directory, and they will be run automatically when you enter single player. This can get messy though, another way to run lua is by making a folder in `garrysmod/addons/<addon_name>/lua/autorun`, and placing your scripts in that (i.e. so that `garrysmod/addons/my_first_addon/lua/autorun/hello.lua` is a valid file). As it turns out, all folders under `garrysmod/addons/<addon_name>` get reflected! This means that if you have a file `garrysmod/addons/data/pac3/my_pac.txt` it will appear in the pac editor as a pac you can load!
This is the method used by addons for Artery. Since Garry's Mod loads addons's autorun BEFORE gamemodes, Artery uses the `data/artery/global/` folder to run scripts after the gamemode as loaded (and therefore has access to the nrequire() function, more on this later).
@@ -22,8 +24,8 @@ Create a file `garrysmod/addons/artery_routelite/data/artery/global/helloworld.t
and copy+paste the following code into it.
-```
-print("Hello, world!")
-```
+ print("Hello, world!")
Now start up garrysmod with the Artery gamemode selected. Head into flatgrass or something. Open your console, you should see "Hello, world!" printed.
+
+Next tutorial: @{tut020_nrequire.md}
diff --git a/tutorials/tut020_nrequire.md b/tutorials/tut020_nrequire.md
new file mode 100644
index 0000000..2d0cb6d
--- /dev/null
+++ b/tutorials/tut020_nrequire.md
@@ -0,0 +1,111 @@
+# Tut 0x020
+
+## nrequire()
+
+Most gamemodes place a table in the global namespace that exposes the various features of the gamemode. Artery takes a different approach.
+
+Artery places a function in the global namespace, `nrequire()`, which acts as the global table, the way to create dependencies between files, and the auto-includer all at once. Let's do a simple example. If you had a folder structure like this:
+```
+gamemode/
+ one/
+ onefile.lua
+ same.lua
+ two/
+ same.lua
+```
+
+and you wanted to access methods or data in the file `onefile.lua`, you would set your files up like this:
+
+garrysmod/gamemodes/artery/gamemode/one/onefile.lua
+
+ local ret = {}
+
+ ret.mydata = 24
+
+ return ret
+
+
+garrysmod/addons/artery_rougelite/data/artery/global/somefile.lua
+
+ local one = nrequire("onefile.lua")
+
+ print(one.mydata) --Prints 24
+
+
+## Resolving conflicts
+
+But what if you wanted to run some data from `one/same.lua`?
+
+garrysmod/gamemodes/artery/gamemode/one/same.lua
+
+ local ret = {}
+
+ ret.data = 3.14
+
+ return ret
+
+
+If you did
+
+
+garrysmod/addons/artery_rougelite/data/artery/global/somefile.lua
+
+ local same = nrequire("same.lua")
+
+ print(same.data)
+
+
+You would get an error:
+```
+Ambiguous scan, there are two or more paths that match "same.lua"
+ gamemode/one/same.lua
+ gamemode/two/same.lua
+Specify more of the file path.
+```
+
+The solution is simply to write more of the file path
+
+ local same = nrequire("one/same.lua")
+ print(same.data)
+
+
+## nrequire() as dependency resolution
+
+You don't actually need to save whatever nrequire() returns. If you know the file `sv_invfuncs` adds a method to the player metatable, `player:HasItem(itemname)` which you want to use, you can just call nrequire() without saving the return table.
+
+garrysmod/addons/artery_routelite/data/artery/global/somefile.lua
+
+ nrequire("sv_invfuncs.lua")
+
+ local ply = Entity(1)
+
+ ply:HasItem("Test Item")
+
+
+## nrequire() as auto includer
+
+nrequire() is automatically loads all the other modules in artery. It runs them in the client or server domain depending on their file name, much like DarkRP. If the file begins with `sv_` it is run on the server only, if it begins with `cl_` it is sent to the client and run on the client only. Anything else will be run on both the server and the client.
+
+Much likes the main files of artery, the files under `garrysmod/addons/artery_rougelite/data/artery/global` will follow the same scheme. If you want your data to only be run on only the server or client, just name the file beginning with `sv_` or `cl_`.
+
+## Using artery modules
+
+Let's do an example. On the left, you will see a lot of modules provided by artery. One of these, under "shared" domain is `log.lua`. Open it in a new tab.
+
+You will see several methods provided by `log.lua`, to use them, you will first use nrequire() to get access to the functions, then call whatever you like.
+
+garrysmod/addons/artery_rougelite/data/artery/global/logthing.lua
+
+ local log = nrequire("log.lua")
+
+ log.debug("Hello, world!")
+
+
+Note that we named the file `logthing.lua`, since the file does not begin with `sv_` or `cl_`, it will be in the shared domain, that is, run once on the client and once on the server. Since it is run twice, we can expect to see 2 messages in the console!
+
+## Next steps
+
+From here the tutorials open up a bit. You can continue on, or read more in depth about the subject. Tutorials named tut030, tut040, ect. are in logical order needed to build an addon for Artery, tutorials named tut031, tut041, ect. would be continuations of tut030 and tut040 respectively.
+
+Further reading: @{tut021_detouring.md}
+Next tutorial: @{tut030_inventories.md}
diff --git a/tutorials/tut021_detouring.md b/tutorials/tut021_detouring.md
new file mode 100644
index 0000000..7e4c980
--- /dev/null
+++ b/tutorials/tut021_detouring.md
@@ -0,0 +1,29 @@
+# Tut 0x021
+## Detouring
+
+Detouring is a method commonly used in lua, even outside of gmod development. The idea is to redirect a function through your function first, then call the function you're redirecting at the end (or maybe the beginning, or anywhere really). Let's see an example:
+
+garrysmod/lua/autorun/detourprint.lua
+
+ local oldprint = print
+ function print(...)
+ local args = {...}
+ args[#args + 1] = "if you know what I mean..."
+ oldprint(unpack(args))
+ end
+
+The above script __replaces__ the print function, with a new function, which eventually calls the original print function, with modified arguments. The above can be done with modules in Artery as well. For example, `log.lua`'s .error() function currently only prints a message in red, it dosn't actually show what failed or why. Let's modify it to show a stack trace.
+
+garrysmod/addons/artery_rougelite/trace_errors.lua
+
+ local log = nrequire("log.lua")
+ local colors = nrequire("config/colortheme.lua") --Holds colors
+
+ local oldlog = log.error
+ function log.error(...)
+ oldlog(unpack({...}))
+ MsgC(colors.console.red,debug.traceback())
+ end
+
+
+For more information on colortheme.lua see @{colortheme.lua}, for more on debug.traceback() see [the gmod wiki](http://wiki.garrysmod.com/page/debug/traceback).
diff --git a/tutorials/tut030_inventories.md b/tutorials/tut030_inventories.md
new file mode 100644
index 0000000..fbf654d
--- /dev/null
+++ b/tutorials/tut030_inventories.md
@@ -0,0 +1,144 @@
+# Tut 0x030
+
+## Inventories
+
+Many gamemode bases try to build an inventory system into the gamemode, leaving the developer with few ways to actually use the system more than "put item in, take item out". Artery somewhat forces you to build your own inventories to use.
+
+In this tutorial, we'll build a simple rougelike inventory, where we expect items to have a "weight", and we can carry as many items as we want, as long as we don't exceed our "max weight".
+
+Inventories and items in Artery are just tables, as per usual in lua. They have a few required fields each. On the left, under "Classes", open invtbl and itemtbl in new tabs.
+
+## A simple inventory
+
+You can see all the fields needed for an inventory, so let's get started. Recall that
+
+ function tbl:func_name(one)
+ ...
+ end
+
+is the same as
+
+ function tbl.func_name(self,one)
+ ...
+ end
+
+lua will automatically create the variable "self" in the first example
+
+garrysmod/addons/artery_rougelite/data/artery/global/rougeinv.lua
+
+ local inventory_registry = nrequire("inventory.lua")
+ local item_registry = nrequire("item.lua")
+ local inv = {}
+
+ inv.items = {} --A table of all the items we have
+ inv.maxweight = 100 --The maximum weight of all our items
+ inv.weight = 0 --The current weight of all our items.
+ --Technically we could calculate the weight based on the "items" table,
+ -- but that might start taking a long time if we have a lot of low-weight items.
+
+ --Returns a position for an item, or nil if we don't have room
+ function inv:FindPlaceFor(item)
+ --Make sure we won't be over weight when we add the item
+ if self.weight + item.weight > self.maxweight then
+ return nil
+ end
+ --[[
+ ALWAYS return a table position, even if you only need a number.
+ This is the same as
+ return {
+ [1] = #self.items + 1
+ }
+ ]]
+ return {#self.items + 1}
+ end
+
+ function inv:CanFitIn(position,item)
+ --Check that we won't be over weight
+ if self.weight + item.weight > self.maxweight then
+ return false
+ end
+
+ --And make sure we don't already have an item in that position
+ if self.items[position[1]] ~= nil then
+ return false
+ end
+
+ --If we won't go over weight, and the position isn't already used, we can fit it!
+ return true
+ end
+
+ --Put something into our inventory
+ function inv:Put(position,item)
+ self.items[position[1]] = item
+ self.weight = self.weight + item.weight
+ end
+
+ --This one is a bit complicated
+ --ptr can be either a string, or a function that takes 1 argument,
+ -- and returns true when given an object we should return.
+ --this function returns a POSITION that can be given back to get, remove, ect.
+ -- the object.
+ function inv:Has(ptr)
+ --Look for an item with the given name
+ if type(ptr) == "string" then
+ for k,v in pairs(self.items) do
+ if v.Name == ptr then
+ return {k}
+ end
+ end
+ elseif type(ptr) == "function" then
+ for k,v in pairs(self.items) do
+ if ptr(v) then
+ return {k}
+ end
+ end
+ end
+ return nil
+ end
+
+ --Removes an item from our inventory, returns the item we removed
+ function inv:Remove(position)
+ local item = self.items[position[1]]
+ self.weight = self.weight - item.weight
+ self.items[position[1]] = nil
+ return item
+ end
+
+ --Gets the item in a given position
+ function inv:Get(position)
+ return self.items[position[1]]
+ end
+
+ --Creates a string that this inventory can be re-created with
+ function inv:Serialize()
+ local ser = {}
+ for k,v in pairs(self.items) do
+ ser[k] = {
+ v.Name,
+ v:Serialize()
+ }
+ end
+ return util.TableToJSON(ser)
+ end
+
+ --Re-creates this inventory from the data created by Serialize()
+ function inv:DeSerialize(data)
+ local self_copy = table.Copy(self)
+ local json = util.JSONToTable(data)
+ for k,v in pairs(json) do
+ local item_name = v[1]
+ local item_data = v[2]
+ local item = item_registry.GetItemFromData(item_name,item_data)
+ self_copy.items[k] = item
+ end
+ for k,v in pair(self_copy.items) do
+ self_copy.weight = self_copy.weight + v.weight
+ end
+ return self_copy
+ end
+
+ --Don't forget to register it with the gamemode!
+ inventory_registry.RegisterInventory(inv)
+
+
+That was a bit long, but we're done! This is the absolute minimum needed to create an Artery inventory. Now you can go in-game and use the console command `artery_printinventories`, and in that list, you will see your newly made inventory!
diff --git a/tutorials/tut031_metatables.md b/tutorials/tut031_metatables.md
new file mode 100644
index 0000000..a397cc9
--- /dev/null
+++ b/tutorials/tut031_metatables.md
@@ -0,0 +1,30 @@
+# Tut 0x031
+
+## Metatables
+
+In the last tutorial we saw how to make a simple inventory, but we used a function from Gmod's library that was a bit silly. When we made a copy of our inventory, we used table.Copy() to return a copy seperate to the version in the registry. There's nothing wrong with this, but if you have a lot of fields, and default tables in your inventory, this can get quite costly. There's a simple alternative, lua metatables.
+
+[Programming in Lua](https://www.lua.org/pil/13.html) does a much better job at explaining metatables than I ever could, so take a look at how they use it. We're not going to use all the features they discuss, just 1: the \_\_index method, to replace the DeSerialize() function from @{tut030_inventories.md}.
+
+ function inv:DeSerialize()
+ local self_copy = {}
+ local mt = {
+ __index = function(table,key)
+ return self[key]
+ end
+ }
+ setmetatable(self_copy,mt)
+ local json = util.JSONToTable(data)
+ for k,v in pairs(json) do
+ local item_name = v[1]
+ local item_data = v[2]
+ local item = item_registry.GetItemFromData(item_name,item_data)
+ self_copy.items[k] = item
+ end
+ for k,v in pair(self_copy.items) do
+ self_copy.weight = self_copy.weight + v.weight
+ end
+ return self_copy
+ end
+
+Making this change for our example inventory isn't really a big deal, since we don't have many functions, but it's easy to get to a point where it's beneficial to not copy all the fields of an inventory whenever you need to deserialize it.
diff --git a/tutorials/tut032_more_fields.md b/tutorials/tut032_more_fields.md
new file mode 100644
index 0000000..225a65d
--- /dev/null
+++ b/tutorials/tut032_more_fields.md
@@ -0,0 +1,5 @@
+# Tut 0x032
+
+## Additional fields for inventories
+
+TODO: Write this tutorial
diff --git a/tutorials/tut040_items.md b/tutorials/tut040_items.md
new file mode 100644
index 0000000..954f9cd
--- /dev/null
+++ b/tutorials/tut040_items.md
@@ -0,0 +1,25 @@
+# Tut 0x040
+
+## Items
+
+Items are a lot like inventories, but much simpler. Let's make one!
+
+ local item_registry = nrequire("item.lua")
+ local item = {}
+
+ item.Name = "My first item"
+
+ function item:Serialize()
+ return ""
+ end
+
+ --Recall that we said in @{tut030_inventories.md} we said items that work in our inventory will have a .weight field
+ item.weight = 20
+
+ function item:DeSerialize()
+ return table.Copy(self)
+ end
+
+ item_registry.RegisterItem(item)
+
+That's it! We're Done! Note you can use the same trick in DeSerialize() as in @{tut031_metatables.md}. You can now load in to the game and use `artery_printitems` to show a list of items, and see "My first item" as one of them!
diff --git a/tutorials/tut041_not_enough_items.md b/tutorials/tut041_not_enough_items.md
new file mode 100644
index 0000000..895e1fd
--- /dev/null
+++ b/tutorials/tut041_not_enough_items.md
@@ -0,0 +1,27 @@
+# Tut 0x041
+
+## Not enough items
+
+It frequently happens that you want many items with only slight variations. In this tutorial we'll see how to create a item drop for every monster. We'll find all the npc's that the game knows about, and create an item (a corpse) for each one.
+
+First, we need to find all the npc's the game knows about, then create an item for each one.
+
+garrysmod/addons/artery_routelite/data/artery/global/npc_corpses.lua
+```
+local base = {}
+
+base.Name = "Meat base"
+
+base.weight = 10
+
+function base:Serialize()
+ return ""
+end
+
+function base:DeSerialize()
+ return table.Copy(self)
+end
+
+
+
+```
diff --git a/tutorials/tut042_too_many_items.md b/tutorials/tut042_too_many_items.md
new file mode 100644
index 0000000..bcea957
--- /dev/null
+++ b/tutorials/tut042_too_many_items.md
@@ -0,0 +1,43 @@
+# Tut 0x042
+
+## Too many items
+
+In the last tutorial we saw how to create lots and lots of items. Sometimes, you think to yourself "that's way too many", and when you do, you simplify. Rougelike games have a tendency to have attributes associated with items. Commonly, "blessed", normal, and "cursed". In this tutorial, we'll be using the technique discussed in @{tut021_detouring.md} to give every item one of these stats by overrideing @{item.lua.RegisterItem} the Serialize() and DeSerialize() methods to give every item a little extra data.
+
+
+garrysmod/addons/artery_rougelite/data/artery/global/item_attributes.txt
+
+ local itm = nrequire("item.lua")
+ local log = nrequire("log.lua")
+
+ local oldregister = item.RegisterItem
+ function itm.RegisterItem(itemtbl)
+ local oldserialize = itemtbl.Serialize
+ local olddeserialize = itemtbl.DeSerialize
+
+ --Add a single character "b" "n" or "c" to an item's serialize to tell us if it's blessed, normal, or cursed
+ function itemtbl.Serialize(self)
+ local firstchar = "n"
+ if self.attribute == 1 then
+ firstchar = "b"
+ elseif self.attribute == -1
+ firstchar = "c"
+ end
+ return firstchar .. oldserialize(self)
+ end
+ --Note that we've added a "attribute" field to each item, which is 1 if blessed, -1 if cursed, and anything else is normal.
+ function itemtbl.DeSerialize(self,data)
+ local firstchar = string.sub(data,0,1)
+ local rest = string.sub(data,1)
+ olddeserialize(self,rest)
+ if firstchar == "b" then
+ self.attribute = 1
+ elseif firstchar == "c" then
+ self.attribute = -1
+ elseif firstchar == "n" then
+ self.attribute = 0
+ else
+ log.error("Deserialized with unknown first character: " .. firstchar)
+ end
+ end
+ end