onEvent('item.right_click', event => {
  if (event.item.id !== 'minecraft:stick') return
  let player = event.player
  let level = event.level

  // find nearest mca villager
  let entities = level.getEntities()
  let nearest = null
  let nearestDist = 999999

  entities.forEach(entity => {
    if (entity.type !== 'mca:female_villager' && entity.type !== 'mca:male_villager') return
    let dx = entity.x - player.x
    let dy = entity.y - player.y
    let dz = entity.z - player.z
    let dist = Math.sqrt(dx*dx + dy*dy + dz*dz)
    if (dist < nearestDist) {
      nearestDist = dist
      nearest = entity
    }
  })

  if (!nearest) { player.tell('§cno mca villager nearby'); return }

  player.tell('§6=== MCA METHOD DUMP: §f' + nearest.fullNBT.getString('villagerName') + ' §6===')

  // dump all methods on the minecraft entity
  let methods = nearest.minecraftEntity.getClass().getMethods()
  let methodNames = []
  for (let i = 0; i < methods.length; i++) {
    methodNames.push(methods[i].getName())
  }
  methodNames.sort()

  // filter for anything that sounds mca related
  let mcaMethods = []
  let memoryMethods = []
  let heartMethods = []
  let relationMethods = []

  methodNames.forEach(name => {
    let lower = name.toLowerCase()
    if (lower.includes('mca') || lower.includes('villager')) mcaMethods.push(name)
    if (lower.includes('memory') || lower.includes('mem')) memoryMethods.push(name)
    if (lower.includes('heart') || lower.includes('relation') || lower.includes('trust')) heartMethods.push(name)
    if (lower.includes('relation') || lower.includes('family') || lower.includes('spouse') || lower.includes('partner')) relationMethods.push(name)
  })

  player.tell('§bmca/villager methods: §f' + (mcaMethods.length > 0 ? mcaMethods.join(', ') : 'none'))
  player.tell('§bmemory methods: §f' + (memoryMethods.length > 0 ? memoryMethods.join(', ') : 'none'))
  player.tell('§bheart/relation methods: §f' + (heartMethods.length > 0 ? heartMethods.join(', ') : 'none'))
  player.tell('§bfamily/spouse methods: §f' + (relationMethods.length > 0 ? relationMethods.join(', ') : 'none'))

  // also dump all fields
  let fields = nearest.minecraftEntity.getClass().getDeclaredFields()
  let fieldNames = []
  for (let i = 0; i < fields.length; i++) {
    fieldNames.push(fields[i].getName())
  }
  fieldNames.sort()

  let mcaFields = []
  fieldNames.forEach(name => {
    let lower = name.toLowerCase()
    if (lower.includes('heart') || lower.includes('memory') || lower.includes('relation') || 
        lower.includes('spouse') || lower.includes('family') || lower.includes('trust') ||
        lower.includes('mca') || lower.includes('player')) {
      mcaFields.push(name)
    }
  })

  player.tell('§dinteresting fields: §f' + (mcaFields.length > 0 ? mcaFields.join(', ') : 'none'))
  player.tell('§7all fields: §f' + fieldNames.join(', '))

  // ============================================================
  // RELATIONSHIPS — forge.net.mca.entity.ai.BreedableRelationship
  // confirmed working: isMarried, isEngaged, isPromised, isProcreating,
  // getRelationshipState (returns enum string), getGender, getPartnerName,
  // getPartnerUUID (both return Optional - need .isPresent() / .get())
  // ============================================================
  let rel = nearest.minecraftEntity.getRelationships()
  player.tell('§6=== RELATIONSHIPS ===')
  player.tell('§bstate: §f' + rel.getRelationshipState())
  player.tell('§bgender: §f' + rel.getGender())
  player.tell('§bisMarried: §f' + rel.isMarried())

  let partnerName = rel.getPartnerName()
  let partnerUUID = rel.getPartnerUUID()
  let partner = rel.getPartner()

  player.tell('§bpartnerName present: §f' + partnerName.isPresent())
  if (partnerName.isPresent()) {
    // .get() returns a TextComponent - need .getString() for raw string
    player.tell('§bpartnerName getString: §f' + partnerName.get().getString())
  }

  player.tell('§bpartnerUUID present: §f' + partnerUUID.isPresent())
  if (partnerUUID.isPresent()) {
    let uuid = partnerUUID.get()
    player.tell('§bpartnerUUID: §f' + uuid)
    // try to look up the player from the UUID
    let partnerPlayer = event.server.getPlayer(uuid.toString())
    if (partnerPlayer) {
      player.tell('§bpartner is online: §f' + partnerPlayer.name.getString())
    } else {
      player.tell('§bpartner is offline')
    }
  }

  player.tell('§bpartner present: §f' + partner.isPresent())
  if (partner.isPresent()) {
    let partnerEntity = partner.get()
    player.tell('§bpartner entity class: §f' + partnerEntity.getClass().getName())
    // dump methods to see what we can call on the live player object
    let pMethods = partnerEntity.getClass().getMethods()
    let pMethodNames = []
    for (let i = 0; i < pMethods.length; i++) { pMethodNames.push(pMethods[i].getName()) }
    pMethodNames.sort()
    player.tell('§bpartner entity methods (filtered): §f' + pMethodNames.filter(n => n.toLowerCase().includes('name') || n.toLowerCase().includes('uuid') || n.toLowerCase().includes('string')).join(', '))
  }

  // family tree
  let familyTree = rel.getFamilyTree()
  player.tell('§6=== FAMILY TREE ===')
  let villagerUUID = nearest.minecraftEntity.getUUID()
  player.tell('§bvillager UUID: §f' + villagerUUID)
  let ownEntry = familyTree.getOrEmpty(villagerUUID)
  player.tell('§bownEntry present: §f' + ownEntry.isPresent())
  if (ownEntry.isPresent()) {
    let entry = ownEntry.get()
    player.tell('§bownEntry class: §f' + entry.getClass().getName())

    // confirmed methods on FamilyTreeNode:
    // addChild, assignParent, assignParents, children, father, gender
    // getAllRelatives, getChildren, getName, getParents, getProfession
    // getRelationshipState, getRelatives, getRoot, getSiblings
    // id, isDeceased, isGrandParent, isParent, isPlayer, isRelative
    // isUncle, isValid, lookup, mother, partner, probablyGenerated
    // removeFather, removeMother, save, setDeceased, setFather, setGender
    // setMother, setName, setProfession, setRelationshipState, siblings
    // streamChildren, streamParents, updatePartner, willBeRemembered

    player.tell('§bnode name: §f' + entry.getName())
    player.tell('§bnode gender: §f' + entry.gender())
    player.tell('§bnode id: §f' + entry.id())
    player.tell('§bnode isPlayer: §f' + entry.isPlayer())
    player.tell('§bnode isDeceased: §f' + entry.isDeceased())
    player.tell('§bnode isValid: §f' + entry.isValid())
    player.tell('§bnode relationshipState: §f' + entry.getRelationshipState())
    player.tell('§bnode partner: §f' + entry.partner())

    // father and mother return Optional UUIDs
    let fatherOpt = entry.father()
    let motherOpt = entry.mother()
    player.tell('§bfather present: §f' + fatherOpt.isPresent())
    if (fatherOpt.isPresent()) player.tell('§bfather UUID: §f' + fatherOpt.get())
    player.tell('§bmother present: §f' + motherOpt.isPresent())
    if (motherOpt.isPresent()) player.tell('§bmother UUID: §f' + motherOpt.get())

    // children
    let children = entry.getChildren()
    player.tell('§bchildren class: §f' + children.getClass().getName())
    player.tell('§bchildren toString: §f' + children.toString())
  }

  // ============================================================
  // LONG TERM MEMORY — forge.net.mca.entity.ai.LongTermMemory
  // raw memories field is a java.util.HashMap
  // key format confirmed: "seen.{playerUUID}" -> long value
  // ============================================================
  let mem = nearest.minecraftEntity.getLongTermMemory()
  player.tell('§6=== LONG TERM MEMORY ===')

  let memoriesField = mem.getClass().getDeclaredField('memories')
  memoriesField.setAccessible(true)
  let memoriesMap = memoriesField.get(mem)

  // iterate the map entries
  let entrySet = memoriesMap.entrySet()
  let iter = entrySet.iterator()
  while (iter.hasNext()) {
    let mapEntry = iter.next()
    player.tell('§b' + mapEntry.getKey() + ': §f' + mapEntry.getValue())
  }

  // also test getMemory and hasMemory with known key format
  let testKey = 'seen.' + rel.getPartnerUUID().get().toString()
  player.tell('§btesting key: §f' + testKey)
  player.tell('§bhasMemory: §f' + mem.hasMemory(testKey))
  if (mem.hasMemory(testKey)) {
    player.tell('§bgetMemory: §f' + mem.getMemory(testKey))
  }
})



// // DIAMOND — right click any entity
// // Goal: find every NBT write path available on a live entity, then attempt
// //        a full NBT restore via reflection (mimicking what the nether portal
// //        transition does internally via load() / lazyData).
// //
// // Outputs three console sections:
// //   [nbt_hunt] NBT METHODS   — every method across the full class hierarchy
// //   [nbt_hunt] NBT FIELDS    — every CompoundTag/NBT field across the hierarchy
// //   [nbt_hunt] LOAD ATTEMPT  — result of calling m_20086_(load) via reflection

// onEvent('item.entity_interact', event => {
//   if (event.item.id !== 'minecraft:diamond') return
//   event.cancel()

//   let player = event.player
//   let entity = event.target
//   let mc = entity.minecraftEntity

//   player.tell('§6=== NBT HUNT: §f' + entity.type + ' §6=== see console')

//   // ── 1. hunt NBT-related methods across the full class hierarchy ──────────
//   try {
//     let nbtMethods = []
//     let cls = mc.getClass()
//     while (cls !== null) {
//       cls.getDeclaredMethods().forEach(m => {
//         let name = m.getName().toLowerCase()
//         if (name.includes('nbt') || name.includes('save') || name.includes('load') ||
//             name.includes('read') || name.includes('write') || name.includes('serial') ||
//             name.includes('deserial') || name.includes('compound') || name.includes('tag')) {
//           let params = []
//           m.getParameterTypes().forEach(p => params.push(p.getName()))
//           nbtMethods.push(m.getName() + '(' + params.join(',') + ') [in ' + cls.getSimpleName() + ']')
//         }
//       })
//       cls = cls.getSuperclass()
//     }
//     nbtMethods.sort()
//     console.info('[nbt_hunt] NBT METHODS (' + nbtMethods.length + '): ' + nbtMethods.join(' | '))
//     player.tell('§7nbt methods: §f' + nbtMethods.length + ' found')
//   } catch (e) {
//     console.info('[nbt_hunt] method hunt failed: ' + e)
//   }

//   // ── 2. hunt NBT-related fields across the full class hierarchy ───────────
//   try {
//     let nbtFields = []
//     let cls = mc.getClass()
//     while (cls !== null) {
//       cls.getDeclaredFields().forEach(f => {
//         let typeName = f.getType().getSimpleName().toLowerCase()
//         let fieldName = f.getName().toLowerCase()
//         if (typeName.includes('compound') || typeName.includes('nbt') || typeName.includes('tag') ||
//             fieldName.includes('nbt') || fieldName.includes('tag') || fieldName.includes('data') ||
//             fieldName.includes('lazy') || fieldName.includes('persist')) {
//           nbtFields.push(f.getName() + ':' + f.getType().getSimpleName() + ' [in ' + cls.getSimpleName() + ']')
//         }
//       })
//       cls = cls.getSuperclass()
//     }
//     nbtFields.sort()
//     console.info('[nbt_hunt] NBT FIELDS (' + nbtFields.length + '): ' + nbtFields.join(' | '))
//     player.tell('§7nbt fields: §f' + nbtFields.length + ' found')
//   } catch (e) {
//     console.info('[nbt_hunt] field hunt failed: ' + e)
//   }

//   // ── 3. full portal-style NBT restore: load() + deserializeCaps() ─────────
//   // NO try/catch — KubeJS Rhino swallows them silently.
//   // CompoundTag class is pulled from m_20086_'s own parameter type declaration
//   // rather than Class.forName(), so we never need a class loader reference.
//   //
//   // Portal sequence:
//   //   1. m_20086_(CompoundTag)        — Entity.load()      — entity data + KubeJS persistentDataKJS hook
//   //   2. deserializeCaps(CompoundTag) — CapabilityProvider — Forge capability data

//   player.tell('§7section 3 starting...')
//   console.info('[nbt_hunt] section 3 starting')

//   let savedNBT = entity.fullNBT.copy()
//   player.tell('§7savedNBT keys: §f' + savedNBT.getAllKeys().size())

//   // ── get CompoundTag class from m_20086_'s own declared parameter type ─────
//   // Walk hierarchy scanning ALL declared methods, collect every one named m_20086_,
//   // then grab the first parameter type. This avoids Class.forName entirely.
//   let CompoundTagClass = null
//   let scanCls = mc.getClass()
//   while (scanCls !== null && CompoundTagClass === null) {
//     let methods = scanCls.getDeclaredMethods()
//     for (let i = 0; i < methods.length; i++) {
//       if (methods[i].getName() === 'm_20086_') {
//         let params = methods[i].getParameterTypes()
//         if (params.length === 1) {
//           CompoundTagClass = params[0]
//           console.info('[nbt_hunt] CompoundTagClass resolved from m_20086_ param: ' + CompoundTagClass.getName())
//           player.tell('§7CompoundTagClass: §f' + CompoundTagClass.getName())
//           break
//         }
//       }
//     }
//     scanCls = scanCls.getSuperclass()
//   }

//   if (CompoundTagClass === null) {
//     player.tell('§cCould not resolve CompoundTagClass — aborting')
//     console.info('[nbt_hunt] ABORT: CompoundTagClass is null')
//     return
//   }

//   // strip fields load() would overwrite with stale values
//   savedNBT.remove('Pos')
//   savedNBT.remove('UUID')
//   savedNBT.remove('Motion')
//   savedNBT.remove('Rotation')

//   // ── step 1: load() via m_20086_ ──────────────────────────────────────────
//   let loadMethod = null
//   let cls1 = mc.getClass()
//   while (cls1 !== null && loadMethod === null) {
//     let methods = cls1.getDeclaredMethods()
//     for (let i = 0; i < methods.length; i++) {
//       let m = methods[i]
//       let params = m.getParameterTypes()
//       if (m.getName() === 'm_20086_' && params.length === 1 && params[0] === CompoundTagClass) {
//         m.setAccessible(true)
//         loadMethod = m
//         break
//       }
//     }
//     cls1 = cls1.getSuperclass()
//   }

//   console.info('[nbt_hunt] loadMethod: ' + (loadMethod !== null ? loadMethod.getDeclaringClass().getSimpleName() : 'NOT FOUND'))
//   player.tell('§7loadMethod: §f' + (loadMethod !== null ? loadMethod.getDeclaringClass().getSimpleName() : 'NOT FOUND'))

//   if (loadMethod !== null) {
//     loadMethod.invoke(mc, savedNBT)
//     let kjsKeys = entity.persistentData.getAllKeys()
//     console.info('[nbt_hunt] STEP 1 done — persistentData keys: ' + kjsKeys)
//     player.tell('§aSTEP 1 load() ok — persistentData keys: §f' + kjsKeys)
//   }

//   // ── step 2: deserializeCaps() ────────────────────────────────────────────
//   let deserMethod = null
//   let cls2 = mc.getClass()
//   while (cls2 !== null && deserMethod === null) {
//     let methods = cls2.getDeclaredMethods()
//     for (let i = 0; i < methods.length; i++) {
//       let m = methods[i]
//       let params = m.getParameterTypes()
//       if (m.getName() === 'deserializeCaps' && params.length === 1 && params[0] === CompoundTagClass) {
//         m.setAccessible(true)
//         deserMethod = m
//         break
//       }
//     }
//     cls2 = cls2.getSuperclass()
//   }

//   console.info('[nbt_hunt] deserMethod: ' + (deserMethod !== null ? deserMethod.getDeclaringClass().getSimpleName() : 'NOT FOUND'))
//   player.tell('§7deserMethod: §f' + (deserMethod !== null ? deserMethod.getDeclaringClass().getSimpleName() : 'NOT FOUND'))

//   if (deserMethod !== null) {
//     deserMethod.invoke(mc, savedNBT)
//     console.info('[nbt_hunt] STEP 2 done — caps deserialized')
//     player.tell('§aSTEP 2 deserializeCaps() ok')
//   }

//   // ── verification ─────────────────────────────────────────────────────────
//   let postNBT = entity.fullNBT
//   let kjsPost = postNBT.getCompound('KubeJSPersistentData')
//   console.info('[nbt_hunt] POST-RESTORE NBT keys: ' + postNBT.getAllKeys().size())
//   console.info('[nbt_hunt] POST-RESTORE KubeJSPersistentData: ' + kjsPost)
//   player.tell('§7post-restore NBT keys: §f' + postNBT.getAllKeys().size())
//   player.tell('§7KubeJSPersistentData: §f' + kjsPost.getAllKeys())

//   player.tell('§7done — check console')
// })







// // ── DIAMOND: cross-dim round trip test ───────────────────────────────────────
// // 1. Writes test values to persistentData
// // 2. Snapshots fullNBT immediately
// // 3. Teleports entity to the other dimension
// // 4. Polls until it arrives, then teleports back
// // 5. Polls until it returns, then calls fullNBTRestore()
// // 6. Reports KubeJSPersistentData keys pre and post restore
// // 7. Monitors 3 ticks to check for rollback

// onEvent('item.entity_interact', event => {
//   if (event.item.id !== 'minecraft:diamond') return
//   event.cancel()

//   let player = event.player
//   let server = event.server
//   let entity = event.target

//   let entityName = entity.type
//   if (entity.fullNBT.contains('villagerName'))
//     entityName = entity.fullNBT.getString('villagerName') + ' (' + entity.type + ')'
//   else if (entity.fullNBT.contains('CustomName'))
//     entityName = entity.fullNBT.getString('CustomName') + ' (' + entity.type + ')'

//   player.tell('§6=== CROSS-DIM NBT TEST: §f' + entityName + ' §6===')
//   player.tell('§7tip: use §fCOBBLESTONE§7 first to write test values, then DIAMOND')

//   // ── snapshot immediately ──────────────────────────────────────────────────
//   let savedNBT = entity.fullNBT.copy()
//   let kjsSnapshot = savedNBT.getCompound('KubeJSPersistentData')
//   console.info('[nbt_test] SNAPSHOT: ' + String(savedNBT))
//   player.tell('§7[tick 0] snapshot — §f' + savedNBT.getAllKeys().size() + ' §7keys, KubeJSPersistentData: §f' + kjsSnapshot.getAllKeys())

//   let entityId = String(entity.id)
//   let uuidArr = entity.fullNBT.getIntArray('UUID')
//   let entityUUID = uuidArr.length === 4
//     ? uuidArr[0] + ',' + uuidArr[1] + ',' + uuidArr[2] + ',' + uuidArr[3]
//     : null

//   let currentDim = entity.level.dimension.toString()
//   let otherDim = currentDim === 'minecraft:the_nether' ? 'minecraft:overworld' : 'minecraft:the_nether'
//   let destX = player.x
//   let destY = player.y
//   let destZ = player.z

//   let destChunkX = Math.floor(destX / 16)
//   let destChunkZ = Math.floor(destZ / 16)
//   let chunkMinX = destChunkX * 16
//   let chunkMinZ = destChunkZ * 16
//   let chunkMaxX = chunkMinX + 15
//   let chunkMaxZ = chunkMinZ + 15

//   function findEntity(level, label) {
//     if (!level) return null
//     let byId = null
//     let byUUID = null
//     level.getEntities().forEach(e => {
//       if (String(e.id) === entityId) byId = e
//       if (entityUUID) {
//         let eu = e.fullNBT.getIntArray('UUID')
//         if (eu.length === 4) {
//           let euStr = eu[0] + ',' + eu[1] + ',' + eu[2] + ',' + eu[3]
//           if (euStr === entityUUID) byUUID = e
//         }
//       }
//     })
//     if (byId) return byId
//     if (byUUID) { player.tell('§e[' + label + '] matched by UUID (id changed)'); return byUUID }
//     return null
//   }

//   // ── tick 1: forceload dest chunk, then teleport to other dim ─────────────
//   server.scheduleInTicks(1, () => {
//     server.runCommandSilent('execute in ' + otherDim + ' run forceload add ' + chunkMinX + ' ' + chunkMinZ + ' ' + chunkMaxX + ' ' + chunkMaxZ)

//     let entityAtTick1 = findEntity(server.getLevel(currentDim), 'tick1-preflight')
//     if (!entityAtTick1) {
//       player.tell('§c[tick 1] entity not found before teleport — aborting')
//       return
//     }

//     player.tell('§7[tick 1] teleporting §f' + currentDim + ' §7-> §f' + otherDim)
//     entityAtTick1.teleportTo(otherDim, destX, destY, destZ, entityAtTick1.yaw, entityAtTick1.pitch)
//     pollLeg1(0)
//   })

//   // ── leg 1: poll until arrived in other dim, then teleport back ───────────
//   function pollLeg1(t) {
//     if (t >= 60) { player.tell('§c[leg 1] timeout — entity never arrived in ' + otherDim); return }
//     server.scheduleInTicks(1, () => {
//       let found = findEntity(server.getLevel(otherDim), 'leg1')
//       if (!found) { pollLeg1(t + 1); return }

//       let kjsLeg1 = found.fullNBT.getCompound('KubeJSPersistentData')
//       player.tell('§a[leg 1] arrived in §f' + otherDim + ' §aafter §f' + t + ' §atick(s)')
//       player.tell('§7[leg 1] KubeJSPersistentData after crossing: §f' + kjsLeg1.getAllKeys())
//       console.info('[nbt_test] LEG1 NBT: ' + String(found.fullNBT))

//       found.teleportTo(currentDim, destX, destY, destZ, found.yaw, found.pitch)
//       pollLeg2(0)
//     })
//   }

//   // ── leg 2: poll until back in original dim, then restore + verify ─────────
//   function pollLeg2(t) {
//     if (t >= 60) { player.tell('§c[leg 2] timeout — entity never returned to ' + currentDim); return }
//     server.scheduleInTicks(1, () => {
//       let found = findEntity(server.getLevel(currentDim), 'leg2')
//       if (!found) { pollLeg2(t + 1); return }

//       player.tell('§a[leg 2] returned to §f' + currentDim + ' §aafter §f' + t + ' §atick(s)')

//       // report state before restore
//       let preKJS = found.fullNBT.getCompound('KubeJSPersistentData')
//       console.info('[nbt_test] PRE-RESTORE NBT: ' + String(found.fullNBT))
//       player.tell('§e[pre-restore] KubeJSPersistentData: §f' + preKJS.getAllKeys())

//       // full portal-style restore
//       let ok = fullNBTRestore(found, savedNBT)
//       player.tell(ok ? '§a[restore] fullNBTRestore() ok' : '§c[restore] fullNBTRestore() FAILED — see console')

//       // report state after restore
//       let postKJS = found.fullNBT.getCompound('KubeJSPersistentData')
//       console.info('[nbt_test] POST-RESTORE NBT: ' + String(found.fullNBT))
//       player.tell('§a[post-restore] KubeJSPersistentData: §f' + postKJS.getAllKeys())
//       player.tell('§7key count — snapshot: §f' + savedNBT.getAllKeys().size() + ' §7post-restore: §f' + found.fullNBT.getAllKeys().size())

//       player.tell('§7--- monitoring 3 ticks for rollback ---')
//       monitorTick(found, 1)
//     })
//   }

//   // ── monitor 3 ticks to check nothing rolls back after restore ─────────────
//   function monitorTick(ref, tickNum) {
//     if (tickNum > 3) {
//       player.tell('§7--- done ---')
//       server.runCommandSilent('execute in ' + otherDim + ' run forceload remove ' + chunkMinX + ' ' + chunkMinZ + ' ' + chunkMaxX + ' ' + chunkMaxZ)
//       return
//     }
//     server.scheduleInTicks(1, () => {
//       let found = findEntity(server.getLevel(currentDim), 'monitor')
//       if (!found) {
//         player.tell('§c[monitor +' + tickNum + '] entity not found')
//         monitorTick(null, tickNum + 1)
//         return
//       }
//       let monKJS = found.fullNBT.getCompound('KubeJSPersistentData')
//       console.info('[nbt_test] MONITOR +' + tickNum + ' NBT: ' + String(found.fullNBT))
//       player.tell('§7[monitor +' + tickNum + '] KubeJSPersistentData: §f' + monKJS.getAllKeys())
//       monitorTick(found, tickNum + 1)
//     })
//   }

//   pollLeg1(0)
// })

// ── fullNBTRestore v2 ─────────────────────────────────────────────────────────
// Uses mergeFullNBT instead of reflection.
// Patches Pos/UUID/Motion/Rotation in the snapshot copy to live values
// so the merge is complete — nothing stripped, nothing clobbered.

function fullNBTRestore(entity, savedNBT) {
  let nbt = savedNBT.copy()

  // overwrite position-related fields in the snapshot with current live values
  // so mergeFullNBT gets a complete NBT with correct coords
  nbt.putDoubleArray('Pos', [entity.x, entity.y, entity.z])
  nbt.putDoubleArray('Motion', [0.0, 0.0, 0.0])
  nbt.putFloatArray('Rotation', [entity.yaw, entity.pitch])

  // leave UUID alone — mergeFullNBT on the live entity should not change its UUID
  // but we need it present and correct so remove the snapshot UUID and let the
  // live entity keep its own
  nbt.remove('UUID')

  entity.mergeFullNBT(nbt)
  return true
}


// ── DIAMOND: cross-dim round trip test ───────────────────────────────────────
// 1. Snapshots fullNBT immediately on interact (use COBBLESTONE first to write test values)
// 2. Teleports entity to the other dimension
// 3. Polls until it arrives, then teleports back
// 4. Polls until it returns, then calls fullNBTRestore()
// 5. Reports KubeJSPersistentData keys pre and post restore
// 6. Monitors 3 ticks to check for rollback

onEvent('item.entity_interact', event => {
  if (event.item.id !== 'minecraft:diamond') return
  event.cancel()

  let player = event.player
  let server = event.server
  let entity = event.target

  let entityName = entity.type
  if (entity.fullNBT.contains('villagerName'))
    entityName = entity.fullNBT.getString('villagerName') + ' (' + entity.type + ')'
  else if (entity.fullNBT.contains('CustomName'))
    entityName = entity.fullNBT.getString('CustomName') + ' (' + entity.type + ')'

  player.tell('§6=== CROSS-DIM NBT TEST v2: §f' + entityName + ' §6===')
  player.tell('§7tip: use §fCOBBLESTONE§7 first to write test values, then DIAMOND')

  // ── snapshot immediately ──────────────────────────────────────────────────
  let savedNBT = entity.fullNBT.copy()
  let kjsSnapshot = savedNBT.getCompound('KubeJSPersistentData')
  console.info('[nbt_test] SNAPSHOT: ' + String(savedNBT))
  player.tell('§7[tick 0] snapshot — §f' + savedNBT.getAllKeys().size() + ' §7keys, KubeJSPersistentData: §f' + kjsSnapshot.getAllKeys())

  let entityId = String(entity.id)
  let uuidArr = entity.fullNBT.getIntArray('UUID')
  let entityUUID = uuidArr.length === 4
    ? uuidArr[0] + ',' + uuidArr[1] + ',' + uuidArr[2] + ',' + uuidArr[3]
    : null

  let currentDim = entity.level.dimension.toString()
  let otherDim = currentDim === 'minecraft:the_nether' ? 'minecraft:overworld' : 'minecraft:the_nether'
  let destX = player.x
  let destY = player.y
  let destZ = player.z

  let destChunkX = Math.floor(destX / 16)
  let destChunkZ = Math.floor(destZ / 16)
  let chunkMinX = destChunkX * 16
  let chunkMinZ = destChunkZ * 16
  let chunkMaxX = chunkMinX + 15
  let chunkMaxZ = chunkMinZ + 15

  function findEntity(level, label) {
    if (!level) return null
    let byId = null
    let byUUID = null
    level.getEntities().forEach(e => {
      if (String(e.id) === entityId) byId = e
      if (entityUUID) {
        let eu = e.fullNBT.getIntArray('UUID')
        if (eu.length === 4) {
          let euStr = eu[0] + ',' + eu[1] + ',' + eu[2] + ',' + eu[3]
          if (euStr === entityUUID) byUUID = e
        }
      }
    })
    if (byId) return byId
    if (byUUID) { player.tell('§e[' + label + '] matched by UUID (id changed)'); return byUUID }
    return null
  }

  // ── tick 1: forceload dest chunk, then teleport ───────────────────────────
  server.scheduleInTicks(1, () => {
    server.runCommandSilent('execute in ' + otherDim + ' run forceload add ' + chunkMinX + ' ' + chunkMinZ + ' ' + chunkMaxX + ' ' + chunkMaxZ)

    let entityAtTick1 = findEntity(server.getLevel(currentDim), 'tick1-preflight')
    if (!entityAtTick1) {
      player.tell('§c[tick 1] entity not found before teleport — aborting')
      return
    }

    player.tell('§7[tick 1] teleporting §f' + currentDim + ' §7-> §f' + otherDim)
    entityAtTick1.teleportTo(otherDim, destX, destY, destZ, entityAtTick1.yaw, entityAtTick1.pitch)
    pollLeg1(0)
  })

  // ── leg 1: poll until arrived in other dim, then teleport back ────────────
  function pollLeg1(t) {
    if (t >= 60) { player.tell('§c[leg 1] timeout — entity never arrived in ' + otherDim); return }
    server.scheduleInTicks(1, () => {
      let found = findEntity(server.getLevel(otherDim), 'leg1')
      if (!found) { pollLeg1(t + 1); return }

      let kjsLeg1 = found.fullNBT.getCompound('KubeJSPersistentData')
      player.tell('§a[leg 1] arrived in §f' + otherDim + ' §aafter §f' + t + ' §atick(s)')
      player.tell('§7[leg 1] KubeJSPersistentData after crossing: §f' + kjsLeg1.getAllKeys())
      console.info('[nbt_test] LEG1 NBT: ' + String(found.fullNBT))

      found.teleportTo(currentDim, destX, destY, destZ, found.yaw, found.pitch)
      pollLeg2(0)
    })
  }

  // ── leg 2: poll until back, then restore + verify ─────────────────────────
  function pollLeg2(t) {
    if (t >= 60) { player.tell('§c[leg 2] timeout — entity never returned to ' + currentDim); return }
    server.scheduleInTicks(1, () => {
      let found = findEntity(server.getLevel(currentDim), 'leg2')
      if (!found) { pollLeg2(t + 1); return }

      player.tell('§a[leg 2] returned to §f' + currentDim + ' §aafter §f' + t + ' §atick(s)')

      let preKJS = found.fullNBT.getCompound('KubeJSPersistentData')
      console.info('[nbt_test] PRE-RESTORE NBT: ' + String(found.fullNBT))
      player.tell('§e[pre-restore] KubeJSPersistentData: §f' + preKJS.getAllKeys())

      // restore using mergeFullNBT with live position patched in
      let ok = fullNBTRestore(found, savedNBT)
      player.tell(ok ? '§a[restore] fullNBTRestore() ok' : '§c[restore] fullNBTRestore() FAILED')

      let postKJS = found.fullNBT.getCompound('KubeJSPersistentData')
      console.info('[nbt_test] POST-RESTORE NBT: ' + String(found.fullNBT))
      player.tell('§a[post-restore] KubeJSPersistentData: §f' + postKJS.getAllKeys())
      player.tell('§7key count — snapshot: §f' + savedNBT.getAllKeys().size() + ' §7post-restore: §f' + found.fullNBT.getAllKeys().size())

      player.tell('§7--- monitoring 3 ticks for rollback ---')
      monitorTick(found, 1)
    })
  }

  // ── monitor 3 ticks ───────────────────────────────────────────────────────
  function monitorTick(ref, tickNum) {
    if (tickNum > 3) {
      player.tell('§7--- done ---')
      server.runCommandSilent('execute in ' + otherDim + ' run forceload remove ' + chunkMinX + ' ' + chunkMinZ + ' ' + chunkMaxX + ' ' + chunkMaxZ)
      return
    }
    server.scheduleInTicks(1, () => {
      let found = findEntity(server.getLevel(currentDim), 'monitor')
      if (!found) {
        player.tell('§c[monitor +' + tickNum + '] entity not found')
        monitorTick(null, tickNum + 1)
        return
      }
      let monKJS = found.fullNBT.getCompound('KubeJSPersistentData')
      console.info('[nbt_test] MONITOR +' + tickNum + ' NBT: ' + String(found.fullNBT))
      player.tell('§7[monitor +' + tickNum + '] KubeJSPersistentData: §f' + monKJS.getAllKeys())
      monitorTick(found, tickNum + 1)
    })
  }

  pollLeg1(0)
})

// // Hold DIAMOND and right click an entity to run the full cross-dim NBT persistence test
// // Snapshot is taken immediately on interact.
// // Everything else (teleport, poll, restore) is delayed by 1 tick.
// // Prints: SNAPSHOT, PRE-RESTORE, POST-RESTORE full NBT to console for diffing.

// onEvent('item.entity_interact', event => {
//   if (event.item.id !== 'minecraft:diamond') return
//   event.cancel()

//   let player = event.player
//   let server = event.server
//   let entity = event.target

//   let entityName = entity.type
//   if (entity.fullNBT.contains('villagerName'))
//     entityName = entity.fullNBT.getString('villagerName') + ' (' + entity.type + ')'
//   else if (entity.fullNBT.contains('CustomName'))
//     entityName = entity.fullNBT.getString('CustomName') + ' (' + entity.type + ')'

//   player.tell('§6=== NBT PERSISTENCE TEST: §f' + entityName + ' §6===')

//   // ── snapshot taken immediately, before anything else ─────────────────────
//   let savedNBT = entity.fullNBT.copy()
//   console.info('[nbt_test] SNAPSHOT NBT: ' + String(savedNBT))
//   player.tell('§7[tick 0] snapshot taken — §f' + savedNBT.getAllKeys().size() + ' §7keys — see console')

//   let entityId = String(entity.id)
//   let uuidArr = entity.fullNBT.getIntArray('UUID')
//   let entityUUID = uuidArr.length === 4
//     ? uuidArr[0] + ',' + uuidArr[1] + ',' + uuidArr[2] + ',' + uuidArr[3]
//     : null

//   let currentDim = entity.level.dimension.toString()
//   let otherDim = currentDim === 'minecraft:the_nether' ? 'minecraft:overworld' : 'minecraft:the_nether'
//   let destX = player.x
//   let destY = player.y
//   let destZ = player.z

//   let destChunkX = Math.floor(destX / 16)
//   let destChunkZ = Math.floor(destZ / 16)
//   let chunkMinX = destChunkX * 16
//   let chunkMinZ = destChunkZ * 16
//   let chunkMaxX = chunkMinX + 15
//   let chunkMaxZ = chunkMinZ + 15

//   function findEntity(level, label) {
//     if (!level) return null
//     let byId = null
//     let byUUID = null
//     level.getEntities().forEach(e => {
//       if (String(e.id) === entityId) byId = e
//       if (entityUUID) {
//         let eu = e.fullNBT.getIntArray('UUID')
//         if (eu.length === 4) {
//           let euStr = eu[0] + ',' + eu[1] + ',' + eu[2] + ',' + eu[3]
//           if (euStr === entityUUID) byUUID = e
//         }
//       }
//     })
//     if (byId) return byId
//     if (byUUID) { player.tell('§e[' + label + '] matched by UUID (id changed)'); return byUUID }
//     return null
//   }

//   // ── everything from here is delayed 1 tick ───────────────────────────────
//   server.scheduleInTicks(1, () => {
//     server.runCommandSilent('execute in ' + otherDim + ' run forceload add ' + chunkMinX + ' ' + chunkMinZ + ' ' + chunkMaxX + ' ' + chunkMaxZ)

//     let entityAtTick1 = findEntity(server.getLevel(currentDim), 'tick1-preflight')
//     if (!entityAtTick1) {
//       player.tell('§c[tick 1] entity not found before teleport — aborting')
//       return
//     }

//     player.tell('§7[tick 1] §f' + currentDim + ' §7-> §f' + otherDim)
//     entityAtTick1.teleportTo(otherDim, destX, destY, destZ, entityAtTick1.yaw, entityAtTick1.pitch)

//     pollLeg1(0)
//   })

//   function pollLeg1(t) {
//     if (t >= 60) { player.tell('§c[leg 1] timeout'); return }
//     server.scheduleInTicks(1, () => {
//       let found = findEntity(server.getLevel(otherDim), 'leg1')
//       if (!found) { pollLeg1(t + 1); return }
//       player.tell('§a[leg 1] arrived after §f' + t + ' §atick(s)')
//       found.teleportTo(currentDim, destX, destY, destZ, found.yaw, found.pitch)
//       pollLeg2(0)
//     })
//   }

//   function pollLeg2(t) {
//     if (t >= 60) { player.tell('§c[leg 2] timeout'); return }
//     server.scheduleInTicks(1, () => {
//       let found = findEntity(server.getLevel(currentDim), 'leg2')
//       if (!found) { pollLeg2(t + 1); return }
//       player.tell('§a[leg 2] arrived after §f' + t + ' §atick(s)')

//       // dump pre-restore
//       console.info('[nbt_test] PRE-RESTORE NBT: ' + String(found.fullNBT))

//       // full restore
//       found.mergeFullNBT(savedNBT)
//       found.mergeFullNBT({ Pos: [found.x, found.y, found.z] })

//       // dump post-restore
//       console.info('[nbt_test] POST-RESTORE NBT: ' + String(found.fullNBT))

//       player.tell('§7key count — snapshot: §f' + savedNBT.getAllKeys().size() + ' §7post-restore: §f' + found.fullNBT.getAllKeys().size())
//       player.tell('§7--- monitoring 3 ticks ---')
//       monitorTick(found, 1)
//     })
//   }

//   function monitorTick(ref, tickNum) {
//     if (tickNum > 3) {
//       player.tell('§7--- done ---')
//       server.runCommandSilent('execute in ' + otherDim + ' run forceload remove ' + chunkMinX + ' ' + chunkMinZ + ' ' + chunkMaxX + ' ' + chunkMaxZ)
//       return
//     }
//     server.scheduleInTicks(1, () => {
//       let found = findEntity(server.getLevel(currentDim), 'monitor')
//       if (!found) { player.tell('§c[monitor +' + tickNum + '] not found'); monitorTick(null, tickNum + 1); return }
//       console.info('[nbt_test] MONITOR +' + tickNum + ' NBT: ' + String(found.fullNBT))
//       player.tell('§7[monitor +' + tickNum + '] NBT dumped to console')
//       monitorTick(found, tickNum + 1)
//     })
//   }

//   pollLeg1(0)
// })









//  NBT persistence test
// Hold COBBLESTONE and right click an MCA villager to write test values
// Hold GRAVEL and right click an MCA villager to read back and report which survived

onEvent('item.entity_interact', event => {
  if (event.item.id !== 'minecraft:cobblestone') return;

  let player = event.player;
  let villager = event.target;
  let villagerName = villager.fullNBT.getString('villagerName') || 'Unknown';
  let testValue = 9876;

  // 2. mergeFullNBT - inside ForgeData
  villager.mergeFullNBT({ ForgeData: { nbt_test_forgedata: testValue } });

  // 3. mergeFullNBT - inside KubeJSPersistentData
  villager.mergeFullNBT({ KubeJSPersistentData: { nbt_test_kubejspersistent: testValue } });

  // 4. entity.persistentData API
  villager.persistentData.putInt('nbt_test_persistentdata', testValue);

  // 5. entity.nbt
  villager.nbt.putInt('nbt_test_entitynbt', testValue);

  player.tell('§a=== WRITE COMPLETE for ' + villagerName + ' ===');
  player.tell('§72. ForgeData.nbt_test_forgedata: §f' + testValue);
  player.tell('§73. KubeJSPersistentData.nbt_test_kubejspersistent: §f' + testValue);
  player.tell('§74. persistentData.nbt_test_persistentdata: §f' + testValue);
  player.tell('§75. entity.nbt.nbt_test_entitynbt: §f' + testValue);
  player.tell('§aNow move the villager through a portal and right click with GRAVEL to read.');
});

onEvent('item.entity_interact', event => {
  if (event.item.id !== 'minecraft:gravel') return;

  let player = event.player;
  let villager = event.target;
  let villagerName = villager.fullNBT.getString('villagerName') || 'Unknown';

  // 1. top level
  let topLevel = villager.fullNBT.contains('nbt_test_toplevel')
    ? villager.fullNBT.getInt('nbt_test_toplevel')
    : 'MISSING';

  // 2. ForgeData
  let forgeData = villager.fullNBT.getCompound('ForgeData').contains('nbt_test_forgedata')
    ? villager.fullNBT.getCompound('ForgeData').getInt('nbt_test_forgedata')
    : 'MISSING';

  // 3. KubeJSPersistentData
  let kubejsPersistent = villager.fullNBT.getCompound('KubeJSPersistentData').contains('nbt_test_kubejspersistent')
    ? villager.fullNBT.getCompound('KubeJSPersistentData').getInt('nbt_test_kubejspersistent')
    : 'MISSING';

  // 4. persistentData API
  let persistentData = villager.persistentData.contains('nbt_test_persistentdata')
    ? villager.persistentData.getInt('nbt_test_persistentdata')
    : 'MISSING';

  // 5. entity.nbt
  let entityNbt = villager.nbt.contains('nbt_test_entitynbt')
    ? villager.nbt.getInt('nbt_test_entitynbt')
    : 'MISSING';

  player.tell('§e=== READ RESULTS for ' + villagerName + ' ===');
  player.tell((topLevel !== 'MISSING' ? '§a✓' : '§c✗') + ' §71. nbt_test_toplevel: §f' + topLevel);
  player.tell((forgeData !== 'MISSING' ? '§a✓' : '§c✗') + ' §72. ForgeData.nbt_test_forgedata: §f' + forgeData);
  player.tell((kubejsPersistent !== 'MISSING' ? '§a✓' : '§c✗') + ' §73. KubeJSPersistentData.nbt_test_kubejspersistent: §f' + kubejsPersistent);
  player.tell((persistentData !== 'MISSING' ? '§a✓' : '§c✗') + ' §74. persistentData.nbt_test_persistentdata: §f' + persistentData);
  player.tell((entityNbt !== 'MISSING' ? '§a✓' : '§c✗') + ' §75. entity.nbt.nbt_test_entitynbt: §f' + entityNbt);
});

