Author: postmistress

Recently i dipped my toes into Battletech, I know, it’s a lot of number crunching and calculations. I do not understand myself most of the things. But, things can get better! I bring you BattleTech Character Generator! <3

What Character Generator?

Lately I was toying with CozyVTT. It is Virtual Tabletop project. Fully OpenSource and free. Before I was trying to work with FoundryVTT, but I found it over complicated and annoying to deal with.

My biggest issue with FoundryVTT was, that it was quite complicated for me to modify and add new features to it. I’m no code expert, but with help of LLM, I was able to modify CozyVTT to be quite good character generator for BattleTech.

Maybe my brain got lazy, as I’m getting older, but I always got bored during calculating stats. All those traits and attributes old fashioned way. Excel? Hell naw!

So, I made my own Battletech character generator, where you can click your character rather easily. Tt checks things for you and you can even modify character sheet based on game you are playing.

The whole thing is around BattleTech: Time of War, so if you are playing this book, this might get handy for you.

Warning

I must warn you though, this is not even ALPHA release. I created this generator for my friends and for myself, so expect bugs. Also, if I will see you guys doing bad stuff with it, I will turn it private again and no BattleTech character generator for you! 🙁

There is even possible to play campaign in it like in any other VTT, I added hexes support, which is not in vanilla CozyVTT and some sample mechs into NPCs …,but it’s far from finished. Now mainly, it’s just character generator.

NOTE: After register I need to approve you, if your approval doesn’t come within day or two, poke me on my emails. You can find them on the bottom of the Home Page.

After selecting modules, my stats didn’t show in sheet!

You need to first hit Apply build to sheet, and then Save.

Distribution

For those interested in hosting this Battletech Character generator on your own server, contact me, I can send you files (at least I will try).

Enjoy!

BattleTech Character generator showcase

Battletech Character Generator  Selection
Character Creation selection
Battletech Character Generator  Life Modules
Life Modules selection showcase
Battletech Character Generator  Rules Verification
Rules verification check
Battletech Character Generator  Inventory
Inventory Management
Finished Character sheet

Features included in Battletech Character Generator

1. BattleTech: A Time of War game system

Added a new native game system:

BATTLETECH_ATOW

Integrated across:

This allows a campaign to behave as a dedicated AToW campaign instead of using a generic or D&D-oriented character system.


2. Full AToW character sheet

Added a dedicated BattleTech AToW character sheet with support for:

The sheet integrates with the existing CozyVTT character infrastructure rather than creating a separate character database.


3. AToW Life Module character creator

This was one of the largest additions.

Added a Life Module-based character generator implementing the AToW character creation structure.

Features include:

The builder can apply the finished Life Module build directly back into the character sheet.


4. Expanded affiliation and faction backgrounds

Stage 0 affiliation choices were greatly expanded.

Added support for numerous subfactions under:

Clan caste packages were also added, including things such as:

Each affiliation package can contribute its own XP, skills, traits and flexible allocations.


5. Master Schools and Officer Candidate School

Implemented AToW education rules for Master Schools, including correct Basic / Advanced / Special Fields.

Supported schools include:

Also added an Officer Candidate School system with:


6. Age and aging system

Added AToW aging milestones:

21
31
41
51
61
71
81
91
101

The system can automatically apply:

The character sheet shows a visible breakdown of what aging changed.

Existing characters can also be recalculated through the Life Module creator without permanently stacking aging effects repeatedly.


7. Character portrait system

Added portrait support to AToW characters.

Features:

The same portrait can now also be reused by the platoon-management cards.


8. AToW equipment catalogue

Added a substantial personal equipment catalogue.

Features include:

This gives AToW characters an actual equipment-management workflow rather than a freeform text field.


Tactical BattleTech additions

9. Hex-grid support

Vanilla CozyVTT originally centered around square grids.

Added:

square
hex-flat
hex-pointy

Support covers:

Hex dimensions are treated consistently between visual rendering and snapping.


10. Hex-aware token snapping

Added proper geometric hex snapping rather than approximating a hex map with square coordinates.

Tokens now snap to the actual center of the nearest:

This also works when placing tokens from campaign rosters and other map interactions.


11. Grid calibration

Added a major map-alignment system for scanned or pre-printed BattleTech maps.

The generated CozyVTT grid can now be calibrated independently over the background image.

Controls include:

Persistent map settings:

gridOffsetX
gridOffsetY
gridScaleX
gridScaleY

Most importantly, calibration affects both the displayed grid and snapping calculations, so tokens continue to land on the actual printed hexes.

Existing maps default to:

offset 0 / 0
scale 100% / 100%

so installation does not disturb old maps.


12. BattleTech unit facing

Added BattleTech-style facing to tokens.

Facing snaps to:

0°
60°
120°
180°
240°
300°

Features:

This was added specifically because facing changes are mechanically important in BattleTech.


13. Terrain marker system

Added BattleTech terrain directly to maps.

DMs can place terrain markers snapped to hexes and click them to open rules cards.

Current terrain includes:

Terrain is stored using normal CozyVTT object tokens with BattleTech metadata, so it did not require a separate terrain database.

Automatic LOS/movement penalties are not yet implemented; currently the markers primarily provide visual and rules-reference functionality.


BattleTech Unit Library

14. Generic BattleTech unit system

Vanilla CozyVTT’s NPC/unit creator was replaced for BattleTech campaigns with a dedicated BattleTech unit library.

Supported top-level types:

Infantry
Ground Vehicle
Aerospace
Mech

Examples of subtypes include:

Infantry

Ground Vehicle

Aerospace

Mech

Unit records can contain:

Units can be placed onto maps and damaged/repaired through the existing CozyVTT creature/token infrastructure.


15. BattleTech preset browser

Added a searchable unit-preset browser.

Features:

No artwork from the sourcebooks was embedded.


16. TechManual example presets

Added a number of example units derived from TechManual worked examples, including:

These serve both as usable units and as examples for the Unit Library.


17. Record Sheets: Succession Wars BattleMechs

A very large number of BattleMech presets were transcribed from Record Sheets: Succession Wars.

The added catalogue includes:

Examples include:

The preset catalogue now contains well over a hundred BattleMech variants.


Platoon management

18. Platoon Board

Added the beginning of a dedicated BattleTech Platoon Manager.

This is intentionally designed more like a card table than a traditional spreadsheet.

A character created in the AToW character generator becomes a soldier card automatically.

Cards can show:

Soldiers can be dragged between:

Groups can represent:

The board layout and membership are persistent.


19. Platoon accessibility

Originally the Platoon Manager was reachable through:

Campaign → Roster → Platoons

It was then expanded so BattleTech platoons can also be accessed directly from the main Dashboard, making the feature much easier to reach.


20. Soldier status and casualties

Platoon members can carry states such as:

ACTIVE
WOUNDED
INCAPACITATED
KIA
MIA
EVACUATED
RESERVE

Only active soldiers contribute to the active combat calculations.

This means removing or incapacitating a soldier immediately changes platoon statistics without deleting the underlying AToW character.


21. Squad/platoon Skill averaging

The Platoon Manager calculates collective infantry skill values from the individual AToW characters.

This includes:

Missing required Skills are reported rather than silently inventing a value.


22. Infantry motive type

Platoons can now select their conventional infantry movement type:

Foot
Motorized
Jump
Mechanized Hover
Mechanized Tracked
Mechanized Wheeled

The choice affects:

For example, conventional mechanized infantry cannot make Anti-‘Mech attacks, so the manager marks that appropriately.


23. Live platoon speed

The manager displays BattleTech movement and the corresponding AToW tactical distance.

Examples:

3 Ground MP
45 m / 5 seconds

or:

5 Hover MP
75 m / 5 seconds

Jump infantry separately displays jump and ground movement.


24. Support weapon system

Platoons can now be equipped with infantry support weapons.

Current support catalogue includes things such as:

Support weapons are assigned to squads rather than just being freeform equipment.


25. Crew requirements and weapon legality

The manager understands that support weapons need operators and sometimes additional crew.

It checks things such as:

An under-crewed support weapon can therefore be identified automatically.


26. Support weapons affect movement

TechManual support-weapon encumbrance rules are incorporated.

For example, depending on motive type, carrying the maximum secondary support weapons can:

So the displayed platoon speed is not merely a static motive-type value.


27. Dynamic infantry damage

The manager is beginning to act as the conversion layer between individual AToW soldiers and the single conventional-infantry unit used in BattleTech.

The calculation considers:

The platoon therefore loses combat effectiveness automatically as soldiers are killed or removed from active duty.

This is one of the core goals of the platoon system:

Individual AToW soldiers
        ↓
Squads / equipment / weapons
        ↓
Calculated conventional infantry platoon
        ↓
BattleTech tactical-map unit

The final direct deployment/conversion into a tactical infantry token is still a future phase.


Database / backend changes

Vanilla CozyVTT’s database has also been extended to support these features.

Major additions include:

All production schema changes have been handled through Prisma migrations and prisma migrate deploy.

  • Guide: Hosting Valheim server on VPS

    Guide: Hosting Valheim server on VPS

    Henlooo! This little guide is based on my latest experience, when I volunteered myself to host server of Valheim for my dear ones. I was motivated from several sides, one of them were prices for Valheim server hosting and second, I love Linux! I couldn’t slip my chance to dip my toes and try this!

    So! What we gonna need?

    Before Valheim server installation

    Alright, here are some requirements before we start installation.

    Is hosting server free?

    Yes, you do not need to own the game to host the server, also you do not need steam account. We will be using SteamCMD, but it’s not necessary to put there your credentials. Only price you pay is price of server itself.

    Where to get server?

    There are many hosting providers out there, you can google them. I personaly have best experience with AlphaVPS, as their prices are reasonable and their staff is very nice. Also, many VPS providers have in their TOS, that they do not allow game servers on their VPS.

    Do I need experience?

    Not much really, if you will be following this guide and read last part about troubleshooting, I believe anyone with help of searching internet + LLM help, could make it work. Biggest pain in the ass are mods, truly.

    Can I transfer to Valheim Server my existing local world?

    Yes, we will get into it 🙂

    VM requirements

    • Public IPv4
    • I recommend Debian/Ubuntu
    • 2 Cores, 4 GB RAM, 16 GB free disk space
    • SSH access to server

    Base Installation, let’s SSH!

    Install package dependencies:

    sudo dpkg --add-architecture i386
    sudo apt update
    
    sudo apt install -y \
      steamcmd \
      lib32gcc-s1 \
      lib32stdc++6 \
      libatomic1 \
      libpulse0 \
      libpulse-dev \
      unzip
    

    Prepare dedicated user for running the sever, do not add password to user. If you are not using pub/privkey pairs to login, it will increase security of this account.

    sudo adduser \
      --system \
      --group \
      --home /srv/valheim \
      valheim

    Create server directory structure

    sudo install -d -o valheim -g valheim /srv/valheim/server
    sudo install -d -o valheim -g valheim /srv/valheim/data
    sudo install -d -o valheim -g valheim /srv/valheim/data/worlds_local
    sudo install -d -o valheim -g valheim /srv/valheim/backups
    
    sudo chown -R valheim:valheim /srv/valheim/data
    sudo chmod 750 /srv/valheim/data
    sudo chmod 750 /srv/valheim/data/worlds_local

    Test the directory structure and your user permissions. You should get “Directory is Writable

    sudo install -d -o valheim -g valheim -m 0750 /srv/valheim/server
    
    sudo -u valheim test -w /srv/valheim/server \
      && echo "Directory is writable" \
      || echo "Directory is NOT writable"

    Let’s install the valheim server!

    sudo -u valheim /usr/games/steamcmd \
      +@sSteamCmdForcePlatformType linux \
      +force_install_dir /srv/valheim/server \
      +login anonymous \
      +app_update 896660 validate \
      +quit

    Valheim Vanilla Server installation finished

    Alright, now you should have installed server. It is now in Vanilla state, no mods, no imported world.

    If you want just vanilla world, without any mods you can stop here. For now, you need to open ports, first run your server and set configuration.

    Open Firewall ports (Firewalld and UFW)

    #For UFW
    sudo ufw allow 2456/udp
    sudo ufw allow 2457/udp
    sudo ufw reload
    
    #For Firewalld
    sudo firewall-cmd --permanent --zone=public --add-port=2456/udp
    sudo firewall-cmd --permanent --zone=public --add-port=2457/udp
    sudo firewall-cmd --reload
    

    Create simple launch script and give it executable property

    sudo nano /srv/valheim/start-server.sh
    #!/bin/bash
    
    cd /srv/valheim/server || exit 1
    
    export SteamAppId=892970
    export LD_LIBRARY_PATH="./linux64:${LD_LIBRARY_PATH:-}"
    
    exec ./valheim_server.x86_64 \
      -nographics \
      -batchmode \
      -name "NAMEYOURSERVER" \
      -port 2456 \
      -world "NewWorld" \
      -password "PASSWORD_TO_LOGIN" \
      -savedir "/srv/valheim/data" \
      -public 0
    sudo chown valheim:valheim /srv/valheim/start-server.sh
    sudo chmod 750 /srv/valheim/start-server.sh

    Now start the server, it will take while as it will start to generate the world aaaand do bunch of stuff.

    sudo -u valheim /srv/valheim/start-server.sh

    You want to see “Game server connected“. Once that is done, you are finished! Create daemon to automatically start your server and keep it running. And connect to the server using your IP address and port 2456 (Like, 194.55.66.77:2456)!

    sudo nano /etc/systemd/system/valheim.service
    [Unit]
    Description=Valheim modded dedicated server
    Documentation=https://www.valheimgame.com/support/a-guide-to-dedicated-servers/
    Wants=network-online.target
    After=network-online.target
    
    [Service]
    Type=simple
    
    User=valheim
    Group=valheim
    
    WorkingDirectory=/srv/valheim/server
    
    Environment=HOME=/home/valheim
    Environment=SteamAppId=892970
    Environment=SteamGameId=892970
    
    ExecStart=/bin/bash /srv/valheim/server/start-server.sh
    
    # Valheim handles Ctrl+C/SIGINT as a graceful save and shutdown.
    KillSignal=SIGINT
    TimeoutStopSec=180
    
    Restart=on-failure
    RestartSec=15
    
    LimitNOFILE=100000
    UMask=0027
    
    [Install]
    WantedBy=multi-user.target

    Now reload the deamons and enable and start the server.

    sudo systemctl daemon-reload
    sudo systemctl enable --now valheim

    Now you have to wait like 10 minutes before you can log in.

    I want server with mods and transfer my local game!

    Now things get bit complicated, but it is manageable. If you will follow this guide step by step, you should be able to pull this off.

    What to be aware of?

    Most important thing is, that mods on server and on client side (your game), need to match. And that means also by the version.

    If you are also importing your world, you need to save game, and import it without playing on the save. Or there will be save and world mismatch and it can cause troubles.

    How to export existing world?
    • Start Valheim.
    • Open Manage Saves.
    • Select the correct world.
    • Use Move to Local if it is currently a cloud save.
    • Exit Valheim completely.
    • Go to: C:\Users\USERNAME\AppData\LocalLow\IronGate\Valheim\worlds_local\ (Windows)
    • Go to: ~/.config/unity3d/IronGate/Valheim/worlds_local/ (Linux)
    • Get: WorldName.db and WorldName.fwl
    • Upload these files on your server with SFTP
    How to export your mod profile

    I will be honest I was doing this only with Thunderstore so if you use different mod manager, you will have to improvise a bit. For this guide to work you need to have BepInExPack downloaded.

    • Open Thunderstore
    • Open the profile used for this world.
    • Select Settings.
    • Open Profile.
    • Export the profile as a file.
    • Upload profile on your server with SFTP, it will be file like *.r2z or something.
    Let’s assemble and install the mods!

    First let’s import the Thunderstore profile

    sudo install -d -o valheim -g valheim -m 0750 \
      /srv/valheim/import/mods \
      /srv/valheim/import/mods/profile \
      /srv/valheim/import/mods/downloaded-profile
    
    sudo install \
      -o valheim \
      -g valheim \
      -m 0640 \
      /home/user/Default_1785711977993.r2z \
      /srv/valheim/import/mods/Default_1785711977993.r2z

    Let’s Inspect and unpack the profile.

    file /srv/valheim/import/mods/Default_1785711977993.r2z
    
    unzip -l /srv/valheim/import/mods/Default_1785711977993.r2z |
    grep -E 'manifest|profile|BepInEx|plugins|config|patchers' |
    head -100
    
    sudo rm -rf /srv/valheim/import/mods/profile
    
    sudo install -d \
      -o valheim \
      -g valheim \
      /srv/valheim/import/mods/profile
    
    sudo -u valheim unzip \
      /srv/valheim/import/mods/Default_1785711977993.r2z \
      -d /srv/valheim/import/mods/profile

    Now we need to create startup script for our server which will be using bepinex

    nano /srv/valheim/server/start-modded.sh

    Paste this content, do not forget to change the password and name. “world” needs to match exact name of your world save, you exported earlier.

    #!/bin/bash
    set -euo pipefail
    
    cd /srv/valheim/server
    
    exec ./start_server_bepinex.sh \
      -name "Your server name" \
      -port 2456 \
      -world "worldname" \
      -password "YourPassword" \
      -savedir "/srv/valheim/data" \
      -public 1

    Let’s make downloader for mods. This will help you to download your mods to correct folders without worrying too much about it.

    First, we will need our modlist file. Here you will be writing what mods you want installed

    sudo nano /srv/valheim/mods.txt

    Example content

    # Core loader
    denikson-BepInExPack_Valheim-5.4.2333
    
    # Server and client mods, write mods with versions here
    ValheimModding-Jotunn-2.29.2
    plumga-Clutter-0.1.7
    Advize-PlantEverything-1.20.0
    OdinPlus-OdinHorse-1.6.5
    TastyChickenLegs-NoSmokeStayLit-2.3.8
    
    # Keep Valheim Plus absent unless both server and clients use it.
    # Grantapher-ValheimPlus_Grantapher_Temporary-9.17.1

    Set correct permissions

    sudo chown root:valheim /srv/valheim/mods.txt
    sudo chmod 640 /srv/valheim/mods.txt

    Install dependencies

    sudo apt update
    sudo apt install -y python3 python3-yaml unzip wget rsync

    Create the download python script

    sudo nano /usr/local/sbin/download-valheim-mods.py

    And paste these contents!

    #!/usr/bin/env python3
    
    from __future__ import annotations
    
    import json
    import re
    import shutil
    import sys
    import tempfile
    import urllib.error
    import urllib.request
    import zipfile
    from collections import deque
    from dataclasses import dataclass
    from pathlib import Path, PurePosixPath
    
    
    MOD_LIST = Path("/srv/valheim/mods.txt")
    
    STAGE = Path(
        "/srv/valheim/import/mods/downloaded-profile"
    )
    
    CACHE = Path(
        "/srv/valheim/import/mods/package-cache"
    )
    
    LOCK_FILE = Path(
        "/srv/valheim/import/mods/mods-resolved.lock"
    )
    
    METADATA_FILES = {
        "icon.png",
        "manifest.json",
        "readme.md",
        "changelog.md",
        "license",
        "license.md",
    }
    
    VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
    
    
    @dataclass(frozen=True)
    class Package:
        namespace: str
        name: str
        version: str
    
        @property
        def identity(self) -> str:
            return f"{self.namespace}-{self.name}"
    
        @property
        def dependency_string(self) -> str:
            return (
                f"{self.namespace}-{self.name}-{self.version}"
            )
    
        @property
        def archive_name(self) -> str:
            return f"{self.dependency_string}.zip"
    
        @property
        def download_url(self) -> str:
            return (
                "https://thunderstore.io/package/download/"
                f"{self.namespace}/{self.name}/{self.version}/"
            )
    
    
    def parse_dependency_string(value: str) -> Package:
        """
        Parse Namespace-Package-Version.
    
        Split the version from the right because package names may
        contain separators. The remaining first separator divides
        namespace from package name.
        """
        value = value.strip()
    
        try:
            package_identity, version = value.rsplit("-", 1)
            namespace, package_name = package_identity.split("-", 1)
        except ValueError as error:
            raise ValueError(
                "Expected Namespace-Package-Version, got: "
                f"{value!r}"
            ) from error
    
        if not namespace or not package_name:
            raise ValueError(
                f"Invalid package identity: {value!r}"
            )
    
        if not VERSION_PATTERN.fullmatch(version):
            raise ValueError(
                f"Invalid semantic version in {value!r}"
            )
    
        return Package(
            namespace=namespace,
            name=package_name,
            version=version,
        )
    
    
    def read_requested_packages() -> list[Package]:
        if not MOD_LIST.is_file():
            raise FileNotFoundError(
                f"Mod list not found: {MOD_LIST}"
            )
    
        packages: list[Package] = []
    
        with MOD_LIST.open("r", encoding="utf-8") as file:
            for line_number, raw_line in enumerate(file, start=1):
                # Allow comments after an entry as well as full-line
                # comments.
                value = raw_line.split("#", 1)[0].strip()
    
                if not value:
                    continue
    
                try:
                    packages.append(
                        parse_dependency_string(value)
                    )
                except ValueError as error:
                    raise ValueError(
                        f"{MOD_LIST}:{line_number}: {error}"
                    ) from error
    
        if not packages:
            raise RuntimeError(
                f"No packages found in {MOD_LIST}"
            )
    
        return packages
    
    
    def download(package: Package, destination: Path) -> None:
        destination.parent.mkdir(
            parents=True,
            exist_ok=True,
        )
    
        partial = destination.with_suffix(
            destination.suffix + ".partial"
        )
    
        partial.unlink(missing_ok=True)
    
        request = urllib.request.Request(
            package.download_url,
            headers={
                "User-Agent": (
                    "Darkpost-Valheim-Mod-Downloader/2.0"
                )
            },
        )
    
        print(f"  Downloading {package.download_url}")
    
        try:
            with urllib.request.urlopen(
                request,
                timeout=180,
            ) as response:
                with partial.open("wb") as output:
                    shutil.copyfileobj(response, output)
    
            # Test before adding the file to the persistent cache.
            with zipfile.ZipFile(partial) as archive:
                bad_member = archive.testzip()
    
                if bad_member is not None:
                    raise RuntimeError(
                        "Corrupt ZIP member: "
                        f"{bad_member}"
                    )
    
            partial.replace(destination)
    
        except urllib.error.HTTPError as error:
            partial.unlink(missing_ok=True)
    
            raise RuntimeError(
                f"Thunderstore returned HTTP {error.code} for "
                f"{package.dependency_string}"
            ) from error
    
        except Exception:
            partial.unlink(missing_ok=True)
            raise
    
    
    def get_archive(package: Package) -> Path:
        archive = CACHE / package.archive_name
    
        if archive.is_file():
            try:
                with zipfile.ZipFile(archive) as zip_file:
                    bad_member = zip_file.testzip()
    
                    if bad_member is None:
                        print("  Using cached archive")
                        return archive
    
                    print(
                        "  Cached archive is corrupt; "
                        "downloading again"
                    )
            except zipfile.BadZipFile:
                print(
                    "  Cached archive is not a valid ZIP; "
                    "downloading again"
                )
    
            archive.unlink(missing_ok=True)
    
        download(package, archive)
        return archive
    
    
    def safe_extract(
        archive_path: Path,
        destination: Path,
    ) -> None:
        """
        Extract a ZIP while rejecting absolute paths and '..'.
        """
        destination.mkdir(
            parents=True,
            exist_ok=True,
        )
    
        with zipfile.ZipFile(archive_path) as archive:
            for member in archive.infolist():
                member_path = PurePosixPath(member.filename)
    
                if member_path.is_absolute():
                    raise RuntimeError(
                        f"Unsafe absolute ZIP path: "
                        f"{member.filename}"
                    )
    
                if ".." in member_path.parts:
                    raise RuntimeError(
                        f"Unsafe parent path in ZIP: "
                        f"{member.filename}"
                    )
    
            archive.extractall(destination)
    
    
    def find_manifest(extracted: Path) -> Path:
        manifests = [
            path
            for path in extracted.rglob("manifest.json")
            if path.is_file()
        ]
    
        if not manifests:
            raise RuntimeError(
                "Package does not contain manifest.json"
            )
    
        # Thunderstore packages normally place it at archive root.
        manifests.sort(
            key=lambda path: (
                len(path.relative_to(extracted).parts),
                str(path),
            )
        )
    
        return manifests[0]
    
    
    def load_manifest(
        extracted: Path,
        package: Package,
    ) -> dict:
        manifest_path = find_manifest(extracted)
    
        try:
            with manifest_path.open(
                "r",
                encoding="utf-8-sig",
            ) as file:
                manifest = json.load(file)
        except (OSError, json.JSONDecodeError) as error:
            raise RuntimeError(
                f"Cannot read manifest for "
                f"{package.dependency_string}: {error}"
            ) from error
    
        dependencies = manifest.get("dependencies", [])
    
        if not isinstance(dependencies, list):
            raise RuntimeError(
                f"Invalid dependencies field in "
                f"{package.dependency_string}"
            )
    
        return manifest
    
    
    def copy_item(
        source: Path,
        destination: Path,
    ) -> None:
        destination.parent.mkdir(
            parents=True,
            exist_ok=True,
        )
    
        if source.is_dir():
            shutil.copytree(
                source,
                destination,
                dirs_exist_ok=True,
            )
        else:
            shutil.copy2(source, destination)
    
    
    def copy_contents(
        source: Path,
        destination: Path,
    ) -> None:
        destination.mkdir(
            parents=True,
            exist_ok=True,
        )
    
        for item in source.iterdir():
            copy_item(
                item,
                destination / item.name,
            )
    
    
    def install_package(
        extracted: Path,
        package: Package,
    ) -> None:
        if package.name == "BepInExPack_Valheim":
            startup_scripts = list(
                extracted.rglob("start_server_bepinex.sh")
            )
    
            if not startup_scripts:
                raise RuntimeError(
                    "BepInExPack_Valheim does not contain "
                    "start_server_bepinex.sh"
                )
    
            # The startup script should be in the directory that
            # represents the Valheim installation root.
            package_root = startup_scripts[0].parent
    
            copy_contents(package_root, STAGE)
            return
    
        handled_layout = False
    
        # Normal layout:
        #
        # BepInEx/plugins
        # BepInEx/config
        # BepInEx/patchers
        for bepinex_directory in extracted.rglob("BepInEx"):
            if not bepinex_directory.is_dir():
                continue
    
            copy_contents(
                bepinex_directory,
                STAGE / "BepInEx",
            )
    
            handled_layout = True
    
        # Some packages start directly with plugins/, config/, etc.
        for directory_name in (
            "plugins",
            "patchers",
            "config",
            "core",
            "monomod",
        ):
            for directory in extracted.rglob(directory_name):
                if not directory.is_dir():
                    continue
    
                if "BepInEx" in directory.parts:
                    continue
    
                copy_contents(
                    directory,
                    STAGE / "BepInEx" / directory_name,
                )
    
                handled_layout = True
    
        # Loose-package fallback. Keep the complete package together,
        # including asset bundles, translations and supporting data.
        if not handled_layout:
            destination = (
                STAGE
                / "BepInEx"
                / "plugins"
                / package.identity
            )
    
            destination.mkdir(
                parents=True,
                exist_ok=True,
            )
    
            for item in extracted.iterdir():
                if item.name.lower() in METADATA_FILES:
                    continue
    
                copy_item(
                    item,
                    destination / item.name,
                )
    
    
    def resolve_and_install(
        requested: list[Package],
    ) -> dict[str, Package]:
        queue: deque[Package] = deque(requested)
    
        resolved: dict[str, Package] = {}
        installed_order: list[Package] = []
    
        while queue:
            package = queue.popleft()
    
            existing = resolved.get(package.identity)
    
            if existing is not None:
                if existing.version != package.version:
                    raise RuntimeError(
                        "Dependency version conflict:\n"
                        f"  {existing.dependency_string}\n"
                        f"  {package.dependency_string}\n"
                        "Choose one compatible version explicitly."
                    )
    
                continue
    
            print(
                f"[{len(resolved) + 1}] "
                f"{package.dependency_string}"
            )
    
            archive = get_archive(package)
    
            with tempfile.TemporaryDirectory(
                prefix="valheim-mod-"
            ) as temporary_directory:
                extracted = Path(temporary_directory)
    
                safe_extract(archive, extracted)
    
                manifest = load_manifest(
                    extracted,
                    package,
                )
    
                manifest_name = manifest.get("name")
                manifest_version = manifest.get("version_number")
    
                if (
                    manifest_version
                    and manifest_version != package.version
                ):
                    raise RuntimeError(
                        "Downloaded package version mismatch: "
                        f"requested {package.version}, "
                        f"manifest contains {manifest_version}"
                    )
    
                print(
                    f"  Package manifest: "
                    f"{manifest_name or package.name} "
                    f"{manifest_version or package.version}"
                )
    
                dependencies = manifest.get(
                    "dependencies",
                    [],
                )
    
                for dependency_value in dependencies:
                    dependency = parse_dependency_string(
                        dependency_value
                    )
    
                    print(
                        "  Requires: "
                        f"{dependency.dependency_string}"
                    )
    
                    queue.append(dependency)
    
                install_package(
                    extracted,
                    package,
                )
    
            resolved[package.identity] = package
            installed_order.append(package)
    
        return resolved
    
    
    def write_lock_file(
        resolved: dict[str, Package],
    ) -> None:
        LOCK_FILE.parent.mkdir(
            parents=True,
            exist_ok=True,
        )
    
        temporary = LOCK_FILE.with_suffix(".lock.partial")
    
        with temporary.open(
            "w",
            encoding="utf-8",
        ) as file:
            file.write(
                "# Resolved Valheim Thunderstore packages\n"
            )
            file.write(
                "# Generated automatically; do not edit.\n\n"
            )
    
            for package in sorted(
                resolved.values(),
                key=lambda item: item.identity.lower(),
            ):
                file.write(
                    package.dependency_string + "\n"
                )
    
        temporary.replace(LOCK_FILE)
    
    
    def main() -> int:
        try:
            requested = read_requested_packages()
    
            print(f"Mod list: {MOD_LIST}")
            print(f"Requested root packages: {len(requested)}")
            print()
    
            shutil.rmtree(
                STAGE,
                ignore_errors=True,
            )
    
            STAGE.mkdir(
                parents=True,
                exist_ok=True,
            )
    
            CACHE.mkdir(
                parents=True,
                exist_ok=True,
            )
    
            resolved = resolve_and_install(requested)
    
            write_lock_file(resolved)
    
            dll_directory = STAGE / "BepInEx"
            dll_files = (
                list(dll_directory.rglob("*.dll"))
                if dll_directory.exists()
                else []
            )
    
            print()
            print("Staging completed successfully.")
            print(f"Requested packages: {len(requested)}")
            print(f"Resolved packages:  {len(resolved)}")
            print(f"DLL files found:    {len(dll_files)}")
            print(f"Staging directory:  {STAGE}")
            print(f"Resolved lock file: {LOCK_FILE}")
    
            return 0
    
        except KeyboardInterrupt:
            print("\nInterrupted.", file=sys.stderr)
            return 130
    
        except Exception as error:
            print(
                f"ERROR: {error}",
                file=sys.stderr,
            )
            return 1
    
    
    if __name__ == "__main__":
        raise SystemExit(main())

    Set permissions once more

    sudo chown root:root /usr/local/sbin/download-valheim-mods.py
    sudo chmod 750 /usr/local/sbin/download-valheim-mods.py

    Download the mods to staging

    sudo /usr/local/sbin/download-valheim-mods.py

    You should see something like this. Now the mods are downloaded to staging folder. They are not yet on your server. That will be next step.

    Mod list: /srv/valheim/mods.txt
    Requested root packages: 6
    
    [1] denikson-BepInExPack_Valheim-5.4.2333
      Downloading ...
      Package manifest: BepInExPack_Valheim 5.4.2333
    [2] ValheimModding-Jotunn-2.29.2
      Downloading ...
    ...
    Staging completed successfully.
    Requested packages: 6
    Resolved packages:  6
    DLL files found:    ...
    Staging directory:  /srv/valheim/import/mods/downloaded-profile
    Resolved lock file: /srv/valheim/import/mods/mods-resolved.lock

    Move mods from staging to production

    sudo rsync -a \
      /srv/valheim/import/mods/downloaded-profile/ \
      /srv/valheim/server/
    
    sudo chown -R valheim:valheim /srv/valheim/server
    
    sudo chmod 750 \
      /srv/valheim/server/start_server_bepinex.sh \
      /srv/valheim/server/start-modded.sh
    And now the map

    To install your map you played on in your local game, place .db and .fwl files into this location

    sudo install \
      -o valheim \
      -g valheim \
      -m 0640 \
      /home/user/worlds_local/worldname.db \
      /srv/valheim/data/worlds_local/worldname.db
    
    sudo install \
      -o valheim \
      -g valheim \
      -m 0640 \
      /home/user/worlds_local/worldname.fwl \
      /srv/valheim/data/worlds_local/worldname.fwl

    Now let’s verify that our user can write to those files!

    sudo -u valheim test -r \
      /srv/valheim/data/worlds_local/SiiskusLefLeg.db &&
    echo "DB readable"
    
    sudo -u valheim test -w \
      /srv/valheim/data/worlds_local/SiiskusLefLeg.db &&
    echo "DB writable"
    
    sudo -u valheim test -r \
      /srv/valheim/data/worlds_local/SiiskusLefLeg.fwl &&
    echo "FWL readable"
    
    sudo -u valheim test -w \
      /srv/valheim/data/worlds_local/SiiskusLefLeg.fwl &&
    echo "FWL writable"
    Let’s create daemon and start the server
    nano /etc/systemd/system/valheim.service
    
    [Unit]
    Description=Valheim modded dedicated server
    Wants=network-online.target
    After=network-online.target
    
    [Service]
    Type=simple
    
    User=valheim
    Group=valheim
    
    WorkingDirectory=/srv/valheim/server
    
    ExecStart=/srv/valheim/server/start-modded.sh
    
    Restart=on-failure
    RestartSec=10
    
    KillSignal=SIGINT
    TimeoutStopSec=180
    
    StandardOutput=journal
    StandardError=journal
    
    # Basic limits
    LimitNOFILE=100000
    
    [Install]
    WantedBy=multi-user.target
    sudo chown valheim:valheim \
      /srv/valheim/server/start-modded.sh \
      /srv/valheim/server/start_server_bepinex.sh
    
    sudo chmod 750 \
      /srv/valheim/server/start-modded.sh \
      /srv/valheim/server/start_server_bepinex.sh
    sudo systemctl daemon-reload
    sudo systemctl enable --now valheim
    How to update mods?

    To update mods, just change versions in mods.txt and rerun the command for downloading mods. And then deploy. You can bundle it in this script.

    nano /usr/local/sbin/update-valheim-mods

    Paste content

    #!/usr/bin/env bash
    set -Eeuo pipefail
    
    SERVICE="valheim"
    
    VALHEIM_ROOT="/srv/valheim"
    SERVER_DIR="${VALHEIM_ROOT}/server"
    DATA_DIR="${VALHEIM_ROOT}/data"
    BACKUP_DIR="${VALHEIM_ROOT}/backups"
    
    MOD_LIST="${VALHEIM_ROOT}/mods.txt"
    DOWNLOADER="/usr/local/sbin/download-valheim-mods.py"
    
    STAGE_DIR="${VALHEIM_ROOT}/import/mods/downloaded-profile"
    STAGE_BEPINEX="${STAGE_DIR}/BepInEx"
    
    ACTIVE_BEPINEX="${SERVER_DIR}/BepInEx"
    ACTIVE_PLUGINS="${ACTIVE_BEPINEX}/plugins"
    ACTIVE_CONFIG="${ACTIVE_BEPINEX}/config"
    
    STAMP="$(date +%F-%H%M%S)"
    BACKUP_FILE="${BACKUP_DIR}/pre-mod-update-${STAMP}.tar.gz"
    OLD_PLUGINS="${SERVER_DIR}/BepInEx/plugins.before-${STAMP}"
    
    SERVER_WAS_ACTIVE=0
    DEPLOYMENT_STARTED=0
    
    
    log() {
        printf '[%s] %s\n' "$(date '+%F %T')" "$*"
    }
    
    
    die() {
        printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2
        exit 1
    }
    
    
    rollback() {
        local exit_code=$?
    
        if [[ "$DEPLOYMENT_STARTED" -ne 1 ]]; then
            exit "$exit_code"
        fi
    
        log "Update failed. Attempting rollback."
    
        systemctl stop "$SERVICE" 2>/dev/null || true
    
        if [[ -f "$BACKUP_FILE" ]]; then
            log "Restoring BepInEx and world backup."
    
            rm -rf "$ACTIVE_BEPINEX"
    
            tar -C "$VALHEIM_ROOT" -xzf "$BACKUP_FILE" || {
                printf 'Rollback extraction failed: %s\n' \
                    "$BACKUP_FILE" >&2
                exit "$exit_code"
            }
    
            chown -R valheim:valheim \
                "$SERVER_DIR" \
                "$DATA_DIR"
        elif [[ -d "$OLD_PLUGINS" ]]; then
            log "Restoring previous plugin directory."
    
            rm -rf "$ACTIVE_PLUGINS"
            mv "$OLD_PLUGINS" "$ACTIVE_PLUGINS"
            chown -R valheim:valheim "$ACTIVE_PLUGINS"
        fi
    
        if [[ "$SERVER_WAS_ACTIVE" -eq 1 ]]; then
            log "Starting rolled-back server."
            systemctl start "$SERVICE" || true
        fi
    
        printf '\nUpdate failed. Backup retained at:\n%s\n' \
            "$BACKUP_FILE" >&2
    
        exit "$exit_code"
    }
    
    
    trap rollback ERR
    
    
    require_root() {
        if [[ "$EUID" -ne 0 ]]; then
            die "Run this script as root or with sudo."
        fi
    }
    
    
    check_requirements() {
        local command_name
    
        for command_name in \
            systemctl \
            tar \
            rsync \
            find \
            grep \
            sed
        do
            command -v "$command_name" >/dev/null 2>&1 ||
                die "Required command not found: $command_name"
        done
    
        [[ -x "$DOWNLOADER" ]] ||
            die "Downloader is missing or not executable: $DOWNLOADER"
    
        [[ -f "$MOD_LIST" ]] ||
            die "Mod list not found: $MOD_LIST"
    
        [[ -d "$SERVER_DIR" ]] ||
            die "Server directory not found: $SERVER_DIR"
    
        [[ -d "${DATA_DIR}/worlds_local" ]] ||
            die "World directory not found: ${DATA_DIR}/worlds_local"
    
        install -d \
            -o valheim \
            -g valheim \
            -m 0750 \
            "$BACKUP_DIR"
    }
    
    
    show_requested_mods() {
        log "Requested root packages:"
    
        grep -Ev '^[[:space:]]*(#|$)' "$MOD_LIST" |
            sed 's/^/  /'
    }
    
    
    build_staging() {
        log "Downloading mods and resolving dependencies."
    
        "$DOWNLOADER"
    
        [[ -d "$STAGE_BEPINEX" ]] ||
            die "Downloader did not create: $STAGE_BEPINEX"
    
        [[ -d "${STAGE_BEPINEX}/plugins" ]] ||
            die "Staged plugin directory is missing."
    
        local dll_count
    
        dll_count="$(
            find "$STAGE_BEPINEX" \
                -type f \
                -iname '*.dll' |
            wc -l
        )"
    
        [[ "$dll_count" -gt 0 ]] ||
            die "No DLL files were found in staging."
    
        log "Staging contains ${dll_count} DLL files."
    }
    
    
    stop_server() {
        if systemctl is-active --quiet "$SERVICE"; then
            SERVER_WAS_ACTIVE=1
            log "Stopping ${SERVICE}.service."
            systemctl stop "$SERVICE"
        else
            log "${SERVICE}.service is already stopped."
        fi
    
        if systemctl is-active --quiet "$SERVICE"; then
            die "Valheim service did not stop."
        fi
    }
    
    
    create_backup() {
        log "Creating backup: $BACKUP_FILE"
    
        local paths=()
    
        [[ -d "$ACTIVE_BEPINEX" ]] &&
            paths+=("server/BepInEx")
    
        [[ -d "${DATA_DIR}/worlds_local" ]] &&
            paths+=("data/worlds_local")
    
        [[ -f "$MOD_LIST" ]] &&
            paths+=("mods.txt")
    
        [[ "${#paths[@]}" -gt 0 ]] ||
            die "Nothing was found to back up."
    
        tar -C "$VALHEIM_ROOT" \
            -czf "$BACKUP_FILE" \
            "${paths[@]}"
    
        test -s "$BACKUP_FILE" ||
            die "Backup file is empty."
    }
    
    
    deploy_plugins() {
        DEPLOYMENT_STARTED=1
    
        if [[ -d "$ACTIVE_PLUGINS" ]]; then
            log "Moving current plugins to:"
            log "$OLD_PLUGINS"
    
            mv "$ACTIVE_PLUGINS" "$OLD_PLUGINS"
        fi
    
        install -d \
            -o valheim \
            -g valheim \
            -m 0750 \
            "$ACTIVE_PLUGINS"
    
        log "Deploying fresh plugin set."
    
        rsync -a \
            "${STAGE_BEPINEX}/plugins/" \
            "${ACTIVE_PLUGINS}/"
    }
    
    
    deploy_supporting_files() {
        log "Updating BepInEx loader and supporting files."
    
        rsync -a \
            --exclude='BepInEx/plugins/' \
            --exclude='BepInEx/config/' \
            "${STAGE_DIR}/" \
            "${SERVER_DIR}/"
    
        if [[ -d "${STAGE_BEPINEX}/config" ]]; then
            log "Adding new configuration files without overwriting existing settings."
    
            install -d \
                -o valheim \
                -g valheim \
                -m 0750 \
                "$ACTIVE_CONFIG"
    
            rsync -a \
                --ignore-existing \
                "${STAGE_BEPINEX}/config/" \
                "${ACTIVE_CONFIG}/"
        fi
    }
    
    
    fix_permissions() {
        log "Fixing ownership and executable permissions."
    
        chown -R valheim:valheim "$SERVER_DIR"
    
        find "$SERVER_DIR" \
            -type d \
            -exec chmod 0750 {} +
    
        find "$SERVER_DIR" \
            -type f \
            -name '*.sh' \
            -exec chmod 0750 {} +
    
        if [[ -f "${SERVER_DIR}/valheim_server.x86_64" ]]; then
            chmod 0750 "${SERVER_DIR}/valheim_server.x86_64"
        fi
    }
    
    
    start_and_verify() {
        log "Starting ${SERVICE}.service."
    
        systemctl start "$SERVICE"
    
        sleep 15
    
        if ! systemctl is-active --quiet "$SERVICE"; then
            systemctl status "$SERVICE" \
                --no-pager \
                -l || true
    
            die "Valheim service failed to remain active."
        fi
    
        local start_time
        local journal
    
        start_time="$(
            systemctl show "$SERVICE" \
                -p ExecMainStartTimestamp \
                --value
        )"
    
        journal="$(
            journalctl \
                -u "$SERVICE" \
                --since "$start_time" \
                --no-pager
        )"
    
        if grep -Eqi \
            'missing dependency|mod version mismatch|failed to load|fatal|unauthorizedaccessexception' \
            <<<"$journal"
        then
            printf '%s\n' "$journal" |
                grep -Ei -B5 -A15 \
                    'missing dependency|mod version mismatch|failed to load|fatal|unauthorizedaccessexception' \
                >&2 || true
    
            die "A critical error was detected in the startup log."
        fi
    
        log "Service is active."
    
        printf '\nLoaded BepInEx plugins:\n'
    
        if [[ -f "${ACTIVE_BEPINEX}/LogOutput.log" ]]; then
            grep 'Loading \[' \
                "${ACTIVE_BEPINEX}/LogOutput.log" |
                sed -E 's/^.*Loading \[([^]]+)\].*$/  \1/' ||
                true
        else
            printf '  BepInEx log has not been created yet.\n'
        fi
    
        printf '\nRelevant server startup lines:\n'
    
        printf '%s\n' "$journal" |
            grep -Ei \
                'setting -savedir|load world:|loading [0-9]+ zdos|game server connected|opened steam server|chainloader startup complete' ||
            true
    }
    
    
    remove_old_plugin_backup() {
        if [[ -d "$OLD_PLUGINS" ]]; then
            log "Previous plugins retained for rollback:"
            log "$OLD_PLUGINS"
        fi
    }
    
    
    main() {
        require_root
        check_requirements
        show_requested_mods
        build_staging
        stop_server
        create_backup
        deploy_plugins
        deploy_supporting_files
        fix_permissions
        start_and_verify
        remove_old_plugin_backup
    
        DEPLOYMENT_STARTED=0
        trap - ERR
    
        printf '\nMod update completed successfully.\n'
        printf 'Backup: %s\n' "$BACKUP_FILE"
    }
    
    
    main "$@"

    Set permissions

    sudo chown root:root /usr/local/sbin/update-valheim-mods
    sudo chmod 750 /usr/local/sbin/update-valheim-mods

    EXECUTE!

    sudo /usr/local/sbin/update-valheim-mods

    Server is not working, I cannot connect?

    During the process a lot of things can mess up, do not feel bad for it. Luckily the server itself is rather talkative. You can inpect what is going on by running launch scripts manualy, or check logs of the service itself.

    sudo journalctl -u valheim -f

    This is stage, where any LLM is extremely helpful and may help you to debug the whole mess. I spent two days on making this work and it was fun experience and when my friends finally landed on server and I saw them playing, my heart got warmer exactly by 3 degrees.

    Anyhow! I wish you luck, and if you run into any problems, leave comment, I will try to help!

  • Star Trek Online: Sahni Nethalis

    Star Trek Online: Sahni Nethalis

  • Service record:
  • Wealth & Property
  • Contacts
  • Crew
  • I.K.S. Poregu Bird-of-Prey
  • Basic information

    Name: Marauder Captain Sahni Nethalis
    Rank: Captain
    Ship: I.K.S. Poregu – Bird of Prey (Comissioned)
    Ship: I.K.S. Oromvu – Maurauding Carrier
    House: Nethalis (Insignificant)
    Assigment: Maurauding corps.
    Birtplace: Qo’nos

    MMORPG Star Trek Online character.

    Physical description

    179 cm tall Orion Female with light green skin, dark eyes and red dyed hair. No visible scarring, on body. Given her Orion origin, she possesses slightly larger strength then human despite her lean and slender body. Like all Orions tests showed, that she inherited her kind radiation tolerance and affecting pheromones.

    Psychological profile

    Captain Sahni Nethalis is an aggressive, decisive officer with exceptional tactical instinct. She consistently accomplishes mission objectives, particularly during boarding actions and commerce raiding, often through unconventional means.

    The subject routinely disregards standing orders when convinced a greater victory can be achieved. Multiple disciplinary actions have failed to alter this behavior. She accepts punishment without complaint and repeats the offense when circumstances demand it.

    Fiercely loyal to the Empire, Captain Nethalis values victory above strict obedience. She is highly effective in combat but poorly suited to politically sensitive operations.

    <>

    Service record:

    Dominion War Aftermath: Joined Bird-of-prey Dre’Kar as young warrior in need of new conscripts

    Battle of Betha Penthe: Defeated and killed Captain of Dre’Kar in duel, after captain issued command to stop combat with Federation vessel and abandon ship. After assuming command, with deceit she and crew managed to force Federation vessel into surrender.

    Klingon Academy Trials: Passed trials during program for ambitious warriors. Left with rank Marauder Captain.

    Bloodbath of T’Ong Nebula: Suspended for year, and punished by “Path of Pain” and Rura Penthe labor for 2 months to repent, after recklessly mistaking civilian vessel for smuggler vessel.

    Battle of Dyson Sphere: Awarded with Badge of Honor for bravery during encirclement of her crew in depth of Dyson cities. Sahni Nethalis hold of 98 of Voth soldiers supported by Mechs with her deployment of 43 warriors and two armored vehicles.

    Capture of Orion Syndicate Carrier: With her crew, she intercepted Orion Syndicate carrier in Khaless Expanse nebula, and boarded it with her crew. After defeating crew, she took command of vessel.


    Skills: Engineering, Melee Combat, Command, Pilot Class A
    Minor skills: Shooting, Cooking, First Aid.

    Wealth & Property

    –No Records–

    Contacts

    Captain Rebbeca USS Scherzo
    Full Image


    Captain Rebecca Quinn
    Commissioned: U.S.S. Scherzo
    Designation: Tactical Officer

    First Encounter: Deep Space 9
    Origin: Sol (Ireland)

    Relationship: Friendship

    Deployment: Alpha Quadrant

    Crew

    Sahni Nethalis and her crew posing before
    First Officer

    Name: Inene
    Race: Orion Female
    Rank: La’ (Alliance: Commander)
    Designation: Cloaking device Engineer Specialist

    Description:
    Smaller Orion female with crimson hair, usually worn loose. Commander Inene usually wears tough leather overall with yellow marking of engineering department, adopted from color scheme of Starfleet.

    Service History:
    Commander Inene enlisted together with Captain Sahni on Bird-of-prey Dre’Kar, being one of original crew. Commander Inene had direct impact on success of Sahni nethalis mutiny against standing captain. After Sahni Nethalis assumed control over the ship, Inene took position of First Officer on ship. Commander Inene followed Captain Nethalis on other ships, it is more then obvious, that two women have tight bond.

    Accolades: Badge of Honor from battle of Dyson sphere.

    Chief Medical Officer

    Name: Ml’ck
    Race: Gorn Male
    Rank: Lagh (Alliance: Jr. Lieutenant)
    Designation: Chief Medical Officer

    Description:
    Hulking Gorn Male with sharp teeth, claws and mind. Ml’ck looks as similar as other gorns, if he would be wearing harness, he might blend in crowd of other Gorns. Distinguishing feature is his white doctor coat, which he wears in any circumstance.

    Service History:
    After graduation from Qo’nos academy, he was assigned to ship I.K.S. Poregu under Captain Sahni command. Rumors on board of ship say, that Captain handpicked him, as only Gorn can wrestle with uncooperative Klingon patients, or those in shock.

    Accolades: Badge of Honor from battle of Dyson sphere.

    Chief Science Officer

    Name: Dehur
    Race: Orion Female
    Rank: Lagh (Alliance: Jr. Lieutenant)
    Designation: Chief Science Officer

    Description:
    Average Orion Female, with pale green skin, short black hair wearing long lab coat of Scientific Officer.

    Service History:
    Dehur is one of original crew members of Bird-of-prey Dre’Kar. Despite fact that Dehur didn’t join mutiny and stayed aside from it, she joined Captain Sahni side aftewards. Dehur seems to be logical and cold mind on board of I.K.S. Poregu. Dehurs main duties are tied to astrometry, exobiology and optometrics.

    Accolades: Badge of Honor from battle of Dyson sphere.

    Chief of Security

    Name: Xunov
    Race: Orion Male
    Rank: Sogh (Alliance: Lieutenant)
    Designation: Chief of Security

    Description:
    Well formed Orion Male with brown hair and prominent jawline and chin. Xunov somewhat looks like cliche soldier and romancer. On board of I.K.S Poregu, his appearance is often target of joke, calling him Orion Kirk or William Riker.

    Service History:
    Enlisted on I.K.S. Poregu after successful mutiny to fill empty slot of Boarding party Officer. Xunov served under Captain Nethalis for a long time and after Battle of Dyson sphere was promoted to Chief of Security.

    Accolades: Badge of Honor from battle of Dyson sphere.

    Chief of Operations

    Name: Lynish
    Race: Orion Female
    Rank: Sogh (Alliance: Lieutenant)
    Designation: Chief of Operations

    Description:
    Orion Female with black hair, blue-green skin and lithe frame. Similar to Inene wearing often full-body overall uniform.

    Service History:
    Fresh from Qo’nos Academy, enlisted on I.K.S. Poregu quickly took position of Chief of Operations and became ships logistical sorceress. Since her arrival Captain Sahni never had to think about quantum torpedos, phaser and disruptor batteries, mines, bloodwine and supply of Gagh. Lynish tends to rather stand in the background, keeping records and ledgers of the ship. It’s her responsibility that ship is stocked.

    Rumors says, that Lynish has ties to black market and secretly earns crew extra Latinum through their marauding raids. Although, these allegations were never proved.

    Boarding Party Officer

    Name: Nesh
    Race: Orion Female
    Rank: Lagh (Alliance: Jr. Lieutenant)
    Designation: Boarding Party Officer (BPO)

    Description:
    Strong and toned Orion Female distinctive by her ginger hair. Lagh Nesh favors rather intricate spins of stock uniforms, trying to distinguish herself from rest of the crew.

    Service History:
    Former Tactical Officer on I.K.S T’Rogh, which was destroyed during Dyson Sphere operation. Lagh Nesh was saved from wreckage by I.K.S Poregu. Honor bound to ship and it’s captain for saving her life, she joined I.K.S. Poregu crew.

    After promotion of Xunov, Nesh took position of BPO, leading most boarding efforts and deployments on ground operations. Together with Xunov, she is closest to what could be called a Marine.

    Chief of Engineering

    Name: Kovlavt
    Race: Klingon Female
    Rank: Lagh (Alliance: Jr. Lieutenant)
    Designation: Chief of Engineering

    Description:
    Slender Klingon Female with subtle ridges and short curly hair.

    Service History:
    Enlisted on I.K.S. Tovh’ra battlecruiser as NCO. After promotion among to Lagh rank, demanded being assigned to BoP warship to earn Honor and fame.

    Kovlavt graduated as Warp Core Specialist from Klingon Academy. She is responsible for juryrigging experimental Bajoran Warp Core to I.K.S. Poregu.

    Brig Officer

    Name: Zakath
    Race: Klingon Female
    Rank: Lagh (Alliance: Jr. Lieutenant)
    Designation: Brig Officer

    Description:
    Bulky Klingon female with stern look, long curly hair and sharp features in face. Zakath is very intimidating Klingon woman, with rough and growly voice.

    Service History:
    From K.D.F.M.P (Klingon Defense Force Military Police, unlike rest of crew, Zakath asked to serve on board of Captains Nethalis ship after her sentence on Rura Penthe. Her motivation is unknown, as Zakath tends to herself and she is rather new addition to I.K.S. crew. There is suspicion she is Intelligence Officer or assigned by High Council to keep Marauder captain in check.

    Zakath main responsibilities on ship consist of: Interrogations, guarding prisoners, keeping captive database, appraisal of captives, sorting of captives, managing brig.

    I.K.S. Poregu Bird-of-Prey

    B'Rel Klingon Warship
  • Shadowrun 5: The Grinman (Toxic Shinto Shaman)

    Shadowrun 5: The Grinman (Toxic Shinto Shaman)

    Okay, so I was thinking about writing some villain. And this idea came to my mind. Toxic Shinto Shaman. I never wrote toxic magicians, so this is my firs one, bear with me.

    Urban legend of The Grinman

    Nobody know his original name, in bustling Neo-Tokyo is very easy to lose your name, even your face. Legends say, that he used to be Shinto shaman, taking care one of the many shrines in Neo-Tokyo.

    He was kind-hearted, humble and gods favored him. But, darkness was circling around his wife, and he wasn’t able to stop it. He failed to notice change in her behavior. One day, when he came to their humble home, he had strange feeling something terrible happened.

    His beloved wife, haunted by dark thoughts jumped under the train. Crushed by sorrow, humble priest descended into the the madness.

    It is said that he now wanders the city’s forgotten stations, maintenance shafts and flooded tunnels. He summons vile spirits, sends them to haunt people. Other say, that with different faces he waits on Railway stations porches and with power of suggestion, lures poor souls to jump under the trains. Where he appears accident happens, and most likely tragic.

    Many claim, that his most favorite spirit are The Hands. Haunting stories tell tales, those hands are hands of his deceased wife. That he corrupted her innocent spirit and now calls her hands to do his biding and torment people.

    Rumors

    • “If a smiling man asks whether you hear the train, don’t answer.”
    • “People who see wet handprints near the tunnel walls should leave immediately.”
    • “Construction deaths spike whenever someone reports laughter in sealed shafts.”
    • “The Hands don’t kill first. They herd.”
    • “He can’t make you jump if you never meet his eyes.”
    • “He stands where no train stops anymore.”

    Stats

    The Grinman

    The grinning japanesese man, toxic shaman

    Substats

    • Initiative: 9 + 1D6 (Physical); 10 + 2D6 (Astral)
    • Condition Monitor (P/S): 10 / 11
    • Limits: Physical 4, Mental 6, Social 8
    • Armor: 9 (Lined Coat or armored clothing)

    Active Skills

    • Sorcery Group: 6 (Spellcasting, Counterspelling, Ritual Spellcasting)
    • Conjuring Group: 6 (Summoning, Binding, Banishing)
    • Influence Group: 5 (Con, Etiquette, Negotiation)
    • Perception: 5
    • Sneaking (Urban): 4
    • Assensing: 5Astral Combat: 4

    Qualities

    • Mentor Spirit (Doom): Provides bonuses to destruction-based magic; embodies the desire to end life and hastening an “apocalypse” for those he targets.
    • Bad Rep: Known as a malevolent urban myth inhabiting the city’s tunnels.
    • Guts: +2 to resist fear and intimidation.
    • Focused Concentration 3: Allows him to sustain mental suggestions without penalty

    Spells

    • Influence: Used for his “whispered suggestions,” implanting post-hypnotic commands that victims carry out as their own ideas.
    • Control Thoughts: Nudges people toward fatal movements or losing their grip.
    • Chaotic World: Induces hesitation and confusion, causing victims to falter at critical moments.
    • Pollutant Stream: A specialized toxic spell that blasts targets with a concentrated stream of filth.
    • Physical Mask: Maintains his calm, polite appearance to hide his true, warped nature.Silence: Used to move through maintenance shafts and tunnels without sound

    The Hands

    The hands crawling through tunnels
    • Initiative: 15 + 2D6 (Physical); 13 + 3D6 (Astral)
    • Skills: Assensing, Astral Combat, Con, Gymnastics, Intimidation, Perception, Unarmed Combat

    Powers

    • Energy Drain (Karma): Feeds on the negative emotional energy and disrupted fate of his victims.
    • Compulsion (Sorrow): Deepens loss and loneliness, nudging victims toward suicide.
    • Shadow Cloak: Allows the Hands to reach out from deep, supernatural shadows.
    • Silence: Ensures the Hands can grasp victims without a sound.
    • Materialization: Used to manifest as enormous gray hands in the physical world.
    • Fear: Living creatures are struck by unreasoning terror when the Hands appear
  • Shadowrun 5: Unique uses of Astral Projection.

    Shadowrun 5: Unique uses of Astral Projection.

    In many campaigns, Astral Projection is overlooked, or it is only used as scouting tool. We all know it. Magician will use astral projection to get close to the target, and scout area and then it will give information to the team.

    But, when you think about it, isn’t it too narrow minded, or too little? This power is unlocked only to full mages, and there should be some more utility to it, right?

    Through years of playing magician in Shadowrun, I encountered several situations, where astral projection was more just scouting tool, and in some cases completely turned tide of battle. Let’s look into those!

    Breaking stalemate aka Astral “Boo!”

    I rank this one up, because it was most funny one. Our group was pinned down by very competent killer, and they couldn’t move. Trying to advance would mean serious injuries as killer was holding the stairs. Situation was dire and grim, as fast response team was on the way, and something had to be done.

    Magician of the group got an idea, they ran into toilet on bottom floor, locked themself in it and declared astral projection. As shoot out continued, the mage manifested behind the killer and did “Boo!”.

    How would professional killer react in such manner, they know there shouldn’t be anyone behind them. They looked over over startled and saw astral manifestation of the mage. This little opening gave rest of the party enough time, to make breakthrough and take the killer out.

    Simple, effective, no rolls, just RP. It can be done in many more situations. Especially if astral form of mage is well described to GM. You see, NPCs are not masters of magic most of the time, so they might confuse mage form for a spirit and focus fire on the form, instead of party. Until, they realize that it does nothing. But, even one round can give enough opening to change course of battle.

    Also this is perfect opportunity to use Intimidate skill, as target can hear you and see you.

    Meeting contacts while projecting

    This one is more straightforward. Is meeting contact too dangerous? Is it too far? This is good use of astral projection. Meet the contact while manifesting.

    There is a catch though. You can’t write down anything, you can’t see screens and what is written on the papers, so every information has to be said verbally to you. Why this matters?

    Because as player you can write it down, but your character can’t. In this type of encounters excel Hermetic Mages over Shamans, as their Logic gives them higher chances on Memory tests.

    Yes, GMs! If your mage player wants to do legwork through astral perception, make them do memory tests on every piece of information. This punishes dump stats, and makes game more believable.

    When someone describes you whole floor plan by words, if you are not genius with trained memory, you will bring back only fragments of it.

    Bad day to be Dual-Natured.

    Paracritter hunting

    This is where you use your Parazoologist contacts, and your Parazoology Knowledge Skill. It can be good easy game session to make money.

    Find paracritter, which is dual-natured and has no means to defend itself in ranged combat. Use your spirits to find such critter and tell your team.

    You do not have to leave your bedroom, just rest on your bed and go hunting with your friends. While your street sam, rigger friends chase down creature, give it some good bully.

    Float above, cast mana barriers, manabolts and other mana spells to slow down and hunt down creature. It will be much easier for your friends to deal with it, when you manage to corner it in mana barrier, or you bring down it’s dices by constant stun damage.

    This approach needs bit investment in drain management, but, it’s worth considering. You do not have to be everywhere physically.

    Hanging around

    Astral space is interesting place. Dangerous, but interesting. In many cases, much more interesting then bubble gum stuck to your ceiling, above your bed, which you observe every night.

    Why not utilize it? If you have Knowledge skill: Awakened Hangout, you can actually go out, without leaving your home. You can flow through mysterious astral plane and appear on places where magicians, awakened creatures appear.

    You may indulge in astral sex (yes it is possible, and very very intense), you may meet new contacts, magical groups. Heck you may even meet vampires, or other infected! If that is your alluring path of progression, you may even convince them to change you (For service or cash), into infected. Immortality and mages go well together.

    Actual exploration

    This one is more session building. You can help your GM. Sometimes coming up with idea what to do next, can be rather daunting. Search magazines about cryptids, conspiracies, mythical places, interesting historical places.

    Aside of your shadowrunning friends, you are the one, which can explore them within a moment and with no cost. Tell your GM about your idea visiting picked place and send them materials. I’m sure they will be happy, that they will get some interesting idea to work with.

    When you start the game, go into astral exploration mode and check that place out. Maybe there grow interesting expensive awakened herbs, awakened critters, mysterious glowing runes or unknown mana barrier? Maybe in those ancient Celtic ruins sits some spirit, which notices you and asks you to come to him!

    In any way, after scouting convince your team, that some holiday and change of Seattle air, would benefit them. And then, of you go on interesting adventure, where you can change and experiment with different styles of game!

    Conclusion

    And there you have it! Quick article. I hope it will give you some ideas for usage of Astral Perception in Shadowrun 5, and how to utilize it aside from simple scouting.

    If you made this far, I love you and thank you for reading!

  • Shadowrun 5: How to RP Spirits

    Shadowrun 5: How to RP Spirits

    So, YouTube or forums are full of questions how to manage spirits by rules, how to use them effectively. Same as most sourcebooks. We will learn from them how to use spirits in game and we will get some basic information about them.

    But, how to actually RP them? How we will portray them in our games, so that they are more than just a bunch of generic numbers that we throw on the enemy?

    That is a question we get little answers to. And because I love playing awakened characters in Shadowrun, and I love spirits, I did some research on that.

    Astral Plane

    Before we jump into spirits themselves, let’s first rewind definition of Astral Plane from SR5 Core Rulebook.

    While the physical world is the one we interact with every day, there is another world, invisible and largely unnoticed, yet indirectly influenced by everyone living in the material plane. This world is known as the astral plane.

    The astral plane is an emotionally charged reflection of the physical world. Only living beings and objects infused with mana truly exist there; ordinary physical objects appear only as faint, intangible shadows.

    Mana, the essence of magic, flows through the astral plane, connecting the metaplanes to the physical world. It is sustained by the life force of all living things on the planet. This collective life energy bathes the astral plane in a constant ambient glow.

    Objects and events from the physical world can still be perceived from the astral, but they appear blurred and muted. Emotional impressions are far more vivid than physical details such as light or sound, often replacing them entirely.

    Places and objects marked by powerful emotions may leave lasting astral echoes. These remnants of the past appear as lingering shadows whose strength depends on the intensity of the emotions associated with them. Such echoes are often more colorful and tangible in the astral plane than the physical objects themselves.

    (page 312)

    So, Astral plane is this vast vibrant, living, breathing space that bridges our physical world with metaplanes. It’s colorful and full of emotions. Spirits fly and lurk in it freely, as it is their home. Bigger spirits might hunt smaller ones. Trickster spirits might try to trick gullible awakened character into traps, while projecting.

    Many tables, put this world on side, and when some Astral Projection scouting is in place, it’s generally skipped. You are there, done.

    But, given the Astral world is the foundation of magic and spirits in general, it makes us forget, how strange this world truly is. Let me entertain you.

    What might the Astral Plane look like?

    You close your eyes in peace of your cramped room, that you call your home. Sounds of Seattle rain meld with sounds of your apartment. The electronics buzzing, shuffling of your blanket and creaking of floors above you, until nothing then just deafening shimmer remains.

    Then you open your eyes, but not your true eyes. They remain shut. You stand up from bed, leaving your body behind and you look around the apartment. Everything is dull, blurred filled with unmistakable fog of background count.

    One of spirits that might be encountered in Astral Plane
    Astral is weird and spirits in it, even weirder. And they might be after you.

    You move through the wall and you are about to see new side of Seattle, the same side, that spirits living in this strange place see every day. The tall, dark corporate skyscrapers sticking out from ground, like menacing claws trying to grasp the sky. They are all dark, unimportant, but yet they are still there. Above them flocks of colorful dots are blazing through the skies with speeds, that normal eye couldn’t catch. Between streets, strange and formless creatures slither, unbothered by physical world. Some small, some big as whales, with teeth sharp and multiple eyes. They search, they lurk and in many cases they hunt. Among them, ever flowing mass of metahumans and their auras are flowing like a river, leading them to their jobs and to their lives.

    Auras, creatures. They are all colorful. Some deep red by hatred, some blue and green filled with peace. You see one spirit, it looks like a man in suitcase. He is following another man, glowing red like hot iron with curiosity and alien intent.

    It all feels like vivid dream. Like you would wake up in depths of Mariana’s trench on bottom of the ocean and your wildest imagination would run wild. You look on your hands, they sparkle with bright yellow, fear and anxiety. Six legged woman grasping street lamp notices. She turns her head your direction and starts to walk “air” your direction. She has no face. You do not wait to find out what she wants. Your quickly return back to your body.

    Creatures are gone, colors are gone, fog is gone and first thing you hear is sound of your fan. World is normal again.

    How to actually RP a spirit

    Okay, now to the meat? What we can make out of it? If you are not in combat, where things are fast paced and you wish to interact with spirits, here it how I think it should go.

    Cornerstone of spirits is EMOTION. They perceive them, they carry them and for them, it’s the most recognizable thing.

    Spirits’ perception of world and you

    Spirits will be asensing you the whole time during conversation. For them worded facts are not as important as emotion behind the words. When you try to explain something to them, they will try to read what emotions are behind it. Are you scared when you plead to them? Are you angry, are you anxious? They will make out of it their own truth.

    You can hit spirit with scientific facts, and it will listen to you, it will try to think about it. But, it will also read your emotions, how much you believe in what you say. Whole magical world is based on my belief, my world. That’s the reason, why spirits take shape and form based on caster and his tradition.

    Carried emotion

    As you can see in examples of spirits Carngull and Eileen I wrote, they carry certain emotions in them. For Carngull, it’s hunger, survival. For Eileen it’s sorrow and wish to live life.

    Therefore when you are creating concept of spirit, try to think about emotion, which resonates with them. It gives them form, and it gives them something players can attach to.

    Things that used to be

    A good way to link those emotions is written in core rulebook. It’s kind of hidden, but when you will pay attention, there is written:

    Which is kind of cool when you think about it. In astral space might be things, that are no longer in physical world, and can be explored. Those things can be linked back to certain spirits. In case of Eileen, it can be wreck of S.S. Clara Barton, or the ship might be still floating around. Or it may be locket in some house, which children of passed away mother still remember.

    Make them weird

    Do not be afraid to use these references. They might play into spirit weirdness. Spirits might even reenact certain events of history, just for sake of it, just because emotions on that place are still present. Bunch of fire spirits might reenact in astral plane Gettysburg battle, just for sake of it.

    Some other spirit might prepare for players dinner, because it’s reenacts or represents certain aspect, like grandmother. Some spirit might reenact Sioux pathfinder out of joy of it.

    Another take you can make, you can surprise caster player, by spirit returning back, without summoning. Maybe spirit enjoyed time with caster so much, it starts to linger in it’s space. Watches the caster from astral. It can be quite hilarious to see shaman getting rid of clingy spirit, they summoned a while ago >:D

    What do spirits like to do when you summon them?

    I was trying to piece together some information about how to RP ghosts, and I stumbled upon amazing post on Reddit about this topic. It was written by Hammaer96, and he proposed this table of spirits and what they might like as task, and what not.

    This is interesting take and I really like it. It can also correlate with existing Astral Reputation, which has quite few ways how to move. You summon spirits to wrong tasks, and they get annoyed and tell among themselves.

    I do not say, give bad reputation for every bad task, but if you conjure fire spirit every day, to guard bottle with bee inside? He might get pretty mad. He expects fight, he wants that thrill and juice of emotion of thrill. No, he watches fucking bee.

    Players, write your own spirits!

    Shadowrun is collaborative storytelling, that means, story you are experiencing shouldn’t come only from your GM. You as player can contribute to story, I dare to say, with 35%.

    Consult with GM possibility to write your own spirits. When you summon spirit it comes from SOMEWHERE and goes SOMEWHERE, when it fulfilled your wishes. Why not summon the same spirit again?

    If you write your own spirits, which are most aligned with your magician, it will add depth to your own character and to story overall. If you summon same spirit again and again, GM will learn it’s personality, party will learn to recognize that spirit and its quirks. It will stop being stat blob, which you throw on enemies.

    How to design your own spirit?

    As I pointed up, I made so far two for my character Sha. When you think about the concept of calling the same spirit again, it makes sense you need to learn it’s habits. For example, what it represents? Is it former human, or just memory of a human? Does it represent hunt, battle, anything really.

    The Location

    Set its location. Every spirit will have some location it favors, is it sea? Alleyways, coffee shop, hospital? Any place where your character actually has to go, to summon this specific spirit.

    The Offering

    Set its offerings. Being on a place is not enough to lure specific spirit. What is it, that tells spirit you are calling them? Leaving hot coffee on table, while you sit in stuffer shack? Footprint in dirt, driven through with nail to lure Sioux tracker? Splash of cider on roots of the tree? Something unique.

    Story Hooks

    Designing your own spirit doesn’t only give your party some other memorable character to interact with, it also gives your character things to do and GM story hooks to develop. Here are some examples.

    Conclusion

    The next time your magician summons a spirit, ask yourself one question:

    “Who answered?”

    If the answer is more than “Force 6 Air Spirit,” you’ve already started telling a better story.

  • Shadowrun 5: Beast Spirit Carngull

    Shadowrun 5: Beast Spirit Carngull

    Ever-hungry scavenger

    This is Shadowrun 5 Critter designed by me, numbers you see are applicable in this game.

    The Terror of the Alleys. Carngull is representation of nature’s adaptability, strength and relentlessness even in midst of plastic jungle.

    Similar to Eileen, this spirit could fall into category of city spirits, as it is drawn to those shamans, which are living within plastic jungle. It might reveal itself even to wilder shamans, but it’s rather unlikely.

    Summoning

    Carngull can be summoned anywhere in Seattle, where presence of city wildlife was recent. Which is quite broad. Carngull will not respond to calls in clean and sterile corporate facilities, as Sha discovered, it is place where he can’t hear the prayers.

    Carngull is lured by daily ritual, which keeps animals on street fed and thriving, and that’s waste of food. Yes, something which would be seen as “anti-nature”, actually keeps devil rats, seagulls, pigeons, stray cats and dogs alive.

    To summon this avatar of street beast, shaman needs to have some food on themselves, throw it on ground and step on it. Inside their mind, they have to clear their mind and forget it.

    After this is done, Carngull is allured by offering and standard summoning can begin. If successful, filth, street debris, organic matter will start to form into horrific creature with blood red glowing eyes. Ready, to sate it’s hunger.

    Force

    Personality

    Carngull is a beast. Or should we say THE BEAST. He is not stupid though, in higher forms it can outsmart most humans. Most animals which are living on the streets wouldn’t survive long if they wouldn’t be smart.

    Carngull is just not aligned with metahuman thinking or customs. He understands, he has thoughts, but rarely finds need to express them. Yes, he can talk.

    He loves chasing, he loves hunt, chasing and tracking down the prey. But, also he is patient, not getting bored waiting and guarding till he is said otherwise.

    Behavior towards Summoners

    Respectful, obedient. Sadly for all, which would want to have conversation or deeper interaction, this is probably not the one. Sha was never able to convince this spirit to hang out with her outside of confines of huntsman/hound. Carngull will obey every command with ferocity and determination, until forced to return back from where he came from.

    The City

    The city is ecosystem, strange one, but it is. From small bugs to big awakened critters, that stalk the alleyways. Sewers, backstreets, abandoned buildings, cracks in skyscrapers, open cloudy skies. All this is inhabited by creatures of sorts.

    Carngull is part of all of them, and he will not abandon them. He made clear to Sha, that he will never cross borders and leave Seattle. Why? Hard to tell, this spirit doesn’t do reasoning, it might be something he chose to.

    Distinctive behavior

    • Never leaves Seattle
    • Never stays longer then needs to
    • When banished, services end it switches to Astral Form and runs away. Can be chased.
    • Attacks Insect Spirits without command almost like berserker.

    Manifestation

    The stench is recognizable. Fur, damp street, dumpsters in back alley and mold of cellars. This is first what people say, they noticed, before they encountered Carngull. It’s also reason why Sha never summons it in her own apartment. Carngull is not only summoned, he is presence in the streets. Hiding in alleys, hearing every beating heart. Many met him, many discarded him as another Beast Spirit, not knowing how impressive he is.

    Carngull (Spirit of Beast)

    B A R S W L I C EDG ESS M
    Initiative
    Astral Initiative
    Skills
    AssensingMAG + INT
    Astral CombatMAG + WIL
    PerceptionMAG + INT
    Unarmed CombatMAG + AGI
    PowersAnimal Control, Astral Form, Enhanced Senses (Hearing, Low-Light Vision, Smell), Fear, Materialization, Movement, Sapience
    Optional Powers

    Pages: 1 2 3

  • Shadowrun 5: Water Spirit Eileen Witkins

    Shadowrun 5: Water Spirit Eileen Witkins

    Haunt of S.S. Clara Barton

    Eileen Witkins, second cook on ship S.S. Clara Barton, sank on February 22, 1942 by German U-Boat (U-129). Or at least, this is what this manifestation of spirit says. It might be truly spirit of Eileen, forever tied to water, or playful spirit of water decided to take on her form.

    Eileen Witkins Card, also usable to summon her from her watery grave.

    Summoning

    By observation of Sha, Eileen seems to listen to summons, only close to the seawater. Anywhere else, any other body of water, random other water spirit appears.

    It is very much possible, that this is only Eileen preference and she might choose to come to aid in other circumstances, but it never happened before.

    After this small ritual, water will open and a beautiful woman in a vintage sailor’s uniform, with haunting blue eyes in sailor dress emerges from the waves.

    Personality

    To those who didn’t summon her, Eileen might appear as malevolent water spirit. Her preferred method of killing is anyways drowning. To others then her Shaman she will be rather dismissive (Unless having good Astral Reputation).

    That doesn’t mean others can’t reach out to her, though. It is possible to create connection with her even without being her summoner. It might just take a while.

    Relations with summoners

    Towards summoners Eileen behaves more cordial. Eileen often lingers long after the summoning is complete. She enjoys sharing meals, listening to old sea stories, or simply sitting beside the water with her summoner in comfortable silence. Unlike many spirits, she rarely disappears immediately after her task is finished unless dismissed.

    Spirit affinity

    To those with spirit affinity, she might even show up to their rescue if they will drowning at the sea. Fact, she is not summoned, doesn’t mean she is not somewhere out there. It’s slight chance, but still a chance.

    The Vat

    Something she never speaks of, but it is way how to gain her respect almost instantly. The Vat is ritual tied to ancient sacrifices called Threefold Death.

    Core of The Vat is, that practitioner will strip themselves of all gear and necessary clothes and let themselves embraced by water spirit. She embraces the practitioner and silently descends into the black water until neither moonlight nor surface can be seen.

    Practitioner is fully at her mercy and she will decide if they will drowned or not. The idea of fully giving yourself to the mercy of the sea is key of this ritual. If the practitioner survives the ritual, Eileen recognizes their bravery and devotion.

    NOTE: Eileen will never perform The Vat with anyone she knows, they have gills or any kind of cyberware close to the lungs. She will try to assensing before the ritual. If she fails and finds out later that practitioner cheater, she will be furious.

    Distinctive Behavior

    • She unconsciously hums old merchant navy shanties while waiting.
    • She instinctively wipes condensation off bottles before handing someone a beer.
    • She always faces the nearest body of water when sitting in silence.

    Manifestations

    In the Astral Plane, billions of spirits roam beneath skies awash with impossible colors. Picking out a single one among them would be all but impossible. Yet somewhere in the deepest corners of the Matrix horror threads, old conspiracy boards, and forgotten trideo discussions—rumors persist among Seattle’s dockworkers. They speak of a woman in a vintage navy uniform who appears on lonely piers shortly before a storm. She stands motionless, watching the sea in silence. Then, just as the first rain begins to fall, she vanishes without a trace.

    Eileen Witkins (Spirit of Water)

    B A R S W L I C EDG ESS M
    Initiative
    Astral Initiative
    Skills
    AssensingMAG + INT
    Astral CombatMAG + WIL
    Exotic Ranged WeaponMAG + AGI
    PerceptionMAG + INT
    Unarmed CombatMAG + AGI
    PowersAstral Form, Concealment, Confusion, Engulf, Materialization, Movement, Sapience, Search
    Optional Powers
    WeaknessesAllergy (Fire, Severe)
    SpecialEileen Witkins moves twice as fast when in water.

    Pages: 1 2 3

  • IPSec configuration Ubuntu and Debian

    IPSec configuration Ubuntu and Debian

    What is happening with IPSec?

    Maybe you noticed, or not, but Fortinet company is abandoning SSL VPN and pushes IPSec VPN. They reason it with SSL VPN not being secure anymore. Which is fine, I guess? Maybe your company already moved to new versions of FortiVPN and you are now dealing with this, or it awaits you.

    Better security is great, right? Well, if you are using Windows or MacOS, you will likely face very few issues. If you are Linux user, you will be facing problems.

    While Fortinet pushed this change, they didn’t put advanced IPSec capabilities from their free FortiVPN clients, for Linux. Those configurations are behind a PAYWALL. You are required to purchase 50 licences / year, to get this thing. It’s truly “Create problem, sell solution thing”, but fret not!

    Challenges

    For Windows and MacOS users, new model comes with some benefits. Authentication can be tied with EntraID MFA and so on. But, on Linux it is bit trickier. So far, to my knowledge, only working way how to connect to IPSec VPN from Linux is by using Strongswan.

    Strongswan is TUI based package to configure and connect to VPNs. Now you probably start to see the problem. If your company will be using EntraID, even Strongswan is not going to help you. You will have hard times to conjure pop up Microsoft Account login window. So, when using Linux, you better hope, your company uses only basic IPSec.

    Another challenge you will be facing, is that every distribution has different approaches/packages. Fedora might use libreswan instead of strongswan and donn’t make started on NixOS.

    Let’s hope you are not System Administrator in some Software company, which has many Developer contractors and every has own distro, haha!

    IPSec and Debian?

    When this change first hit me, I was truly struggling to find way how to connect to VPN. I was trying Libreswan, Strongswan and it took me several days. We were going back and forth. But, I finally succeeded and found way, how to make Strongswan work on Debian based distros.

    Note: This what I will show you, is ONLY for specific configuration. That configuration is, as follows.

    • Debian based distribution (Frankly it goes same on every debian based distro)
    • IPSec with EAP and PSK configured.
    • Alignment of Jupiter and Saturn.

    How to configure IPSec on Debian.

    Let’s start with preparation of packages and our service:

    Now, in your /etc directory, you will have two files ipsec.conf and ipsec.secrets.

    Those are your main files that will interest you.

    For EAP/PSK configuration you will first want to edit ipsec.conf file. Paste in this:

    Note: left = client, right = remote gateway.

    Config files

    Green is name of your connection.

    Yellow is guide what to fill, you can figure it out.

    Once you have these set, let’s go on ipsec.secrets

    VPN operation

    Now when you have your configuration files ready, you can start to operate your VPN.

    Conclusion

    And there you have it! It will really depend on your configuration and how complicated you have it.

    This article should serve you only as baseline from where to start. It can also serve as template, for your FortiGate configuration, so transition is painless for MacOs, Windows and even Linux.

    If I could choose, I wouldn’t buy/use Fortinet at all.

  • 7 useful security headers for NGINX.

    7 useful security headers for NGINX.

    So! I saw amazing video on the YouTube, which explains beautifully something obscure about NGINX, and that is headers. I had quite problem to understand headers, as in NGINX lore, they are quite on background. You will learn how to set it as reverse proxy, and then you just stop thinking about it. Only after some time, you will understand that there is more to NGINX as a whole. I stumbled across this video, and I found it super helpful. So, you do not have to watch it (I recommend you should), I wrote down most important headers and you can just copy, paste them into your configuration. Video link is down bellow!

    server_tokens off;

    Hides the NGINX version from HTTP headers. This makes it harder for attackers to identify the exact NGINX version and search for version-specific vulnerabilities.

    Place this in `/etc/nginx/nginx.conf`, inside the `http` block.

    more_clear_headers Server;

    Completely removes the `Server` header from HTTP responses.

    Requires the headers-more module:

    apt install libnginx-mod-http-headers-more-filter

    Place this in the public-facing NGINX `server` block, usually in `/etc/nginx/sites-available/*.conf`.

    add_header Content-Security-Policy “default-src ‘self’;” always;

    Restricts the browser to loading content only from the same origin as the website, unless other CSP rules allow additional sources. This helps control where scripts, images, styles, and other resources can be loaded from.

    Place this on the public-facing NGINX reverse proxy, usually inside the website’s `server` block.

    add_header X-Frame-Options “DENY” always;

    Prevents the website from being loaded inside an iframe. This helps protect against clickjacking attacks.

    Place this in the public-facing NGINX `server` block, or globally in the `http` block if it should apply to all sites.

    add_header X-Content-Type-Options “nosniff” always;

    Stops browsers from guessing file types. This prevents files from being interpreted as something else, such as treating an uploaded image or CSS file as executable JavaScript. This helps reduce some XSS risks.

    Place this in the public-facing NGINX `server` block, or globally in the `http` block if it should apply to all sites.

    proxy_hide_header X-Runtime;

    Removes the `X-Runtime` header from backend responses. This hides backend request processing time from clients.

    Use this on the reverse proxy, inside the relevant `location` or `server` block.

    proxy_hide_header X-Powered-By;

    Removes the `X-Powered-By` header from backend responses. This avoids exposing backend technologies such as PHP, Express, ASP.NET, Laravel, or framework versions.

    Use this on the reverse proxy, inside the relevant `location` or `server` block.

    Original explanation on YouTube:

    This text I wrote based on amazing video by The Lazy SysAdmin