Skip to content

Multiblock Mining #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/block-interactions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ dependencies {
implementation(project(":modules:player"))

compileOnly(project(":modules:quest"))

implementation("io.github.jglrxavpok.hephaistos:common:2.2.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,21 @@ public PickaxeHandler() {
}

private void handleLongDiggingStart(PlayerLongDiggingStartEvent event) {
var ore = Ore.fromBlock(event.getBlock());
if (ore == null || event.getBlock().compare(OreBlockHandler.REPLACEMENT_BLOCK, Block.Comparator.STATE)) return;

// Ensure they have a pickaxe in hand and get the pickaxe
var item = Item.fromItemStack(event.getPlayer().getItemInMainHand());
//todo will currently fail on any non-custom item

var pickaxe = item.getComponent(Pickaxe.class);
if (pickaxe == null) return; // Not holding a pickaxe

// Start mining the block
event.setDiggingBlock(
ore.health(),
pickaxe::miningSpeed
);
// var ore = Ore.fromBlock(event.getBlock());
// if (ore == null || event.getBlock().compare(OreBlockHandler.REPLACEMENT_BLOCK, Block.Comparator.STATE)) return;
//
// // Ensure they have a pickaxe in hand and get the pickaxe
// var item = Item.fromItemStack(event.getPlayer().getItemInMainHand());
// //todo will currently fail on any non-custom item
//
// var pickaxe = item.getComponent(Pickaxe.class);
// if (pickaxe == null) return; // Not holding a pickaxe
//
// // Start mining the block
// event.setDiggingBlock(
// ore.health(),
// pickaxe::miningSpeed
// );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package net.hollowcube.blocks.resource;

public class BlockResource {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.hollowcube.blocks.resource;

//{
// "namespace": "starlight:oak_tree",
// "loot_table": "starlight:oak_tree",
// "type": "schematic",
// // size: {x: 3, y: 3, z: 3}, // Would prevent a mistake of accidentally adding a big one, im a fan of this
// "schematics": [
// "tree/foresttree1",
// "tree/foresttree2",
// "tree/foresttree3",
// "tree/foresttree4",
// "tree/foresttree5"
// ]
//}

import com.google.auto.service.AutoService;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.hollowcube.dfu.ExtraCodecs;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.utils.NamespaceID;

import java.util.List;

public record MultiBlockResource(
NamespaceID namespace,
NamespaceID lootTable,
Vec size,
List<String> schematics
) implements Resource {

public static final Codec<MultiBlockResource> CODEC = RecordCodecBuilder.create(i -> i.group(
ExtraCodecs.NAMESPACE_ID.fieldOf("namespace").forGetter(MultiBlockResource::namespace),
ExtraCodecs.NAMESPACE_ID.optionalFieldOf("loot_table", NamespaceID.from("starlight:empty")).forGetter(MultiBlockResource::lootTable),
ExtraCodecs.VEC.fieldOf("size").forGetter(MultiBlockResource::size),
Codec.STRING.listOf().fieldOf("schematics").forGetter(MultiBlockResource::schematics)
).apply(i, MultiBlockResource::new));


@AutoService(Resource.Factory.class)
public static class Factory extends Resource.Factory {
public Factory() {
super("multiblock", MultiBlockResource.class, MultiBlockResource.CODEC);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package net.hollowcube.blocks.resource;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.hollowcube.dfu.ExtraCodecs;
import net.hollowcube.lang.LanguageProvider;
import net.hollowcube.registry.Registry;
import net.hollowcube.registry.ResourceFactory;
import net.kyori.adventure.text.Component;
import net.minestom.server.tag.Tag;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;

/*

{
"namespace": "starlight:oak_tree",
"loot_table": "starlight:oak_tree",
"type": "schematic",
// size: {x: 3, y: 3, z: 3}, // Would prevent a mistake of accidentally adding a big one, im a fan of this
"schematics": [
"tree/foresttree1",
"tree/foresttree2",
"tree/foresttree3",
"tree/foresttree4",
"tree/foresttree5"
]
}

{
"namespace": "starlight:gold_ore",
"loot_table": "starlight:gold_ore",
"type": "block",
"block": "minecraft:gold_ore",
"replacement": "minecraft:bedrock"
}

*/

public interface Resource extends net.hollowcube.registry.Resource {

Tag<String> TAG = Tag.String("resource");

/**
* @return The translation key for this item
* @see LanguageProvider#get(Component)
*/
@Contract(pure = true)
default @NotNull String translationKey() {
return String.format("resource.%s.%s.name", namespace().namespace(), namespace().path());
}

Codec<Resource> CODEC = ExtraCodecs.lazy(() -> Factory.CODEC).dispatch(Factory::from, Factory::codec);

Registry<Resource> REGISTRY = Registry.lazy(() -> Registry.codec("resource", CODEC));

static @UnknownNullability Resource fromNamespaceId(@NotNull NamespaceID namespace) {
return REGISTRY.get(namespace);
}

static @UnknownNullability Resource fromNamespaceId(@NotNull String namespace) {
return REGISTRY.get(namespace);
}


class Factory extends ResourceFactory<Resource> {
static Registry<Factory> REGISTRY = Registry.service("resource_type", Resource.Factory.class);
static Registry.Index<Class<?>, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type);

static Codec<Factory> CODEC = Codec.STRING.xmap(ns -> REGISTRY.required(ns), Factory::name);

public Factory(String id, Class<? extends Resource> type, Codec<? extends Resource> codec) {
super(NamespaceID.from("starlight", id), type, codec);
}

public static @NotNull Factory from(@NotNull Resource resource) {
return TYPE_REGISTRY.get(resource.getClass());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package net.hollowcube.blocks.resource;

import com.google.auto.service.AutoService;
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import net.hollowcube.blocks.resource.handler.ResourceSpawner;
import net.hollowcube.blocks.schem.SchematicManager;
import net.hollowcube.player.event.PlayerLongDiggingStartEvent;
import net.hollowcube.server.Facet;
import net.hollowcube.server.ServerWrapper;
import net.minestom.server.coordinate.Point;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.utils.Rotation;
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.atomic.AtomicInteger;

@AutoService(Facet.class)
public class ResourceFacet implements Facet {

// Globally increasing ID for resources in the world
private final AtomicInteger nextId = new AtomicInteger(0);

// All resources currently in the server
// todo probably would make more sense to do this per instance rather than per server
private final Int2ObjectMap<WorldResource> resources = Int2ObjectMaps.synchronize(new Int2ObjectArrayMap<>());

private final BlockHandler resourceSpawnerHandler = new ResourceSpawner(this);

//todo ResourceSpawner block handler which will delay and then spawn a resource in a given position
// added to the world when a resource is removed.


@Override
public void hook(@NotNull ServerWrapper server) {
var node = EventNode.all("awghauwghuiawg");
node.addListener(PlayerLongDiggingStartEvent.class, this::handleDigging);
node.addListener(PlayerBlockBreakEvent.class, this::blockBroken);
server.addEventNode(node);

server.registerBlockHandler(() -> resourceSpawnerHandler);
}

public void addResourceSpawner(@NotNull Resource resource, @NotNull Instance instance, @NotNull Point pos) {
var block = instance.getBlock(pos)
.withTag(Resource.TAG, resource.name())
.withHandler(resourceSpawnerHandler);
instance.setBlock(pos, block);
}

public void spawnResource(@NotNull Resource resource, @NotNull Instance instance, @NotNull Point pos) {
var worldResource = new WorldResource(nextId.getAndIncrement(), resource, instance, pos);
resources.put(worldResource.id(), worldResource);

//todo i guess this logic could exist in MultiBlockResource? This is not very generic
if (resource instanceof MultiBlockResource res) {
var schematicName = res.schematics().get(worldResource.id() % res.schematics().size());
var schematic = SchematicManager.get(schematicName);
var rotation = Rotation.values()[(worldResource.id() % 4) * 2];

schematic.applyManual(rotation, (blockPos, block) -> {
var taggedBlock = block.withTag(WorldResource.ID_TAG, worldResource.id());
instance.setBlock(pos.add(blockPos), taggedBlock);
});
}

}

private void handleDigging(@NotNull PlayerLongDiggingStartEvent event) {
final var block = event.getBlock();
final var resourceId = block.getTag(WorldResource.ID_TAG);
if (resourceId == null) return;

//todo get health from resource
event.setDiggingBlock(5, () -> 1);
}

private void blockBroken(@NotNull PlayerBlockBreakEvent event) {
final var instance = event.getInstance();
final var block = event.getBlock();
final var resourceId = block.getTag(WorldResource.ID_TAG);
if (resourceId == null) return;

var worldResource = resources.get((int) resourceId);
resources.remove((int) resourceId);

final var pos = worldResource.pos();
final var res = worldResource.resource();
if (res instanceof MultiBlockResource resource) {
//todo this might be problematic for "randomness" (getting both schematic and rotation from this number)
var schematicName = resource.schematics().get(resourceId % resource.schematics().size());
var schematic = SchematicManager.get(schematicName);
var rotation = Rotation.values()[(resourceId % 4) * 2];

schematic.applyManual(rotation, (relBlockPos, b) -> {
final var blockPos = pos.add(relBlockPos);

// Spawn particle
ServerPacket packet = ParticleCreator.createParticlePacket(
Particle.BLOCK, false, blockPos.x(), blockPos.y(), blockPos.z(), 0.51f, 0.51f, 0.51f,
0.15f, 25, binaryWriter -> binaryWriter.writeVarInt(b.stateId()));
event.getPlayer().sendPacketToViewersAndSelf(packet);

// Remove the block
instance.setBlock(blockPos, Block.AIR);
});

//todo add the resource back to the world after a delay
}

// Add a resource spawner block at the position
addResourceSpawner(res, instance, pos);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.hollowcube.blocks.resource;

import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.Instance;
import net.minestom.server.tag.Tag;
import org.jetbrains.annotations.NotNull;

public record WorldResource(
int id,
@NotNull Resource resource,
@NotNull Instance instance,
@NotNull Point pos
) {
public static final Tag<Integer> ID_TAG = Tag.Integer("resource_id");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package net.hollowcube.blocks.resource.handler;

import net.hollowcube.blocks.resource.Resource;
import net.hollowcube.blocks.resource.ResourceFacet;
import net.hollowcube.debug.Simulation;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.thread.TickThread;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ThreadLocalRandom;

public class ResourceSpawner implements BlockHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceSpawner.class);

private final ResourceFacet manager;

public ResourceSpawner(ResourceFacet manager) {
this.manager = manager;
}

@Override
public @NotNull NamespaceID getNamespaceId() {
return NamespaceID.from("starlight:resource_spawner");
}

@Override
public boolean isTickable() {
return true;
}

@Override
public void onPlace(@NotNull Placement placement) {
if (!placement.getBlock().hasTag(Resource.TAG)) {
var instance = placement.getInstance();
var block = placement.getBlock();
var pos = placement.getBlockPosition();

// Block handler should not be present if the resource tag is not present. Remove it and log error.
LOGGER.error("Resource spawner present on a block without resource tag: {} at {} in {}", block, pos, instance);
instance.setBlock(pos, block.withHandler(null));

//todo ensure the tag is valid
}
}

@Override
public void tick(@NotNull Tick tick) {
if (!Simulation.isRunning()) return;

var thread = TickThread.current();
if (thread == null) return;

// Tick once per second
if (thread.getTick() % 20 != 0) return;
// 25% chance to spawn each tick
if (ThreadLocalRandom.current().nextInt(4) != 0) return;

var block = tick.getBlock();
var resource = Resource.fromNamespaceId(block.getTag(Resource.TAG));
assert resource != null; // Checked in placement handler

var instance = tick.getInstance();
var pos = tick.getBlockPosition();
manager.spawnResource(resource, instance, pos);
}
}
Loading