@@ -5,9 +5,12 @@ import at.petrak.hexcasting.api.casting.iota.IotaType
55import at.petrak.hexcasting.api.casting.iota.PatternIota
66import at.petrak.hexcasting.api.casting.math.HexPattern
77import at.petrak.hexcasting.api.utils.extractMedia
8+ import at.petrak.hexcasting.api.utils.getInt
89import at.petrak.hexcasting.xplat.IXplatAbstractions
910import gay.`object`.hexdebug.blocks.base.BaseContainer
11+ import gay.`object`.hexdebug.blocks.base.ContainerDataDelegate
1012import gay.`object`.hexdebug.blocks.base.ContainerDataLongDelegate
13+ import gay.`object`.hexdebug.blocks.base.ContainerDataSelectionDelegate
1114import gay.`object`.hexdebug.casting.eval.FakeCastEnv
1215import gay.`object`.hexdebug.config.HexDebugConfig
1316import gay.`object`.hexdebug.gui.splicing.SplicingTableMenu
@@ -50,20 +53,40 @@ class SplicingTableBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(
5053 index3 = SplicingTableDataSlot .MEDIA_3 .index,
5154 )
5255
56+ private var selection by ContainerDataSelectionDelegate (
57+ containerData,
58+ fromIndex = SplicingTableDataSlot .SELECTION_FROM .index,
59+ toIndex = SplicingTableDataSlot .SELECTION_TO .index,
60+ )
61+
62+ private var viewStartIndex by ContainerDataDelegate (
63+ containerData,
64+ index = SplicingTableDataSlot .VIEW_START_INDEX .index,
65+ )
66+
5367 val analogOutputSignal get() = if (! listStack.isEmpty) 15 else 0
5468
69+ // TODO: save?
5570 private val undoStack = UndoStack ()
5671
5772 override fun load (tag : CompoundTag ) {
5873 super .load(tag)
5974 ContainerHelper .loadAllItems(tag, stacks)
6075 media = tag.getLong(" media" )
76+ selection = Selection .fromRawIndices(
77+ from = tag.getInt(" selectionFrom" , - 1 ),
78+ to = tag.getInt(" selectionTo" , - 1 ),
79+ )
80+ viewStartIndex = tag.getInt(" viewStartIndex" )
6181 }
6282
6383 override fun saveAdditional (tag : CompoundTag ) {
6484 super .saveAdditional(tag)
6585 ContainerHelper .saveAllItems(tag, stacks)
6686 tag.putLong(" media" , media)
87+ tag.putInt(" selectionFrom" , selection?.from ? : - 1 )
88+ tag.putInt(" selectionTo" , selection?.to ? : - 1 )
89+ tag.putInt(" viewStartIndex" , viewStartIndex)
6790 }
6891
6992 override fun createMenu (i : Int , inventory : Inventory , player : Player ) =
@@ -77,18 +100,29 @@ class SplicingTableBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(
77100 }
78101
79102 /* * Only returns null if it fails to convert `this.level` to [ServerLevel]. */
80- private fun getData (player : ServerPlayer ? , selection : Selection ? ): SplicingTableData ? {
103+ private fun getData (player : ServerPlayer ? ): SplicingTableData ? {
81104 return SplicingTableData (
82105 player = player,
83106 level = level as ? ServerLevel ? : return null ,
84107 undoStack = undoStack,
85108 selection = selection,
109+ viewStartIndex = viewStartIndex,
86110 listHolder = IXplatAbstractions .INSTANCE .findDataHolder(listStack),
87111 clipboardHolder = IXplatAbstractions .INSTANCE .findDataHolder(clipboardStack),
88112 )
89113 }
90114
91- override fun getClientView () = getData(null , null )?.run {
115+ private fun setupUndoStack (data : SplicingTableData ) {
116+ if (undoStack.size == 0 ) {
117+ data.pushUndoState(
118+ list = Some (data.list.orEmpty()),
119+ clipboard = Some (data.clipboard),
120+ selection = Some (selection),
121+ )
122+ }
123+ }
124+
125+ override fun getClientView () = getData(null )?.run {
92126 val env = FakeCastEnv (level)
93127 SplicingTableClientView (
94128 list = list?.map { IotaClientView (it, env) },
@@ -101,9 +135,11 @@ class SplicingTableBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(
101135 }
102136
103137 override fun listStackChanged (stack : ItemStack ) {
104- // when the list item is removed, clear the undo stack
138+ // when the list item is removed, clear the undo stack, selection, and view index
105139 if (stack.isEmpty) {
106140 undoStack.clear()
141+ selection = null
142+ viewStartIndex = 0
107143 }
108144 }
109145
@@ -119,65 +155,123 @@ class SplicingTableBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(
119155 }
120156 }
121157
122- override fun runAction (action : SplicingTableAction , player : ServerPlayer ? , selection : Selection ? ): Selection ? {
123- if (media < mediaCost) return selection
124- val data = getData(player, selection) ? : return selection
125- if (undoStack.size == 0 ) {
126- data.pushUndoState(
127- list = Some (data.list.orEmpty()),
128- clipboard = Some (data.clipboard),
129- selection = Some (selection),
130- )
131- }
132- return convertAndRun(action.value, data)
158+ override fun runAction (action : SplicingTableAction , player : ServerPlayer ? ) {
159+ if (media < mediaCost) return
160+ val data = getData(player) ? : return
161+ setupUndoStack(data)
162+ convertAndRun(action.value, data)
133163 }
134164
135165 private fun <T : SplicingTableData > convertAndRun (
136166 actionValue : SplicingTableAction .Value <T >,
137167 data : SplicingTableData ,
138- ): Selection ? {
139- val converted = actionValue.convert(data) ? : return data.selection
140- return actionValue.run (converted).also { consumeMedia(converted) }
141- }
142-
143- override fun drawPattern (
144- player : ServerPlayer ? ,
145- pattern : HexPattern ,
146- index : Int ,
147- selection : Selection ?
148- ): Pair <Selection ?, ResolvedPatternType > {
149- val errorResult = selection to ResolvedPatternType .ERRORED
150- if (media < mediaCost) return errorResult
151- val data = getData(player, selection)
168+ ) {
169+ val converted = actionValue.convert(data) ? : return
170+ actionValue.run (converted)
171+ postRunAction(converted)
172+ }
173+
174+ override fun drawPattern (player : ServerPlayer ? , pattern : HexPattern , index : Int ): ResolvedPatternType {
175+ if (media < mediaCost) return ResolvedPatternType .ERRORED
176+ val data = getData(player)
152177 ?.let (ReadWriteList ::convertOrNull)
153- ? : return errorResult
154- if (undoStack.size == 0 ) {
155- data.pushUndoState(
156- list = Some (data.list),
157- clipboard = Some (data.clipboard),
158- selection = Some (selection),
159- )
160- }
161- return drawPattern(pattern, data).also { consumeMedia(data) }
178+ ? : return ResolvedPatternType .ERRORED
179+ setupUndoStack(data)
180+ val result = drawPattern(pattern, data)
181+ postRunAction(data)
182+ return result
162183 }
163184
164185 private fun drawPattern (pattern : HexPattern , data : ReadWriteList ) = data.run {
165- selection .mutableSubList(list).apply {
186+ typedSelection .mutableSubList(list).apply {
166187 clear()
167188 add(PatternIota (pattern))
168189 }
169190 if (writeList(list)) {
170191 shouldConsumeMedia = true
192+ selection = Selection .edge(typedSelection.start + 1 )
171193 pushUndoState(
172194 list = Some (list),
173- selection = Some (Selection .edge(selection.start + 1 )),
174- ) to ResolvedPatternType .ESCAPED
195+ selection = Some (selection),
196+ )
197+ ResolvedPatternType .ESCAPED
198+ } else {
199+ ResolvedPatternType .ERRORED
200+ }
201+ }
202+
203+ override fun selectIndex (player : ServerPlayer ? , index : Int , hasShiftDown : Boolean , isIota : Boolean ) {
204+ // scuffed: we don't really need the player here, but the code assumes it's present
205+ val data = getData(player)
206+ ?.let (ReadList ::convertOrNull)
207+ ? : return
208+
209+ if (isIota) {
210+ selectIota(data, index, hasShiftDown)
175211 } else {
176- selection to ResolvedPatternType . ERRORED
212+ selectEdge(data, index, hasShiftDown)
177213 }
178214 }
179215
180- private fun consumeMedia (data : SplicingTableData ) {
216+ private fun selectIota (data : ReadList , index : Int , hasShiftDown : Boolean ) {
217+ if (! data.isInRange(index)) return
218+
219+ val selection = selection
220+ this .selection = if (isOnlyIotaSelected(index)) {
221+ null
222+ } else if (hasShiftDown && selection != null ) {
223+ if (selection is Selection .Edge && index < selection.from) {
224+ Selection .of(selection.from - 1 , index)
225+ } else {
226+ Selection .of(selection.from, index)
227+ }
228+ } else {
229+ Selection .withSize(index, 1 )
230+ }
231+ }
232+
233+ private fun selectEdge (data : ReadList , index : Int , hasShiftDown : Boolean ) {
234+ if (! isEdgeInRange(data, index)) return
235+
236+ val selection = selection
237+ this .selection = if (isEdgeSelected(index)) {
238+ null
239+ } else if (hasShiftDown && selection != null ) {
240+ if (selection is Selection .Edge && index < selection.from) {
241+ Selection .of(selection.from - 1 , index)
242+ } else if (index > selection.from) {
243+ Selection .of(selection.from, index - 1 )
244+ } else {
245+ Selection .of(selection.from, index)
246+ }
247+ } else {
248+ Selection .edge(index)
249+ }
250+ }
251+
252+ private fun isOnlyIotaSelected (index : Int ) =
253+ selection?.let { it.size == 1 && it.from == index } ? : false
254+
255+ private fun isEdgeSelected (index : Int ) =
256+ selection?.let { it.start == index && it.end == null } ? : false
257+
258+ private fun isEdgeInRange (data : ReadList , index : Int ) =
259+ // cell to the right is in range
260+ data.isInRange(index)
261+ // cell to the left is in range
262+ || data.isInRange(index - 1 )
263+ // allow selecting leftmost edge of empty list
264+ || (index == 0 && data.list.size == 0 )
265+
266+ private fun postRunAction (data : SplicingTableData ) {
267+ selection = data.selection
268+
269+ // if there is no list, or the list is too short to fill the screen, set the start index to 0
270+ // otherwise, clamp the start index such that the end index stays in range
271+ val lastIndex = data.list?.lastIndex ? : 0
272+ val maxStartIndex = lastIndex - VIEW_END_INDEX_OFFSET
273+ viewStartIndex = if (maxStartIndex > 0 ) data.viewStartIndex.coerceIn(0 , maxStartIndex) else 0
274+
181275 if (data.shouldConsumeMedia) {
182276 media = (media - mediaCost).coerceIn(0 , maxMedia)
183277 refillMedia()
0 commit comments