Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

New program: Graphical Mount Manager

After my unfortunate AcetoneISO failure, I decided it was easier to write a new program to manage my disc image mounts.

I haven't built the dpkg yet, but the application is all ready to go.

Here's a copy of the readme file.

Graphical Mount Manager is graphical program that makes it easy to mount and unmount iso files. For a user who doesn't care what the path will be, and just wants to open the file, gmm is the right choice.

App window

Upstream

This project's upstream is at https://bgstack15.ddns.net/cgit/gmm.

Alternatives

AcetoneISO is the obvious alternative. It stopped working for me.

Reason for existence

AcetoneISO stopped working for the author.

Using

The gtk frontend supports drag-and-drop. Drag an iso file from the file manager over the list of mounts. The tk frontend does not support dnd at this time.

Call the program, with a filename, to mount the file. The default behavior is to not show the gui if given a path(s) to mount, but you can force the gui to show with --gui.

Doubleclick an entry in the list to open it with xdg-open. You might find the following xdg-mime commands useful.

xdg-mime default xfe.desktop inode/directory
xdg-mime default xfe.desktop inode/mount-point

Dependencies

Building

Just make && sudo make install, or use the package build recipe.

Future improvements

https://stackoverflow.com/questions/380870/make-sure-only-a-single-instance-of-a-program-is-running

Provide a socket/method that will let the running program refresh. Add function that refreshes the list, and add a menu entry for it. Have the program try to visit the socket and trigger the refresh function. Maybe just a 5-second timer?

References

Brightness for Lego Universe

It's very simple. For the right-side monitor where Lego Universe runs, adjust gamma. To turn it on, right brightness-for-lego-universe on. To set gamma back to normal, run it without the parameter.

#!/bin/sh
# Startdate: 2024-12-01-1 15:22
# Purpose: set gamma on my right monitor if playing DLU
unset _gamma
_gamma="1:1:1" # normal contrast
test "${1}" = "on" && GAME=1
test -n "${GAME}" && _gamma="1.3:1.3:1.3"
xrandr --output HDMI-1 --gamma "${_gamma}"

autologin lego universe menu

I have written a wrapper around my Autologin for Lego Universe utility. This is a graphical menu program that lets you easily add new accounts, and then choose one to auto-type for you. Obviously there's no security at all.

files/2024/listings/autologin-lego-universe-menu.py (Source)

#!/usr/bin/env python3
# File: autologin-lego-universe-menu.py
# Location: blog exclusive
# Author: bgstack15
# SPDX-License-Identifier: GPL-3.0-only
# Startdate: 2024-11-29-6 13:24
# Title: Menu for Autologin for Lego Universe
# Purpose: provide a menu of which account to log in with to DLU
# History:
# Usage:
#    no parameters or environment flags. Just run the graphical program.
# Reference:
#    fprintd_tk
#    https://coderslegacy.com/python/list-of-tkinter-widgets/
#    https://stackoverflow.com/questions/3352918/how-to-center-a-window-on-the-screen-in-tkinter
# Improve:
#    big idea: icon per player/account entry?
# Dependencies:
#    dep-devuan: python3-tkstackrpms, python3-screeninfo
import tkinter as tk, tkinter.simpledialog, os, configparser, json, sys
from screeninfo import get_monitors
import tkstackrpms as stk
CONF_FILE = os.path.join(os.getenv("HOME"),".config","autologin-lego-universe-menu.conf")
ABOUT_TEXT = """
autologin-lego-universe-menu
(C) 2024 bgstack15
SPDX-License-Identifier: GPL-3.0-only
"""
# from so 3352918
def place_popup(popup: tk.Toplevel, root: tk.Tk, width: int, height: int) -> None:
   """Places a new window in the middle of the selected screen"""
   monitor = get_monitor_from_coord(root.winfo_x(), root.winfo_y())
   print(f"DEBUG: chose monitor {monitor}, from x {root.winfo_x()}, y {root.winfo_y()}")
   popup.geometry(
      f"{width}x{height}+{(monitor.width - width) // 2 + monitor.x}+{(monitor.height - height) // 2+ monitor.y}")
def get_monitor_from_coord(x, y):
   """Find the active monitor from tkinter geometry coords"""
   monitors = get_monitors()
   for m in reversed(monitors):
      print(f"DEBUG: checking monitor {m}")
      if m.x <= x <= m.width + m.x and m.y <= y <= m.height + m.y:
         return m
   return monitors[0]
def ferror(*args, **kwargs):
   print(*args, file=sys.stderr, **kwargs)
#class Entry():
#   def __init__(self, username = None, password = None):
#      self.username = username
#      self.password = password
#   def d(self):
#      """ to dict """
#      return {"username": self.username, "password": self.password}
#   def __repr__(self):
#      return str(self.d())
#   def __lt__(self,other):
#      return self.username < other.username
#
#class EntryEncoder(json.JSONEncoder):
#   def default(self, o):
#      return o.d()
def AddEditWindow(title,uservar, pwvar, ok_button_func, cancel_button_func, isEdit = False):
   window = tk.Toplevel()
   window.minsize(100,50)
   window.title(title)
   window.grid()
   tk.Label(window, text="Username",underline=0).grid(row=0,column=0)
   tk.Label(window, text="Password",underline=0).grid(row=1,column=0)
   window.ent_username = stk.Entry(window,textvariable = uservar)
   window.ent_username.grid(row=0,column=1)
   window.ent_password = stk.Entry(window,textvariable = pwvar)
   window.ent_password.grid(row=1,column=1)
   btn_ok = tk.Button(window, text="OK",underline=0,command=ok_button_func).grid(row=0,column=2)
   btn_cancel = tk.Button(window, text="Cancel",underline=0,command=cancel_button_func).grid(row=1,column=2)
   window.bind("<Alt-u>",lambda a=1,b=window.ent_username.focus_set: accelerator_helper_func(a,b))
   window.bind("<Alt-p>",lambda a=1,b=window.ent_password.focus_set: accelerator_helper_func(a,b))
   window.bind("<Alt-o>",lambda a=1,b=ok_button_func: accelerator_helper_func(a,b))
   window.bind("<Alt-c>",lambda a=1,b=cancel_button_func: accelerator_helper_func(a,b))
   window.bind("<Escape>",lambda a=1,b=cancel_button_func: accelerator_helper_func(a,b))
   window.ent_username.bind("<Return>",lambda a=1,b=window.ent_password.focus_set: accelerator_helper_func(a,b))
   window.ent_password.bind("<Return>",lambda a=1,b=ok_button_func: accelerator_helper_func(a,b))
   if isEdit:
      window.ent_username.config(state="disabled")
      window.ent_password.focus_set()
   else:
      window.ent_username.focus_set()
   return window
def accelerator_helper_func(o1 = None, button_func = None, o3 = None):
   """
   This belongs in tkstackrpms, to facilitate a window.bind("<Alt-u>",button_func) to call button_func.
   There is a limitation where what I wanted to call was a built-in and didn't accept more than one parameter.
   Parameter 1 always seems to be discarded or otherwise useless.
   """
   print(f"DEBUG: got accelerator o1 = {o1}, button_func = {button_func}, o3 = {o3}")
   # o2 will be the function to call
   try:
      button_func()
   except Exception as e:
      ferror(f"ERROR: while using accelerator helper func, got error {e}")
class App(tk.Frame):
   def __init__(self, master):
      super().__init__(master)
      self.master.title("Autologin for Lego Universe")
      imgicon = stk.get_scaled_icon("/mnt/public/Games/lego-universe/lego-universe-16x16.png",16,"default","","apps")
      self.master.tk.call("wm","iconphoto",self.master._w,imgicon)
      #place_popup(self.master,root,300,300)
      self.grid()
      # variables
      self.entries = []
      self.addedit_username = tk.StringVar()
      self.addedit_password = tk.StringVar()
      self.listbox = tk.Listbox(master)
      #listbox.insert(0,"hello world")
      #listbox.insert(0,"charlie")
      self.listbox.grid(column=0,row=0,columnspan=4,rowspan=6)
      self.listbox.bind('<Double-1>', self.func_login)
      tk.Button(self.master,text="Login",underline=0,command=self.func_login).grid(column=0,row=7)
      tk.Button(self.master,text="Add",underline=0,command=self.func_add).grid(column=1,row=7)
      tk.Button(self.master,text="Edit",underline=0,command=self.func_edit).grid(column=2,row=7)
      tk.Button(self.master,text="Remove",underline=0,command=self.func_remove).grid(column=3,row=7)
      tk.Button(self.master,text="About",underline=1,command=self.func_about).grid(column=0,row=8)
      self.master.bind("<Alt-l>",self.func_login)
      self.master.bind("<Alt-a>",self.func_add)
      self.master.bind("<Alt-e>",self.func_edit)
      self.master.bind("<Alt-r>",self.func_remove)
      self.master.bind("<Alt-b>",self.func_about)
      # initial load
      self.refresh_form()
      self.listbox.select_set(0)
   # functions
   def func_about(self, o1 = None):
      """ Display about dialog. """
      tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show()
   def func_login(self, o1 = None):
      """ Get currently-selected entry in listbox, and enter that info to autologin-lego-universe.sh """
      print(f"DEBUG: login stub, got o1 {o1}")
      entry = self.get_selected_user()
      command = f""". /mnt/public/Games/lego-universe/autologin-lego-universe.sh ; USERNAME="{entry['username']}" PASSWORD="{entry['password']}" autologin_lego_universe"""
      print(f"Running command {command}")
      self.master.wm_state("iconic")
      os.system(command)
      sys.exit(0)
   def func_add(self, o1 = None):
      """ Ask for a new login to save to list. """
      print(f"DEBUG: stub func_add, got o1 {o1}")
      # try closing any open add/edit window
      self.close_addwindow()
      self.addedit_username.set("")
      self.addedit_password.set("")
      self.addwindow = AddEditWindow("Add entry",self.addedit_username, self.addedit_password, self.update_entry, self.close_addwindow, False)
   def func_edit(self, o1 = None):
      """ Ask for a new login to save to list. """
      print(f"DEBUG: stub func_edit, got o1 {o1}")
      print(f"DEBUG: need to get currently selected entry")
      # try closing any open add/edit window
      self.close_addwindow()
      entry = self.get_selected_user()
      if entry["username"] is None or entry["username"] == "None":
         return False
      self.addedit_username.set(entry["username"])
      self.addedit_password.set(entry["password"])
      self.addwindow = AddEditWindow("Edit entry",self.addedit_username, self.addedit_password, self.update_entry, self.close_addwindow, True)
   def get_selected_user(self):
      """ Return the dict of the currently-selected user, else return empty dict """
      output = {}
      # a single-selection listbox
      username = None
      password = None
      try:
         username = self.listbox.get(self.listbox.curselection())
         try:
            password = [i["password"] for i in self.entries if i["username"] == username][0]
         except Exception as e:
            print(f"FAILURE when looking for {username} in entries, got {e}")
      except tkinter.TclError as e:
         # no entry selected
         pass
      return {"username":username,"password":password}
   def close_addwindow(self, o1 = None):
      try:
         self.addwindow.destroy()
      except Exception as e:
         ferror(f"DEBUG: when trying to hide add/edit window, got {e}")
   def func_remove(self, o1 = None):
      """ Ask for a new login to save to list. """
      print(f"DEBUG: stub func_remove, got o1 {o1}")
      entry = self.get_selected_user()
      print(f"DEBUG: got entry {entry}")
      try:
         u = entry["username"]
         if u:
            try:
               self.entries.remove(entry)
            except ValueError as e:
               # the dict item was not in the list
               ferror(f"Probably just not in the list, but got error {e}. This is probably not a big deal.")
               pass
      except Exception as e:
         ferror(f"When trying to remove, got error {e}")
      self.save_config()
   def refresh_form(self):
      """ Load all data """
      self.entries = self.load_config()
      self.listbox.delete(0,tk.END)
      x = 0
      print(f"DEBUG: refreshing form, from entries {self.entries}")
      for e in self.entries:
         print(f"Looping over {e}")
         self.listbox.insert(x, e["username"])
         x = x + 1
   def load_config(self):
      """ Read ~/.config/autologin-lego-universe-menu.conf and get all username/pw listed there """
      print(f"DEBUG: loading config")
      try:
         with open(CONF_FILE,"r") as o:
            entries = json.load(o)
      except FileNotFoundError:
         print(f"DEBUG: need to generate new config file")
         with open(CONF_FILE,"a") as o:
            o.write("[]")
         entries = []
      #print(f"DEBUG, got entries in {type(entries)} below:")
      #print(entries)
      return entries
   def update_entry(self, o1 = None):
      """ Take add/edit window information and find the corresponding entry and update it, or else add a new one. """
      #print(f"DEBUG: will look for entry {self.addedit_username.get()}, and assign pw {self.addedit_password.get()}")
      new_u = self.addedit_username.get()
      new_p = self.addedit_password.get()
      updated = False
      for e in self.entries:
         if e["username"] == new_u:
            print(f"DEBUG: found entry {e}, updating pw for it.")
            e["password"] = new_p
            updated = True
            break
      if not updated:
         print(f"DEBUG: adding new entry, u={new_u},p={new_p}")
         self.entries.append(
            {
               "username": new_u,
               "password": new_p,
            }
         )
      self.save_config()
   def save_config(self, o1 = None):
      """ Save config, including the latest entry from add/edit window """
      self.close_addwindow()
      print(f"DEBUG: please save entries. Currently:")
      print(self.entries)
      #print(f"And also {self.addedit_username.get()}, {self.addedit_password.get()}")
      with open(CONF_FILE,"w") as o:
         o.write(json.dumps(self.entries))
      self.refresh_form()
# main
if "__main__" == __name__:
   root = tk.Tk()
   # naive, because it combines all monitors into one giant display for calculating.
   # FUTUREIMPROVMENT: get placement working
   root.eval('tk::PlaceWindow . center')
   alu = App(root)
   # Interesting, but not correct. It always chooses monitor 0 but the window would popup on monitor 1 if we do nothing special about geometry
   #print(f"DEBUG: screen width is {root.winfo_screenwidth()}")
   #print(f"DEBUG: screen height is {root.winfo_screenheight()}")
   #place_popup(alu.master,root,300,300)
   alu.mainloop()

The idea with this program is to make it easy to choose between credentials. That's about it.

Screenshot of Menu for Autologin for Lego Universe, which also shows the Add Entry window

Walk through my git config

And now for something completely different, here's my workstations' ~/.gitconfig, the global git config. So this is not for any particular repository; it's the overall settings.

[user]
    name = B. Stack
    email = bgstack15@gmail.com
[credential]
    helper = cache --timeout=7200
[diff]
    color = auto
[pull]
    rebase = false
[http]
    emptyAuth = true
[sendemail]
   smtpserver = server2.ipa.internal.com
   smtpserverport = 25

Section [user] is of course my name and email.

Section [credential] caches my password for 2 hours. That seems to be a good number.

Section [diff] sets color to auto, so when not piping to something else, display pretty Christmas colors!

Section [pull]. I don't remember why I have this, but probably to silence some warning or deprecation notice. Whatever.

Section [http] is useful with kerberos. When a git remote of http://gitserver.ipa.internal.com accepts kerberos authentication, it sends back a challenge to authenticate. Git needs emptyAuth=true or else it will never get around to submitting the user's kerberos ticket. This requires a newer version of Git than what CentOS 7 has.

Section [sendemail] I barely use, but was for when I was trying to generate git emails. I only used it once or twice, and I didn't like the data leakage that happened, so I ended up not using it.

Conclusion

I started off of course, thinking that I wouldn't want to customize my tools because then I'd have to carry those everywhere I go. But over time, more and more settings are useful to me and I need them. So just like my .vimrc, my .gitconfig has grown.

What's in your git config?

References

for git 2.11 and higher, krberos auth https://docs.gitlab.com/ee/integration/kerberos_troubleshooting.html

Patching Lego Universe to not crash in wine

The old non-free OS game Lego Universe runs moderately well in Wine. However, it does tend to crash with errors like the following.

0138:err:virtual:allocate_virtual_memory out of memory for allocation, base (nil) size 00100000

Usually when entering a different region and the game is clearly trying to load all the assets for this region. The game will freeze up for a second, and then crash. What solved the errors was using a 4GB Patch – NTCore utility that patches the binary headers so the program can address more than 2GB of RAM.

That was it! This small program works in Wine. You pass it a binary name to patch. I backed up the file, of course, and the differences are very small.

Screenshot of vimdiff :!xxd for the files

References

  1. 4GB Patch – NTCore found from Unified Memory Architecture (UMA) · Issue #4362 · doitsujin/dxvk

Failed research

From https://simpler-website.pages.dev/html/2021/1/wine-environment-variables/

export __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia
# specifically for my setup from `vulkaninfo | grep -i 'gpu id :'`
export DXVK_FILTER_DEVICE_NAME="Intel(R) UHD Graphics 630 (CFL GT2)"

From https://github.com/UchuServer/Uchu experimental

export WINEDLLOVERRIDES="dinput8.dll=n,b"

I tried winetricks d3dx9_43 but it didn't help. I then realized it uses d3dx9_34 but that didn't help either. I tried using wine first, then native, but that breaks the game and rendering even on the login screen.

From https://forum.winehq.org/viewtopic.php?t=36996

winetricks l3codecx directshow wmp10 # but skipped wmp10; these did not help

From https://askubuntu.com/questions/835039/games-not-running-properly

# regedit HKEY_CURRENT_USER/Software/Wine/Direct3D -> string VideoMemorySize = 512

From https://forums.linuxmint.com/viewtopic.php?t=167002 # regedit HKEY_CURRENT_USER/Software/Wine/Direct3D -> VideoMemorySize=2048, DirectDrawRenderer=""

I also tried winetricks dxvk which also added d3dcompiler_43 d3dx9 d3dcompiler_47 dxvk but this completely broke the game. So I reset my wineprefix afterwards.

I didn't record where this came from, but this didn't help.

echo $(( 1024 * 1024 )) | sudo tee /proc/sys/vm/min_free_kbytes

Adding Frostburgh to Darkflame Lego Universe

I run a private Darkflame Lego Universe instance for myself. There are instructions out in the wild for adding the seasonal map Frostburgh. Here are my steps for the docker-compose based setup that I use, to get Frostburgh as a playable and visitable map.

  1. Download his asset.

    This is from url https://www.mediafire.com/file/a04fq7yah3fbpst/frosty_22Jan24.zip/file. I had to use a browser because that is some landing page and not a direct link.

    $ sha256sum frosty_22Jan24.zip
    39ed16b1b1616c21dcd0691a8bc6edb53260cfd195c84bd4dcf6466f094d5132  frosty_22Jan24.zip
    
  2. Convert original fdb to sqlite using https://fdb.lu-dev.net/

    $ ll
    total 56784
    -r--r--r-- 1 bgstack15 bgstack15 42140012 Nov 26 2011 cdclient.fdb
    -rw-rw-r-- 1 bgstack15 bgstack15 16003072 Nov  8 2024 cdclient.sqlite
    $ sha256sum *
    875d887f806de7c3e0a9c3974f11dc2d5375d3ecfa3b5d3be5314d3da34b3aa4  cdclient.fdb
    8f6ca2963ac3f33e23e5d38eed62f1795dc7d96aed084ea7fa97fb18ad574681  cdclient.sqlite
    
  3. run FROSTY.sql

    This is where I diverge minorly, because on GNU/Linux it's easy to redirect input and run the queries that add the relevant parts to the database. After extracting the zip file, run the queries.

    $ <FROSTY.sql sqlite3 cdclient.sqlite
    
  4. convert sql back to fdb

    Use the same webpage to get it back. My directory now looks like:

    $ sha256sum cdclient.fdb*
    bcb9cabb8274c1fcc0918115268e068b114b51fccbbc7d7cbed1b0810bfd854c  cdclient.fdb
    875d887f806de7c3e0a9c3974f11dc2d5375d3ecfa3b5d3be5314d3da34b3aa4  cdclient.fdb.orig
    
  5. Update client files

    1. Update locale.xml

      On the client, manually update the locale/locale.xml with the contents of the locale.xml.txt from the zip. Add these <phrase> objects where they obviously go, below the last </phrase>, above the <fonts>.

    2. Update res directory

      Copy the res/ directory from the expanded zip to the client directory. This will overwrite a few files.

      cp -pr res/ ~/.wine-lego/drive_c/lego-universe-client/res/
      
    3. Update cdclient.fdb

      Replace the cdclient.fdb with the converted one from step 4.

  6. Update server files

    1. Server locale.xml

      Take that client-updated locale.xml and place on server's client/locale/locale.xml file.

    2. Update res directory

      Copy the from-zip res/ directory to server's client directory.

      ~/darkflameserver/client$ 7za x frosty_22Jan24.zip res
      
    3. Update CDServer.sqlite

      I had updated CDServer.sqlite previously to fix a unicode eror for the Nexus dashboard admin mail function. So I also did this to the server's server/res/CDServer.sqlite:

      <FROSTY.sql sqlite3 ~/darkflameserver/server/res/CDServer.sqlite
      

      Thankfully the server uses sqlite so we do not need to convert this back to fdb.

    4. Prepare the launchpads

      I had to add to my docker-compose file container darkflameserver the volume entry:

         - ./server/vanity:/app/vanity/
      

      And then prepare it on my local filesystem from the default built-in ones.

      sudo docker cp da59f6c5e6c4:/app/vanity ~/darkflamserver/server/
      

      And then fix file permissions. And the format appears to be different now for objects in the vanity dev-tribute.xml. I found https://github.com/DarkflameUniverse/DarkflameServer/tree/v2.2.0/vanity which shows an NPC.xml which was indicated to be modified. Those contents now exist in dev-tribute.xml:

      <object name="Nimbus to Frostburgh" lot="12442">
           <locations>
               <location zone="1200" x="-362.17" y="290.31" z="209.85" rw="0.92" rx="0.0" ry="-0.38" rz="0.0" />
           </locations>
      </object>
      <object name="Frostburgh to Nimbus" lot="12460">
           <locations>
               <location zone="1260" x="-668.58" y="569.1109" z="629.85" rw="0.974" rx="0.0" ry="-0.22" rz="0.0" />
           </locations>
      </object>
      

Now, restart the server and client. After all that I was able to visit Frostburgh from the launchpad placed near the launchpad to Starbase 3001 in Nimbus Station!

Screenshot of the new launchpad to Frostburgh:

Perhaps it would be easy to update the xml file around the holiday season to disable the launchpad, I don't plan on doing that. I guess Frostburgh is here to stay!

Frostburgh is here!

Acetoneiso silent error: A plea for help

I have recently discovered a problem with AcetoneISO (or on launchpad that nobody else seems to have experienced. I wonder if it's disused. Even I have alternatives: just sudo /usr/bin/mount * /mnt/cdrom permissions or cdemu.

The purpose of acetoneiso is to pass a filename to it, which it should then mount, nominally to ~/virtual-drives/1/ and so on. However, nowadays, it fails, on multiple systems that I have tested. It doesn't throw any errors graphically. You have to run acetoneiso from a terminal and then you'll see this:

QIODevice::read (QFile, "/home/bgstack15/virtual-drives/1/readme.txt"): device not open

That's it. And Internet searching has gotten me nowhere. I'm guessing there's some busted thing in QT but I know nothing about QT or how to troubleshoot. I'm guessing GTK_DEBUG=1 won't help here. So if any of you readers know how to help me, please point me in a direction!

Screenshot of acetoneiso

Python error in Radicale led to me fixing my httpd config

When I ran OS updates for the month of November, I realized that my Radicale CalDAV server was not working. It was running, but it had an application error. After doing all the requisite research, I opened a bug report.

Nov 01 11:01:13 server3.ipa.internal.com radicale[8675]: [8675/Thread-2] [INFO] Successful login: 'bgstack15@IPA.INTERNAL.COM'
Nov 01 11:01:13 server3.ipa.internal.com radicale[8675]: [8675/Thread-2] [ERROR] An exception occurred during REPORT request on '/bgstack15/': Error in section 'read-domain-principal' of rights file '/etc/radicale/rights': Replacement index 0 out of range for positional args tuple

To simplify out the examples/boilerplate, my rule for calculating per-user permissions when using GSSAPI (kerberos/ldap) auth was throwing a python error.

[principal-domain]
user: (.+)@IPA.EXAMPLE.COM
collection: {0}
permissions: RW

It appears that whatever new logic happened in the 3.3.0 release revised how the regex capture groups work across the attributes of a permission directive.

It worked before. Or at least, it didn't throw errors before. (Technically I'd been patching out the domain name inside radicale already so this rule wasn't even in use).

So, the Radicale team suggested that I strip the domain name at the reverse proxy level, which inspired me to search, and this time I found a solution!

In my httpd config:

RewriteEngine On
RewriteRule ^/radicale$ /radicale/ [R,L]
<Location "/radicale/">
   ProxyPreserveHost On
   Include conf.d/auth-gssapi.cnf
   # which includes these lines:
   #GSS_NAME returns username@IPA.EXAMPLE.COM which merely needs additional rules in /etc/radicale/rights
   #RequestHeader set X_REMOTE_USER "%{GSS_NAME}e"
   Require valid-user
   AuthName "GSSAPI protected"
   ProxyPass        http://localhost:5232/ retry=20 connectiontimeout=300 timeout=300
   ProxyPassReverse http://localhost:5232/
   RequestHeader    set X-Script-Name /radicale
+   RequestHeader    edit X_REMOTE_USER "^(.*)@.*" "$1"
</Location>

So all I needed to do was treat the now-extant request header as a string variable and do a simple regex manipulation to preserve everything before the at sign.

Now I will no longer need to maintain that ridiculous app/__init__.py one-line patch after every python3 or radicale update.

I am always amused that some complex problems can be solved by a one-line change. It's usually the one-line changes that represent hours and hours and hours of work, eh?

Lego Universe use gamepad controller

If you want to use a usb gamepad controller (no affiliation/kickbacks here: I'm just a happy user) to emulate keypresses for the game of Lego Universe (DarkflameServer), here is how to repeat what I set up.

Install qjoypad. Run, and load this configuration. This is for the default keyboard configuration in-game, which I never tried to change. It might not even be possible.

files/2024/listings/lego-universe-qjoypad.lyt (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# QJoyPad 4.3 Layout File
# Startdate: 2024-10-23-4 15:25
# Author: bgstack15

Joystick 1 {
	Axis 1: gradient, +key 40, -key 38
	Axis 2: gradient, +key 39, -key 25
	Axis 3: +key 11, -key 0
	Axis 4: gradient, maxSpeed 1, tCurve 0, mouse+h
	Axis 5: gradient, maxSpeed 1, mouse+v
	Axis 6: +key 14, -key 0
	Axis 7: gradient, +key 40, -key 38
	Axis 8: gradient, +key 39, -key 25
	Button 1: key 65
	Button 2: key 64
	Button 3: key 50
	Button 4: key 37
	Button 5: key 12
	Button 6: key 13
	Button 7: key 56
	Button 8: mouse 1
	Button 11: rapidfire, mouse 3
}

Screenshot of qjoypad configuration window. This is not enough information to configure your controller for yourself.

The gamepad will have this layout.

Key mapping of XBOX 360-like controller, that is useful for Lego Universe default keybinds

Lego Universe cancel mission

If you are using Darkflame Server to emulate the long-gone Lego Universe MMO, and you accidentally accept a mission you cannot finish, you can use an admin command to remove the mission from your list.

You have to be an admin (aka GM or Mythran) on the server, which is not covered here, to be able to run the macro. It appears DarkFlame added a command for resetting an individual mission.

Screenshot of mission that I will never get to complete.

To determine which mission you need to scrub, search the text in client file locale/locale.xml. You want to grab the id number from the MissionText_[id]_in_progress.

    <phrase id="MissionText_1194_in_progress">
        <translation locale="en_US">Visit 5 Block Yard or Avant Grove Properties today.</translation>
        <translation locale="de_DE">Besuche heute 5 Steinhof- oder Avanthain-Grundstücke.</translation>
        <translation locale="en_GB">Visit 5 Block Yard or Avant Grove Properties today.</translation>
    </phrase>

You can confirm the mission id is correct on explorer.lu (lu:explorer). So with the mission id, you can go chat this in-game:

/gmlevel 8
/resetmission 1194
/gmlevel 0

Screenshot of entering admin chat command to hide the mission.

References

  1. LU Development Network | LU Development Network
  2. Game master levels in DarkflameServer/docs/Commands.md at main · DarkflameUniverse/DarkflameServer
  3. move command resetmission to gmlevel 0 by bgstack15 · Pull Request #1638 · DarkflameUniverse/DarkflameServer