starlit  Artifact [668ccf7fa8]

Artifact 668ccf7fa8d168da81b5ab186fc78685aad4c223637af01532bb6c536d97456a:


local lib = starlit.mod.lib

local E = {}
starlit.mod.electronics = E

--------------------------------
-- software context interface --
-------------------------------- ---------------------------------------
-- any time a program is run() or bgProc()eeded, a context argument MUST
-- be passed to the entry point. this argument must have at least the
-- following fields and functions:

-- string   context ('suit', 'device', etc)
--          names the context in which the program is being run
-- swCap    program
--          a structure as returned from usableSoftware(), providing the
--          program with access to and information about its substrate
-- function verify() --> bool
--          return true if the chip the program was loaded from is still
--          accessible
-- function drawCurrent(power, time, whatFor, [min]) --> J
--          attempt to draw current from the power source of whatever core
--          the program is running on
-- function pullConf() --> ()
--          updates the conf structure provided to the program as part of
--          its initial context
-- function saveConf([conf]) --> ()
--          saves any changes made to the conf structure
-- function availableChips() --> inventory list
--          returns the list of chips that are available in the execution
--          context. needed to retrieve system files

-- currently, this interface is implemented by
-- - starlit/interfaces.lua
-- - starlit/suit.lua

---------------------
-- item registries --
---------------------

-- a dynamo is any item that produces power and can be slotted into a power
-- source slot. this includes batteries, but also things like radiothermal
-- dynamos.
starlit.item.dynamo = lib.registry.mk 'starlit_electronics:dynamo'

-- batteries hold a charge of power (measured in kJ). how much they can hold
-- (and how much power they can discharge?) depends on their quality
starlit.item.battery = lib.registry.mk 'starlit_electronics:battery'

-- a battery has the properties:
--  class
--	 |- capacity (J ): amount of energy the battery can hold
--	 |- dischargeRate  (W ): rate at which battery can supply power/be charged
--	 |- decay   (J/J): rate at which the battery capacity degrades while
--	                   discharging. decay=0 batteries require no maintenance;
--	                   decay=1 batteries are effectively disposable
--	 |- leak    (fac): charging inefficiency. depends on the energy storage
--	                   technology. when N J are drawn from a power source,
--	                   only (N*leak) J actually make it into the battery.
--	                   leak=0 is a supercapacitor, leak=1 is /dev/null
--	 |- size      (m): each suit has a limit to how big of a battery it can take
--	instance
--	 |- degrade (mJ): how much the battery has degraded. instance max charge is
--	 |                determined by $capacity - @degrade
--	 |- %wear ÷ 2¹⁶ : used as a factor to determine battery charge

-- chips are standardized data storage hardware that can contain a certain amount
-- of software. in addition to their flash storage, they also provide a given amount
-- of working memory and processor power. processor power speeds up operations like
-- crafting, while programs require a certain amount of memory.
-- chips have a variable number of program slots and a single bootloader slot
--
starlit.item.chip = lib.registry.mk 'starlit_electronics:chip'

-- software is of one of the following types:
--   schematic: program for your matter compiler that enables crafting a given item.
--       output (convertible to ItemStack): the result
--   driver: inserted into a Core to control attached hardware
--     suitPower: provides suit functionality like nanoshredding or healing
--                passive powers are iterated on suit application/configuration and upon fst-tick
--   cost: what the software needs to run. some fields are fab-specific
--		   energy: for fab, total energy cost of process in joules
--               for suitPassive, added suit power consumption in watts
starlit.item.sw = lib.registry.mk 'starlit_electronics:sw'
-- chip    = lib.color(0, 0, .3);

E.schematicGroups = lib.registry.mk 'starlit_electronics:schematicGroups'
E.schematicGroupMembers = {}
E.schematicGroups.foreach('starlit_electronics:ensure-memlist', {}, function(id,g)
	E.schematicGroupMembers[id] = {}
end)
function E.schematicGroupLink(group, item)
	table.insert(E.schematicGroupMembers[group], item)
end

E.schematicGroups.link('starlit_electronics:chip', {
	title = 'Chip', icon = 'starlit-item-chip.png';
	description = 'Standardized data storage and compute modules';
})

E.schematicGroups.link('starlit_electronics:battery', {
	title = 'Battery', icon = 'starlit-item-battery.png';
	description = 'Portable power storage cells are essential to all aspects of survival';
})

E.schematicGroups.link('starlit_electronics:decayCell', {
	title = 'Decay Cell', icon = 'starlit-item-decaycell.png';
	description = "Radioisotope generators can pack much more power into a smaller amount of space than conventional batteries, but they can't be recharged, dump power and heat whether they're in use or not, and their power yield drops towards zero over their usable lifetime.";
})


-------------------------
-- batteries & dynamos --
-------------------------

E.battery = {}
local function accessor(ty, fn)
	return function(stack, ...)
		local function fail()
			error(string.format('object %q is not a %s', stack:get_name(), ty))
		end

		if not stack or stack:is_empty() then fail() end

		if minetest.get_item_group(stack:get_name(), ty) == 0 then fail() end

		return fn(stack,
		          stack:get_definition()._starlit[ty],
		          stack:get_meta(), ...)
	end
end

-- return a wear level that won't destroy the item
-- local function safeWear(fac) return math.min(math.max(fac,0),1) * 0xFFFE end
-- local function safeWearToFac(w) return w/0xFFFE end

E.battery.update = accessor('battery', function(stack, batClass, meta)
	-- local cap = E.battery.capacity(stack)
	local charge = meta:get_float 'starlit_electronics:battery_charge'
	meta:set_string('count_meta', string.format('%s%%', math.floor(charge * 100)))
	meta:set_int('count_alignment', 14)
end)

-- E.battery.capacity(bat) --> charge (J)
E.battery.capacity = accessor('battery', function(stack, batClass, meta)
	local dmg = meta:get_int 'starlit_electronics:battery_degrade' -- µJ/μW
	local dmg_J = dmg / 1000
	return (batClass.capacity - dmg_J)
end)

-- E.battery.charge(bat) --> charge (J)
E.battery.charge = accessor('battery', function(stack, batClass, meta)
	local fac = meta:get_float 'starlit_electronics:battery_charge'
	-- local fac = 1 - safeWearToFac(stack:get_wear())
	return E.battery.capacity(stack) * fac
end)

-- E.battery.dischargeRate(bat) --> dischargeRate (W)
E.battery.dischargeRate = accessor('battery', function(stack, batClass, meta)
	local dmg = meta:get_int 'starlit_electronics:battery_degrade' -- µJ/μW
	local dmg_W = dmg / 1000
	return batClass.dischargeRate - dmg_W
end);


-- E.battery.drawCurrent(bat, power, time, test) --> supply (J), wasteHeat (J)
--       bat   = battery stack
--     power J = joules of energy user wishes to consume
--      time s = the amount of time available for this transaction
--    supply J = how much power was actually provided in $time seconds
-- wasteHeat J = how heat is generated in the process
--      test   = if true, the battery is not actually modified
E.battery.drawCurrent = accessor('battery', function(s, bc, m, power, time, test)
	local ch = E.battery.charge(s)
	local maxPower = math.min(E.battery.dischargeRate(s)*time, power, ch)
	ch = ch - maxPower

	if not test then
		local degrade = m:get_int 'starlit_electronics:battery_degrade' or 0
		degrade = degrade + maxPower * bc.decay
		-- for each joule of power drawn, capacity degrades by `decay` J
		-- this should ordinarily be on the order of mJ or smaller
		m:set_int('starlit_electronics:battery_degrade', degrade)
		-- s:set_wear(safeWear(1 - (ch / E.battery.capacity(s))))
		m:set_float('starlit_electronics:battery_charge', ch / E.battery.capacity(s))
		E.battery.update(s)
	end

	return maxPower, 0 -- FIXME specify waste heat
end)

-- E.battery.recharge(bat, power, time) --> draw (J)
--     bat   = battery stack
--   power J = joules of energy user wishes to charge the battery with
--    time s = the amount of time available for this transaction
--    draw J = how much power was actually drawn in $time seconds
E.battery.recharge = accessor('battery', function(s, bc, m, power, time)
	local ch = E.battery.charge(s)
	local cap = E.battery.capacity(s)
	local maxPower = math.min(E.battery.dischargeRate(s)*time, power)
	local total = math.min(ch + maxPower, cap)
	-- s:set_wear(safeWear(1 - (total/cap)))
	m:set_float('starlit_electronics:battery_charge', total/cap)
	E.battery.update(s)
	return maxPower, 0 -- FIXME
end)

E.battery.setCharge = accessor('battery', function(s, bc, m, newPower)
	local cap = E.battery.capacity(s)
	local power = math.min(cap, newPower)
	-- s:set_wear(safeWear(1 - (power/cap)))
	m:set_float('starlit_electronics:battery_charge', power/cap)
	E.battery.update(s)
end)
E.battery.setChargeF = accessor('battery', function(s, bc, m, newPowerF)
	local power = math.min(1.0, newPowerF)
	m:set_float('starlit_electronics:battery_charge', power)
	E.battery.update(s)
end)

E.dynamo = { kind = {} }

E.dynamo.drawCurrent = accessor('dynamo', function(s,c,m, power, time, test)
	return c.vtable.drawCurrent(s, power, time, test)
end)
E.dynamo.totalPower    = accessor('dynamo', function(s,c,m) return c.vtable.totalPower(s) end)
E.dynamo.dischargeRate = accessor('dynamo', function(s,c,m) return c.vtable.dischargeRate   (s) end)
E.dynamo.initialPower  = accessor('dynamo', function(s,c,m) return c.vtable.initialPower(s) end)
E.dynamo.wasteHeat     = accessor('dynamo', function(s,c,m) return c.vtable.wasteHeat(s) end)
-- baseline waste heat, produced whether or not power is being drawn. for batteries this is 0, but for
-- radiothermal generators it may be high

E.dynamo.kind.battery = {
	drawCurrent = E.battery.drawCurrent;
	totalPower = E.battery.charge;
	initialPower = E.battery.capacity;
	dischargeRate = E.battery.dischargeRate;
	wasteHeat = function() return 0 end;
};

starlit.item.battery.foreach('starlit_electronics:battery-gen', {}, function(id, def)
	minetest.register_tool(id, {
		short_description = def.name;
		groups = { battery = 1; dynamo = 1; electronic = 1; };
		inventory_image = def.img or 'starlit-item-battery.png';
		description = starlit.ui.tooltip {
			title = def.name;
			desc = def.desc;
			color = lib.color(0,.2,1);
			props = {
				{ title = 'Optimal Capacity', affinity = 'info';
					desc = lib.math.siUI('J', def.capacity) };
				{ title = 'Discharge Rate', affinity = 'info';
					desc = lib.math.siUI('W', def.dischargeRate) };
				{ title = 'Charge Efficiency', affinity = 'info';
					desc = string.format('%s%%', (1-def.leak) * 100) };
				{ title = 'Size', affinity = 'info';
					desc = lib.math.siUI('m', def.fab.size.print) };
			};
		};
		_starlit = {
			event = {
				create = function(st, how)
					--[[if not how.gift then -- cheap hack to make starting batteries fully charged
						E.battery.setCharge(st, 0)
					end]]
					E.battery.update(st)
				end;
			};
			fab = def.fab;
			reverseEngineer = def.reverseEngineer;
			dynamo = {
				vtable = E.dynamo.kind.battery;
			};
			battery = def;
		};
	})
end)


-- to use the power functions, consider the following situation. you have
-- a high-tier battery charger that can draw 100kW. (for simplicity, assume
-- it supports only one battery). if you install a low-tier battery, and
-- the charging callback is called every five seconds, you might use
-- a `recharge` call that looks like
--
--   starlit.mod.electronics.battery.recharge(bat, 5 * 100*1e4, 5)
--
-- this would offer the battery 500kJ over five seconds. the battery will
-- determine how much power it can actually make use of in 5 five seconds,
-- and then return that amount.
--
-- always remember to save the battery back to its inventory slot after
-- modifying its ItemStack with one of these functions!


-- battery types
-- supercapacitor: low capacity, no degrade, high dischargeRate, no leak
-- chemical: high capacity, high degrade, mid dischargeRate, low leak

-- battery tiers
-- makeshift: cheap, weak, low quality
-- imperial ("da red wunz go fasta"): powerful, low quality
-- commune ("snooty sophisticates"): limited power, high quality, expensive
-- usukwinya ("value engineering"): high power, mid quality, affordable
-- eluthrai ("uncompromising"): high power, high quality, wildly expensive
-- firstborn ("god-tier"): exceptional

local batteryTiers = {
	makeshift = {
		name = 'Makeshift'; capacity = .5, decay = 3, leak = 2, dischargeRate = 1,
-- 		fab = starlit.type.fab {
-- 			element = {copper=10};
-- 			cost = {power = 0.3};
-- 			time = {print = .25};
-- 		};
		fab = starlit.world.tier.fabsum('makeshift', 'battery');
		desc = "Every attosecond this electrical abomination doesn't explode in your face is but the unearned grace of the Wild Gods.";
		complexity = 1;
		sw = {rarity = 1};
	};
	imperial  = {
		name = 'Imperial'; capacity = 2, decay = 2, leak = 2, dischargeRate = 2; 
-- 		fab = starlit.type.fab {
-- 			element = {iron = 20};
-- 			size = { print = 0.1 };
-- 			cost = {power = 2};
-- 			time = {print = .5};
-- 		};
		fab = starlit.world.tier.fabsum('imperial', 'battery');
		desc = "The Empire's native technology is a lumbering titan: bulky, inefficient, unreliable, ugly, and awesomely powerful. Their batteries are no exception, with raw capacity and throughput that exceed even Usukinwya designs.";
		drm = 1;
		complexity = 2;
		sw = {rarity = 2};
	};
	commune   = {
		name = 'Commune'; capacity = 1, decay = .5, leak = .2, dischargeRate = 1; 
		fab = starlit.world.tier.fabsum('commune', 'battery');
-- 		fab = starlit.type.fab {
-- 			element = {vanadium=10};
-- 			metal = {aluminum=10};
-- 			size = { print = 0.05 };
-- 			cost = {power = 1};
-- 		};
		desc = "The Commune's proprietary battery designs prioritize reliability, compactness, and maintenance concerns above raw throughput, with an elegance of engineering and design that would make a Su'ikuri cry.";
		complexity = 5;
		sw = {rarity = 3};
	};
	usukwinya = {
		name = 'Usukwinya'; capacity = 2, decay = 1, leak = 1, dischargeRate = 1.5,
		fab = starlit.world.tier.fabsum('usukwinya', 'battery');
-- 		fab = starlit.type.fab {
-- 			element = {argon=10, vanadium=10};
-- 			size = { print = 0.07 };
-- 			cost = {power = .8};
-- 		};
		desc = "A race of consummate value engineers, the Usukwinya have spent thousands of years refining their tech to be as cheap to build as possible, without compromising much on quality. The Tradebirds drive an infamously hard bargain, but their batteries are more than worth their meagre cost.";
		drm = 2;
		sw = {rarity = 10};
		complexity = 15;
	};
	eluthrai  = {
		name = 'Eluthrai'; capacity = 3, decay = .4, leak = .1, dischargeRate = 1.5,
		fab = starlit.world.tier.fabsum('eluthrai', 'battery');
-- 		fab = starlit.type.fab {
-- 			element = {beryllium=20, platinum=20, technetium = 1};
-- 			metal = {cinderstone = 10};
-- 			size = { print = 0.03 };
-- 			cost = {power = 10};
-- 			time = {print = 2};
-- 		};
		desc = "The uncompromising Eluthrai are never satisfied until every quantifiable characteristic of their tech is maximally optimised down to the picoscale. Their batteries are some of the best in the Reach, and unquestionably the most expensive -- especially for those lesser races trying to copy the designs without the benefit of the sublime autofabricator ecosystem of the Eluthrai themselves.";
		complexity = 200;
		sw = {rarity = 0}; -- you think you're gonna buy eluthran schematics on SuperDiscountNanoWare.space??
	};
	firstborn = {
		name = 'Firstborn'; capacity = 5, decay = 0.1, leak = 0, dischargeRate = 3;
		fab = starlit.world.tier.fabsum('firstborn', 'battery');
-- 		fab = starlit.type.fab {
-- 			element = {neodymium=20, xenon=150, technetium=5};
-- 			metal = {sunsteel = 10};
-- 			crystal = {astrite = 1};
-- 			size = { print = 0.05 };
-- 			cost = {power = 50};
-- 			time = {print = 4};
-- 		};
		desc = "Firstborn engineering seamlessly merges psionic effects with a mastery of the physical universe unattained by even the greatest of the living Starsouls. Their batteries reach levels of performance that strongly imply Quantum Gravity Theory -- and several major holy books -- need to be rewritten. From the ground up.";
		complexity = 1000;
		sw = {rarity = 0}; -- lol no
	};
}

local batterySizes = {
	small = {name = 'Small', capacity = .5, dischargeRate =  .5, complexity = 1, matMult = .5,
		fab = starlit.type.fab {size={print=0.1},cost={power=.5},time={print=25}}};
	mid   = {                capacity =  1, dischargeRate =   1, complexity = 1, matMult = 1,
		fab = starlit.type.fab {size={print=0.3},cost={power=1},time={print=40}}};
	large = {name = 'Large', capacity =  2, dischargeRate = 1.5, complexity = 1, matMult = 1.5,
		fab = starlit.type.fab {size={print=0.5},cost={power=2},time={print=60}}};
	huge  = {name = 'Huge',  capacity =  3, dischargeRate =   2, complexity = 1, matMult = 2,
		fab = starlit.type.fab {size={print=0.8},cost={power=8},time={print=120}}};
}

local batteryTypes = {
	supercapacitor = {
		name = 'Supercapacitor';
		desc = 'Room-temperature superconductors make for very reliable, high-discharge rate, but low-capacity batteries.';
		fab = starlit.type.fab {
			metal = { enodium = 5 };
			size = {print=0.8};
			cost = {power = 1e3};
		};
		sw = {
			cost = {
				cycles = 48e9; -- 48 bil cycles
				ram = 4e9; -- 10GB
			};
			pgmSize = 2e9; -- 2GB
			rarity = 5;
		};
		capacity = 50e3, dischargeRate = 1000;
		leak = 0, decay = 1e-6;

		complexity = 3;
	};
	chemical = {
		name = 'Chemical';
		desc = '';
		fab = starlit.type.fab {
			element = { lithium = 3 };
			size = {print=1.0};
			cost = {power = .5e3};
		};
		sw = {
			cost = {
				cycles = 16e9; -- 16 bil cycles
				ram = 1e9; -- 1GB
			};
			pgmSize = 512e6; -- 512MB
			rarity = 2;
		};
		capacity = 200e3, dischargeRate = 200;
		leak = 0.2, decay = 1e-2;
		complexity = 1;
	};
	carbon = {
		name = 'Carbon';
		desc = 'Carbon nanotubes form the basis of many important metamaterials, chief among them power-polymer.';
		capacity = 1;
		fab = starlit.type.fab {
			element = { carbon = 40 };
			size = {print=0.5};
			cost = {power = 2.5e3};
		};
		sw = {
			cost = {
				cycles = 256e9; -- 256 bil cycles
				ram = 16e9; -- 64GB
			};
			pgmSize = 4e9; -- 4GB
			rarity = 10;
		};
		capacity = 100e3, dischargeRate = 500;
		leak = 0.1, decay = 1e-3;
		complexity = 10;
	};
	hybrid = {
		name = 'Hybrid';
		desc = '';
		capacity = 1;
		fab = starlit.type.fab {
			element = {
				lithium = 10;
				carbon = 20;
			};
			size = {print=1.5};
			cost = {power = 10e3};
		};
		sw = {
			cost = {
				cycles = 512e9; -- 512 bil cycles
				ram = 24e9; -- 96GB
			};
			pgmSize = 7e9; -- 7GB
			rarity = 15;
		};
		capacity = 300e3, dischargeRate = 350;
		leak = 0.3, decay = 1e-5;
		complexity = 30;
	};
}

--[[
local function elemath(dest, src, mult)
	dest = dest or {}
	for k,v in pairs(src) do
		if not dest[k] then dest[k] = 0 end
		dest[k] = dest[k] + v*mult
	end
	return dest
end]]

for bTypeName, bType in pairs(batteryTypes) do
for bTierName, bTier in pairs(batteryTiers) do
for bSizeName, bSize in pairs(batterySizes) do
	-- elemath(elementCost, bType.fab.element or {}, bSize.matMult)
	-- elemath(elementCost, bTier.fab.element or {}, bSize.matMult)
	-- elemath(metalCost, bType.fab.metal or {}, bSize.matMult)
	-- elemath(metalCost, bTier.fab.metal or {}, bSize.matMult)
	local fab = bType.fab + bTier.fab + bSize.fab + starlit.type.fab {
		element = {copper = 10};
	}
	local baseID = string.format('battery_%s_%s_%s',
		bTypeName, bTierName, bSizeName)
	local id = 'starlit_electronics:'..baseID
	local name = string.format('%s %s Battery', bTier.name, bType.name)
	if bSize.name then name = bSize.name .. ' ' .. name end
	local function batStat(s)
		if s == 'size' then
			--return bType.fab[s] * (bTier.fab[s] or 1) * (bSize.fab[s] or 1)
			return fab.size and fab.size.print or 1
		else
			return bType[s] * (bTier[s] or 1) * (bSize[s] or 1)
		end
	end

	local swID = 'starlit_electronics:schematic_'..baseID
	fab.flag = {print=true}

	starlit.item.battery.link(id, {
		name = name;
		desc = table.concat({
			bType.desc or '';
			bTier.desc or '';
			bSize.desc or '';
		}, ' ');

		fab = fab;
		reverseEngineer = {
			complexity = bTier.complexity * bSize.complexity * bType.complexity;
			sw = swID;
		};

		capacity = batStat 'capacity';
		dischargeRate  = batStat 'dischargeRate';
		leak     = batStat 'leak';
		decay    = batStat 'decay';
	})

	local rare
	if bType.sw.rarity == 0 or bTier.sw.rarity == 0 then
		-- rarity is measured such that the player has a 1/r
		-- chance of finding a given item, or if r=0, no chance
		-- whatsoever (the sw must be obtained e.g. by reverse-
		-- engineering alien tech)
		rare = 0
	else
		rare = bType.sw.rarity + bTier.sw.rarity
	end

	starlit.item.sw.link(swID, {
		kind = 'schematic';
		name = name .. ' Schematic';
		input = fab;
		output = id;
		size = bType.sw.pgmSize;
		cost = bType.sw.cost;
		rarity = rare;
	})

	E.schematicGroupLink('starlit_electronics:battery', swID)

end end end


-----------
-- chips --
-----------

E.sw = {}
function E.sw.findSchematicFor(item)
	local id = ItemStack(item):get_name()
	local fm = minetest.registered_items[id]._starlit
	if not (fm and fm.reverseEngineer) then return nil end
	local id = fm.reverseEngineer.sw
	return id, starlit.item.sw.db[id]
end

E.chip = { file = {} }
do local T,G = lib.marshal.t, lib.marshal.g
	-- love too reinvent unions from first principles
	E.chip.data = G.struct {
		label = T.str;
		uuid = T.u64;
		files = G.array(16, G.class(G.struct {
			kind = G.enum {
				'sw'; -- a piece of installed software
				'note'; -- a user-readable text file
				'research'; -- saved RE progress
				'genome'; -- for use with plant biosequencer?
				'blob'; -- opaque binary blob, so 3d-pty mods can use the
                    -- file mechanism to store arbirary data.
			};
			drm = T.u8; -- inhibit copying
			name = T.str;
			body = T.text;
		}, function(file) -- enc
			assert(E.chip.file[file.kind], string.format('invalid file kind "%s"', file.kind))
			local b = E.chip.file[file.kind].enc(file.body)
			return {
				kind = file.kind;
				drm = file.drm;
				name = file.name;
				body = b;
			}
		end, function(file) -- dec
			local f, ns = E.chip.file[file.kind].dec(file.body)
			file.body = f
			return file, ns
		end));
		bootSlot = T.u8; -- indexes into files; 0 = no bootloader
	}
	E.chip.file.sw = G.struct {
		pgmId = T.str;
		conf = G.array(16, G.struct {
			key = T.str, value = T.str;
		});
	}
	E.chip.file.note = G.struct {
		author = T.str;
		entries = G.array(16, G.struct {
			title = T.str;
			body = T.str;
		});
	}
	E.chip.file.research = G.struct {
		itemId = T.str;
		progress = T.clamp;
	}
	E.chip.file.blob = G.struct {
		kind = T.str; -- MT ID that identifies a blob file type belonging to an external mod
		size = T.u8; -- this must be manually reported since we don't know how to evaluate it
		data = T.text;
	}
	function E.chip.fileSize(file)
		-- boy howdy
		if file.kind == 'blob' then
			return file.body.size
		elseif file.kind == 'note' then
			local sz = 0x10 + #file.body.author
			for _, e in pairs(file.body.entries) do
				sz = sz + #e.title + #e.body + 0x10 -- header overhead
			end
			return sz
		elseif file.kind == 'research' then
			local re = assert(minetest.registered_items[file.body.itemId]._starlit.reverseEngineer)
			return starlit.item.sw.db[re.sw].size * file.body.progress
		elseif file.kind == 'sw' then
			return starlit.item.sw.db[file.body.pgmId].size
		elseif file.kind == 'genome' then
			return 0 -- TODO
		end
	end
	local metaKey = 'starlit_electronics:chip'
	function E.chip.read(chip)
		local m = chip:get_meta()
		local blob = m:get_string(metaKey)
		if blob and blob ~= '' then
			return E.chip.data.dec(lib.str.meta_dearmor(blob))
		else -- prepare to format the chip
			return {
				label = '';
				bootSlot = 0;
				uuid = math.floor(math.random(0,2^32));
				files = {};
			}
		end
	end
	function E.chip.write(chip, data)
		local m = chip:get_meta()
		m:set_string(metaKey, lib.str.meta_armor(E.chip.data.enc(data)))
		E.chip.update(chip)
	end
	function E.chip.fileOpen(chip, inode, fn)
		local c = E.chip.read(chip)
		if fn(c.files[inode]) then
			E.chip.write(chip, c)
			return true
		end
		return false
	end
	function E.chip.fileWrite(chip, inode, file)
		local c = E.chip.read(chip)
		c.files[inode] = file
		E.chip.write(chip, c)
	end
	function E.chip.usedSpace(chip, d)
		d = d or E.chip.read(chip)
		local sz = 0
		for _, f in pairs(d.files) do
			sz = sz + E.chip.fileSize(f)
		end
		return sz
	end
	function E.chip.freeSpace(chip, d)
		local used = E.chip.usedSpace(chip,d)
		local max = assert(chip:get_definition()._starlit.chip.flash)
		return max - used
	end
	function E.chip.install(chip, file)
		-- remember to write out the itemstack after using this function!
		local d = E.chip.read(chip)
		if E.chip.freeSpace(chip, d) - E.chip.fileSize(file) >= 0 then
			table.insert(d.files, file)
			E.chip.write(chip, d)
			return true
		else
			return false
		end
	end
end

function E.chip.files(ch)
	local m = ch:get_meta()
	if not m:contains 'starlit_electronics:chip' then
		return nil
	end
	local data = E.chip.read(ch)
	local f = 0
	return function()
		f = f + 1
		return data.files[f], f
	end
end

function E.chip.describe(ch, defOnly)
	local def, data if defOnly then
		def, data = ch, {}
	else
		def = ch:get_definition()
		local m = ch:get_meta()
		if m:contains 'starlit_electronics:chip' then
			data = E.chip.read(ch)
		else
			data = {}
			defOnly = true
		end
		def = assert(def._starlit.chip)
	end
	local props = {
		{title = 'Clock Rate', affinity = 'info';
		 desc  = lib.math.siUI('Hz', def.clockRate)};
		{title = 'RAM', affinity = 'info';
		 desc  = lib.math.siUI('B', def.ram)};
	}
	if not defOnly then
		table.insert(props, {
			title = 'Free Storage', affinity = 'info';
			 desc = lib.math.siUI('B', E.chip.freeSpace(ch, data)) .. ' / '
			     .. lib.math.siUI('B', def.flash);
		})
		local swAffMap = {
			schematic = 'schematic';
			suitPower = 'ability';
			driver = 'driver';
		}
		for i, e in ipairs(data.files) do
			local aff = 'neutral'
			local name = e.name
			local disabled = false
			if e.kind == 'sw' then
				for _,cf in pairs(e.body.conf) do
					if cf.key == 'disable' and cf.value == 'yes' then
						disabled = true
						break
					end
				end
				local sw = starlit.item.sw.db[e.body.pgmId]
				aff = swAffMap[sw.kind] or 'good'
				if name == '' then name = sw.name end
			end
			name = name or '<???>'
			table.insert(props, disabled and {
				title = name;
				affinity = aff;
				desc = '<off>';
			} or {
				--title = name;
				affinity = aff;
				desc = name;
			})
		end
	else
		table.insert(props, {
			title = 'Flash Storage', affinity = 'info';
			 desc = lib.math.siUI('B', def.flash);
		 })
	end
	return starlit.ui.tooltip {
		title = data.label and data.label~='' and string.format('<%s>', data.label) or def.name;
		color = lib.color(.6,.6,.6);
		desc = def.desc;
		props = props;
	};
end

function E.chip.update(chip)
	chip:get_meta():set_string('description', E.chip.describe(chip))
end

starlit.item.chip.foreach('starlit_electronics:chip-gen', {}, function(id, def)
	minetest.register_craftitem(id, {
		short_description = def.name;
		description = E.chip.describe(def, true);
		inventory_image = def.img or 'starlit-item-chip.png';
		groups = {chip = 1};
		_starlit = {
			fab = def.fab;
			chip = def;
		};
	})
end)

-- in case other mods want to define their own tiers
E.chip.tiers = lib.registry.mk 'starlit_electronics:chipTiers'
E.chip.tiers.meld {
	-- GP chips
	tiny    = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e10, size = 1};
	small   = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e9, size = 3};
	med     = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e8, size = 6};
	large   = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e7, size = 8};
	-- specialized chips
	compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e9, size = 4};
	data    = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e6, size = 4};
	lp      = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e11, size = 4};
	carbon  = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e10, size = 2, circ='carbon'};
}

E.chip.tiers.foreach('starlit_electronics:genChips', {}, function(id, t)
	id = t.id or string.format('%s:chip_%s', minetest.get_current_modname(), id)
	local circMat = t.circ or 'silicon';
	starlit.item.chip.link(id, {
		name = t.name;
		clockRate = t.clockRate;
		flash = t.flash;
		ram = t.ram;
		powerEfficiency = t.powerEfficiency; -- cycles per joule
		fab = starlit.type.fab {
			flag = {
				silicompile = true;
			};
			time = {
				silicompile = t.size * 24*60;
			};
			cost = {
				power = 50e3 + t.size * 15e2;
			};
			element = {
				[circMat] = 50 * t.size;
				copper = 30;
				gold = 15;
			};
		};
	})
end)

function E.chip.findBest(test, ...)
	local chip, bestFitness
	for id, c in pairs(starlit.item.chip.db) do
		local fit, fitness = test(c, ...)
		if fit and (bestFitness == nil or fitness > bestFitness) then
			chip, bestFitness = id, fitness
		end
	end
	return chip, starlit.item.chip.db[chip], bestFitness
end

function E.chip.findForStorage(sz)
	return E.chip.findBest(function(c)
		return c.flash >= sz, -math.abs(c.flash - sz)
	end)
end

function E.chip.sumCompute(chips)
	local c = {
		cycles = 0;
		ram = 0;
		flashFree = 0;
		powerEfficiency = 0;
	}
	local n = 0
	for _, e in pairs(chips) do
		n = n + 1
		if not e:is_empty() then
			local ch = e:get_definition()._starlit.chip
			c.cycles = c.cycles + ch.clockRate
			c.ram = c.ram + ch.ram
			c.flashFree = c.flashFree + E.chip.freeSpace(e)
			c.powerEfficiency = c.powerEfficiency + ch.powerEfficiency
		end
	end
	if n > 0 then c.powerEfficiency = c.powerEfficiency / n end
	return c
end

E.chip.fileHandle = lib.class {
	__name = 'starlit_electronics:chip.fileHandle';
	construct = function(chip, inode) -- stack, int --> fd
		return { chip = chip, inode = inode }
	end;
	__index = {
		read = function(self)
			local dat = E.chip.read(self.chip)
			return dat.files[self.inode]
		end;
		write = function(self,data)
			return E.chip.fileWrite(self.chip, self.inode, data)
		end;
		erase = function(self)
			local dat = E.chip.read(self.chip)
			table.remove(dat.files, self.inode)
			E.chip.write(self.chip, dat)
			self.inode = nil
		end;
		open = function(self,fn)
			return E.chip.fileOpen(self.chip, self.inode, fn)
		end;
	};
}

function E.chip.usableSoftware(chips,pgm,pred)
	pred = pred or function() return true end
	local comp = E.chip.sumCompute(chips)
	local r = {}
	local unusable = {}
	local sw if pgm then
		if type(pgm) == 'string' then
			pgm = {starlit.item.sw.db[pgm]}
		end
		sw = pgm
	else
		sw = {}
		for i, e in ipairs(chips) do
			if (not e:is_empty())
			   and minetest.get_item_group(e:get_name(), 'chip') ~= 0
			then
				for fl, inode in E.chip.files(e) do
					if fl.kind == 'sw' then
						local s = starlit.item.sw.db[fl.body.pgmId]
						table.insert(sw, {
							sw = s, chip = e, chipSlot = i;
							file = fl, inode = inode;
							id = fl.body.pgmId;
						})
					end
				end
			end
		end
	end

	for _, s in pairs(sw) do
		if s.sw.cost.ram <= comp.ram and pred(s) then
			table.insert(r, {
				id = s.id;
				sw = s.sw;
				chip = s.chip, chipSlot = s.chipSlot;
				file = s.file;
				fd = E.chip.fileHandle(s.chip, s.inode);
				speed = s.sw.cost.cycles / comp.cycles;
				powerCost = s.sw.cost.cycles / comp.powerEfficiency;
				comp = comp;
			})
		else
			table.insert(unusable, {
				sw = s.sw;
				chip = s.chip;
				ramNeeded = s.sw.cost.ram - comp.ram;
			})
		end
	end
	return r, unusable
end

starlit.include 'sw'