1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-22 17:37:38 +00:00

Compare commits

...

75 Commits

Author SHA1 Message Date
Jonathan Coates
3493159a05 Bump CC:T to 1.109.7 2024-03-10 19:10:09 +00:00
Jonathan Coates
eead67e314 Fix a couple of warnings 2024-03-10 12:04:40 +00:00
Jonathan Coates
3b8813cf8f Slightly more detailed negative allocation logging
Hopefully will help debug #1739. Maybe.
2024-03-10 11:26:36 +00:00
Jonathan Coates
a9191a4d4e Don't cache the client monitor
When rendering non-origin monitors, we would fetch the origin monitor,
read its client state, and then cache that on the current monitor to
avoid repeated lookups.

However, if the origin monitor is unloaded/removed on the client, and
then loaded agin, this cache will be not be invalidated, causing us to
render both the old and new monitor!

I think the correct thing to do here is cache the origin monitor. This
allows us to check when the origin monitor has been removed, and
invalidate the cache if needed.

However, I'm wary of any other edge cases here, so for now we do
something much simpler, and remove the cache entirely. This does mean
that monitors now need to perform extra block entity lookups, but the
performance cost doesn't appear to be too bad.

Fixes #1741
2024-03-10 10:57:56 +00:00
Jonathan Coates
451a2593ce Move WiredNode default methods to the impl 2024-03-10 10:00:52 +00:00
Jonathan Coates
d38b1da974 Don't propagate redstone when blink/label changes
Historically, computers tracked whether any world-visible state
(on/off/blinking, label and redstone outputs) had changed with a single
"has changed" flag. While this is simple to use, this has the curious
side effect of that term.setCursorBlink() or os.setComputerLabel() would
cause a block update!

This isn't really a problem in practice - it just means slightly more
block updates. However, the redstone propagation sometimes causes the
computer to invalidate/recheck peripherals, which masks several other
(yet unfixed) bugs.
2024-03-06 18:59:38 +00:00
Jonathan Coates
6e374579a4 Standardise on term colour parsing
- colors.toBlit now performs bounds checks on the passed value,
   preventing weird behaviour like color.toBlit(2 ^ 16) returning "10".

 - The window API now uses colors.toBlit (or rather a copy of it) for
   parsing colours, allowing doing silly things like
   term.setTextColour(colours.blue + 5).

 - Add some top-level documentation to the term API to explain some of
   the basics.

Closes #1736
2024-03-06 10:18:40 +00:00
Jonathan Coates
4daa2a2b6a Reschedule block entities when chunks are loaded
Minecraft sometimes keeps chunks in-memory, but not actively loaded. If
we schedule a block entity to be ticked and that chunk is is then
transitioned to this partially-loaded state, then the block entity is
never actually ticked.

This is most visible with monitors. When a monitor's contents changes,
if the monitor is not already marked as changed, we set it as changed
and schedule a tick (see ServerMonitor). However, if the tick is
dropped, we don't clear the changed flag, meaning subsequent changes
don't requeue the monitor to be ticked, and so the monitor is never
updated.

We fix this by maintaining a list of block entities whose tick was
dropped. If these block entities (or rather their owning chunk) is ever
re-loaded, then we reschedule them to be ticked.

An alternative approach here would be to add the scheduled tick directly
to the LevelChunk. However, getting hold of the LevelChunk for unloaded
blocks is quiet nasty, so I think best avoided.

Fixes #1146. Fixes #1560 - I believe the second one is a duplicate, and
I noticed too late :D.
2024-02-26 19:25:38 +00:00
Jonathan Coates
84b6edab82 More efficient removal of wired nodes from networks
When we remove a wired node from a network, we need to find connected
components in the rest of the graph. Typically, this requires a
traversal of the whole graph, taking O(|V| + |E|) time.

If we remove a lot of nodes at once (such as when unloading chunks),
this ends up being quadratic in the number of nodes. In some test
networks, this can take anywhere from a few seconds, to hanging the game
indefinitely.

This attempts to reduce the cases where this can happen, with a couple
of optimisations:

 - Instead of constructing a new hash set of reachable nodes (requiring
   multiple allocations and hash lookups), we store reachability as a
   temporary field on the WiredNode.

 - We abort our traversal of the graph if we can prove the graph remains
   connected after removing the node.

There's definitely future work to be done here in optimising large wired
networks, but this is a good first step.
2024-02-24 15:02:34 +00:00
Jonathan Coates
31aaf46d09 Deprecate WiredNetwork
We don't actually need this to be in the public API.
2024-02-24 14:55:22 +00:00
Jonathan Coates
2d11b51c62 Clean up the wired network tests
- Replace usages of WiredNetwork.connect/disconnect/remove with the
   WiredNode equivalents.

 - Convert "testLarge" into a proper JMH benchmark.

 - Don't put a peripheral on every node in the benchmarks. This isn't
   entirely representative, and means the peripheral juggling code ends
   up dominating the benchmark time.
2024-02-24 14:52:44 +00:00
Jonathan Coates
a0f759527d Bump CC:T to 1.109.6 2024-02-18 18:33:42 +00:00
Jonathan Coates
385e4210fa Update Cobalt to 0.9.1 for weak table fixes 2024-02-18 18:23:43 +00:00
Jonathan Coates
d2896473f2 Update our parse errors to match latest illuaminate
We've been out-of-date for a while now, as we needed to update
lua_menhir to work with lrgrep 3.

 - Better handling of standalone names/expressions - we now correctly
   handle lists of names.

 - Handle missing commas in tables in a few more places.
2024-02-08 19:22:14 +00:00
Jonathan Coates
f14cb2a3d1 Fix MemoryMount using incorrect file lengths
- We checked the backing array when reading rather than the file's
   length, so could read beyond the end of the file.
 - We used the entry length when resizing, which effectively meant we
   doubled the size of the backing array on each write.
2024-02-07 21:36:17 +00:00
Jonathan Coates
8db5c6bc3a Allow ILuaAPIs to be exposed as a module
- cc.require now uses the internal _LOADED table to get the list of
   built-in globals. This fixes several globals not showing up on the
   list (e.g. utf8), and allows us to inject more modules from the Java
   side.

 - ILuaAPI now has a getModuleName() function. This is used to inject
   the API into the aforementioned _LOADED table, allowing it to be
   "require"d.
2024-02-05 18:53:24 +00:00
Jonathan Coates
f26e443e81 Bump CC:T to 1.109.5 2024-01-31 20:53:28 +00:00
Jonathan Coates
033378333f Standardise how we discard "char" events
One common issue we get when a program exits after handling a "key"
event is that it leaves the "char" event on the queue. This means that
the shell (or whatever program we switch in to) then receives the "char"
event, often displaying it to the screen.

Previously we've got around this by doing sleep(0) before exiting the
program. However, we also see this problem in edit's run handler script,
and I'm less comfortable doing the same hack there.

This adds a new internal discard_char function, which will either
wait one tick or return when seeing a char/key_up event.

Fixes #1705
2024-01-31 20:49:43 +00:00
Jonathan Coates
ebeaa757a9 Change how we put test libraries on the class path
- Mark our core test-fixtures jar as part of the "cctest", rather than
   a separate library. I'm fairly sure this was actually using the
   classpath version of CC rather than the legacyClasspath version!

 - Add a new "testMinecraftLibrary" configuration, instead of trying to
   infer it from the classpath. We have to jump through some hoops to
   avoid having multiple versions of a library on the classpath at once,
   but it's not too bad.

I'm working on a patch to bsl which might allow us to kill of
legacyClasspath instead. Please, anything is better than this.
2024-01-31 19:49:36 +00:00
Jonathan Coates
57b1a65db3 Fix using a tab instead of space
Annoying that pre-commit didn't catch this!
2024-01-30 22:01:57 +00:00
Jonathan Coates
27c72a4571 Use client-side commands for opening computer folders
Forge doesn't run client-side commands from sendUnsignedCommand, so we
still require a mixin there.

We do need to change the command name, as Fabric doesn't properly merge
the two command trees.
2024-01-30 22:00:36 +00:00
Jonathan Coates
f284328656 Regenerate Gradle wrapper 2024-01-29 22:14:48 +00:00
Jonathan Coates
6b83c63991 Switch to our own Gradle plugin for vanilla Minecraft
I didn't make a new years resolution to stop writing build tooling, but
maybe I should have.

This replaces our use of VanillaGradle with a new project,
VanillaExtract. This offers a couple of useful features for multi-loader
dev, including Parchment and Unpick support, both of which we now use in
CC:T.
2024-01-29 20:59:16 +00:00
Jonathan Coates
b27526bd21 Bump CC:T to 1.109.4 2024-01-27 10:26:56 +00:00
Jonathan Coates
cb25f6c08a Update Cobalt to 0.9.0
- Debug hooks are now correctly called for every function.
 - Fix several minor inconsistencies with debug.getinfo.
 - Fix Lua tables being sized incorrectly when created from varargs.
2024-01-27 10:04:34 +00:00
Jonathan Coates
d38b1d04e7 Update Gradle to 8.5
- Update FG to 6.0.20 - no major changes, but required for the Gradle
   update.
 - Update Loom to 1.5.x - this adds Vineflower support by default, so we
   can remove loom-vineflower.
2024-01-27 09:28:13 +00:00
Jonathan Coates
9ccee75a99 Fix the docs for ReadHandle.read's "count"
This was copied over from the old binary handle, and so states we
always return a single number if no count is given. This is only the
case when the file is opened in binary mode.
2024-01-23 22:39:49 +00:00
Jonathan Coates
359c8d6652 Reformat JSON by wrapping CachedOutput
Rather than mixing-in to CachedOutput, we just wrap our DataProviders to
use a custom CachedOutput which reformats the JSON before writing. This
allows us to drop mixins for common+non-client code.
2024-01-21 17:50:59 +00:00
Jonathan Coates
1788afacfc Remove note about disabling websocket limits
I suspect this was copied from the file limit, which can be turned off
by setting to 0.

Fixes #1691
2024-01-21 16:32:07 +00:00
Jonathan Coates
f695f22d8a Atomic update of disk drive item stacks
Disk drives have had a long-standing issue with mutating their contents
on the computer thread, potentially leading to all sorts of odd bugs.

We tried to fix this by moving setDiskLabel and the mounting code to run
on the main thread. Unfortunately, this means there is a slight delay to
mounts being attached, breaking disk startup.

This commit implements an alternative solution - we now do mounting on
the computer thread again. If the disk's stack is modified, we update it
in the peripheral-facing item, but not the actual inventory. The next
time the disk drive is ticked, we then sync the two items.

This does mean that there is a fraction of a tick where the two will be
out-of-sync. This isn't ideal - it would potentially be possible to
cycle through disk ids - but I don't really think that's avoidable
without significantly complicating the IMedia API.

Fixes #1649, fixes #1686.
2024-01-20 18:46:43 +00:00
Jonathan Coates
bc03090ca4 Merge pull request #1684 from cc-tweaked/hotfix/turtle-modellers-redo
Rewrite turtle upgrade modeller registration API
2024-01-17 19:45:48 +00:00
Jonathan Coates
a617d0d566 Rewrite turtle upgrade modeller registration API
Originally we exposed a single registerTurtleUpgradeModellermethod which
could be called from both Fabric (during a mod's client init) and Forge
(during FMLClientSetupEvent).

This was fine until we allowed upgrades to specify model dependencies,
which would then automatically loaded, as this means model loading now
depends on upgrade modellers being loaded. Unknown to me, this is not
guaranteed to be the case on Forge - mod setup happens at the same time
as resource reloading!

Unfortunately there's not really a salvageable way of fixing this with
the current API. Forge now uses a registration event-based system,
meaning we can guarantee all modellers are loaded before models are
baked.
2024-01-16 23:00:49 +00:00
Jonathan Coates
36599b321e Backport small changes from the 1.20.4 branch
- Add support for version overrides/exclusions in our dependency check.
   Sometimes mod loaders use different versions to vanilla, and we need
   some way to handle that.

 - Rescan wired network connections on the tick after invalidation,
   rather than when invalidated.

 - Convert some constant lambdas to static method references. Lambdas
   don't allocate if they don't capture variables, so this has the same
   performance and is a little less ugly.

 - Small code-style/formatting changes.
2024-01-16 21:42:25 +00:00
Jonathan Coates
1d6e3f4fc0 Change ComponentLookup to use ServerLevel
Makes this more consistent with the rest of the peripheral code, and our
changes in 1.20.4.
2024-01-15 08:28:59 +00:00
Jonathan Coates
30dc4cb38c Simplify our networking multi-platform code
Historically we used Forge's SimpleChannel methods (and
PacketDistributor) to send the packets to the client. However, we don't
need to do that - it is sufficient to convert it to a vanilla packet,
and send the packet ourselves.

Given we need to do this on Fabric, it makes sense to do this on Forge
as well. This allows us to unify (and thus simplify) a lot of how packet
sending works.

At the same time, we also remove the handling of speaker audio during
decoding. We originally did this to avoid the additional copy of audio
data. However, this doesn't work on 1.20.4 (as packets aren't
encoded/decoded on singleplayer), so it makes sense to do this
Correctly(TM).

This also allows us to get rid of ClientNetworkContext.get(). We do
still need to service load this class (as Forge's networking isn't split
up in the same way Fabric's is), but we'll be able to drop that in
1.20.4.

Finally, we move the record playing code from ClientNetworkContext to
ClientPlatformHelper. This means the network context no longer needs to
be platform-specific!
2024-01-14 22:53:36 +00:00
Jonathan Coates
be4512d1c3 Construct ComponentAccesses with the BE
After embarrassing, let's do some proper work.

Rather than passing the level and position each time we call
ComponentAccess.get(), we now pass them at construction time (in the
form of the BE). This makes the consuming code a little cleaner, and is
required for the NeoForge changes in 1.20.4.
2024-01-14 17:46:37 +00:00
Jonathan Coates
e6ee292850 Fix incorrect "Fix incorrect "incorrect incorrect"" 2024-01-14 16:27:55 +00:00
Jonathan Coates
9d36f72bad Fix incorrect "incorrect incorrect" 2024-01-14 16:12:52 +00:00
Jonathan Coates
b5923c4462 Flesh out the printer documentation slightly 2024-01-14 12:25:04 +00:00
Jonathan Coates
4d1e689719 Fix endPage() not updating the printer block state
This meant that we didn't show the bottom slot was full until other
items were moved in the inventory.
2024-01-14 12:23:55 +00:00
Marcus
9d4af07568 fix: breaking_changes flattening link incorrect (#1679) 2024-01-10 22:18:22 +00:00
lonevox
89294f4a22 Fix incorrect Lua list indexes in NBT tags (#1678) 2024-01-10 19:16:15 +00:00
Jonathan Coates
133b51b092 Don't warn when allocating 0 bytes
I was able to reproduce this by starting two computers, and then warming
up the JIT by running:

  while true do os.queueEvent("x") os.pullEvent("x") end

and then running the following on one computer, while typing on the
other:

  while true do end

I'm not quite sure why this happens. It's possible that once the JIT is
warm, we can resume computers without actually allocating anything,
though I'm a little unconvinced.

Fixes #1672
2024-01-08 21:52:30 +00:00
Jonathan Coates
272010e945 Require Minecraft 1.20.1
Closes #1671
2024-01-08 21:33:55 +00:00
Jonathan Coates
e0889c613a Mark "check valid item" test as required
This has passed for years now, no reason for it to be optional.
2024-01-07 13:35:38 +00:00
Jonathan Coates
f115d43d07 Fix some dependencies not appearing in the POM
Again! This time it was just the night-config ones.
2024-01-03 21:05:03 +00:00
Jonathan Coates
8be6b1b772 Bump CC:T to 1.109.3 2024-01-03 20:00:52 +00:00
Jonathan Coates
104d5e70de Update to Cobalt 0.8.2
- Fix error when using "goto" as the first statement in an if block.
 - Fix incorrect resizing of table's hash part when adding and removing
   keys.
2024-01-03 18:44:37 +00:00
Jonathan Coates
e3bda2f763 Add command computers to the operator blocks tab
Fixes #1666
2024-01-03 18:42:31 +00:00
Jonathan Coates
234f69e8e5 Add a MessageType for network messages
Everything old is new again!

CC's network message implementation has gone through several iterations:

 - Originally network messages were implemented with a single class,
   which held an packet id/type and and opaque blobs of data (as
   string/int/byte/NBT arrays), and a big switch statement to decode and
   process this data.

 - In 42d3901ee3, we split the messages
   into different classes all inheriting from NetworkMessage - this bit
   we've stuck with ever since.

   Each packet had a `getId(): int` method, which returned the
   discriminator for this packet.

 - However, getId() was only used when registering the packet, not when
   sending, and so in ce0685c31f we
   removed it, just passing in a constant integer at registration
   instead.

 - In 53abe5e56e, we made some relatively
   minor changes to make the code more multi-loader/split-source
   friendly. However, this meant when we finally came to add Fabric
   support (8152f19b6e), we had to
   re-implement a lot of Forge's network code.

In 1.20.4, Forge moves to a system much closer to Fabric's (and indeed,
Minecraft's own CustomPacketPayload), and so it makes sense to adapt to
that now. As such, we:

 - Add a new MessageType interface. This is implemented by the
   loader-specific modules, and holds whatever information is needed to
   register the packet (e.g. discriminator, reader function).

 - Each NetworkMessage now has a type(): MessageType<?> function. This
   is used by the Fabric networking code (and for NeoForge's on 1.20.4)
   instead of a class lookup.

 - NetworkMessages now creates/stores these MessageType<T>s (much like
   we'd do for registries), and provides getters for the
   clientbound/serverbound messages. Mod initialisers then call these
   getters to register packets.

 - For Forge, this is relatively unchanged. For Fabric, we now
   `FabricPacket`s.
2024-01-03 10:23:41 +00:00
Jonathan Coates
ed3a17f9b9 Fix trailing-comma errors on method calls
We were only matching `f(a, ` patterns, and not `x:f(a, `. We now just
match against any usages of call_args - hadn't quite realised we could
do that!
2023-12-28 17:07:39 +00:00
Jonathan Coates
0349c2b1f9 Allow local domains in the standalone emulator
- Clean up option parsing a bit, so it uses the Option, rather than its
   corresponding character code.
 - Add a new -L/--allow-local-domains flag to remove the $private rule
   from the HTTP rules.
2023-12-20 17:43:08 +00:00
Jonathan Coates
03f9e6bd6d Prevent sending too many websocket messages at once 2023-12-20 17:30:57 +00:00
Jonathan Coates
9d8c933a14 Remove several usages of ComputerFamily
While ComputerFamily is still useful, there's definitely some places
where it adds an extra layer of indirection. This commit attempts to
clean up some places where we no longer need it.

 - Remove ComputerFamily from AbstractComputerBlock. The only place this
   was needed is in TurtleBlock, and that can be replaced with normal
   Minecraft explosion resistence!

 - Pass in the fuel limit to the turtle block entity, rather than
   deriving it from current family.

 - The turtle BERs now derive their model from the turtle's item, rather
   than the turtle's family.

 - When creating upgrade/overlay recipes, use the item's name, rather
   than {pocket,turtle}_family. This means we can drop getFamily() from
   IComputerItem (it is still needed on to handle the UI).

 - We replace IComputerItem.withFamily with a method to change to a
   different item of the same type. ComputerUpgradeRecipe no longer
   takes a family, and instead just uses the result's item.

 - Computer blocks now use the normal Block.asItem() to find their
   corresponding item, rather than looking it up via family.

The above means we can remove all the family-based XyzItem.create(...)
methods, which have always felt a little ugly.

We still need ComputerFamily for a couple of things:
 - Permission checks for command computers.
 - Checks for mouse/colour support in ServerComputer.
 - UI textures.
2023-12-20 14:17:38 +00:00
Jonathan Coates
78bb3da58c Improve our version tooling
- Add a check to ensure declared dependencies in the :core project, and
   those inherited from Minecraft are the same.
 - Compute the next Cobalt version, rather than specifying it manually.
 - Add the gradle versions plugin (and version catalog update), and
   update some versions.
2023-12-19 18:12:21 +00:00
Jonathan Coates
39a5e40c92 Bump CC:T to 1.109.2
Take two!
2023-12-16 22:46:30 +00:00
Jonathan Coates
763ba51919 Update Cobalt to 0.8.1 2023-12-16 22:39:48 +00:00
Jonathan Coates
cf6ec8c28f Add a slightly cleaner system for excluding deps
Previously we prevented our published full jar depending on any of the
other projects by excluding the whole cc.tweaked jar. However, as Cobalt
also now lives in that group, this meant we were missing the Cobalt
dependency.

Rather than specifying a wildcard, we now exclude the dependencies when
adding them to the project.
2023-12-16 22:35:15 +00:00
Jonathan Coates
95d3b646b2 Bump CC:T to 1.109.1 2023-12-16 19:09:39 +00:00
Jonathan Coates
488f66eead Fix mouse_drag not firing for right/middle buttons
This is a bit of an odd combination of a few bugs:
 - When the terminal component is blurred, we fire a mouse_up event for
   the last-held button. However, we had an off-by-1 error here, so this
   only triggered for the right/middle buttons.

 - This was obsucuring the second bug, which is when we clicked within
   the terminal, this caused the terminal to be blurred (thus releasing
   the mouse) and then focused again.

   We fix this by only setting the focus if there's actually a change.

Fixes #1655
2023-12-10 12:01:34 +00:00
Jonathan Coates
1f7d245876 Specify charset when printing error messages 2023-12-08 09:52:17 +00:00
Jonathan Coates
af12b3a0ea Fix goto/:: tokens erroring in error reporting 2023-12-07 19:47:39 +00:00
Jonathan Coates
eb3e8ba677 Fix deadlock when adding/removing observers
When adding/removing observers, we locked on the observer, then
acquired the global lock. When a metric is observed, then we acquire the
global lock and then the observer lock.

If these happen at the same time, we can easily end up with a deadlock.
We simply avoid holding the observer lock for the entire add/remove
process (instead only locking when actually needed).

Closes #1639
2023-12-01 12:33:03 +00:00
Jonathan Coates
2043939531 Add compostors to the list of usable blocks
Fixes #1638
2023-11-22 18:24:59 +00:00
Jonathan Coates
84a799d27a Add abstract classes for our generic peripherals
This commit adds abstract classes to describe the interface for our
mod-loader-specific generic peripherals (inventories, fluid storage,
item storage).

This offers several advantages:
 - Javadoc to illuaminate conversion no longer needs the Forge project
   (just core and common).

 - Ensures we have a consistent interface between Forge and Fabric.

Note, this does /not/ implement fluid or energy storage for Fabric. We
probably could do fluid without issue, but not something worth doing
right now.
2023-11-22 18:20:15 +00:00
Jonathan Coates
fe826f5c9c Allow generic sources to have instance methods
Rather than assuming static methods are generic, and instance methods
are direct, the Generator now has separate entrypoints for handling
instance and generic methods.

As a result of this change, we've also relaxed some of the validation
code. As a result, we now allow calling private/protected methods
which are annotated with @LuaFunction.
2023-11-22 10:06:11 +00:00
Jonathan Coates
f8b7422294 Fix several issues with the web emulator
- Bump TeaVM version to fix issues with locales and our "export"
   generation.
 - Fix TComputerThread not requeuing correctly.
2023-11-15 13:12:31 +00:00
Jonathan Coates
b343c01216 Bump CC:T to 1.109.0 2023-11-15 09:39:52 +00:00
Jonathan Coates
76968f2f28 Track allocations while executing computers
This adds a new "java_allocation" metric, which tracks the number of
bytes allocated while executing the computer (as measured by Java). This
is not an 100% reliable number, but hopefully gives some insight into
what computers are doing.
2023-11-09 18:36:35 +00:00
Jonathan Coates
1d365f5a0b Add option to allow repetition in JSON serialiser
Closes #1588
2023-11-08 21:29:43 +00:00
Jonathan Coates
7b240cbf7e Merge pull request #1615 from cc-tweaked/feature/much-breakage-very-wow
Remove text mode, update Cobalt
2023-11-08 20:05:49 +00:00
Jonathan Coates
d272a327c7 Update CraftOS version to 1.9 2023-11-08 19:40:14 +00:00
Jonathan Coates
0c0556a5bc Always use raw bytes in file handles
Historically CC has supported two modes when working with file handles
(and HTTP requests):

 - Text mode, which reads/write using UTF-8.
 - Binary mode, which reads/writes the raw bytes.

However, this can be confusing at times. CC/Lua doesn't actually support
unicode, so any characters beyond the 0.255 range were replaced with
'?'. This meant that most of the time you were better off just using
binary mode.

This commit unifies text and binary mode - we now /always/ read the raw
bytes of the file, rather than converting to/from UTF-8. Binary mode now
only specifies whether handle.read() returns a number (and .write(123)
writes a byte rather than coercing to a string).

 - Refactor the entire handle hierarchy. We now have an AbstractMount
   base class, which has the concrete implementation of all methods. The
   public-facing classes then re-export these methods by annotating
   them with @LuaFunction.

   These implementations are based on the
   Binary{Readable,Writable}Handle classes. The Encoded{..}Handle
   versions are now entirely removed.

 - As we no longer need to use BufferedReader/BufferedWriter, we can
   remove quite a lot of logic in Filesystem to handle wrapping
   closeable objects.

 - Add a new WritableMount.openFile method, which generalises
   openForWrite/openForAppend to accept OpenOptions. This allows us to
   support update mode (r+, w+) in fs.open.

 - fs.open now uses the new handle types, and supports update (r+, w+)
   mode.

 - http.request now uses the new readable handle type. We no longer
   encode the request body to UTF-8, nor decode the response from UTF-8.

 - Websockets now return text frame's contents directly, rather than
   converting it from UTF-8. Sending text frames now attempts to treat
   the passed string as UTF-8, rather than treating it as latin1.
2023-11-08 19:40:14 +00:00
Jonathan Coates
87345c6b2e Add pasting support to the standalone emulator
- Move paste normalisation code to StringUtil, so it can be shared by
   emulators.
 - Add paste support to the emulator.
2023-11-08 19:40:14 +00:00
Jonathan Coates
784e623776 Update Cobalt to 0.8.0
- Update Cobalt to 0.8.0, switching our Lua version to 5.2(ish).

 - Remove our `load` wrapper, as we no longer need to inject _ENV into
   the enviroment table.

 - Update the parser to handle labels and goto. This doesn't check that
   gotos are well formed, but at least means the parser doesn't fall
   over on them.

 - Update our docs to reflect the changes to Cobalt.
2023-11-08 18:42:17 +00:00
316 changed files with 7074 additions and 4210 deletions

2
.gitignore vendored
View File

@@ -7,7 +7,9 @@
/logs
/build
/projects/*/logs
/projects/fabric/fabricloader.log
/projects/*/build
/projects/*/src/test/generated_tests/
/buildSrc/build
/out
/buildSrc/out

View File

@@ -42,7 +42,6 @@ repositories {
url "https://squiddev.cc/maven/"
content {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
}
}
}
@@ -76,8 +75,8 @@ minecraft {
```
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into
exposing more features.
subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file
an issue to let me know!
We bundle the API sources with the jar, so documentation should be easily viewable within your editor. Alternatively,
the generated documentation [can be browsed online](https://tweaked.cc/javadoc/).

View File

@@ -13,6 +13,8 @@ plugins {
publishing
alias(libs.plugins.taskTree)
alias(libs.plugins.githubRelease)
alias(libs.plugins.gradleVersions)
alias(libs.plugins.versionCatalogUpdate)
id("org.jetbrains.gradle.plugin.idea-ext")
id("cc-tweaked")
}
@@ -102,3 +104,9 @@ idea.project.settings.compiler.javac {
}
.toMap()
}
versionCatalogUpdate {
sortByKey.set(false)
pin { versions.addAll("fastutil", "guava", "netty", "slf4j") }
keep { keepUnusedLibraries.set(true) }
}

View File

@@ -5,6 +5,8 @@
plugins {
`java-gradle-plugin`
`kotlin-dsl`
alias(libs.plugins.gradleVersions)
alias(libs.plugins.versionCatalogUpdate)
}
// Duplicated in settings.gradle.kts
@@ -27,19 +29,19 @@ repositories {
}
}
maven("https://repo.spongepowered.org/repository/maven-public/") {
name = "Sponge"
content {
includeGroup("org.spongepowered")
}
}
maven("https://maven.fabricmc.net/") {
name = "Fabric"
content {
includeGroup("net.fabricmc")
}
}
maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
includeGroup("cc.tweaked.vanilla-extract")
}
}
}
dependencies {
@@ -53,8 +55,7 @@ dependencies {
implementation(libs.ideaExt)
implementation(libs.librarian)
implementation(libs.minotaur)
implementation(libs.vanillaGradle)
implementation(libs.vineflower)
implementation(libs.vanillaExtract)
}
gradlePlugin {
@@ -75,3 +76,9 @@ gradlePlugin {
}
}
}
versionCatalogUpdate {
sortByKey.set(false)
keep { keepUnusedLibraries.set(true) }
catalogFile.set(file("../gradle/libs.versions.toml"))
}

View File

@@ -12,7 +12,6 @@ import cc.tweaked.gradle.MinecraftConfigurations
plugins {
`java-library`
id("fabric-loom")
id("io.github.juuxel.loom-vineflower")
id("cc-tweaked.java-convention")
}

View File

@@ -57,7 +57,6 @@ repositories {
filter {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
// Things we mirror
includeGroup("commoble.morered")
includeGroup("dev.architectury")
@@ -77,6 +76,12 @@ dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
checkstyle(libs.findLibrary("checkstyle").get())
constraints {
checkstyle("org.codehaus.plexus:plexus-container-default:2.1.1") {
because("2.1.0 depends on deprecated Google collections module")
}
}
errorprone(libs.findLibrary("errorProne-core").get())
errorprone(libs.findLibrary("nullAway").get())
}
@@ -202,6 +207,7 @@ spotless {
val ktlintConfig = mapOf(
"ktlint_standard_no-wildcard-imports" to "disabled",
"ktlint_standard_class-naming" to "disabled",
"ktlint_standard_function-naming" to "disabled",
"ij_kotlin_allow_trailing_comma" to "true",
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
)

View File

@@ -10,25 +10,31 @@ import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("cc-tweaked.java-convention")
id("org.spongepowered.gradle.vanilla")
id("cc.tweaked.vanilla-extract")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
minecraft {
version(mcVersion)
mappings {
parchment(libs.findVersion("parchmentMc").get().toString(), libs.findVersion("parchment").get().toString())
}
unpick(libs.findLibrary("yarn").get())
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
// Depend on error prone annotations to silence a lot of compile warnings.
compileOnlyApi(libs.findLibrary("errorProne.annotations").get())
compileOnly(libs.findLibrary("errorProne.annotations").get())
}
MinecraftConfigurations.setup(project)
MinecraftConfigurations.setupBasic(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = null)

View File

@@ -10,9 +10,11 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.attributes.TestSuiteType
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.reporting.ReportingExtension
@@ -73,11 +75,17 @@ abstract class CCTweakedExtension(
*/
val sourceDirectories: SetProperty<SourceSetReference> = project.objects.setProperty(SourceSetReference::class.java)
/**
* Dependencies excluded from published artifacts.
*/
private val excludedDeps: ListProperty<Dependency> = project.objects.listProperty(Dependency::class.java)
/** All source sets referenced by this project. */
val sourceSets = sourceDirectories.map { x -> x.map { it.sourceSet } }
init {
sourceDirectories.finalizeValueOnRead()
excludedDeps.finalizeValueOnRead()
project.afterEvaluate { sourceDirectories.disallowChanges() }
}
@@ -246,6 +254,20 @@ abstract class CCTweakedExtension(
).resolve().single()
}
/**
* Exclude a dependency from being published in Maven.
*/
fun exclude(dep: Dependency) {
excludedDeps.add(dep)
}
/**
* Configure a [MavenDependencySpec].
*/
fun configureExcludes(spec: MavenDependencySpec) {
for (dep in excludedDeps.get()) spec.exclude(dep)
}
companion object {
private val COMMIT_COUNTS = Pattern.compile("""^\s*[0-9]+\s+(.*)$""")
private val IGNORED_USERS = setOf(

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.DependencyResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin
abstract class DependencyCheck : DefaultTask() {
@get:Input
abstract val configuration: ListProperty<Configuration>
/**
* A mapping of module coordinates (`group:module`) to versions, overriding the requested version.
*/
@get:Input
abstract val overrides: MapProperty<String, String>
init {
description = "Check :core's dependencies are consistent with Minecraft's."
group = LifecycleBasePlugin.VERIFICATION_GROUP
configuration.finalizeValueOnRead()
overrides.finalizeValueOnRead()
}
/**
* Override a module with a different version.
*/
fun override(module: Provider<MinimalExternalModuleDependency>, version: String) {
overrides.putAll(project.provider { mutableMapOf(module.get().module.toString() to version) })
}
@TaskAction
fun run() {
var ok = true
for (configuration in configuration.get()) {
configuration.incoming.resolutionResult.allDependencies {
if (!check(this@allDependencies)) ok = false
}
}
if (!ok) {
throw GradleException("Mismatched versions in Minecraft dependencies. gradle/libs.versions.toml may need updating.")
}
}
private fun check(dependency: DependencyResult): Boolean {
if (dependency !is ResolvedDependencyResult) {
logger.warn("Found unexpected dependency result {}", dependency)
return false
}
// Skip dependencies on non-modules.
val requested = dependency.requested
if (requested !is ModuleComponentSelector) return true
// If this dependency is specified within some project (so is non-transitive), or is pulled in via Minecraft,
// then check for consistency.
// It would be nice to be smarter about transitive dependencies, but avoiding false positives is hard.
val from = dependency.from.id
if (
from is ProjectComponentIdentifier ||
from is ModuleComponentIdentifier && (from.group == "net.minecraft" || from.group == "io.netty")
) {
// If the version is different between the requested and selected version, report an error.
val selected = dependency.selected.moduleVersion!!.version
val requestedVersion = overrides.get()["${requested.group}:${requested.module}"] ?: requested.version
if (requestedVersion != selected) {
logger.error("Requested dependency {} (via {}) but got version {}", requested, from, selected)
return false
}
return true
}
return true
}
}

View File

@@ -6,7 +6,6 @@ package cc.tweaked.gradle
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.file.FileSystemLocationProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec
@@ -129,3 +128,30 @@ fun <T> Property<T>.setProvider(provider: Provider<out T>) = set(provider)
/** Short-cut method to get the absolute path of a [FileSystemLocation] provider. */
fun Provider<out FileSystemLocation>.getAbsolutePath(): String = get().asFile.absolutePath
/**
* Get the version immediately after the provided version.
*
* For example, given "1.2.3", this will return "1.2.4".
*/
fun getNextVersion(version: String): String {
// Split a version like x.y.z-SNAPSHOT into x.y.z and -SNAPSHOT
val dashIndex = version.indexOf('-')
val mainVersion = if (dashIndex < 0) version else version.substring(0, dashIndex)
// Find the last component in x.y.z and increment it.
val lastIndex = mainVersion.lastIndexOf('.')
if (lastIndex < 0) throw IllegalArgumentException("Cannot parse version format \"$version\"")
val lastVersion = try {
version.substring(lastIndex + 1).toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("Cannot parse version format \"$version\"", e)
}
// Then append all components together.
val out = StringBuilder()
out.append(version, 0, lastIndex + 1)
out.append(lastVersion + 1)
if (dashIndex >= 0) out.append(version, dashIndex, version.length)
return out.toString()
}

View File

@@ -6,6 +6,8 @@ package cc.tweaked.gradle
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.plugins.BasePluginExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.specs.Spec
@@ -26,8 +28,13 @@ class MavenDependencySpec {
fun exclude(dep: Dependency) {
exclude {
// We have to cheat a little for project dependencies, as the project name doesn't match the artifact group.
val name = when (dep) {
is ProjectDependency -> dep.dependencyProject.extensions.getByType(BasePluginExtension::class.java).archivesName.get()
else -> dep.name
}
(dep.group.isNullOrEmpty() || dep.group == it.groupId) &&
(dep.name.isNullOrEmpty() || dep.name == it.artifactId) &&
(name.isNullOrEmpty() || name == it.artifactId) &&
(dep.version.isNullOrEmpty() || dep.version == it.version)
}
}

View File

@@ -4,23 +4,17 @@
package cc.tweaked.gradle
import cc.tweaked.vanillaextract.configurations.Capabilities
import cc.tweaked.vanillaextract.configurations.MinecraftSetup
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.java.TargetJvmVersion
import org.gradle.api.capabilities.Capability
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.named
/**
* This sets up a separate client-only source set, and extends that and the main/common source set with additional
@@ -59,31 +53,13 @@ class MinecraftConfigurations private constructor(private val project: Project)
}
configurations.named(client.implementationConfigurationName) { extendsFrom(clientApi) }
/*
Now add outgoing variants for the main and common source sets that we can consume downstream. This is possibly
the worst way to do things, but unfortunately the alternatives don't actually work very well:
- Just using source set outputs: This means dependencies don't propagate, which means when :fabric depends
on :fabric-api, we don't inherit the fake :common-api in IDEA.
- Having separate common/main jars: Nice in principle, but unfortunately Forge needs a separate deobf jar
task (as the original jar is obfuscated), and IDEA is not able to map its output back to a source set.
This works for now, but is incredibly brittle. It's part of the reason we can't use testFixtures inside our
MC projects, as that adds a project(self) -> test dependency, which would pull in the jar instead.
Note we register a fake client jar here. It's not actually needed, but is there to make sure IDEA has
a way to tell that client classes are needed at runtime.
I'm so sorry, deeply aware how cursed this is.
*/
setupOutgoing(main, "CommonOnly")
project.tasks.register(client.jarTaskName, Jar::class.java) {
description = "An empty jar standing in for the client classes."
group = BasePlugin.BUILD_GROUP
archiveClassifier.set("client")
}
setupOutgoing(client)
MinecraftSetup(project).setupOutgoingConfigurations()
// Reset the client classpath (Loom configures it slightly differently to this) and add a main -> client
// dependency. Here we /can/ use source set outputs as we add transitive deps by patching the classpath. Nasty,
@@ -106,88 +82,39 @@ class MinecraftConfigurations private constructor(private val project: Project)
project.tasks.named("jar", Jar::class.java) { from(client.output) }
project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) }
setupBasic()
}
private fun setupBasic() {
val client = sourceSets["client"]
project.extensions.configure(CCTweakedExtension::class.java) {
sourceDirectories.add(SourceSetReference.internal(client))
}
}
private fun setupOutgoing(sourceSet: SourceSet, suffix: String = "") {
setupOutgoing("${sourceSet.apiElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_API)) {
description = "API elements for ${sourceSet.name}"
extendsFrom(configurations[sourceSet.apiConfigurationName])
}
setupOutgoing("${sourceSet.runtimeElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_RUNTIME)) {
description = "Runtime elements for ${sourceSet.name}"
extendsFrom(configurations[sourceSet.implementationConfigurationName], configurations[sourceSet.runtimeOnlyConfigurationName])
}
}
/**
* Set up an outgoing configuration for a specific source set. We set an additional "main" or "client" capability
* (depending on the source set name) which allows downstream projects to consume them separately (see
* [DependencyHandler.commonClasses] and [DependencyHandler.clientClasses]).
*/
private fun setupOutgoing(name: String, sourceSet: SourceSet, usage: Usage, configure: Configuration.() -> Unit) {
configurations.register(name) {
isVisible = false
isCanBeConsumed = true
isCanBeResolved = false
configure(this)
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, usage)
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attributeProvider(
TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE,
java.toolchain.languageVersion.map { it.asInt() },
)
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
// Register a task to check there are no conflicts with the core project.
val checkDependencyConsistency =
project.tasks.register("checkDependencyConsistency", DependencyCheck::class.java) {
// We need to check both the main and client classpath *configurations*, as the actual configuration
configuration.add(configurations.named(main.runtimeClasspathConfigurationName))
configuration.add(configurations.named(client.runtimeClasspathConfigurationName))
}
outgoing {
capability(BasicOutgoingCapability(project, sourceSet.name))
// We have two outgoing variants here: the original jar and the classes.
artifact(project.tasks.named(sourceSet.jarTaskName))
variants.create("classes") {
attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
sourceSet.output.classesDirs.forEach { artifact(it) { builtBy(sourceSet.output) } }
}
}
}
project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
}
companion object {
fun setupBasic(project: Project) {
MinecraftConfigurations(project).setupBasic()
}
fun setup(project: Project) {
MinecraftConfigurations(project).setup()
}
}
}
private class BasicIncomingCapability(private val module: ModuleDependency, private val name: String) : Capability {
override fun getGroup(): String = module.group!!
override fun getName(): String = "${module.name}-$name"
override fun getVersion(): String? = null
}
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency =
Capabilities.clientClasses(create(notation) as ModuleDependency)
private class BasicOutgoingCapability(private val project: Project, private val name: String) : Capability {
override fun getGroup(): String = project.group.toString()
override fun getName(): String = "${project.name}-$name"
override fun getVersion(): String = project.version.toString()
}
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency {
val dep = create(notation) as ModuleDependency
dep.capabilities { requireCapability(BasicIncomingCapability(dep, "client")) }
return dep
}
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency {
val dep = create(notation) as ModuleDependency
dep.capabilities { requireCapability(BasicIncomingCapability(dep, "main")) }
return dep
}
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency =
Capabilities.commonClasses(create(notation) as ModuleDependency)

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: MPL-2.0
The [`file_transfer`] event is queued when a user drags-and-drops a file on an open computer.
This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.ReadHandle`] with an
additional [getName][`TransferredFile.getName`] method.
## Return values

View File

@@ -134,7 +134,7 @@ accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, whic
As mentioned above, [`speaker.playAudio`] accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use
[`io.lines`], which provides a nice way to loop over chunks of a file. You can of course just use [`fs.open`] and
[`fs.BinaryReadHandle.read`] if you prefer.
[`fs.ReadHandle.read`] if you prefer.
## Processing audio
As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes.

View File

@@ -21,6 +21,20 @@ of the mod should run fine on later versions.
However, some changes to the underlying game, or CC: Tweaked's own internals may break some programs. This page serves
as documentation for breaking changes and "gotchas" one should look out for between versions.
## CC: Tweaked 1.109.0 to 1.109.3 {#cct-1.109}
- Update to Lua 5.2:
- Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs.
- Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. `getfenv`/`setfenv`
now only work on Lua functions with an `_ENV` upvalue. `getfenv` will return the global environment when called
with other functions, and `setfenv` will have no effect.
- `load`/`loadstring` defaults to using the global environment (`_G`) rather than the current coroutine's
environment.
- Support for dumping functions (`string.dump`) and loading binary chunks has been removed.
- `math.random` now uses Lua 5.4's random number generator.
- File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
## Minecraft 1.13 {#mc-1.13}
- The "key code" for [`key`] and [`key_up`] events has changed, due to Minecraft updating to LWJGL 3. Make sure you're
using the constants provided by the [`keys`] API, rather than hard-coding numerical values.
@@ -62,6 +76,6 @@ as documentation for breaking changes and "gotchas" one should look out for betw
- Programs containing `/` are looked up in the current directory and are no longer looked up on the path. For instance,
you can no longer type `turtle/excavate` to run `/rom/programs/turtle/excavate.lua`.
[flattening]: https://minecraft.wiki.com/w/Java_Edition_1.13/Flattening
[flattening]: https://minecraft.wiki/w/Java_Edition_1.13/Flattening
[legal_data_pack]: https://minecraft.gamepedia.com/Tutorials/Creating_a_data_pack#Legal_characters
[datapack-example]: https://github.com/cc-tweaked/datapack-example "An example datapack for CC: Tweaked"

View File

@@ -9,17 +9,19 @@ SPDX-License-Identifier: MPL-2.0
-->
# Lua 5.2/5.3 features in CC: Tweaked
CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However, Cobalt and CC:T implement additional features from Lua 5.2 and 5.3 (as well as some deprecated 5.0 features) that are not available in base 5.1. This page lists all of the compatibility for these newer versions.
CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.2. However, Cobalt and CC:T implement additional
features from Lua 5.2 and 5.3 (as well as some deprecated 5.0 and 5.1 features). This page lists all of the
compatibility for these newer versions.
## Lua 5.2
| Feature | Supported? | Notes |
|---------------------------------------------------------------|------------|-------------------------------------------------------------------|
| `goto`/labels | | |
| `_ENV` | 🔶 | The `_ENV` global points to `getfenv()`, but it cannot be set. |
| `goto`/labels | | |
| `_ENV` | | |
| `\z` escape | ✔ | |
| `\xNN` escape | ✔ | |
| Hex literal fractional/exponent parts | ✔ | |
| Empty statements | | |
| Empty statements | | |
| `__len` metamethod | ✔ | |
| `__ipairs` metamethod | ❌ | Deprecated in Lua 5.3. `ipairs` uses `__len`/`__index` instead. |
| `__pairs` metamethod | ✔ | |
@@ -27,12 +29,12 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
| `collectgarbage` isrunning, generational, incremental options | ❌ | `collectgarbage` does not exist in CC:T. |
| New `load` syntax | ✔ | |
| `loadfile` mode parameter | ✔ | Supports both 5.1 and 5.2+ syntax. |
| Removed `loadstring` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `getfenv`, `setfenv` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `loadstring` | | |
| Removed `getfenv`, `setfenv` | 🔶 | Only supports closures with an `_ENV` upvalue. |
| `rawlen` function | ✔ | |
| Negative index to `select` | ✔ | |
| Removed `unpack` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Arguments to `xpcall` | ✔ | |
| Removed `unpack` | | |
| Arguments to `xpcall` | ✔ | |
| Second return value from `coroutine.running` | ✔ | |
| Removed `module` | ✔ | |
| `package.loaders` -> `package.searchers` | ❌ | |
@@ -40,14 +42,14 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
| `package.config` | ✔ | |
| `package.searchpath` | ✔ | |
| Removed `package.seeall` | ✔ | |
| `string.dump` on functions with upvalues (blanks them out) | | |
| `string.rep` separator | ✔ | |
| `string.dump` on functions with upvalues (blanks them out) | | `string.dump` is not supported |
| `string.rep` separator | ✔ | |
| `%g` match group | ❌ | |
| Removal of `%z` match group | ❌ | |
| Removed `table.maxn` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `table.maxn` | | |
| `table.pack`/`table.unpack` | ✔ | |
| `math.log` base argument | ✔ | |
| Removed `math.log10` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `math.log10` | | |
| `*L` mode to `file:read` | ✔ | |
| `os.execute` exit type + return value | ❌ | `os.execute` does not exist in CC:T. |
| `os.exit` close argument | ❌ | `os.exit` does not exist in CC:T. |
@@ -61,7 +63,7 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
| Tail call hooks | ❌ | |
| `=` prefix for chunks | ✔ | |
| Yield across C boundary | ✔ | |
| Removal of ambiguity error | | |
| Removal of ambiguity error | | |
| Identifiers may no longer use locale-dependent letters | ✔ | |
| Ephemeron tables | ❌ | |
| Identical functions may be reused | ❌ | Removed in Lua 5.4 |

View File

@@ -95,10 +95,10 @@ function pullEventRaw(filter) end
-- nearest multiple of 0.05.
function sleep(time) end
--- Get the current CraftOS version (for example, `CraftOS 1.8`).
--- Get the current CraftOS version (for example, `CraftOS 1.9`).
--
-- This is defined by `bios.lua`. For the current version of CC:Tweaked, this
-- should return `CraftOS 1.8`.
-- should return `CraftOS 1.9`.
--
-- @treturn string The current CraftOS version.
-- @usage os.version()

View File

@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties
isUnstable=false
modVersion=1.108.4
modVersion=1.109.7
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.20.1

View File

@@ -10,28 +10,30 @@
fabric-api = "0.86.1+1.20.1"
fabric-loader = "0.14.21"
forge = "47.1.0"
forgeSpi = "6.0.0"
forgeSpi = "7.0.1"
mixin = "0.8.5"
parchment = "2023.08.20"
parchmentMc = "1.20.1"
yarn = "1.20.1+build.10"
# Normal dependencies
asm = "9.5"
autoService = "1.1.1"
checkerFramework = "3.32.0"
cobalt = "0.7.3"
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
commonsCli = "1.3.1"
# Core dependencies (these versions are tied to the version Minecraft uses)
fastutil = "8.5.9"
guava = "31.1-jre"
jetbrainsAnnotations = "24.0.1"
netty = "4.1.82.Final"
slf4j = "2.0.1"
# Core dependencies (independent of Minecraft)
asm = "9.6"
autoService = "1.1.1"
checkerFramework = "3.42.0"
cobalt = "0.9.1"
commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2"
jzlib = "1.1.3"
kotlin = "1.8.10"
kotlin-coroutines = "1.6.4"
netty = "4.1.82.Final"
kotlin = "1.9.21"
kotlin-coroutines = "1.7.3"
nightConfig = "3.6.7"
slf4j = "2.0.1"
# Minecraft mods
emi = "1.0.8+1.20.1"
@@ -47,30 +49,32 @@ sodium = "mc1.20-0.4.10"
# Testing
hamcrest = "2.2"
jqwik = "1.7.4"
junit = "5.10.0"
jqwik = "1.8.2"
junit = "5.10.1"
jmh = "1.37"
# Build tools
cctJavadoc = "1.8.0"
checkstyle = "10.12.3"
cctJavadoc = "1.8.2"
checkstyle = "10.12.6"
curseForgeGradle = "1.0.14"
errorProne-core = "2.21.1"
errorProne-core = "2.23.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.3.7"
forgeGradle = "6.0.8"
githubRelease = "2.4.1"
fabric-loom = "1.5.7"
forgeGradle = "6.0.20"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
illuaminate = "0.1.0-44-g9ee0055"
librarian = "1.+"
lwjgl = "3.3.1"
lwjgl = "3.3.3"
minotaur = "2.+"
mixinGradle = "0.7.+"
mixinGradle = "0.7.38"
nullAway = "0.9.9"
spotless = "6.21.0"
spotless = "6.23.3"
taskTree = "2.1.1"
teavm = "0.9.0-SQUID.1"
vanillaGradle = "0.2.1-SNAPSHOT"
vineflower = "1.11.0"
teavm = "0.10.0-SQUID.2"
vanillaExtract = "0.1.1"
versionCatalogUpdate = "0.8.1"
[libraries]
# Normal dependencies
@@ -78,7 +82,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
cobalt = { module = "cc.tweaked:cobalt", version.ref = "cobalt" }
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
forgeSpi = { module = "net.minecraftforge:forgespi", version.ref = "forgeSpi" }
@@ -124,6 +128,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
# LWJGL
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
@@ -156,16 +162,18 @@ teavm-metaprogramming-api = { module = "org.teavm:teavm-metaprogramming-api", ve
teavm-metaprogramming-impl = { module = "org.teavm:teavm-metaprogramming-impl", version.ref = "teavm" }
teavm-platform = { module = "org.teavm:teavm-platform", version.ref = "teavm" }
teavm-tooling = { module = "org.teavm:teavm-tooling", version.ref = "teavm" }
vanillaGradle = { module = "org.spongepowered:vanillagradle", version.ref = "vanillaGradle" }
vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" }
vanillaExtract = { module = "cc.tweaked.vanilla-extract:plugin", version.ref = "vanillaExtract" }
yarn = { module = "net.fabricmc:yarn", version.ref = "yarn" }
[plugins]
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" }
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }
[bundles]
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
@@ -175,7 +183,6 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"]
externalMods-forge-compile = ["moreRed", "oculus", "jei-api"]
externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric = ["nightConfig-core", "nightConfig-toml"]
externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"]
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
@@ -184,5 +191,5 @@ test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
testRuntime = ["junit-jupiter-engine", "jqwik-engine"]
# Build tools
teavm-api = [ "teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api" ]
teavm-tooling = [ "teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl" ]
teavm-api = ["teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api"]
teavm-tooling = ["teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl"]

Binary file not shown.

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

22
gradlew vendored
View File

@@ -83,7 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -130,10 +131,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -198,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

View File

@@ -6,7 +6,7 @@
(sources
/doc/
/projects/forge/build/docs/luaJavadoc/
/projects/common/build/docs/luaJavadoc/
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
/projects/core/src/main/resources/data/computercraft/lua/rom/
/projects/core/src/test/resources/test-rom
@@ -36,7 +36,7 @@
(library-path
/doc/stub/
/projects/forge/build/docs/luaJavadoc/
/projects/common/build/docs/luaJavadoc/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/command/
@@ -88,7 +88,7 @@
(/doc/stub/
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
/projects/forge/build/docs/luaJavadoc/)
/projects/common/build/docs/luaJavadoc/)
(linters -var:unused-global)
(lint (allow-toplevel-global true)))
@@ -105,6 +105,10 @@
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua)
(linters -var:deprecated))
;; Suppress unused variable warnings in the parser.
(at /projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/parser.lua
(linters -var:unused))
(at /projects/core/src/test/resources/test-rom
; We should still be able to test deprecated members.
(linters -var:deprecated)

1376
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"rehype-highlight": "^7.0.0",
"rehype-react": "^8.0.0",
"rollup": "^4.0.0",
"tsx": "^3.12.10",
"tsx": "^4.7.0",
"typescript": "^5.2.2"
}
}

View File

@@ -27,8 +27,13 @@ public final class ComputerCraftAPIClient {
* @param serialiser The turtle upgrade serialiser.
* @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade.
* @deprecated This method can lead to confusing load behaviour on Forge. Use
* {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} on Fabric, or
* {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} on Forge.
*/
@Deprecated(forRemoval = true)
public static <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
// TODO(1.20.4): Remove this
getInstance().registerTurtleUpgradeModeller(serialiser, modeller);
}

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
/**
* A functional interface to register a {@link TurtleUpgradeModeller} for a class of turtle upgrades.
* <p>
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
* multiple loaders.
*/
@FunctionalInterface
public interface RegisterTurtleUpgradeModeller {
/**
* Register a {@link TurtleUpgradeModeller}.
*
* @param serialiser The turtle upgrade serialiser.
* @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade.
*/
<T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller);
}

View File

@@ -4,12 +4,10 @@
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.client.ComputerCraftAPIClient;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.nbt.CompoundTag;
@@ -21,9 +19,13 @@ import java.util.List;
/**
* Provides models for a {@link ITurtleUpgrade}.
* <p>
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
* on Forge
*
* @param <T> The type of turtle upgrade this modeller applies to.
* @see ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller) To register a modeller.
* @see RegisterTurtleUpgradeModeller For multi-loader registration support.
*/
public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
/**

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.api.network.wired;
import dan200.computercraft.api.peripheral.IPeripheral;
import org.jetbrains.annotations.ApiStatus;
import java.util.Map;
@@ -22,6 +23,7 @@ import java.util.Map;
*
* @see WiredNode#getNetwork()
*/
@ApiStatus.NonExtendable
public interface WiredNetwork {
/**
* Create a connection between two nodes.
@@ -35,7 +37,9 @@ public interface WiredNetwork {
* @throws IllegalArgumentException If {@code left} and {@code right} are equal.
* @see WiredNode#connectTo(WiredNode)
* @see WiredNetwork#connect(WiredNode, WiredNode)
* @deprecated Use {@link WiredNode#connectTo(WiredNode)}
*/
@Deprecated
boolean connect(WiredNode left, WiredNode right);
/**
@@ -50,7 +54,9 @@ public interface WiredNetwork {
* @throws IllegalArgumentException If {@code left} and {@code right} are equal.
* @see WiredNode#disconnectFrom(WiredNode)
* @see WiredNetwork#connect(WiredNode, WiredNode)
* @deprecated Use {@link WiredNode#disconnectFrom(WiredNode)}
*/
@Deprecated
boolean disconnect(WiredNode left, WiredNode right);
/**
@@ -64,7 +70,9 @@ public interface WiredNetwork {
* only element.
* @throws IllegalArgumentException If the node is not in the network.
* @see WiredNode#remove()
* @deprecated Use {@link WiredNode#remove()}
*/
@Deprecated
boolean remove(WiredNode node);
/**
@@ -77,6 +85,8 @@ public interface WiredNetwork {
* @param peripherals The new peripherals for this node.
* @throws IllegalArgumentException If the node is not in the network.
* @see WiredNode#updatePeripherals(Map)
* @deprecated Use {@link WiredNode#updatePeripherals(Map)}
*/
@Deprecated
void updatePeripherals(WiredNode node, Map<String, IPeripheral> peripherals);
}

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.api.network.wired;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.peripheral.IPeripheral;
import org.jetbrains.annotations.ApiStatus;
import java.util.Map;
@@ -22,6 +23,7 @@ import java.util.Map;
* Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever
* be used on the main server thread.
*/
@ApiStatus.NonExtendable
public interface WiredNode extends PacketNetwork {
/**
* The associated element for this network node.
@@ -37,7 +39,9 @@ public interface WiredNode extends PacketNetwork {
* This should only be used on the server thread.
*
* @return This node's network.
* @deprecated Use the connect/disconnect/remove methods on {@link WiredNode}.
*/
@Deprecated
WiredNetwork getNetwork();
/**
@@ -47,12 +51,9 @@ public interface WiredNode extends PacketNetwork {
*
* @param node The other node to connect to.
* @return {@code true} if a connection was created or {@code false} if the connection already exists.
* @see WiredNetwork#connect(WiredNode, WiredNode)
* @see WiredNode#disconnectFrom(WiredNode)
*/
default boolean connectTo(WiredNode node) {
return getNetwork().connect(this, node);
}
boolean connectTo(WiredNode node);
/**
* Destroy a connection between this node and another.
@@ -61,13 +62,9 @@ public interface WiredNode extends PacketNetwork {
*
* @param node The other node to disconnect from.
* @return {@code true} if a connection was destroyed or {@code false} if no connection exists.
* @throws IllegalArgumentException If {@code node} is not on the same network.
* @see WiredNetwork#disconnect(WiredNode, WiredNode)
* @see WiredNode#connectTo(WiredNode)
*/
default boolean disconnectFrom(WiredNode node) {
return getNetwork().disconnect(this, node);
}
boolean disconnectFrom(WiredNode node);
/**
* Sever all connections this node has, removing it from this network.
@@ -78,11 +75,8 @@ public interface WiredNode extends PacketNetwork {
* @return Whether this node was removed from the network. One cannot remove a node from a network where it is the
* only element.
* @throws IllegalArgumentException If the node is not in the network.
* @see WiredNetwork#remove(WiredNode)
*/
default boolean remove() {
return getNetwork().remove(this);
}
boolean remove();
/**
* Mark this node's peripherals as having changed.
@@ -91,9 +85,6 @@ public interface WiredNode extends PacketNetwork {
* that your network element owns.
*
* @param peripherals The new peripherals for this node.
* @see WiredNetwork#updatePeripherals(WiredNode, Map)
*/
default void updatePeripherals(Map<String, IPeripheral> peripherals) {
getNetwork().updatePeripherals(this, peripherals);
}
void updatePeripherals(Map<String, IPeripheral> peripherals);
}

View File

@@ -48,13 +48,8 @@ import java.util.function.Function;
* }
* }</pre>
* <p>
* Finally, we need to register a model for our upgrade. This is done with
* {@link dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller}:
*
* <pre>{@code
* // Register our model inside FMLClientSetupEvent
* ComputerCraftAPIClient.registerTurtleUpgradeModeller(MY_UPGRADE.get(), TurtleUpgradeModeller.flatItem())
* }</pre>
* Finally, we need to register a model for our upgrade. The way to do this varies on mod loader, see
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information.
* <p>
* {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
*

View File

@@ -2,13 +2,12 @@
//
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.annotationProcessorEverywhere
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
import cc.tweaked.gradle.*
plugins {
id("cc-tweaked.vanilla")
id("cc-tweaked.gametest")
id("cc-tweaked.illuaminate")
id("cc-tweaked.publishing")
}
@@ -19,6 +18,18 @@ minecraft {
)
}
configurations {
register("cctJavadoc")
}
repositories {
maven("https://maven.minecraftforge.net/") {
content {
includeModule("org.spongepowered", "mixin")
}
}
}
dependencies {
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
implementation(project(":core"))
@@ -28,7 +39,6 @@ dependencies {
compileOnly(libs.bundles.externalMods.common)
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
compileOnly(libs.mixin)
annotationProcessorEverywhere(libs.autoService)
testFixturesAnnotationProcessor(libs.autoService)
@@ -36,9 +46,62 @@ dependencies {
testImplementation(libs.bundles.test)
testRuntimeOnly(libs.bundles.testRuntime)
testImplementation(libs.jmh)
testAnnotationProcessor(libs.jmh.processor)
testModCompileOnly(libs.mixin)
testModImplementation(testFixtures(project(":core")))
testModImplementation(testFixtures(project(":common")))
testModImplementation(libs.bundles.kotlin)
testFixturesImplementation(testFixtures(project(":core")))
"cctJavadoc"(libs.cctJavadoc)
}
illuaminate {
version.set(libs.versions.illuaminate)
}
val luaJavadoc by tasks.registering(Javadoc::class) {
description = "Generates documentation for Java-side Lua functions."
group = JavaBasePlugin.DOCUMENTATION_GROUP
val sourceSets = listOf(sourceSets.main.get(), project(":core").sourceSets.main.get())
for (sourceSet in sourceSets) {
source(sourceSet.java)
classpath += sourceSet.compileClasspath
}
destinationDir = layout.buildDirectory.dir("docs/luaJavadoc").get().asFile
val options = options as StandardJavadocDocletOptions
options.docletpath = configurations["cctJavadoc"].files.toList()
options.doclet = "cc.tweaked.javadoc.LuaDoclet"
options.addStringOption("project-root", rootProject.file(".").absolutePath)
options.noTimestamp(false)
javadocTool.set(
javaToolchains.javadocToolFor {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
},
)
}
val lintLua by tasks.registering(IlluaminateExec::class) {
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Lint Lua (and Lua docs) with illuaminate"
// Config files
inputs.file(rootProject.file("illuaminate.sexp")).withPropertyName("illuaminate.sexp")
// Sources
inputs.files(rootProject.fileTree("doc")).withPropertyName("docs")
inputs.files(project(":core").fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom")
inputs.files(luaJavadoc)
args = listOf("lint")
workingDir = rootProject.projectDir
doFirst { if (System.getenv("GITHUB_ACTIONS") != null) println("::add-matcher::.github/matchers/illuaminate.json") }
doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
}

View File

@@ -17,8 +17,6 @@ import dan200.computercraft.client.render.monitor.MonitorRenderState;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
@@ -28,7 +26,6 @@ import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.PauseAwareTimer;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.Util;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
@@ -43,7 +40,6 @@ import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import javax.annotation.Nullable;
import java.io.File;
import java.util.function.Consumer;
/**
@@ -71,10 +67,6 @@ public final class ClientHooks {
ClientPocketComputers.reset();
}
public static boolean onChatMessage(String message) {
return handleOpenComputerCommand(message);
}
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit)
|| MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit);
@@ -109,34 +101,6 @@ public final class ClientHooks {
SpeakerManager.onPlayStreaming(engine, channel, stream);
}
/**
* Handle the {@link CommandComputerCraft#OPEN_COMPUTER} "clientside command". This isn't a true command, as we
* don't want it to actually be visible to the user.
*
* @param message The current chat message.
* @return Whether to cancel sending this message.
*/
private static boolean handleOpenComputerCommand(String message) {
if (!message.startsWith(CommandComputerCraft.OPEN_COMPUTER)) return false;
var server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) return false;
var idStr = message.substring(CommandComputerCraft.OPEN_COMPUTER.length()).trim();
int id;
try {
id = Integer.parseInt(idStr);
} catch (NumberFormatException ignore) {
return false;
}
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) return false;
Util.getPlatform().openFile(file);
return true;
}
/**
* Add additional information about the currently targeted block to the debug screen.
*

View File

@@ -4,8 +4,12 @@
package dan200.computercraft.client;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.ComputerCraftAPIClient;
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.pocket.ClientPocketComputers;
@@ -16,11 +20,14 @@ import dan200.computercraft.client.turtle.TurtleModemModeller;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemColor;
import net.minecraft.client.gui.screens.MenuScreens;
@@ -30,6 +37,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceProvider;
@@ -39,6 +47,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ItemLike;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -60,18 +69,6 @@ public final class ClientRegistry {
* Register any client-side objects which don't have to be done on the main thread.
*/
public static void register() {
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_NORMAL.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new);
@@ -103,6 +100,20 @@ public final class ClientRegistry {
);
}
public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
register.register(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
register.register(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
register.register(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
register.register(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
register.register(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
}
@SafeVarargs
private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
@@ -179,4 +190,45 @@ public final class ClientRegistry {
return function.unclampedCall(stack, level, entity, layer);
}
}
/**
* Register client-side commands.
*
* @param dispatcher The dispatcher to register the commands to.
* @param sendError A function to send an error message.
* @param <T> The type of the client-side command context.
*/
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
.then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
.executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
));
}
/**
* Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
*
* @param context The command context.
* @param sendError A function to send an error message.
* @param id The computer's id.
* @param <T> The type of the client-side command context.
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
*/
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
var server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) {
sendError.accept(context, Component.literal("Not on a single-player server"));
return 0;
}
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) {
sendError.accept(context, Component.literal("Computer's folder does not exist"));
return 0;
}
Util.getPlatform().openFile(file);
return 1;
}
}

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.InputHandler;
@@ -19,6 +19,7 @@ import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
@@ -144,6 +145,11 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|| super.mouseDragged(x, y, button, deltaX, deltaY);
}
@Override
public void setFocused(@Nullable GuiEventListener listener) {
// Don't clear and re-focus if we're already focused.
if (listener != getFocused()) super.setFocused(listener);
}
@Override
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
@@ -201,7 +207,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
return;
}
if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientPlatformHelper.get()::sendToServer);
if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer);
}
public void uploadResult(UploadResult result, @Nullable Component message) {

View File

@@ -4,7 +4,7 @@
package dan200.computercraft.client.gui;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
@@ -29,51 +29,51 @@ public final class ClientInputHandler implements InputHandler {
@Override
public void turnOn() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
}
@Override
public void shutdown() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
}
@Override
public void reboot() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
ClientPlatformHelper.get().sendToServer(new QueueEventServerMessage(menu, event, arguments));
ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
}
@Override
public void keyDown(int key, boolean repeat) {
ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
}
@Override
public void keyUp(int key) {
ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
}
@Override
public void mouseClick(int button, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
}
@Override
public void mouseUp(int button, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
}
@Override
public void mouseDrag(int button, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
}
@Override
public void mouseScroll(int direction, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
}
}

View File

@@ -8,8 +8,8 @@ import com.mojang.blaze3d.vertex.Tesselator;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.core.InputHandler;
import net.minecraft.SharedConstants;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractWidget;
@@ -112,26 +112,8 @@ public class TerminalWidget extends AbstractWidget {
}
private void paste() {
var clipboard = Minecraft.getInstance().keyboardHandler.getClipboard();
// Clip to the first occurrence of \r or \n
var newLineIndex1 = clipboard.indexOf('\r');
var newLineIndex2 = clipboard.indexOf('\n');
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
clipboard = clipboard.substring(0, Math.min(newLineIndex1, newLineIndex2));
} else if (newLineIndex1 >= 0) {
clipboard = clipboard.substring(0, newLineIndex1);
} else if (newLineIndex2 >= 0) {
clipboard = clipboard.substring(0, newLineIndex2);
}
// Filter the string
clipboard = SharedConstants.filterText(clipboard);
if (!clipboard.isEmpty()) {
// Clip to 512 characters and queue the event
if (clipboard.length() > 512) clipboard = clipboard.substring(0, 512);
computer.queueEvent("paste", new Object[]{ clipboard });
}
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
}
@Override
@@ -264,7 +246,7 @@ public class TerminalWidget extends AbstractWidget {
keysDown.clear();
// When blurring, we should make the last mouse button go up
if (lastMouseButton > 0) {
if (lastMouseButton >= 0) {
computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = -1;
}

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.network;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import net.minecraft.client.Minecraft;
/**
* Methods for sending packets from clients to the server.
*/
public final class ClientNetworking {
private ClientNetworking() {
}
/**
* Send a network message to the server.
*
* @param message The message to send.
*/
public static void sendToServer(NetworkMessage<ServerNetworkContext> message) {
var connection = Minecraft.getInstance().getConnection();
if (connection != null) connection.send(ClientPlatformHelper.get().createPacket(message));
}
}

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import dan200.computercraft.client.ClientTableFormatter;
import dan200.computercraft.client.gui.AbstractComputerScreen;
import dan200.computercraft.client.gui.OptionScreen;
@@ -17,30 +18,30 @@ import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* The base implementation of {@link ClientNetworkContext}.
* <p>
* This should be extended by mod loader specific modules with the remaining abstract methods.
* The client-side implementation of {@link ClientNetworkContext}.
*/
public abstract class AbstractClientNetworkContext implements ClientNetworkContext {
@AutoService(ClientNetworkContext.class)
public final class ClientNetworkContextImpl implements ClientNetworkContext {
@Override
public final void handleChatTable(TableBuilder table) {
public void handleChatTable(TableBuilder table) {
ClientTableFormatter.INSTANCE.display(table);
}
@Override
public final void handleComputerTerminal(int containerId, TerminalState terminal) {
public void handleComputerTerminal(int containerId, TerminalState terminal) {
Player player = Minecraft.getInstance().player;
if (player != null && player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
menu.updateTerminal(terminal);
@@ -48,7 +49,7 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
}
@Override
public final void handleMonitorData(BlockPos pos, TerminalState terminal) {
public void handleMonitorData(BlockPos pos, TerminalState terminal) {
var player = Minecraft.getInstance().player;
if (player == null) return;
@@ -59,44 +60,46 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
}
@Override
public final void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name) {
var mc = Minecraft.getInstance();
ClientPlatformHelper.get().playStreamingMusic(pos, sound);
if (name != null) mc.gui.setNowPlaying(Component.literal(name));
}
@Override
public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
var computer = ClientPocketComputers.get(instanceId, terminal.colour);
computer.setState(state, lightState);
if (terminal.hasTerminal()) computer.setTerminal(terminal);
}
@Override
public final void handlePocketComputerDeleted(int instanceId) {
public void handlePocketComputerDeleted(int instanceId) {
ClientPocketComputers.remove(instanceId);
}
@Override
public final void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume);
public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
}
@Override
public final void handleSpeakerAudioPush(UUID source, ByteBuf buffer) {
SpeakerManager.getSound(source).pushAudio(buffer);
}
@Override
public final void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
public void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
SpeakerManager.moveSound(source, reifyPosition(position));
}
@Override
public final void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
public void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
SpeakerManager.getSound(source).playSound(reifyPosition(position), sound, volume, pitch);
}
@Override
public final void handleSpeakerStop(UUID source) {
public void handleSpeakerStop(UUID source) {
SpeakerManager.stopSound(source);
}
@Override
public final void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
public void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
var minecraft = Minecraft.getInstance();
var screen = OptionScreen.unwrap(minecraft.screen);

View File

@@ -9,6 +9,10 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerGamePacketListener;
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
@@ -18,11 +22,12 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
}
/**
* Send a network message to the server.
* Convert a serverbound {@link NetworkMessage} to a Minecraft {@link Packet}.
*
* @param message The message to send.
* @param message The messsge to convert.
* @return The converted message.
*/
void sendToServer(NetworkMessage<ServerNetworkContext> message);
Packet<ServerGamePacketListener> createPacket(NetworkMessage<ServerNetworkContext> message);
/**
* Render a {@link BakedModel}, using any loader-specific hooks.
@@ -35,4 +40,13 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
* @param tints Block colour tints to apply to the model.
*/
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints);
/**
* Play a record at a particular position.
*
* @param pos The position to play this record.
* @param sound The record to play, or {@code null} to stop it.
* @see net.minecraft.client.renderer.LevelRenderer#playStreamingMusic(SoundEvent, BlockPos)
*/
void playStreamingMusic(BlockPos pos, @Nullable SoundEvent sound);
}

View File

@@ -11,7 +11,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
@@ -21,7 +20,6 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
@@ -29,8 +27,6 @@ import net.minecraft.world.phys.HitResult;
import javax.annotation.Nullable;
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_normal", "inventory");
private static final ModelResourceLocation ADVANCED_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced", "inventory");
private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
private static final ResourceLocation ELF_OVERLAY_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
@@ -42,13 +38,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
font = context.getFont();
}
public static ResourceLocation getTurtleModel(ComputerFamily family, boolean coloured) {
return switch (family) {
default -> coloured ? COLOUR_TURTLE_MODEL : NORMAL_TURTLE_MODEL;
case ADVANCED -> coloured ? COLOUR_TURTLE_MODEL : ADVANCED_TURTLE_MODEL;
};
}
public static @Nullable ResourceLocation getTurtleOverlayModel(@Nullable ResourceLocation overlay, boolean christmas) {
if (overlay != null) return overlay;
if (christmas) return ELF_OVERLAY_MODEL;
@@ -78,7 +67,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
var matrix = transform.last().pose();
var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
var width = -font.width(label) / 2.0f;
// TODO: Check this looks okay
font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
font.drawInBatch(label, width, (float) 0, 0xffffffff, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
@@ -96,10 +84,18 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
// Render the turtle
var colour = turtle.getColour();
var family = turtle.getFamily();
var overlay = turtle.getOverlay();
renderModel(transform, buffers, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
if (colour == -1) {
// Render the turtle using its item model.
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper();
var model = modelManager.getItemModel(turtle.getBlockState().getBlock().asItem());
if (model == null) model = modelManager.getModelManager().getMissingModel();
renderModel(transform, buffers, lightmapCoord, overlayLight, model, null);
} else {
// Otherwise render it using the colour item.
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ colour });
}
// Render the overlay
var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);

View File

@@ -58,9 +58,9 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
@Override
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
// Render from the origin monitor
var originTerminal = monitor.getClientMonitor();
var originTerminal = monitor.getOriginClientMonitor();
if (originTerminal == null) return;
var origin = originTerminal.getOrigin();
var renderState = originTerminal.getRenderState(MonitorRenderState::new);
var monitorPos = monitor.getBlockPos();

View File

@@ -6,7 +6,7 @@ package dan200.computercraft.client.sound;
import com.mojang.blaze3d.audio.Channel;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import io.netty.buffer.ByteBuf;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.sounds.AudioStream;
import net.minecraft.client.sounds.SoundEngine;
import org.lwjgl.BufferUtils;
@@ -36,7 +36,7 @@ class DfpwmStream implements AudioStream {
/**
* The {@link Channel} which this sound is playing on.
*
* @see SpeakerInstance#pushAudio(ByteBuf)
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
*/
@Nullable
Channel channel;
@@ -44,7 +44,7 @@ class DfpwmStream implements AudioStream {
/**
* The underlying {@link SoundEngine} executor.
*
* @see SpeakerInstance#pushAudio(ByteBuf)
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
* @see SoundEngine#executor
*/
@Nullable
@@ -58,12 +58,12 @@ class DfpwmStream implements AudioStream {
DfpwmStream() {
}
void push(ByteBuf input) {
var readable = input.readableBytes();
void push(ByteBuffer input) {
var readable = input.remaining();
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
for (var i = 0; i < readable; i++) {
var inputByte = input.readByte();
var inputByte = input.get();
for (var j = 0; j < 8; j++) {
var currentBit = (inputByte & 1) != 0;
var target = currentBit ? 127 : -128;

View File

@@ -7,11 +7,11 @@ package dan200.computercraft.client.sound;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
@@ -25,7 +25,7 @@ public class SpeakerInstance {
SpeakerInstance() {
}
public synchronized void pushAudio(ByteBuf buffer) {
private void pushAudio(ByteBuffer buffer) {
var sound = this.sound;
var stream = currentStream;
@@ -43,7 +43,9 @@ public class SpeakerInstance {
}
}
public void playAudio(SpeakerPosition position, float volume) {
public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) {
pushAudio(buffer);
var soundManager = Minecraft.getInstance().getSoundManager();
if (sound != null && sound.stream != currentStream) {

View File

@@ -11,11 +11,14 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.impl.PlatformHelper;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager;
import net.minecraft.client.Minecraft;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.WeakHashMap;
@@ -24,14 +27,15 @@ import java.util.stream.Stream;
/**
* A registry of {@link TurtleUpgradeModeller}s.
*
* @see dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller)
*/
public final class TurtleUpgradeModellers {
private static final Logger LOG = LoggerFactory.getLogger(TurtleUpgradeModellers.class);
private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side) ->
new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
private static final Map<TurtleUpgradeSerialiser<?>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
private static volatile boolean fetchedModels;
/**
* In order to avoid a double lookup of {@link ITurtleUpgrade} to {@link UpgradeManager.UpgradeWrapper} to
@@ -45,12 +49,18 @@ public final class TurtleUpgradeModellers {
}
public static <T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
synchronized (turtleModels) {
if (turtleModels.containsKey(serialiser)) {
throw new IllegalStateException("Modeller already registered for serialiser");
}
if (fetchedModels) {
// TODO(1.20.4): Replace with an error.
LOG.warn(
"Turtle upgrade serialiser {} was registered too late, its models may not be loaded correctly. If you are " +
"the mod author, you may be using a deprecated API - see https://github.com/cc-tweaked/CC-Tweaked/pull/1684 " +
"for further information.",
PlatformHelper.get().getRegistryKey(TurtleUpgradeSerialiser.registryId(), serialiser)
);
}
turtleModels.put(serialiser, modeller);
if (turtleModels.putIfAbsent(serialiser, modeller) != null) {
throw new IllegalStateException("Modeller already registered for serialiser");
}
}
@@ -75,6 +85,7 @@ public final class TurtleUpgradeModellers {
}
public static Stream<ResourceLocation> getDependencies() {
fetchedModels = true;
return turtleModels.values().stream().flatMap(x -> x.getDependencies().stream());
}
}

View File

@@ -1,13 +0,0 @@
{
"required": true,
"package": "dan200.computercraft.mixin.client",
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"injectors": {
"defaultRequire": 1
},
"client": [
"ClientPacketListenerMixin"
],
"refmap": "client-computercraft.refmap.json"
}

View File

@@ -174,6 +174,7 @@ public final class LanguageProvider implements DataProvider {
// Metrics
add(Metrics.COMPUTER_TASKS, "Tasks");
add(Metrics.SERVER_TASKS, "Server tasks");
add(Metrics.JAVA_ALLOCATION, "Java Allocations");
add(Metrics.PERIPHERAL_OPS, "Peripheral calls");
add(Metrics.FS_OPS, "Filesystem operations");
add(Metrics.HTTP_REQUESTS, "HTTP requests");

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
/**
* Wraps an existing {@link DataProvider}, passing generated JSON through {@link PrettyJsonWriter}.
*
* @param provider The provider to wrap.
* @param <T> The type of the provider to wrap.
*/
public record PrettyDataProvider<T extends DataProvider>(T provider) implements DataProvider {
@Override
public CompletableFuture<?> run(CachedOutput cachedOutput) {
return provider.run(new Output(cachedOutput));
}
@Override
public String getName() {
return provider.getName();
}
private record Output(CachedOutput output) implements CachedOutput {
@SuppressWarnings("deprecation")
private static final HashFunction HASH_FUNCTION = Hashing.sha1();
@Override
public void writeIfNeeded(Path path, byte[] bytes, HashCode hashCode) throws IOException {
if (path.getFileName().toString().endsWith(".json")) {
bytes = PrettyJsonWriter.reformat(bytes);
hashCode = HASH_FUNCTION.hashBytes(bytes);
}
output.writeIfNeeded(path, bytes, hashCode);
}
}
}

View File

@@ -22,11 +22,10 @@ import java.util.List;
/**
* Alternative version of {@link JsonWriter} which attempts to lay out the JSON in a more compact format.
* <p>
* Yes, this is at least a little deranged.
*
* @see PrettyDataProvider
*/
public class PrettyJsonWriter extends JsonWriter {
public static final boolean ENABLED = System.getProperty("cct.pretty-json") != null;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
private static final int MAX_WIDTH = 120;
@@ -44,17 +43,6 @@ public class PrettyJsonWriter extends JsonWriter {
this.out = out;
}
/**
* Create a JSON writer. This will either be a pretty or normal version, depending on whether the global flag is
* set.
*
* @param out The writer to emit to.
* @return The constructed JSON writer.
*/
public static JsonWriter createWriter(Writer out) {
return ENABLED ? new PrettyJsonWriter(out) : new JsonWriter(out);
}
/**
* Reformat a JSON string with our pretty printer.
*
@@ -62,8 +50,6 @@ public class PrettyJsonWriter extends JsonWriter {
* @return The reformatted string.
*/
public static byte[] reformat(byte[] contents) {
if (!ENABLED) return contents;
JsonElement object;
try (var reader = new InputStreamReader(new ByteArrayInputStream(contents), StandardCharsets.UTF_8)) {
object = GSON.fromJson(reader, JsonElement.class);

View File

@@ -13,7 +13,6 @@ import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.platform.RecipeIngredients;
import dan200.computercraft.shared.platform.RegistryWrappers;
@@ -38,7 +37,6 @@ import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Blocks;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Consumer;
@@ -106,14 +104,13 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private void turtleUpgrades(Consumer<FinishedRecipe> add) {
for (var turtleItem : turtleItems()) {
var base = turtleItem.create(-1, null, -1, null, null, 0, null);
var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
var name = RegistryWrappers.ITEMS.getKey(turtleItem);
for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) {
var result = turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), -1, null);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, result.getItem())
.group(String.format("%s:turtle_%s", ComputerCraftAPI.MOD_ID, nameId))
.group(name.toString())
.pattern("#T")
.define('T', base.getItem())
.define('#', upgrade.getCraftingItem().getItem())
@@ -121,9 +118,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("turtle_%s/%s/%s",
nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
))
name.withSuffix(String.format("/%s/%s", upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()))
);
}
}
@@ -141,15 +136,13 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private void pocketUpgrades(Consumer<FinishedRecipe> add) {
for (var pocket : pocketComputerItems()) {
var base = pocket.create(-1, null, -1, null);
if (base.isEmpty()) continue;
var nameId = pocket.getFamily().name().toLowerCase(Locale.ROOT);
var name = RegistryWrappers.ITEMS.getKey(pocket).withPath(x -> x.replace("pocket_computer_", "pocket_"));
for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) {
var result = pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, result.getItem())
.group(String.format("%s:pocket_%s", ComputerCraftAPI.MOD_ID, nameId))
.group(name.toString())
.pattern("#")
.pattern("P")
.define('P', base.getItem())
@@ -158,9 +151,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("pocket_%s/%s/%s",
nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
))
name.withSuffix(String.format("/%s/%s", upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()))
);
}
}
@@ -190,12 +181,10 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private void turtleOverlay(Consumer<FinishedRecipe> add, String overlay, Consumer<ShapelessRecipeBuilder> build) {
for (var turtleItem : turtleItems()) {
var base = turtleItem.create(-1, null, -1, null, null, 0, null);
var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
var group = "%s:turtle_%s_overlay".formatted(ComputerCraftAPI.MOD_ID, nameId);
var name = RegistryWrappers.ITEMS.getKey(turtleItem);
var builder = ShapelessRecipeBuilder.shapeless(RecipeCategory.REDSTONE, base.getItem())
.group(group)
.group(name.withSuffix("_overlay").toString())
.unlockedBy("has_turtle", inventoryChange(base.getItem()));
build.accept(builder);
builder
@@ -204,7 +193,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
RecipeWrapper
.wrap(ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get(), add)
.withExtraData(x -> x.addProperty("overlay", new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay).toString())),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_%s_overlays/%s".formatted(nameId, overlay))
name.withSuffix("_overlays/" + overlay)
);
}
}
@@ -253,7 +242,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade")
);
@@ -277,7 +266,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.NORMAL)));
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.TURTLE_ADVANCED.get())
@@ -288,7 +277,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)));
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.TURTLE_ADVANCED.get())
@@ -300,7 +289,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('B', ingredients.goldBlock())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade")
);
@@ -367,7 +356,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade")
);
@@ -519,10 +508,6 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
return tag;
}
private static Consumer<JsonObject> family(ComputerFamily family) {
return json -> json.addProperty("family", family.getSerializedName());
}
private static void addSpecial(Consumer<FinishedRecipe> add, SimpleCraftingRecipeSerializer<?> special) {
SpecialRecipeBuilder.special(special).save(add, RegistryWrappers.RECIPE_SERIALIZERS.getKey(special).toString());
}

View File

@@ -58,7 +58,10 @@ class TagProvider {
tags.tag(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).addTag(BlockTags.WOOL).add(Blocks.COBWEB);
tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE).addTag(BlockTags.CAULDRONS).addTag(BlockTags.BEEHIVES);
tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE)
.addTag(BlockTags.BEEHIVES)
.addTag(BlockTags.CAULDRONS)
.add(Blocks.COMPOSTER);
// Make all blocks aside from command computer mineable.
tags.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(
@@ -76,6 +79,8 @@ class TagProvider {
ModRegistry.Blocks.WIRED_MODEM_FULL.get(),
ModRegistry.Blocks.CABLE.get()
);
tags.tag(BlockTags.WITHER_IMMUNE).add(ModRegistry.Blocks.COMPUTER_COMMAND.get());
}
public static void itemTags(ItemTagConsumer tags) {

View File

@@ -123,7 +123,7 @@ public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends
// TODO: Can we track which mod this resource came from and use that instead? It's theoretically possible,
// but maybe not ideal for datapacks.
var modId = id.getNamespace();
if (modId.equals("minecraft") || modId.equals("")) modId = ComputerCraftAPI.MOD_ID;
if (modId.equals("minecraft") || modId.isEmpty()) modId = ComputerCraftAPI.MOD_ID;
var upgrade = serialiser.fromJson(id, root);
if (!upgrade.getUpgradeID().equals(id)) {

View File

@@ -4,45 +4,66 @@
package dan200.computercraft.impl.network.wired;
import org.jetbrains.annotations.Contract;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
/**
* Verifies certain elements of a network are "well formed".
* Verifies certain elements of a network are well-formed.
* <p>
* This adds substantial overhead to network modification, and so should only be enabled
* in a development environment.
* This adds substantial overhead to network modification, and so is only enabled when assertions are enabled.
*/
public final class InvariantChecker {
final class InvariantChecker {
private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
private static final boolean ENABLED = false;
private InvariantChecker() {
}
public static void checkNode(WiredNodeImpl node) {
if (!ENABLED) return;
static void checkNode(WiredNodeImpl node) {
assert checkNodeImpl(node) : "Node invariants failed. See logs.";
}
var network = node.network;
if (network == null) {
LOG.error("Node's network is null", new Exception());
return;
private static boolean checkNodeImpl(WiredNodeImpl node) {
var okay = true;
if (node.currentSet != null) {
okay = false;
LOG.error("{}: currentSet was not cleared.", node);
}
if (network.nodes == null || !network.nodes.contains(node)) {
LOG.error("Node's network does not contain node", new Exception());
var network = makeNullable(node.network);
if (network == null) {
okay = false;
LOG.error("{}: Node's network is null.", node);
} else if (makeNullable(network.nodes) == null || !network.nodes.contains(node)) {
okay = false;
LOG.error("{}: Node's network does not contain node.", node);
}
for (var neighbour : node.neighbours) {
if (!neighbour.neighbours.contains(node)) {
LOG.error("Neighbour is missing node", new Exception());
okay = false;
LOG.error("{}: Neighbour {}'s neighbour set does not contain origianl node.", node, neighbour);
}
}
return okay;
}
public static void checkNetwork(WiredNetworkImpl network) {
if (!ENABLED) return;
static void checkNetwork(WiredNetworkImpl network) {
assert checkNetworkImpl(network) : "Network invariants failed. See logs.";
}
for (var node : network.nodes) checkNode(node);
private static boolean checkNetworkImpl(WiredNetworkImpl network) {
var okay = true;
for (var node : network.nodes) okay &= checkNodeImpl(node);
return okay;
}
@Contract("")
private static <T> @Nullable T makeNullable(T object) {
return object;
}
}

View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.network.wired;
import dan200.computercraft.api.network.wired.WiredNode;
import javax.annotation.Nullable;
import java.util.Objects;
/**
* A disjoint-set/union-find of {@link WiredNodeImpl}s.
* <p>
* Rather than actually maintaining a list of included nodes, wired nodes store {@linkplain WiredNodeImpl#currentSet the
* set they're part of}. This means that we can only have one disjoint-set at once, but that is not a problem in
* practice.
*
* @see WiredNodeImpl#currentSet
* @see WiredNetworkImpl#remove(WiredNode)
* @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint-set data structure</a>
*/
class NodeSet {
private NodeSet parent = this;
private int size = 1;
private @Nullable WiredNetworkImpl network;
private boolean isRoot() {
return parent == this;
}
/**
* Resolve this union, finding the root {@link NodeSet}.
*
* @return The root union.
*/
NodeSet find() {
var self = this;
while (!self.isRoot()) self = self.parent = self.parent.parent;
return self;
}
/**
* Get the size of this node set.
*
* @return The size of the set.
*/
int size() {
return find().size;
}
/**
* Add a node to this {@link NodeSet}.
*
* @param node The node to add to the set.
*/
void addNode(WiredNodeImpl node) {
if (!isRoot()) throw new IllegalStateException("Cannot grow a non-root set.");
if (node.currentSet != null) throw new IllegalArgumentException("Node is already in a set.");
node.currentSet = this;
size++;
}
/**
* Merge two nodes sets together.
*
* @param left The first union.
* @param right The second union.
* @return The union which was subsumed.
*/
public static NodeSet merge(NodeSet left, NodeSet right) {
if (!left.isRoot() || !right.isRoot()) throw new IllegalArgumentException("Cannot union a non-root set.");
if (left == right) throw new IllegalArgumentException("Cannot merge a node into itself.");
return left.size >= right.size ? mergeInto(left, right) : mergeInto(right, left);
}
private static NodeSet mergeInto(NodeSet root, NodeSet child) {
assert root.size > child.size;
child.parent = root;
root.size += child.size;
return child;
}
void setNetwork(WiredNetworkImpl network) {
if (!isRoot()) throw new IllegalStateException("Set is not the root.");
if (this.network != null) throw new IllegalStateException("Set already has a network.");
this.network = network;
}
/**
* Get the associated network.
*
* @return The associated network.
*/
WiredNetworkImpl network() {
return Objects.requireNonNull(find().network);
}
}

View File

@@ -8,6 +8,7 @@ import dan200.computercraft.api.network.Packet;
import dan200.computercraft.api.network.wired.WiredNetwork;
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.Nullability;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
@@ -187,10 +188,76 @@ final class WiredNetworkImpl implements WiredNetwork {
return true;
}
var reachable = reachableNodes(neighbours.iterator().next());
assert neighbours.size() >= 2 : "Must have more than one neighbour.";
/*
Otherwise we need to find all sets of connected nodes within the graph, and split them off into their own
networks.
With our current graph representation[^1], this requires a traversal of the graph, taking O(|V| + |E))
time, which can get quite expensive for large graphs. We try to avoid this traversal where possible, by
optimising for the case where the graph remains fully connected after removing this node, for instance,
removing "A" here:
A---B B
| | => |
C---D C---D
We observe that these sorts of loops tend to be local, and so try to identify them as quickly as possible.
To do this, we do a standard breadth-first traversal of the graph starting at the neighbours of the
removed node, building sets of connected nodes.
If, at any point, all nodes visited so far are connected to each other, then we know all remaining nodes
will also be connected. This allows us to abort our traversal of the graph, and just remove the node (much
like we do in the single neighbour case above).
Otherwise, we then just create a new network for each disjoint set of connected nodes.
{^1]:
There are efficient (near-logarithmic) algorithms for this (e.g. https://arxiv.org/pdf/1609.05867.pdf),
but they are significantly more complex to implement.
*/
// Create a new set of nodes for each neighbour, and add them to our queue of nodes to visit.
List<WiredNodeImpl> queue = new ArrayList<>();
Set<NodeSet> nodeSets = new HashSet<>(neighbours.size());
for (var neighbour : neighbours) {
nodeSets.add(neighbour.currentSet = new NodeSet());
queue.add(neighbour);
}
// Perform a breadth-first search of the graph, starting from the neighbours.
graphSearch:
for (var i = 0; i < queue.size(); i++) {
var enqueuedNode = queue.get(i);
for (var neighbour : enqueuedNode.neighbours) {
var nodeSet = Nullability.assertNonNull(enqueuedNode.currentSet).find();
// The neighbour has no set and so has not been visited yet. Add it to the current set and enqueue
// it to be visited.
if (neighbour.currentSet == null) {
nodeSet.addNode(neighbour);
queue.add(neighbour);
continue;
}
// Otherwise, take the union of the two nodes' sets if needed. If we've only got a single node set
// left, then we know the whole graph is network is connected (even if not all nodes have been
// visited) and so can abort early.
var neighbourSet = neighbour.currentSet.find();
if (nodeSet != neighbourSet) {
var removed = nodeSets.remove(NodeSet.merge(nodeSet, neighbourSet));
assert removed : "Merged set should have been ";
if (nodeSets.size() == 1) break graphSearch;
}
}
}
// If we have a single subset, then all nodes are reachable - just clear the set and exit.
if (nodeSets.size() == 1) {
assert nodeSets.iterator().next().size() == queue.size();
for (var neighbour : queue) neighbour.currentSet = null;
// If all nodes are reachable then exit.
if (reachable.size() == nodes.size()) {
// Broadcast our simple peripheral changes
removeSingleNode(wired, wiredNetwork);
InvariantChecker.checkNode(wired);
@@ -198,43 +265,46 @@ final class WiredNetworkImpl implements WiredNetwork {
return true;
}
// A split may cause 2..neighbours.size() separate networks, so we
// iterate through our neighbour list, generating child networks.
neighbours.removeAll(reachable);
var maximals = new ArrayList<WiredNetworkImpl>(neighbours.size() + 1);
maximals.add(wiredNetwork);
maximals.add(new WiredNetworkImpl(reachable));
assert queue.size() == nodes.size() : "Expected queue to contain all nodes.";
while (!neighbours.isEmpty()) {
reachable = reachableNodes(neighbours.iterator().next());
neighbours.removeAll(reachable);
maximals.add(new WiredNetworkImpl(reachable));
// Otherwise we need to create our new networks.
var networks = new ArrayList<WiredNetworkImpl>(1 + nodeSets.size());
// Add the network we've created for the removed node.
networks.add(wiredNetwork);
// And then create a new network for each disjoint subset.
for (var set : nodeSets) {
var network = new WiredNetworkImpl(new HashSet<>(set.size()));
set.setNetwork(network);
networks.add(network);
}
for (var network : maximals) network.lock.writeLock().lock();
for (var network : networks) network.lock.writeLock().lock();
try {
// We special case the original node: detaching all peripherals when needed.
wired.network = wiredNetwork;
wired.peripherals = Map.of();
wired.neighbours.clear();
// Ensure every network is finalised
for (var network : maximals) {
for (var child : network.nodes) {
child.network = network;
network.peripherals.putAll(child.peripherals);
}
// Add all nodes to their appropriate network.
for (var child : queue) {
var network = Nullability.assertNonNull(child.currentSet).network();
child.currentSet = null;
child.network = network;
network.nodes.add(child);
network.peripherals.putAll(child.peripherals);
}
for (var network : maximals) InvariantChecker.checkNetwork(network);
for (var network : networks) InvariantChecker.checkNetwork(network);
InvariantChecker.checkNode(wired);
// Then broadcast network changes once all nodes are finalised
for (var network : maximals) {
for (var network : networks) {
WiredNetworkChangeImpl.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
}
} finally {
for (var network : maximals) network.lock.writeLock().unlock();
for (var network : networks) network.lock.writeLock().unlock();
}
nodes.clear();
@@ -373,22 +443,4 @@ final class WiredNetworkImpl implements WiredNetwork {
throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
}
}
private static Set<WiredNodeImpl> reachableNodes(WiredNodeImpl start) {
Queue<WiredNodeImpl> enqueued = new ArrayDeque<>();
var reachable = new HashSet<WiredNodeImpl>();
reachable.add(start);
enqueued.add(start);
WiredNodeImpl node;
while ((node = enqueued.poll()) != null) {
for (var neighbour : node.neighbours) {
// Otherwise attempt to enqueue this neighbour as well.
if (reachable.add(neighbour)) enqueued.add(neighbour);
}
}
return reachable;
}
}

View File

@@ -27,11 +27,39 @@ public final class WiredNodeImpl implements WiredNode {
final HashSet<WiredNodeImpl> neighbours = new HashSet<>();
volatile WiredNetworkImpl network;
/**
* A temporary field used when checking network connectivity.
*
* @see WiredNetworkImpl#remove(WiredNode)
*/
@Nullable
NodeSet currentSet;
public WiredNodeImpl(WiredElement element) {
this.element = element;
network = new WiredNetworkImpl(this);
}
@Override
public boolean connectTo(WiredNode node) {
return network.connect(this, node);
}
@Override
public boolean disconnectFrom(WiredNode node) {
return network == ((WiredNodeImpl) node).network && network.disconnect(this, node);
}
@Override
public boolean remove() {
return network.remove(this);
}
@Override
public void updatePeripherals(Map<String, IPeripheral> peripherals) {
network.updatePeripherals(this, peripherals);
}
@Override
public synchronized void addReceiver(PacketReceiver receiver) {
if (receivers == null) receivers = new HashSet<>();

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin;
import dan200.computercraft.data.PrettyJsonWriter;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(targets = "net/minecraft/data/HashCache$CacheUpdater")
public class CacheUpdaterMixin {
@SuppressWarnings("UnusedMethod")
@ModifyArg(
method = "writeIfNeeded",
at = @At(value = "INVOKE", target = "Ljava/nio/file/Files;write(Ljava/nio/file/Path;[B[Ljava/nio/file/OpenOption;)Ljava/nio/file/Path;"),
require = 0
)
private byte[] reformatJson(byte[] contents) {
// It would be cleaner to do this inside DataProvider.saveStable, but Forge's version of Mixin doesn't allow us
// to inject into interfaces.
return PrettyJsonWriter.reformat(contents);
}
}

View File

@@ -14,12 +14,16 @@ import dan200.computercraft.shared.computer.metrics.ComputerMBean;
import dan200.computercraft.shared.peripheral.monitor.MonitorWatcher;
import dan200.computercraft.shared.util.DropConsumer;
import dan200.computercraft.shared.util.TickScheduler;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
@@ -69,10 +73,19 @@ public final class CommonHooks {
NetworkUtils.reset();
}
public static void onServerChunkUnload(LevelChunk chunk) {
if (!(chunk.getLevel() instanceof ServerLevel)) throw new IllegalArgumentException("Not a server chunk.");
TickScheduler.onChunkUnload(chunk);
}
public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
MonitorWatcher.onWatch(chunk, player);
}
public static void onChunkTicketLevelChanged(ServerLevel level, long chunkPos, int oldLevel, int newLevel) {
TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
}
public static final ResourceLocation TREASURE_DISK_LOOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
private static final Set<ResourceLocation> TREASURE_DISK_LOOT_TABLES = Set.of(
@@ -111,4 +124,17 @@ public final class CommonHooks {
public static boolean onLivingDrop(Entity entity, ItemStack stack) {
return DropConsumer.onLivingDrop(entity, stack);
}
/**
* Add items to an existing creative tab.
*
* @param key The {@link ResourceKey} for this creative tab.
* @param context Additional parameters used for building the contents.
* @param out The creative tab output to append items to.
*/
public static void onBuildCreativeTab(ResourceKey<CreativeModeTab> key, CreativeModeTab.ItemDisplayParameters context, CreativeModeTab.Output out) {
if (key == CreativeModeTabs.OP_BLOCKS && context.hasPermissions()) {
out.accept(ModRegistry.Items.COMPUTER_COMMAND.get());
}
}
}

View File

@@ -34,6 +34,7 @@ import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.computer.items.CommandComputerItem;
import dan200.computercraft.shared.computer.items.ComputerItem;
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
import dan200.computercraft.shared.data.ConstantLootConditionSerializer;
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
@@ -139,19 +140,17 @@ public final class ModRegistry {
}
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_NORMAL = REGISTRY.register("computer_normal",
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), ComputerFamily.NORMAL, BlockEntities.COMPUTER_NORMAL));
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), BlockEntities.COMPUTER_NORMAL));
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.COMPUTER_ADVANCED));
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), BlockEntities.COMPUTER_ADVANCED));
public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command", () -> new CommandComputerBlock<>(
computerProperties().strength(-1, 6000000.0F),
ComputerFamily.COMMAND, BlockEntities.COMPUTER_COMMAND
));
public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command",
() -> new CommandComputerBlock<>(computerProperties().strength(-1, 6000000.0F), BlockEntities.COMPUTER_COMMAND));
public static final RegistryEntry<TurtleBlock> TURTLE_NORMAL = REGISTRY.register("turtle_normal",
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.STONE), ComputerFamily.NORMAL, BlockEntities.TURTLE_NORMAL));
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.STONE), BlockEntities.TURTLE_NORMAL));
public static final RegistryEntry<TurtleBlock> TURTLE_ADVANCED = REGISTRY.register("turtle_advanced",
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.TURTLE_ADVANCED));
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.GOLD).explosionResistance(TurtleBlock.IMMUNE_EXPLOSION_RESISTANCE), BlockEntities.TURTLE_ADVANCED));
public static final RegistryEntry<SpeakerBlock> SPEAKER = REGISTRY.register("speaker", () -> new SpeakerBlock(properties().mapColor(MapColor.STONE)));
public static final RegistryEntry<DiskDriveBlock> DISK_DRIVE = REGISTRY.register("disk_drive", () -> new DiskDriveBlock(properties().mapColor(MapColor.STONE)));
@@ -192,9 +191,9 @@ public final class ModRegistry {
ofBlock(Blocks.COMPUTER_COMMAND, (p, s) -> new CommandComputerBlockEntity(BlockEntities.COMPUTER_COMMAND.get(), p, s));
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_NORMAL =
ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, ComputerFamily.NORMAL));
ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, () -> Config.turtleFuelLimit, ComputerFamily.NORMAL));
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_ADVANCED =
ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, ComputerFamily.ADVANCED));
ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, () -> Config.advancedTurtleFuelLimit, ComputerFamily.ADVANCED));
public static final RegistryEntry<BlockEntityType<SpeakerBlockEntity>> SPEAKER =
ofBlock(Blocks.SPEAKER, (p, s) -> new SpeakerBlockEntity(BlockEntities.SPEAKER.get(), p, s));
@@ -311,7 +310,10 @@ public final class ModRegistry {
() -> new MenuType<>(PrinterMenu::new, FeatureFlags.VANILLA_SET));
public static final RegistryEntry<MenuType<HeldItemMenu>> PRINTOUT = REGISTRY.register("printout",
() -> ContainerData.toType(HeldItemContainerData::new, HeldItemMenu::createPrintout));
() -> ContainerData.toType(
HeldItemContainerData::new,
(id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.getHand())
));
public static final RegistryEntry<MenuType<ViewComputerMenu>> VIEW_COMPUTER = REGISTRY.register("view_computer",
() -> ContainerData.toType(ComputerContainerData::new, ViewComputerMenu::new));
@@ -371,11 +373,11 @@ public final class ModRegistry {
public static final RegistryEntry<SimpleCraftingRecipeSerializer<ClearColourRecipe>> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new);
public static final RegistryEntry<RecipeSerializer<TurtleRecipe>> TURTLE = REGISTRY.register("turtle", () -> TurtleRecipe.validatingSerialiser(TurtleRecipe::of));
public static final RegistryEntry<SimpleCraftingRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serialiser::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PocketComputerUpgradeRecipe>> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::new);
public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", () -> CustomShapedRecipe.validatingSerialiser(ComputerUpgradeRecipe::of));
}
public static class Permissions {
@@ -464,8 +466,8 @@ public final class ModRegistry {
* Register any objects which must be done on the main thread.
*/
public static void registerMainThread() {
CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
CauldronInteraction.WATER.put(Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
CauldronInteraction.WATER.put(Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
}
private static void addTurtle(CreativeModeTab.Output out, TurtleItem turtle) {

View File

@@ -54,7 +54,12 @@ import static net.minecraft.commands.Commands.literal;
public final class CommandComputerCraft {
public static final UUID SYSTEM_UUID = new UUID(0, 0);
public static final String OPEN_COMPUTER = "computercraft open-computer ";
/**
* The client-side command to open the folder. Ideally this would live under the main {@code computercraft}
* namespace, but unfortunately that overrides commands, rather than merging them.
*/
public static final String CLIENT_OPEN_FOLDER = "computercraft-computer-folder";
private CommandComputerCraft() {
}
@@ -389,7 +394,7 @@ public final class CommandComputerCraft {
return link(
text("\u270E"),
"/" + OPEN_COMPUTER + id,
"/" + CLIENT_OPEN_FOLDER + " " + id,
Component.translatable("commands.computercraft.dump.open_path")
);
}

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.shared.command.text;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.command.CommandUtils;
import dan200.computercraft.shared.network.client.ChatTableClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
@@ -105,7 +105,7 @@ public class TableBuilder {
if (CommandUtils.isPlayer(source)) {
trim(18);
var player = (ServerPlayer) Nullability.assertNonNull(source.getEntity());
PlatformHelper.get().sendToPlayer(new ChatTableClientMessage(this), player);
ServerNetworking.sendToPlayer(new ChatTableClientMessage(this), player);
} else {
trim(100);
new ServerTableFormatter(source).display(this);

View File

@@ -4,8 +4,6 @@
package dan200.computercraft.shared.common;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.network.container.HeldItemContainerData;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.MenuProvider;
@@ -28,10 +26,6 @@ public class HeldItemMenu extends AbstractContainerMenu {
stack = player.getItemInHand(hand).copy();
}
public static HeldItemMenu createPrintout(int id, Inventory inventory, HeldItemContainerData data) {
return new HeldItemMenu(ModRegistry.Menus.PRINTOUT.get(), id, inventory.player, data.getHand());
}
public ItemStack getStack() {
return stack;
}

View File

@@ -7,7 +7,6 @@ package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.util.BlockEntityHelpers;
@@ -42,13 +41,11 @@ import javax.annotation.Nullable;
public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntity> extends HorizontalDirectionalBlock implements IBundledRedstoneBlock, EntityBlock {
private static final ResourceLocation DROP = new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer");
private final ComputerFamily family;
protected final RegistryEntry<BlockEntityType<T>> type;
private final BlockEntityTicker<T> serverTicker = (level, pos, state, computer) -> computer.serverTick();
protected AbstractComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
protected AbstractComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings);
this.family = family;
this.type = type;
}
@@ -82,10 +79,6 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
protected abstract ItemStack getItem(AbstractComputerBlockEntity tile);
public ComputerFamily getFamily() {
return family;
}
@Override
@Deprecated
public int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction incomingSide) {

View File

@@ -25,7 +25,6 @@ import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -51,7 +50,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
private boolean fresh = false;
private int invalidSides = 0;
private final ComponentAccess<IPeripheral> peripherals = PlatformHelper.get().createPeripheralAccess(d -> invalidSides |= 1 << d.ordinal());
private final ComponentAccess<IPeripheral> peripherals = PlatformHelper.get().createPeripheralAccess(this, d -> invalidSides |= 1 << d.ordinal());
private LockCode lockCode = LockCode.NO_LOCK;
@@ -114,16 +113,13 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return InteractionResult.PASS;
}
public void neighborChanged(BlockPos neighbour) {
updateInputAt(neighbour);
}
protected void serverTick() {
if (getLevel().isClientSide) return;
if (computerID < 0 && !startOn) return; // Don't tick if we don't need a computer!
var computer = createServerComputer();
// Update any peripherals that have changed.
if (invalidSides != 0) {
for (var direction : DirectionUtil.FACINGS) {
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(computer, direction);
@@ -140,16 +136,30 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
fresh = false;
computerID = computer.getID();
label = computer.getLabel();
on = computer.isOn();
// Update the block state if needed. We don't fire a block update intentionally,
// as this only really is needed on the client side.
// If the on state has changed, mark as as dirty.
var newOn = computer.isOn();
if (on != newOn) {
on = newOn;
setChanged();
}
// If the label has changed, mark as dirty and sync to client.
var newLabel = computer.getLabel();
if (!Objects.equals(label, newLabel)) {
label = newLabel;
BlockEntityHelpers.updateBlock(this);
}
// Update the block state if needed.
updateBlockState(computer.getState());
// TODO: This should ideally be split up into label/id/on (which should save NBT and sync to client) and
// redstone (which should update outputs)
if (computer.hasOutputChanged()) updateOutput();
var changes = computer.pollAndResetChanges();
if (changes != 0) {
for (var direction : DirectionUtil.FACINGS) {
if ((changes & (1 << remapToLocalSide(direction).ordinal())) != 0) updateRedstoneTo(direction);
}
}
}
protected abstract void updateBlockState(ComputerState newState);
@@ -199,11 +209,15 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return localSide;
}
private void updateRedstoneInputs(ServerComputer computer) {
var pos = getBlockPos();
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
}
/**
* Update the redstone input on a particular side.
* <p>
* This is called <em>immediately</em> when a neighbouring block changes (see {@link #neighborChanged(BlockPos)}).
*
* @param computer The current server computer.
* @param dir The direction to update in.
* @param targetPos The position of the adjacent block, equal to {@code getBlockPos().offset(dir)}.
*/
private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
var offsetSide = dir.getOpposite();
var localDir = remapToLocalSide(dir);
@@ -212,13 +226,22 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
}
/**
* Update the peripheral on a particular side.
* <p>
* This is called from {@link #serverTick()}, after a peripheral has been marked as invalid (such as in
* {@link #neighborChanged(BlockPos)})
*
* @param computer The current server computer.
* @param dir The direction to update in.
*/
private void refreshPeripheral(ServerComputer computer, Direction dir) {
invalidSides &= ~(1 << dir.ordinal());
var localDir = remapToLocalSide(dir);
if (isPeripheralBlockedOnSide(localDir)) return;
var peripheral = peripherals.get((ServerLevel) getLevel(), getBlockPos(), dir);
var peripheral = peripherals.get(dir);
computer.setPeripheral(localDir, peripheral);
}
@@ -244,7 +267,18 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
}
}
private void updateInputAt(BlockPos neighbour) {
/**
* Called when a neighbour block changes.
* <p>
* This finds the side the neighbour block is on, and updates the inputs accordingly.
* <p>
* We do <strong>NOT</strong> update the peripheral immediately. Blocks and block entities are sometimes
* inconsistent at the point where an update is received, and so we instead just mark that side as dirty (see
* {@link #invalidSides}) and refresh it {@linkplain #serverTick() next tick}.
*
* @param neighbour The position of the neighbour block.
*/
public void neighborChanged(BlockPos neighbour) {
var computer = getServerComputer();
if (computer == null) return;
@@ -259,22 +293,28 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
// If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
// handle this incorrectly.
updateRedstoneInputs(computer);
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
}
/**
* Update the block's state and propagate redstone output.
* Update outputs in a specific direction.
*
* @param direction The direction to propagate outputs in.
*/
public void updateOutput() {
BlockEntityHelpers.updateBlock(this);
for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
protected void updateRedstoneTo(Direction direction) {
RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), direction);
var computer = getServerComputer();
if (computer != null) updateRedstoneInputs(computer);
if (computer != null) updateRedstoneInput(computer, direction, getBlockPos().relative(direction));
}
protected abstract ServerComputer createComputer(int id);
/**
* Update all redstone outputs.
*/
public void updateRedstone() {
for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir);
}
@Override
public final int getComputerID() {
@@ -332,6 +372,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return computer;
}
protected abstract ServerComputer createComputer(int id);
@Nullable
public ServerComputer getServerComputer() {
return getLevel().isClientSide || getLevel().getServer() == null ? null : ServerContext.get(getLevel().getServer()).registry().get(instanceID);

View File

@@ -4,7 +4,6 @@
package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.platform.RegistryEntry;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -17,7 +16,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
* @see dan200.computercraft.shared.computer.items.CommandComputerItem
*/
public class CommandComputerBlock<T extends CommandComputerBlockEntity> extends ComputerBlock<T> implements GameMasterBlock {
public CommandComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
super(settings, family, type);
public CommandComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings, type);
}
}

View File

@@ -20,28 +20,24 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
public class CommandComputerBlockEntity extends ComputerBlockEntity {
public class CommandReceiver implements CommandSource {
private final Map<Integer, String> output = new HashMap<>();
private final List<String> output = new ArrayList<>();
public void clearOutput() {
output.clear();
}
public Map<Integer, String> getOutput() {
return output;
}
public Map<Integer, String> copyOutput() {
return new HashMap<>(output);
public List<String> copyOutput() {
return new ArrayList<>(output);
}
@Override
public void sendSystemMessage(Component textComponent) {
output.put(output.size() + 1, textComponent.getString());
output.add(textComponent.getString());
}
@Override

View File

@@ -4,9 +4,8 @@
package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.items.ComputerItemFactory;
import dan200.computercraft.shared.computer.items.ComputerItem;
import dan200.computercraft.shared.platform.RegistryEntry;
import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack;
@@ -25,8 +24,8 @@ public class ComputerBlock<T extends ComputerBlockEntity> extends AbstractComput
public static final EnumProperty<ComputerState> STATE = EnumProperty.create("state", ComputerState.class);
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public ComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
super(settings, family, type);
public ComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings, type);
registerDefaultState(defaultBlockState()
.setValue(FACING, Direction.NORTH)
.setValue(STATE, ComputerState.OFF)
@@ -46,6 +45,9 @@ public class ComputerBlock<T extends ComputerBlockEntity> extends AbstractComput
@Override
protected ItemStack getItem(AbstractComputerBlockEntity tile) {
return tile instanceof ComputerBlockEntity ? ComputerItemFactory.create((ComputerBlockEntity) tile) : ItemStack.EMPTY;
if (!(tile instanceof ComputerBlockEntity computer)) return ItemStack.EMPTY;
if (!(asItem() instanceof ComputerItem item)) return ItemStack.EMPTY;
return item.create(computer.getComputerID(), computer.getLabel());
}
}

View File

@@ -32,11 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Override
protected ServerComputer createComputer(int id) {
var family = getFamily();
return new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
family, Config.computerTermWidth,
Config.computerTermHeight
getFamily(), Config.computerTermWidth, Config.computerTermHeight
);
}
@@ -53,7 +51,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
protected void updateBlockState(ComputerState newState) {
var existing = getBlockState();
if (existing.getValue(ComputerBlock.STATE) != newState) {
getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), 3);
getLevel().setBlock(getBlockPos(), existing.setValue(ComputerBlock.STATE, newState), ComputerBlock.UPDATE_CLIENTS);
}
}

View File

@@ -4,33 +4,8 @@
package dan200.computercraft.shared.computer.core;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.StringRepresentable;
public enum ComputerFamily implements StringRepresentable {
NORMAL("normal"),
ADVANCED("advanced"),
COMMAND("command");
private final String name;
ComputerFamily(String name) {
this.name = name;
}
public static ComputerFamily getFamily(JsonObject json, String name) {
var familyName = GsonHelper.getAsString(json, name);
for (var family : values()) {
if (family.getSerializedName().equalsIgnoreCase(familyName)) return family;
}
throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
}
@Override
public String getSerializedName() {
return name;
}
public enum ComputerFamily {
NORMAL,
ADVANCED,
COMMAND,
}

View File

@@ -21,7 +21,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
import static dan200.computercraft.api.filesystem.MountConstants.NO_SUCH_FILE;
/**
* A mount backed by Minecraft's {@link ResourceManager}.

View File

@@ -20,7 +20,7 @@ import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -42,7 +42,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private final NetworkedTerminal terminal;
private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
private boolean changedLastFrame;
private int ticksSincePing;
public ServerComputer(
@@ -96,10 +95,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
public void tickServer() {
ticksSincePing++;
computer.tick();
changedLastFrame = computer.pollAndResetChanged();
if (terminalChanged.getAndSet(false)) onTerminalChanged();
}
@@ -119,8 +115,8 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return ticksSincePing > 100;
}
public boolean hasOutputChanged() {
return changedLastFrame;
public int pollAndResetChanges() {
return computer.pollAndResetChanges();
}
public int register() {
@@ -142,7 +138,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
for (var player : server.getPlayerList().getPlayers()) {
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
PlatformHelper.get().sendToPlayer(createPacket.apply(player.containerMenu), player);
ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
}
}
}
@@ -167,7 +163,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
}
public ComputerState getState() {
if (!isOn()) return ComputerState.OFF;
if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
}

View File

@@ -8,7 +8,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.config.Config;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
@@ -22,11 +21,8 @@ import javax.annotation.Nullable;
import java.util.List;
public abstract class AbstractComputerItem extends BlockItem implements IComputerItem, IMedia {
private final ComputerFamily family;
public AbstractComputerItem(AbstractComputerBlock<?> block, Properties settings) {
super(block, settings);
family = block.getFamily();
}
@Override
@@ -45,13 +41,6 @@ public abstract class AbstractComputerItem extends BlockItem implements ICompute
return IComputerItem.super.getLabel(stack);
}
@Override
public final ComputerFamily getFamily() {
return family;
}
// IMedia implementation
@Override
public boolean setLabel(ItemStack stack, @Nullable String label) {
if (label != null) {

View File

@@ -5,8 +5,8 @@
package dan200.computercraft.shared.computer.items;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
@@ -24,9 +24,9 @@ public class ComputerItem extends AbstractComputerItem {
}
@Override
public ItemStack withFamily(ItemStack stack, ComputerFamily family) {
var result = ComputerItemFactory.create(getComputerID(stack), null, family);
if (stack.hasCustomHoverName()) result.setHoverName(stack.getHoverName());
return result;
public ItemStack changeItem(ItemStack stack, Item newItem) {
return newItem instanceof ComputerItem computer
? computer.create(getComputerID(stack), getLabel(stack))
: ItemStack.EMPTY;
}
}

View File

@@ -1,29 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.computer.items;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
public final class ComputerItemFactory {
private ComputerItemFactory() {
}
public static ItemStack create(ComputerBlockEntity tile) {
return create(tile.getComputerID(), tile.getLabel(), tile.getFamily());
}
public static ItemStack create(int id, @Nullable String label, ComputerFamily family) {
return switch (family) {
case NORMAL -> ModRegistry.Items.COMPUTER_NORMAL.get().create(id, label);
case ADVANCED -> ModRegistry.Items.COMPUTER_ADVANCED.get().create(id, label);
case COMMAND -> ModRegistry.Items.COMPUTER_COMMAND.get().create(id, label);
};
}
}

View File

@@ -4,7 +4,7 @@
package dan200.computercraft.shared.computer.items;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
@@ -21,7 +21,15 @@ public interface IComputerItem {
return stack.hasCustomHoverName() ? stack.getHoverName().getString() : null;
}
ComputerFamily getFamily();
ItemStack withFamily(ItemStack stack, ComputerFamily family);
/**
* Create a new stack, changing the underlying item.
* <p>
* This should copy the computer's data to a different item of the same type (for instance, converting a normal
* computer to an advanced one).
*
* @param stack The current computer stack.
* @param newItem The new item.
* @return The new stack, possibly {@linkplain ItemStack#EMPTY empty} if {@code newItem} is of the same type.
*/
ItemStack changeItem(ItemStack stack, Item newItem);
}

View File

@@ -11,7 +11,7 @@ import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.UploadResultMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.chat.Component;
@@ -138,7 +138,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
return;
}
PlatformHelper.get().sendToPlayer(finishUpload(uploader), uploader);
ServerNetworking.sendToPlayer(finishUpload(uploader), uploader);
}
private UploadResultMessage finishUpload(ServerPlayer player) {
@@ -159,7 +159,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
toUpload.stream().map(x -> new TransferredFile(x.getName(), new ByteBufferChannel(x.getBytes()))).toList(),
() -> {
if (player.isAlive() && player.containerMenu == owner) {
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
ServerNetworking.sendToPlayer(UploadResultMessage.consumed(owner), player);
}
}),
});

View File

@@ -46,12 +46,14 @@ public final class GlobalMetrics {
* Add a new global metrics observer. This will receive metrics data for all computers.
*
* @param tracker The observer to add.
* @return Whether the observer was added. {@code false} if the observer was already registered.
*/
public void addObserver(ComputerMetricsObserver tracker) {
public boolean addObserver(ComputerMetricsObserver tracker) {
synchronized (lock) {
if (trackers.contains(tracker)) return;
if (trackers.contains(tracker)) return false;
trackers.add(tracker);
enabled = true;
return true;
}
}
@@ -59,11 +61,13 @@ public final class GlobalMetrics {
* Remove a previously-registered global metrics observer.
*
* @param tracker The observer to add.
* @return Whether the observer was removed. {@code false} if the observer was not registered.
*/
public void removeObserver(ComputerMetricsObserver tracker) {
public boolean removeObserver(ComputerMetricsObserver tracker) {
synchronized (lock) {
trackers.remove(tracker);
var changed = trackers.remove(tracker);
enabled = !trackers.isEmpty();
return changed;
}
}

View File

@@ -10,6 +10,7 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.metrics.ComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import javax.annotation.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -21,29 +22,31 @@ import java.util.Map;
*/
public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
private final GlobalMetrics owner;
private boolean tracking = false;
@GuardedBy("this")
private final List<ComputerMetrics> timings = new ArrayList<>();
@GuardedBy("this")
private final Map<ServerComputer, ComputerMetrics> timingLookup = new MapMaker().weakKeys().makeMap();
public BasicComputerMetricsObserver(GlobalMetrics owner) {
this.owner = owner;
}
public synchronized void start() {
if (!tracking) owner.addObserver(this);
tracking = true;
public void start() {
if (!owner.addObserver(this)) return;
timings.clear();
timingLookup.clear();
synchronized (this) {
timings.clear();
timingLookup.clear();
}
}
public synchronized boolean stop() {
if (!tracking) return false;
owner.removeObserver(this);
tracking = false;
timingLookup.clear();
public boolean stop() {
if (!owner.removeObserver(this)) return false;
synchronized (this) {
timingLookup.clear();
}
return true;
}
@@ -57,6 +60,7 @@ public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
return new ArrayList<>(timings);
}
@GuardedBy("this")
private ComputerMetrics getMetrics(ServerComputer computer) {
var existing = timingLookup.get(computer);
if (existing != null) return existing;

View File

@@ -4,57 +4,44 @@
package dan200.computercraft.shared.computer.recipe;
import com.google.gson.JsonObject;
import com.mojang.serialization.DataResult;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
/**
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
* family} to another.
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting to it a new item (for instance a normal
* turtle to an advanced one).
*
* @see IComputerItem#changeItem(ItemStack, Item)
*/
public final class ComputerUpgradeRecipe extends ComputerConvertRecipe {
private final ComputerFamily family;
private final Item result;
private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe, ComputerFamily family) {
private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
super(identifier, recipe);
this.family = family;
this.result = recipe.result().getItem();
}
public static DataResult<ComputerUpgradeRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
if (!(recipe.result().getItem() instanceof IComputerItem)) {
return DataResult.error(() -> recipe.result().getItem() + " is not a computer item");
}
return DataResult.success(new ComputerUpgradeRecipe(id, recipe));
}
@Override
protected ItemStack convert(IComputerItem item, ItemStack stack) {
return item.withFamily(stack, family);
return item.changeItem(stack, result);
}
@Override
public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
}
public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
@Override
public ComputerUpgradeRecipe fromJson(ResourceLocation identifier, JsonObject json) {
var recipe = ShapedRecipeSpec.fromJson(json);
var family = ComputerFamily.getFamily(json, "family");
return new ComputerUpgradeRecipe(identifier, recipe, family);
}
@Override
public ComputerUpgradeRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
var recipe = ShapedRecipeSpec.fromNetwork(buf);
var family = buf.readEnum(ComputerFamily.class);
return new ComputerUpgradeRecipe(identifier, recipe, family);
}
@Override
public void toNetwork(FriendlyByteBuf buf, ComputerUpgradeRecipe recipe) {
recipe.toSpec().toNetwork(buf);
buf.writeEnum(recipe.family);
}
}
}

View File

@@ -224,7 +224,7 @@ public final class ConfigSpec {
.defineInRange("max_requests", CoreConfig.httpMaxRequests, 0, Integer.MAX_VALUE);
httpMaxWebsockets = builder
.comment("The number of websockets a computer can have open at one time. Set to 0 for unlimited.")
.comment("The number of websockets a computer can have open at one time.")
.defineInRange("max_websockets", CoreConfig.httpMaxWebsockets, 1, Integer.MAX_VALUE);
builder

View File

@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network;
/**
* A type of message to send over the network.
* <p>
* Much like recipe or argument serialisers, each type of {@link NetworkMessage} should have a unique type associated
* with it. This holds platform-specific information about how the packet should be sent over the network.
*
* @param <T> The type of message to send
* @see NetworkMessages
* @see NetworkMessage#type()
*/
public interface MessageType<T extends NetworkMessage<?>> {
}

View File

@@ -17,6 +17,13 @@ import net.minecraft.network.FriendlyByteBuf;
* @see ServerNetworkContext
*/
public interface NetworkMessage<T> {
/**
* Get the type of this message.
*
* @return The type of this message.
*/
MessageType<?> type();
/**
* Write this packet to a buffer.
* <p>
@@ -24,7 +31,7 @@ public interface NetworkMessage<T> {
*
* @param buf The buffer to write data to.
*/
void toBytes(FriendlyByteBuf buf);
void write(FriendlyByteBuf buf);
/**
* Handle this {@link NetworkMessage}.

View File

@@ -4,48 +4,84 @@
package dan200.computercraft.shared.network;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.network.client.*;
import dan200.computercraft.shared.network.server.*;
import dan200.computercraft.shared.platform.PlatformHelper;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import java.util.function.Function;
import java.util.*;
/**
* Registry for all packets provided by CC: Tweaked.
* List of all {@link MessageType}s provided by CC: Tweaked.
*
* @see PlatformHelper The platform helper is used to send packets.
*/
public final class NetworkMessages {
private static final IntSet seenIds = new IntOpenHashSet();
private static final Set<String> seenChannel = new HashSet<>();
private static final List<MessageType<? extends NetworkMessage<ServerNetworkContext>>> serverMessages = new ArrayList<>();
private static final List<MessageType<? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
public static final MessageType<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound(0, "computer_action", ComputerActionServerMessage.class, ComputerActionServerMessage::new);
public static final MessageType<QueueEventServerMessage> QUEUE_EVENT = registerServerbound(1, "queue_event", QueueEventServerMessage.class, QueueEventServerMessage::new);
public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(2, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new);
public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(3, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new);
public static final MessageType<UploadFileMessage> UPLOAD_FILE = registerServerbound(4, "upload_file", UploadFileMessage.class, UploadFileMessage::new);
public static final MessageType<ChatTableClientMessage> CHAT_TABLE = registerClientbound(10, "chat_table", ChatTableClientMessage.class, ChatTableClientMessage::new);
public static final MessageType<PocketComputerDataMessage> POCKET_COMPUTER_DATA = registerClientbound(11, "pocket_computer_data", PocketComputerDataMessage.class, PocketComputerDataMessage::new);
public static final MessageType<PocketComputerDeletedClientMessage> POCKET_COMPUTER_DELETED = registerClientbound(12, "pocket_computer_deleted", PocketComputerDeletedClientMessage.class, PocketComputerDeletedClientMessage::new);
public static final MessageType<ComputerTerminalClientMessage> COMPUTER_TERMINAL = registerClientbound(13, "computer_terminal", ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new);
public static final MessageType<PlayRecordClientMessage> PLAY_RECORD = registerClientbound(14, "play_record", PlayRecordClientMessage.class, PlayRecordClientMessage::new);
public static final MessageType<MonitorClientMessage> MONITOR_CLIENT = registerClientbound(15, "monitor_client", MonitorClientMessage.class, MonitorClientMessage::new);
public static final MessageType<SpeakerAudioClientMessage> SPEAKER_AUDIO = registerClientbound(16, "speaker_audio", SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new);
public static final MessageType<SpeakerMoveClientMessage> SPEAKER_MOVE = registerClientbound(17, "speaker_move", SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new);
public static final MessageType<SpeakerPlayClientMessage> SPEAKER_PLAY = registerClientbound(18, "speaker_play", SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new);
public static final MessageType<SpeakerStopClientMessage> SPEAKER_STOP = registerClientbound(19, "speaker_stop", SpeakerStopClientMessage.class, SpeakerStopClientMessage::new);
public static final MessageType<UploadResultMessage> UPLOAD_RESULT = registerClientbound(20, "upload_result", UploadResultMessage.class, UploadResultMessage::new);
public static final MessageType<UpgradesLoadedMessage> UPGRADES_LOADED = registerClientbound(21, "upgrades_loaded", UpgradesLoadedMessage.class, UpgradesLoadedMessage::new);
private NetworkMessages() {
}
public interface PacketRegistry {
<T extends NetworkMessage<ClientNetworkContext>> void registerClientbound(int id, Class<T> type, Function<FriendlyByteBuf, T> decoder);
<T extends NetworkMessage<ServerNetworkContext>> void registerServerbound(int id, Class<T> type, Function<FriendlyByteBuf, T> decoder);
private static <C, T extends NetworkMessage<C>> MessageType<T> register(
List<MessageType<? extends NetworkMessage<C>>> messages,
int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader
) {
if (!seenIds.add(id)) throw new IllegalArgumentException("Duplicate id " + id);
if (!seenChannel.add(channel)) throw new IllegalArgumentException("Duplicate channel " + channel);
var type = PlatformHelper.get().createMessageType(id, new ResourceLocation(ComputerCraftAPI.MOD_ID, channel), klass, reader);
messages.add(type);
return type;
}
public static void register(PacketRegistry registry) {
// Server messages
registry.registerServerbound(0, ComputerActionServerMessage.class, ComputerActionServerMessage::new);
registry.registerServerbound(1, QueueEventServerMessage.class, QueueEventServerMessage::new);
registry.registerServerbound(2, KeyEventServerMessage.class, KeyEventServerMessage::new);
registry.registerServerbound(3, MouseEventServerMessage.class, MouseEventServerMessage::new);
registry.registerServerbound(4, UploadFileMessage.class, UploadFileMessage::new);
private static <T extends NetworkMessage<ServerNetworkContext>> MessageType<T> registerServerbound(int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
return register(serverMessages, id, channel, klass, reader);
}
// Client messages
registry.registerClientbound(10, ChatTableClientMessage.class, ChatTableClientMessage::new);
registry.registerClientbound(11, PocketComputerDataMessage.class, PocketComputerDataMessage::new);
registry.registerClientbound(12, PocketComputerDeletedClientMessage.class, PocketComputerDeletedClientMessage::new);
registry.registerClientbound(13, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new);
registry.registerClientbound(14, PlayRecordClientMessage.class, PlayRecordClientMessage::new);
registry.registerClientbound(15, MonitorClientMessage.class, MonitorClientMessage::new);
registry.registerClientbound(16, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new);
registry.registerClientbound(17, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new);
registry.registerClientbound(18, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new);
registry.registerClientbound(19, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new);
registry.registerClientbound(20, UploadResultMessage.class, UploadResultMessage::new);
registry.registerClientbound(21, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new);
private static <T extends NetworkMessage<ClientNetworkContext>> MessageType<T> registerClientbound(int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
return register(clientMessages, id, channel, klass, reader);
}
/**
* Get all serverbound message types.
*
* @return An unmodifiable sequence of all serverbound message types.
*/
public static Collection<MessageType<? extends NetworkMessage<ServerNetworkContext>>> getServerbound() {
return Collections.unmodifiableCollection(serverMessages);
}
/**
* Get all clientbound message types.
*
* @return An unmodifiable sequence of all clientbound message types.
*/
public static Collection<MessageType<? extends NetworkMessage<ClientNetworkContext>>> getClientbound() {
return Collections.unmodifiableCollection(clientMessages);
}
}

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
@@ -43,7 +45,7 @@ public class ChatTableClientMessage implements NetworkMessage<ClientNetworkConte
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUtf(table.getId(), MAX_LEN);
buf.writeVarInt(table.getColumns());
buf.writeBoolean(table.getHeaders() != null);
@@ -63,4 +65,9 @@ public class ChatTableClientMessage implements NetworkMessage<ClientNetworkConte
public void handle(ClientNetworkContext context) {
context.handleChatTable(table);
}
@Override
public MessageType<ChatTableClientMessage> type() {
return NetworkMessages.CHAT_TABLE;
}
}

View File

@@ -4,30 +4,24 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* The context under which clientbound packets are evaluated.
*/
public interface ClientNetworkContext {
static ClientNetworkContext get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ClientNetworkContext.class, Instance.ERROR) : instance;
}
void handleChatTable(TableBuilder table);
void handleComputerTerminal(int containerId, TerminalState terminal);
@@ -40,9 +34,7 @@ public interface ClientNetworkContext {
void handlePocketComputerDeleted(int instanceId);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume);
void handleSpeakerAudioPush(UUID source, ByteBuf buffer);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
@@ -51,18 +43,4 @@ public interface ClientNetworkContext {
void handleSpeakerStop(UUID source);
void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage);
final class Instance {
static final @Nullable ClientNetworkContext INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(ClientNetworkContext.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
}

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -25,7 +27,7 @@ public class ComputerTerminalClientMessage implements NetworkMessage<ClientNetwo
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(containerId);
terminal.write(buf);
}
@@ -34,4 +36,9 @@ public class ComputerTerminalClientMessage implements NetworkMessage<ClientNetwo
public void handle(ClientNetworkContext context) {
context.handleComputerTerminal(containerId, terminal);
}
@Override
public MessageType<ComputerTerminalClientMessage> type() {
return NetworkMessages.COMPUTER_TERMINAL;
}
}

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
@@ -25,7 +27,7 @@ public class MonitorClientMessage implements NetworkMessage<ClientNetworkContext
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeBlockPos(pos);
state.write(buf);
}
@@ -34,4 +36,9 @@ public class MonitorClientMessage implements NetworkMessage<ClientNetworkContext
public void handle(ClientNetworkContext context) {
context.handleMonitorData(pos, state);
}
@Override
public MessageType<MonitorClientMessage> type() {
return NetworkMessages.MONITOR_CLIENT;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
@@ -43,7 +45,7 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeBlockPos(pos);
buf.writeNullable(soundEvent, (b, e) -> e.writeToNetwork(b));
buf.writeNullable(name, FriendlyByteBuf::writeUtf);
@@ -53,4 +55,9 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
public void handle(ClientNetworkContext context) {
context.handlePlayRecord(pos, soundEvent, name);
}
@Override
public MessageType<PlayRecordClientMessage> type() {
return NetworkMessages.PLAY_RECORD;
}
}

View File

@@ -7,7 +7,9 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf;
@@ -35,7 +37,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId);
buf.writeEnum(state);
buf.writeVarInt(lightState);
@@ -46,4 +48,9 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
public void handle(ClientNetworkContext context) {
context.handlePocketComputerData(instanceId, state, lightState, terminal);
}
@Override
public MessageType<PocketComputerDataMessage> type() {
return NetworkMessages.POCKET_COMPUTER_DATA;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
@@ -20,7 +22,7 @@ public class PocketComputerDeletedClientMessage implements NetworkMessage<Client
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId);
}
@@ -28,4 +30,9 @@ public class PocketComputerDeletedClientMessage implements NetworkMessage<Client
public void handle(ClientNetworkContext context) {
context.handlePocketComputerDeleted(instanceId);
}
@Override
public MessageType<PocketComputerDeletedClientMessage> type() {
return NetworkMessages.POCKET_COMPUTER_DELETED;
}
}

View File

@@ -4,17 +4,16 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
/**
* Starts a sound on the client.
* <p>
@@ -25,7 +24,7 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
private final UUID source;
private final SpeakerPosition.Message pos;
private final @Nullable ByteBuffer content;
private final ByteBuffer content;
private final float volume;
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
@@ -40,22 +39,26 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
pos = SpeakerPosition.Message.read(buf);
volume = buf.readFloat();
// TODO: Remove this, so we no longer need a getter for ClientNetworkContext. However, doing so without
// leaking or redundantly copying the buffer is hard.
ClientNetworkContext.get().handleSpeakerAudioPush(source, buf);
content = null;
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
content = ByteBuffer.wrap(bytes);
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
buf.writeFloat(volume);
buf.writeBytes(assertNonNull(content).duplicate());
buf.writeBytes(content.duplicate());
}
@Override
public void handle(ClientNetworkContext context) {
context.handleSpeakerAudio(source, pos, volume);
context.handleSpeakerAudio(source, pos, volume, content);
}
@Override
public MessageType<SpeakerAudioClientMessage> type() {
return NetworkMessages.SPEAKER_AUDIO;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
@@ -33,7 +35,7 @@ public class SpeakerMoveClientMessage implements NetworkMessage<ClientNetworkCon
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
}
@@ -42,4 +44,9 @@ public class SpeakerMoveClientMessage implements NetworkMessage<ClientNetworkCon
public void handle(ClientNetworkContext context) {
context.handleSpeakerMove(source, pos);
}
@Override
public MessageType<SpeakerMoveClientMessage> type() {
return NetworkMessages.SPEAKER_MOVE;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
@@ -43,7 +45,7 @@ public class SpeakerPlayClientMessage implements NetworkMessage<ClientNetworkCon
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
buf.writeResourceLocation(sound);
@@ -55,4 +57,9 @@ public class SpeakerPlayClientMessage implements NetworkMessage<ClientNetworkCon
public void handle(ClientNetworkContext context) {
context.handleSpeakerPlay(source, pos, sound, volume, pitch);
}
@Override
public MessageType<SpeakerPlayClientMessage> type() {
return NetworkMessages.SPEAKER_PLAY;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import net.minecraft.network.FriendlyByteBuf;
@@ -29,7 +31,7 @@ public class SpeakerStopClientMessage implements NetworkMessage<ClientNetworkCon
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
}
@@ -37,4 +39,9 @@ public class SpeakerStopClientMessage implements NetworkMessage<ClientNetworkCon
public void handle(ClientNetworkContext context) {
context.handleSpeakerStop(source);
}
@Override
public MessageType<SpeakerStopClientMessage> type() {
return NetworkMessages.SPEAKER_STOP;
}
}

View File

@@ -13,7 +13,9 @@ import dan200.computercraft.api.upgrades.UpgradeSerialiser;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.core.Registry;
import net.minecraft.network.FriendlyByteBuf;
@@ -27,7 +29,7 @@ import java.util.Objects;
/**
* Syncs turtle and pocket upgrades to the client.
*/
public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContext> {
public final class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContext> {
private final Map<String, UpgradeManager.UpgradeWrapper<TurtleUpgradeSerialiser<?>, ITurtleUpgrade>> turtleUpgrades;
private final Map<String, UpgradeManager.UpgradeWrapper<PocketUpgradeSerialiser<?>, IPocketUpgrade>> pocketUpgrades;
@@ -65,7 +67,7 @@ public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContex
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
toBytes(buf, TurtleUpgradeSerialiser.registryId(), turtleUpgrades);
toBytes(buf, PocketUpgradeSerialiser.registryId(), pocketUpgrades);
}
@@ -95,4 +97,9 @@ public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContex
TurtleUpgrades.instance().loadFromNetwork(turtleUpgrades);
PocketUpgrades.instance().loadFromNetwork(pocketUpgrades);
}
@Override
public MessageType<UpgradesLoadedMessage> type() {
return NetworkMessages.UPGRADES_LOADED;
}
}

View File

@@ -6,7 +6,9 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -43,7 +45,7 @@ public class UploadResultMessage implements NetworkMessage<ClientNetworkContext>
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(containerId);
buf.writeEnum(result);
if (result == UploadResult.ERROR) buf.writeComponent(Nullability.assertNonNull(errorMessage));
@@ -53,4 +55,9 @@ public class UploadResultMessage implements NetworkMessage<ClientNetworkContext>
public void handle(ClientNetworkContext context) {
context.handleUploadResult(containerId, result, errorMessage);
}
@Override
public MessageType<UploadResultMessage> type() {
return NetworkMessages.UPLOAD_RESULT;
}
}

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -23,8 +25,8 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeEnum(action);
}
@@ -37,6 +39,11 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
}
}
@Override
public MessageType<ComputerActionServerMessage> type() {
return NetworkMessages.COMPUTER_ACTION;
}
public enum Action {
TURN_ON,
SHUTDOWN,

View File

@@ -28,7 +28,7 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw
@Override
@OverridingMethodsMustInvokeSuper
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(containerId);
}

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -30,8 +32,8 @@ public class KeyEventServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeByte(type);
buf.writeVarInt(key);
}
@@ -45,4 +47,9 @@ public class KeyEventServerMessage extends ComputerServerMessage {
input.keyDown(key, type == TYPE_REPEAT);
}
}
@Override
public MessageType<KeyEventServerMessage> type() {
return NetworkMessages.KEY_EVENT;
}
}

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -37,8 +39,8 @@ public class MouseEventServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeByte(type);
buf.writeVarInt(arg);
buf.writeVarInt(x);
@@ -55,4 +57,9 @@ public class MouseEventServerMessage extends ComputerServerMessage {
case TYPE_SCROLL -> input.mouseScroll(arg, x, y);
}
}
@Override
public MessageType<MouseEventServerMessage> type() {
return NetworkMessages.MOUSE_EVENT;
}
}

Some files were not shown because too many files have changed in this diff Show More