starlit  Check-in [4732f8d454]

Overview
Comment:we have always been at war with east minecraft
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | trunk
Files: files | file ages | folders
SHA3-256: 4732f8d454ad0b335f1bea1bc365f82ef4121188f11df10e4562ed5a077ad111
User & Date: lexi on 2025-01-19 19:18:55
Other Links: manifest | tags
Context
2025-01-19
19:18
we have always been at war with east minecraft Leaf check-in: 4732f8d454 user: lexi tags: trunk
19:05
add to lore, add weather data, etc check-in: caec179da9 user: lexi tags: trunk
Changes

Modified mods/starlit-building/init.lua from [cc61bb3736] to [33f1e7300a].

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
..
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
...
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
...
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
...
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
--[[ {
	part = {
		['starlit_building:pipe'] = 'myMod:stage3';
	};
	tool = {
		['starlit:screwdriver'] = 'myMod:otherThing_stage1';
		['starlit:saw'] = function(node, tool)
			minetest.replace_node(node, {name='myMod:stage1'})
			minetest.drop_item(node, 'starlit_building:pipe')
		end;
		['myMod:laserWrench'] = {
			allow = function(node, tool) ... end;
			handle = function(node, tool) ... end;
		};
	};
} ]]
................................................................................
	if e.fab then
		scmID = string.format('%s_schematic', id)
		rev = {
			sw = scmID;
			complexity = e.complexity or 1;
		}
	end
	minetest.register_craftitem(id, {
		short_description = e.name;
		description = starlit.ui.tooltip {
			title = e.name;
			desc = e.desc;
			props = props;
		};
		stack_max = e.max or (e.mass and math.min(math.max(math.floor(500 / e.mass), 1), 500)) or 10;
		inventory_image = e.img;
		on_place = function(stack, luser, point)
			local node = minetest.get_node(point.under)
			local function tryBuild()
				local p = B.path[node.name]
				if not p then return nil end
				if (not p.part) or (not p.part[id]) then return nil end
				local n = p.part[id]
				local obj
				if type(n) == 'function' then
................................................................................
				else
					obj = ItemStack(n)
					stack:take_item(1)
				end
				local pname = obj:get_name()
				local stg = B.stage.db[pname]
				node.name = pname
				minetest.swap_node(point.under, node)
				-- TODO make a noise
				if stg.onBuild then
					stg.onBuild(point.under, luser, stack)
				end
				return stack
			end

................................................................................
				if not p then return nil end
				if type(p) == 'function' then
					p = p(stack, node, point)
					if p == nil then return nil end
				else
					stack:take_item(1)
				end
				minetest.rotate_and_place(ItemStack(p), luser, point, true)
				-- TODO make a noise
				return stack
			end

			return tryBuild() or tryBegin() or stack
		end;
		_starlit = {
................................................................................
	local grp = e.groups and table.copy(e.groups) or {}
	grp.stage = 1
	local meta = {
		stage = id;
		recover = e.recover;
	}
	for k,v in pairs(e.meta or {}) do meta[k] = v end
	minetest.register_node(id, {
		description = 'Construction';
		drawtype = (e.box  and 'nodebox')
		        or (e.mesh and 'mesh')
		        or 'regular';
		paramtype  = e.paramtype or (e.box or e.mesh or e.light) and 'light' or nil;
		paramtype2 = e.paramtype2 or 'none';
		tiles = e.tex;







|
|







 







|









|







 







|







 







|







 







|







7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
..
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
...
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
...
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
...
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
--[[ {
	part = {
		['starlit_building:pipe'] = 'myMod:stage3';
	};
	tool = {
		['starlit:screwdriver'] = 'myMod:otherThing_stage1';
		['starlit:saw'] = function(node, tool)
			core.replace_node(node, {name='myMod:stage1'})
			core.drop_item(node, 'starlit_building:pipe')
		end;
		['myMod:laserWrench'] = {
			allow = function(node, tool) ... end;
			handle = function(node, tool) ... end;
		};
	};
} ]]
................................................................................
	if e.fab then
		scmID = string.format('%s_schematic', id)
		rev = {
			sw = scmID;
			complexity = e.complexity or 1;
		}
	end
	core.register_craftitem(id, {
		short_description = e.name;
		description = starlit.ui.tooltip {
			title = e.name;
			desc = e.desc;
			props = props;
		};
		stack_max = e.max or (e.mass and math.min(math.max(math.floor(500 / e.mass), 1), 500)) or 10;
		inventory_image = e.img;
		on_place = function(stack, luser, point)
			local node = core.get_node(point.under)
			local function tryBuild()
				local p = B.path[node.name]
				if not p then return nil end
				if (not p.part) or (not p.part[id]) then return nil end
				local n = p.part[id]
				local obj
				if type(n) == 'function' then
................................................................................
				else
					obj = ItemStack(n)
					stack:take_item(1)
				end
				local pname = obj:get_name()
				local stg = B.stage.db[pname]
				node.name = pname
				core.swap_node(point.under, node)
				-- TODO make a noise
				if stg.onBuild then
					stg.onBuild(point.under, luser, stack)
				end
				return stack
			end

................................................................................
				if not p then return nil end
				if type(p) == 'function' then
					p = p(stack, node, point)
					if p == nil then return nil end
				else
					stack:take_item(1)
				end
				core.rotate_and_place(ItemStack(p), luser, point, true)
				-- TODO make a noise
				return stack
			end

			return tryBuild() or tryBegin() or stack
		end;
		_starlit = {
................................................................................
	local grp = e.groups and table.copy(e.groups) or {}
	grp.stage = 1
	local meta = {
		stage = id;
		recover = e.recover;
	}
	for k,v in pairs(e.meta or {}) do meta[k] = v end
	core.register_node(id, {
		description = 'Construction';
		drawtype = (e.box  and 'nodebox')
		        or (e.mesh and 'mesh')
		        or 'regular';
		paramtype  = e.paramtype or (e.box or e.mesh or e.light) and 'light' or nil;
		paramtype2 = e.paramtype2 or 'none';
		tiles = e.tex;

Modified mods/starlit-eco/init.lua from [f0d21c2132] to [c892dab0f5].

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
		{-0.100, 'starlit:clear'};
		{ 0.300, 'starlit:cloudy'};
		{ 0.600, 'starlit:precip'};
		{ 0.850, 'starlit:storm'};
		{ 0.900, 'starlit:tstorm'};
	};
})
minetest.register_craftitem('starlit_eco:fiber', {
	description = "Plant Fiber";
	groups = {fiber = 1};
	inventory_image = lib.image('starlit-eco-plant-fiber.png'):shift(lib.color(0,1,0)):render();
	_starlit = {
		recover_vary = function(rng, ctx)
			return starlit.type.fab {
				element = { carbon   = rng:int(0,1) };







|







163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
		{-0.100, 'starlit:clear'};
		{ 0.300, 'starlit:cloudy'};
		{ 0.600, 'starlit:precip'};
		{ 0.850, 'starlit:storm'};
		{ 0.900, 'starlit:tstorm'};
	};
})
core.register_craftitem('starlit_eco:fiber', {
	description = "Plant Fiber";
	groups = {fiber = 1};
	inventory_image = lib.image('starlit-eco-plant-fiber.png'):shift(lib.color(0,1,0)):render();
	_starlit = {
		recover_vary = function(rng, ctx)
			return starlit.type.fab {
				element = { carbon   = rng:int(0,1) };

Modified mods/starlit-eco/trees.lua from [d43bf4970e] to [6da5b86eb1].

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
...
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
...
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
	base.groups.wood = 1
	base.groups.log = 1
	base.groups.falling_node = 1

	local live = table.copy(base)
	live.drop = id
	live.groups.alive = 1
	minetest.register_node(id, base)
	minetest.register_node(id..'_live', live)
end

regLog('starlit_eco:lambent_pine_log', {
	description = 'Lambent Pine Log';
	drawtype = 'normal';
	tiles = {
		'starlit-eco-tree-lambent-pine-trunk-top.png';
................................................................................

starlit.item.seed.link('starlit_eco:lambent_pine_seed', {
	name = 'Lambent Pine Seed';
	tex = lib.image('starlit-eco-plant-seeds.png'):shift{hue=150, sat=-.5, lum=.8}:render();
	grow = {kind = 'tree', id = 'starlit_eco:lambent_pine'};
})

minetest.register_node('starlit_eco:lambent_pine_bulb', {
	description = 'Lambent Pine Bulb';
	drawtype = 'nodebox';
	connects_to = {'starlit_eco:lambent_pine_needles'};
	node_box = {
		type = 'connected';
		connect_top = {
			{-.1,  .5, -.1,
................................................................................
	groups = {plant=1, attached_node = 4};
	tiles = {
		'starlit-eco-tree-lambent-pine-bulb.png';
	};
	_starlit = woodProps{};
})

minetest.register_node('starlit_eco:lambent_pine_needles', {
	description = 'Lambent Pine Needles';
	groups = {plant = 1;};
	drop = '';
	tiles = {
		'starlit-eco-tree-lambent-pine-needles.png';
		'starlit-eco-tree-lambent-pine-needles.png';
	};
................................................................................
	tiles = {
		'starlit-eco-tree-starblossom-trunk-top.png';
		'starlit-eco-tree-starblossom-trunk.png';
	};
	_starlit = woodProps{};
})

minetest.register_node('starlit_eco:starblossom_leaves', {
	description = 'Starblossom Leaves';
	groups = {plant = 1;};
	drop = '';
	tiles = {
		'starlit-eco-tree-starblossom-leaves.png';
		'starlit-eco-tree-starblossom-leaves.png';
	};
	_starlit = leafProps{};
});
minetest.register_node('starlit_eco:starblossom_leaves_shine', {
	description = 'Shining Starblossom Leaves';
	groups = {plant = 1;};
	drop = '';
	paramtype = 'light';
	light_source = 4;
	tiles = {
		'starlit-eco-tree-starblossom-leaves.png';
................................................................................
				fill_ratio = 0.001;
				y_min = 0, y_max = 512;
			};
		};
	};
}

minetest.register_abm {
	label = "lambent pine fruiting";
	nodenames = {'starlit_eco:lambent_pine_needles'};
	neighbors = {'starlit_eco:lambent_pine_log_live'};
	chance = 40;
	interval = 80;
	catch_up = true;
	action = function(pos, node)
		local po = pos:offset(0,-1,0)
		if minetest.get_node(po).name == "air" then
			minetest.add_node(po, {name='starlit_eco:lambent_pine_bulb'})
		end
	end;
}







|
|







 







|







 







|







 







|









|







 







|








|
|



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
...
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
...
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
	base.groups.wood = 1
	base.groups.log = 1
	base.groups.falling_node = 1

	local live = table.copy(base)
	live.drop = id
	live.groups.alive = 1
	core.register_node(id, base)
	core.register_node(id..'_live', live)
end

regLog('starlit_eco:lambent_pine_log', {
	description = 'Lambent Pine Log';
	drawtype = 'normal';
	tiles = {
		'starlit-eco-tree-lambent-pine-trunk-top.png';
................................................................................

starlit.item.seed.link('starlit_eco:lambent_pine_seed', {
	name = 'Lambent Pine Seed';
	tex = lib.image('starlit-eco-plant-seeds.png'):shift{hue=150, sat=-.5, lum=.8}:render();
	grow = {kind = 'tree', id = 'starlit_eco:lambent_pine'};
})

core.register_node('starlit_eco:lambent_pine_bulb', {
	description = 'Lambent Pine Bulb';
	drawtype = 'nodebox';
	connects_to = {'starlit_eco:lambent_pine_needles'};
	node_box = {
		type = 'connected';
		connect_top = {
			{-.1,  .5, -.1,
................................................................................
	groups = {plant=1, attached_node = 4};
	tiles = {
		'starlit-eco-tree-lambent-pine-bulb.png';
	};
	_starlit = woodProps{};
})

core.register_node('starlit_eco:lambent_pine_needles', {
	description = 'Lambent Pine Needles';
	groups = {plant = 1;};
	drop = '';
	tiles = {
		'starlit-eco-tree-lambent-pine-needles.png';
		'starlit-eco-tree-lambent-pine-needles.png';
	};
................................................................................
	tiles = {
		'starlit-eco-tree-starblossom-trunk-top.png';
		'starlit-eco-tree-starblossom-trunk.png';
	};
	_starlit = woodProps{};
})

core.register_node('starlit_eco:starblossom_leaves', {
	description = 'Starblossom Leaves';
	groups = {plant = 1;};
	drop = '';
	tiles = {
		'starlit-eco-tree-starblossom-leaves.png';
		'starlit-eco-tree-starblossom-leaves.png';
	};
	_starlit = leafProps{};
});
core.register_node('starlit_eco:starblossom_leaves_shine', {
	description = 'Shining Starblossom Leaves';
	groups = {plant = 1;};
	drop = '';
	paramtype = 'light';
	light_source = 4;
	tiles = {
		'starlit-eco-tree-starblossom-leaves.png';
................................................................................
				fill_ratio = 0.001;
				y_min = 0, y_max = 512;
			};
		};
	};
}

core.register_abm {
	label = "lambent pine fruiting";
	nodenames = {'starlit_eco:lambent_pine_needles'};
	neighbors = {'starlit_eco:lambent_pine_log_live'};
	chance = 40;
	interval = 80;
	catch_up = true;
	action = function(pos, node)
		local po = pos:offset(0,-1,0)
		if core.get_node(po).name == "air" then
			core.add_node(po, {name='starlit_eco:lambent_pine_bulb'})
		end
	end;
}

Modified mods/starlit-electronics/init.lua from [668ccf7fa8] to [e24965a858].

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
...
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
...
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
...
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
...
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
...
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
...
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
	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

................................................................................
	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);
................................................................................
-----------
-- 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
................................................................................
		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
................................................................................
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;
................................................................................
	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
................................................................................
			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;







|







 







|







 







|







 







|







 







|







 







|







 







|







118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
...
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
...
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
...
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
...
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
...
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
...
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
	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 core.get_item_group(stack:get_name(), ty) == 0 then fail() end

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

................................................................................
	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)
	core.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);
................................................................................
-----------
-- chips --
-----------

E.sw = {}
function E.sw.findSchematicFor(item)
	local id = ItemStack(item):get_name()
	local fm = core.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
................................................................................
		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(core.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
................................................................................
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)
	core.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;
................................................................................
	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', core.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
................................................................................
			pgm = {starlit.item.sw.db[pgm]}
		end
		sw = pgm
	else
		sw = {}
		for i, e in ipairs(chips) do
			if (not e:is_empty())
			   and core.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;

Modified mods/starlit-electronics/sw.lua from [1c1e9dd876] to [922972e287].

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
..
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
..
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
...
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
		return items, charges
	end

	return function(user, ctx)
		local function cleanup()
			user.action.prog.shred = nil
			if user.action.sfx.shred then
				minetest.sound_fade(user.action.sfx.shred, 1, 0)
				user.action.sfx.shred = nil
			end
			if user.action.fx.shred then
				user.action.fx.shred.abort()
			end
		end

................................................................................
			cleanup()
			return false
		end
		local shredTime = 1.0
		local soundPitch = 1.0 -- TODO
		local pdraw = prop.powerDraw or 0

		if minetest.is_protected(what, user.entity:get_player_name()) then return end
		local node = minetest.get_node(what)
		local nd = minetest.registered_nodes[node.name]
		local elt, fab, vary
		if nd._starlit then
			fab = nd._starlit.recover or nd._starlit.fab
			vary = nd._starlit.recover_vary
		end
		if fab then
			if fab.flag then
................................................................................
			if p < pdx then
				cleanup()
				return false
			elseif not user.action.prog.shred then
				cleanup() -- kill danglers
				-- begin
				user.action.prog.shred = 0
				user.action.sfx.shred = minetest.sound_play('starlit-nano-shred', {
					object = user.entity;
					max_hear_distance = prop.range*2;
					loop = true;
					pitch = soundPitch;
				})
				user.action.fx.shred = starlit.fx.nano.shred(user, what, prop, shredTime, node)
			else
				user.action.prog.shred = user.action.prog.shred + ctx.how.delta or 0
			end
			--print('shred progress: ', user.action.prog.shred)
			if user.action.prog.shred >= shredTime then
				minetest.remove_node(what)
				minetest.check_for_falling(what)
				--print('shred complete')
				user:suitSound 'starlit-success'
				if fab then
					local vf = fab
					if vary then
						local rng = (starlit.world.seedbank+0xa891f62)[minetest.hash_node_position(what)]
						vf = vf + vary(rng, {})
					end
					local items, charges = fabToItemsAndCharges(vf)
					for i, it in ipairs(items) do user:give(it) end
					-- TODO give gasses, liquids
				end
				cleanup()
................................................................................
		run = function(user, ctx)
		end;
		--[[
		bgProc = function(user, ctx, interval, runState)
			if runState.flags.compiled == true then return false end
			-- only so many nanides to go around
			runState.flags.compiled = true
			local time = minetest.get_gametime()
			local cyclesLeft = ctx.comp.cycles * interval

			for id, e in ipairs(ctx.file.body.conf) do
				if e.key == 'job' then
					local t = J.dec(e.value)
					local remove = false
					local r = starlit.item.sw.db[t.schematic]







|







 







|
|
|







 







|











|
|





|







 







|







32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
..
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
..
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
...
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
		return items, charges
	end

	return function(user, ctx)
		local function cleanup()
			user.action.prog.shred = nil
			if user.action.sfx.shred then
				core.sound_fade(user.action.sfx.shred, 1, 0)
				user.action.sfx.shred = nil
			end
			if user.action.fx.shred then
				user.action.fx.shred.abort()
			end
		end

................................................................................
			cleanup()
			return false
		end
		local shredTime = 1.0
		local soundPitch = 1.0 -- TODO
		local pdraw = prop.powerDraw or 0

		if core.is_protected(what, user.entity:get_player_name()) then return end
		local node = core.get_node(what)
		local nd = core.registered_nodes[node.name]
		local elt, fab, vary
		if nd._starlit then
			fab = nd._starlit.recover or nd._starlit.fab
			vary = nd._starlit.recover_vary
		end
		if fab then
			if fab.flag then
................................................................................
			if p < pdx then
				cleanup()
				return false
			elseif not user.action.prog.shred then
				cleanup() -- kill danglers
				-- begin
				user.action.prog.shred = 0
				user.action.sfx.shred = core.sound_play('starlit-nano-shred', {
					object = user.entity;
					max_hear_distance = prop.range*2;
					loop = true;
					pitch = soundPitch;
				})
				user.action.fx.shred = starlit.fx.nano.shred(user, what, prop, shredTime, node)
			else
				user.action.prog.shred = user.action.prog.shred + ctx.how.delta or 0
			end
			--print('shred progress: ', user.action.prog.shred)
			if user.action.prog.shred >= shredTime then
				core.remove_node(what)
				core.check_for_falling(what)
				--print('shred complete')
				user:suitSound 'starlit-success'
				if fab then
					local vf = fab
					if vary then
						local rng = (starlit.world.seedbank+0xa891f62)[core.hash_node_position(what)]
						vf = vf + vary(rng, {})
					end
					local items, charges = fabToItemsAndCharges(vf)
					for i, it in ipairs(items) do user:give(it) end
					-- TODO give gasses, liquids
				end
				cleanup()
................................................................................
		run = function(user, ctx)
		end;
		--[[
		bgProc = function(user, ctx, interval, runState)
			if runState.flags.compiled == true then return false end
			-- only so many nanides to go around
			runState.flags.compiled = true
			local time = core.get_gametime()
			local cyclesLeft = ctx.comp.cycles * interval

			for id, e in ipairs(ctx.file.body.conf) do
				if e.key == 'job' then
					local t = J.dec(e.value)
					local remove = false
					local r = starlit.item.sw.db[t.schematic]

Modified mods/starlit-material/init.lua from [158d4b1720] to [3f132c9f77].

1
2
3
4
5
6
7
8
9
10
11
12
13
local lib = starlit.mod.lib
local M = {
	canisterSizes = lib.registry.mk 'starlit_material:canister-size';
}
M.canisterSizes.foreach('starlit_material:canister_link', {}, function(id, sz)
	starlit.item.canister.link(minetest.get_current_modname() .. ':canister_' .. id, {
		name = sz.name;
		slots = sz.slots;
		vol = sz.vol; -- too big for suit?
		desc = sz.desc;
	})
end)
M.canisterSizes.meld {





|







1
2
3
4
5
6
7
8
9
10
11
12
13
local lib = starlit.mod.lib
local M = {
	canisterSizes = lib.registry.mk 'starlit_material:canister-size';
}
M.canisterSizes.foreach('starlit_material:canister_link', {}, function(id, sz)
	starlit.item.canister.link(core.get_current_modname() .. ':canister_' .. id, {
		name = sz.name;
		slots = sz.slots;
		vol = sz.vol; -- too big for suit?
		desc = sz.desc;
	})
end)
M.canisterSizes.meld {

Modified mods/starlit-tech/init.lua from [91056054d4] to [9e988e1fe2].

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
...
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
...
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
		local grp = {
			object = 1;
			attached_node = 1;
		}
		if def.group then
			for k,v in pairs(def.group((i/stages))) do grp[k]=v end
		end
		minetest.register_node(stID(i), {
			short_description = def.name;
			description = starlit.ui.tooltip {
				title = def.name;
				desc = def.desc;
				color = def.color;
				props = {
					{title = 'Burn Remaining', desc=lib.math.timespec(stageTimeout * i), affinity=i > (stages/2) and 'good' or 'bad'};
................................................................................
			};
			tiles = def.tile((i/stages));
			paramtype = 'light';
			paramtype2 = 'wallmounted';
			wallmounted_rotate_vertical = true;
			light_source = math.floor(lib.math.lerp(i/stages, 0, def.glow));
			on_construct = i ~= 0 and function(pos)
				local t = minetest.get_node_timer(pos)
				t:start(stageTimeout)
				if def.ctor then def.ctor(pos, (i/stages)) end
			end or nil;
			on_destruct = def.dtor and function(pos)
				def.dtor(pos, (i/stages))
			end or nil;
			on_timer = i ~= 0 and function(pos)
				local me = minetest.get_node(pos)
				minetest.swap_node(pos, {name=stID(i-1), param2=me.param2})
				return i > 1
			end or nil;
			_starlit = (function()
				local meta = {
					mass = def.mass;
					reverseEngineer = {
						complexity = 1;
................................................................................
			radiate = function(rp, pos)
				return 15 * f
			end;
		};
	} end;
}

minetest.register_node('starlit_tech:crate', {
	short_description = 'Crate';
	description = starlit.ui.tooltip {
		title = 'Crate';
		desc = 'A sturdy but lightweight aluminum storage crate.';
		props = { {title='Mass', affinity='info', desc='100g'} };
	};
	drawtype = 'nodebox';
................................................................................
			time = {
				shred = 1;
				shredPower = 3;
			};
		};
	};
	on_construct = function(pos)
		local m = minetest.get_meta(pos)
		local inv = m:get_inventory()
		inv:set_size('starlit:contents', 12)
	end;
	on_rightclick = function(pos, node, luser)
		if not luser then return end
		local user = starlit.activeUsers[luser:get_player_name()]
		user:openUI('starlit:box', 'index', {







|







 







|







|
|







 







|







 







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
...
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
...
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
		local grp = {
			object = 1;
			attached_node = 1;
		}
		if def.group then
			for k,v in pairs(def.group((i/stages))) do grp[k]=v end
		end
		core.register_node(stID(i), {
			short_description = def.name;
			description = starlit.ui.tooltip {
				title = def.name;
				desc = def.desc;
				color = def.color;
				props = {
					{title = 'Burn Remaining', desc=lib.math.timespec(stageTimeout * i), affinity=i > (stages/2) and 'good' or 'bad'};
................................................................................
			};
			tiles = def.tile((i/stages));
			paramtype = 'light';
			paramtype2 = 'wallmounted';
			wallmounted_rotate_vertical = true;
			light_source = math.floor(lib.math.lerp(i/stages, 0, def.glow));
			on_construct = i ~= 0 and function(pos)
				local t = core.get_node_timer(pos)
				t:start(stageTimeout)
				if def.ctor then def.ctor(pos, (i/stages)) end
			end or nil;
			on_destruct = def.dtor and function(pos)
				def.dtor(pos, (i/stages))
			end or nil;
			on_timer = i ~= 0 and function(pos)
				local me = core.get_node(pos)
				core.swap_node(pos, {name=stID(i-1), param2=me.param2})
				return i > 1
			end or nil;
			_starlit = (function()
				local meta = {
					mass = def.mass;
					reverseEngineer = {
						complexity = 1;
................................................................................
			radiate = function(rp, pos)
				return 15 * f
			end;
		};
	} end;
}

core.register_node('starlit_tech:crate', {
	short_description = 'Crate';
	description = starlit.ui.tooltip {
		title = 'Crate';
		desc = 'A sturdy but lightweight aluminum storage crate.';
		props = { {title='Mass', affinity='info', desc='100g'} };
	};
	drawtype = 'nodebox';
................................................................................
			time = {
				shred = 1;
				shredPower = 3;
			};
		};
	};
	on_construct = function(pos)
		local m = core.get_meta(pos)
		local inv = m:get_inventory()
		inv:set_size('starlit:contents', 12)
	end;
	on_rightclick = function(pos, node, luser)
		if not luser then return end
		local user = starlit.activeUsers[luser:get_player_name()]
		user:openUI('starlit:box', 'index', {

Modified mods/starlit/effect.lua from [9a580a99bf] to [ee92b6e11c].

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
...
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
...
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
...
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
...
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
...
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
...
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
...
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
	elseif d.pos then -- find effects anchored here and people in range
		for id,effect in pairs(starlit.effect.active) do
			if not effect.anchor then goto skip end -- this intentionally excludes attached effects
			if ineffectrange(effect,d.pos,d.range) then
				effects[#effects+1] = {v=effect,i=id}
			end
		::skip::end
		local ppl = minetest.get_objects_inside_radius(d.pos,d.range)
		if #targets == 0 then targets = ppl else
			for _,p in pairs(ppl) do targets[#targets+1] = p end
		end
	end

	-- iterate over targets to remove from any effect's influence
	for _,t in pairs(targets) do
................................................................................
				if v == effect then starlit.effect.active[k] = nil break end
			end
		end
	end
end

starlit.effect.ensorcelled = function(player,effect)
	if type(player) == 'string' then player = minetest.get_player_by_name(player) end
	for _,s in pairs(starlit.effect.active) do
		if effect and (s.name ~= effect) then goto skip end
		for _,sub in pairs(s.subjects) do
			if sub.player == player then return s end
		end
	::skip::end
	return false
................................................................................
				end
			end
		until idx >= #starlit.effect.active
	end
end

-- when a new effect is created, we analyze it and make the appropriate calls
-- to minetest.after to queue up the events. each job returned needs to be
-- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling
-- necessary :D

starlit.effect.cast = function(proto)
	local s = table.copy(proto)
	s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
	s.impacts = s.impacts or {} s.subjects = s.subjects or {}
	s.delay = s.delay or 0
	s.visual = function(subj, def)
		s.vfx[#s.vfx + 1] = {
			handle = minetest.add_particlespawner(def);
			subject = subj;
		}
	end
	s.visual_caster = function(def) -- convenience function
		local d = table.copy(def)
		d.attached = s.caster
		s.visual(nil, d)
................................................................................
			s.impacts[#s.impacts+1] = rec
			etbl[#etbl+1] = rec
		end
		return etbl
	end
	s.abort = function()
		for _,j in ipairs(s.jobs) do j:cancel() end
		for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
		for _,i in ipairs(s.sfx) do s.silence(i) end
		for _,i in ipairs(s.impacts) do i.effect:stop() end
	end
	s.release_subject = function(si)
		local t = s.subjects[si]
		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
		for _,f in pairs(s.vfx) do
			if f.subject == t then minetest.delete_particlespawner(f.handle) end
		end
		s.subjects[si] = nil
	end
	local interpret_timespec = function(when)
		if when == nil then return 0 end
		local t if type(when) == 'number' then
			t = s.duration * when
................................................................................
		end
		if t then return math.min(s.duration,math.max(0,t)) end

		log.err('invalid timespec ' .. dump(when))
		return 0
	end
	s.queue = function(when,fn)
		local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
		local timepast = interpret_timespec(when)
		if not timepast then timepast = 0 end
		local timeleft = s.duration - timepast
		local howlong = (s.delay + timepast) - elapsed
		if howlong < 0 then
			log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
			howlong = 0
		end
		s.jobs[#s.jobs+1] = minetest.after(howlong, function()
			-- this is somewhat awkward. since we're using a non-polling approach, we
			-- need to find a way to account for a caster or subject walking into an
			-- existing antimagic field, or someone with an existing antimagic aura
			-- walking into range of the anchor. so every time a effect effect would
			-- take place, we first check to see if it's in range of something nasty
			if not s.disjunction and -- avoid self-disjunction
				((s.caster and starlit.effect.probe(s.caster:get_pos()).disjunction) or
................................................................................
			for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
		elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
		else specs[#specs+1] = { spec = {pos = spec.where} } end

		for _,sp in pairs(specs) do
			sp.spec.gain = sp.spec.gain or spec.gain
			local so = {
				handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral);
				ctl = spec;
				-- object = sp.obj;
				subject = sp.subject;
			}
			stbl[#stbl+1] = so
			s.sfx[#s.sfx+1] = so
		end
................................................................................
					for _,snd in pairs(snds) do s.silence(snd) end
				end)
			end
		end)
	end
	s.silence = function(sound)
		if not sound.handle then return end
		if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
		else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
	end
	local startqueued, termqueued = false, false
	local myid = #starlit.effect.active+1
	s.cancel = function()
		s.abort()
		starlit.effect.active[myid] = nil
	end
................................................................................
					iteration = iteration;
					iterationcount = itercount;
					timeleft = timeleft;
					timeelapsed = s.duration - timeleft;
					lastreturn = lastreturn;
				}
				if nr ~= false and iteration < itercount then
					s.jobs[#s.jobs+1] = minetest.after(int.period,
						function() iterate(nr) end)
				end
			end
			if int.after
				then s.queue(int.after, iterate)
				else s.queue({whence=0, secs=s.period}, iterate)
			end
................................................................................
		end
	end
	if s.sounds then
		for when,what in pairs(s.sounds) do s.play(when,what) end
	end
	starlit.effect.active[myid] = s
	if not termqueued then
		s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function()
			if s.terminate then s:terminate() end
			starlit.effect.active[myid] = nil
		end)
	end
	s.starttime = minetest.get_server_uptime()
	return s
end

minetest.register_on_dieplayer(function(player)
	starlit.effect.disjoin{target=player}
end)







|







 







|







 







|










|







 







|








|







 







|








|







 







|







 







|
|







 







|







 







|




|



|


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
...
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
...
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
...
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
...
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
...
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
...
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
...
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
	elseif d.pos then -- find effects anchored here and people in range
		for id,effect in pairs(starlit.effect.active) do
			if not effect.anchor then goto skip end -- this intentionally excludes attached effects
			if ineffectrange(effect,d.pos,d.range) then
				effects[#effects+1] = {v=effect,i=id}
			end
		::skip::end
		local ppl = core.get_objects_inside_radius(d.pos,d.range)
		if #targets == 0 then targets = ppl else
			for _,p in pairs(ppl) do targets[#targets+1] = p end
		end
	end

	-- iterate over targets to remove from any effect's influence
	for _,t in pairs(targets) do
................................................................................
				if v == effect then starlit.effect.active[k] = nil break end
			end
		end
	end
end

starlit.effect.ensorcelled = function(player,effect)
	if type(player) == 'string' then player = core.get_player_by_name(player) end
	for _,s in pairs(starlit.effect.active) do
		if effect and (s.name ~= effect) then goto skip end
		for _,sub in pairs(s.subjects) do
			if sub.player == player then return s end
		end
	::skip::end
	return false
................................................................................
				end
			end
		until idx >= #starlit.effect.active
	end
end

-- when a new effect is created, we analyze it and make the appropriate calls
-- to core.after to queue up the events. each job returned needs to be
-- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling
-- necessary :D

starlit.effect.cast = function(proto)
	local s = table.copy(proto)
	s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
	s.impacts = s.impacts or {} s.subjects = s.subjects or {}
	s.delay = s.delay or 0
	s.visual = function(subj, def)
		s.vfx[#s.vfx + 1] = {
			handle = core.add_particlespawner(def);
			subject = subj;
		}
	end
	s.visual_caster = function(def) -- convenience function
		local d = table.copy(def)
		d.attached = s.caster
		s.visual(nil, d)
................................................................................
			s.impacts[#s.impacts+1] = rec
			etbl[#etbl+1] = rec
		end
		return etbl
	end
	s.abort = function()
		for _,j in ipairs(s.jobs) do j:cancel() end
		for _,v in ipairs(s.vfx) do core.delete_particlespawner(v.handle) end
		for _,i in ipairs(s.sfx) do s.silence(i) end
		for _,i in ipairs(s.impacts) do i.effect:stop() end
	end
	s.release_subject = function(si)
		local t = s.subjects[si]
		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
		for _,f in pairs(s.vfx) do
			if f.subject == t then core.delete_particlespawner(f.handle) end
		end
		s.subjects[si] = nil
	end
	local interpret_timespec = function(when)
		if when == nil then return 0 end
		local t if type(when) == 'number' then
			t = s.duration * when
................................................................................
		end
		if t then return math.min(s.duration,math.max(0,t)) end

		log.err('invalid timespec ' .. dump(when))
		return 0
	end
	s.queue = function(when,fn)
		local elapsed = s.starttime and core.get_server_uptime() - s.starttime or 0
		local timepast = interpret_timespec(when)
		if not timepast then timepast = 0 end
		local timeleft = s.duration - timepast
		local howlong = (s.delay + timepast) - elapsed
		if howlong < 0 then
			log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
			howlong = 0
		end
		s.jobs[#s.jobs+1] = core.after(howlong, function()
			-- this is somewhat awkward. since we're using a non-polling approach, we
			-- need to find a way to account for a caster or subject walking into an
			-- existing antimagic field, or someone with an existing antimagic aura
			-- walking into range of the anchor. so every time a effect effect would
			-- take place, we first check to see if it's in range of something nasty
			if not s.disjunction and -- avoid self-disjunction
				((s.caster and starlit.effect.probe(s.caster:get_pos()).disjunction) or
................................................................................
			for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
		elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
		else specs[#specs+1] = { spec = {pos = spec.where} } end

		for _,sp in pairs(specs) do
			sp.spec.gain = sp.spec.gain or spec.gain
			local so = {
				handle = core.sound_play(spec.sound, sp.spec, spec.ephemeral);
				ctl = spec;
				-- object = sp.obj;
				subject = sp.subject;
			}
			stbl[#stbl+1] = so
			s.sfx[#s.sfx+1] = so
		end
................................................................................
					for _,snd in pairs(snds) do s.silence(snd) end
				end)
			end
		end)
	end
	s.silence = function(sound)
		if not sound.handle then return end
		if sound.ctl.fade == 0 then core.sound_stop(sound.handle)
		else core.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
	end
	local startqueued, termqueued = false, false
	local myid = #starlit.effect.active+1
	s.cancel = function()
		s.abort()
		starlit.effect.active[myid] = nil
	end
................................................................................
					iteration = iteration;
					iterationcount = itercount;
					timeleft = timeleft;
					timeelapsed = s.duration - timeleft;
					lastreturn = lastreturn;
				}
				if nr ~= false and iteration < itercount then
					s.jobs[#s.jobs+1] = core.after(int.period,
						function() iterate(nr) end)
				end
			end
			if int.after
				then s.queue(int.after, iterate)
				else s.queue({whence=0, secs=s.period}, iterate)
			end
................................................................................
		end
	end
	if s.sounds then
		for when,what in pairs(s.sounds) do s.play(when,what) end
	end
	starlit.effect.active[myid] = s
	if not termqueued then
		s.jobs[#s.jobs+1] = core.after(s.delay + s.duration, function()
			if s.terminate then s:terminate() end
			starlit.effect.active[myid] = nil
		end)
	end
	s.starttime = core.get_server_uptime()
	return s
end

core.register_on_dieplayer(function(player)
	starlit.effect.disjoin{target=player}
end)

Modified mods/starlit/element.lua from [f0333e36f4] to [79d7647b1a].

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
..
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
...
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
...
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
...
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
...
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
			indsz, indsz,
			indsz, indsz,
			indicator);
	end
end

M.element.foreach('starlit:gen-forms', {}, function(id, m)
	local eltID = F('%s:element_%s', minetest.get_current_modname(), id)
-- 	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
	local tt = function(t, d, g)
		return starlit.ui.tooltip {
			title = t, desc = d;
			color = lib.color(0.1,0.2,0.1);
			props = {
				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
................................................................................
	m.form = m.form or {}


	if not (m.gas or m.liquid) then
		local brickID = eltID .. '_brick'
		local brickName = F('%s Brick', lib.str.capitalize(m.name))
		m.form.brick = brickID
		minetest.register_craftitem(brickID, {
			short_description = brickName;
			description = tt(brickName, F('A small brick of %s, ready to be worked by a matter compiler', m.name), 1);
			inventory_image = img(lib.image 'starlit-item-brick.png');
			wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
			stack_max = 500;
			groups = {element=1, brick=1};
			_starlit = {
................................................................................
				};
			};
		});
	end

	--[[
	local chunk = F('starlit-element-%s-powder.png', id);
	minetest.register_craftitem(eltID, {
		short_description = eltName;
		description = tt(eltName, F('A 1g chunk of elemental %s, ready to be worked by a cold matter compiler', m.name), 1);
		inventory_image = iblit(chunk);
		wield_image = powder;
		stack_max = 1000; -- 1kg
		groups = {element = 1, chunk = 1};
		_starlit = {
................................................................................
end)


M.metal.foreach('starlit:gen-forms', {}, function(id, m)
	if m.elemental then -- avoid multiple forms for same material
		m.form = M.element.db[m.elemental].form;
	else
		local baseID = F('%s:metal_%s_', minetest.get_current_modname(), id)
		local brickID = baseID .. 'brick'
		local brickName = F('%s Brick', lib.str.capitalize(m.name))
		m.form = m.form or {}
		m.form.brick = brickID
		local tt = function(t, d, g)
			return starlit.ui.tooltip {
				title = t, desc = d;
................................................................................
		end
		local iblit = mkEltIndicator(mcomp)
		local function img(s)
			return iblit(s:colorize(m.color):render())
		end

		local mass = 1
		minetest.register_craftitem(brickID, {
			short_description = brickName;
			description = tt(brickName, F('A small brick of %s, ready to be worked by a matter compiler', m.name), mass);
			inventory_image = img(lib.image('starlit-item-brick.png'));
			wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
			groups = {metal = 1, brick = 1};
			stack_max = 500;
			_starlit = {
................................................................................
		color = lib.color(0.2,0.1,0.1);
		props = props;
	};	
end

starlit.item.canister = lib.registry.mk 'starlit:canister';
starlit.item.canister.foreach('starlit:item-gen', {}, function(id, c)
	minetest.register_craftitem(id, {
		short_description = c.name;
		description = canisterDesc(nil, c);
		inventory_image = c.image or 'starlit-item-element-canister.png';
		groups = {canister = 1};
		stack_max = 1;
		_starlit = {
			canister = c;







|







 







|







 







|







 







|







 







|







 







|







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
..
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
...
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
...
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
...
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
...
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
			indsz, indsz,
			indsz, indsz,
			indicator);
	end
end

M.element.foreach('starlit:gen-forms', {}, function(id, m)
	local eltID = F('%s:element_%s', core.get_current_modname(), id)
-- 	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
	local tt = function(t, d, g)
		return starlit.ui.tooltip {
			title = t, desc = d;
			color = lib.color(0.1,0.2,0.1);
			props = {
				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
................................................................................
	m.form = m.form or {}


	if not (m.gas or m.liquid) then
		local brickID = eltID .. '_brick'
		local brickName = F('%s Brick', lib.str.capitalize(m.name))
		m.form.brick = brickID
		core.register_craftitem(brickID, {
			short_description = brickName;
			description = tt(brickName, F('A small brick of %s, ready to be worked by a matter compiler', m.name), 1);
			inventory_image = img(lib.image 'starlit-item-brick.png');
			wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
			stack_max = 500;
			groups = {element=1, brick=1};
			_starlit = {
................................................................................
				};
			};
		});
	end

	--[[
	local chunk = F('starlit-element-%s-powder.png', id);
	core.register_craftitem(eltID, {
		short_description = eltName;
		description = tt(eltName, F('A 1g chunk of elemental %s, ready to be worked by a cold matter compiler', m.name), 1);
		inventory_image = iblit(chunk);
		wield_image = powder;
		stack_max = 1000; -- 1kg
		groups = {element = 1, chunk = 1};
		_starlit = {
................................................................................
end)


M.metal.foreach('starlit:gen-forms', {}, function(id, m)
	if m.elemental then -- avoid multiple forms for same material
		m.form = M.element.db[m.elemental].form;
	else
		local baseID = F('%s:metal_%s_', core.get_current_modname(), id)
		local brickID = baseID .. 'brick'
		local brickName = F('%s Brick', lib.str.capitalize(m.name))
		m.form = m.form or {}
		m.form.brick = brickID
		local tt = function(t, d, g)
			return starlit.ui.tooltip {
				title = t, desc = d;
................................................................................
		end
		local iblit = mkEltIndicator(mcomp)
		local function img(s)
			return iblit(s:colorize(m.color):render())
		end

		local mass = 1
		core.register_craftitem(brickID, {
			short_description = brickName;
			description = tt(brickName, F('A small brick of %s, ready to be worked by a matter compiler', m.name), mass);
			inventory_image = img(lib.image('starlit-item-brick.png'));
			wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
			groups = {metal = 1, brick = 1};
			stack_max = 500;
			_starlit = {
................................................................................
		color = lib.color(0.2,0.1,0.1);
		props = props;
	};	
end

starlit.item.canister = lib.registry.mk 'starlit:canister';
starlit.item.canister.foreach('starlit:item-gen', {}, function(id, c)
	core.register_craftitem(id, {
		short_description = c.name;
		description = canisterDesc(nil, c);
		inventory_image = c.image or 'starlit-item-element-canister.png';
		groups = {canister = 1};
		stack_max = 1;
		_starlit = {
			canister = c;

Modified mods/starlit/fab.lua from [06631e46d8] to [7c2e295411].

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
	};
-- 	crystal = {
-- 		op = fQuant;
-- 	};
	item = {
		name = {"item", "items"};
		string = function(x, n)
			local i = minetest.registered_items[x]
			return tostring(n) .. 'x ' .. i.short_description
		end;
		image = function(x, n)
			return ItemStack(x):get_definition().inventory_image
		end;
		inventory = function(x, n, stack)
			x = ItemStack(x)







|







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
	};
-- 	crystal = {
-- 		op = fQuant;
-- 	};
	item = {
		name = {"item", "items"};
		string = function(x, n)
			local i = core.registered_items[x]
			return tostring(n) .. 'x ' .. i.short_description
		end;
		image = function(x, n)
			return ItemStack(x):get_definition().inventory_image
		end;
		inventory = function(x, n, stack)
			x = ItemStack(x)

Modified mods/starlit/food.lua from [c58f501acb] to [09c96accb0].

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
..
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		desc = f.desc;
		props = props;
		color = f.color;
	};
end

F.foreach('starlit:gen-food', {}, function(id, f)
	minetest.register_item(id, {
		type = f.itemType or 'none';
		inventory_image = f.tex;
		short_description = f.name;
		description = foodTip(nil, f);
		on_use = function(st, luser)
			local user = starlit.activeUsers[luser:get_player_name()]
			st:take_item(1)
................................................................................
			recover = f.recover;
			mass = f.mass;
		};
	})
end)

starlit.item.seed.foreach('starlit:gen-seed', {}, function(id, s)
	minetest.register_item(id, {
		type = 'none';
		inventory_image = s.tex;
		short_description = s.name;
		description = starlit.ui.tooltip {
			title = s.name;
			color = s.color;
			desc = s.desc;







|







 







|







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
..
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		desc = f.desc;
		props = props;
		color = f.color;
	};
end

F.foreach('starlit:gen-food', {}, function(id, f)
	core.register_item(id, {
		type = f.itemType or 'none';
		inventory_image = f.tex;
		short_description = f.name;
		description = foodTip(nil, f);
		on_use = function(st, luser)
			local user = starlit.activeUsers[luser:get_player_name()]
			st:take_item(1)
................................................................................
			recover = f.recover;
			mass = f.mass;
		};
	})
end)

starlit.item.seed.foreach('starlit:gen-seed', {}, function(id, s)
	core.register_item(id, {
		type = 'none';
		inventory_image = s.tex;
		short_description = s.name;
		description = starlit.ui.tooltip {
			title = s.name;
			color = s.color;
			desc = s.desc;

Modified mods/starlit/init.lua from [277b90dbc5] to [ae90c8058c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
..
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
...
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
...
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
...
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
...
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
...
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
...
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
...
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
-- [Êž] starlit/init.lua
--  ~ lexi hale <lexi@hale.su>
--  ? basic setup, game rules, terrain
--  © EUPL v1.2

local T = minetest.get_translator 'starlit'

-- TODO enforce latest engine version

local mod = {
	-- subordinate mods register here
	lib = vtlib;
		-- vtlib should be accessed as starlit.mod.lib by starlit modules for the sake of proper encapsulation. vtlib should simply be a provider, not a hardcoded dependency
}
local lib = mod.lib


starlit = {
	ident = minetest.get_current_modname();
	mod = mod;
	translator = T;

	constant = {
		light = { --minetest units
			dim = 3;
			lamp = 7;
			bright = 10;
			brightest = 14; -- only sun and growlights
		};
		heat = { -- celsius
			freezing = 0;
................................................................................
			thermalConductivity = 0.05; -- κ
		};
		rad = {
		};

		phys = {
			--- HACK HACK HAAAAAAAAAAACK
			engineGravity = minetest.settings:get('movement_gravity') or 9.81
		};
	};

	activeUsers = {
		-- map of username -> user object
	};
	activeUI = {
................................................................................

	-- standardized effects
	fx = {};

	type = {};
	world = {
		defaultScenario = 'starlit_scenario:imperialExpat';
		seedbank = lib.math.seedbank(minetest.get_mapgen_setting 'seed');
		mineral = lib.registry.mk 'starlit:mineral';
		material = { -- raw materials
			element = lib.registry.mk 'starlit:element';
			-- elements are automatically sorted into the following categories
			-- if they match. however, it's possible to have a metal/gas/liquid
			-- that *isn't* a pure element, so these need separate registries
			-- for alloys and mixtures like steel and water
................................................................................
	};

	jobs = {};
}

-- TODO deal with core.DEFAULT_PHYSICS once it hits master

starlit.cfgDir = minetest.get_worldpath() .. '/' .. starlit.ident

local logger = function(module)
	local function argjoin(arg, nxt, ...)
		if arg and not nxt then return tostring(arg) end
		if not arg then return "(nil)" end
		return tostring(arg) .. ' ' .. argjoin(nxt, ...)
	end
	local lg = {}
	local setup = function(fn, lvl)
		lvl = lvl or fn
		local function emit(...)
			local call = (fn == 'fatal') and error
				or function(str) minetest.log(lvl, str) end
			if module
				then call(string.format('[%s :: %s] %s',starlit.ident,module,argjoin(...)))
				else call(string.format('[%s] %s',starlit.ident,argjoin(...)))
			end
		end
		lg[fn       ] = function(...) emit(...)                end
		lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn
................................................................................
end

starlit.logger = logger

local log = logger()

function starlit.evaluate(name, ...)
	local path = minetest.get_modpath(minetest.get_current_modname())
	local filename = string.format('%s/%s', path, name)
	log.info('loading', filename)
	local chunk, err = loadfile(filename, filename)
	if not chunk then error(err) end
	return chunk(...)
end

function starlit.include(name, ...) -- semantic variant used for loading modules
	return starlit.evaluate(name..'.lua', ...)
end

function starlit.region.radiator.scan(pos,node)
	local R = starlit.region
	local phash = minetest.hash_node_position(pos)
	if R.radiator.sources[phash] then return end -- already loaded
	node = node or minetest.get_node(pos)

	local def = minetest.registered_nodes[node.name]
	local cl = def._starlit and def._starlit.radiator
	if not cl then return nil end
	local min,max = cl.maxEffectArea and cl.maxEffectArea(pos) or nil
	if not min then
		assert(cl.radius, 'no radius callback for radiator')
		local r = cl.radius(pos)
		local vr = vector.new(r,r,r)
		min,max = pos-vr, pos+vr
	end
	local id = R.radiator.store:insert_area(min,max, minetest.pos_to_string(pos))
	R.radiator.sources[phash] = id
end

function starlit.region.radiator.unload(pos)
	local R = starlit.region
	local phash = minetest.hash_node_position(pos)
	local id = R.radiator.sources[phash]
	R.radiator.store:remove_area(id)
	R.radiator.sources[phash] = nil
end

minetest.register_lbm {
	label = 'build radiator index';
	name = 'starlit:loadradiatorboxes';
	nodenames = {'group:radiator'};
	run_at_every_load = true;
	action = function(pos, node, dt)
		starlit.region.radiator.scan(pos, node)
	end;
................................................................................
	-- NOTE: temp emitter nodes are responsible for decaching themselves in their on_destruct cb
}


function starlit.startJob(id, interval, job)
	local lastRun
	local function start()
		starlit.jobs[id] = minetest.after(interval, function()
			local t = minetest.get_gametime()
			local d = lastRun and t - lastRun or nil
			lastRun = t
			local continue = job(d, interval)
			if continue == true or continue == nil then
				start()
			elseif continue ~= false then
				interval = continue
................................................................................
starlit.include 'element'

starlit.include 'terrain'
starlit.include 'interfaces'
starlit.include 'compile'
starlit.include 'suit'

-- minetest.settings:set('movement_gravity', starlit.world.planet.gravity) -- ??? seriously???
-- THIS OVERRIDES THE GLOBAL SETTING *AND PERSISTS IT* WHAT IN THE SATANIC FUCK

---------------
-- callbacks --
---------------
-- here we connect our types up to the minetest API

local function userCB(fn)
	return function(luser, ...)
		local name = luser:get_player_name()
		local user = starlit.activeUsers[name]
		return fn(user, ...)
	end
end

minetest.register_on_joinplayer(function(luser, lastLogin)
	-- TODO check that necessary CSMs are installed
	local user = starlit.type.user(luser)

	if lastLogin == nil then
		user:onSignup()
	end
	user:onJoin()

	starlit.activeUsers[user.name] = user
end)

minetest.register_on_leaveplayer(function(luser)
	starlit.activeUsers[luser:get_player_name()]:onPart()
end)

minetest.register_on_player_receive_fields(function(luser, formid, fields)
	local name = luser:get_player_name()
	local user = starlit.activeUsers[name]
	if not user then return false end
	if formid == '' then -- main menu
		return starlit.ui.userMenuDispatch(user,fields)
	end
	local ui = starlit.interface.db[formid]
................................................................................
	user:onRespond(ui, state, fields)
	if fields.quit then
		starlit.activeUI[name] = nil
	end
	return true
end)

minetest.register_on_respawnplayer(userCB(function(user)
	return user:onRespawn()
end))

minetest.register_on_dieplayer(userCB(function(user, reason)
	return user:onDie(reason)
end))

minetest.register_on_punchnode(function(pos,node,puncher,point)
	local user = starlit.activeUsers[puncher:get_player_name()]
	local oldTgt = user.action.tgt
	user.action.tgt = point
	if bit.band(user.action.bits, 0x80)==0 then
		user.action.bits = bit.bor(user.action.bits, 0x80)
		--user:trigger('primary', {state = 'init'})
	else
................................................................................
		user:trigger('retarget', {oldTgt = oldTgt})
	end
end
-- sigh
--[[
core.noneitemdef_default.on_place = function(...)
	if not triggerPower(...) then
		minetest.item_place(...)
	end
end
core.noneitemdef_default.on_use           = function(...) triggerPower(...) end
core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end
]]
minetest.register_item(":", {
	type = "none",
	wield_image = "wieldhand.png",
	wield_scale = {x=1,y=1,z=2.5},
	on_secondary_use = function(...) triggerPower(...) end;
-- 	on_use = function(...) print'base' end;
	after_use = function(i,u,n,p)
		if (u:is_player()) then triggerPower(i,u,p) end
	end;
})
minetest.register_item("starlit:_hand_dig", {
	type = "none",
	wield_image = "wieldhand.png",
	wield_scale = {x=1,y=1,z=2.5},
	tool_capabilities = {
		groupcaps = {
			object = {maxlevel=1, times = {.10,.20,.40}};
			plant = {maxlevel=1, times = {.50}};
................................................................................

			-- sand, dirt, gravel
			looseClump = {maxlevel=1, times = {1.5, 2.5}};
		};
	}
})

minetest.register_on_player_inventory_action(function(luser, act, inv, p)
	local name = luser:get_player_name()
	local user = starlit.activeUsers[name]
	-- allow UIs to update on UI changes
	local state = starlit.activeUI[name]
	if state then
		local ui = starlit.interface.db[state.form]
		ui:cb('onMoveItem', user, act, inv, p)
	end
end)

minetest.register_on_player_hpchange(function(luser, delta, cause)
	local user = starlit.activeUsers[luser:get_player_name()]
	if cause.type == 'fall' then
		delta = user:damageModifier('bluntForceTrauma', (delta * 50))
		-- justification: a short fall can do around
		-- five points of damage, which is nearly 50%
		-- of the default hp_max. since we crank up
		-- hp by a factor of 50~40, damage should be
		-- cranked by similarly
	end
	return delta
end, true)

function minetest.handle_node_drops(pos, drops, digger)
	local function jitter(pos)
		local function r(x) return x+math.random(-0.01, 0.01) end
		return vector.new(
			r(pos.x),
			r(pos.y),
			r(pos.z)
		)
	end
	for i, it in ipairs(drops) do
		if type(it) == 'string' then it = ItemStack(it) end
		if not it:is_empty() then
			local ent = minetest.add_item(jitter(pos), it)
			if ent ~= nil then -- avoid crash when dropping unknown item
				local dp = vector.new(0,0,0)
				if digger then dp = digger:get_pos() end
				local delta = dp - ent:get_pos()
				ent:add_velocity(vector.new(delta.x,0,delta.z));
			end
		end
	end
end


-- TODO timer iterates live UI






|












|




|







 







|







 







|







 







|












|







 







|













|

|

|









|





|





|







 







|
|







 







|





|









|











|



|







 







|



|



|







 







|





|









|







 







|










|












|











|













1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
..
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
...
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
...
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
...
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
...
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
...
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
...
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
...
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
-- [Êž] starlit/init.lua
--  ~ lexi hale <lexi@hale.su>
--  ? basic setup, game rules, terrain
--  © EUPL v1.2

local T = core.get_translator 'starlit'

-- TODO enforce latest engine version

local mod = {
	-- subordinate mods register here
	lib = vtlib;
		-- vtlib should be accessed as starlit.mod.lib by starlit modules for the sake of proper encapsulation. vtlib should simply be a provider, not a hardcoded dependency
}
local lib = mod.lib


starlit = {
	ident = core.get_current_modname();
	mod = mod;
	translator = T;

	constant = {
		light = { --luanti units
			dim = 3;
			lamp = 7;
			bright = 10;
			brightest = 14; -- only sun and growlights
		};
		heat = { -- celsius
			freezing = 0;
................................................................................
			thermalConductivity = 0.05; -- κ
		};
		rad = {
		};

		phys = {
			--- HACK HACK HAAAAAAAAAAACK
			engineGravity = core.settings:get('movement_gravity') or 9.81
		};
	};

	activeUsers = {
		-- map of username -> user object
	};
	activeUI = {
................................................................................

	-- standardized effects
	fx = {};

	type = {};
	world = {
		defaultScenario = 'starlit_scenario:imperialExpat';
		seedbank = lib.math.seedbank(core.get_mapgen_setting 'seed');
		mineral = lib.registry.mk 'starlit:mineral';
		material = { -- raw materials
			element = lib.registry.mk 'starlit:element';
			-- elements are automatically sorted into the following categories
			-- if they match. however, it's possible to have a metal/gas/liquid
			-- that *isn't* a pure element, so these need separate registries
			-- for alloys and mixtures like steel and water
................................................................................
	};

	jobs = {};
}

-- TODO deal with core.DEFAULT_PHYSICS once it hits master

starlit.cfgDir = core.get_worldpath() .. '/' .. starlit.ident

local logger = function(module)
	local function argjoin(arg, nxt, ...)
		if arg and not nxt then return tostring(arg) end
		if not arg then return "(nil)" end
		return tostring(arg) .. ' ' .. argjoin(nxt, ...)
	end
	local lg = {}
	local setup = function(fn, lvl)
		lvl = lvl or fn
		local function emit(...)
			local call = (fn == 'fatal') and error
				or function(str) core.log(lvl, str) end
			if module
				then call(string.format('[%s :: %s] %s',starlit.ident,module,argjoin(...)))
				else call(string.format('[%s] %s',starlit.ident,argjoin(...)))
			end
		end
		lg[fn       ] = function(...) emit(...)                end
		lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn
................................................................................
end

starlit.logger = logger

local log = logger()

function starlit.evaluate(name, ...)
	local path = core.get_modpath(core.get_current_modname())
	local filename = string.format('%s/%s', path, name)
	log.info('loading', filename)
	local chunk, err = loadfile(filename, filename)
	if not chunk then error(err) end
	return chunk(...)
end

function starlit.include(name, ...) -- semantic variant used for loading modules
	return starlit.evaluate(name..'.lua', ...)
end

function starlit.region.radiator.scan(pos,node)
	local R = starlit.region
	local phash = core.hash_node_position(pos)
	if R.radiator.sources[phash] then return end -- already loaded
	node = node or core.get_node(pos)

	local def = core.registered_nodes[node.name]
	local cl = def._starlit and def._starlit.radiator
	if not cl then return nil end
	local min,max = cl.maxEffectArea and cl.maxEffectArea(pos) or nil
	if not min then
		assert(cl.radius, 'no radius callback for radiator')
		local r = cl.radius(pos)
		local vr = vector.new(r,r,r)
		min,max = pos-vr, pos+vr
	end
	local id = R.radiator.store:insert_area(min,max, core.pos_to_string(pos))
	R.radiator.sources[phash] = id
end

function starlit.region.radiator.unload(pos)
	local R = starlit.region
	local phash = core.hash_node_position(pos)
	local id = R.radiator.sources[phash]
	R.radiator.store:remove_area(id)
	R.radiator.sources[phash] = nil
end

core.register_lbm {
	label = 'build radiator index';
	name = 'starlit:loadradiatorboxes';
	nodenames = {'group:radiator'};
	run_at_every_load = true;
	action = function(pos, node, dt)
		starlit.region.radiator.scan(pos, node)
	end;
................................................................................
	-- NOTE: temp emitter nodes are responsible for decaching themselves in their on_destruct cb
}


function starlit.startJob(id, interval, job)
	local lastRun
	local function start()
		starlit.jobs[id] = core.after(interval, function()
			local t = core.get_gametime()
			local d = lastRun and t - lastRun or nil
			lastRun = t
			local continue = job(d, interval)
			if continue == true or continue == nil then
				start()
			elseif continue ~= false then
				interval = continue
................................................................................
starlit.include 'element'

starlit.include 'terrain'
starlit.include 'interfaces'
starlit.include 'compile'
starlit.include 'suit'

-- core.settings:set('movement_gravity', starlit.world.planet.gravity) -- ??? seriously???
-- THIS OVERRIDES THE GLOBAL SETTING *AND PERSISTS IT* WHAT IN THE SATANIC FUCK

---------------
-- callbacks --
---------------
-- here we connect our types up to the luanti API

local function userCB(fn)
	return function(luser, ...)
		local name = luser:get_player_name()
		local user = starlit.activeUsers[name]
		return fn(user, ...)
	end
end

core.register_on_joinplayer(function(luser, lastLogin)
	-- TODO check that necessary CSMs are installed
	local user = starlit.type.user(luser)

	if lastLogin == nil then
		user:onSignup()
	end
	user:onJoin()

	starlit.activeUsers[user.name] = user
end)

core.register_on_leaveplayer(function(luser)
	starlit.activeUsers[luser:get_player_name()]:onPart()
end)

core.register_on_player_receive_fields(function(luser, formid, fields)
	local name = luser:get_player_name()
	local user = starlit.activeUsers[name]
	if not user then return false end
	if formid == '' then -- main menu
		return starlit.ui.userMenuDispatch(user,fields)
	end
	local ui = starlit.interface.db[formid]
................................................................................
	user:onRespond(ui, state, fields)
	if fields.quit then
		starlit.activeUI[name] = nil
	end
	return true
end)

core.register_on_respawnplayer(userCB(function(user)
	return user:onRespawn()
end))

core.register_on_dieplayer(userCB(function(user, reason)
	return user:onDie(reason)
end))

core.register_on_punchnode(function(pos,node,puncher,point)
	local user = starlit.activeUsers[puncher:get_player_name()]
	local oldTgt = user.action.tgt
	user.action.tgt = point
	if bit.band(user.action.bits, 0x80)==0 then
		user.action.bits = bit.bor(user.action.bits, 0x80)
		--user:trigger('primary', {state = 'init'})
	else
................................................................................
		user:trigger('retarget', {oldTgt = oldTgt})
	end
end
-- sigh
--[[
core.noneitemdef_default.on_place = function(...)
	if not triggerPower(...) then
		core.item_place(...)
	end
end
core.noneitemdef_default.on_use           = function(...) triggerPower(...) end
core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end
]]
core.register_item(":", {
	type = "none",
	wield_image = "wieldhand.png",
	wield_scale = {x=1,y=1,z=2.5},
	on_secondary_use = function(...) triggerPower(...) end;
-- 	on_use = function(...) print'base' end;
	after_use = function(i,u,n,p)
		if (u:is_player()) then triggerPower(i,u,p) end
	end;
})
core.register_item("starlit:_hand_dig", {
	type = "none",
	wield_image = "wieldhand.png",
	wield_scale = {x=1,y=1,z=2.5},
	tool_capabilities = {
		groupcaps = {
			object = {maxlevel=1, times = {.10,.20,.40}};
			plant = {maxlevel=1, times = {.50}};
................................................................................

			-- sand, dirt, gravel
			looseClump = {maxlevel=1, times = {1.5, 2.5}};
		};
	}
})

core.register_on_player_inventory_action(function(luser, act, inv, p)
	local name = luser:get_player_name()
	local user = starlit.activeUsers[name]
	-- allow UIs to update on UI changes
	local state = starlit.activeUI[name]
	if state then
		local ui = starlit.interface.db[state.form]
		ui:cb('onMoveItem', user, act, inv, p)
	end
end)

core.register_on_player_hpchange(function(luser, delta, cause)
	local user = starlit.activeUsers[luser:get_player_name()]
	if cause.type == 'fall' then
		delta = user:damageModifier('bluntForceTrauma', (delta * 50))
		-- justification: a short fall can do around
		-- five points of damage, which is nearly 50%
		-- of the default hp_max. since we crank up
		-- hp by a factor of 50~40, damage should be
		-- cranked by similarly
	end
	return delta
end, true)

function core.handle_node_drops(pos, drops, digger)
	local function jitter(pos)
		local function r(x) return x+math.random(-0.01, 0.01) end
		return vector.new(
			r(pos.x),
			r(pos.y),
			r(pos.z)
		)
	end
	for i, it in ipairs(drops) do
		if type(it) == 'string' then it = ItemStack(it) end
		if not it:is_empty() then
			local ent = core.add_item(jitter(pos), it)
			if ent ~= nil then -- avoid crash when dropping unknown item
				local dp = vector.new(0,0,0)
				if digger then dp = digger:get_pos() end
				local delta = dp - ent:get_pos()
				ent:add_velocity(vector.new(delta.x,0,delta.z));
			end
		end
	end
end


-- TODO timer iterates live UI

Modified mods/starlit/interfaces.lua from [42263b381c] to [7199aa7738].

565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
				if q.quit then
					user:suitSound 'starlit-quit' -- TODO better sound
				end
			end;
			render = function(state, user)
				local body = {kind='vert', w=6; mode='hw', spacing=.5, padding=1 }
				for i, l in ipairs(state.ctx.inv) do
					local inv = minetest.get_meta(l.pos):get_inventory()
					local w = l.w or 6
					if l.label then
						table.insert(body, {kind = 'hbar'; text = l.label, w=w+.5, h = .5})
					end
					table.insert(body, {kind = 'list';
						w = w, h = inv:get_size(l.id)/w;
						node = l.pos, inv = l.id;







|







565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
				if q.quit then
					user:suitSound 'starlit-quit' -- TODO better sound
				end
			end;
			render = function(state, user)
				local body = {kind='vert', w=6; mode='hw', spacing=.5, padding=1 }
				for i, l in ipairs(state.ctx.inv) do
					local inv = core.get_meta(l.pos):get_inventory()
					local w = l.w or 6
					if l.label then
						table.insert(body, {kind = 'hbar'; text = l.label, w=w+.5, h = .5})
					end
					table.insert(body, {kind = 'list';
						w = w, h = inv:get_size(l.id)/w;
						node = l.pos, inv = l.id;

Modified mods/starlit/species.lua from [e0959a01e2] to [236ca7459a].

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
				end
			elseif ctx.how.state == 'prog' then
				local d = ctx.how.delta
				local p = user.action.prog.sprint
				-- is the player currently holding any of WASD
				local isMoving = bit.band(0x0f, user.entity:get_player_control_bits()) ~= 0
				if p and isMoving then
					user.cooldownTimes.stamina = minetest.get_gametime()
					p.cb = p.cb + cost*d
					if p.cb >= 5 then
						user:statDelta('stamina', -p.cb)
						p.cb = 0
						if user:effectiveStat 'stamina' < cost then halt() end
					end
				end







|







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
				end
			elseif ctx.how.state == 'prog' then
				local d = ctx.how.delta
				local p = user.action.prog.sprint
				-- is the player currently holding any of WASD
				local isMoving = bit.band(0x0f, user.entity:get_player_control_bits()) ~= 0
				if p and isMoving then
					user.cooldownTimes.stamina = core.get_gametime()
					p.cb = p.cb + cost*d
					if p.cb >= 5 then
						user:statDelta('stamina', -p.cb)
						p.cb = 0
						if user:effectiveStat 'stamina' < cost then halt() end
					end
				end

Modified mods/starlit/suit.lua from [963193f502] to [d40c92ebbb].

195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
...
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
...
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
...
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
				end

				a.suit[name] = img
			end
		end
	end

	minetest.register_tool(id, {
		short_description = def.name;
		description = starlit.ui.tooltip {
			title = def.name;
			desc = def.desc;
			color = lib.color(.1, .7, 1);
		};
		groups = {
................................................................................
	};
	starlit_suit_canisters = {
		suitSlot = true;
		itemClass = 'canister';
	};
}

minetest.register_allow_player_inventory_action(function(luser, act, inv, p)
	local user = starlit.activeUsers[luser:get_player_name()]
	local function grp(i,g)
		return minetest.get_item_group(i:get_name(), g) ~= 0
	end
	local function checkBaseRestrictions(list)
		local restrictions = slotProps[list]
		if not restrictions then return nil, true end
		if restrictions.suitSlot then
			if user:naked() then return restrictions, false end
		end
................................................................................
		return true
	end
	local function itemCanLeave(item, list)
		local rst, ok = checkBaseRestrictions(list)
		if not ok then return false end
		if rst == nil then return true end

		if minetest.get_item_group(item:get_name(), 'specialInventory') then

		end

		if rst.maintenanceNode then return false end
		return true
	end

................................................................................
		if not itemFits(p.stack, p.listname) then return 0 end
	elseif act == 'take' then
		if not itemCanLeave(p.stack, p.listname) then return 0 end
	end
	return true
end)

minetest.register_on_player_inventory_action(function(luser, act, inv, p)
	local user = starlit.activeUsers[luser:get_player_name()]
	local function slotChange(slot,a,item)
		local s = slotProps[slot]
		if slot == 'starlit_suit' then
			user:updateSuit()
			if user:naked() then
				starlit.type.suit.purgeInventories(user.entity)







|







 







|


|







 







|







 







|







195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
...
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
...
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
...
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
				end

				a.suit[name] = img
			end
		end
	end

	core.register_tool(id, {
		short_description = def.name;
		description = starlit.ui.tooltip {
			title = def.name;
			desc = def.desc;
			color = lib.color(.1, .7, 1);
		};
		groups = {
................................................................................
	};
	starlit_suit_canisters = {
		suitSlot = true;
		itemClass = 'canister';
	};
}

core.register_allow_player_inventory_action(function(luser, act, inv, p)
	local user = starlit.activeUsers[luser:get_player_name()]
	local function grp(i,g)
		return core.get_item_group(i:get_name(), g) ~= 0
	end
	local function checkBaseRestrictions(list)
		local restrictions = slotProps[list]
		if not restrictions then return nil, true end
		if restrictions.suitSlot then
			if user:naked() then return restrictions, false end
		end
................................................................................
		return true
	end
	local function itemCanLeave(item, list)
		local rst, ok = checkBaseRestrictions(list)
		if not ok then return false end
		if rst == nil then return true end

		if core.get_item_group(item:get_name(), 'specialInventory') then

		end

		if rst.maintenanceNode then return false end
		return true
	end

................................................................................
		if not itemFits(p.stack, p.listname) then return 0 end
	elseif act == 'take' then
		if not itemCanLeave(p.stack, p.listname) then return 0 end
	end
	return true
end)

core.register_on_player_inventory_action(function(luser, act, inv, p)
	local user = starlit.activeUsers[luser:get_player_name()]
	local function slotChange(slot,a,item)
		local s = slotProps[slot]
		if slot == 'starlit_suit' then
			user:updateSuit()
			if user:naked() then
				starlit.type.suit.purgeInventories(user.entity)

Modified mods/starlit/terrain.lua from [eeb268d7fe] to [6e0c0d7cf5].

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
..
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
...
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
...
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
...
303
304
305
306
307
308
309
310
311
312
313
	max_items = 3;
	items = {
		{rarity = 2, items = {'starlit:soil_clump'}};
		{rarity = 3, items = {'starlit:soil_clump'}};
		{rarity = 4, items = {'starlit:soil_clump'}};
	};
}
minetest.register_node('starlit:soil', {
	description = T 'Soil';
	tiles = {'starlit-terrain-soil.png'};
	groups = {looseClump = 2, soil = 1};
	drop = soilDrop;
	sounds = soilSounds;
	_starlit = {
		kind = 'block';
		elements = {};
		recover = soilRec;
	};
})


minetest.register_node('starlit:sand', {
	description = T 'Sand';
	tiles = {'starlit-terrain-sand.png'};
	groups = {looseClump = 1, sand = 1, falling_node = 1};
	drop = '';
	sounds = sandSounds;
	_starlit = {
		kind = 'block';
		fab = starlit.type.fab { element = { silicon = 25 } };
	};
})

minetest.register_node('starlit:lifesilt', {
	description = T 'Lifesilt';
	tiles = {'starlit-terrain-lifesilt.png'};
	groups = {looseClump = 1, lifesilt = 1, falling_node = 1};
	drop = '';
	sounds = sandSounds;
	_starlit = {
		kind = 'block';
		recover = starlit.type.fab { element = { carbon = 16, silicon = 16, rubidium = 4 } };
	};
})

minetest.register_craftitem('starlit:soil_clump', {
	short_description = T 'Soil';
	description = starlit.ui.tooltip {
		title = T 'Soil';
		desc = 'A handful of nutrient-packed soil, suitable for growing plants';
		color = lib.color(0.3,0.2,0.1);
	};
	inventory_image = 'starlit-item-soil.png';
	groups = {looseClump = 2, soil = 1};
	on_place = function(me, luser, point)
		if me:get_count() < 3 then return end
		if minetest.place_node(point.above, {name = 'starlit:soil'}, luser) then
			me:take_item(3)
		end
		return me
	end;
	_starlit = {
		recover = starlit.type.fab { element = { carbon = 25 / 4 } };
	};
................................................................................
		items = {
			{
				items = {'starlit:soil'}, rarity = 2;
				tool_groups = { 'shovel', 'trowel' };
			};
		};
	}
	minetest.register_node(def.name, {
		description = T 'Greengraze';
		tiles = {
			def.img .. '.png';
			'starlit-terrain-soil.png';
			{
				name = 'starlit-terrain-soil.png^' .. def.img ..'-overlay.png';
				tileable_vertical = false;
................................................................................
	desc = T 'Undergloam';
	-- fungal carpet
	img = 'starlit-terrain-undergloam';
	recover = soilRec;
}

for _, w in pairs {false,true} do
	minetest.register_node('starlit:liquid_water' .. (w and '_flowing' or ''), {
		description = T 'Water';
		drawtype = 'liquid';
		waving = 3;
		tiles = {
			{
-- 				name = "default_water_source_animated.png";
				name = "starlit-water.png";
................................................................................
		groups = {water = 3, liquid = 3};
	});
end


starlit.world.mineral.foreach('starlit:mineral-generate', {}, function(node,m)
	local grp = {mineral = 1}
	minetest.register_node(node, {
		short_description = m.name;
		description = starlit.ui.tooltip {
			title = m.name;
			desc = m.desc;
			color = m.tone;
		};
		tiles = m.tiles or 
................................................................................
	})
	if not m.excludeOre then
		local seed = 0
-- 		grp.ore = 1
		for i = 1, #m.name do
			seed = seed*50 + string.byte(m.name, i)
		end
		minetest.register_ore {
			ore = node;
			ore_type = m.dist.kind;
			wherein = m.dist.among;
			clust_scarcity = m.dist.rare;
			clust_num_ores = m.dist.ores;
			clust_size = m.dist.clust;
			y_min = m.dist.height[1], y_max = m.dist.height[2];
................................................................................
		time = { shred = 3; };
		cost = { shredPower = 3; };
	};
})

-- map generation

minetest.register_alias('mapgen_stone', 'starlit:mineral_feldspar')
minetest.register_alias('mapgen_water_source', 'starlit:liquid_water')
minetest.register_alias('mapgen_river_water_source', 'starlit:liquid_water')








|













|











|











|










|







 







|







 







|







 







|







 







|







 







|
|
|

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
..
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
...
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
...
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
...
303
304
305
306
307
308
309
310
311
312
313
	max_items = 3;
	items = {
		{rarity = 2, items = {'starlit:soil_clump'}};
		{rarity = 3, items = {'starlit:soil_clump'}};
		{rarity = 4, items = {'starlit:soil_clump'}};
	};
}
core.register_node('starlit:soil', {
	description = T 'Soil';
	tiles = {'starlit-terrain-soil.png'};
	groups = {looseClump = 2, soil = 1};
	drop = soilDrop;
	sounds = soilSounds;
	_starlit = {
		kind = 'block';
		elements = {};
		recover = soilRec;
	};
})


core.register_node('starlit:sand', {
	description = T 'Sand';
	tiles = {'starlit-terrain-sand.png'};
	groups = {looseClump = 1, sand = 1, falling_node = 1};
	drop = '';
	sounds = sandSounds;
	_starlit = {
		kind = 'block';
		fab = starlit.type.fab { element = { silicon = 25 } };
	};
})

core.register_node('starlit:lifesilt', {
	description = T 'Lifesilt';
	tiles = {'starlit-terrain-lifesilt.png'};
	groups = {looseClump = 1, lifesilt = 1, falling_node = 1};
	drop = '';
	sounds = sandSounds;
	_starlit = {
		kind = 'block';
		recover = starlit.type.fab { element = { carbon = 16, silicon = 16, rubidium = 4 } };
	};
})

core.register_craftitem('starlit:soil_clump', {
	short_description = T 'Soil';
	description = starlit.ui.tooltip {
		title = T 'Soil';
		desc = 'A handful of nutrient-packed soil, suitable for growing plants';
		color = lib.color(0.3,0.2,0.1);
	};
	inventory_image = 'starlit-item-soil.png';
	groups = {looseClump = 2, soil = 1};
	on_place = function(me, luser, point)
		if me:get_count() < 3 then return end
		if core.place_node(point.above, {name = 'starlit:soil'}, luser) then
			me:take_item(3)
		end
		return me
	end;
	_starlit = {
		recover = starlit.type.fab { element = { carbon = 25 / 4 } };
	};
................................................................................
		items = {
			{
				items = {'starlit:soil'}, rarity = 2;
				tool_groups = { 'shovel', 'trowel' };
			};
		};
	}
	core.register_node(def.name, {
		description = T 'Greengraze';
		tiles = {
			def.img .. '.png';
			'starlit-terrain-soil.png';
			{
				name = 'starlit-terrain-soil.png^' .. def.img ..'-overlay.png';
				tileable_vertical = false;
................................................................................
	desc = T 'Undergloam';
	-- fungal carpet
	img = 'starlit-terrain-undergloam';
	recover = soilRec;
}

for _, w in pairs {false,true} do
	core.register_node('starlit:liquid_water' .. (w and '_flowing' or ''), {
		description = T 'Water';
		drawtype = 'liquid';
		waving = 3;
		tiles = {
			{
-- 				name = "default_water_source_animated.png";
				name = "starlit-water.png";
................................................................................
		groups = {water = 3, liquid = 3};
	});
end


starlit.world.mineral.foreach('starlit:mineral-generate', {}, function(node,m)
	local grp = {mineral = 1}
	core.register_node(node, {
		short_description = m.name;
		description = starlit.ui.tooltip {
			title = m.name;
			desc = m.desc;
			color = m.tone;
		};
		tiles = m.tiles or 
................................................................................
	})
	if not m.excludeOre then
		local seed = 0
-- 		grp.ore = 1
		for i = 1, #m.name do
			seed = seed*50 + string.byte(m.name, i)
		end
		core.register_ore {
			ore = node;
			ore_type = m.dist.kind;
			wherein = m.dist.among;
			clust_scarcity = m.dist.rare;
			clust_num_ores = m.dist.ores;
			clust_size = m.dist.clust;
			y_min = m.dist.height[1], y_max = m.dist.height[2];
................................................................................
		time = { shred = 3; };
		cost = { shredPower = 3; };
	};
})

-- map generation

core.register_alias('mapgen_stone', 'starlit:mineral_feldspar')
core.register_alias('mapgen_water_source', 'starlit:liquid_water')
core.register_alias('mapgen_river_water_source', 'starlit:liquid_water')

Modified mods/starlit/ui.lua from [01dfc2a3de] to [3ce8488360].

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
..
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
			return state, created
		end;
		render = function(self, state, user)
			return self.pages[state.page].render(state, user)
		end;
		show = function(self, user)
			local state = self:begin(user)
			minetest.show_formspec(user.name, self.id,self:render(state, user))
		end;
		open = function(self, user, page, ...)
			user:suitSound 'starlit-nav'
			self:begin(user, page, ...)
			self:show(user)
		end;
		close = function(self, user)
			local state = starlit.activeUI[user.name]
			if state and state.form == self.id then
				self:cb('onClose', user)
				starlit.activeUI[user.name] = nil
				minetest.close_formspec(user.name, self.id)
			end
		end;
	};
	construct = function(p)
		if not p.id then error('UI missing id') end
		p.pages = p.pages or {}
		return p
................................................................................
			hue = 260, sat = 0, lum = 0
		};
	}
	local lines = state.lines
	local cmod = string.format('^[hsl:%s:%s:%s',
		state.color.hue, state.color.sat*0xff, state.color.lum*0xff)

	local E = minetest.formspec_escape
	if state.padding/2 > state.x then state.x = state.padding/2 end
	if state.padding/2 > state.y then state.y = state.padding/2 end

	local function btnColorDef(sel)
		local function climg(state,img)
			local selstr
			if sel == nil then







|











|







 







|







46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
..
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
			return state, created
		end;
		render = function(self, state, user)
			return self.pages[state.page].render(state, user)
		end;
		show = function(self, user)
			local state = self:begin(user)
			core.show_formspec(user.name, self.id,self:render(state, user))
		end;
		open = function(self, user, page, ...)
			user:suitSound 'starlit-nav'
			self:begin(user, page, ...)
			self:show(user)
		end;
		close = function(self, user)
			local state = starlit.activeUI[user.name]
			if state and state.form == self.id then
				self:cb('onClose', user)
				starlit.activeUI[user.name] = nil
				core.close_formspec(user.name, self.id)
			end
		end;
	};
	construct = function(p)
		if not p.id then error('UI missing id') end
		p.pages = p.pages or {}
		return p
................................................................................
			hue = 260, sat = 0, lum = 0
		};
	}
	local lines = state.lines
	local cmod = string.format('^[hsl:%s:%s:%s',
		state.color.hue, state.color.sat*0xff, state.color.lum*0xff)

	local E = core.formspec_escape
	if state.padding/2 > state.x then state.x = state.padding/2 end
	if state.padding/2 > state.y then state.y = state.padding/2 end

	local function btnColorDef(sel)
		local function climg(state,img)
			local selstr
			if sel == nil then

Modified mods/starlit/user.lua from [9accce5f34] to [1a0e392600].

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
...
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
...
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
...
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
...
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
...
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
...
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
...
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
....
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
....
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
....
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
....
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
....
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
....
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
....
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
--    client. it provides for initial signup and join,
--    managing the HUD, skinning the player model,
--    effecting weather changes, etc.

local lib = starlit.mod.lib

local function hudAdjustBacklight(img)
	local night = math.abs(minetest.get_timeofday() - .5) * 2
	local opacity = night*0.8
	return img:fade(opacity)
end

local userStore = lib.marshal.metaStore {
	persona = {
		key  = 'starlit:persona';
................................................................................
starlit.type.user = lib.class {
	name = 'starlit:user';
	leds = leds;
	construct = function(ident)
		local name, luser
		if type(ident) == 'string' then
			name = ident
			luser = minetest.get_player_by_name(name)
		else
			luser = ident
			name = luser:get_player_name()
		end
		return {
			entity = luser;
			name = name;
................................................................................
		}
	end;
	__index = {
		--------------
		-- overlays --
		--------------
		updateOverlays = function(self)
			-- minetest: because fuck you, that's why
			local engineGravity = starlit.constant.phys.engineGravity
			local targetGravity = starlit.world.planet.gravity
			local phys = {
				speed = self.pheno:trait('speed',1);
				jump = self.pheno:trait('jump',1);
				gravity = targetGravity / engineGravity;
				speed_climb = 1;
................................................................................
			self.hud.elt.time = self:attachTextBox {
				name = 'time';
				align = {x=0, y=1};
				pos = {x=0.5, y=1};
				ofs = {x=0,y=-95};
				text = function(user)
					local cal = starlit.world.time.calendar[user.pref.calendar]
					return cal.time(minetest.get_timeofday())
				end;
			}
			self.hud.elt.temp = self:attachMeter {
				name = 'temp';
				align = {x=1, y=-1};
				pos = {x=0, y=1};
				ofs = {x=20, y=-20};
................................................................................
		updateHUD = function(self)
			for name, e in pairs(self.hud.elt) do
				if e.update then e.update() end
			end
			self:updateLEDs()
		end;
		updateLEDs = function(self)
			local time = minetest.get_gametime()
			local function updateSide(name, ofs, tx)
				local del = {}
				local idx = 0
				for i, l in ipairs(self.hud.led[name]) do
					if time - l.origin > 3 then
						if l.elt then self.entity:hud_remove(l.elt.id) end
						self.hud.led.map[l.kind] = nil
................................................................................
			inv:set_stack('hand', 1, hnd)
		end;

		---------------------
		-- intel-gathering --
		---------------------
		clientInfo = function(self)
			return minetest.get_player_information(self.name)
		end;
		species = function(self)
			return starlit.world.species.index[self.persona.species]
		end;
		-- can the suit heater sustain its current internal temperature in an area of t°C
		tempCanSustain = function(self, t)
			if self:naked() then return false end
................................................................................

			-- i feel like there has to be a better way
			local posrng = starlit.world.seedbank[0x13f19] -- TODO player-specific seed
			local cx = posrng:int(-500,500) --math.random(-500,500)
			local iter, startPoint = 1
			repeat local temp = -100
				local cz = posrng:int(-500,500)
				local cy = minetest.get_spawn_level(cx, cz)
				if cy then
					startPoint = vector.new(cx,cy,cz)
					temp = starlit.world.climate.eval(startPoint,.5,.5).surfaceTemp
				end
				iter = iter + 1
				if iter > 100 then break end -- avoid infiniloop in pathological conditions
			until temp > -2
................................................................................
		onDie = function(self, reason)
			local inv = self.entity:get_inventory()
			local where = self.entity:get_pos()
			local function dropInv(lst)
				local l = inv:get_list(lst)
				for i, o in ipairs(l) do
					if o and not o:is_empty() then
						minetest.item_drop(o, self.entity, where)
					end
				end
				inv:set_list(lst, {})
			end
			dropInv 'main'
			dropInv 'starlit_suit'
			self:updateSuit()
................................................................................
		-- environment suit & body --
		-----------------------------
		suitStack = function(self)
			return self.entity:get_inventory():get_stack('starlit_suit', 1)
		end;
		suitSound = function(self, sfx)
			-- trigger a sound effect from the player's suit computer
			minetest.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true)
		end;
		suitPowerStateSet = function(self, state, silent)
			-- necessary to enable reacting to power state changes
			-- e.g. to play sound effects, display warnings
			local os
			self:forSuit(function(s)
				os=s:powerState()
................................................................................
				return true
			end
			return false
		end;

		alarm = function(self, urgency, kind, minFreq)
			minFreq = minFreq or 1.5
			local time = minetest.get_gametime()
			local led = leds[kind]

			local ul = self.hud.led.map[kind]
			if ul then
				if time - ul.origin > minFreq then
					ul.origin = time
				else return end
................................................................................

		--[[
			freq = freq or 3
			local urgencies = {
				[1] = {sound = 'starlit-alarm'};
				[2] = {sound = 'starlit-alarm-urgent'};
			}
		   local gt = minetest.get_gametime()
		   local urg = urgencies[urgency] or urgencies[#urgencies]

		   if gt - self.cooldownTimes.alarm < freq then return end

		   self.cooldownTimes.alarm = gt
		   self:suitSound(urg.sound)

................................................................................
			   }
			   elt.ofs.x = elt.ofs.x + where.ofs.x
			   elt.ofs.y = elt.ofs.y + where.ofs.y
			   local attached = self:attachImage(elt)
				table.insert(self.hud.alarm, attached)

			   -- HATE. HATE. HAAAAAAAAAAATE
			   minetest.after(freq/2, function()
				   for k,v in pairs(self.hud.alarm) do
					   self.entity:hud_remove(v.id)
				   end
				   self.hud.alarm={}
			   end)
		   end]]
	   end;
................................................................................
		---------------
		-- inventory --
		---------------
		give = function(self, item)
			item = ItemStack(item)
			local inv = self.entity:get_inventory()
			local function is(grp)
				return minetest.get_item_group(item:get_name(), grp) ~= 0
			end
			-- TODO notif popups
			if is 'specialInventory' then
			--[[
				if is 'powder' then
					if self:naked() then return item end
					local cans = inv:get_list 'starlit_suit_canisters'
................................................................................
			else
				return inv:add_item('main', item)
			end
		end;
		thrustUpon = function(self, item)
			local r = self:give(st)
			if not r:is_empty() then
				return minetest.add_item(self.entity:get_pos(), r)
			end
		end;
		consume = function(self, stack, n)
			n = n or 1
			if n == 0 then n = stack:get_count() end
			local fd = stack:take_item(n)
			local stats = starlit.world.food.effectiveStats(fd)
................................................................................
				u:statDelta('health', -5*biointerval)
			end

			if water == 0 then -- dying of thirst
				u:statDelta('health', -20*biointerval)
			end

			if sp < 1.0 and minetest.get_gametime() - u.cooldownTimes.stamina > 5.0 then
				u:statDelta('stamina', (u:phenoTrait('staminaRegen',1) * penaltyFromFatigue) / heatPenalty)
-- 				print('stam', u:effectiveStat 'stamina', u:phenoTrait('staminaRegen',1) / heatPenalty, heatPenalty)
			end

			local morale, mp = u:effectiveStat 'morale'
			local pr = u:phenoTrait 'numinaRegen'
			u:statDelta('numina', pr * penaltyFromFatigue * mp)
................................................................................
	manv = 0x020;
	snk  = 0x040;
	dig  = 0x080;
	put  = 0x100;
	zoom = 0x200;
}
-- this is the painful part
minetest.register_globalstep(function(delta)
	local doNothing,mustInit,mustHalt = 0,1,2
	for id, user in pairs(starlit.activeUsers) do
		local ent = user.entity
		local bits = ent:get_player_control_bits()

		local function what(b)
			if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
...
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
...
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
...
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
...
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
...
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
...
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
...
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
....
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
....
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
....
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
....
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
....
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
....
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
....
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
--    client. it provides for initial signup and join,
--    managing the HUD, skinning the player model,
--    effecting weather changes, etc.

local lib = starlit.mod.lib

local function hudAdjustBacklight(img)
	local night = math.abs(core.get_timeofday() - .5) * 2
	local opacity = night*0.8
	return img:fade(opacity)
end

local userStore = lib.marshal.metaStore {
	persona = {
		key  = 'starlit:persona';
................................................................................
starlit.type.user = lib.class {
	name = 'starlit:user';
	leds = leds;
	construct = function(ident)
		local name, luser
		if type(ident) == 'string' then
			name = ident
			luser = core.get_player_by_name(name)
		else
			luser = ident
			name = luser:get_player_name()
		end
		return {
			entity = luser;
			name = name;
................................................................................
		}
	end;
	__index = {
		--------------
		-- overlays --
		--------------
		updateOverlays = function(self)
			-- luanti: because fuck you, that's why
			local engineGravity = starlit.constant.phys.engineGravity
			local targetGravity = starlit.world.planet.gravity
			local phys = {
				speed = self.pheno:trait('speed',1);
				jump = self.pheno:trait('jump',1);
				gravity = targetGravity / engineGravity;
				speed_climb = 1;
................................................................................
			self.hud.elt.time = self:attachTextBox {
				name = 'time';
				align = {x=0, y=1};
				pos = {x=0.5, y=1};
				ofs = {x=0,y=-95};
				text = function(user)
					local cal = starlit.world.time.calendar[user.pref.calendar]
					return cal.time(core.get_timeofday())
				end;
			}
			self.hud.elt.temp = self:attachMeter {
				name = 'temp';
				align = {x=1, y=-1};
				pos = {x=0, y=1};
				ofs = {x=20, y=-20};
................................................................................
		updateHUD = function(self)
			for name, e in pairs(self.hud.elt) do
				if e.update then e.update() end
			end
			self:updateLEDs()
		end;
		updateLEDs = function(self)
			local time = core.get_gametime()
			local function updateSide(name, ofs, tx)
				local del = {}
				local idx = 0
				for i, l in ipairs(self.hud.led[name]) do
					if time - l.origin > 3 then
						if l.elt then self.entity:hud_remove(l.elt.id) end
						self.hud.led.map[l.kind] = nil
................................................................................
			inv:set_stack('hand', 1, hnd)
		end;

		---------------------
		-- intel-gathering --
		---------------------
		clientInfo = function(self)
			return core.get_player_information(self.name)
		end;
		species = function(self)
			return starlit.world.species.index[self.persona.species]
		end;
		-- can the suit heater sustain its current internal temperature in an area of t°C
		tempCanSustain = function(self, t)
			if self:naked() then return false end
................................................................................

			-- i feel like there has to be a better way
			local posrng = starlit.world.seedbank[0x13f19] -- TODO player-specific seed
			local cx = posrng:int(-500,500) --math.random(-500,500)
			local iter, startPoint = 1
			repeat local temp = -100
				local cz = posrng:int(-500,500)
				local cy = core.get_spawn_level(cx, cz)
				if cy then
					startPoint = vector.new(cx,cy,cz)
					temp = starlit.world.climate.eval(startPoint,.5,.5).surfaceTemp
				end
				iter = iter + 1
				if iter > 100 then break end -- avoid infiniloop in pathological conditions
			until temp > -2
................................................................................
		onDie = function(self, reason)
			local inv = self.entity:get_inventory()
			local where = self.entity:get_pos()
			local function dropInv(lst)
				local l = inv:get_list(lst)
				for i, o in ipairs(l) do
					if o and not o:is_empty() then
						core.item_drop(o, self.entity, where)
					end
				end
				inv:set_list(lst, {})
			end
			dropInv 'main'
			dropInv 'starlit_suit'
			self:updateSuit()
................................................................................
		-- environment suit & body --
		-----------------------------
		suitStack = function(self)
			return self.entity:get_inventory():get_stack('starlit_suit', 1)
		end;
		suitSound = function(self, sfx)
			-- trigger a sound effect from the player's suit computer
			core.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true)
		end;
		suitPowerStateSet = function(self, state, silent)
			-- necessary to enable reacting to power state changes
			-- e.g. to play sound effects, display warnings
			local os
			self:forSuit(function(s)
				os=s:powerState()
................................................................................
				return true
			end
			return false
		end;

		alarm = function(self, urgency, kind, minFreq)
			minFreq = minFreq or 1.5
			local time = core.get_gametime()
			local led = leds[kind]

			local ul = self.hud.led.map[kind]
			if ul then
				if time - ul.origin > minFreq then
					ul.origin = time
				else return end
................................................................................

		--[[
			freq = freq or 3
			local urgencies = {
				[1] = {sound = 'starlit-alarm'};
				[2] = {sound = 'starlit-alarm-urgent'};
			}
		   local gt = core.get_gametime()
		   local urg = urgencies[urgency] or urgencies[#urgencies]

		   if gt - self.cooldownTimes.alarm < freq then return end

		   self.cooldownTimes.alarm = gt
		   self:suitSound(urg.sound)

................................................................................
			   }
			   elt.ofs.x = elt.ofs.x + where.ofs.x
			   elt.ofs.y = elt.ofs.y + where.ofs.y
			   local attached = self:attachImage(elt)
				table.insert(self.hud.alarm, attached)

			   -- HATE. HATE. HAAAAAAAAAAATE
			   core.after(freq/2, function()
				   for k,v in pairs(self.hud.alarm) do
					   self.entity:hud_remove(v.id)
				   end
				   self.hud.alarm={}
			   end)
		   end]]
	   end;
................................................................................
		---------------
		-- inventory --
		---------------
		give = function(self, item)
			item = ItemStack(item)
			local inv = self.entity:get_inventory()
			local function is(grp)
				return core.get_item_group(item:get_name(), grp) ~= 0
			end
			-- TODO notif popups
			if is 'specialInventory' then
			--[[
				if is 'powder' then
					if self:naked() then return item end
					local cans = inv:get_list 'starlit_suit_canisters'
................................................................................
			else
				return inv:add_item('main', item)
			end
		end;
		thrustUpon = function(self, item)
			local r = self:give(st)
			if not r:is_empty() then
				return core.add_item(self.entity:get_pos(), r)
			end
		end;
		consume = function(self, stack, n)
			n = n or 1
			if n == 0 then n = stack:get_count() end
			local fd = stack:take_item(n)
			local stats = starlit.world.food.effectiveStats(fd)
................................................................................
				u:statDelta('health', -5*biointerval)
			end

			if water == 0 then -- dying of thirst
				u:statDelta('health', -20*biointerval)
			end

			if sp < 1.0 and core.get_gametime() - u.cooldownTimes.stamina > 5.0 then
				u:statDelta('stamina', (u:phenoTrait('staminaRegen',1) * penaltyFromFatigue) / heatPenalty)
-- 				print('stam', u:effectiveStat 'stamina', u:phenoTrait('staminaRegen',1) / heatPenalty, heatPenalty)
			end

			local morale, mp = u:effectiveStat 'morale'
			local pr = u:phenoTrait 'numinaRegen'
			u:statDelta('numina', pr * penaltyFromFatigue * mp)
................................................................................
	manv = 0x020;
	snk  = 0x040;
	dig  = 0x080;
	put  = 0x100;
	zoom = 0x200;
}
-- this is the painful part
core.register_globalstep(function(delta)
	local doNothing,mustInit,mustHalt = 0,1,2
	for id, user in pairs(starlit.activeUsers) do
		local ent = user.entity
		local bits = ent:get_player_control_bits()

		local function what(b)
			if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then

Modified mods/starlit/world.lua from [de51a702a5] to [e94ad91b55].

1
2
3
4
5
6
7
8
9
10
11
12
..
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
..
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
...
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
...
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
...
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
...
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
local lib = starlit.mod.lib
local world = starlit.world

function world.date()
	local days = minetest.get_day_count()
	local year = math.floor(days / world.planet.orbit);
	local day = days % world.planet.orbit;
	return {
		year = year, day = day;
		season = day / world.planet.orbit + 0.5; -- begin summer
	}
end
................................................................................
local heatRange = {min = -50, max = 50} -- translate mt temps into real temps

-- this function provides the basis for temperature calculation,
-- which is performed by adding this value to the ambient temperature,
-- determined by querying nearby group:heatSource items in accordance
-- with the inverse-square law
function world.climate.eval(pos, tod, season)
	local data = minetest.get_biome_data(pos)
	local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)]
-- 	print('climate:', dump(data))
	local heat, humid = data.heat, data.humidity
	heat = lerp(heat/100, heatRange.min, heatRange.max)
	tod = tod or minetest.get_timeofday()
	heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)
-- 	print('base heat', heat)

	local td = world.date()
	heat = heat + gradient(biome.seasonalTemp, season or td.season)
-- 	print('seasonal heat', heat)
	if pos.y > 0 then
................................................................................

local vdsq = lib.math.vdsq
function world.climate.temp(pos, timeshift) --> irradiance at pos in W
	local cl = world.climate.eval(pos)
	local radCenters = starlit.region.radiator.store:get_areas_for_pos(pos, false, true)
	local irradiance = 0
	for _,e in pairs(radCenters) do
		local rpos = minetest.string_to_pos(e.data)
		local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name])
		local rc = rdef._starlit.radiator
		local r_max = rc.radius(rpos)

		local dist_sq = vdsq(rpos,pos)
		if dist_sq <= r_max^2 then
			-- cheap bad way
			-- if minetest.line_of_sight(rpos,pos) then
			--
			-- expensive way
			local obstruct = 0
			local ray = Raycast(rpos, pos, true, true)
			for p in ray do
				if p.type == 'node' then obstruct = obstruct + 1 end
			end
................................................................................
	end
	local w = world.climate.weatherAt(pos, timeshift)

	return irradiance + cl.surfaceTemp
end

function world.ecology.biomeAt(pos)
	return world.ecology.biomes.db[minetest.get_biome_name(minetest.get_biome_data(pos).biome)]
end


minetest.after(0, function()
	world.climate.weatherMap.kind = minetest.get_perlin {
		seed = 0x925afe;
		octaves = 2;
		spread = vector.new(256,256,120);
	};
	world.climate.weatherMap.severity = minetest.get_perlin {
		seed = 0x39de1d;
		octaves = 1;
		spread = vector.new(256,256,60);
	};
end)

function world.climate.weatherAt(pos, timeshift)
	timeshift = timeshift or 0
	local wv  = world.climate.weatherMap.kind:get_3d(vector.new(pos.x, pos.z, minetest.get_gametime() + timeshift))
	local sev = world.climate.weatherMap.severity:get_3d(vector.new(pos.x, pos.z, minetest.get_gametime() + timeshift))
	local b = world.ecology.biomeAt(pos)
	local w = 'starlit:clear'
	for i,v in ipairs(b.weather) do
		if wv < v[1] then
			w = v[2]
			break
		end
................................................................................
world.climate.weather.link('starlit:meteorShower', {
	name = 'Meteor Shower';
	danger = 2;
})

world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b)
	b.def.name = id
	minetest.register_biome(b.def)
end)

world.ecology.plants.foreach('starlit:plant-gen', {}, function(id, b)
	local stageCt = #b.stages
	local function stageID(n)
		if n == stageCt then return id end
		return id .. string.format('_stage_%s', n)
................................................................................
				end;
			};
		}
		if st.swap then
			base.node_dig_prediction = ""
			function base.after_dig_node(pos, node, digger)
				node.name = stageID(st.swap)
				minetest.swap_node(pos, node)
				return true
			end
		end
		if st.biolum then base.light_source = st.biolum; end
		return base
	end
	for i, v in ipairs(b.stages) do
		local n = regStage(i, v)
		minetest.register_node(stageID(i), n)
		b.stageNodes[i] = stageID(i)
	end
	b.fullyGrown = stageID(stageCt)

	local dec = {
		deco_type = 'simple';
		decoration = b.stageNodes;
		height = 1;
		param2 = b.meshOpt or 0;
	}
	for k,v in pairs(b.decoration) do dec[k] = v end
	b.decoration = minetest.register_decoration(dec)
end)

local toward = lib.math.toward
local hfinterval = 1.5
starlit.startJob('starlit:temps', hfinterval, function(delta)

	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
................................................................................
	--   d  = Tₑ − Tₚ = -40°C
	--   ΔT = κ×d = -.4°C/s
	-- too cold:
	--		x = beginning of danger zone
	--    κ × (x - Tₚ) = y where y < Tₚ
	-- our final change in temperature is computed as tΔC where t is time
	local kappa = starlit.constant.heat.thermalConductivity
	local now = minetest.get_gametime()
	for name,user in pairs(starlit.activeUsers) do
		local tr = user:species().tempRange
		local t = starlit.world.climate.temp(user.entity:get_pos())

		local weather,wsev = starlit.world.climate.weatherAt(user.entity:get_pos())
		local wfac
		if user.env.weather == nil
................................................................................
world.ecology.trees.foreach('starlit:tree-gen', {}, function(id, t)
	for i,td in ipairs(t.decorate) do
		local dec = {
			deco_type = 'lsystem';
			treedef = t.def;
		}
		for k,v in pairs(td) do dec[k]=v end
		minetest.register_decoration(dec)
	end
end)

minetest.register_abm {
	label = "plant growth";
	nodenames = {'group:plant_grow'};
	chance = 15;
	interval = 20;
	catch_up = true;
	action = function(pos, node)
		local def = minetest.registered_nodes[node.name]._starlit.plant
		-- 5 W: maximum power for UV lamps
		-- 7 W: maximum solar power
		local uv = (minetest.get_natural_light(pos) / 15) * 7
		-- TODO compute artificial contribution
		local req = lib.tbl.defaults({
			uv = 3;
			soil = 'soil';
			temp = -10;
			humid = nil;
		}, def.growReq);

		-- TODO check other reqs

		if uv > req.uv then
			local plant = starlit.world.ecology.plants.db[def.id]
			local nextStage = plant.stageNodes[def.stage + 1]
			minetest.swap_node(pos, {name=nextStage})
		end
	end;
}




|







 







|
|



|







 







|
|






|







 







|



|
|




|








|
|







 







|







 







|








|











|







 







|







 







|



|






|


|













|



1
2
3
4
5
6
7
8
9
10
11
12
..
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
..
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
...
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
...
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
...
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
...
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
local lib = starlit.mod.lib
local world = starlit.world

function world.date()
	local days = core.get_day_count()
	local year = math.floor(days / world.planet.orbit);
	local day = days % world.planet.orbit;
	return {
		year = year, day = day;
		season = day / world.planet.orbit + 0.5; -- begin summer
	}
end
................................................................................
local heatRange = {min = -50, max = 50} -- translate mt temps into real temps

-- this function provides the basis for temperature calculation,
-- which is performed by adding this value to the ambient temperature,
-- determined by querying nearby group:heatSource items in accordance
-- with the inverse-square law
function world.climate.eval(pos, tod, season)
	local data = core.get_biome_data(pos)
	local biome = world.ecology.biomes.db[core.get_biome_name(data.biome)]
-- 	print('climate:', dump(data))
	local heat, humid = data.heat, data.humidity
	heat = lerp(heat/100, heatRange.min, heatRange.max)
	tod = tod or core.get_timeofday()
	heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)
-- 	print('base heat', heat)

	local td = world.date()
	heat = heat + gradient(biome.seasonalTemp, season or td.season)
-- 	print('seasonal heat', heat)
	if pos.y > 0 then
................................................................................

local vdsq = lib.math.vdsq
function world.climate.temp(pos, timeshift) --> irradiance at pos in W
	local cl = world.climate.eval(pos)
	local radCenters = starlit.region.radiator.store:get_areas_for_pos(pos, false, true)
	local irradiance = 0
	for _,e in pairs(radCenters) do
		local rpos = core.string_to_pos(e.data)
		local rdef = assert(core.registered_nodes[assert(core.get_node(rpos)).name])
		local rc = rdef._starlit.radiator
		local r_max = rc.radius(rpos)

		local dist_sq = vdsq(rpos,pos)
		if dist_sq <= r_max^2 then
			-- cheap bad way
			-- if core.line_of_sight(rpos,pos) then
			--
			-- expensive way
			local obstruct = 0
			local ray = Raycast(rpos, pos, true, true)
			for p in ray do
				if p.type == 'node' then obstruct = obstruct + 1 end
			end
................................................................................
	end
	local w = world.climate.weatherAt(pos, timeshift)

	return irradiance + cl.surfaceTemp
end

function world.ecology.biomeAt(pos)
	return world.ecology.biomes.db[core.get_biome_name(core.get_biome_data(pos).biome)]
end


core.after(0, function()
	world.climate.weatherMap.kind = core.get_perlin {
		seed = 0x925afe;
		octaves = 2;
		spread = vector.new(256,256,120);
	};
	world.climate.weatherMap.severity = core.get_perlin {
		seed = 0x39de1d;
		octaves = 1;
		spread = vector.new(256,256,60);
	};
end)

function world.climate.weatherAt(pos, timeshift)
	timeshift = timeshift or 0
	local wv  = world.climate.weatherMap.kind:get_3d(vector.new(pos.x, pos.z, core.get_gametime() + timeshift))
	local sev = world.climate.weatherMap.severity:get_3d(vector.new(pos.x, pos.z, core.get_gametime() + timeshift))
	local b = world.ecology.biomeAt(pos)
	local w = 'starlit:clear'
	for i,v in ipairs(b.weather) do
		if wv < v[1] then
			w = v[2]
			break
		end
................................................................................
world.climate.weather.link('starlit:meteorShower', {
	name = 'Meteor Shower';
	danger = 2;
})

world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b)
	b.def.name = id
	core.register_biome(b.def)
end)

world.ecology.plants.foreach('starlit:plant-gen', {}, function(id, b)
	local stageCt = #b.stages
	local function stageID(n)
		if n == stageCt then return id end
		return id .. string.format('_stage_%s', n)
................................................................................
				end;
			};
		}
		if st.swap then
			base.node_dig_prediction = ""
			function base.after_dig_node(pos, node, digger)
				node.name = stageID(st.swap)
				core.swap_node(pos, node)
				return true
			end
		end
		if st.biolum then base.light_source = st.biolum; end
		return base
	end
	for i, v in ipairs(b.stages) do
		local n = regStage(i, v)
		core.register_node(stageID(i), n)
		b.stageNodes[i] = stageID(i)
	end
	b.fullyGrown = stageID(stageCt)

	local dec = {
		deco_type = 'simple';
		decoration = b.stageNodes;
		height = 1;
		param2 = b.meshOpt or 0;
	}
	for k,v in pairs(b.decoration) do dec[k] = v end
	b.decoration = core.register_decoration(dec)
end)

local toward = lib.math.toward
local hfinterval = 1.5
starlit.startJob('starlit:temps', hfinterval, function(delta)

	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
................................................................................
	--   d  = Tₑ − Tₚ = -40°C
	--   ΔT = κ×d = -.4°C/s
	-- too cold:
	--		x = beginning of danger zone
	--    κ × (x - Tₚ) = y where y < Tₚ
	-- our final change in temperature is computed as tΔC where t is time
	local kappa = starlit.constant.heat.thermalConductivity
	local now = core.get_gametime()
	for name,user in pairs(starlit.activeUsers) do
		local tr = user:species().tempRange
		local t = starlit.world.climate.temp(user.entity:get_pos())

		local weather,wsev = starlit.world.climate.weatherAt(user.entity:get_pos())
		local wfac
		if user.env.weather == nil
................................................................................
world.ecology.trees.foreach('starlit:tree-gen', {}, function(id, t)
	for i,td in ipairs(t.decorate) do
		local dec = {
			deco_type = 'lsystem';
			treedef = t.def;
		}
		for k,v in pairs(td) do dec[k]=v end
		core.register_decoration(dec)
	end
end)

core.register_abm {
	label = "plant growth";
	nodenames = {'group:plant_grow'};
	chance = 15;
	interval = 20;
	catch_up = true;
	action = function(pos, node)
		local def = core.registered_nodes[node.name]._starlit.plant
		-- 5 W: maximum power for UV lamps
		-- 7 W: maximum solar power
		local uv = (core.get_natural_light(pos) / 15) * 7
		-- TODO compute artificial contribution
		local req = lib.tbl.defaults({
			uv = 3;
			soil = 'soil';
			temp = -10;
			humid = nil;
		}, def.growReq);

		-- TODO check other reqs

		if uv > req.uv then
			local plant = starlit.world.ecology.plants.db[def.id]
			local nextStage = plant.stageNodes[def.stage + 1]
			core.swap_node(pos, {name=nextStage})
		end
	end;
}

Modified mods/vtlib/color.lua from [d9e5a5527c] to [f14f6eaefb].

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
		end;

		int = function(self)
			return bit.bor(bit.lshift(self:int24(), 8), math.floor(0xff*(self.alpha or 1.0)))
		end;

		fmt = function(self, text) return
			minetest.colorize(self:hex(), text)
		end;

		bg = function(self, text) return
			text .. minetest.get_background_escape_sequence(self:hex())
		end;

		lum = function(self) return
			(self.red + self.green + self.blue) / 3
		end;

		pair = function(self) --> bg, fg







|



|







125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
		end;

		int = function(self)
			return bit.bor(bit.lshift(self:int24(), 8), math.floor(0xff*(self.alpha or 1.0)))
		end;

		fmt = function(self, text) return
			core.colorize(self:hex(), text)
		end;

		bg = function(self, text) return
			text .. core.get_background_escape_sequence(self:hex())
		end;

		lum = function(self) return
			(self.red + self.green + self.blue) / 3
		end;

		pair = function(self) --> bg, fg

Modified mods/vtlib/dbg.lua from [3d3bef9b5d] to [8f550c074a].

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local lastmod, lastarea

function dbg.debugger(area)
	local depth = 0
	local d = {}
	function d.enter() depth = depth+1 end
	function d.exit() depth = depth-1 end
	local mod = minetest.get_current_modname()
	if dbg.aloud then
		function d.report(fmt, ...)
			local where = debug.getinfo(2)
			local caller = debug.getinfo(3)
			if mod and (lastmod ~= mod or lastarea ~= area) then
				local ms = mod or ''
				if area then ms = ms .. '.' .. area end







|







6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local lastmod, lastarea

function dbg.debugger(area)
	local depth = 0
	local d = {}
	function d.enter() depth = depth+1 end
	function d.exit() depth = depth-1 end
	local mod = core.get_current_modname()
	if dbg.aloud then
		function d.report(fmt, ...)
			local where = debug.getinfo(2)
			local caller = debug.getinfo(3)
			if mod and (lastmod ~= mod or lastarea ~= area) then
				local ms = mod or ''
				if area then ms = ms .. '.' .. area end

Modified mods/vtlib/init.lua from [d021d078eb] to [d81d22bdc5].

1
2
3
4
5
6
7
8
9
local ident = minetest.get_current_modname()
local path = minetest.get_modpath(ident)

local lib = {}
_G[ident] = lib

local function
component(name)
	local p = string.format('%s/%s.lua', path, name)
|
|







1
2
3
4
5
6
7
8
9
local ident = core.get_current_modname()
local path = core.get_modpath(ident)

local lib = {}
_G[ident] = lib

local function
component(name)
	local p = string.format('%s/%s.lua', path, name)

Modified mods/vtlib/item.lua from [29827ac7a0] to [3c399f0296].

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
..
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
				count = b:get_count() - a:get_count();
			}
		end
	end
end

-- it is extremely unfortunate this function needs to exist.
-- minetest needs to export its matching capabilities already
fn.groupmatch = function(identity,item,exact)
	if exact == nil then exact = true end
	local count
	if type(identity) == 'table' then
		count = identity.count
		identity = identity.name
	else
................................................................................
	end

	if lib.str.beginswith(identity, 'group:') then
		local stack = ItemStack(item)
		local groups = lib.str.explode(string.sub(identity,7), ',')
		for _,g in pairs(groups) do
			local rn,rv = lib.tbl.split(g,'=')
			local gv = minetest.get_item_group(stack:get_name(), rn)
			if rv then
				if gv ~= tonumber(rv) then return false, stack end
			else
				if (not gv) or gv == 0 then return false, stack end
			end
		end








|







 







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
..
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
				count = b:get_count() - a:get_count();
			}
		end
	end
end

-- it is extremely unfortunate this function needs to exist.
-- luanti needs to export its matching capabilities already
fn.groupmatch = function(identity,item,exact)
	if exact == nil then exact = true end
	local count
	if type(identity) == 'table' then
		count = identity.count
		identity = identity.name
	else
................................................................................
	end

	if lib.str.beginswith(identity, 'group:') then
		local stack = ItemStack(item)
		local groups = lib.str.explode(string.sub(identity,7), ',')
		for _,g in pairs(groups) do
			local rn,rv = lib.tbl.split(g,'=')
			local gv = core.get_item_group(stack:get_name(), rn)
			if rv then
				if gv ~= tonumber(rv) then return false, stack end
			else
				if (not gv) or gv == 0 then return false, stack end
			end
		end

Modified mods/vtlib/marshal.lua from [979397aff5] to [63566b8bdf].

91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
	end

	return encoder
end

function m.metaStore(map, prefix)
	report('generating metaStore for %s', dump(map))
	if prefix == true then prefix = minetest.get_current_modname() end
	local function keyFor(k)
		k = map[k].key
		if prefix then return prefix .. ':' .. k end
		return k
	end

	return function(obj)







|







91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
	end

	return encoder
end

function m.metaStore(map, prefix)
	report('generating metaStore for %s', dump(map))
	if prefix == true then prefix = core.get_current_modname() end
	local function keyFor(k)
		k = map[k].key
		if prefix then return prefix .. ':' .. k end
		return k
	end

	return function(obj)

Modified mods/vtlib/math.lua from [65dcc21fc6] to [3b6ca155c9].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local lib = ...
local fn = {}

fn.vsep = function(vec) -- separate a vector into a direction + magnitude
	return vec:normalize(), vec:length()
end

-- minetest now only provides the version of this function that sqrts the result
-- which is pointlessly wasteful much of the time
fn.vdsq = function(a,b)
	local d = vector.subtract(a,b)
	return (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2)
end

fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local lib = ...
local fn = {}

fn.vsep = function(vec) -- separate a vector into a direction + magnitude
	return vec:normalize(), vec:length()
end

-- luanti now only provides the version of this function that sqrts the result
-- which is pointlessly wasteful much of the time
fn.vdsq = function(a,b)
	local d = vector.subtract(a,b)
	return (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2)
end

fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points

Modified mods/vtlib/node.lua from [75e0d781d2] to [aa93f93a5c].

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
...
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
...
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
...
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
...
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
			z = pos.z + r(0 - range, range);
		}
	end
	for name, inv in pairs(meta.inventory) do
		if only and not lib.tbl.has(only,name) then goto skip end
		for _, item in pairs(inv) do
			if not item:is_empty() then
				minetest.add_item(offset(pos,0.4), item)
			end
		end
	::skip::end
end;

local force = function(pos,preload_for)
	local n = minetest.get_node_or_nil(pos)
	if preload_for then lib.node.preload(pos,preload_for) end
	if n then return n end

	minetest.load_area(pos)
	return minetest.get_node(pos)
end;

local amass = function(startpoint,names,directions)
	if not directions then directions = ofs.neighbors end
	local check = function(n)
		return lib.tbl.has(names, n.name, function(check,against)
			return lib.item.groupmatch(against,check)
................................................................................
	until i > #stack
	return nodes, positions
end;

local is_air = function(pos)
	local n = force(pos)
	if n.name == 'air' then return true end
	local d = minetest.registered_nodes[n.name]
	if not d then return false end
	return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true)
end;

local is_clear = function(pos)
	if not lib.node.is_air(pos) then return false end
	local ents = minetest.get_objects_inside_radius(pos,0.5)
	if #ents > 0 then return false end
	return true
end;

local function boxwarp(nb, mogrifier, par)
	if nb == nil then
		return
................................................................................
		end
	end; 

	is_air = is_air;
	is_clear = is_clear;

	insert = function(item, slot, npos, user, inv)
		inv = inv or minetest.get_meta(npos):get_inventory()
		if inv:room_for_item(slot,item) then
			inv:add_item(slot,item)
		else repeat
			if user then
				local ui = user:get_inventory()
				if ui:room_for_item('main', item) then
					ui:add_item('main', item)
					break
				end
			end
			minetest.add_item(npos, item)
		until true end
	end;

	install_bed = function(bed, where, dir)
		local bottom = bed .. '_bottom'
		local top = bed .. '_top'
		local d
		if type(dir) == 'number' then
			d = dir
			dir = minetest.facedir_to_dir(d)
		else
			d = minetest.dir_to_facedir(dir)
		end
		if not is_clear(where) and is_clear(where - dir) then return false end
		minetest.set_node(where,       {name = top, param2 = d})
		minetest.set_node(where - dir, {name = bottom, param2 = d})
		return true
	end;

	get_arrival_point = function(pos)
		local try = function(p)
			local air = lib.node.is_clear
			if air(p) then
................................................................................
		end
	end;


	forneighbor = function(pos, n, fn)
		for _,p in pairs(n) do
			local sum = vector.add(pos, p)
			local n = minetest.get_node(sum)
			if n.name == 'ignore' then
				minetest.load_area(sum)
				n = minetest.get_node(sum)
			end
			if fn(sum, n) == false then break end
		end
	end;

	amass = amass;
	
................................................................................
			x = math.floor(pos.x / 16);
			y = math.floor(pos.y / 16);
			z = math.floor(pos.z / 16);
		}
	end;

	preload = function(pos, user)
		minetest.load_area(pos)
		user:send_mapblock(lib.node.blockpos(pos))
	end;

	discharger = function(pos)
		local below = force(vector.subtract(pos,{x=0,y=1,z=0}))
		if below.name == 'hopper:hopper'
		or below.name == 'hopper:hopper_side' then
			local hopper = minetest.get_meta(below):get_inventory()
			return function(i)
				if hopper:room_for_item('main',i) then
					return hopper:add_item('main',i), true
				end
				return i, false
			end
		else
................................................................................
			max_items = 1;
			items = {
				{ items = {id} };
			};
		}
		local next_apn = tbl.after_place_node
		tbl.after_place_node = function(...) local pos, who, stack = ...
			minetest.get_meta(pos):from_table(stack:get_meta():to_table())
			if next_apn then return next_apn(...) end
		end
		local next_pm = tbl.preserve_metadata
		tbl.preserve_metadata = function(...) local pos, node, meta, drops = ...
			drops[1]:get_meta():from_table({fields = meta})
			if next_pm then return next_pm(...) end
		end
		return tbl
	end;
	reg_autopreserve = function(id, tbl)
		minetest.register_node(id, lib.node.autopreserve(id, tbl))
	end;

	boxwarp = boxwarp;
	boxwarped = boxwarped;
}







|






|



|
|







 







|






|







 







|










|









|

|


|
|







 







|

|
|







 







|







|







 







|










|





64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
...
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
...
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
...
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
...
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
			z = pos.z + r(0 - range, range);
		}
	end
	for name, inv in pairs(meta.inventory) do
		if only and not lib.tbl.has(only,name) then goto skip end
		for _, item in pairs(inv) do
			if not item:is_empty() then
				core.add_item(offset(pos,0.4), item)
			end
		end
	::skip::end
end;

local force = function(pos,preload_for)
	local n = core.get_node_or_nil(pos)
	if preload_for then lib.node.preload(pos,preload_for) end
	if n then return n end

	core.load_area(pos)
	return core.get_node(pos)
end;

local amass = function(startpoint,names,directions)
	if not directions then directions = ofs.neighbors end
	local check = function(n)
		return lib.tbl.has(names, n.name, function(check,against)
			return lib.item.groupmatch(against,check)
................................................................................
	until i > #stack
	return nodes, positions
end;

local is_air = function(pos)
	local n = force(pos)
	if n.name == 'air' then return true end
	local d = core.registered_nodes[n.name]
	if not d then return false end
	return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true)
end;

local is_clear = function(pos)
	if not lib.node.is_air(pos) then return false end
	local ents = core.get_objects_inside_radius(pos,0.5)
	if #ents > 0 then return false end
	return true
end;

local function boxwarp(nb, mogrifier, par)
	if nb == nil then
		return
................................................................................
		end
	end; 

	is_air = is_air;
	is_clear = is_clear;

	insert = function(item, slot, npos, user, inv)
		inv = inv or core.get_meta(npos):get_inventory()
		if inv:room_for_item(slot,item) then
			inv:add_item(slot,item)
		else repeat
			if user then
				local ui = user:get_inventory()
				if ui:room_for_item('main', item) then
					ui:add_item('main', item)
					break
				end
			end
			core.add_item(npos, item)
		until true end
	end;

	install_bed = function(bed, where, dir)
		local bottom = bed .. '_bottom'
		local top = bed .. '_top'
		local d
		if type(dir) == 'number' then
			d = dir
			dir = core.facedir_to_dir(d)
		else
			d = core.dir_to_facedir(dir)
		end
		if not is_clear(where) and is_clear(where - dir) then return false end
		core.set_node(where,       {name = top, param2 = d})
		core.set_node(where - dir, {name = bottom, param2 = d})
		return true
	end;

	get_arrival_point = function(pos)
		local try = function(p)
			local air = lib.node.is_clear
			if air(p) then
................................................................................
		end
	end;


	forneighbor = function(pos, n, fn)
		for _,p in pairs(n) do
			local sum = vector.add(pos, p)
			local n = core.get_node(sum)
			if n.name == 'ignore' then
				core.load_area(sum)
				n = core.get_node(sum)
			end
			if fn(sum, n) == false then break end
		end
	end;

	amass = amass;
	
................................................................................
			x = math.floor(pos.x / 16);
			y = math.floor(pos.y / 16);
			z = math.floor(pos.z / 16);
		}
	end;

	preload = function(pos, user)
		core.load_area(pos)
		user:send_mapblock(lib.node.blockpos(pos))
	end;

	discharger = function(pos)
		local below = force(vector.subtract(pos,{x=0,y=1,z=0}))
		if below.name == 'hopper:hopper'
		or below.name == 'hopper:hopper_side' then
			local hopper = core.get_meta(below):get_inventory()
			return function(i)
				if hopper:room_for_item('main',i) then
					return hopper:add_item('main',i), true
				end
				return i, false
			end
		else
................................................................................
			max_items = 1;
			items = {
				{ items = {id} };
			};
		}
		local next_apn = tbl.after_place_node
		tbl.after_place_node = function(...) local pos, who, stack = ...
			core.get_meta(pos):from_table(stack:get_meta():to_table())
			if next_apn then return next_apn(...) end
		end
		local next_pm = tbl.preserve_metadata
		tbl.preserve_metadata = function(...) local pos, node, meta, drops = ...
			drops[1]:get_meta():from_table({fields = meta})
			if next_pm then return next_pm(...) end
		end
		return tbl
	end;
	reg_autopreserve = function(id, tbl)
		core.register_node(id, lib.node.autopreserve(id, tbl))
	end;

	boxwarp = boxwarp;
	boxwarped = boxwarped;
}

Modified mods/vtlib/obj.lua from [7fd77150af] to [42f785b164].

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
..
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
-- from the game API

local fn = {}
local lib = ...

-- WARNING: INEFFICIENT AS FUCK
fn.identify = function(objref) --> objectid
	for _, o in pairs(minetest.get_connected_players()) do
		if objref == o then return o:get_player_name(), 'player' end
	end
	for id, le in pairs(minetest.luaentities) do
		if le.object == objref then return id, 'entity' end
	end
end

fn.handle = lib.class {
	__newindex = function(self,key,newval)
		local hnd if self.player
			then hnd = minetest.get_player_by_name(self._id)
			else hnd = minetest.luaentities[self._id]
		end
		if key == 'id' then
			if type(newval) == 'string' then
				local p = minetest.get_player_by_name(newval)
				if p then
					self._id = newval
					self.player = true
					return
				end
			end
			if minetest.luaentities[newval] then
				self._id = newval
				self.player = false
			else error('attempted to assign invalid ID to entity handle') end
		elseif key == 'obj' then
			local no, kind = fn.identify(newval)
			if no then
				self._id = no
................................................................................
			else error('attempted to assign invalid ObjectRef to entity handle') end
		elseif key == 'stack' and self.kind == 'item' then
			hnd:set_item(newval)
		end
	end;
	__index = function(self,key)
		local hnd if self.player then
			hnd = minetest.get_player_by_name(self._id)
		else
			hnd = minetest.luaentities[self._id]
		end
		if key == 'online' then
			return hnd ~= nil
		elseif key == 'id' then
			if self.player then return nil
			else return self._id end
		elseif key == 'obj' then
................................................................................
					return maxy-miny, miny
				else return 0 end
			end
		end
	end;
	construct = function(h)
		local kind, id
		if type(h) == 'string' and minetest.get_player_by_name(h) ~= nil then
			kind = 'player';
			id = h
		elseif minetest.luaentities[h] then
			kind = 'entity';
			id = h
		else id, kind = fn.identify(h) end

		if not id then
			error('attempted to construct object handle from invalid value')
		end







|


|







|
|



|






|







 







|

|







 







|


|







2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
..
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
-- from the game API

local fn = {}
local lib = ...

-- WARNING: INEFFICIENT AS FUCK
fn.identify = function(objref) --> objectid
	for _, o in pairs(core.get_connected_players()) do
		if objref == o then return o:get_player_name(), 'player' end
	end
	for id, le in pairs(core.luaentities) do
		if le.object == objref then return id, 'entity' end
	end
end

fn.handle = lib.class {
	__newindex = function(self,key,newval)
		local hnd if self.player
			then hnd = core.get_player_by_name(self._id)
			else hnd = core.luaentities[self._id]
		end
		if key == 'id' then
			if type(newval) == 'string' then
				local p = core.get_player_by_name(newval)
				if p then
					self._id = newval
					self.player = true
					return
				end
			end
			if core.luaentities[newval] then
				self._id = newval
				self.player = false
			else error('attempted to assign invalid ID to entity handle') end
		elseif key == 'obj' then
			local no, kind = fn.identify(newval)
			if no then
				self._id = no
................................................................................
			else error('attempted to assign invalid ObjectRef to entity handle') end
		elseif key == 'stack' and self.kind == 'item' then
			hnd:set_item(newval)
		end
	end;
	__index = function(self,key)
		local hnd if self.player then
			hnd = core.get_player_by_name(self._id)
		else
			hnd = core.luaentities[self._id]
		end
		if key == 'online' then
			return hnd ~= nil
		elseif key == 'id' then
			if self.player then return nil
			else return self._id end
		elseif key == 'obj' then
................................................................................
					return maxy-miny, miny
				else return 0 end
			end
		end
	end;
	construct = function(h)
		local kind, id
		if type(h) == 'string' and core.get_player_by_name(h) ~= nil then
			kind = 'player';
			id = h
		elseif core.luaentities[h] then
			kind = 'entity';
			id = h
		else id, kind = fn.identify(h) end

		if not id then
			error('attempted to construct object handle from invalid value')
		end

Modified mods/vtlib/str.lua from [ded7121fc6] to [b2f5363bf4].

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
		['\xf2'] = '\2';
		['\xf3'] = '\3';
		['\xf4'] = '\0';
	};
}

local utf8
if _G.minetest then
	if minetest.global_exists 'utf8' then
		utf8 = _G.utf8
	end
else
	utf8 = _G.utf8
end
if not utf8 then -- sigh
	utf8 = {}
................................................................................
		end
		return str
	end;

	meta_armor = function(str,mark_struct)
		-- binary values stored in metadata need to be sanitized so
		-- they don't contain values that will disrupt parsing of the
		-- KV store, as minetest (stupidly) uses in-band signalling
		local sanitized = string.gsub(str, '.', function(char)
			if sanitable.from[char] then
				return '\xfe' .. sanitable.from[char]
			else return char end
		end)
		if sanitized ~= str and mark_struct then
			-- use different type code to mark struct headers for







|
|







 







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
		['\xf2'] = '\2';
		['\xf3'] = '\3';
		['\xf4'] = '\0';
	};
}

local utf8
if _G.core then
	if core.global_exists 'utf8' then
		utf8 = _G.utf8
	end
else
	utf8 = _G.utf8
end
if not utf8 then -- sigh
	utf8 = {}
................................................................................
		end
		return str
	end;

	meta_armor = function(str,mark_struct)
		-- binary values stored in metadata need to be sanitized so
		-- they don't contain values that will disrupt parsing of the
		-- KV store, as luanti (stupidly) uses in-band signalling
		local sanitized = string.gsub(str, '.', function(char)
			if sanitable.from[char] then
				return '\xfe' .. sanitable.from[char]
			else return char end
		end)
		if sanitized ~= str and mark_struct then
			-- use different type code to mark struct headers for

Modified mods/vtlib/ui.lua from [765cced2b5] to [77b0e87fc7].

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
..
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
..
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
				self.curs.y = math.max(h, self.curs.y + self.curs.maxh)
				self.curs.maxh = 0
			end;
			attach = function(self, elt, x, y, w, h, ...)
				local content = ''
				if self.width - self.curs.x < w then self:nl() end
				for _, v in pairs{...} do
					content = content .. ';' .. minetest.formspec_escape(tostring(v))
				end
				self.src = self.src .. string.format('%s[%f,%f;%f,%f%s]', elt, x,y, w,h, content)
				if h > self.curs.maxh then self.curs.maxh = h end
			end;
			add = function(self, elt, w, h, ...)
				local ax, ay = self.curs.x, self.curs.y
				self:attach(elt, ax,ay, w,h, ...)
................................................................................
		-- takes a configuration table mapping affinities to colors.
		-- 'neutral' is the only required affinity
		return function(a)
			local color = a.color and a.color:readable(0.65, 1.0)
			if color == nil then color = l.color(.5,.5,.5) end
			local str = a.title
			if a.desc then
				str = str .. '\n' .. color:fmt(minetest.wrap_text(a.desc,60))
			end
			if a.props then
				-- str = str .. '\n'
				for _,prop in pairs(a.props) do
					local c
					if prop.color and l.color.id(prop.color) then
						c = prop.color:readable(0.6, 1.0)
................................................................................

					str = str .. '\n ' .. c:fmt('* ')

					if prop.title then
						str = str .. c:brighten(1.2):fmt(prop.title) .. ': '
					end

					local lines = minetest.wrap_text(prop.desc, 55, true)
					str = str .. c:fmt(lines[1])
					for i=2,#lines do
						str = str .. '\n' .. string.rep(' ',5) .. c:fmt(lines[i])
					end
				end
			end
			return color:darken(0.8):bg(str)
		end;
	end;
}







|







 







|







 







|










10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
..
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
..
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
				self.curs.y = math.max(h, self.curs.y + self.curs.maxh)
				self.curs.maxh = 0
			end;
			attach = function(self, elt, x, y, w, h, ...)
				local content = ''
				if self.width - self.curs.x < w then self:nl() end
				for _, v in pairs{...} do
					content = content .. ';' .. core.formspec_escape(tostring(v))
				end
				self.src = self.src .. string.format('%s[%f,%f;%f,%f%s]', elt, x,y, w,h, content)
				if h > self.curs.maxh then self.curs.maxh = h end
			end;
			add = function(self, elt, w, h, ...)
				local ax, ay = self.curs.x, self.curs.y
				self:attach(elt, ax,ay, w,h, ...)
................................................................................
		-- takes a configuration table mapping affinities to colors.
		-- 'neutral' is the only required affinity
		return function(a)
			local color = a.color and a.color:readable(0.65, 1.0)
			if color == nil then color = l.color(.5,.5,.5) end
			local str = a.title
			if a.desc then
				str = str .. '\n' .. color:fmt(core.wrap_text(a.desc,60))
			end
			if a.props then
				-- str = str .. '\n'
				for _,prop in pairs(a.props) do
					local c
					if prop.color and l.color.id(prop.color) then
						c = prop.color:readable(0.6, 1.0)
................................................................................

					str = str .. '\n ' .. c:fmt('* ')

					if prop.title then
						str = str .. c:brighten(1.2):fmt(prop.title) .. ': '
					end

					local lines = core.wrap_text(prop.desc, 55, true)
					str = str .. c:fmt(lines[1])
					for i=2,#lines do
						str = str .. '\n' .. string.rep(' ',5) .. c:fmt(lines[i])
					end
				end
			end
			return color:darken(0.8):bg(str)
		end;
	end;
}

Modified starlit.ct from [ee106030ea] to [f4a136801c].

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
159
160
161
162
163
164
165
166
~~~
$ curl https://c.hale.su/starlit/tarball/starlit.tar.gz | tar x
$ curl https://c.hale.su/starlit/uv/asset.cpxz | xzcat | cpio -i
~~~
eventually i might make proper release tarballs available that bundle the game and assets together, but the game isn't stable enough to do that yet.

## engine
starlit is developed against a bleeding-edge version of minetest. it definitely won't work with anything older than v5.7, and ideally you should build directly from master.

starlit is best used with a patched version of minetest, though it is compatible with vanilla. the recommended patches are:

* [>p11143 11143] - fix third-person view orientation

	p11143: https://github.com/minetest/minetest/pull/11143.diff

### shadows
i was delighted to see dynamic shadows land in minetest, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starlit player character meshes. for the sake of those who don't mind these glitches, Starlit does enable shadows, but i unfortunately have to recommend that you disable them until the minetest devs get their act together on this feature.

## gameplay
starlit is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety. you do have some control over your environment, but it's limited and exerting it is much more expensive than you might be used to -- the focus of the game is figuring out how to work with nature, not against it. Farthest Shadow has little patience for those who do not show her the respect a living world is due, and she is unconcerned with human virtues like "mercy" or "fairness" or "proportionate retribution".

this is to say, starlit is [*mean], by design.

* chance plays an important role. your escape pod might land in the midst of a lush, temperate forest with plenty of nearby shipwrecks to scavenge. or it might land in the exact geographic center of a vast, harsh desert that your suit's cooling systems can't protect you from, ten klicks from anything of value. "unfair", you say? tough. Farthest Shadow doesn't care about your feelings.
* death is much worse than a slap on the wrist. when you die, you drop your possessions and your suit, and respawn naked at your spawn point. this is a serious danger, as you might be kilometers away from your spawn point -- and there's no guarantee someone else won't take your suit before you can find your way back to it. good luck crossing long distances without climate control! if you haven't carefully prepared for this eventuality by keeping a spare suit by your spawn point, death can be devastating, to the point of making the game unsurvivable without another player's help. 

### scenarios
your starting character configuration depends on the scenario you select. (right now this is configured in minetest settings, which is intensely awkward, but i don't have a better solution). the scenario controls your species, sex, and starting inventory. [*neither species nor sex is cosmetic]; e.g. human females are physically weaker but psionically stronger than males. the current playable scenarios are:

#### Imperial Expat
[*phenotype]: human female
[*starting gear]: Commune survival kit
> Hoping to escape a miserable life deep in the grinding gears of the capitalist machine for the bracing freedom of the frontier, you sought entry as a colonist to the new Commune world of Thousand Petal. Fate -- which is to say, terrorists -- intervened, and you wound up stranded on Farthest Shadow with little more than the nanosuit on your back, ship blown to tatters and your soul thoroughly mauled by the explosion of a twisted alien artifact -- which SOMEONE neglected to inform you your ride would be carrying.
> At least you got some handy psionic powers out of this whole clusterfuck. Hopefully they're safe to use.

................................................................................
a Ritual is triggered directly from the psionics menu. as the name implies, these are complex, powerful abilities that require large amounts of Psi and time to meditate before they trigger, and any interruption will cancel the ability (though it will not restore any lost psi). the most famous Ritual is of course Conjoin Metric, which Starlit astropaths use in conjunction with powerful amplifiers to perform long-distance FTL jumps -- but without centuries of dedication to the art, the best you can hope for if you manage to learn this storied power is to move yourself a few kilometers.

a Contextual ability is triggered in a specific situation, usually by interacting with a certain kind of object. Contextual abilities often require specialized equipment, to the point that many Starlit practitioners maintain their own Psionics Lab.

## legal
starlit source code (*.lua, *.conf, *.txt, *.csd files) is released under the GNU AGPLv3.
assets (images, sounds, models, and anything else in the repo that doesn't qualify as source code) are released under the CC-BY-NC-SA 3.0 license.
sound files with the prefix `default-` are taken from Minetest Game, whose assets are available under the CC-BY-SA 3.0 license.







|

|






|


|







|







 







|
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
159
160
161
162
163
164
165
166
~~~
$ curl https://c.hale.su/starlit/tarball/starlit.tar.gz | tar x
$ curl https://c.hale.su/starlit/uv/asset.cpxz | xzcat | cpio -i
~~~
eventually i might make proper release tarballs available that bundle the game and assets together, but the game isn't stable enough to do that yet.

## engine
starlit is developed against a bleeding-edge version of Luanti. it definitely won't work with anything older than v5.7, and ideally you should build directly from master.

starlit is best used with a patched version of Luanti, though it is compatible with vanilla. the recommended patches are:

* [>p11143 11143] - fix third-person view orientation

	p11143: https://github.com/minetest/minetest/pull/11143.diff

### shadows
i was delighted to see dynamic shadows land in Luanti, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starlit player character meshes. for the sake of those who don't mind these glitches, Starlit does enable shadows, but i unfortunately have to recommend that you disable them until the Luanti devs get their act together on this feature.

## gameplay
starlit is somewhat unusual in how it uses the Luanti engine. it's a voxel game but not of the minecraft variety. you do have some control over your environment, but it's limited and exerting it is much more expensive than you might be used to -- the focus of the game is figuring out how to work with nature, not against it. Farthest Shadow has little patience for those who do not show her the respect a living world is due, and she is unconcerned with human virtues like "mercy" or "fairness" or "proportionate retribution".

this is to say, starlit is [*mean], by design.

* chance plays an important role. your escape pod might land in the midst of a lush, temperate forest with plenty of nearby shipwrecks to scavenge. or it might land in the exact geographic center of a vast, harsh desert that your suit's cooling systems can't protect you from, ten klicks from anything of value. "unfair", you say? tough. Farthest Shadow doesn't care about your feelings.
* death is much worse than a slap on the wrist. when you die, you drop your possessions and your suit, and respawn naked at your spawn point. this is a serious danger, as you might be kilometers away from your spawn point -- and there's no guarantee someone else won't take your suit before you can find your way back to it. good luck crossing long distances without climate control! if you haven't carefully prepared for this eventuality by keeping a spare suit by your spawn point, death can be devastating, to the point of making the game unsurvivable without another player's help. 

### scenarios
your starting character configuration depends on the scenario you select. (right now this is configured in Luanti settings, which is intensely awkward, but i don't have a better solution). the scenario controls your species, sex, and starting inventory. [*neither species nor sex is cosmetic]; e.g. human females are physically weaker but psionically stronger than males. the current playable scenarios are:

#### Imperial Expat
[*phenotype]: human female
[*starting gear]: Commune survival kit
> Hoping to escape a miserable life deep in the grinding gears of the capitalist machine for the bracing freedom of the frontier, you sought entry as a colonist to the new Commune world of Thousand Petal. Fate -- which is to say, terrorists -- intervened, and you wound up stranded on Farthest Shadow with little more than the nanosuit on your back, ship blown to tatters and your soul thoroughly mauled by the explosion of a twisted alien artifact -- which SOMEONE neglected to inform you your ride would be carrying.
> At least you got some handy psionic powers out of this whole clusterfuck. Hopefully they're safe to use.

................................................................................
a Ritual is triggered directly from the psionics menu. as the name implies, these are complex, powerful abilities that require large amounts of Psi and time to meditate before they trigger, and any interruption will cancel the ability (though it will not restore any lost psi). the most famous Ritual is of course Conjoin Metric, which Starlit astropaths use in conjunction with powerful amplifiers to perform long-distance FTL jumps -- but without centuries of dedication to the art, the best you can hope for if you manage to learn this storied power is to move yourself a few kilometers.

a Contextual ability is triggered in a specific situation, usually by interacting with a certain kind of object. Contextual abilities often require specialized equipment, to the point that many Starlit practitioners maintain their own Psionics Lab.

## legal
starlit source code (*.lua, *.conf, *.txt, *.csd files) is released under the GNU AGPLv3.
assets (images, sounds, models, and anything else in the repo that doesn't qualify as source code) are released under the CC-BY-NC-SA 3.0 license.
sound files with the prefix `default-` are taken from Luanti Game, whose assets are available under the CC-BY-SA 3.0 license.