本篇教程由作者设定未经允许禁止转载。

前情提要

由于本人的学习能力不得行,看不懂高深的 纯AE 自动化,而史蒂夫工厂既没有 1.12.2(GTCE, CEu),又没有 1.19/1.20(GTM),没法借鉴 GTNH 大佬们的操作,于是想到利用 CC:T 来进行将 AE 发配出来的材料分配到装配线中的自动化操作。本来想着就搜下看有没有大佬用 CC:T、CC 或 OC 做的装配线自动化,直接抄作业完事儿了,但由于自己搜索能力太差,搜出来的要么只有思路没有代码,要么有代码也无法处理最为麻烦的不满一组多物品分散情况,于是不得已,瞎搓了个海龟的发配代码。

本来代码搓完了,问题解决了,那为啥要发这教程呢?分享一下自己的经验是肯定的,还有那就是希望初学者们不要看到英文文档一堆看也看不懂的玩意儿望而却步,希望能通过本教程的引导,手搓出个属于自己的小乌龟,通过实践来进行学习。课后习题那也肯定是必不可少的

展示视频在这儿

框架

先不考虑计算机啊海龟啊代码啊之类的细枝末节,来想想我们自己在面对从 AE 发来的物品时,该如何分配物品到装配线中的各个输入总线中去。

  1. 毋庸置疑,需要先打开箱子[1]看看[2] AE 给我们发来了啥玩意儿

  2. 之后,查找 JEI[3]当然也可以是NEI HEI REI EMI一个一个物品比较[4]得到 AE 想让我们合成什么

  3. 走到[5]对应的输入总线总线前,将相应物品依照 JEI 指示[6]放入输入总线中

  4. 走回[7]箱子处,[8]到输入总线中没物品后再进行下一个物品的合成

至于流体的话,用钢桶+流体转置的方式实在过于抽象,而且样板也编不下,需要用到超级高深的“假合成”。1.19 及以后本体就可发配流体,而且也能编下更多的物品,直接流体抽取到对应输入仓即可。1.12 有个附属也可直接发配物品与流体,即使没有装这个附属也能用折中的 AE 直发流体法暴力解决。总之,流体的解决方式多样且不具有技术上的难度,故不考虑。

所以,我们仅需要用乌龟完成上面标红的八个动作,即可完成乌龟对发送来物品的自动化。

同时注意到,动作[3][6]都需要用到 JEI,因此在让乌龟动起来之前,需要我们手动为乌龟量身定制一套“龟EI”,供其查找使用。

因此,我们得到如下大框架:

  • 一个或多个程序为乌龟得到合成表

  • 一个程序实现上述八个动作

实现与技术细节

为乌龟制作龟EI

GetRecipe.lua

turtle乌龟.get得到Item物品Detail详细信息() 可以告诉我们选择到的物品具体信息,具体可以自己在乌龟里开机后输入lua,再输入 turtle,getItemDetail() 观察得出结果,会发现出来的是形如 {count=???, name=???, damage=???} 的玩意儿。如我们令 temp 为上面那个输出的玩意儿,那么 lua 会认为 temp["count"] 就是 count 等号后面的东西,temp["name"],temp["damage"] 也是一样的。同时,通过多次的尝试我们也可很容易得到如果 name 和 name 相等、damage 和 damage 相等,那么这就是同一个东西。如此,我们先得到一个很重要的判断给出的两个物品是否为同一物品的 function动作 (专业术语为函数)

local function EqualItem(l,r)
    if l["name"]==r["name"] and
    l["damage"]==r["damage"] then
        return true
    end
    return false
end

if如果 与 then然后 之间的一坨如果 l, r 它们的 name and和 damage 都相等,就执行 then 到 end结尾 之间的那个 return返回 true真的,返回的意思就是结束这个操作,并把这个操作所在的地方删了改成 return 后面的东西。

看不懂上面的依托?没关系!你只要知道这个动作可以得到 l 和 r 到底对应的是不是同一个物品即可,相当于专为物品而设计的 l==r。

为了得到合成表,我们肯定是要把需要的物品按照 JEI 里的从左上到右下填充,那么对于乌龟来说,仅需要从左往右依次做 getItemDetail 操作即可获得此合成表,于是我们有 如一行只有一条语句 Lua 语句后的分号可不加,这里加了只是作者习惯使然

local t=turtle;t.select(1);
local h=t.getItemDetail();
local slot=1;local list={};
while h do
    table.insert(list,h);
    slot = slot + 1;t.select(slot);
    h=t.getItemDetail();
end

第一眼就出现了个新词, local。观察一下上面的程序,只有当某个“字母”,或者更专业一点,变量第一次出现的时候,才会在其前面出现。现在,只要把这个词当作告诉计算机这里要新建一个名字叫做 xxx 的变量即可,如同只有新建了 word 文档后你才能在里面输入文章,把它当作一种习惯。

那 local t=turtle 又该怎么理解?t.getItemDetail() 和 turtle.getItemDetail() 一样吗?这个 turtle 和上面提到过的 temp 实际上差不多,都是形如{???=???, ???=???…}的结构,而 . 的操作就相当于拿到 turtle 中 getItemDetail=??? 的那个 ???。那既然 t 的值都和 turtle 一样,那么 t.xxx(…) 的效果就和 turtle.xxx(…) 完全相同,因此也可以理解为为了简写而把 turtle 给重命名了。

list={},想必大家都已经猜到了,这就是建一个类似于 turtle 的表,不过非常简单,是空的。至于 table.insert(list,h),就是把 h 追加到 list 中,table.insert({...},1),那个 {...} 就会变成 {..., 1},在最后多出了个 1 出来。

while 是我们碰到的第一个结构标志,while 后面的式子为真,就重复执行 do end 之间的一堆,直到 while 后的式子为假。而 getItemDetail() 有个特点,如果是没有东西的格子它对应的值为假,所以 do end 间的操作到选择格中没物品的时候才结束。t.select(x) 即将选择格移动到 x 的位置。

所以,整个 while 做的操作就是从第一格开始通过扫描格子的方式得到某个物品的合成表。

仅仅有合成表,我们的 龟EI 还不太够,因为 AE 发配的时候会把所有未满一组的东西合起来当一组发配,如果我们仅拿着这个合成表来做判断会极端复杂,所以除了合成操作外,我们的合成表应该包含一个“表头”,由顺序的物品种类构成,以方便之后对要合成哪个物品进行判断。对此,我们得到如下程序,

local slot=1;t.select(slot);local i=1;
local h=t.getItemDetail();
local head={};
table.insert(head,h);
while h do
    if not EqualItem(h,head[i]) then
        i=i+1;table.insert(head,h);
    end
    slot = slot + 1;t.select(slot);
    h=t.getItemDetail();
end

EqualItem终于出现了!显然,i 用来标记 head 里最后加进来的那个物品。对 head[i],可以这样理解,虽然 head 看起来可能像 {9,8,7},并没有什么 xxx=9,但实际上这样的结构已经默认了 {1=9,2=8,3=7},所以我们用 head[3] 就可以拿到 head 的最后加入值 7。这样就把那些一样的物品给筛去,得到表头 head。

由于我们可能要做很多物品的合成表,而这些合成表都需要用文件存起来以便日后使用,因此最终我们可以得到这样的程序,GetRecipe.lua,

--coded by ASQTTR
local save="saves/";local t=turtle;
local num=1;
local function EqualItem(l,r)
    if l["name"]==r["name"] and
    l["damage"]==r["damage"] then
        return true
    end
    return false
end
while io.open(save..tostring(num),"r") do
    num=num+1;
end
local file=io.open(save..tostring(num),"w");
local slot=1;t.select(slot);local i=1;
local h=t.getItemDetail();
local list={};local head={};
table.insert(head,h);
while h do
    table.insert(list,h);
    if not EqualItem(list[slot],head[i]) then
        i=i+1;table.insert(head,h);
    end
    slot = slot + 1;t.select(slot);
    h=t.getItemDetail();
end
file:write(tostring(#head).."\n");
for v=1,#head do
    local u=head[v];
    local out=
    u.name..'\n'..
    tostring(u.damage)..'\n';
    file:write(out);
end
file:write(tostring(#list).."\n");
for v=1,#list do
    local u=list[v];
    local out=
    u.name..'\n'..
    tostring(u.damage)..'\n'..
    tostring(u.count)..'\n';
    file:write(out);
end
io.close(file);

哇!这么长,我咋看得懂啊!从上往下,io.open()为打开某个文件,逗号前面是文件所在位置,后面是打开的模式,r代表读,这里主要用了如果文件不存在 io.open() 为假的特点。这里是想要将每次加入的合成表在 saves 中按照名字 1 2 3…排列下去,所以以此来筛选看第一个不存在的文件名是哪个,如此,tostring() 就很好懂了,把数字改成文字,即字符串,..是把两个字符串连起来的意思。接下来,以写模式打开那个没找到的文件,新建它,后面的就是先前分析的核心程序,得到合成表拼在了一起。接下来的,就是先写入 #head head的长度 和 list 的长度,后写入具体的信息。for 有很多种变体,但用的最多的就是上面展示的那样,某个变量=1,?,表示这个变量从 1 取到 ?,每此循环加一,那这 for 的意思就是将 head 和 list 中的表头和合成操作写入文件中。io.close()当然就是把文件给关了,不然写不进去。

Dump.lua

龟EI 的合成表现在都已经存在 saves 里了,但真正有用的是合成表组而非合成表,因为一般一个 ME 控制器可以发配多个物品,我们的乌龟要根据这九个物品合成表组成的合成表组来作为参照。为了将 saves 中的合成表打包成合成表组,可拿到,

--coded by ASQTTR
local num=1;local dir="disk/saves/";
print("Where do you want to dump to?");
local ofile=io.open(io.read(),"w");
print("Some description about the recipe-group:");
term.setTextColor(colors.gray);
print("press a single line of % to finish editing the description.");
term.setTextColor(colors.white);
local line=io.read;
while line=='%' do
    ofile:write(line.."\n");line=io.read();
end
ofile:write("%\n");
while io.open(dir..tostring(num),"r") do
    num=num+1;
end
num=num-1;ofile:write(tostring(num)..'\n');
print("Dump start");
for i=1,num do
    print("Now is dumping No."..tostring(i).." recip");
    local infile=io.open(dir..tostring(i),"r");
    ofile:write(infile:read("a")..'\n');io.close(infile);
end
io.close(ofile);

print() 和 term.setTextColor 是用来输出提示信息和改变字的颜色的,对理解逻辑几乎没用可以直接删去。把这些花里胡哨的玩意儿全部剔除后,很容易发现这个程序实际上就是将 saves 里的 1 2 3…原封不动地复制到输出文件中,同时在最前面加上此合成组中合成表的个数。

让乌龟动起来!

构建合成组

为了方便起见,我们把需要加入的合成组名字放在某个文件中,并用 config 变量来代表这个被打开的文件,由于文件在遇到%前的部分为描述注释,故需要舍去。将一个合成表看成一个 unit,unit={head=???,op=???},head 就是这个合成表的表头,op 即为合成操作。将 unit table.insert() 到 list 中去,即可将相应的合成组存到内存中,有程序,

local list={};
local OpFile=config:read();
while OpFile do
    OpFile=io.open(OpFile);
    while OpFile:read()~="%" do end;
    local num=tonumber(OpFile:read());
    for i=1,num do
        local unit={};local head={};local op={};
        local tNum=tonumber(OpFile:read());
        for j=1,tNum do
            local temp={};
            temp["name"]=OpFile:read();
            temp["damage"]=tonumber(OpFile:read());
            table.insert(head,temp);
        end
        tNum=tonumber(OpFile:read());
        for j=1,tNum do
            local temp={};
            temp["name"]=OpFile:read();
            temp["damage"]=tonumber(OpFile:read());
            temp["count"]=tonumber(OpFile:read());
            table.insert(op,temp);
        end
        unit["head"]=head;unit["op"]=op;
        table.insert(list,unit);
    end
    OpFile=config:read();
end

~= 是不等于的意思,和字面意思一样。

等待

仅需利用 os.pullEvent("redstone"),在输入红石信号后等待结束。因此我们可以利用外部的红石信号来控制乌龟停止等待,开始工作。

打开箱子

别整啥这么文雅的打开,直接把箱子挖了,反正效果一样。利用 turtle.dig() 来挖掘乌龟前方的方块,所以在放下乌龟时需要面对着箱子,如果方向放错了也没事,进入 Lua 后输入 turtle.turnLeft()左转,turtle.turnRight()右转。

确定合成表

t.select(1);t.dig();
local ttt=0;local flag=true;
for i=1,#list do
    ttt=ttt+1;
    flag=true;local head=list[i]["head"];
    local slot=1;local p;local q;
    t.select(slot);p=t.getItemDetail();
    for j=1,#head do
        q=p;
        flag=(flag and EqualItem(p,head[j]));
        if flag==false then
            break;
        end
        while EqualItem(p,q) do
            slot=slot+1;t.select(slot);
            p=t.getItemDetail();
        end
    end
    if flag then
        break;
    end
end

顺序一个一个合成表来看,如果全对上那就是这个合成表了,对不上继续搜索下一个。

操作

向后走一格,根据合成操作放东西到头顶上的输入总线。特别注意,输入总线不能朝下。

local slot=1;local op=list[ttt]["op"];
t.select(slot);
print("Start crafting No.",ttt,"recipe");
for i=1,#op do
    while not EqualItem(op[i],t.getItemDetail()) do
        slot=slot+1;t.select(slot);
    end
    t.back();t.dropUp(op[i]["count"]);
end
for i=1,#op do
    t.forward();
end
t.select(slot+1);
t.place();

RunASSline.lua

最后,加上外部大循环以及一开始必要的引导,可以得到最终的代码

--coded by ASQTTR
local ConfigDir="ASSlineConfig";
local log=io.open("log","w");
local t=turtle;
local config=io.open(ConfigDir);
local function EqualItem(l,r)
    if l==nil or r==nil then
        return false
    end
    if l["name"]==r["name"] and
    l["damage"]==r["damage"] then
        return true
    end
    return false
end
local FirstEntryINFO=
"We noticed that it is your first time to run this program, "..
"so we will give you a small tuition to help you get familiar with it.\n"..
"First, count the number of recipe groups you want to load in the turtle, "..
"and please input it.";
local iFile;
if config==nil then
    io.close(config);
    term.setTextColor(colors.lightGray);
    print(FirstEntryINFO);
    term.setTextColor(colors.purple);
    local num=io.read();
    config=io.open(ConfigDir,"w");
    term.setTextColor(colors.lightGray);
    print("Next, input the recipe group(s)");
    for i=1,num do
        term.setTextColor(colors.yellow);
        io.write(tostring(i)..':');
        term.setTextColor(colors.white);
        config:write(io.read("l")..'\n');
    end
    io.close(config);
    term.setTextColor(colors.lightGray);
    io.write("Your recipe group(s) path now is saving in this config file: ");
    term.setTextColor(colors.yellow);io.write(ConfigDir..'\n');
    term.setTextColor(colors.white);
    config=io.open(ConfigDir);
end
local list={};
local OpFile=config:read();
while OpFile do
    OpFile=io.open(OpFile);
    while OpFile:read()~="%" do end;
    local num=tonumber(OpFile:read());
    for i=1,num do
        local unit={};local head={};local op={};
        local tNum=tonumber(OpFile:read());
        for j=1,tNum do
            local temp={};
            temp["name"]=OpFile:read();
            temp["damage"]=tonumber(OpFile:read());
            table.insert(head,temp);
        end
        tNum=tonumber(OpFile:read());
        for j=1,tNum do
            local temp={};
            temp["name"]=OpFile:read();
            temp["damage"]=tonumber(OpFile:read());
            temp["count"]=tonumber(OpFile:read());
            table.insert(op,temp);
        end
        unit["head"]=head;unit["op"]=op;
        table.insert(list,unit);
    end
    OpFile=config:read();
end
while true do
print("Waiting for redstone signal..");
os.pullEvent("redstone");
t.select(1);t.dig();
local ttt=0;local flag=true;
for i=1,#list do
    ttt=ttt+1;
    flag=true;local head=list[i]["head"];
    local slot=1;local p;local q;
    t.select(slot);p=t.getItemDetail();
    for j=1,#head do
        q=p;
        flag=(flag and EqualItem(p,head[j]));
        if flag==false then
            break;
        end
        while EqualItem(p,q) do
            slot=slot+1;t.select(slot);
            p=t.getItemDetail();
        end
    end
    if flag then
        break;
    end
end
local slot=1;local op=list[ttt]["op"];
t.select(slot);
print("Start crafting No.",ttt,"recipe");
for i=1,#op do
    while not EqualItem(op[i],t.getItemDetail()) do
        slot=slot+1;t.select(slot);
    end
    t.back();t.dropUp(op[i]["count"]);
end
for i=1,#op do
    t.forward();
end
t.select(slot+1);
t.place();
end

发信说明

至于什么时候发出红石信号让乌龟开始工作,当然是当箱子中有物品且输入总线中没物品的时候,发送信号告诉乌龟你该去工作了。

课后习题

  1. 试说明使用顺序查找而非查找树或其他快速搜索法原因。

  2. 试说明 RunASSline.lua 无法处理 16 格物品装配线配方原因。

  3. 试说明 GetRecipe.lua 无法处理 16 格物品装配线配方原因。

  4. 试修改 RunASSline.lua,并合理布局 AE 系统,使得乌龟能自动补充燃料。

  5. 试利用第二只乌龟,编写恰当的程序与设计合理的布局,分配 AE 系统发出的物品给多条用装载了 RunASSline.lua 的乌龟维护的装配线,实现多条装配线并行处理。