Skip to content

Instantly share code, notes, and snippets.

@CoderPuppy
Last active February 5, 2021 04:17
Show Gist options
  • Save CoderPuppy/0a984e9276c415b08315275356b997d2 to your computer and use it in GitHub Desktop.
Save CoderPuppy/0a984e9276c415b08315275356b997d2 to your computer and use it in GitHub Desktop.
ComputerCraft Inventory System
local I = dofile 'cc/mining/inventory.lua'
xpcall(function()
dofile 'cc/mining/inventory-ui.lua' (I)
end, function(err)
if err == 'Terminated' then return end
sleep(3)
term.setTextColor(colors.red)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
print(err)
local i = 3
while true do
local _, msg = pcall(error, '@', i)
if msg == '@' then
break
end
if i > 10 then break end
print(i, msg)
i = i + 1
end
end)
--[[
-- this is a stupid approach
local function split_pat(pat)
local initial = nil
or pat:match '^%b[][?*+-]?'
or pat:match '^%%.[?*+-]?'
or pat:match '^[^[%%][?*+-]?'
return initial, pat:sub(#initial + 1)
end
local function fuzzy_match(pat, str)
local rest = pat
local pat_pos = 1
while #rest > 0 do
local initial
initial, rest = split_pat(rest)
local last = initial:sub(#initial)
if initial ~= '%*' and last == '*' then
initial = initial:sub(1, #initial - 1) .. '+'
elseif initial ~= '%?' and last == '?' then
initial = initial:sub(1, #initial - 1)
end
for str_pos, m in str:gmatch('()(' .. initial .. ')') do
local subpat = '^' .. initial
local rest_run = rest
local longest = m
while true do
local part
part, rest_run = split_pat(rest_run)
local try_pat = subpat .. part
local m = str:match(try_pat, str_pos)
if m then
longest = m
subpat = try_pat
else
break
end
end
end
pat_pos = pat_pos + #initial
end
end
--]]
--[[
local function fuzzy_match(pat, str)
local tbl = {}
for pat_i = 1, #pat do
local pat_c = pat:sub(pat_i, pat_i)
for str_i = 1, #str do
local str_c = str:sub(str_i, str_i)
if str_c == pat_c then
else
end
end
end
end
--]]
return function(I)
local width, height = term.getSize()
local function format_count(n)
local suffix = ''
local decimal = false
if n > 1000 then
n = n / 1000
suffix = 'k'
decimal = n < 10000
end
return string.format(decimal and '%.1f%s' or '%d%s', n, suffix)
end
local keys_down = {}
local list_model
local function make_list_model()
local model = {
search = '';
search_i = 1;
list = {};
list_i = 1;
selected_i = 1;
-- required fields:
-- iter
-- item_str = function(item) return 'display string' end;
-- item_searchable = function(item) return 'string to search' end;
-- order = function(a, b) return a < b end;
}
return model
end
local function list_model_draw_search()
term.setCursorPos(1, 1)
term.blit(
list_model.search:sub(list_model.search_i) .. string.rep(' ', width - #list_model.search - list_model.search_i + 1),
string.rep('f', width),
string.rep('1', width)
)
term.setCursorPos(#list_model.search - list_model.search_i + 2, 1)
end
local function list_model_draw_item(i)
if i < list_model.list_i or i > list_model.list_i + height - 2 then
return
end
term.setCursorPos(1, i - list_model.list_i + 2)
local item = list_model.list[i]
local selected = i == list_model.selected_i
if item then
term.blit(
list_model.item_str(item),
string.rep(selected and '0' or '8', width),
string.rep(selected and '8' or '7', width)
)
else
term.blit(
string.rep(' ', width),
string.rep('c', width),
string.rep('f', width)
)
end
end
local function list_model_fix_scroll(skip_draw)
local old_list_i = list_model.list_i
if list_model.list_i < 1 then
list_model.list_i = 1
elseif list_model.list_i ~= 1 and list_model.list_i > #list_model.list - height + 2 then
list_model.list_i = #list_model.list - height + 2
end
if list_model.selected_i < 1 then
list_model.selected_i = 1
elseif list_model.selected_i > #list_model.list then
list_model.selected_i = #list_model.list
end
if list_model.selected_i < list_model.list_i then
list_model.list_i = list_model.selected_i
elseif list_model.selected_i > list_model.list_i + height - 2 then
list_model.list_i = list_model.selected_i - height + 2
else
return
end
if not skip_draw then
term.scroll(list_model.list_i - old_list_i)
if list_model.list_i < old_list_i then
for i = list_model.list_i, math.min(list_model.list_i + height - 2, old_list_i) do
list_model_draw_item(i)
end
elseif list_model.list_i > old_list_i then
for i = math.max(list_model.list_i - height + 2, old_list_i), list_model.list_i + height - 2 do
list_model_draw_item(i)
end
end
list_model_draw_search()
end
return list_model.list_i ~= old_list_i
end
local function list_model_update_list()
local prev_selected = list_model.list[list_model.selected_i]
local new_select_i
local new_list = { n = 0; }
local ignore_case = not list_model.search:match '[A-Z]'
for item in list_model.iter() do
local name = list_model.item_searchable(item)
-- item_type.example.displayName
if ignore_case then
name = string.lower(name)
end
local ok, match = pcall(string.match, name, list_model.search)
if not ok then return end
if match then
new_list.n = new_list.n + 1
new_list[new_list.n] = item
if item == selected then
new_select_i = new_list.n
end
end
end
table.sort(new_list, list_model.order)
list_model.list = new_list
list_model.selected_i = new_select_i or 1
list_model_fix_scroll(true)
for i = list_model.list_i, list_model.list_i + height - 2 do
list_model_draw_item(i)
end
end
local function list_model_full_update()
list_model_update_list()
list_model_draw_search()
term.setCursorBlink(true)
end
function handle_list_model(evt)
if not list_model then return end
if evt[1] == 'key' then
if evt[2] == keys.backspace then
if keys_down.ctrl then
if keys_down.shift then
list_model.search = ''
else
list_model.search = list_model.search:gsub('[^%s]+[%s]*$', '')
end
else
list_model.search = list_model.search:sub(1, #list_model.search - 1)
end
list_model_full_update()
elseif evt[2] == keys.down then
list_model.selected_i = list_model.selected_i + 1
if not list_model_fix_scroll() then
list_model_draw_item(list_model.selected_i)
end
list_model_draw_item(list_model.selected_i - 1)
elseif evt[2] == keys.up then
list_model.selected_i = list_model.selected_i - 1
if not list_model_fix_scroll() then
list_model_draw_item(list_model.selected_i)
end
list_model_draw_item(list_model.selected_i + 1)
end
elseif evt[1] == 'char' then
if not keys_down.ctrl and not keys_down.alt then
list_model.search = list_model.search .. evt[2]
list_model_full_update()
end
end
end
local inv_list_model; do
local function custom_next(s, prev_item)
local key, item = next(s, prev_item and prev_item.key)
-- if item and item.number <= 0 then
-- return custom_next(s, item)
-- else
return item
-- end
end
inv_list_model = make_list_model()
function inv_list_model.iter()
return custom_next, I.state.item_types
end
function inv_list_model.item_str(item)
local name = item.example.displayName
local count = format_count(item.number)
return name .. string.rep(' ', width - #name - #count) .. count
end
function inv_list_model.item_searchable(item)
return item.example.displayName
end
function inv_list_model.order(a, b)
-- return a.example.displayName < b.example.displayName
return a.number > b.number
end
end
local ext_list_model, ext_handle; do
ext_list_model = make_list_model()
local function custom_next(s, prev_item)
local slot, item = next(s, prev_item and prev_item.slot)
if item then
item.slot = slot
item.item_type = I.identify(item, function()
return ext_handle.getItemDetail(slot)
end)
end
return item
end
function ext_list_model.iter()
return custom_next, ext_handle.list()
end
function ext_list_model.item_str(item)
local name = item.item_type.example.displayName
local count = tostring(item.count)
return name .. string.rep(' ', width - #name - #count) .. count
end
function ext_list_model.item_searchable(item)
return item.item_type.example.displayName
end;
function ext_list_model.order(a, b)
return a.slot < b.slot
end
end
if I.initialize() > 16 then
I.save()
end
ext_handle = peripheral.wrap'minecraft:chest_0'
-- TODO: this is a bit messy
ext_handle.num_slots = ext_handle.size()
-- list_model = ext_list_model
list_model = inv_list_model
list_model_full_update()
while true do
local evt = table.pack(os.pullEvent())
if evt[1] == 'key' then
keys_down[evt[2]] = true
elseif evt[1] == 'key_up' then
keys_down[evt[2]] = nil
end
keys_down.ctrl = keys_down[keys.leftCtrl ] or keys_down[keys.rightCtrl ]
keys_down.shift = keys_down[keys.leftShift] or keys_down[keys.rightShift]
keys_down.alt = keys_down[keys.leftAlt ] or keys_down[keys.rightAlt ]
keys_down.mods = (keys_down.ctrl and 'c' or '') .. (keys_down.alt and 'a' or '') .. (keys_down.shift and 's' or '')
if evt[1] == 'key' and evt[2] == keys.s and keys_down.mods == 'c' then
I.save()
elseif evt[1] == 'key' and evt[2] == keys.enter and list_model and not keys_down.shift then
if list_model == ext_list_model then
local item = list_model.list[list_model.selected_i]
if not item then
-- skip
elseif keys_down.alt then
-- TODO
else
-- this will get refreshed anyways by list_model_full_update
-- and it causes problem with inventory's logging
-- because it's recursive
item.item_type = nil
local n = I.insert(ext_handle, item.slot, keys_down.ctrl and item.count or 1, item)
item.count = item.count - n
if item.count <= 0 then
table.remove(list_model.list, list_model.selected_i)
list_model_fix_scroll()
for i = list_model.selected_i, list_model.list_i + height - 2 do
list_model_draw_item(i)
end
else
list_model_draw_item(list_model.selected_i)
end
end
elseif list_model == inv_list_model then
local item_type = list_model.list[list_model.selected_i]
if not item_type then
-- skip
elseif keys_down.alt then
-- TODO
else
-- TODO: maybe this shouldn't reference ext_handle
assert(ext_handle)
local list = ext_handle.list()
local remaining = keys_down.ctrl and item_type.example.maxCount or 1
for slot = 1, ext_handle.num_slots do
local stack = list[slot]
if not stack or (stack.name == item_type.name and stack.nbt == item_type.nbt) then
local n = I.extract(item_type, ext_handle, slot, remaining)
remaining = remaining - n
if remaining <= 0 then
break
end
end
end
list_model_draw_item(list_model.selected_i)
end
end
elseif evt[1] == 'key' and evt[2] == keys.tab and keys_down.mods == '' and list_model then
if list_model == inv_list_model then
list_model = ext_list_model
elseif list_model == ext_list_model then
list_model = inv_list_model
end
list_model_full_update()
end
handle_list_model(evt)
end
end
if false then
local Tchest = T {
name = T.string;
num_slots = T.int;
empties = T.map(T.int, T.lit(true));
item_types = T.map(Titem_type, T.lit(true));
}
local Titem_type = T {
key = T.string;
name = T.string;
nbt = T.union(T.string, T.null);
example = T.any;
number = T.int;
-- TODO: maybe figure out the max stack size
-- max_stack_size = T.int;
chests = T.map(Tchest, T.map(T.int, T.int));
partials = T.map(Tchest, T.int);
}
local Tsave = T {
id = T.int;
item_types = T.map(T.string, T {
name = T.string;
nbt = T.union(T.string, T.null);
example = T.any;
number = T.int;
chests = T.map(T.string, T.map(T.int, T.int));
partials = T.map(T.string, T.int);
});
chests = T.map(T.string, T {
num_slots = T.int;
empties = T.map(T.int, T.lit(true));
item_types = T.map(T.string, T.lit(true));
});
chest_room = T.map(T.string, T.lit(true));
}
end
local log; do
local f = fs.open('log', 'a')
function log(msg)
f.write(os.date'%F %T\t' .. msg .. '\n')
f.flush()
end
log 'start'
end
local save_id
local item_types
local chests
local chest_room
local transaction_log
-- the transaction log records virtual updates which are not saved to disk otherwise
-- TODO: in flight records physical updates which are in progress
local function item_type_key(name, nbt)
return string.format('%q%q', name, nbt)
end
local function serialize(v)
local t = type(v)
if t == 'string' then
return string.format('%q', v)
elseif t == 'number' or t == 'boolean' then
return tostring(t)
elseif t == 'table' then
local s = '{'
for k, v in pairs(v) do
s = string.format('%s[%s]=%s;', s, serialize(k), serialize(v))
end
return s .. '}'
else
error(string.format('unhandled type: %q', t))
end
end
-- TODO: one function which writes to the transaction log and performs the transaction
local function track_add_item_type(itk, detail)
-- TODO: there's no good reason for itk to be passed in
-- just because it was already computed in `insert`
-- where this was extracted from
local item_type = {
key = itk;
name = detail.name;
nbt = detail.nbt;
example = detail;
number = 0;
chests = {};
partials = {};
}
item_types[itk] = item_type
return item_type
end
local function track_insert(chest, slot, item_type, num, full)
item_type.partials[chest] = not full and slot or nil
if chest.empties[slot] then
chest.empties[slot] = nil
if not next(chest.empties) then
chest_room[chest] = nil
end
end
chest.item_types[item_type] = true
local chest_stacks = item_type.chests[chest]
if not chest_stacks then
chest_stacks = {}
item_type.chests[chest] = chest_stacks
end
chest_stacks[slot] = (chest_stacks[slot] or 0) + num
item_type.number = item_type.number + num
end
local function track_extract(chest, slot, item_type, num)
local chest_slots = item_type.chests[chest]
local slot_num = chest_slots[slot]
chest_slots[slot] = slot_num - num
item_type.number = item_type.number - num
if num >= slot_num then
assert(num == slot_num)
assert(item_type.partials[chest] == slot)
item_type.partials[chest] = nil
chest_slots[slot] = nil
if not next(chest_slots) then
item_type.chests[chest] = nil
chest.item_types[item_type] = nil
if not next(item_type.chests) then
-- TODO: destroy the item type?
end
end
chest.empties[slot] = true
chest_room[chest] = true
else
item_type.partials[chest] = slot
end
end
local function initialize()
if transaction_log then
transaction_log.close()
end
-- TODO: filesystem layout
if fs.exists('inv-save-new') then
assert(not fs.isDir('inv-save-new'))
fs.delete('inv-save')
fs.move('inv-save-new', 'inv-save')
end
local h = fs.open('inv-save', 'r')
if h then
local save = textutils.unserialize(h.readAll())
h.close()
save_id = save.id
chests = {}
for name, save_chest in pairs(save.chests) do
local chest = {
name = name;
num_slots = save_chest.num_slots;
empties = save_chest.empties;
item_types = {};
}
chests[name] = chest
end
item_types = {}
for key, save_item_type in pairs(save.item_types) do
local item_type = {
key = key;
name = save_item_type.name;
nbt = save_item_type.nbt;
example = save_item_type.example;
number = save_item_type.number;
chests = {};
partials = {};
}
for chest_name, slots in pairs(save_item_type.chests) do
item_type.chests[chests[chest_name]] = slots
end
for chest_name, slot in pairs(save_item_type.partials) do
item_type.partials[chests[chest_name]] = slot
end
item_types[item_type.key] = item_type
end
for name, save_chest in pairs(save.chests) do
local chest = chests[name]
for itk in pairs(save_chest.item_types) do
chest.item_types[item_types[itk]] = true
end
end
chest_room = {}
for name in pairs(save.chest_room) do
chest_room[chests[name]] = true
end
else
save_id = -1
item_types = {}
chests = {}
chest_room = {}
end
local n = 0
-- TODO: filesystem layout
local h = fs.open('inv-transaction-log', 'r')
if h then
assert(tostring(save_id) == h.readLine(), 'TODO')
while true do
local line = h.readLine(true)
if not line then break end
local entry = textutils.unserialize(line)
log('replay: ' .. textutils.serialize(entry))
if entry.type == 'add_chest' then
-- TODO: this is quite limited
-- see `add_chest` for more about this
local chest = {
name = entry.name;
num_slots = entry.num_slots;
empties = {};
item_types = {};
}
chests[chest.name] = chest
for i = 1, chest.num_slots do
chest.empties[i] = true
end
chest_room[chest] = true
elseif entry.type == 'add_item_type' then
track_add_item_type(entry.item_type_key, entry.detail)
elseif entry.type == 'insert' then
track_insert(
chests[entry.chest_name], entry.slot,
item_types[entry.item_type_key],
entry.num, entry.full
)
elseif entry.type == 'extract' then
track_extract(
chests[entry.chest_name], entry.slot,
item_types[entry.item_type_key],
entry.num
)
else
error(string.format('unhandled transaction type: %q', entry.type))
end
n = n + 1
end
h.close()
-- TODO: filesystem layout
transaction_log = fs.open('inv-transaction-log', 'a')
else
-- TODO: filesystem layout
transaction_log = fs.open('inv-transaction-log', 'w')
transaction_log.write(string.format('%d\n', save_id))
transaction_log.flush()
end
-- TODO: in flight
return n
end
local function save()
transaction_log.close()
local save = {
id = save_id + 1;
item_types = {};
chests = {};
chest_room = {};
}
for key, item_type in pairs(item_types) do
local save_item_type = {
name = item_type.name;
nbt = item_type.nbt;
example = item_type.example;
number = item_type.number;
chests = {};
partials = {};
}
for chest, slots in pairs(item_type.chests) do
save_item_type.chests[chest.name] = slots
end
for chest, slot in pairs(item_type.partials) do
save_item_type.partials[chest.name] = slot
end
save.item_types[key] = save_item_type
end
for name, chest in pairs(chests) do
local save_chest = {
num_slots = chest.num_slots;
empties = chest.empties;
item_types = {};
}
for item_type in pairs(chest.item_types) do
save_chest.item_types[item_type.key] = true
end
save.chests[chest.name] = save_chest
end
for chest in pairs(chest_room) do
save.chest_room[chest.name] = true
end
-- TODO: filesystem layout
local h = fs.open('inv-save-new', 'w')
h.write(textutils.serialize(save))
h.close()
fs.delete('inv-save')
fs.move('inv-save-new', 'inv-save')
save_id = save.id
-- TODO: filesystem layout
transaction_log = fs.open('inv-transaction-log', 'w')
transaction_log.write(string.format('%d\n', save_id))
transaction_log.flush()
end
local function reset()
save_id = nil
item_types = nil
chests = nil
chest_room = nil
if transaction_log then
transaction_log.close()
transaction_log = nil
end
-- TODO: filesystem layout
fs.delete('inv-save')
fs.delete('inv-save-new')
fs.delete('inv-transaction-log')
end
local function close()
transaction_log.close()
transaction_log = nil
save_id = nil
item_types = nil
chests = nil
chest_room = nil
end
local function identify(detail, add)
local itk = item_type_key(detail.name, detail.nbt)
local item_type = item_types[itk]
if not item_type and add then
if not detail.displayName then
detail = add()
end
-- create the item type if none exists
item_type = track_add_item_type(itk, detail)
transaction_log.write(string.format(
'{'
.. ' type = "add_item_type";'
.. ' item_type_key = %q;'
.. ' detail = %s;'
.. '}\n',
itk, serialize(detail)
))
transaction_log.flush()
end
return item_type
end
local function add_chest(name)
local inv = peripheral.wrap(name)
local chest = {
name = name;
num_slots = inv.size();
empties = {};
item_types = {};
}
chests[chest.name] = chest
local contents = inv.list()
local has_room = false
for i = 1, chest.num_slots do
if contents[i] then
error 'TODO'
-- it can't just index it, because there are rules (specifically about only one partial per item type and chest)
-- maybe just return nil, saying we can't add this chest
-- or it could move items around to maintain the rules
-- remember to change the transaction log stuff if implementing this
else
has_room = true
chest.empties[i] = true
end
end
if has_room then
chest_room[chest] = true
end
transaction_log.write(string.format('{ type = "add_chest"; name = %q; num_slots = %d; }\n', name, chest.num_slots))
transaction_log.flush()
return chest
end
local function insert(inv, slot, amt, detail)
local detail = detail or inv.getItemDetail(slot)
log('insert: ' .. textutils.serialize(detail))
-- find or create a corresponding item type
local item_type = identify(detail, function()
return inv.getItemDetail(slot)
end)
-- move the entire stack
local remaining = amt or detail.count
while remaining > 0 do
-- find a place to put it
local dst_chest, dst_slot
-- first try a slot with some of this item type (but not full)
dst_chest, dst_slot = next(item_type.partials)
if not dst_chest then
-- otherwise find an empty slot
for chest in pairs(chest_room) do
dst_slot = next(chest.empties)
if dst_slot then
dst_chest = chest
break
end
end
assert(dst_chest, 'TODO: no room in system')
end
-- TODO: in flight
local n = inv.pushItems(dst_chest.name, slot, remaining, dst_slot)
track_insert(dst_chest, dst_slot, item_type, n, n < remaining)
transaction_log.write(string.format(
'{'
.. ' type = "insert";'
.. ' chest_name = %q;'
.. ' slot = %d;'
.. ' item_type_key = %q;'
.. ' num = %d;'
.. ' full = %s;'
.. '}\n',
dst_chest.name, dst_slot, item_type.key, n, n < remaining
))
transaction_log.flush()
remaining = remaining - n
end
return amt or detail.count, item_type
end
local function extract(item_type, inv, dst_slot, amt)
local transferred = 0
local remaining = amt or item_type.number
local function extract_part(chest, slot)
local chest_slots = item_type.chests[chest]
local slot_num = chest_slots[slot]
local pull = slot_num < remaining and slot_num or remaining
-- TODO: in flight
local n = inv.pullItems(chest.name, slot, pull, dst_slot)
remaining = remaining - n
transferred = transferred + n
track_extract(chest, slot, item_type, n)
transaction_log.write(string.format(
'{'
.. ' type = "extract";'
.. ' chest_name = %q;'
.. ' slot = %d;'
.. ' item_type_key = %q;'
.. ' num = %d;'
.. '}\n',
chest.name, slot, item_type.key, n
))
transaction_log.flush()
if n < pull then
-- no more room in the destination slot
-- this is a slightly hacky way to break out
remaining = 0
end
end
if remaining > 0 then
for chest, slot in pairs(item_type.partials) do
extract_part(chest, slot)
if remaining <= 0 then
assert(remaining == 0)
break
end
end
end
if remaining > 0 then
for chest, slots in pairs(item_type.chests) do
for slot, number in pairs(slots) do
extract_part(chest, slot)
if remaining <= 0 then
assert(remaining == 0)
break
end
end
if remaining <= 0 then
assert(remaining == 0)
break
end
end
end
return transferred
end
return {
log = log;
state = setmetatable({}, {
__index = function(self, key)
if key == 'save_id' then
return save_id
elseif key == 'item_types' then
return item_types
elseif key == 'chests' then
return chests
elseif key == 'chest_room' then
return chest_room
else
return nil
end
end;
__newindex = function(self, key, val)
error('bad')
end;
});
item_type_key = item_type_key;
identify = identify;
initialize = initialize;
save = save;
reset = reset;
close = close;
track_insert = track_insert;
track_extract = track_extract;
add_chest = add_chest;
insert = insert;
extract = extract;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment