Skip to content

Conversation

@Tides
Copy link
Member

@Tides Tides commented Aug 30, 2025

Fully implements all inventory click events

Summary by CodeRabbit

  • New Features

    • Quick-move (shift-click) transfers between containers and player inventory.
    • Creative-mode clone (middle-click) fills carried item to max stack.
    • Crafting table now preserves and returns crafting results.
  • Bug Fixes

    • More reliable item throwing, pickup, and pickup-all stack merging/transfers.
    • Improved slot mapping for container clicks and stronger item equality/handling.
  • Tests

    • Added comprehensive inventory interaction tests and test doubles for player/server.
  • Chores

    • Expanded test assembly visibility and added static-analysis/nullability annotations.

@coderabbitai
Copy link

coderabbitai bot commented Aug 30, 2025

Walkthrough

Refactors container slot resolution to a SlotDifference value type and GetSlot API; adds a range indexer and changes Viewers initializer. Implements crafting result caching. Reworks inventory click handling (QuickMove, Clone, Throw, Pickup flows) and updates packet handling to use GetSlot/GetItem. Adds ItemStack equality operators and nullable annotation, test fakes, unit tests, and exposes internals to tests.

Changes

Cohort / File(s) Summary
Container slot API refactor
Obsidian.API/Containers/BaseContainer.cs
Replaced tuple GetDifference(short) with GetSlot(short) returning SlotDifference (public readonly record struct moved to namespace scope); added ItemStack?[] this[Range range] indexer; changed Viewers initializer.
Crafting result storage
Obsidian.API/Containers/CraftingTable.cs
Added private result field and implemented SetResult(ItemStack?) / GetResult() to persist crafting output.
Event args factory
Obsidian.API/Events/ContainerClickEventArgs.cs
Added internal static factory Create(...) to construct and initialize ContainerClickEventArgs.
Item semantics & annotations
Obsidian.API/Inventory/ItemStack.cs, Obsidian.API/Utilities/Extensions.cs
Added ItemStack equality operators == and !=; removed unused using; annotated IsNullOrAir parameter with [NotNullWhen(false)].
Inventory click handling
Obsidian/Events/MainEventHandler.Inventory.cs
Reworked OnInventoryClick into specialized handlers (HandleQuickMove, HandleClone, HandleThrow, TryMoveItem, ThrowItem, SpawnThrownItem, HandlePickupDirectItem); updated Pickup/PickupAll/Throw logic, crafting result usage, and added OnInventoryClickTest.
Networking packet slot mapping
Obsidian/Net/Packets/Play/Serverbound/ContainerClickPacket.cs, Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs
Replaced GetDifferenceGetSlot; map ChangedSlots via GetSlot; select actual container without mutating base variable; switched index access to GetItem/RemoveItem.
Test infrastructure and cases
Obsidian.Tests/Fakes/FakePlayer.cs, Obsidian.Tests/Fakes/FakeServer.cs, Obsidian.Tests/Player.cs
Added FakePlayer and FakeServer test doubles (extensive stub surfaces) and Player tests exercising Pickup, QuickMove, Clone, Throw, and PickupAll via OnInventoryClickTest and ContainerClickEventArgs.Create.
Internals visibility
Obsidian/InternalsVisibleTo.cs
Added assembly: InternalsVisibleTo("Obsidian.Tests").

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Server
  participant Player
  participant BaseContainer
  participant Handler as MainEventHandler
  participant CraftingTable

  Client->>Server: ContainerClickPacket(ClickedSlot, Button, ClickType)
  Server->>BaseContainer: GetSlot(ClickedSlot) => SlotDifference(slot, forPlayer)
  Server->>Handler: OnInventoryClick(args)

  rect rgba(220,235,255,0.35)
  note right of Handler: Dispatch by ClickType
  alt QuickMove
    Handler->>Handler: HandleQuickMove(args)
    Handler->>Player: TryMoveItem(from↔inventory)
  else Clone (Creative)
    Handler->>Handler: HandleClone(args)
    Handler->>Player: Set CarriedItem to full stack
  else Throw
    Handler->>Handler: HandleThrow(args)
    Handler->>Handler: ThrowItem(...)
    opt not test
      Handler->>Server: SpawnThrownItem(player, thrownItem)
    end
  else Pickup / PickupAll
    Handler->>BaseContainer: GetItem(slot)
    Handler->>Player: Merge with CarriedItem (+= / -=)
    Handler->>BaseContainer: SetItem(slot, updated)
  end
  end

  opt Crafting result update
    Handler->>CraftingTable: SetResult(result)
  end

  Handler-->>Server: Apply inventory updates
  Server-->>Client: Sync packets
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I hopped through slots with nimble feet,
Cloned a carrot stack so neat and sweet.
I nudged a byte, I nudged a stack,
Spawned a test and put it back.
— your little rabbit QA 🥕


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a0d6f56 and 95ea697.

📒 Files selected for processing (1)
  • Obsidian/Events/MainEventHandler.Inventory.cs (13 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Obsidian/Events/MainEventHandler.Inventory.cs
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch more-inventory-features

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added api Relates to Obsidian.API tests Relates to Obsidian.Tests labels Aug 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Obsidian/Net/Packets/Play/Serverbound/ContainerClickPacket.cs (1)

71-79: Fix slot mapping and changed-slot validation (off-by-one and wrong container).

  • Compute mapping against the base (opened) container, not the possibly re-assigned container.
  • Use GetSlot for each ChangedSlots entry; the current “> container.Size” check is both fragile and wrong for boundary cases (clickedSlot == Size). This can cause out-of-range access and desync.

Apply:

-        var container = player.OpenedContainer ?? player.Inventory;
-
-        var (slot, forPlayer) = container.GetSlot(ClickedSlot);
-
-        if (this.IsPlayerInventory || forPlayer)
-            container = player.Inventory;
-
-        var clickedItem = slot != -999 ? container[slot] : null;
+        var baseContainer = player.OpenedContainer ?? player.Inventory;
+        var (slot, forPlayer) = baseContainer.GetSlot(ClickedSlot);
+        var container = (this.IsPlayerInventory || forPlayer) ? player.Inventory : baseContainer;
+        var clickedItem = slot != -999 ? container.GetItem(slot) : null;
-        foreach (var (changedSlot, hashedItem) in this.ChangedSlots)
-        {
-            var currentContainer = changedSlot > container.Size || forPlayer ? player.Inventory : player.OpenedContainer;
-
-            var checkedItem = currentContainer[changedSlot];
-
-            if (hashedItem == null)
-            {
-                currentContainer.RemoveItem(changedSlot);
-                continue;
-            }
-
-            if (checkedItem != null && !hashedItem.Compare(checkedItem))
-                invalidItems.Add(changedSlot, hashedItem);
-        }
+        foreach (var (changedSlot, hashedItem) in this.ChangedSlots)
+        {
+            var (mappedSlot, isPlayerSlot) = baseContainer.GetSlot(changedSlot);
+            var currentContainer = isPlayerSlot ? player.Inventory : baseContainer;
+
+            if (hashedItem == null)
+            {
+                currentContainer.RemoveItem(mappedSlot);
+                continue;
+            }
+
+            var checkedItem = currentContainer.GetItem(mappedSlot);
+            if (checkedItem != null && !hashedItem.Compare(checkedItem))
+                invalidItems.Add(changedSlot, hashedItem);
+        }

Also applies to: 95-110

Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs (1)

25-38: Held-item equipment update never fires after GetSlot change.

The comparison uses ClickedSlot (window slot), but you set the item at slot (inventory slot). Hotbar indices are 36–44; CurrentHeldItemSlot is 0–8.

Apply:

-        var (slot, isForPlayer) = inventory.GetSlot(ClickedSlot);
+        var (slot, isForPlayer) = inventory.GetSlot(ClickedSlot);

         if (isForPlayer)
             inventory = player.Inventory;

         inventory.SetItem(slot, ClickedItem);

-        if (player.CurrentHeldItemSlot == ClickedSlot)
+        // If we updated the held hotbar slot, broadcast equipment.
+        if (isForPlayer && slot >= 36 && slot <= 44 && (slot - 36) == player.CurrentHeldItemSlot)
         {
             var heldItem = player.GetHeldItem();
Obsidian/Events/MainEventHandler.Inventory.cs (2)

76-86: Clear crafting result via API, not by removing slot 9.

Result is now cached via CraftingTable.SetResult. Ensure it’s cleared when no recipe is found to avoid stale outputs.

         if (recipe is null)
         {
-            if (container[9] != null)
-                container.RemoveItem(9);
+            table.SetResult(null);

             await player.Client.QueuePacketAsync(new ContainerSetSlotPacket
             {
                 Slot = 0,
                 ContainerId = player.CurrentContainerId,
                 SlotData = null
             });

Also applies to: 60-71


438-446: Possible NRE: currentItem can be null.

Use IsNullOrAir() to handle null safely.

-        if (carriedItem != null)
+        if (carriedItem != null)
         {
-            if (currentItem.IsAir)
+            if (currentItem.IsNullOrAir())
                 container.RemoveItem(clickedSlot);
             else
                 container.SetItem(clickedSlot, currentItem);
🧹 Nitpick comments (12)
Obsidian.API/Containers/BaseContainer.cs (4)

21-21: Range indexer returns a copy, not a live view. Document or expose an API that makes this clear.

Callers might expect mutations to reflect back into the container. Consider renaming or documenting the copy semantics; alternatively expose a method to set a range.


120-129: Doc comment is outdated; return type is SlotDifference (slot + forPlayer).

Update XML docs to reflect both values and consider validating boundary cases (e.g., clickedSlot == Size).

Example doc tweak:

-    /// <returns>The actual slot number for the given clicked slot.</returns>
+    /// <returns>A SlotDifference containing the resolved slot and whether it maps to the player's inventory.</returns>

140-147: Manual Deconstruct likely redundant for record struct.

Record structs synthesize Deconstruct for primary constructor members. You can remove this method unless you’re customizing names/behavior.

-public readonly record struct SlotDifference(short Slot, bool ForPlayer)
-{
-    public void Deconstruct(out short slot, out bool forPlayer)
-    {
-        slot = this.Slot;
-        forPlayer = this.ForPlayer;
-    }
-}
+public readonly record struct SlotDifference(short Slot, bool ForPlayer);

18-18: Add explicit C# language version to Obsidian.API project
To enable the C# 12 list-expression syntax ([]), add a <LangVersion> entry in Obsidian.API/Obsidian.API.csproj—e.g.:

<PropertyGroup>
  …
  <LangVersion>latestMajor</LangVersion>
</PropertyGroup>

Alternatively, ensure your CI uses the .NET 9 SDK (which defaults to a C# version ≥ 12).

Obsidian.Tests/Player.cs (1)

1-149: Nice coverage; consider two more tests.

  • QuickMove from player slot to container while a window is open (regresses the from==to bug).
  • Pickup-left outside: verify entity spawn path (can run in “test mode” to assert non-null thrown result).

I can add these tests targeting the fixed QuickMove and Throw paths if you want.

Obsidian.Tests/Fakes/FakePlayer.cs (7)

29-29: Avoid C# 12 target-typed collection for broader compatibility

Use an explicit constructor to keep the test project flexible on LangVersion.

-    public List<short> DraggedSlots { get; } = [];
+    public List<short> DraggedSlots { get; } = new List<short>();

13-13: Make CurrentContainerId settable for tests

Allow tests to switch container contexts without redefining the fake.

-    public byte CurrentContainerId => 0;
+    public byte CurrentContainerId { get; set; } = 0;

167-167: Implement SetGamemodeAsync as a simple setter

Keeps behavior consistent and avoids unnecessary exceptions.

-    public ValueTask SetGamemodeAsync(Gamemode gamemode) => throw new NotImplementedException();
+    public ValueTask SetGamemodeAsync(Gamemode gamemode)
+    {
+        this.Gamemode = gamemode;
+        return ValueTask.CompletedTask;
+    }

137-139: Return sensible defaults for held/offhand items

Prevents throws if tests read these.

-    public ItemStack GetHeldItem() => throw new NotImplementedException();
+    public ItemStack GetHeldItem() => this.Inventory.GetItem(this.CurrentHeldItemSlot) ?? ItemStack.Air;
@@
-    public ItemStack GetOffHandItem() => throw new NotImplementedException();
+    public ItemStack GetOffHandItem() => ItemStack.Air;

84-84: Give Uuid a default value

Useful for server player registry tests; avoids default(Guid) collisions.

-    public Guid Uuid { get; set; }
+    public Guid Uuid { get; set; } = Guid.NewGuid();

20-21: LoadedChunks should not throw

If any code enumerates it, throwing will surprise tests. Prefer an empty set.

-    public ConcurrentHashSet<long> LoadedChunks => throw new NotImplementedException();
+    public ConcurrentHashSet<long> LoadedChunks { get; } = new ConcurrentHashSet<long>();

158-166: Most messaging/networking methods can be no-ops

If not explicitly asserted in tests, return CompletedTask to keep fakes lightweight.

-    public ValueTask SendActionBarAsync(string text) => throw new NotImplementedException();
+    public ValueTask SendActionBarAsync(string text) => ValueTask.CompletedTask;
@@
-    public ValueTask SendMessageAsync(ChatMessage message) => throw new NotImplementedException();
+    public ValueTask SendMessageAsync(ChatMessage message) => ValueTask.CompletedTask;
@@
-    public ValueTask SendMessageAsync(ChatMessage message, Guid sender, SecureMessageSignature messageSignature) => throw new NotImplementedException();
+    public ValueTask SendMessageAsync(ChatMessage message, Guid sender, SecureMessageSignature messageSignature) => ValueTask.CompletedTask;
@@
-    public ValueTask SendPlayerInfoAsync() => throw new NotImplementedException();
+    public ValueTask SendPlayerInfoAsync() => ValueTask.CompletedTask;
@@
-    public ValueTask SendSoundAsync(ISoundEffect soundEffect) => throw new NotImplementedException();
+    public ValueTask SendSoundAsync(ISoundEffect soundEffect) => ValueTask.CompletedTask;
@@
-    public ValueTask SendSubtitleAsync(ChatMessage subtitle, int fadeIn, int stay, int fadeOut) => throw new NotImplementedException();
+    public ValueTask SendSubtitleAsync(ChatMessage subtitle, int fadeIn, int stay, int fadeOut) => ValueTask.CompletedTask;
@@
-    public ValueTask SendTitleAsync(ChatMessage title, int fadeIn, int stay, int fadeOut) => throw new NotImplementedException();
+    public ValueTask SendTitleAsync(ChatMessage title, int fadeIn, int stay, int fadeOut) => ValueTask.CompletedTask;
@@
-    public ValueTask SendTitleAsync(ChatMessage title, ChatMessage subtitle, int fadeIn, int stay, int fadeOut) => throw new NotImplementedException();
+    public ValueTask SendTitleAsync(ChatMessage title, ChatMessage subtitle, int fadeIn, int stay, int fadeOut) => ValueTask.CompletedTask;
@@
-    public ValueTask SetActionBarTextAsync(ChatMessage message) => throw new NotImplementedException();
+    public ValueTask SetActionBarTextAsync(ChatMessage message) => ValueTask.CompletedTask;
@@
-    public ValueTask UpdateDisplayNameAsync(string newDisplayName) => throw new NotImplementedException();
+    public ValueTask UpdateDisplayNameAsync(string newDisplayName) => ValueTask.CompletedTask;
@@
-    public ValueTask UpdatePlayerInfoAsync() => throw new NotImplementedException();
+    public ValueTask UpdatePlayerInfoAsync() => ValueTask.CompletedTask;

Also applies to: 171-184

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9300d80 and 30cee86.

📒 Files selected for processing (12)
  • Obsidian.API/Containers/BaseContainer.cs (3 hunks)
  • Obsidian.API/Containers/CraftingTable.cs (2 hunks)
  • Obsidian.API/Events/ContainerClickEventArgs.cs (1 hunks)
  • Obsidian.API/Inventory/ItemStack.cs (3 hunks)
  • Obsidian.API/Utilities/Extensions.cs (2 hunks)
  • Obsidian.Tests/Fakes/FakePlayer.cs (1 hunks)
  • Obsidian.Tests/Fakes/FakeServer.cs (1 hunks)
  • Obsidian.Tests/Player.cs (1 hunks)
  • Obsidian/Events/MainEventHandler.Inventory.cs (9 hunks)
  • Obsidian/InternalsVisibleTo.cs (1 hunks)
  • Obsidian/Net/Packets/Play/Serverbound/ContainerClickPacket.cs (1 hunks)
  • Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
Obsidian.API/Containers/CraftingTable.cs (1)
Obsidian.API/Containers/BaseContainer.cs (3)
  • ItemStack (64-64)
  • ItemStack (157-157)
  • SetResult (155-155)
Obsidian.Tests/Fakes/FakeServer.cs (2)
Obsidian.API/Configuration/ServerConfiguration.cs (1)
  • ServerConfiguration (5-108)
Obsidian.Tests/Fakes/FakePlayer.cs (6)
  • Task (140-140)
  • Task (151-151)
  • Task (155-155)
  • Task (156-156)
  • Task (157-157)
  • Task (181-181)
Obsidian.API/Utilities/Extensions.cs (4)
Obsidian.API/Containers/BaseContainer.cs (2)
  • ItemStack (64-64)
  • ItemStack (157-157)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (9-109)
  • ItemStack (28-34)
  • ItemStack (36-36)
Obsidian.API/_Interfaces/IPlayer.cs (2)
  • ItemStack (131-131)
  • ItemStack (132-132)
Obsidian/Entities/Player.cs (2)
  • ItemStack (187-187)
  • ItemStack (188-188)
Obsidian.API/Events/ContainerClickEventArgs.cs (2)
Obsidian.API/_Interfaces/IServer.cs (3)
  • IPlayer (58-58)
  • IPlayer (59-59)
  • IPlayer (60-60)
Obsidian.API/Containers/BaseContainer.cs (3)
  • BaseContainer (6-138)
  • BaseContainer (23-23)
  • BaseContainer (25-30)
Obsidian/Events/MainEventHandler.Inventory.cs (5)
Obsidian.API/Containers/BaseContainer.cs (11)
  • SetResult (155-155)
  • BaseContainer (6-138)
  • BaseContainer (23-23)
  • BaseContainer (25-30)
  • RemoveItem (66-74)
  • RemoveItem (76-88)
  • RemoveItem (90-103)
  • RemoveItem (105-118)
  • SetItem (62-62)
  • ItemStack (64-64)
  • ItemStack (157-157)
Obsidian.API/Containers/CraftingTable.cs (2)
  • SetResult (17-17)
  • ItemStack (19-19)
Obsidian.API/Events/ContainerClickEventArgs.cs (3)
  • ContainerClickEventArgs (5-65)
  • ContainerClickEventArgs (39-39)
  • ContainerClickEventArgs (46-64)
Obsidian.API/Utilities/Extensions.cs (1)
  • IsNullOrAir (14-14)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (9-109)
  • ItemStack (28-34)
  • ItemStack (36-36)
Obsidian.API/Inventory/ItemStack.cs (3)
Obsidian.API/Containers/BaseContainer.cs (2)
  • ItemStack (64-64)
  • ItemStack (157-157)
Obsidian.API/Registries/ItemsRegistry.cs (3)
  • ItemStack (16-16)
  • ItemStack (18-18)
  • ItemStack (20-20)
Obsidian.API/Inventory/DataComponents/DataComponentsStorage.cs (1)
  • ContainsKey (42-42)
Obsidian.Tests/Player.cs (6)
Obsidian/Events/MainEventHandler.Inventory.cs (3)
  • MainEventHandler (10-484)
  • ItemStack (289-303)
  • OnInventoryClickTest (455-483)
Obsidian.Tests/Fakes/FakeServer.cs (1)
  • FakeServer (12-62)
Obsidian.Tests/Fakes/FakePlayer.cs (3)
  • FakePlayer (11-185)
  • ItemStack (137-137)
  • ItemStack (139-139)
Obsidian.API/Containers/BaseContainer.cs (3)
  • ItemStack (64-64)
  • ItemStack (157-157)
  • SetItem (62-62)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (9-109)
  • ItemStack (28-34)
  • ItemStack (36-36)
Obsidian.API/Events/ContainerClickEventArgs.cs (3)
  • ContainerClickEventArgs (5-65)
  • ContainerClickEventArgs (39-39)
  • ContainerClickEventArgs (46-64)
Obsidian.Tests/Fakes/FakePlayer.cs (3)
Obsidian.API/Containers/BaseContainer.cs (5)
  • BaseContainer (6-138)
  • BaseContainer (23-23)
  • BaseContainer (25-30)
  • ItemStack (64-64)
  • ItemStack (157-157)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (9-109)
  • ItemStack (28-34)
  • ItemStack (36-36)
Obsidian/Events/MainEventHandler.Inventory.cs (2)
  • ItemStack (289-303)
  • ValueTask (50-87)
🔇 Additional comments (5)
Obsidian/InternalsVisibleTo.cs (1)

3-3: No strong-naming detected — PublicKey not required
Searches across all *.csproj and *.props files found no <SignAssembly>, <AssemblyOriginatorKeyFile>, <PublicKey> or legacy signing attributes, so this assembly isn’t strong-named and no PublicKey is needed in InternalsVisibleTo.

Obsidian.API/Containers/CraftingTable.cs (2)

7-8: Clarify result lifecycle: when is it invalidated/refreshed?

Caching result decouples it from slot 9. Ensure it’s cleared/updated whenever the grid changes to avoid stale outputs.

Do you want a helper on the container to invalidate result when inputs mutate?


17-20: Verify removal of all legacy slot-9 calls
rg returned no matches for SetItem(9, …) or GetItem(9)—manually inspect container/UI/serialization code to ensure no numeric slot-9 references remain.

Obsidian.API/Events/ContainerClickEventArgs.cs (1)

46-64: Factory looks good.

The Create(...) helper keeps tests clean and correctly sets required fields.

Obsidian.Tests/Fakes/FakeServer.cs (1)

38-43: No-op properties unused—no implementation needed
Tests don’t reference CommandHandler, ScoreboardManager, or EventDispatcher, so leaving them throwing NotImplementedException is acceptable.

@Tides Tides added epic Very cool server Relates to the server implementation priority: high Very important labels Aug 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Obsidian/Events/MainEventHandler.Inventory.cs (2)

221-250: Guard QuickCraft finalize with IsNullOrAir and fix merge into existing stacks.

Current code subtracts from carried but does not increment existing items in target slots; items disappear.

Apply:

-        if (!player.IsDragging && player.CarriedItem != null)
+        if (!player.IsDragging && !player.CarriedItem.IsNullOrAir())
@@
-            for (var i = 0; i < player.DraggedSlots.Count; i++)
+            for (var i = 0; i < player.DraggedSlots.Count; i++)
             {
                 var slotIndex = span[i];
                 var item = container.GetItem(slotIndex);
 
                 var amount = Math.Min(perSlot, player.CarriedItem.MaxStackSize - (item?.Count ?? 0));
 
                 if (amount > 0)
                 {
-                    container.SetItem(slotIndex, item ?? new(player.CarriedItem, amount));
+                    if (item is null)
+                        container.SetItem(slotIndex, new(player.CarriedItem, amount));
+                    else
+                        item += amount;
                     player.CarriedItem -= amount;
                 }
 
                 if (player.CarriedItem.Count <= 0)
                     break;
             }

259-264: QuickCraft AddSlotRight/AddSlotMiddle misuse carried instance; fixes needed.

  • AddSlotRight assigns the carried object to the slot, then decrements carried, aliasing the same instance.
  • AddSlotMiddle fills a full stack but doesn’t adjust carried.

Apply:

-            case DraggingState.AddSlotRight:
-                container.SetItem(clickedSlot, player.CarriedItem);
-                player.CarriedItem -= 1;
+            case DraggingState.AddSlotRight:
+                {
+                    var existing = container.GetItem(clickedSlot);
+                    if (existing.IsNullOrAir())
+                    {
+                        container.SetItem(clickedSlot, new(player.CarriedItem, 1));
+                        player.CarriedItem -= 1;
+                    }
+                    else if (existing == player.CarriedItem && existing.Count < existing.MaxStackSize)
+                    {
+                        existing += 1;
+                        player.CarriedItem -= 1;
+                    }
+                }
                 break;
             case DraggingState.AddSlotMiddle:
-                container.SetItem(clickedSlot, new(player.CarriedItem, player.CarriedItem.MaxStackSize));
+                {
+                    var existing = container.GetItem(clickedSlot);
+                    var room = (existing?.MaxStackSize ?? player.CarriedItem.MaxStackSize) - (existing?.Count ?? 0);
+                    if (room <= 0) break;
+                    var take = Math.Min(room, player.CarriedItem.Count);
+                    if (existing is null)
+                        container.SetItem(clickedSlot, new(player.CarriedItem, take));
+                    else
+                        existing += take;
+                    player.CarriedItem -= take;
+                }
                 break;
♻️ Duplicate comments (3)
Obsidian/Events/MainEventHandler.Inventory.cs (3)

353-360: Pickup outside now spawns a dropped item.

Nice fix; this addresses the previously noted omission.


91-103: QuickMove can select the same container for source and target.

When clicking a non-player slot, container == player.OpenedContainer, but to is also player.OpenedContainer, so TryMoveItem operates within the same container. Derive target from the actual source instead.

Apply:

-        TryMoveItem(container, clickedSlot, player.OpenedContainer ?? player.Inventory);
+        var from = container;
+        var to = ReferenceEquals(from, player.Inventory)
+            ? (player.OpenedContainer ?? player.Inventory)
+            : player.Inventory;
+        TryMoveItem(from, clickedSlot, to);

288-302: ThrowItem returns leftover stack instead of thrown amount.

This corrupts state and spawns wrong quantities. Return the actual thrown stack and perform bounded subtraction.

Apply:

-    private static ItemStack? ThrowItem(IPlayer player, BaseContainer container, short clickedSlot, sbyte button, bool forPlayer = false)
+    private static ItemStack? ThrowItem(IPlayer player, BaseContainer container, short clickedSlot, sbyte button, bool forPlayer = false)
     {
         var amountToRemove = button == 0 ? 1 : 64;
 
-        ItemStack? removedItem;
+        ItemStack? thrown;
         if (forPlayer)
         {
-            player.CarriedItem -= amountToRemove;
-            removedItem = player.CarriedItem;
+            if (player.CarriedItem.IsNullOrAir())
+                return null;
+            var take = Math.Min(amountToRemove, player.CarriedItem.Count);
+            thrown = new ItemStack(player.CarriedItem, take);
+            player.CarriedItem -= take;
         }
         else
-            container.RemoveItem(clickedSlot, amountToRemove, out removedItem);
+        {
+            var src = container.GetItem(clickedSlot);
+            if (src.IsNullOrAir())
+                return null;
+            var take = Math.Min(amountToRemove, src.Count);
+            thrown = new ItemStack(src, take);
+            container.RemoveItem(clickedSlot, take);
+        }
 
-        return removedItem;
+        return thrown;
     }
🧹 Nitpick comments (3)
Obsidian/Events/MainEventHandler.Inventory.cs (2)

104-146: Minor cleanups in TryMoveItem; semantics mostly OK.

  • No-op: from.SetItem(fromSlot, sourceItem) at the end is unnecessary; sourceItem is already a reference to the slot item.

Apply:

-        from.SetItem(fromSlot, sourceItem);
         return false;

158-212: PickupAll uses value equality; confirm intended matching criteria.

Using operator != will match by Holder+components, ignoring Count. If you want type-only matching (e.g., ignore custom names), add an explicit predicate.

I can provide a helper like MatchesForPickupAll(item, carried) if desired.

Obsidian.API/Containers/BaseContainer.cs (1)

21-21: Range indexer returns a copy; ensure callers don’t expect a live view.

Array slicing creates a new array; mutations won’t reflect back into the container. Consider documenting this or exposing Span/ReadOnlySpan-based accessors for perf-sensitive paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 30cee86 and a282ca2.

📒 Files selected for processing (6)
  • Obsidian.API/Containers/BaseContainer.cs (3 hunks)
  • Obsidian.API/Inventory/ItemStack.cs (2 hunks)
  • Obsidian.API/Utilities/Extensions.cs (2 hunks)
  • Obsidian.Tests/Player.cs (1 hunks)
  • Obsidian/Events/MainEventHandler.Inventory.cs (11 hunks)
  • Obsidian/Net/Packets/Play/Serverbound/ContainerClickPacket.cs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Obsidian.API/Utilities/Extensions.cs
  • Obsidian.Tests/Player.cs
  • Obsidian.API/Inventory/ItemStack.cs
🧰 Additional context used
🧬 Code graph analysis (3)
Obsidian/Events/MainEventHandler.Inventory.cs (5)
Obsidian.API/Containers/BaseContainer.cs (11)
  • SetResult (150-150)
  • BaseContainer (6-140)
  • BaseContainer (23-23)
  • BaseContainer (25-30)
  • RemoveItem (66-74)
  • RemoveItem (76-88)
  • RemoveItem (90-103)
  • RemoveItem (105-118)
  • SetItem (62-62)
  • ItemStack (64-64)
  • ItemStack (152-152)
Obsidian.API/Containers/CraftingTable.cs (2)
  • SetResult (17-17)
  • ItemStack (19-19)
Obsidian.API/Events/ContainerClickEventArgs.cs (3)
  • ContainerClickEventArgs (5-65)
  • ContainerClickEventArgs (39-39)
  • ContainerClickEventArgs (46-64)
Obsidian.API/Utilities/Extensions.cs (1)
  • IsNullOrAir (14-14)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (7-107)
  • ItemStack (26-32)
  • ItemStack (34-34)
Obsidian.API/Containers/BaseContainer.cs (1)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (7-107)
  • ItemStack (26-32)
  • ItemStack (34-34)
Obsidian/Net/Packets/Play/Serverbound/ContainerClickPacket.cs (1)
Obsidian.API/Containers/BaseContainer.cs (4)
  • RemoveItem (66-74)
  • RemoveItem (76-88)
  • RemoveItem (90-103)
  • RemoveItem (105-118)
🔇 Additional comments (11)
Obsidian/Net/Packets/Play/Serverbound/ContainerClickPacket.cs (5)

2-2: Import aligns with API evolution.

Adding the Obsidian.API import is consistent with the new GetSlot/SlotDifference usage.


72-79: Container/slot resolution looks correct.

Deriving (slot, forPlayer) via baseContainer.GetSlot and then choosing the actual container avoids mutating baseContainer and keeps event source accurate. The -999 passthrough is preserved in clickedItem.


91-92: Confirm SetSlot StateId handling.

StateId = 0 may be intentional to force a hard resync, but some clients expect the current StateId echo. Please confirm protocol expectations for your server/client mix.

Would you like a quick script to grep for other SetSlot usages to ensure consistent StateId semantics?


98-107: ChangedSlots mapping is consistent with GetSlot.

Mapping each changedSlot through baseContainer.GetSlot and operating on the resolved container/slot prevents cross-inventory mismatches. Good use of GetItem/RemoveItem APIs.


126-133: Event dispatch passes resolved slot/container.

Passing the resolved slot with the derived container keeps downstream handlers simple. Note that ContainerClickEventArgs.IsPlayerInventory reflects ContainerId==0 (window vs. player inv), not whether the clicked slot was in the player area—handlers should not rely on it for target derivation.

Obsidian/Events/MainEventHandler.Inventory.cs (3)

38-42: New handlers wired into switch.

QuickMove and Clone are now reachable. Nice.


270-286: Throw outside inventory: behavior likely incomplete.

Returning early on OutsideInventory skips throwing from the carried stack for ClickType.Throw. Confirm intended behavior; parity with Pickup’s outside drop may be expected here too.

If needed, I can draft the outside-throw branch to remove from carried and SpawnThrownItem.


455-483: Test entrypoint mirrors runtime switch.

OnInventoryClickTest matches the dispatcher, good for unit coverage.

Obsidian.API/Containers/BaseContainer.cs (3)

18-19: Modernized empty collection initializer.

Viewer list initializer with [] is fine and concise.


142-143: SlotDifference record is a good fit for deconstruction and clarity.

Nice API ergonomics.


1-154: No stale GetDifference references remain. Rename to GetSlot is complete.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
Obsidian.API/Containers/BaseContainer.cs (1)

130-131: Incorrect mapping for player hotbar (last 9 slots) — implement piecewise mapping.

Current logic maps Size+27..Size+35 to 36..44, which is outside the 0..35 player inventory range and will misroute clicks. Use the standard two-range mapping (main 27 → 9..35, hotbar 9 → 0..8).

Apply:

-    public virtual SlotDifference GetSlot(short clickedSlot) =>
-        clickedSlot >= this.Size ? new((short)(clickedSlot - this.Size + 9), true) : new(clickedSlot, false);
+    public virtual SlotDifference GetSlot(short clickedSlot)
+    {
+        // Negative (e.g., -999 outside): do not map; caller must handle before indexing.
+        if (clickedSlot < 0)
+            return new(clickedSlot, false);
+
+        // Container area
+        if (clickedSlot < this.Size)
+            return new(clickedSlot, false);
+
+        // Player area appended after container
+        int rel = clickedSlot - this.Size; // 0..35
+        // Main inventory (27): map to 9..35
+        if (rel < 27)
+            return new((short)(rel + 9), true);
+        // Hotbar (9): map to 0..8
+        return new((short)(rel - 27), true);
+    }

Please add tests for:

  • clickedSlot: Size-1 → (Slot=Size-1, ForPlayer=false)
  • Size → (9, true), Size+26 → (35, true)
  • Size+27 → (0, true), Size+35 → (8, true)
  • -999 → (-999, false)
🧹 Nitpick comments (4)
Obsidian.API/Containers/BaseContainer.cs (4)

21-21: Range indexer returns a copied array; confirm this allocation and copy semantics are intentional.

x[range] on arrays creates a new array. If you intended a non-allocating view, expose ReadOnlySpan<ItemStack?> or ArraySegment<ItemStack?> instead.

Example:

- public ItemStack?[] this[Range range] { get => this.items[range]; }
+ public ArraySegment<ItemStack?> this[Range range]
+ {
+     get
+     {
+         var (start, length) = range.GetOffsetAndLength(this.items.Length);
+         return new ArraySegment<ItemStack?>(this.items, start, length);
+     }
+ }

120-129: Clarify docs: describe player-main vs hotbar mapping and negative sentinel slots.

Document that: (a) slots < Size map to the container; (b) Size..Size+26 map to player main inventory (9..35); (c) Size+27..Size+35 map to hotbar (0..8); (d) negative slots (e.g., -999) are not mapped and should be handled by the caller (throw/“outside” actions).

Proposed doc tweak:

-/// Gets the slot difference for the given clicked slot.
+/// Maps a window (clicked) slot index to a concrete slot in either the container or the player's inventory.
...
-/// A SlotDifference containing the resolved slot and whether it maps to the player's inventory.
+/// A SlotDifference containing:
+/// - Slot: the resolved slot index (container: 0..Size-1; player main: 9..35; hotbar: 0..8).
+/// - ForPlayer: true if the slot resolves to the player's inventory (main/hotbar), false for the container.
...
-/// This method is used to determine the actual slot number for a clicked slot in a container.
+/// Mapping rules:
+/// - clickedSlot < Size  => container slot (Slot = clickedSlot, ForPlayer = false)
+/// - Size .. Size+26     => player main 27 slots (Slot = clickedSlot - Size + 9, ForPlayer = true)
+/// - Size+27 .. Size+35  => player hotbar 9 slots (Slot = clickedSlot - (Size + 27), ForPlayer = true)
+/// - clickedSlot < 0     => special/“outside” clicks; returned as-is (caller must handle before indexing)

142-142: Type addition looks good; consider adding XML docs for fields.

Record struct is appropriate. Optional: document Slot and ForPlayer, or rename ForPlayer → IsPlayerInventory for clarity.


18-18: Refactor Viewers to IReadOnlyList with controlled mutators and specify LangVersion

  • Change public List<IPlayer> Viewers { get; } = []; (BaseContainer.cs:18) to:
    private readonly List<IPlayer> _viewers = new();
    public IReadOnlyList<IPlayer> Viewers => _viewers;
    public void AddViewer(IPlayer player) => _viewers.Add(player);
    public bool RemoveViewer(IPlayer player) => _viewers.Remove(player);
  • Obsidian.API.csproj targets net9.0 with <EnablePreviewFeatures>true</EnablePreviewFeatures> but lacks <LangVersion>; add <LangVersion>12.0</LangVersion> (or preview) to enable C# 12 collection expressions.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a282ca2 and b8a3515.

📒 Files selected for processing (1)
  • Obsidian.API/Containers/BaseContainer.cs (3 hunks)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Obsidian/Events/MainEventHandler.Inventory.cs (1)

221-254: Guard QuickCraft paths and forbid Creative-only clone in Survival.

  • Ensure QuickCraft right/left phases only run when player.CarriedItem is non-null.
  • Middle-add currently clones a full stack without mode checks, enabling dupes in Survival.

Apply:

-        if (!player.IsDragging && !player.CarriedItem.IsNullOrAir())
+        if (!player.IsDragging && !player.CarriedItem.IsNullOrAir())
         {
             ...
         }
         ...
         switch (state)
         {
             case DraggingState.AddSlotLeft:
                 ...
                 break;
             case DraggingState.AddSlotRight:
-                var existingItem = container.GetItem(clickedSlot);
+                if (player.CarriedItem.IsNullOrAir())
+                    break;
+                var existingItem = container.GetItem(clickedSlot);
                 ...
                 break;
             case DraggingState.AddSlotMiddle:
-                container.SetItem(clickedSlot, new(player.CarriedItem, player.CarriedItem.MaxStackSize));
+                if (player.Gamemode == Gamemode.Creative && !player.CarriedItem.IsNullOrAir())
+                    container.SetItem(clickedSlot, new(player.CarriedItem, player.CarriedItem.MaxStackSize));
                 break;
             default:
                 break;
         }

Also applies to: 263-275, 277-279

♻️ Duplicate comments (2)
Obsidian/Events/MainEventHandler.Inventory.cs (2)

91-102: Fix QuickMove destination: from==to causes no-op or self-merge.

When clicking inside a container (e.g., chest), player.OpenedContainer equals container, so to becomes the same as from. Shift-click should move between the active container and the player inventory.

Apply:

     private static void HandleQuickMove(ContainerClickEventArgs args)
     {
         var container = args.Container;
         var player = args.Player;
         var clickedSlot = args.ClickedSlot;
         var clickedItem = args.Item;

         if (clickedItem.IsNullOrAir())
             return;

-        TryMoveItem(container, clickedSlot, player.OpenedContainer ?? player.Inventory);
+        var from = container;
+        var to = ReferenceEquals(from, player.Inventory)
+            ? (player.OpenedContainer ?? player.Inventory)
+            : player.Inventory;
+        if (ReferenceEquals(from, to))
+            return;
+        TryMoveItem(from, clickedSlot, to);
     }

303-317: ThrowItem returns the leftover stack; return the thrown amount instead and fix amount rules.

Currently spawns wrong quantities and corrupts state. Also, outside-drop (forPlayer=true) should drop all on left-click and one on right-click; Q/ctrl+Q should drop one/all from the slot.

Apply:

-    private static ItemStack? ThrowItem(IPlayer player, BaseContainer container, short clickedSlot, sbyte button, bool forPlayer = false)
+    private static ItemStack? ThrowItem(IPlayer player, BaseContainer container, short clickedSlot, sbyte button, bool forPlayer = false)
     {
-        var amountToRemove = button == 0 ? 1 : 64;
-
-        ItemStack? removedItem;
-        if (forPlayer)
-        {
-            player.CarriedItem -= amountToRemove;
-            removedItem = player.CarriedItem;
-        }
-        else
-            container.RemoveItem(clickedSlot, amountToRemove, out removedItem);
-
-        return removedItem;
+        if (forPlayer)
+        {
+            var src = player.CarriedItem;
+            if (src.IsNullOrAir())
+                return null;
+            var take = button == 0 ? src.Count : 1; // left: drop all; right: drop one
+            take = Math.Min(take, src.Count);
+            var thrown = new ItemStack(src, take);
+            player.CarriedItem -= take;
+            return thrown;
+        }
+        else
+        {
+            var src = container.GetItem(clickedSlot);
+            if (src.IsNullOrAir())
+                return null;
+            var take = button == 0 ? 1 : src.Count; // Q: one, Ctrl+Q: entire stack
+            take = Math.Min(take, src.Count);
+            var thrown = new ItemStack(src, take);
+            container.RemoveItem(clickedSlot, take);
+            return thrown;
+        }
     }
🧹 Nitpick comments (2)
Obsidian/Events/MainEventHandler.Inventory.cs (2)

158-212: PickupAll relies on IsPlayerInventory; prefer identity checks.

args.IsPlayerInventory can be inconsistent across contexts. Derive intent via ReferenceEquals(args.Container, player.Inventory) to avoid edge cases when a window is open but the actual container is the player inventory.

Proposed tweak:

-        if (amountNeeded > 0 && !args.IsPlayerInventory)
+        if (amountNeeded > 0 && !ReferenceEquals(container, player.Inventory))

441-468: Minor: variable naming is misleading in HandleSwap.

carriedItem is actually the clicked-slot item (args.Item). Consider renaming to clickedItem for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b8a3515 and a0d6f56.

📒 Files selected for processing (1)
  • Obsidian/Events/MainEventHandler.Inventory.cs (13 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Obsidian/Events/MainEventHandler.Inventory.cs (5)
Obsidian.API/Containers/BaseContainer.cs (11)
  • SetResult (150-150)
  • BaseContainer (6-140)
  • BaseContainer (23-23)
  • BaseContainer (25-30)
  • RemoveItem (66-74)
  • RemoveItem (76-88)
  • RemoveItem (90-103)
  • RemoveItem (105-118)
  • SetItem (62-62)
  • ItemStack (64-64)
  • ItemStack (152-152)
Obsidian.API/Containers/CraftingTable.cs (2)
  • SetResult (17-17)
  • ItemStack (19-19)
Obsidian.API/Events/ContainerClickEventArgs.cs (3)
  • ContainerClickEventArgs (5-65)
  • ContainerClickEventArgs (39-39)
  • ContainerClickEventArgs (46-64)
Obsidian.API/Utilities/Extensions.cs (1)
  • IsNullOrAir (14-14)
Obsidian.API/Inventory/ItemStack.cs (3)
  • ItemStack (7-107)
  • ItemStack (26-32)
  • ItemStack (34-34)
🔇 Additional comments (3)
Obsidian/Events/MainEventHandler.Inventory.cs (3)

65-66: Crafting result flow looks good.

Switching to CraftingTable.SetResult(...) and mirroring via ContainerSetSlotPacket keeps server state and client view consistent.

Also applies to: 80-88


147-156: Clone behavior matches Creative semantics.

Fills carried stack to max from the clicked item; looks correct.


285-301: Throw outside guard is fine; keep.

Early-return on OutsideInventory in Throw context is reasonable, since Q/ctrl+Q target an actual slot.

@Tides Tides merged commit 9447826 into 1.21.x Sep 1, 2025
3 checks passed
@Tides Tides deleted the more-inventory-features branch September 1, 2025 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Relates to Obsidian.API epic Very cool priority: high Very important server Relates to the server implementation tests Relates to Obsidian.Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants