I am posting a few videos to my youtube channel on occasion. For anyone wondering why there is such a prolonged delay between these postings, the reason is simply the fact that I do not often have time to record. I have a house full of children, so I can rarely take the time to record the voice over for these videos. Whenever I get a chance to record some, I will do so and get more videos posted.
banal.musings
The disappointingly typical mutterings of an omphaloskeptic hypochondriac.
Monday, June 22, 2026
Monday, June 15, 2026
Lamy Al-Star
First of all, before I get into the process of reaming this pen for its faults, let me just state foremost that this is possibly my current favorite pen. It is in close competition with the TWSBI 580AL and Lamy 2000, but the contour of the grip is the best. I purchased a Lamy Safari Vista about two months ago and have adored writing with it ever since, so I naturally thought an aluminum version of it would be such a nice step up.
Wrong.
I mean... maybe not wrong, but wrong. I guess I was wrong, that's really the problem. I presumed an aluminum version would bring a hefty, cool shaft into the equation. It does not. The aluminum is milled so thin the weight is imperceptibly different from the resin. I do love the color of the pen, but it's not functionally different to my experience. In the future, I'd just save the money and get another resin version.
It does feel slightly thicker in the shaft than the resin version, I suppose. So there's some small difference - I am just shocked that the weight is almost exactly the same. Or that I am completely incapable of detecting any weight differential.
These thoughts aside, I think I might actually recommend this pen over the TWSBI Eco, which is something I hadn't previously thought possible.
Friday, April 3, 2026
Crimson Desert
It's a good game. The controls really set the game back, but it seems like the developer is working on this. I don't like the combat, personally. I have never been a fan of souls-like combat. The open world, discovery, freedom to make decisions, multiple approaches to puzzles, and deep systems are top-notch. It is better than any other game to date in these departments. The visuals are also quite good and the game engine seems well optimized.
The idea in Crimson Desert is that you will enter combat, have no idea what to do and get instantly one-shot. Then you repeat over and over until you figure out the "trick" to beating the boss. Then you do that trick until you get it right. Victory. Not my cup of tea, but it seems the majority of modern "gamers" love this combat system, so I certainly understand why the developers utilized it.
I have enjoyed the game and I think the negative reviews are largely unwaranted.
Friday, April 22, 2022
Coding in TTS (3)
And to round things out for this time, this next bit is part of a painfully long function which (as far as I can tell at the moment) is required to be painfully long because it is the global object dropped code, so in it must go every decision related to any time any object is dropped anywhere on the board for any reason. Which is a lot of decisions as it turns out.
I don't like this piece. Not because I think I did it wrong (though it is possible there's a more efficient set of logic to accomplish the same result). I don't like it because I don't like any piece of code that is decision nested inside decision nested inside decision nested inside decision, etc, etc. Once I get a certain amount of layers my head starts to hurt and I begin to become overwhelmed with all of the things I must remember about the prior decision but not code at the moment because I must finish with this next decision and its nested counterparts first. Headaches ensue.
Also, this is nowhere near the most deeply nested set of if/then statements I've ever written. We don't need to talk about how far this can go.
if obj.tag == 'Card' and obj.hasTag("Battalion") then
--find battalion card type
local cardType = Global.call("findBattalionType", {obj.guid})
--check if the battalion card dropped is in the discard zone
local discardIndex = {sdiscard, kdiscard}
local lzIndex = {slaneIndex, klaneIndex}
local footmanBlockIndex = {sfootmanIndex, kfootmanIndex}
local breacherBlockIndex = {sbreacherIndex, kbreacherIndex}
local berserkerBlockIndex = {sberserkerIndex, kberserkerIndex}
local actionZoneIndex = {sactionZone, kactionZone}
local typeCount = 0
local typeAction = 0
for a = 1, 2 do
local discardObjects = getObjectFromGUID(discardIndex[a]).getObjects()
for _, card in ipairs(discardObjects) do
--if the battalion card is in the discard zone, check if the game is in the skirmish phase
if card.guid == obj.guid then
--[battalion, discard, skirmish] find the active lane
laneZoneIndex = lzIndex[a]
for d = 1, 5 do
laneObjects = getObjectFromGUID(laneZoneIndex[d]).getObjects()
for _, tile in ipairs(laneObjects) do
if tile.hasTag("lane") then
if tile.getStateId() == 2 then
--[battalion, discard, skirmish, active lane] count battalion cards in the lane and count card type in lane
for _, stuffs in ipairs(laneObjects) do
if stuffs.tag == 'Card' and stuffs.hasTag("Battalion") and stuffs.hasTag(cardType) then
typeCount = typeCount + 1
end
end
--[battalion, discard, skirmish, active lane] set counter block to new value
if cardType == "Footman" then
blockIdIndex = footmanBlockIndex[a]
else
if cardType == "Breacher" then
blockIdIndex = breacherBlockIndex[a]
else
if cardType == "Berserker" then
blockIdIndex = berserkerBlockIndex[a]
else
blockIdIndex = nil
end
end
end
if blockIdIndex != nil then
counterBlock = getObjectFromGUID(blockIdIndex[d])
counterBlock.setName(typeCount)
end
--[battalion, discard, skirmish, active lane] count action dice matching card type in action zone
actionZoneObjects = getObjectFromGUID(actionZoneIndex[a]).getObjects()
for _, actionDie in ipairs(actionZoneObjects) do
if actionDie.getName() == cardType then
typeAction = typeAction + 1
--secure action die in case one needs to be removed
anActionDie = getObjectFromGUID(actionDie.guid)
end
end
--[battalion, discard, skirmish, active lane] remove action dice more than card type count
if typeAction > typeCount then
anActionDie.destruct()
end
end
end
end
end
end
end
end
end
Coding in TTS (2)
The routine for rebuilding the holding zone is similarly simple:
function rebuildHold(nfo)
--the holding zone GUID is in nfo[1]
zoneObjects = getObjectFromGUID(nfo[1]).getObjects()
--the default starting position is in nfo[2]
sPos = nfo[2]
--the separation increment is in nfo[3]
incVal = nfo[3]
--the action zone is in nfo[4]
actionObjects = getObjectFromGUID(nfo[4]).getObjects()
--set up the count variable
dieCount = 0
--count the number of vanguard action dice currently in the action zone
for _, die in ipairs(actionObjects) do
if die.hasTag("action") then
if die.getName() != "Footman" and die.getName() != "Breacher" and die.getName() != "Berserker" then
dieCount = dieCount + 1
end
end
end
--find new starting position based on the number of dice being returned to the holding zone
sPos = sPos + (incVal * dieCount)
--move all objects currently in the holding zone arbitrarily
for _, obj in ipairs(zoneObjects) do
vPos = obj.getPosition()
nPos = {sPos, vPos[2], vPos[3]}
obj.setPosition(nPos)
sPos = sPos + incVal
end
end
This is the first routine I wrote which I passed multiple values through. I never doubted it was possible, I just didn't want to mess with it while trying to test out whether or not things were possible in TTS (so that I wasn't confused about where the errors might be coming from should there be any)
Same as before, there are two scripting zones at play (actionZone and holdingZone).
First, loop through the action zone and simply count any die which is an action die but is not a Battalion die. Since I've named the dice when they are generated, this is being done using the name. You probably can see this plainly, but just a "programming" note is the fact that writing die.getName() is completely arbitrary. If I had written for _, apples in ipairs(actionObjects) do then it would be written apples.getName(). Like I said, I am sure that's patently obvious, but sometimes when I read other people's code I'm like "is he writing die because that's a thing this program understands or just because he decided it would be called die and defined that elsewhere?"
Second, calculate how many die "positions" are going to be used by the number of Champion dice currently in the action zone (using the incVal which is holding the current increment value for die positioning).
Third, move any dice currently in the holding zone down to make room for the dice which are about to be moved in. I use the vector vPos just becuase it is convenient to grab the current vector of whatever object already exists and simply move the X value to the new starting position without trying to remember or calculate the Y or Z values. Again, this is likely very, very standard practice. But I think very absolutely. I am most comfortable telling the script to put something at exactly these coordinates right here. So, I get really proud of myself any time I remember to program something in a relative way.
Coding in Tabletop Simulator (TTS)
Sometimes, I feel like my code is really elegant. I have the bits I need and there isn't a ton of logic to flesh out, so something that felt complicated when I was thinking about adding it turns out to be pretty simple when I actually write the bits:
--Build loop index
holdIndex = {sactionHold, kactionHold}
actionIndex = {sactionZone, kactionZone}
startingPosition = {8.5, -9.2}
incrementValues = {1, -1}
--loop through both sides to clear action dice
for i = 1, 2 do
startPlace = startingPosition[i]
advanceVal = incrementValues[i]
actionHold = getObjectFromGUID(holdIndex[i])
actionObjects = getObjectFromGUID(actionIndex[i]).getObjects()
--rebuild holding zone
Global.call("rebuildHold", {holdIndex[i], startPlace, advanceVal, actionIndex[i]})
for _, die in ipairs(actionObjects) do
if die.hasTag("action") then
if die.getName() == "Footman" or die.getName() == "Breacher" or die.getName() == "Berserker" then
--just remove the action die from the game by destroying it.
--Battalion cannot change lanes, so when a lane becomes inactive, the Battalion's actions become meaningless until next round
die.destruct()
else
dPos = {startPlace, 1.16, 27}
die.setPosition(dPos)
die.setLock(true)
startPlace = startPlace + advanceVal
end
end
end
end
This bit uses two scripting zones I placed on the table (four actually, because everything is divided into the two sides). As I've mentioned before, I tend to just focus on getting parts working more than on writing clean or simple or efficient code. In fact, in the past, I've mostly only cleaned up inefficient code when it was actually affecting performance (like calling 10 queries on a database in order to keep the data set small instead of calling 1 query and using a very large data set to resolve everything. I spent a year cleaning this type of methodology up at work)
The first, obvious thing you'll notice if you're opening up my code is that most of the foundational bits I've written are divided into two parts - one for one side of the table and the second for the other side. It still works this way, of course, but now I am (duh) using a loop and a couple indexes to divide up the unique identities for each side.
actionHold is the part on the side of the table where the action dice are initially set up for the round.
actionZone is the actual tile between the lane Guardians where players can interact with their action dice.
All this routine is doing is taking all of the action dice off of the action tile and placing them back into the holding area. This is to support the concept that a Champion which still possesses an action may be moved to an active lane and still use their action. So, I figured the logic for this was to 1) establish a die at a set point in the game which either exists or doesn't exist, 2) move that die into the action zone any time the Champion is moved into an active lane, 3) preserve the die whenever a lane goes inactive without the Champion using their action, 4) remove all remaining dice at the end of the round prior to setting up the next round. That's why I figured a holding zone and an active zone would be ideal.
As far as the holding zone being visible to the players, I am not sure what direction would be best on that. I definitely considered keeping the holding zone hidden, so that the only time the players see the action dice is when they are placed on the action tile. This could streamline player recognition of what the dice are and what to use them for. On the other hand, there is benefit to having them visible to both players throughout the round. It gives an overall view of what the round will look like.
We talked about the die removal (which is not the above function). I am still torn about just assuming that any battalion being removed from the board is going to take an action with them. This is actually based more on the times when a player might choose which of their own Battalion to remove than on when their opponent will defeat one. There are circumstances, like using a Paragon reaction or just dealing with untargeted damage, that a player will decide which battalion to remove. For these circumstances, it is best to let the player manage the removal of the dice.
This, of course, means that there is potential confusion. If a player does not remove a die when they are supposed to do so, then they might mistakenly (or purposely) gain extra actions that they should not have just because the dice exist for the sake of clarity (clarity causing obfuscation). This is really a result of the fact that is is INCREDIBLY inconvenient to tie action dice to any specific card in the game. There is an expansion concept which would give all battalion individual names and portraits. In this version, sure, it would be easy to tie the dice to the cards. But, right now, not so much. It's definitely something that could be handled very cleanly outside of TTS.
Anyway, the basics here are:
Find where the first die should be placed when moved to the holding zone: startingPosition
Find out how many units to separate the dice when subsequent dice are moved: incrementValues
Call a different routine which counts how many dice are going to be moved and moves everything already in the holding zone enough distance away from the starting position to make room for the dice being moved in: rebuildHold
Loop through all dice in the action zone and destroy any battalion dice while moving any Champion dice to the holding zone.
When a die is moved, advance the startingPosition value so the next die which gets moved will not get moved to the exact same position (not really a problem other than the visual confusion it causes and the fact that it sort of defeats the whole purpose of having the dice in the holding zone visible to the players in the first place.) If I were hiding the dice being held, then this whole thing would be a bit simpler.
Monday, February 7, 2022
Logitech Mouse Follow-Up
Well, I've had this Logitech G502 SE for about a year and a half now. I wasn't super sold on it in the first place, but there are some failings that have really stuck out over its lifespan which have finally made me desperate to move away from it to a different mouse. Sadly, I cannot move back to the Razer Lancehead that I loved so much, because it was poorly made and lasted less than a year before it stopped functioning correctly. I am not about to buy another Razer mouse at this point, because I don't have the money to do so. However, thankfully, I do have a few mice laying around. So I will be transitioning between them until I settle on the correct course of action.
In reality, the mouse I've used the most over the past year or so had been the Logitech M590, which can easily switch between my laptop and my main system. I have also come to realize that, for whatever reason, I really love mice with silent clicks. I might have simply moved to the M590 as my main mouse except for the fact that it is much too small. I can only use it for a few hours before my hand starts to cramp from the tight claw-grip it forces me into. I didn't prefer large mice in the past, but after the Roccat Kone XTD showed me what I'd been missing, I've been enamored.
This brings me to the first failing of the Logitech G502: it's too small. I was wary of moving to an ergonomic mouse in the first place, since I've long preferred ambidextrous layouts. The ergonomic shaping has not been a large hindrance and I can even see how it might be nice to use, however the mouse is too small for my hand to get any real benefit from its angled shape. It is some relief from the tiny size of the M590, but not really very much.
The second failing for Logitech is their software. I originally complained about the trash software that Razer forces on all of its customers. I stand by that complaint. However, I had no concept that Synapse in all of its revolting bloaty spyware glory could possibly be BETTER than its competition. Logitech's G HUB is everything bad that Synapse is, plus it doesn't even work correctly. At least Synapse has the good grace to function as expected most of the time. G HUB seems to want to change my profile to useless defaults every time I press Alt-Tab. As if Logitech's software engineers failed to consider a world where computer users might switch between tasks. It is infuriating and frustrating. There's also no way to stop it beyond simply turning the software off. You can retain profiles, if you like going into G HUB settings any time you want to switch profiles to change the default to the one you currently deisre. It is clunky, unnecessary and disheartening.
I will say that the G502 at least gave me what I expected: it has lasted. There's nothing wrong with the mouse on the hardware side. Well, at least nothing wrong with it that wasn't already wrong with it when I first opened the box. See, the G502 line has an incredibly annoying mouse button pullback issue (the button sticks to your finger and then snaps off, resulting in an irritatingly pingy slapback click after each press). It is atrocious. I've attempted to ignore it for a year and a half.
I am so ready to be done with this mouse. Logitech is great, but their mice are just as big of failures as any other company's.