Merge branch 'mc-1.16.x' into mc-1.18.x

This commit is contained in:
Jonathan Coates 2023-07-07 00:02:42 +01:00
commit edf372a695
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
24 changed files with 368 additions and 160 deletions

View File

@ -5,7 +5,7 @@ kotlin.stdlib.default.dependency=false
kotlin.jvm.target.validation.mode=error kotlin.jvm.target.validation.mode=error
# Mod properties # Mod properties
modVersion=1.101.2 modVersion=1.101.3
# Minecraft properties: We want to configure this here so we can read it in settings.gradle # Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.18.2 mcVersion=1.18.2

View File

@ -8,9 +8,13 @@
import com.google.common.net.InetAddresses; import com.google.common.net.InetAddresses;
import dan200.computercraft.ComputerCraft; import dan200.computercraft.ComputerCraft;
import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** /**
* A predicate on an address. Matches against a domain and an ip address. * A predicate on an address. Matches against a domain and an ip address.
@ -135,13 +139,36 @@ final class PrivatePattern implements AddressPredicate
{ {
static final PrivatePattern INSTANCE = new PrivatePattern(); static final PrivatePattern INSTANCE = new PrivatePattern();
private static final Set<InetAddress> additionalAddresses = Arrays.stream( new String[] {
// Block various cloud providers internal IPs.
"100.100.100.200", // Alibaba
"192.0.0.192", // Oracle
} ).map( InetAddresses::forString ).collect( Collectors.toSet() );
@Override @Override
public boolean matches( InetAddress socketAddress ) public boolean matches( InetAddress socketAddress )
{ {
return socketAddress.isAnyLocalAddress() return socketAddress.isAnyLocalAddress() // 0.0.0.0, ::0
|| socketAddress.isLoopbackAddress() || socketAddress.isLoopbackAddress() // 127.0.0.0/8, ::1
|| socketAddress.isLinkLocalAddress() || socketAddress.isLinkLocalAddress() // 169.254.0.0/16, fe80::/10
|| socketAddress.isSiteLocalAddress(); || socketAddress.isSiteLocalAddress() // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fec0::/10
|| socketAddress.isMulticastAddress() // 224.0.0.0/4, ff00::/8
|| isUniqueLocalAddress( socketAddress ) // fd00::/8
|| additionalAddresses.contains( socketAddress );
}
/**
* Determine if an IP address lives inside the ULA address range.
*
* @param address The IP address to test.
* @return Whether this address sits in the ULA address range.
* @see <a href="https://en.wikipedia.org/wiki/Unique_local_address">Unique local address on Wikipedia</a>
*/
private boolean isUniqueLocalAddress( InetAddress address )
{
// ULA is actually defined as fc00::/7 (so both fc00::/8 and fd00::/8). However, only the latter is actually
// defined right now, so let's be conservative.
return address instanceof Inet6Address && (address.getAddress()[0] & 0xff) == 0xfd;
} }
} }

View File

@ -7,10 +7,13 @@
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.core.ServerComputer;
@ -200,7 +203,10 @@ else if( b.getLevel() == world )
.then( command( "queue" ) .then( command( "queue" )
.requires( UserLevel.ANYONE ) .requires( UserLevel.ANYONE )
.arg( "computer", manyComputers() ) .arg(
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument( "computer", manyComputers() )
.suggests( ( context, builder ) -> Suggestions.empty() )
)
.argManyValue( "args", StringArgumentType.string(), Collections.emptyList() ) .argManyValue( "args", StringArgumentType.string(), Collections.emptyList() )
.executes( ( ctx, args ) -> { .executes( ( ctx, args ) -> {
Collection<ServerComputer> computers = getComputersArgument( ctx, "computer" ); Collection<ServerComputer> computers = getComputersArgument( ctx, "computer" );

View File

@ -61,12 +61,36 @@ public boolean test( CommandSourceStack source )
return source.hasPermission( toLevel() ); return source.hasPermission( toLevel() );
} }
/**
* Take the union of two {@link UserLevel}s.
* <p>
* This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a b).test(s)}.
*
* @param left The first user level to take the union of.
* @param right The second user level to take the union of.
* @return The union of two levels.
*/
public static UserLevel union( UserLevel left, UserLevel right )
{
if( left == right ) return left;
// x ANYONE = ANYONE
if( left == ANYONE || right == ANYONE ) return ANYONE;
// x OWNER = OWNER
if( left == OWNER ) return right;
if( right == OWNER ) return left;
// At this point, we have x != y and x, y { OP, OWNER_OP }.
return OWNER_OP;
}
private static boolean isOwner( CommandSourceStack source ) private static boolean isOwner( CommandSourceStack source )
{ {
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
Entity sender = source.getEntity(); Entity sender = source.getEntity();
return server.isDedicatedServer() return server.isDedicatedServer()
? source.getEntity() == null && source.hasPermission( 4 ) && source.getTextName().equals( "Server" ) ? source.getEntity() == null && source.hasPermission( 4 ) && source.getTextName().equals( "Server" )
: sender instanceof Player player && player.getGameProfile().getName().equalsIgnoreCase( server.getServerModName() ); : sender instanceof Player player && server.isSingleplayerOwner( player.getGameProfile() );
} }
} }

View File

@ -52,10 +52,15 @@ public CommandBuilder<S> requires( Predicate<S> predicate )
return this; return this;
} }
public CommandBuilder<S> arg( ArgumentBuilder<S, ?> arg )
{
args.add( arg );
return this;
}
public CommandBuilder<S> arg( String name, ArgumentType<?> type ) public CommandBuilder<S> arg( String name, ArgumentType<?> type )
{ {
args.add( RequiredArgumentBuilder.argument( name, type ) ); return arg( RequiredArgumentBuilder.argument( name, type ) );
return this;
} }
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue( String name, ArgumentType<T> type, List<T> empty ) public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue( String name, ArgumentType<T> type, List<T> empty )
@ -84,7 +89,7 @@ private <T, U> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany( String nam
return command -> { return command -> {
// The node for no arguments // The node for no arguments
ArgumentBuilder<S, ?> tail = tail( ctx -> command.run( ctx, empty.get() ) ); ArgumentBuilder<S, ?> tail = setupTail( ctx -> command.run( ctx, empty.get() ) );
// The node for one or more arguments // The node for one or more arguments
ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder
@ -93,7 +98,7 @@ private <T, U> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany( String nam
// Chain all of them together! // Chain all of them together!
tail.then( moreArg ); tail.then( moreArg );
return link( tail ); return buildTail( tail );
}; };
} }
@ -106,22 +111,18 @@ private static <T> List<T> getList( CommandContext<?> context, String name )
@Override @Override
public CommandNode<S> executes( Command<S> command ) public CommandNode<S> executes( Command<S> command )
{ {
if( args.isEmpty() ) throw new IllegalStateException( "Cannot have empty arg chain builder" ); return buildTail( setupTail( command ) );
return link( tail( command ) );
} }
private ArgumentBuilder<S, ?> tail( Command<S> command ) private ArgumentBuilder<S, ?> setupTail( Command<S> command )
{ {
ArgumentBuilder<S, ?> defaultTail = args.get( args.size() - 1 ); return args.get( args.size() - 1 ).executes( command );
defaultTail.executes( command );
if( requires != null ) defaultTail.requires( requires );
return defaultTail;
} }
private CommandNode<S> link( ArgumentBuilder<S, ?> tail ) private CommandNode<S> buildTail( ArgumentBuilder<S, ?> tail )
{ {
for( int i = args.size() - 2; i >= 0; i-- ) tail = args.get( i ).then( tail ); for( int i = args.size() - 2; i >= 0; i-- ) tail = args.get( i ).then( tail );
if( requires != null ) tail.requires( requires );
return tail.build(); return tail.build();
} }
} }

View File

@ -12,6 +12,7 @@
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode;
import dan200.computercraft.shared.command.UserLevel;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.ClickEvent;
@ -22,6 +23,10 @@
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static dan200.computercraft.shared.command.text.ChatHelpers.coloured; import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
import static dan200.computercraft.shared.command.text.ChatHelpers.translate; import static dan200.computercraft.shared.command.text.ChatHelpers.translate;
@ -44,6 +49,33 @@ public static HelpingArgumentBuilder choice( String literal )
return new HelpingArgumentBuilder( literal ); return new HelpingArgumentBuilder( literal );
} }
@Override
public LiteralArgumentBuilder<CommandSourceStack> requires( Predicate<CommandSourceStack> requirement )
{
throw new IllegalStateException( "Cannot use requires on a HelpingArgumentBuilder" );
}
@Override
public Predicate<CommandSourceStack> getRequirement()
{
// The requirement of this node is the union of all child's requirements.
List<Predicate<CommandSourceStack>> requirements = Stream.concat(
children.stream().map( ArgumentBuilder::getRequirement ),
getArguments().stream().map( CommandNode::getRequirement )
).collect( Collectors.toList() );
// If all requirements are a UserLevel, take the union of those instead.
UserLevel userLevel = UserLevel.OWNER;
for( Predicate<CommandSourceStack> requirement : requirements )
{
if( !(requirement instanceof UserLevel level) ) return x -> requirements.stream().anyMatch( y -> y.test( x ) );
userLevel = UserLevel.union( userLevel, level );
}
return userLevel;
}
@Override @Override
public LiteralArgumentBuilder<CommandSourceStack> executes( final Command<CommandSourceStack> command ) public LiteralArgumentBuilder<CommandSourceStack> executes( final Command<CommandSourceStack> command )
{ {
@ -99,9 +131,7 @@ private LiteralCommandNode<CommandSourceStack> buildImpl( String id, String comm
helpCommand.node = node; helpCommand.node = node;
// Set up a /... help command // Set up a /... help command
LiteralArgumentBuilder<CommandSourceStack> helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal( "help" ) LiteralArgumentBuilder<CommandSourceStack> helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal( "help" ).executes( helpCommand );
.requires( x -> getArguments().stream().anyMatch( y -> y.getRequirement().test( x ) ) )
.executes( helpCommand );
// Add all normal command children to this and the help node // Add all normal command children to this and the help node
for( CommandNode<CommandSourceStack> child : getArguments() ) for( CommandNode<CommandSourceStack> child : getArguments() )

View File

@ -167,7 +167,7 @@ public void setPlacedBy( @Nonnull Level world, @Nonnull BlockPos pos, @Nonnull B
@Override @Override
public float getExplosionResistance( BlockState state, BlockGetter world, BlockPos pos, Explosion explosion ) public float getExplosionResistance( BlockState state, BlockGetter world, BlockPos pos, Explosion explosion )
{ {
Entity exploder = explosion.getExploder(); Entity exploder = explosion == null ? null : explosion.getExploder();
if( getFamily() == ComputerFamily.ADVANCED || exploder instanceof LivingEntity || exploder instanceof AbstractHurtingProjectile ) if( getFamily() == ComputerFamily.ADVANCED || exploder instanceof LivingEntity || exploder instanceof AbstractHurtingProjectile )
{ {
return 2000; return 2000;

View File

@ -82,7 +82,7 @@ public TurtleModem( ResourceLocation id, ItemStack stack, boolean advanced )
} }
else else
{ {
leftOffModel = new ResourceLocation( ComputerCraft.MOD_ID, "turtle_modem_normal_off_left" ); leftOffModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_off_left" );
rightOffModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_off_right" ); rightOffModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_off_right" );
leftOnModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_on_left" ); leftOnModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_on_left" );
rightOnModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_on_right" ); rightOnModel = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_modem_normal_on_right" );

View File

@ -153,12 +153,22 @@ function locate(_nTimeout, _bDebug)
if tFix.nDistance == 0 then if tFix.nDistance == 0 then
pos1, pos2 = tFix.vPosition, nil pos1, pos2 = tFix.vPosition, nil
else else
table.insert(tFixes, tFix) -- Insert our new position in our table, with a maximum of three items. If this is close to a
-- previous position, replace that instead of inserting.
local insIndex = math.min(3, #tFixes + 1)
for i, older in pairs(tFixes) do
if (older.vPosition - tFix.vPosition):length() < 1 then
insIndex = i
break
end
end
tFixes[insIndex] = tFix
if #tFixes >= 3 then if #tFixes >= 3 then
if not pos1 then if not pos1 then
pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[#tFixes]) pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[3])
else else
pos1, pos2 = narrow(pos1, pos2, tFixes[#tFixes]) pos1, pos2 = narrow(pos1, pos2, tFixes[3])
end end
end end
end end

View File

@ -74,8 +74,7 @@ function undefine(name)
details[name] = nil details[name] = nil
end end
local function set_value(name, value) local function set_value(name, new)
local new = reserialize(value)
local old = values[name] local old = values[name]
if old == nil then if old == nil then
local opt = details[name] local opt = details[name]
@ -103,7 +102,7 @@ function set(name, value)
local opt = details[name] local opt = details[name]
if opt and opt.type then expect(2, value, opt.type) end if opt and opt.type then expect(2, value, opt.type) end
set_value(name, value) set_value(name, reserialize(value))
end end
--- Get the value of a setting. --- Get the value of a setting.
@ -214,7 +213,9 @@ function load(sPath)
if type(k) == "string" and (ty_v == "string" or ty_v == "number" or ty_v == "boolean" or ty_v == "table") then if type(k) == "string" and (ty_v == "string" or ty_v == "number" or ty_v == "boolean" or ty_v == "table") then
local opt = details[k] local opt = details[k]
if not opt or not opt.type or ty_v == opt.type then if not opt or not opt.type or ty_v == opt.type then
set_value(k, v) -- This may fail if the table is recursive (or otherwise cannot be serialized).
local ok, v = pcall(reserialize, v)
if ok then set_value(k, v) end
end end
end end
end end

View File

@ -292,6 +292,13 @@ local g_tLuaKeywords = {
["while"] = true, ["while"] = true,
} }
--- A version of the ipairs iterator which ignores metamethods
local function inext(tbl, i)
i = (i or 0) + 1
local v = rawget(tbl, i)
if v == nil then return nil else return i, v end
end
local serialize_infinity = math.huge local serialize_infinity = math.huge
local function serialize_impl(t, tracking, indent, opts) local function serialize_impl(t, tracking, indent, opts)
local sType = type(t) local sType = type(t)
@ -318,11 +325,11 @@ local function serialize_impl(t, tracking, indent, opts)
result = open result = open
local seen_keys = {} local seen_keys = {}
for k, v in ipairs(t) do for k, v in inext, t do
seen_keys[k] = true seen_keys[k] = true
result = result .. sub_indent .. serialize_impl(v, tracking, sub_indent, opts) .. comma result = result .. sub_indent .. serialize_impl(v, tracking, sub_indent, opts) .. comma
end end
for k, v in pairs(t) do for k, v in next, t do
if not seen_keys[k] then if not seen_keys[k] then
local sEntry local sEntry
if type(k) == "string" and not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then if type(k) == "string" and not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then

View File

@ -127,11 +127,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
local sEmptyTextColor = tEmptyColorLines[nTextColor] local sEmptyTextColor = tEmptyColorLines[nTextColor]
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
for y = 1, nHeight do for y = 1, nHeight do
tLines[y] = { tLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor }
text = sEmptyText,
textColor = sEmptyTextColor,
backgroundColor = sEmptyBackgroundColor,
}
end end
for i = 0, 15 do for i = 0, 15 do
@ -161,7 +157,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
local function redrawLine(n) local function redrawLine(n)
local tLine = tLines[n] local tLine = tLines[n]
parent.setCursorPos(nX, nY + n - 1) parent.setCursorPos(nX, nY + n - 1)
parent.blit(tLine.text, tLine.textColor, tLine.backgroundColor) parent.blit(tLine[1], tLine[2], tLine[3])
end end
local function redraw() local function redraw()
@ -184,9 +180,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
-- Modify line -- Modify line
local tLine = tLines[nCursorY] local tLine = tLines[nCursorY]
if nStart == 1 and nEnd == nWidth then if nStart == 1 and nEnd == nWidth then
tLine.text = sText tLine[1] = sText
tLine.textColor = sTextColor tLine[2] = sTextColor
tLine.backgroundColor = sBackgroundColor tLine[3] = sBackgroundColor
else else
local sClippedText, sClippedTextColor, sClippedBackgroundColor local sClippedText, sClippedTextColor, sClippedBackgroundColor
if nStart < 1 then if nStart < 1 then
@ -206,9 +202,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
sClippedBackgroundColor = sBackgroundColor sClippedBackgroundColor = sBackgroundColor
end end
local sOldText = tLine.text local sOldText = tLine[1]
local sOldTextColor = tLine.textColor local sOldTextColor = tLine[2]
local sOldBackgroundColor = tLine.backgroundColor local sOldBackgroundColor = tLine[3]
local sNewText, sNewTextColor, sNewBackgroundColor local sNewText, sNewTextColor, sNewBackgroundColor
if nStart > 1 then if nStart > 1 then
local nOldEnd = nStart - 1 local nOldEnd = nStart - 1
@ -227,9 +223,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth) sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth)
end end
tLine.text = sNewText tLine[1] = sNewText
tLine.textColor = sNewTextColor tLine[2] = sNewTextColor
tLine.backgroundColor = sNewBackgroundColor tLine[3] = sNewBackgroundColor
end end
-- Redraw line -- Redraw line
@ -276,11 +272,10 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
local sEmptyTextColor = tEmptyColorLines[nTextColor] local sEmptyTextColor = tEmptyColorLines[nTextColor]
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
for y = 1, nHeight do for y = 1, nHeight do
tLines[y] = { local line = tLines[y]
text = sEmptyText, line[1] = sEmptyText
textColor = sEmptyTextColor, line[2] = sEmptyTextColor
backgroundColor = sEmptyBackgroundColor, line[3] = sEmptyBackgroundColor
}
end end
if bVisible then if bVisible then
redraw() redraw()
@ -291,14 +286,10 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
function window.clearLine() function window.clearLine()
if nCursorY >= 1 and nCursorY <= nHeight then if nCursorY >= 1 and nCursorY <= nHeight then
local sEmptyText = sEmptySpaceLine local line = tLines[nCursorY]
local sEmptyTextColor = tEmptyColorLines[nTextColor] line[1] = sEmptySpaceLine
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] line[2] = tEmptyColorLines[nTextColor]
tLines[nCursorY] = { line[3] = tEmptyColorLines[nBackgroundColor]
text = sEmptyText,
textColor = sEmptyTextColor,
backgroundColor = sEmptyBackgroundColor,
}
if bVisible then if bVisible then
redrawLine(nCursorY) redrawLine(nCursorY)
updateCursorColor() updateCursorColor()
@ -427,11 +418,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
if y >= 1 and y <= nHeight then if y >= 1 and y <= nHeight then
tNewLines[newY] = tLines[y] tNewLines[newY] = tLines[y]
else else
tNewLines[newY] = { tNewLines[newY] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor }
text = sEmptyText,
textColor = sEmptyTextColor,
backgroundColor = sEmptyBackgroundColor,
}
end end
end end
tLines = tNewLines tLines = tNewLines
@ -474,7 +461,8 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
error("Line is out of range.", 2) error("Line is out of range.", 2)
end end
return tLines[y].text, tLines[y].textColor, tLines[y].backgroundColor local line = tLines[y]
return line[1], line[2], line[3]
end end
-- Other functions -- Other functions
@ -570,26 +558,22 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
for y = 1, new_height do for y = 1, new_height do
if y > nHeight then if y > nHeight then
tNewLines[y] = { tNewLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor }
text = sEmptyText,
textColor = sEmptyTextColor,
backgroundColor = sEmptyBackgroundColor,
}
else else
local tOldLine = tLines[y] local tOldLine = tLines[y]
if new_width == nWidth then if new_width == nWidth then
tNewLines[y] = tOldLine tNewLines[y] = tOldLine
elseif new_width < nWidth then elseif new_width < nWidth then
tNewLines[y] = { tNewLines[y] = {
text = string_sub(tOldLine.text, 1, new_width), string_sub(tOldLine[1], 1, new_width),
textColor = string_sub(tOldLine.textColor, 1, new_width), string_sub(tOldLine[2], 1, new_width),
backgroundColor = string_sub(tOldLine.backgroundColor, 1, new_width), string_sub(tOldLine[3], 1, new_width),
} }
else else
tNewLines[y] = { tNewLines[y] = {
text = tOldLine.text .. string_sub(sEmptyText, nWidth + 1, new_width), tOldLine[1] .. string_sub(sEmptyText, nWidth + 1, new_width),
textColor = tOldLine.textColor .. string_sub(sEmptyTextColor, nWidth + 1, new_width), tOldLine[2] .. string_sub(sEmptyTextColor, nWidth + 1, new_width),
backgroundColor = tOldLine.backgroundColor .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width), tOldLine[3] .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width),
} }
end end
end end

View File

@ -1,3 +1,21 @@
# New features in CC: Tweaked 1.101.3
* Improve syntax errors when missing commas in tables, and on trailing commas in parameter lists.
* `speaker` program now reports an error on common unsupported audio formats.
* Small optimisations to the `window` API.
Several bug fixes:
* Fix the REPL syntax reporting crashing on valid parses.
* Ignore metatables in `textutils.serialize`.
* Fix `gps.locate` returning `nan` when receiving a duplicate location (Wojbie).
* Ignore metatables in `textutils.serialize`.
* Fix wireless turtles having an invalid model.
* Fix crash when turtles are exploded by a null explosion.
* Lua REPL no longer accepts `)(` as a valid expression.
* Fix several inconsistencies with `require`/`package.path` in the Lua REPL (Wojbie).
* Fix private several IP address ranges not being blocked by the `$private` rule.
* Improve permission checks in the `/computercraft` command.
# New features in CC: Tweaked 1.101.2 # New features in CC: Tweaked 1.101.2
* Error messages in `edit` are now displayed in red on advanced computers. * Error messages in `edit` are now displayed in red on advanced computers.

View File

@ -1,16 +1,19 @@
New features in CC: Tweaked 1.101.2 New features in CC: Tweaked 1.101.3
* Error messages in `edit` are now displayed in red on advanced computers. * Improve syntax errors when missing commas in tables, and on trailing commas in parameter lists.
* Improvements to the display of errors in the shell and REPL. * `speaker` program now reports an error on common unsupported audio formats.
* Small optimisations to the `window` API.
Several bug fixes: Several bug fixes:
* Fix `import.lua` failing to upload a file. * Fix the REPL syntax reporting crashing on valid parses.
* Fix several issues with sparse Lua tables (Shiranuit). * Ignore metatables in `textutils.serialize`.
* Computer upgrades now accept normal computers, rather than uselessly allowing you to upgrade an advanced computer to an advanced computer! * Fix `gps.locate` returning `nan` when receiving a duplicate location (Wojbie).
* Correctly clamp speaker volume. * Ignore metatables in `textutils.serialize`.
* Fix rednet queueing the wrong message when sending a message to the current computer. * Fix wireless turtles having an invalid model.
* Fix the Lua VM crashing when a `__len` metamethod yields. * Fix crash when turtles are exploded by a null explosion.
* Trim spaces from filesystem paths. * Lua REPL no longer accepts `)(` as a valid expression.
* Correctly format 12AM/PM with `%I`. * Fix several inconsistencies with `require`/`package.path` in the Lua REPL (Wojbie).
* Fix private several IP address ranges not being blocked by the `$private` rule.
* Improve permission checks in the `/computercraft` command.
Type "help changelog" to see the full version history. Type "help changelog" to see the full version history.

View File

@ -364,6 +364,48 @@ function errors.table_key_equals(start_pos, end_pos)
} }
end end
--[[- There is a trailing comma in this list of function arguments.
@tparam number token The token id.
@tparam number token_start The start position of the token.
@tparam number token_end The end position of the token.
@tparam number prev The start position of the previous entry.
@treturn table The resulting parse error.
]]
function errors.missing_table_comma(token, token_start, token_end, prev)
expect(1, token, "number")
expect(2, token_start, "number")
expect(3, token_end, "number")
expect(4, prev, "number")
return {
"Unexpected " .. token_names[token] .. " in table.",
annotate(token_start, token_end),
annotate(prev + 1, prev + 1, "Are you missing a comma here?"),
}
end
--[[- There is a trailing comma in this list of function arguments.
@tparam number comma_start The start position of the `,` token.
@tparam number comma_end The end position of the `,` token.
@tparam number paren_start The start position of the `)` token.
@tparam number paren_end The end position of the `)` token.
@treturn table The resulting parse error.
]]
function errors.trailing_call_comma(comma_start, comma_end, paren_start, paren_end)
expect(1, comma_start, "number")
expect(2, comma_end, "number")
expect(3, paren_start, "number")
expect(4, paren_end, "number")
return {
"Unexpected " .. code(")") .. " in function call.",
annotate(paren_start, paren_end),
annotate(comma_start, comma_end, "Tip: Try removing this " .. code(",") .. "."),
}
end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Statement parsing errors -- Statement parsing errors
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@ -17,6 +17,8 @@ local error_printer = require "cc.internal.error_printer"
local error_sentinel = {} local error_sentinel = {}
local function make_context(input) local function make_context(input)
expect(1, input, "string")
local context = {} local context = {}
local lines = { 1 } local lines = { 1 }
@ -69,8 +71,9 @@ local function parse(input, start_symbol)
expect(2, start_symbol, "number") expect(2, start_symbol, "number")
local context = make_context(input) local context = make_context(input)
function context.report(msg) function context.report(msg, ...)
expect(1, msg, "table") expect(1, msg, "table", "function")
if type(msg) == "function" then msg = msg(...) end
error_printer(context, msg) error_printer(context, msg)
error(error_sentinel) error(error_sentinel)
end end
@ -106,8 +109,9 @@ local function parse_repl(input)
local context = make_context(input) local context = make_context(input)
local last_error = nil local last_error = nil
function context.report(msg) function context.report(msg, ...)
expect(1, msg, "table") expect(1, msg, "table", "function")
if type(msg) == "function" then msg = msg(...) end
last_error = msg last_error = msg
error(error_sentinel) error(error_sentinel)
end end
@ -120,22 +124,35 @@ local function parse_repl(input)
assert(coroutine.resume(parsers[i], context, coroutine.yield, start_code)) assert(coroutine.resume(parsers[i], context, coroutine.yield, start_code))
end end
-- Run all parsers together in parallel, feeding them one token at a time.
-- Once all parsers have failed, report the last failure (corresponding to
-- the longest parse).
local ok, err = pcall(function() local ok, err = pcall(function()
local parsers_n = #parsers local parsers_n = #parsers
while true do while true do
local token, start, finish = lexer() local token, start, finish = lexer()
local stop = true local all_failed = true
for i = 1, parsers_n do for i = 1, parsers_n do
local parser = parsers[i] local parser = parsers[i]
if coroutine.status(parser) ~= "dead" then if parser then
stop = false
local ok, err = coroutine.resume(parser, token, start, finish) local ok, err = coroutine.resume(parser, token, start, finish)
if not ok and err ~= error_sentinel then error(err, 0) end if ok then
-- This parser accepted our input, succeed immediately.
if coroutine.status(parser) == "dead" then return end
all_failed = false -- Otherwise continue parsing.
elseif err ~= error_sentinel then
-- An internal error occurred: propagate it.
error(err, 0)
else
-- The parser failed, stub it out so we don't try to continue using it.
parsers[i] = false
end
end end
end end
if stop then error(error_sentinel) end if all_failed then error(error_sentinel) end
end end
end) end)

View File

@ -92,7 +92,7 @@ local function lex_number(context, str, start)
local contents = sub(str, start, pos - 1) local contents = sub(str, start, pos - 1)
if not tonumber(contents) then if not tonumber(contents) then
-- TODO: Separate error for "2..3"? -- TODO: Separate error for "2..3"?
context.report(errors.malformed_number(start, pos - 1)) context.report(errors.malformed_number, start, pos - 1)
end end
return tokens.NUMBER, pos - 1 return tokens.NUMBER, pos - 1
@ -114,14 +114,14 @@ local function lex_string(context, str, start_pos, quote)
return tokens.STRING, pos return tokens.STRING, pos
elseif c == "\n" or c == "\r" or c == "" then elseif c == "\n" or c == "\r" or c == "" then
-- We don't call newline here, as that's done for the next token. -- We don't call newline here, as that's done for the next token.
context.report(errors.unfinished_string(start_pos, pos, quote)) context.report(errors.unfinished_string, start_pos, pos, quote)
return tokens.STRING, pos - 1 return tokens.STRING, pos - 1
elseif c == "\\" then elseif c == "\\" then
c = sub(str, pos + 1, pos + 1) c = sub(str, pos + 1, pos + 1)
if c == "\n" or c == "\r" then if c == "\n" or c == "\r" then
pos = newline(context, str, pos + 1, c) pos = newline(context, str, pos + 1, c)
elseif c == "" then elseif c == "" then
context.report(errors.unfinished_string_escape(start_pos, pos, quote)) context.report(errors.unfinished_string_escape, start_pos, pos, quote)
return tokens.STRING, pos return tokens.STRING, pos
elseif c == "z" then elseif c == "z" then
pos = pos + 2 pos = pos + 2
@ -129,7 +129,7 @@ local function lex_string(context, str, start_pos, quote)
local next_pos, _, c = find(str, "([%S\r\n])", pos) local next_pos, _, c = find(str, "([%S\r\n])", pos)
if not next_pos then if not next_pos then
context.report(errors.unfinished_string(start_pos, #str, quote)) context.report(errors.unfinished_string, start_pos, #str, quote)
return tokens.STRING, #str return tokens.STRING, #str
end end
@ -192,7 +192,7 @@ local function lex_long_str(context, str, start, len)
elseif c == "[" then elseif c == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[") local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[")
if ok and boundary_pos - pos == len and len == 1 then if ok and boundary_pos - pos == len and len == 1 then
context.report(errors.nested_long_str(pos, boundary_pos)) context.report(errors.nested_long_str, pos, boundary_pos)
end end
pos = boundary_pos pos = boundary_pos
@ -234,12 +234,12 @@ local function lex_token(context, str, pos)
local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos) local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos)
if end_pos then return tokens.STRING, end_pos end if end_pos then return tokens.STRING, end_pos end
context.report(errors.unfinished_long_string(pos, boundary_pos, boundary_pos - pos)) context.report(errors.unfinished_long_string, pos, boundary_pos, boundary_pos - pos)
return tokens.ERROR, #str return tokens.ERROR, #str
elseif pos + 1 == boundary_pos then -- Just a "[" elseif pos + 1 == boundary_pos then -- Just a "["
return tokens.OSQUARE, pos return tokens.OSQUARE, pos
else -- Malformed long string, for instance "[=" else -- Malformed long string, for instance "[="
context.report(errors.malformed_long_string(pos, boundary_pos, boundary_pos - pos)) context.report(errors.malformed_long_string, pos, boundary_pos, boundary_pos - pos)
return tokens.ERROR, boundary_pos return tokens.ERROR, boundary_pos
end end
@ -256,7 +256,7 @@ local function lex_token(context, str, pos)
local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos) local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos)
if end_pos then return tokens.COMMENT, end_pos end if end_pos then return tokens.COMMENT, end_pos end
context.report(errors.unfinished_long_comment(pos, boundary_pos, boundary_pos - comment_pos)) context.report(errors.unfinished_long_comment, pos, boundary_pos, boundary_pos - comment_pos)
return tokens.ERROR, #str return tokens.ERROR, #str
end end
end end
@ -313,18 +313,18 @@ local function lex_token(context, str, pos)
if end_pos - pos <= 3 then if end_pos - pos <= 3 then
local contents = sub(str, pos, end_pos) local contents = sub(str, pos, end_pos)
if contents == "&&" then if contents == "&&" then
context.report(errors.wrong_and(pos, end_pos)) context.report(errors.wrong_and, pos, end_pos)
return tokens.AND, end_pos return tokens.AND, end_pos
elseif contents == "||" then elseif contents == "||" then
context.report(errors.wrong_or(pos, end_pos)) context.report(errors.wrong_or, pos, end_pos)
return tokens.OR, end_pos return tokens.OR, end_pos
elseif contents == "!=" or contents == "<>" then elseif contents == "!=" or contents == "<>" then
context.report(errors.wrong_ne(pos, end_pos)) context.report(errors.wrong_ne, pos, end_pos)
return tokens.NE, end_pos return tokens.NE, end_pos
end end
end end
context.report(errors.unexpected_character(pos)) context.report(errors.unexpected_character, pos)
return tokens.ERROR, end_pos return tokens.ERROR, end_pos
end end
end end

File diff suppressed because one or more lines are too long

View File

@ -107,7 +107,9 @@ local function set_status(text, ok)
status_text = text status_text = text
end end
if not bReadOnly and fs.getFreeSpace(sPath) < 1024 then if bReadOnly then
set_status("File is read only", false)
elseif fs.getFreeSpace(sPath) < 1024 then
set_status("Disk is low on space", false) set_status("Disk is low on space", false)
else else
local message local message

View File

@ -26,6 +26,12 @@ local function pcm_decoder(chunk)
return buffer return buffer
end end
local function report_invalid_format(format)
printError(("speaker cannot play %s files."):format(format))
local pp = require "cc.pretty"
pp.print("Run '" .. pp.text("help speaker", colours.lightGrey) .. "' for information on supported formats.")
end
local cmd = ... local cmd = ...
if cmd == "stop" then if cmd == "stop" then
@ -93,6 +99,11 @@ elseif cmd == "play" then
handle.read(4) handle.read(4)
start = nil start = nil
-- Detect several other common audio files.
elseif start == "OggS" then return report_invalid_format("Ogg")
elseif start == "fLaC" then return report_invalid_format("FLAC")
elseif start:sub(1, 3) == "ID3" then return report_invalid_format("MP3")
elseif start == "<!DO" --[[<!DOCTYPE]] then return report_invalid_format("HTML")
end end
print("Playing " .. file) print("Playing " .. file)

View File

@ -21,21 +21,13 @@ local tEnv = {
} }
setmetatable(tEnv, { __index = _ENV }) setmetatable(tEnv, { __index = _ENV })
-- Replace our package.path, so that it loads from the current directory, rather -- Replace our require with new instance that loads from the current directory
-- than from /rom/programs. This makes it a little more friendly to use and -- rather than from /rom/programs. This makes it more friendly to use and closer
-- closer to what you'd expect. -- to what you'd expect.
do do
local make_package = require "cc.require".make
local dir = shell.dir() local dir = shell.dir()
if dir:sub(1, 1) ~= "/" then dir = "/" .. dir end _ENV.require, _ENV.package = make_package(_ENV, dir)
if dir:sub(-1) ~= "/" then dir = dir .. "/" end
local strip_path = "?;?.lua;?/init.lua;"
local path = package.path
if path:sub(1, #strip_path) == strip_path then
path = path:sub(#strip_path + 1)
end
package.path = dir .. "?;" .. dir .. "?.lua;" .. dir .. "?/init.lua;" .. path
end end
if term.isColour() then if term.isColour() then
@ -78,18 +70,13 @@ while running do
local name, offset = "=lua[" .. chunk_idx .. "]", 0 local name, offset = "=lua[" .. chunk_idx .. "]", 0
local force_print = 0
local func, err = load(input, name, "t", tEnv) local func, err = load(input, name, "t", tEnv)
if load("return " .. input) then
local expr_func = load("return _echo(" .. input .. ");", name, "t", tEnv) -- We wrap the expression with a call to _echo(...), which prevents tail
if not func then -- calls (and thus confusing errors). Note we check this is a valid
if expr_func then -- expression separately, to avoid accepting inputs like `)--` (which are
func = expr_func -- parsed as `_echo()--)`.
offset = 13 func = load("return _echo(" .. input .. "\n)", name, "t", tEnv)
force_print = 1
end
elseif expr_func then
func = expr_func
offset = 13 offset = 13
end end
@ -99,9 +86,8 @@ while running do
local results = table.pack(exception.try(func)) local results = table.pack(exception.try(func))
if results[1] then if results[1] then
local n = 1 for i = 2, results.n do
while n < results.n or n <= force_print do local value = results[i]
local value = results[n + 1]
local ok, serialised = pcall(pretty.pretty, value, { local ok, serialised = pcall(pretty.pretty, value, {
function_args = settings.get("lua.function_args"), function_args = settings.get("lua.function_args"),
function_source = settings.get("lua.function_source"), function_source = settings.get("lua.function_source"),
@ -111,7 +97,6 @@ while running do
else else
print(tostring(value)) print(tostring(value))
end end
n = n + 1
end end
else else
printError(results[2]) printError(results[2])

View File

@ -34,7 +34,14 @@ public void matchesPort()
@ValueSource( strings = { @ValueSource( strings = {
"0.0.0.0", "[::]", "0.0.0.0", "[::]",
"localhost", "127.0.0.1.nip.io", "127.0.0.1", "[::1]", "localhost", "127.0.0.1.nip.io", "127.0.0.1", "[::1]",
"172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1" "172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1",
// Multicast
"224.0.0.1", "ff02::1",
// Cloud metadata providers
"100.100.100.200", // Alibaba
"192.0.0.192", // Oracle
"fd00:ec2::254", // AWS
"169.254.169.254" // AWS, Digital Ocean, GCP, etc..
} ) } )
public void blocksLocalDomains( String domain ) public void blocksLocalDomains( String domain )
{ {

View File

@ -49,4 +49,18 @@ describe("cc.internal.syntax", function()
describe_golden("the lexer", "lexer_spec.md", true) describe_golden("the lexer", "lexer_spec.md", true)
describe_golden("the parser", "parser_spec.md", false) describe_golden("the parser", "parser_spec.md", false)
describe_golden("the parser (all states)", "parser_exhaustive_spec.md", false) describe_golden("the parser (all states)", "parser_exhaustive_spec.md", false)
describe("the REPL input parser", function()
it("returns true when accepted by both parsers", function()
helpers.with_window(50, 10, function()
expect(syntax.parse_repl("print(x)")):eq(true)
end)
end)
it("returns true when accepted by one parser", function()
helpers.with_window(50, 10, function()
expect(syntax.parse_repl("x")):eq(true)
end)
end)
end)
end) end)

View File

@ -45,8 +45,9 @@ local function capture_parser(input, print_tokens, start)
end end
local context = make_context(input) local context = make_context(input)
function context.report(message) function context.report(message, ...)
expect(3, message, "table") expect(3, message, "table", "function")
if type(message) == "function" then message = message(...) end
for _, msg in ipairs(message) do for _, msg in ipairs(message) do
if type(msg) == "table" and msg.tag == "annotate" then if type(msg) == "table" and msg.tag == "annotate" then