· 10 years ago · Aug 28, 2016, 06:24 PM
1--[[ INSTRUCTIONS:
2I. Common stuff:
31) Copy custom_tech_tree.lua to your mod directory.
42) Add a line to your modmain:
5 modimport "custom_tech_tree.lua"
63) After that add one of these lines to modmain:
7 AddNewTechTree("YOUR_TREE_NAME")
8 AddNewTechTree("ANOTHER_TREE_NAME",3) --makes 3 levels, e.g. ANOTHER_TREE_NAME_ONE, also _TWO and _THREE
9 etc. The number means number of levels.
10
11II. Structure prefab:
121) Add "prototyper" tag to crafting structure in pristine state (before SetPristine).
13 inst:AddTag("prototyper")
142) Also you may add "giftmachine" tag if you want.
153) Add prototyper:
16 inst:AddComponent("prototyper")
17 inst.components.prototyper.trees = TUNING.PROTOTYPER_TREES.YOUR_TREE_NAME_ONE
18
19III. Add a recipe:
20 AddRecipe("your_item_prefab", { Ingredient("cutgrass", 1), },
21 RECIPETABS.TOOLS, TECH.YOUR_TREE_NAME_ONE,
22 nil, nil, nil, nil, nil,
23 "images/your_altas_name.xml")
24
25IV. Add text constants (you can support a few languages if you like):
26 STRINGS.UI.CRAFTING.YOUR_TREE_NAME_ONE = "You need a <custom structure name> to make it."
27
28--]]
29_G=GLOBAL
30
31if _G.rawget(_G,"AddNewTechTree") then --Make compatible with other mods.
32 AddNewTechTree = _G.AddNewTechTree
33 return
34end
35
36--Prepare variables. Save existing environment.
37
38local db = {} -- db.MAGIC == 0
39local db_new_techs = {} -- Only new trees, e.g. db_new_techs.NEWTREE == 0
40local db_new_classified = {} --e.g. db_new_classified.NEWTREE == "custom_NEWTREE_level"
41local db_new_builder_name = {} -- e.g. db_new_builder_name.NEWTREE == "builder.accessible_tech_trees.NEWTREE"
42
43for k,v in pairs(_G.TECH.NONE) do --initialize (copy)
44 db[k] = 0
45end
46if TUNING.PROTOTYPER_TREES then --just for sure
47 for k,v in pairs(TUNING.PROTOTYPER_TREES.SCIENCEMACHINE) do
48 db[k] = 0
49 end
50end
51
52local function UpdateDB(newtree_name) --add new element to our table, create common stuff
53 db[newtree_name] = 0
54 db_new_techs[newtree_name] = 0
55 db_new_classified[newtree_name] = "custom_" .. newtree_name .. "_level"
56 db_new_builder_name[newtree_name] = "builder.accessible_tech_trees." .. newtree_name
57end
58
59--Custom little hack instrument.
60local getupvalue, setupvalue, getinfo = _G.debug.getupvalue, _G.debug.setupvalue, _G.debug.getinfo
61local function inject_local(fn, local_fn_name)
62 print("INJECT... Trying to find",local_fn_name)
63 local info = getinfo(fn, "u")
64 local nups = info and info.nups
65 for i = 1, nups do
66 local name, val = getupvalue(fn, i)
67 if (name == local_fn_name) then
68 return val, i
69 end
70 end
71 print("CRITICAL ERROR: Can't find variable "..tostring(upvalue_name).."!")
72end
73
74--Thanks rezecib for the all places in the code:
75--http://forums.kleientertainment.com/topic/69813-how-to-make-custom-tech-tree/
76--1) constants, TECH.NONE.NEWTREE = 0, also TECH.NEWTREE_ONE, etc (these are optional, but get used in the recipes a lot)
77
78local function AddNewTechConstants(newtree_name)
79 _G.TECH.NONE[newtree_name] = 0
80 _G.TECH.LOST[newtree_name] = 10
81 if TUNING.PROTOTYPER_TREES then
82 for k,tbl in pairs(TUNING.PROTOTYPER_TREES) do
83 tbl[newtree_name] = 0
84 end
85 end
86end
87
88local TECH_LEVELS = {'_ONE','_TWO','_THREE','_FOUR','_FIVE'} -- e.g. NEWTREE_ONE
89local saved_tech_names = {} --for text hint in GetHintTextForRecipe
90local function AddTechLevel(newtree_name, level)
91 level = level or 1
92 local level_name
93 if TECH_LEVELS[level] then
94 level_name = newtree_name .. TECH_LEVELS[level]
95 else
96 level_name = newtree_name .. "_" .. tostring(level)
97 end
98 --Save new name
99 if not saved_tech_names[newtree_name] then
100 saved_tech_names[newtree_name] = {}
101 end
102 saved_tech_names[newtree_name][level] = level_name
103 _G.TECH[level_name] = {[newtree_name] = level} --for using in recipes
104 --for using in new crafting structures:
105 if TUNING.PROTOTYPER_TREES then
106 local new_tree = {} --make new instance
107 for k,v in pairs(db) do --copy old data to new instance
108 new_tree[k] = v
109 end
110 new_tree[newtree_name] = level --> make structure useful
111 TUNING.PROTOTYPER_TREES[level_name] = new_tree
112 end
113end
114
115
116
117--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.
118
119--NB: No bonuses for custom tech trees! //star
120--But we still need to make a proxy table because NEWTREE still exists if we leave science machine.
121
122local prototyper = _G.require "components/prototyper"
123local old_TurnOn = prototyper.TurnOn
124function prototyper:TurnOn(doer, ...)
125 if doer.task_custom_tech then
126 doer.task_custom_tech:Cancel()
127 end
128 doer.task_custom_tech = doer:DoTaskInTime(2.5,function(player)
129 local trees_changed = false
130 local tech_tree = player.components.builder.accessible_tech_trees
131 for tech_name, _ in pairs(db_new_techs) do
132 if tech_tree[tech_name] > 0 then
133 trees_changed = true
134 tech_tree[tech_name] = 0
135 end
136 end
137 if trees_changed then
138 player:PushEvent("techtreechange", {level = tech_tree})
139 player.replica.builder:SetTechTrees(tech_tree)
140 end
141 player.task_custom_tech = nil
142 end)
143 return old_TurnOn(self,doer, ...)
144end
145
146
147
148
149--We will just ignore that feature. It should break nothing because prototyper:GetTechTrees() will return proper table with new tech trees.
150
151
152
153--3) components/builder_replica has Setters and Getters for each tree's intrinsic bonuses, these are called later in recipepopup
154
155local replica = _G.require "components/builder_replica"
156local old_SetTechTrees = replica.SetTechTrees
157function replica:SetTechTrees(techlevels,...)
158 if self.classified ~= nil then
159 for tech_name,v in pairs(db_new_techs) do
160 self.classified[db_new_classified[tech_name] ]:set(techlevels[tech_name] or 0)
161 end
162 end
163 return old_SetTechTrees(self,techlevels,...)
164end
165
166
167
168--4) KnowsRecipe explicitly checks each tree in both builder and builder_replica
169
170--No custom bonus.
171--So we should just restrict custom tech trees as bonus.
172
173local AllRecipes = _G.AllRecipes
174
175local builder = _G.require "components/builder"
176local old_KnowsRecipe = builder.KnowsRecipe
177function builder:KnowsRecipe(recname)
178 local result = old_KnowsRecipe(self, recname)
179 if result then
180 if self.freebuildmode or table.contains(self.recipes, recname) then
181 return true
182 end
183 local recipe = AllRecipes[recname]
184 for tech_name,v in pairs(db_new_techs) do
185 if recipe.level[tech_name] > 0 then
186 return false --no custom bonus
187 end
188 end
189 end
190 return result
191end
192
193local old_KnowsRecipe_replica = replica.KnowsRecipe
194function replica:KnowsRecipe(recname)
195 if self.inst.components.builder ~= nil then
196 return self.inst.components.builder:KnowsRecipe(recname)
197 end
198 local result = old_KnowsRecipe_replica(self, recname)
199 if result then
200 if self.classified.isfreebuildmode:value() or
201 self.classified.recipes[recname] ~= nil and self.classified.recipes[recname]:value()
202 then
203 return true
204 end
205 local recipe = AllRecipes[recname]
206 for tech_name,v in pairs(db_new_techs) do
207 if recipe.level[tech_name] > 0 then
208 return false --no custom bonus
209 end
210 end
211 end
212 return result
213end
214
215--5) components/prototyper constructor, self.trees.NEWTREE = 0, and have any prototyper prefabs you make increase that
216
217AddClassPostConstruct("components/prototyper", function(this) --increase table used by :GetTechTrees function.
218 this.trees = {}
219 for k,v in pairs(db) do --db is already prepared for copying.
220 this.trees[k] = v
221 end
222end)
223
224--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.
225
226--Need to inject in local function OnTechTreesDirty and replace it.
227--Pricey hack, so should be done only once per game session.
228local function PlayerClassifiedHack()
229 local RegisterNetListeners_fn = inject_local(_G.Prefabs.player_classified.fn, "RegisterNetListeners")
230 local OnTechTreesDirty_fn_old, var_num = inject_local(RegisterNetListeners_fn, "OnTechTreesDirty")
231 local OnTechTreesDirty_fn_new = function(inst)
232 for tech_name,v in pairs(db_new_techs) do
233 if inst[db_new_classified[tech_name] ] == nil then
234 print("error: inst."..db_new_classified[tech_name].." == nil")
235 else
236 inst.techtrees[tech_name] = inst[db_new_classified[tech_name] ]:value()
237 end
238 end
239 return OnTechTreesDirty_fn_old(inst)
240 end
241 setupvalue(RegisterNetListeners_fn, var_num, OnTechTreesDirty_fn_new)
242end
243local player_classified_hacked = false
244AddPrefabPostInit("world",function(w)
245 if not player_classified_hacked then
246 player_classified_hacked = true
247 PlayerClassifiedHack()
248 end
249end)
250
251--Create network variables.
252local net_tinybyte = _G.net_tinybyte
253AddPrefabPostInit("player_classified",function(inst) --print("ADDPREFABPOSTINIT player_classified")
254 for tech_name,v in pairs(db_new_techs) do
255 inst[db_new_classified[tech_name] ] = net_tinybyte(inst.GUID, db_new_builder_name[tech_name], "techtreesdirty")
256 end
257end)
258
259
260--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)
261
262AddPrefabPostInit("world",function(w) --Fixing all recipes. Just for sure.
263 for rec_name, recipe in pairs(_G.AllRecipes) do
264 for tree_name,_ in pairs(db_new_techs) do
265 recipe.level[tree_name] = recipe.level[tree_name] or 0
266 end
267 end
268end)
269
270--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)
271
272-- (+) Done in AddTechLevel().
273
274--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
275
276local save_hint_recipe
277
278--We need just a link to recipe.
279do
280 local recipepopup = _G.require "widgets/recipepopup"
281 local RecipePopup_Refresh_fn = recipepopup.Refresh
282 local old_GetHintTextForRecipe, num_var = inject_local(RecipePopup_Refresh_fn,"GetHintTextForRecipe")
283 if old_GetHintTextForRecipe then
284 setupvalue(RecipePopup_Refresh_fn, num_var, function(player, recipe)
285 save_hint_recipe = recipe
286 return old_GetHintTextForRecipe(player, recipe)
287 end)
288 end
289end
290
291--Here we can use the link.
292local CRAFTING = _G.STRINGS.UI.CRAFTING --See STRINGS.UI.CRAFTING.NEEDSCIENCEMACHINE
293AddClassPostConstruct("widgets/recipepopup",function(self)
294 local old_SetString = self.teaser.SetString
295 function self.teaser:SetString(str)
296 --print("Show text",str,tostring(save_hint_recipe))
297 if str == "Text not found." and save_hint_recipe ~= nil then --Probably custom recipe
298 local custom_tech, custom_level
299 print("Search tech") _G.arr(save_hint_recipe.level)
300 for tech_name, _ in pairs(db_new_techs) do
301 custom_level = save_hint_recipe.level[tech_name]
302 if custom_level > 0 then
303 custom_tech = tech_name
304 break
305 end
306 end
307 print("Found",custom_tech)
308 if custom_tech then
309 str = CRAFTING[saved_tech_names[custom_tech][custom_level] ] or str
310 print("New str=",str)
311 end
312 end
313 return old_SetString(self,str)
314 end
315end)
316
317--Main function of the lib.
318function AddNewTechTree(newtree_name, num_levels)
319 UpdateDB(newtree_name) --update local tables
320 AddNewTechConstants(newtree_name) --_G.TECH.NEWTREE
321 num_levels = num_levels or 1
322 for i = 1, num_levels do --_G.TECH.NEWTREE_ONE and TUNING.PROTOTYPER_TREES.NEWTREE_ONE, two, three etc.
323 AddTechLevel(newtree_name, i)
324 end
325end
326
327--Global define
328_G.AddNewTechTree = AddNewTechTree
329
330
331_G.temp_debug = {
332 db = db,
333 db_new_techs = db_new_techs,
334 db_new_classified = db_new_classified,
335 db_new_builder_name = db_new_builder_name,
336}