#priority init 2000 import hooks; import saving; import attributes; export TechnologyType, TechnologyNode, TechnologyClass; export TechAddModifier, parseModifier; export getTechnology, getTechnologyCount; export getTechnologyID, getTechnologyIdent; export TechnologyGridSpec, getTechnologyGridSpec; export TechnologyGrid; //{{{ Technologies interface ITechnologyHook { void unlock(TechnologyNode@ node, Empire& emp) const; bool canUnlock(TechnologyNode@ node, Empire& emp) const; bool canBeSecret(TechnologyNode@ node, Empire& emp) const; bool getSecondaryUnlock(TechnologyNode@ node, Empire@ emp, string& text) const; bool canSecondaryUnlock(TechnologyNode@ node, Empire& emp) const; bool consumeSecondary(TechnologyNode@ node, Empire& emp) const; void reverseSecondary(TechnologyNode@ node, Empire& emp) const; void tick(TechnologyNode@ node, Empire& emp, double time) const; void onStateChange(TechnologyNode@ node, Empire@ emp) const; void addToDescription(TechnologyNode@ node, Empire@ emp, string& description) const; void modPointCost(const TechnologyNode@ node, Empire& emp, double& pointCost) const; void modTimeCost(const TechnologyNode@ node, Empire& emp, double& timeCost) const; }; class TechnologyHook : ITechnologyHook, Hook { void unlock(TechnologyNode@ node, Empire& emp) const {} bool canUnlock(TechnologyNode@ node, Empire& emp) const { return true; } bool canBeSecret(TechnologyNode@ node, Empire& emp) const { return true; } bool getSecondaryUnlock(TechnologyNode@ node, Empire@ emp, string& text) const { return false; } bool canSecondaryUnlock(TechnologyNode@ node, Empire& emp) const { return true; } bool consumeSecondary(TechnologyNode@ node, Empire& emp) const { return true; } void reverseSecondary(TechnologyNode@ node, Empire& emp) const {} void tick(TechnologyNode@ node, Empire& emp, double time) const {} void onStateChange(TechnologyNode@ node, Empire@ emp) const {} void addToDescription(TechnologyNode@ node, Empire@ emp, string& description) const {} void modPointCost(const TechnologyNode@ node, Empire& emp, double& pointCost) const {} void modTimeCost(const TechnologyNode@ node, Empire& emp, double& timeCost) const {} }; enum TechnologyClass { Tech_Boost, Tech_Upgrade, Tech_BigUpgrade, Tech_Unlock, Tech_Keystone, Tech_Secret, Tech_Special, }; tidy final class TechnologyType { uint id; string ident; string name; string description; string blurb; string secondaryTerm = locale::RESEARCH_STUDY; Sprite icon; Sprite symbol; Color color; uint cls = Tech_Upgrade; string dlc; string dlcReplace; string category = "Vanilla"; bool secret = false; double secretFrequency = 1.0; bool defaultUnlock = false; double pointCost = 0; double timeCost = 0; array hooks; }; tidy final class TechnologyNode : Serializable, Savable { int id = -1; vec2i position; const TechnologyType@ type; //Whether the node has completely finished unlocking bool unlocked = false; //Whether the node has been purchased in some way, and will unlock in time bool bought = false; //Whether this node can be bought (has bought nodes adjacent) bool available = false; //Whether this node can start unlocking (has unlocked nodes adjacent) bool unlockable = false; //Whether this node was unlocked using its secondary option bool secondaryUnlock = false; //Whether this node has been queued to be researched bool queued = false; //Whether this is a secret node bool secret = false; bool secretPicked = false; //Unlock time remaining double timer = -1.0; void write(Message& msg) { msg.writeSignedSmall(id); msg.writeSignedSmall(position.x); msg.writeSignedSmall(position.y); msg.writeSmall(type.id); msg << unlocked << bought; msg << available << unlockable << queued; msg << secret << secretPicked; msg << float(timer); } void read(Message& msg) { id = msg.readSignedSmall(); position.x = msg.readSignedSmall(); position.y = msg.readSignedSmall(); @type = getTechnology(msg.readSmall()); msg >> unlocked >> bought; msg >> available >> unlockable >> queued; msg >> secret >> secretPicked; timer = msg.read_float(); } void writeStatus(Message& msg) { msg << unlocked << bought; msg << available << unlockable << queued; msg << secret << secretPicked; if(timer >= 0) { msg.write1(); msg << float(timer); } else { msg.write0(); } } void readStatus(Message& msg) { msg >> unlocked >> bought; msg >> available >> unlockable >> queued; msg >> secret >> secretPicked; if(msg.readBit()) timer = msg.read_float(); else timer = -1.0; } void save(SaveFile& file) { file << id; file.writeIdentifier(SI_Technology, type.id); file << position; file << unlocked << bought; file << available << unlockable; file << secondaryUnlock; file << timer; file << queued; file << secret << secretPicked; } void load(SaveFile& file) { file >> id; @type = getTechnology(file.readIdentifier(SI_Technology)); file >> position; file >> unlocked >> bought; file >> available >> unlockable; file >> secondaryUnlock; file >> timer; if(file >= SV_0135) file >> queued; if(file >= SV_0137) file >> secret >> secretPicked; } double getPointCost(Empire@ emp = null) const { double pointCost = type.pointCost; if(emp !is null) { for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) type.hooks[i].modPointCost(this, emp, pointCost); } if(emp !is null) pointCost *= emp.ResearchCostFactor; return pointCost; } double getTimeCost(Empire@ emp = null) const { double timeCost = type.timeCost; if(emp !is null) { for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) type.hooks[i].modTimeCost(this, emp, timeCost); } return timeCost; } bool hasSecondary(Empire@ emp = null) { string tmp; for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) if(type.hooks[i].getSecondaryUnlock(this, emp, tmp)) return true; return false; } bool canSecondaryUnlock(Empire& emp) { if(!hasSecondary(emp)) return false; if(secret && !secretPicked) return false; for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) if(!type.hooks[i].canSecondaryUnlock(this, emp)) return false; return true; } bool canUnlock(Empire& emp) { if(!available) return false; if(secret && !secretPicked) return false; for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) if(!type.hooks[i].canUnlock(this, emp)) return false; return true; } bool hasRequirements(Empire& emp) { for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) if(!type.hooks[i].canUnlock(this, emp)) return false; return true; } bool canBeSecret(Empire& emp) { for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) if(!type.hooks[i].canBeSecret(this, emp)) return false; return true; } string getSecondaryCost(Empire@ emp = null, const string& sep = locale::RESEARCH_COST_JOIN) { string text, elem; for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) { if(type.hooks[i].getSecondaryUnlock(this, emp, elem)) { if(text.length != 0) text += sep; text += elem; elem = ""; } } return text; } bool consumeSecondary(Empire& emp) { for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) { if(!type.hooks[i].consumeSecondary(this, emp)) { for(uint n = 0; n < i; ++n) type.hooks[n].reverseSecondary(this, emp); return false; } } return true; } #section server void buy(Empire& emp) { timer = getTimeCost(emp); bought = true; queued = false; } void tick(Empire& emp, TechnologyGrid& grid, double time) { if(available && queued && !bought) { double cost = getPointCost(emp); if(cost > 0) { if(emp.ResearchPoints >= type.pointCost && canUnlock(emp)) emp.research(id); } else { if(canUnlock(emp)) emp.research(id, secondary=true); } return; } if(timer >= 0 && unlockable) { timer -= time * emp.ResearchUnlockSpeed; if(timer <= 0) { timer = -1.0; grid.markUnlocked(position, emp); unlock(emp); } } for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) type.hooks[i].tick(this, emp, time); } void unlock(Empire& emp) { unlocked = true; for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) type.hooks[i].unlock(this, emp); if(type.cls == Tech_Unlock) emp.modAttribute(EA_ResearchUnlocksDone, AC_Add, 1.0); } #section all void stateChange(Empire@ emp) { for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) type.hooks[i].onStateChange(this, emp); } int opCmp(const TechnologyNode& other) const { if(other.timer < timer) return 1; if(timer < other.timer) return -1; return 0; } }; tidy final class TechnologyGrid : Savable { HexGridi indices; vec2i minPos; vec2i maxPos; array nodes; void save(SaveFile& file) { file << minPos; file << maxPos; uint cnt = nodes.length; file << cnt; for(uint i = 0; i < cnt; ++i) file << nodes[i]; } void load(SaveFile& file) { file >> minPos; file >> maxPos; uint cnt = 0; file >> cnt; nodes.length = cnt; for(uint i = 0; i < cnt; ++i) { TechnologyNode node; file >> node; @nodes[i] = node; } regenGrid(); } void regenBounds() { minPos = vec2i(INT_MAX, INT_MAX); maxPos = vec2i(INT_MIN, INT_MIN); for(uint i = 0, cnt = nodes.length; i < cnt; ++i) { auto@ node = nodes[i]; minPos.x = min(minPos.x, node.position.x); minPos.y = min(minPos.y, node.position.y); maxPos.x = max(maxPos.x, node.position.x); maxPos.y = max(maxPos.y, node.position.y); } if(minPos.y % 2 != 0) minPos.y -= 1; regenGrid(); } void insert(const vec2i& pos, TechnologyNode@ node) { nodes.insertLast(node); regenBounds(); } void remove(const vec2i& pos) { auto@ node = getNode(pos); if(node !is null) { nodes.remove(node); regenBounds(); } } void regenGrid() { indices.resize(maxPos.x - minPos.x + 1, maxPos.y - minPos.y + 1); indices.clear(-1); for(uint i = 0, cnt = nodes.length; i < cnt; ++i) { vec2u relPos = vec2u(nodes[i].position - minPos); if(!indices.valid(relPos)) continue; indices[relPos] = int(i); } } TechnologyNode@ getNode(const vec2i& pos) { vec2u relPos = vec2u(pos - minPos); if(!indices.valid(relPos)) return null; int ind = indices[relPos]; if(ind == -1 || ind >= int(nodes.length)) return null; return nodes[ind]; } int getIndex(const vec2i& pos) { vec2u relPos = vec2u(pos - minPos); if(!indices.valid(relPos)) return -1; int ind = indices[relPos]; if(ind == -1 || ind >= int(nodes.length)) return -1; return ind; } TechnologyNode@ getAdjacentNode(const vec2i& pos, uint adj) { vec2i adv = pos; if(!doAdvance(adv, HexGridAdjacency(adj))) return null; return getNode(adv); } bool doAdvance(vec2i& pos, HexGridAdjacency adj) { vec2u flipped(pos.y-minPos.y, pos.x-minPos.x); if(!advanceHexPosition(flipped, vec2u(maxPos.y-minPos.y+1, maxPos.x-minPos.x+1), adj)) return false; pos = vec2i(int(flipped.y)+minPos.x, int(flipped.x)+minPos.y); return true; } void markBought(const vec2i& pos, Empire@ emp = null) { auto@ node = getNode(pos); if(node is null) return; node.bought = true; for(uint i = 0; i < 6; ++i) { vec2i otherPos = pos; if(doAdvance(otherPos, HexGridAdjacency(i))) { auto@ otherNode = getNode(otherPos); if(otherNode !is null && !otherNode.available) { otherNode.available = true; otherNode.stateChange(emp); } } } } void markUnlocked(const vec2i& pos, Empire@ emp = null) { auto@ node = getNode(pos); if(node is null) return; node.bought = true; node.unlocked = true; node.available = true; node.unlockable = true; for(uint i = 0; i < 6; ++i) { vec2i otherPos = pos; if(doAdvance(otherPos, HexGridAdjacency(i))) { auto@ otherNode = getNode(otherPos); if(otherNode !is null && (!otherNode.available || !otherNode.unlockable)) { otherNode.available = true; otherNode.unlockable = true; otherNode.stateChange(emp); } } } } }; tidy final class TechnologyGridSpec { uint id; string ident; HexGridi indices; array nodes; vec2i minPos; vec2i maxPos; array def_nodes; array def_positions; void resolve() { if(def_nodes.length == 0) { minPos = vec2i(); maxPos = vec2i(); return; } minPos = vec2i(INT_MAX, INT_MAX); maxPos = vec2i(INT_MIN, INT_MIN); for(uint i = 0, cnt = def_positions.length; i < cnt; ++i) { TechnologyNode node; node.position = def_positions[i]; @node.type = getTechnology(def_nodes[i]); if(node.type is null) { error("Technology grid "+ident+" can't find technology "+def_nodes[i]); continue; } node.secret = node.type.secret; minPos.x = min(minPos.x, node.position.x); minPos.y = min(minPos.y, node.position.y); maxPos.x = max(maxPos.x, node.position.x); maxPos.y = max(maxPos.y, node.position.y); nodes.insertLast(node); } if(minPos.y % 2 != 0) minPos.y -= 1; indices.resize(maxPos.x - minPos.x + 1, maxPos.y - minPos.y + 1); indices.clear(-1); for(uint i = 0, cnt = nodes.length; i < cnt; ++i) { vec2u relPos = vec2u(nodes[i].position - minPos); if(!indices.valid(relPos)) continue; indices[relPos] = int(i); } } TechnologyGrid@ create() const { TechnologyGrid grid; grid.nodes.length = nodes.length; grid.indices = indices; grid.minPos = minPos; grid.maxPos = maxPos; for(uint i = 0, cnt = nodes.length; i < cnt; ++i) { TechnologyNode@ spec = nodes[i]; TechnologyNode n; n.id = int(i); @n.type = spec.type; n.position = spec.position; n.secret = spec.secret; if(n.type.dlc.length != 0) { if(!hasDLC(n.type.dlc)) { auto@ otherType = getTechnology(n.type.dlcReplace); if(otherType !is null) @n.type = otherType; } } @grid.nodes[i] = n; } for(uint i = 0, cnt = nodes.length; i < cnt; ++i) { if(nodes[i].type.defaultUnlock) grid.markUnlocked(nodes[i].position); } return grid; } }; //}}} //{{{ Subsystem modifiers tidy final class TechAddModifier { string spec; array subsys; string modifier; array arguments; void apply(Empire@ emp, array@ ids = null) const { set_int improvedSubsystems; if(ids !is null) ids.length = 0; { WriteLock lock(emp.subsystemDataMutex); for(uint j = 0, jcnt = subsys.length; j < jcnt; ++j) { const SubsystemDef@ def = subsys[j]; if(!def.hasModifier(modifier)) continue; uint id = uint(-1); switch(arguments.length) { case 0: id = emp.addModifier(def, modifier); break; case 1: id = emp.addModifier(def, modifier, toFloat(arguments[0])); break; case 2: id = emp.addModifier(def, modifier, toFloat(arguments[0]), toFloat(arguments[1])); break; default: id = emp.addModifier(def, modifier, toFloat(arguments[0]), toFloat(arguments[1]), toFloat(arguments[2])); break; } if(ids !is null) ids.insertLast(id); improvedSubsystems.insert(def.index); } } //Auto-update all designs related to the improved subsystems //TODO: Shit's slow. Check index set overlap? if(!isShadow) { WriteLock lck(emp.designMutex); uint cnt = emp.designCount; for(uint i = 0; i < cnt; ++i) { const Design@ dsg = emp.designs[i]; if(dsg.updated !is null) continue; uint sysCnt = dsg.subsystemCount; for(uint j = 0; j < sysCnt; ++j) { const SubsystemDef@ def = dsg.subsystems[j].type; if(improvedSubsystems.contains(def.index)) { emp.flagDesignOld(dsg); break; } } } } } void remove(Empire@ emp, array& ids) const { set_int improvedSubsystems; { WriteLock lock(emp.subsystemDataMutex); uint index = 0; for(uint j = 0, jcnt = min(subsys.length, ids.length); j < jcnt; ++j) { const SubsystemDef@ def = subsys[j]; if(!def.hasModifier(modifier)) continue; emp.removeModifier(def, ids[index++]); improvedSubsystems.insert(def.index); } } //Auto-update all designs related to the improved subsystems //TODO: Shit's slow. Check index set overlap? if(!isShadow) { WriteLock lck(emp.designMutex); uint cnt = emp.designCount; for(uint i = 0; i < cnt; ++i) { const Design@ dsg = emp.designs[i]; if(dsg.updated !is null) continue; uint sysCnt = dsg.subsystemCount; for(uint j = 0; j < sysCnt; ++j) { const SubsystemDef@ def = dsg.subsystems[j].type; if(improvedSubsystems.contains(def.index)) { emp.flagDesignOld(dsg); break; } } } } } } TechAddModifier@ parseModifier(const string& value) { TechAddModifier modifier; string subsysName; int index = value.findFirst("::"); if(index > 0) { modifier.spec = value; modifier.modifier = value.substr(index+2, value.length() - (index+2)); subsysName = value.substr(0,index); } else { modifier.modifier = value; } int brkt = modifier.modifier.findFirst("("); if(brkt != -1) { string funcName; funcSplit(modifier.modifier, funcName, modifier.arguments); modifier.modifier = funcName; } if(subsysName.length == 0 || subsysName == "any") subsysName = "mod/"+modifier.modifier; parseSubsysSpec(modifier.subsys, subsysName); if(modifier.subsys.length == 0) { error(format("No subsystems matched '$2' / '$3'", value, subsysName)); return null; } return modifier; }; void parseSubsysSpec(array@ list, const string& spec) { auto@ conds = spec.split(","); list.length = getSubsystemDefCount(); for(uint i = 0, cnt = list.length; i < cnt; ++i) @list[i] = getSubsystemDef(i); for(uint i = 0, cnt = conds.length; i < cnt; ++i) filterSubsys(list, conds[i]); } void filterSubsys(array@ list, const string& spec) { uint cnt = list.length; if(spec.startswith("tag/")) { string tag = spec.substr(4, spec.length - 4); for(uint i = 0; i < cnt; ++i) { if(!list[i].hasTag(tag)) { @list[i] = list[cnt-1]; --cnt; --i; } } list.length = cnt; } else if(spec.startswith("module/")) { string mod = spec.substr(7, spec.length - 7); for(uint i = 0; i < cnt; ++i) { if(list[i].module(mod) is null) { @list[i] = list[cnt-1]; --cnt; --i; } } list.length = cnt; } else if(spec.startswith("mod/")) { string mod = spec.substr(4, spec.length - 4); for(uint i = 0; i < cnt; ++i) { if(!list[i].hasModifier(mod)) { @list[i] = list[cnt-1]; --cnt; --i; } } list.length = cnt; } else if(spec.startswith("hull/")) { string tag = spec.substr(5, spec.length - 5); for(uint i = 0; i < cnt; ++i) { if(!list[i].hasHullTag(tag)) { @list[i] = list[cnt-1]; --cnt; --i; } } list.length = cnt; } else { //Single subsystem const SubsystemDef@ def = getSubsystemDef(spec); if(def !is null) { list.length = 1; @list[0] = def; cnt = 1; } else { list.length = 0; cnt = 0; } } } //}}} //{{{ Data Files array techs; dictionary techIdents; int getTechnologyID(const string& ident) { TechnologyType@ type; techIdents.get(ident, @type); if(type !is null) return int(type.id); return -1; } string getTechnologyIdent(int id) { if(id < 0 || id >= int(techs.length)) return "-"; return techs[id].ident; } uint getTechnologyCount() { return techs.length; } const TechnologyType@ getTechnology(uint id) { if(id < techs.length) return techs[id]; else return null; } const TechnologyType@ getTechnology(const string& name) { TechnologyType@ tech; techIdents.get(name, @tech); return tech; } void addTechnology(TechnologyType@ tech) { tech.id = techs.length; techs.insertLast(tech); techIdents.set(tech.ident, @tech); } void saveIdentifiers(SaveFile& file) { for(uint i = 0, cnt = techs.length; i < cnt; ++i) { auto@ type = techs[i]; file.addIdentifier(SI_Technology, type.id, type.ident); } } array grids; dictionary gridIdents; const TechnologyGridSpec@ getTechnologyGridSpec(uint id) { if(id < grids.length) return grids[id]; else return null; } const TechnologyGridSpec@ getTechnologyGridSpec(const string& name) { TechnologyGridSpec@ tech; gridIdents.get(name, @tech); return tech; } void addTechnologyGridSpec(TechnologyGridSpec@ tech) { tech.id = grids.length; grids.insertLast(tech); gridIdents.set(tech.ident, @tech); } void parseLine(string& line, TechnologyType@ type, ReadFile@ file) { //Hook line auto@ hook = cast(parseHook(line, "research_effects::", instantiate=false, file=file)); if(hook !is null) type.hooks.insertLast(hook); } void loadTech(TechnologyType@ type, ReadFile@ file, const string& key, const string& value) { if(key.equals_nocase("Name")) { type.name = localize(value); } else if(key.equals_nocase("Description")) { type.description = localize(value); } else if(key.equals_nocase("Blurb")) { type.blurb = localize(value); } else if(key.equals_nocase("Icon")) { type.icon = getSprite(value); } else if(key.equals_nocase("Symbol")) { type.symbol = getSprite(value); } else if(key.equals_nocase("Color")) { type.color = toColor(value); } else if(key.equals_nocase("Point Cost")) { type.pointCost = toDouble(value); } else if(key.equals_nocase("Time Cost")) { type.timeCost = toDouble(value); } else if(key.equals_nocase("Default Unlock")) { type.defaultUnlock = toBool(value); } else if(key.equals_nocase("Secondary Term")) { type.secondaryTerm = localize(value); } else if(key.equals_nocase("Secret")) { type.secret = toBool(value); } else if(key.equals_nocase("Category")) { type.category = value; } else if(key.equals_nocase("DLC")) { type.dlc = value; } else if(key.equals_nocase("DLC Replace")) { type.dlcReplace = value; } else if(key.equals_nocase("Secret Frequency")) { type.secretFrequency = toDouble(value); } else if(key.equals_nocase("Class")) { type.cls = getResearchClass(value); } else { parseLine(file.line, type, file); } } uint getResearchClass(const string& value) { if(value.equals_nocase("Boost")) { return Tech_Boost; } else if(value.equals_nocase("Upgrade")) { return Tech_Upgrade; } else if(value.equals_nocase("Unlock")) { return Tech_Unlock; } else if(value.equals_nocase("Keystone")) { return Tech_Keystone; } else if(value.equals_nocase("Special")) { return Tech_Special; } else if(value.equals_nocase("Secret")) { return Tech_Secret; } else if(value.equals_nocase("BigUpgrade")) { return Tech_BigUpgrade; } return Tech_Boost; } void loadGrid(TechnologyGridSpec@ grid, ReadFile@ file, const string& key, const string& value) { array@ pos = value.split(","); if(pos.length != 2) { file.error("Invalid node specification."); return; } vec2i gridPos = vec2i(toInt(pos[0]), toInt(pos[1])); int index = grid.def_positions.find(gridPos); if(index != -1) { grid.def_nodes.removeAt(index); grid.def_positions.removeAt(index); } grid.def_nodes.insertLast(key); grid.def_positions.insertLast(gridPos); } void loadResearch(const string& filename) { ReadFile file(filename, true); string key, value; TechnologyType@ tech; TechnologyGridSpec@ grid; uint index = 0; while(file++) { if(file.fullLine) { if(tech !is null) parseLine(file.line, tech, file); } else if(file.key.equals_nocase("Technology")) { @tech = TechnologyType(); @grid = null; tech.ident = file.value; addTechnology(tech); } else if(file.key.equals_nocase("Grid")) { @tech = null; @grid = null; if(!gridIdents.get(file.value, @grid)) { @grid = TechnologyGridSpec(); grid.ident = file.value; addTechnologyGridSpec(grid); } } else if(file.key.equals_nocase("DLC") && tech is null) { if(!hasDLC(file.value)) @grid = null; } else if(file.key.equals_nocase("Vanilla Only")) { if(toBool(value)) { if(isModdedGame && !path_inside("data", resolve("data/research/base_grid.txt"))) @grid = null; } } else { if(grid !is null) loadGrid(grid, file, file.key, file.value); else if(tech !is null) loadTech(tech, file, file.key, file.value); } } } void preInit() { FileList list("data/research", "*.txt", true); for(uint i = 0, cnt = list.length; i < cnt; ++i) loadResearch(list.path[i]); } void init() { for(uint i = 0, cnt = techs.length; i < cnt; ++i) { auto@ type = techs[i]; for(uint n = 0, ncnt = type.hooks.length; n < ncnt; ++n) { if(!cast(type.hooks[n]).instantiate()) error("Could not instantiate hook "+addrstr(type.hooks[n])+" on tech "+type.ident); } } for(uint i = 0, cnt = grids.length; i < cnt; ++i) grids[i].resolve(); } //}}}