Compare commits

...

24 Commits

Author SHA1 Message Date
df6a14f2b1
feat: save playerData when possible 2023-07-30 11:37:03 +08:00
89841beeb7
feat: add quaedam:projections tag to quaedam:music_projection 2023-07-30 11:25:18 +08:00
7ca6689bad
fix: repeat after loading from save 2023-07-30 11:21:00 +08:00
3ce9081400
feat: save rhythm seed 2023-07-30 11:20:52 +08:00
1bd5965ba1
fix: crash on fast-forwarded music player 2023-07-30 10:40:32 +08:00
eb772916dc
feat: adjust drop-out parameters 2023-07-30 10:38:29 +08:00
421f80c9a1
style: format code 2023-07-30 10:37:44 +08:00
fd7766b38c
fix: smart instrument block entity serialization 2023-07-30 10:33:10 +08:00
b574d8a3d4
feat: add translations for smart instrument 2023-07-30 10:27:30 +08:00
96c48fa0db
refactor: rename cyber instrument -> smart instrument 2023-07-30 10:26:39 +08:00
fe5eeb0364
feat: causality anchor effect on cyber instrument 2023-07-30 10:24:46 +08:00
b183e99b3c
fix: always return InteractionResult#SUCCESS when effect available 2023-07-30 10:19:58 +08:00
bdccd0c7f3
feat: adjust rhythm random to change per-second 2023-07-30 10:17:38 +08:00
726a0337c5
feat: drop-out for drum-like instruments 2023-07-30 10:17:21 +08:00
0aee57947d
feat: full-range for music projection PSH interface 2023-07-30 10:05:19 +08:00
336cab7de4
feat: adjust composer parameters 2023-07-30 10:05:05 +08:00
1d2d9ce4d8
fix: distance attenuation 2023-07-30 10:02:12 +08:00
bbef6cb342
feat: composer 2023-07-30 09:57:56 +08:00
314957eeb5
fix: client-side note player 2023-07-29 21:41:40 +08:00
79b11498c1
feat: item model for cyber instrument 2023-07-29 21:33:35 +08:00
730f6b9781
feat: music player 2023-07-29 21:33:08 +08:00
d5c9eb6b41
fix: boolean & cycle values not saved properly 2023-07-29 20:03:55 +08:00
2f7c9a883a
feat: add textures for music projection 2023-07-29 19:58:54 +08:00
e917807adc
feat: add music projection stub 2023-07-29 19:52:23 +08:00
16 changed files with 628 additions and 6 deletions

View File

@ -18,6 +18,7 @@ import quaedam.projection.SimpleProjectionUpdate
import quaedam.projection.misc.NoiseProjection
import quaedam.projection.misc.SkylightProjection
import quaedam.projection.misc.SoundProjection
import quaedam.projection.music.MusicProjection
import quaedam.projection.swarm.SwarmProjection
import quaedam.projector.Projector
import quaedam.shell.ProjectionShell
@ -53,6 +54,7 @@ object Quaedam {
SwarmProjection
SoundProjection
NoiseProjection
MusicProjection
ProjectionCommand
SimpleProjectionUpdate
ProjectionShell

View File

@ -0,0 +1,91 @@
package quaedam.projection.music
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument
import kotlin.math.abs
import kotlin.random.Random
import kotlin.random.nextInt
/**
* The composer for music.
* rhythmRandom is used for a better rhythm sync between different instruments.
*/
class Composer(val noteRandom: Random, val rhythmRandom: Random, val instrument: NoteBlockInstrument) {
data class Note(val note: Int, val volume: Float, val time: Int)
val baseTime = arrayOf(5, 5, 3, 3, 4, 4, 2, 2, 8).random(rhythmRandom)
val baseNote = noteRandom.nextInt(5..19)
val mayDropOut = instrument in arrayOf(
NoteBlockInstrument.BASEDRUM,
NoteBlockInstrument.HAT,
NoteBlockInstrument.SNARE,
)
fun composeMusic(): List<Note> {
var note = (0..rhythmRandom.nextInt(4)).flatMap { composeSection() }
note = decorate(note)
if (mayDropOut && rhythmRandom.nextInt(6) != 0) {
val dropRate = arrayOf(2, 3, 3, 4, 4, 4, 4, 6).random(rhythmRandom)
note = note.chunked(dropRate).map {
val first = it.first()
Note(first.note, first.volume, it.sumOf { note -> note.time })
}
}
return note
}
fun decorate(notes: List<Note>) = notes.map {
if (noteRandom.nextInt(4) == 0) {
doDecorate(it)
} else {
it
}
}
fun doDecorate(note: Note): Note {
var noteVal = note.note
if (noteRandom.nextInt(4) == 0) {
if (noteRandom.nextBoolean()) {
noteVal += 1
} else {
noteVal -= 1
}
}
var volume = note.volume
if (noteRandom.nextInt(4) == 0) {
volume *= noteRandom.nextFloat() * 0.8f + 0.6f
}
return Note(noteVal, volume, note.time)
}
fun composeSection(depth: Int = 0): List<Note> {
if (depth < 3 && rhythmRandom.nextBoolean()) {
val notes = (0..rhythmRandom.nextInt(3 - depth)).flatMap { composeSection(depth + 1) }
if (depth == 2) {
return (0..rhythmRandom.nextInt(3)).flatMap { notes }
} else {
return notes
}
} else {
var notePointer = baseNote + noteRandom.nextInt(-3..3)
var direction = -1
var directionCounter = 0
return (0..rhythmRandom.nextInt(4..16)).map {
if (directionCounter == 0) {
// start new direction
directionCounter = rhythmRandom.nextInt(2..6)
direction = if (directionCounter % 2 == 0) {
rhythmRandom.nextInt(-2..2)
} else {
noteRandom.nextInt(-3..3)
}
}
notePointer = abs(notePointer + direction) % 25
directionCounter--
Note(notePointer, 1.0f, baseTime + rhythmRandom.nextInt(-1..1))
}
}
}
}

View File

@ -0,0 +1,128 @@
package quaedam.projection.music
import dev.architectury.utils.GameInstance
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.resources.sounds.SoundInstance
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.nbt.CompoundTag
import net.minecraft.sounds.SoundEvent
import net.minecraft.sounds.SoundSource
import net.minecraft.util.RandomSource
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.NoteBlock
import net.minecraft.world.level.block.entity.SkullBlockEntity
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import quaedam.projector.Projector
import kotlin.random.Random
class MusicPlayer(
val noteSeed: Long,
val rhythmSeed: Long,
val level: Level,
val pos: BlockPos,
val startedAt: Long = level.gameTime
) {
companion object {
const val TAG_NOTE_SEED = "NoteSeed"
const val TAG_RHYTHM_SEED = "RhythmSeed"
const val TAG_STARTED_AT = "StartedAt"
}
constructor(tag: CompoundTag, level: Level, pos: BlockPos) : this(
tag.getLong(TAG_NOTE_SEED),
tag.getLong(TAG_RHYTHM_SEED),
level,
pos,
tag.getLong(TAG_STARTED_AT)
)
var notes = Composer(
noteRandom = Random(noteSeed),
rhythmRandom = Random(rhythmSeed),
instrument = level.getBlockState(pos).getValue(BlockStateProperties.NOTEBLOCK_INSTRUMENT)
).composeMusic().toMutableList()
val totalTime = notes.sumOf { it.time }.toLong()
var remainingTime = totalTime
val isEnd get() = remainingTime <= 0 || notes.isEmpty()
var noteTime = 0
init {
val currentRemaining = totalTime - (level.gameTime - startedAt)
while (remainingTime > currentRemaining && !isEnd) {
// seek to current position
remainingTime -= fetchNote().time
}
}
private fun fetchNote() = notes.removeFirst()
fun tick() {
if (isEnd)
return
if (noteTime <= 0) {
// start new note
val note = fetchNote()
remainingTime -= note.time
noteTime = note.time
if (level.isClientSide) {
// play note
val projections = Projector.findNearbyProjections(level, pos, MusicProjection.effect.get())
.takeIf { it.isNotEmpty() } ?: listOf(MusicProjectionEffect())
val volume = 3.0f * projections.maxOf { it.volumeFactor } * note.volume
val particle = projections.any { it.particle }
val instrument = level.getBlockState(pos).getValue(BlockStateProperties.NOTEBLOCK_INSTRUMENT)
val pitch = if (instrument.isTunable) {
NoteBlock.getPitchFromNote(note.note)
} else {
1.0f
}
val holder = if (instrument.hasCustomSound()) {
val entity = level.getBlockEntity(pos.below())
(entity as? SkullBlockEntity)?.noteBlockSound?.let {
Holder.direct(SoundEvent.createVariableRangeEvent(it))
}
} else {
null
} ?: instrument.soundEvent
if (particle) {
level.addParticle(
ParticleTypes.NOTE,
pos.x.toDouble() + 0.5,
pos.y.toDouble() + 1.2,
pos.z.toDouble() + 0.5,
note.time.toDouble() / 24.0,
0.0,
0.0
)
}
val instance = SimpleSoundInstance(
holder.value().location,
SoundSource.RECORDS,
volume,
pitch,
RandomSource.create(level.random.nextLong()),
false, 0, SoundInstance.Attenuation.LINEAR,
pos.x.toDouble() + 0.5,
pos.y.toDouble() + 0.5,
pos.z.toDouble() + 0.5,
false
)
GameInstance.getClient().soundManager.play(instance)
}
}
noteTime--
}
fun toTag() = CompoundTag().apply {
putLong(TAG_NOTE_SEED, noteSeed)
putLong(TAG_RHYTHM_SEED, rhythmSeed)
putLong(TAG_STARTED_AT, startedAt)
}
}

View File

@ -0,0 +1,78 @@
package quaedam.projection.music
import net.minecraft.nbt.CompoundTag
import net.minecraft.world.item.BlockItem
import net.minecraft.world.item.Item
import quaedam.Quaedam
import quaedam.projection.EntityProjectionBlock
import quaedam.projection.ProjectionEffect
import quaedam.projection.ProjectionEffectType
import quaedam.projection.SimpleProjectionEntity
import quaedam.shell.ProjectionEffectShell
import quaedam.shell.buildProjectionEffectShell
import kotlin.math.min
object MusicProjection {
const val ID = "music_projection"
const val SHORT_ID = "music"
val block = Quaedam.blocks.register(ID) { MusicProjectionBlock }!!
val item = Quaedam.items.register(ID) {
BlockItem(
MusicProjectionBlock, Item.Properties()
.`arch$tab`(Quaedam.creativeModeTab)
)
}!!
val effect = Quaedam.projectionEffects.register(SHORT_ID) {
ProjectionEffectType { MusicProjectionEffect() }
}!!
val blockEntity = Quaedam.blockEntities.register(ID) {
SimpleProjectionEntity.createBlockEntityType(block) { MusicProjectionEffect() }
}!!
init {
SmartInstrument
}
}
object MusicProjectionBlock : EntityProjectionBlock<MusicProjectionEffect>(createProperties().lightLevel { 3 }) {
override val blockEntity = MusicProjection.blockEntity
}
data class MusicProjectionEffect(var volumeFactor: Float = 1.0f, var particle: Boolean = true) : ProjectionEffect(),
ProjectionEffectShell.Provider {
companion object {
const val TAG_VOLUME_FACTOR = "VolumeFactor"
const val TAG_PARTICLE = "Particle"
}
override val type
get() = MusicProjection.effect.get()!!
override fun toNbt(tag: CompoundTag) {
tag.putFloat(TAG_VOLUME_FACTOR, volumeFactor)
tag.putBoolean(TAG_PARTICLE, particle)
}
override fun fromNbt(tag: CompoundTag, trusted: Boolean) {
volumeFactor = tag.getFloat(TAG_VOLUME_FACTOR)
particle = tag.getBoolean(TAG_PARTICLE)
if (!trusted) {
volumeFactor = min(volumeFactor, 5.0f)
}
}
override fun createShell() = buildProjectionEffectShell(this) {
floatSlider("quaedam.shell.music.volume_factor", ::volumeFactor, 0.0f..5.0f, 0.1f)
boolean("quaedam.shell.music.particle", ::particle)
}
}

View File

@ -0,0 +1,236 @@
package quaedam.projection.music
import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.BlockItem
import net.minecraft.world.item.Item
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument
import net.minecraft.world.level.material.MapColor
import net.minecraft.world.phys.BlockHitResult
import quaedam.Quaedam
import quaedam.misc.causality.CausalityAnchor
import quaedam.projector.Projector
import quaedam.utils.getChunksNearby
import quaedam.utils.sendBlockUpdated
object SmartInstrument {
const val ID = "smart_instrument"
val block = Quaedam.blocks.register(ID) { SmartInstrumentBlock }!!
val item = Quaedam.items.register(ID) {
BlockItem(
SmartInstrumentBlock, Item.Properties()
.`arch$tab`(Quaedam.creativeModeTab)
)
}!!
val blockEntity = Quaedam.blockEntities.register(ID) {
BlockEntityType.Builder.of(::SmartInstrumentBlockEntity, block.get()).build(null)
}!!
}
object SmartInstrumentBlock : Block(
Properties.of()
.strength(2.7f)
.requiresCorrectToolForDrops()
.mapColor(MapColor.COLOR_BROWN)
.randomTicks()
), EntityBlock {
init {
registerDefaultState(
defaultBlockState()
.setValue(BlockStateProperties.NOTEBLOCK_INSTRUMENT, NoteBlockInstrument.HARP)
)
}
override fun newBlockEntity(pos: BlockPos, state: BlockState) = SmartInstrumentBlockEntity(pos, state)
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
super.createBlockStateDefinition(builder)
builder.add(BlockStateProperties.NOTEBLOCK_INSTRUMENT)
}
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
override fun neighborChanged(
state: BlockState,
level: Level,
pos: BlockPos,
neighborBlock: Block,
neighborPos: BlockPos,
movedByPiston: Boolean
) {
super.neighborChanged(state, level, pos, neighborBlock, neighborPos, movedByPiston)
level.setBlock(
pos,
state.setValue(BlockStateProperties.NOTEBLOCK_INSTRUMENT, level.getBlockState(pos.below()).instrument()),
UPDATE_ALL
)
}
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
override fun onPlace(state: BlockState, level: Level, pos: BlockPos, oldState: BlockState, movedByPiston: Boolean) {
super.onPlace(state, level, pos, oldState, movedByPiston)
level.setBlock(
pos,
state.setValue(BlockStateProperties.NOTEBLOCK_INSTRUMENT, level.getBlockState(pos.below()).instrument()),
UPDATE_ALL
)
}
@Suppress("OVERRIDE_DEPRECATION")
override fun randomTick(
state: BlockState,
level: ServerLevel,
pos: BlockPos,
random: RandomSource
) {
if (Projector.findNearbyProjections(level, pos, MusicProjection.effect.get()).isNotEmpty()) {
val entity = level.getBlockEntity(pos) as SmartInstrumentBlockEntity
if (entity.player == null) {
entity.startMusic()
}
}
}
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
override fun use(
state: BlockState,
level: Level,
pos: BlockPos,
player: Player,
hand: InteractionHand,
hit: BlockHitResult
): InteractionResult {
if (Projector.findNearbyProjections(level, pos, MusicProjection.effect.get()).isNotEmpty()
|| CausalityAnchor.checkEffect(level, pos)
) {
val entity = level.getBlockEntity(pos) as SmartInstrumentBlockEntity
if (entity.player == null) {
entity.startMusic()
}
return InteractionResult.SUCCESS
}
return super.use(state, level, pos, player, hand, hit)
}
override fun <T : BlockEntity?> getTicker(
level: Level,
state: BlockState,
blockEntityType: BlockEntityType<T>
): BlockEntityTicker<T> {
return BlockEntityTicker { _, _, _, entity ->
(entity as? SmartInstrumentBlockEntity)?.tick()
}
}
}
class SmartInstrumentBlockEntity(pos: BlockPos, state: BlockState) :
BlockEntity(SmartInstrument.blockEntity.get(), pos, state) {
companion object {
const val TAG_MUSIC = "Music"
}
// delay MusicPlayer initialization until level is available
var playerData: CompoundTag? = null
var player: MusicPlayer? = null
override fun getUpdateTag(): CompoundTag = saveWithoutMetadata()
override fun getUpdatePacket(): Packet<ClientGamePacketListener> = ClientboundBlockEntityDataPacket.create(this)
override fun load(tag: CompoundTag) {
super.load(tag)
if (TAG_MUSIC in tag) {
try {
player = MusicPlayer(tag.getCompound(TAG_MUSIC), level!!, blockPos)
} catch (e: Throwable) {
playerData = tag.getCompound(TAG_MUSIC)
}
}
}
override fun saveAdditional(tag: CompoundTag) {
super.saveAdditional(tag)
if (playerData != null) {
tag.put(TAG_MUSIC, playerData!!)
}
if (player != null) {
tag.put(TAG_MUSIC, player!!.toTag())
}
}
private fun checkProjections() =
Projector.findNearbyProjections(level!!, blockPos, MusicProjection.effect.get()).isNotEmpty()
|| CausalityAnchor.checkEffect(level!!, blockPos)
fun startMusic(force: Boolean = false, synced: Boolean = false) {
if ((player == null || force) && !level!!.isClientSide && checkProjections()) {
player = MusicPlayer(level!!.random.nextLong(), level!!.gameTime / 20, level!!, blockPos)
setChanged()
sendBlockUpdated()
if (!synced) {
// sync start to other instruments
level!!.getChunksNearby(blockPos, 1)
.flatMap {
it.blockEntities
.filterValues { entity -> entity is SmartInstrumentBlockEntity }
.filterKeys { pos -> pos.distSqr(blockPos) < 100 }
.values
}
.filterNot { it == this }
.filterIsInstance<SmartInstrumentBlockEntity>()
.forEach { it.startMusic(force = true, synced = true) }
}
}
}
fun tick() {
if (playerData != null) {
player = MusicPlayer(playerData!!, level!!, blockPos)
playerData = null
}
if (player != null) {
if (checkProjections()) {
player!!.tick()
if (!level!!.isClientSide) {
if (player!!.isEnd) {
player = null
setChanged()
sendBlockUpdated()
if (CausalityAnchor.checkEffect(level!!, blockPos) || level!!.random.nextInt(7) != 0) {
startMusic()
}
}
}
} else {
player = null
setChanged()
sendBlockUpdated()
}
}
}
}

View File

@ -28,9 +28,9 @@ class ProjectionEffectShell(val effect: ProjectionEffect) {
fun text(key: String, value: Component) = row(key) { StringWidget(value, it.font) }
fun doubleSlider(key: String, property: KMutableProperty0<Double>, range: ClosedRange<Double>, step: Double) {
fun doubleSlider(key: String, property: KMutableProperty0<Double>, range: ClosedRange<Double>, rawStep: Double) {
val len = range.endInclusive - range.start
val step = step / len
val step = rawStep / len
row(key) {
object : AbstractSliderButton(
0, 0, width, height,
@ -53,6 +53,35 @@ class ProjectionEffectShell(val effect: ProjectionEffect) {
}
}
fun floatSlider(key: String, property: KMutableProperty0<Float>, range: ClosedRange<Float>, rawStep: Float) {
val len = range.endInclusive - range.start
val step = rawStep / len
row(key) {
object : AbstractSliderButton(
0,
0,
width,
height,
Component.literal(String.format("%.2f", property.get())),
(property.get() - range.start) / len.toDouble()
) {
override fun updateMessage() {
message = Component.literal(String.format("%.2f", property.get()))
}
override fun applyValue() {
val diff = value % step
if (diff < step * 0.5) {
value -= diff
} else {
value += (step - diff)
}
property.set(range.start + (value.toFloat() * len))
}
}
}
}
fun intSlider(key: String, property: KMutableProperty0<Int>, range: IntProgression) {
val len = range.last - range.first
val step = range.step.toDouble() / len
@ -80,9 +109,25 @@ class ProjectionEffectShell(val effect: ProjectionEffect) {
fun intCycle(key: String, property: KMutableProperty0<Int>, range: IntProgression) =
row(key) {
CycleButton.builder<Int> { Component.literal(it.toString()) }
CycleButton.builder<Int> {
property.set(it)
Component.literal(it.toString())
}
.displayOnlyValue()
.withValues(range.toList())
.withInitialValue(property.get())
.create(0, 0, width, height, Component.translatable(key))
}
fun boolean(key: String, property: KMutableProperty0<Boolean>) =
row(key) {
CycleButton.builder<Boolean> {
property.set(it)
Component.translatable("$key.$it")
}
.displayOnlyValue()
.withValues(listOf(true, false))
.withInitialValue(property.get())
.create(0, 0, width, height, Component.translatable(key))
}

View File

@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "quaedam:block/music_projection"
}
}
}

View File

@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "quaedam:block/smart_instrument"
}
}
}

View File

@ -5,6 +5,8 @@
"block.quaedam.swarm_projection": "Swarm Projection",
"block.quaedam.sound_projection": "Sound Projection",
"block.quaedam.noise_projection": "Noise Projection",
"block.quaedam.music_projection": "Music Projection",
"block.quaedam.smart_instrument": "Smart Instrument",
"block.quaedam.causality_anchor": "Causality Anchor",
"block.quaedam.reality_stabler": "Reality Stabler",
"entity.quaedam.projected_person": "Virtual Person",
@ -19,5 +21,9 @@
"quaedam.shell.noise.rate": "Rate",
"quaedam.shell.noise.amount": "Amount",
"quaedam.shell.swarm.max_count": "Max Count",
"quaedam.shell.sound.rate": "Rate"
"quaedam.shell.sound.rate": "Rate",
"quaedam.shell.music.volume_factor": "Volume Factor",
"quaedam.shell.music.particle": "Particle",
"quaedam.shell.music.particle.true": "Display",
"quaedam.shell.music.particle.false": "Hidden"
}

View File

@ -5,8 +5,10 @@
"block.quaedam.swarm_projection": "人群投影",
"block.quaedam.sound_projection": "声音投影",
"block.quaedam.noise_projection": "噪音投影",
"block.quaedam.music_projection": "音乐投影",
"block.quaedam.causality_anchor": "因果锚",
"block.quaedam.reality_stabler": "现实稳定器",
"block.quaedam.smart_instrument": "智能乐器",
"entity.quaedam.projected_person": "虚拟个体",
"item.quaedam.projection_shell": "投影操作面板",
"quaedam.screen.projection_shell": "投影操作",
@ -19,5 +21,9 @@
"quaedam.shell.noise.rate": "速率",
"quaedam.shell.noise.amount": "数量",
"quaedam.shell.swarm.max_count": "最大数量",
"quaedam.shell.sound.rate": "速率"
"quaedam.shell.sound.rate": "速率",
"quaedam.shell.music.volume_factor": "响度因子",
"quaedam.shell.music.particle": "粒子效果",
"quaedam.shell.music.particle.true": "显示",
"quaedam.shell.music.particle.false": "隐藏"
}

View File

@ -0,0 +1,6 @@
{
"parent": "quaedam:block/projection",
"textures": {
"ext": "quaedam:block/music_projection"
}
}

View File

@ -0,0 +1,3 @@
{
"parent": "minecraft:block/note_block"
}

View File

@ -0,0 +1,3 @@
{
"parent": "quaedam:block/music_projection"
}

View File

@ -0,0 +1,3 @@
{
"parent": "quaedam:block/smart_instrument"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

View File

@ -3,6 +3,7 @@
"quaedam:noise_projection",
"quaedam:sound_projection",
"quaedam:skylight_projection",
"quaedam:swarm_projection"
"quaedam:swarm_projection",
"quaedam:music_projection"
]
}