Jump to content

Custom tech trees dont like each other


Recommended Posts

So there isn't a lot of mods with custom tech trees and i guess there's a reason why for this?

It seems all custom tech trees ive seen on mods all use the same method of making a tech tree, including my new character. 

I tested 3 mods with custom tech trees, my Shallotte Mod, Windy Plains Mod, and the felix mod. They all lead to this(Or something closely similar):

[00:04:15]: [string "../mods/workshop-923823873/custom_tech_tree..."]:134: attempt to compare number with nil
LUA ERROR stack traceback:
    ../mods/workshop-923823873/custom_tech_tree.lua:134 in (field) fn (Lua) <130-144>
    scripts/scheduler.lua:177 in (method) OnTick (Lua) <155-207>
    scripts/scheduler.lua:371 in (global) RunScheduler (Lua) <369-377>
    scripts/update.lua:170 in () ? (Lua) <149-228>

I dont know who was the originator of the custom_tech_tree.lua but i was wondering if anyone can fix this common bug with all custom tech trees? Supposedly it was originally compatible but i keep encountering this error when i enable two mods with tech trees.

 

--[[ INSTRUCTIONS:
I. Common stuff:
1) Copy custom_tech_tree.lua to your mod directory.
2) Add a line to your modmain:
	modimport "custom_tech_tree.lua"
3) After that add one of these lines to modmain:
	AddNewTechTree("YOUR_TREE_NAME")
	AddNewTechTree("ANOTHER_TREE_NAME",3) --makes 3 levels, e.g. ANOTHER_TREE_NAME_ONE, also _TWO and _THREE
	etc. The number means number of levels.

II. Structure prefab:
1) Add "prototyper" tag to crafting structure in pristine state (before SetPristine).
	inst:AddTag("prototyper")
2) Also you may add "giftmachine" tag if you want.
3) Add prototyper:
	inst:AddComponent("prototyper")
	inst.components.prototyper.trees = TUNING.PROTOTYPER_TREES.YOUR_TREE_NAME_ONE

III. Add a recipe:
	AddRecipe("your_item_prefab", { Ingredient("cutgrass", 1), },
	RECIPETABS.TOOLS, TECH.YOUR_TREE_NAME_ONE,
	nil, nil, nil, nil, nil,
	"images/your_altas_name.xml")

IV. Add text constants (you can support a few languages if you like):
	STRINGS.UI.CRAFTING.YOUR_TREE_NAME_ONE = "You need a <custom structure name> to make it."

--]]
_G=GLOBAL

if _G.rawget(_G,"AddNewTechTree") then --Make compatible with other mods.
	AddNewTechTree = _G.AddNewTechTree --adding to the env of the mod.
	return
end

--Prepare variables. Save existing environment.

local db = {} -- db.MAGIC == 0
local db_new_techs = {} -- Only new trees, e.g. db_new_techs.NEWTREE == 0
local db_new_classified = {} --e.g.  db_new_classified.NEWTREE == "custom_NEWTREE_level"
local db_new_builder_name = {} -- e.g. db_new_builder_name.NEWTREE == "builder.accessible_tech_trees.NEWTREE"

for k,v in pairs(_G.TECH.NONE) do --initialize (copy)
	db[k] = 0
end
if TUNING.PROTOTYPER_TREES then --just for sure
	for k,v in pairs(TUNING.PROTOTYPER_TREES.SCIENCEMACHINE) do
		db[k] = 0
	end
end

local function UpdateDB(newtree_name) --add new element to our table, create common stuff
	db[newtree_name] = 0
	db_new_techs[newtree_name] = 0
	db_new_classified[newtree_name] = "custom_" .. newtree_name .. "_level"
	db_new_builder_name[newtree_name] = "builder.accessible_tech_trees." .. newtree_name
end

--Custom little hack instrument.
local getupvalue, setupvalue, getinfo = _G.debug.getupvalue, _G.debug.setupvalue, _G.debug.getinfo
local function inject_local(fn, local_fn_name)
	print("INJECT... Trying to find",local_fn_name)
	local info = getinfo(fn, "u")
	local nups = info and info.nups
	for i = 1, nups do
		local name, val = getupvalue(fn, i)
		if (name == local_fn_name) then
			return val, i
		end
	end
	print("CRITICAL ERROR: Can't find variable "..tostring(upvalue_name).."!")
end



--Thanks rezecib for the all places in the code:
--http://forums.kleientertainment.com/topic/69813-how-to-make-custom-tech-tree/
--1) constants, TECH.NONE.NEWTREE = 0, also TECH.NEWTREE_ONE, etc (these are optional, but get used in the recipes a lot)

local function AddNewTechConstants(newtree_name)
	_G.TECH.NONE[newtree_name] = 0
	_G.TECH.LOST[newtree_name] = 10
	if TUNING.PROTOTYPER_TREES then
		for k,tbl in pairs(TUNING.PROTOTYPER_TREES) do
			tbl[newtree_name] = 0
		end
	end
end

local TECH_LEVELS = {'_ONE','_TWO','_THREE','_FOUR','_FIVE'} -- e.g. NEWTREE_ONE
local saved_tech_names = {} --for text hint in GetHintTextForRecipe
local function AddTechLevel(newtree_name, level) 
	level = level or 1
	local level_name
	if TECH_LEVELS[level] then 
		level_name = newtree_name .. TECH_LEVELS[level]
	else
		level_name = newtree_name .. "_" .. tostring(level)
	end
	--Save new name
	if not saved_tech_names[newtree_name] then
		saved_tech_names[newtree_name] = {}
	end
	saved_tech_names[newtree_name][level] = level_name
	_G.TECH[level_name] = {[newtree_name] = level} --for using in recipes
	--for using in new crafting structures:
	if TUNING.PROTOTYPER_TREES then
		local new_tree = {} --make new instance
		for k,v in pairs(db) do --copy old data to new instance
			new_tree[k] = v
		end
		new_tree[newtree_name] = level --> make structure useful
		TUNING.PROTOTYPER_TREES[level_name] = new_tree
	end
end



--2) components/builder:EvaluateTechTrees makes specific mention to each of the trees in order to bring in self.bonus. Looks like it might involve a messy override in order to add intrinsic character bonuses to a tree, because it does both the check and the result without any functions in between to hook into... You could maybe do it by replacing self.accessible_tech_trees with a proxy table that has a metatable so you can intercept the assignment? That's pretty gross, though.

--NB: No bonuses for custom tech trees! //star

--But we still to check if player goes away (over time).
local prototyper = _G.require "components/prototyper"
local old_TurnOn = prototyper.TurnOn
function prototyper:TurnOn(doer, ...)
	if doer.task_custom_tech then
		doer.task_custom_tech:Cancel()
	end
	doer.task_custom_tech = doer:DoTaskInTime(1.5,function(player)
		local trees_changed = false
		local tech_tree = player.components.builder.accessible_tech_trees
		for tech_name, _ in pairs(db_new_techs) do
			if tech_tree[tech_name] > 0 then
				trees_changed = true
				tech_tree[tech_name] = 0
			end
		end
		if trees_changed then
			player:PushEvent("techtreechange", {level = tech_tree})
			player.replica.builder:SetTechTrees(tech_tree)
		end
		player.task_custom_tech = nil
	end)
	return old_TurnOn(self,doer, ...)
end



--3) components/builder_replica has Setters and Getters for each tree's intrinsic bonuses, these are called later in recipepopup

local replica = _G.require "components/builder_replica"
local old_SetTechTrees = replica.SetTechTrees
function replica:SetTechTrees(techlevels,...)
	if self.classified ~= nil then
		for tech_name,v in pairs(db_new_techs) do
			self.classified[db_new_classified[tech_name] ]:set(techlevels[tech_name] or 0)
		end
	end
	return old_SetTechTrees(self,techlevels,...)
end



--4) KnowsRecipe explicitly checks each tree in both builder and builder_replica

--No custom bonus.
--So we should just restrict custom tech trees as bonus.

local AllRecipes = _G.AllRecipes

local builder = _G.require "components/builder"
local old_KnowsRecipe = builder.KnowsRecipe
function builder:KnowsRecipe(recname)
	local result = old_KnowsRecipe(self, recname)
	if result then
		if self.freebuildmode or table.contains(self.recipes, recname) then
			return true
		end
		local recipe = AllRecipes[recname]
		for tech_name,v in pairs(db_new_techs) do
			if recipe.level[tech_name] > 0 then
				return false --no custom bonus
			end
		end
	end
	return result
end

local old_KnowsRecipe_replica = replica.KnowsRecipe
function replica:KnowsRecipe(recname)
    if self.inst.components.builder ~= nil then
        return self.inst.components.builder:KnowsRecipe(recname)
	end
	local result = old_KnowsRecipe_replica(self, recname)
	if result then
		if self.classified.isfreebuildmode:value() or 
			self.classified.recipes[recname] ~= nil and self.classified.recipes[recname]:value()
		then
			return true
		end
		local recipe = AllRecipes[recname]
		for tech_name,v in pairs(db_new_techs) do
			if recipe.level[tech_name] > 0 then
				return false --no custom bonus
			end
		end
	end
	return result
end



--5) components/prototyper constructor, self.trees.NEWTREE = 0, and have any prototyper prefabs you make increase that

AddClassPostConstruct("components/prototyper", function(this) --increase table used by :GetTechTrees function.
	this.trees = {}
	for k,v in pairs(db) do --db is already prepared for copying.
		this.trees[k] = v
	end
end)



--6) prefabs/player_classified OnTechTreesDirty checks each of them explicitly. You could add a listener for "techtreesdirty" and make your check there. Also inst.newtreelevel would need to be added.

--Need to inject in local function OnTechTreesDirty and replace it.
--Pricey hack, so should be done only once per game session.
local function PlayerClassifiedHack()
	local RegisterNetListeners_fn = inject_local(_G.Prefabs.player_classified.fn, "RegisterNetListeners")
	local OnTechTreesDirty_fn_old, var_num = inject_local(RegisterNetListeners_fn, "OnTechTreesDirty")
	local OnTechTreesDirty_fn_new = function(inst)
		for tech_name,v in pairs(db_new_techs) do
			if inst[db_new_classified[tech_name] ] == nil then
				print("error: inst."..db_new_classified[tech_name].." == nil")
			else
				inst.techtrees[tech_name] = inst[db_new_classified[tech_name] ]:value()
			end
		end
		return OnTechTreesDirty_fn_old(inst)
	end
	setupvalue(RegisterNetListeners_fn, var_num, OnTechTreesDirty_fn_new)
end
local player_classified_hacked = false
AddPrefabPostInit("world",function(w)
	if not player_classified_hacked then
		player_classified_hacked = true
		PlayerClassifiedHack()
	end
end)

--Create network variables. 
local net_tinybyte = _G.net_tinybyte
AddPrefabPostInit("player_classified",function(inst) --print("ADDPREFABPOSTINIT player_classified")
	for tech_name,v in pairs(db_new_techs) do
		inst[db_new_classified[tech_name] ] = net_tinybyte(inst.GUID, db_new_builder_name[tech_name], "techtreesdirty")
	end
end)



--7) recipe, self.level.NEWTREE = self.level.NEWTREE or 0 needs to be added, could be done by overriding Recipe._ctor, or doing an AddClassPostConstruct (maybe? the recipe.lua file doesn't return the class though, so maybe not)

AddPrefabPostInit("world",function(w) --Fixing all recipes. Just for sure.
	for rec_name, recipe in pairs(_G.AllRecipes) do
		for tree_name,_ in pairs(db_new_techs) do
			recipe.level[tree_name] = recipe.level[tree_name] or 0
		end
	end
end)



--8) tuning, PROTOTYPER_TREES needs to have each of its entries modified (although I think if you don't they should default to zero from the other changes)

-- (+) Done in AddTechLevel().

--9) widgets/recipepopup GetHintTextForRecipe takes into account player intrinsic bonuses to trees for the help text. This looks like it might need to be overridden

local save_hint_recipe

--We need just a link to recipe.
do
	local recipepopup = _G.require "widgets/recipepopup"
	local RecipePopup_Refresh_fn = recipepopup.Refresh
	local old_GetHintTextForRecipe, num_var = inject_local(RecipePopup_Refresh_fn,"GetHintTextForRecipe")
	if old_GetHintTextForRecipe then
		setupvalue(RecipePopup_Refresh_fn, num_var, function(player, recipe)
			save_hint_recipe = recipe
			return old_GetHintTextForRecipe(player, recipe)
		end)
	end
end

--Here we can use the link. 
local CRAFTING = _G.STRINGS.UI.CRAFTING --See STRINGS.UI.CRAFTING.NEEDSCIENCEMACHINE
AddClassPostConstruct("widgets/recipepopup",function(self)
	local old_SetString = self.teaser.SetString
	function self.teaser:SetString(str)
		--print("Show text",str,tostring(save_hint_recipe))
		if str == "Text not found." and save_hint_recipe ~= nil  then --Probably custom recipe
			local custom_tech, custom_level
			for tech_name, _ in pairs(db_new_techs) do --Check if it's really custom.
				custom_level = save_hint_recipe.level[tech_name]
				if custom_level > 0 then
					custom_tech = tech_name
					break
				end
			end
			if custom_tech then
				str = CRAFTING[saved_tech_names[custom_tech][custom_level] ] or str
			end
		end
		return old_SetString(self,str)
	end
end)



--Main function of the lib.
function AddNewTechTree(newtree_name, num_levels)
	UpdateDB(newtree_name) --update local tables
	AddNewTechConstants(newtree_name) --_G.TECH.NEWTREE
	num_levels = num_levels or 1
	for i = 1, num_levels do --_G.TECH.NEWTREE_ONE and TUNING.PROTOTYPER_TREES.NEWTREE_ONE, two, three etc.
		AddTechLevel(newtree_name, i)
	end
end

--Global define
_G.AddNewTechTree = AddNewTechTree
Edited by rons0n
Link to comment
Share on other sites

Is it this one? 

The one Maris posted here is the one everyone is using within their own custom tech trees. There was another modder while I was searching through the fourms called Zarklord who made his own custom tech tree but when testing it in a non-dedicated server it didnt work.(I think anyway, the way I used it didnt work).

@Maris Is there a way to update the custom_tech_tree.lua, as it seems to no longer be compatible with other custom tech tree mods.

Link to comment
Share on other sites

Alright just turns out the windy plains mod doesnt seem to like other Prototypers.

If anyone out here plans on making a custom prototyper I suggest using Felix, <Defaults>, or mine(which is pretty much Felix) way of making prototypers. just giving an FYI

Edited by rons0n
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...