Skip to content

Latest commit

 

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ParaForge

ParaForge

Take a 3D model from Blender to the Paralives Build Mode catalogue, in two clicks.

English  ·  Français

License GPL-3.0-or-later Blender 4.2+ CI Latest release


ParaForge is a Blender add-on for Paralives. It checks a mesh against every rule the game imposes, fixes what can be fixed, rebuilds the textures into the maps the game actually reads, writes the FBX and its PNGs straight into a .mod folder, and then declares the item in the Build Mode catalogue.

The game never has to be running, and no file inside the installation is ever touched.

Interface in French by default, English on request, switchable from the panel header.

Blender 4.2 or newer. Developed and tested against Blender 5.2 LTS.

Tip

Complementary Modding Tool: Pair ParaForge with ParaReload, an in-game BepInEx plugin for Paralives that hot-reloads modified 3D models, textures, and settings live without restarting the game. Save in Blender with ParaForge, and see your asset update instantly in-game!

Tip

Exporting something detailed? ParaLOD makes high poly assets usable in Paralives. The game gives the physics collider the same mesh as the renderer, so a 393,214 triangle model also carries a 393,214 triangle collider. Measured at 6 frames per second, against 380 once the collider is simplified. ParaLOD does it automatically and leaves the rendered mesh untouched.

Note

Extracting from The Sims: sims-package2glb reads The Sims 2, 3 and 4 .package mods and writes the models and textures out as glTF.


Screenshots

ParaForge Blender Interface and Checklist
ParaForge panel in Blender 5.2 showing automated checklist validation, asset rules, and export tools.


In-Game Paralives Preview
Exported custom 3D asset placed live in Paralives Build Mode alongside hot-reloading console commands.


Table of contents


Why this exists

Paralives ships a real modding toolkit inside the game, and it works. What it does not do is stop you from getting the mesh wrong. The official path is: export from Blender, drop the files in a mod folder, restart the game, open the Control Panel, create an Item, create a Prefab, add an ItemMeshReference, pick the mesh, fit the bounding box, assign a surface, tag it, pick a swatch group, then find out whether the orientation, the scale, the origin and the texture roles were right.

Every mistake costs a full game restart, because the game imports its assets at launch and has no hot reload.

ParaForge moves the whole verification into Blender, where a mistake costs nothing, and then writes the catalogue entry itself so the Control Panel pass disappears.

Install

Download paraforge-x.y.z.zip from the Releases page, then in Blender:

Edit > Preferences > Get Extensions > Install from Disk, and pick the zip.

The panel appears in the 3D viewport sidebar, tab ParaForge. Press N to open the sidebar.

Use it

  1. Pick the target mod, or create one with the + next to the selector. Mods live in %USERPROFILE%\AppData\LocalLow\Paralives\Paralives\. Avoid Local.mod: it is the game's own scratch folder, fine for a quick try, but it cannot be published to the Workshop.
  2. Select the mesh. The checklist appears in the panel and as an overlay in the viewport.
  3. Fix everything safe sorts out unit scale, transforms, origin, the colour attribute and the texture roles.
  4. Check the orientation against the green arrow, then confirm. This is the one thing no tool can work out for you.
  5. 1. Export to Paralives writes the mesh and the textures.
  6. 2. Create the item in the catalogue declares the item.

Both steps are needed. Without the second, the files sit in the mod and nothing appears in Build Mode.

Then restart Paralives. There is no hot reload.

What the checklist verifies

Each line is green, orange or red, with the reason, and a button that fixes it where a fix exists.

Check What it means
Scene unit scale Paralives works in metres at scale 1.0
Transforms applied Rotation and scale baked, or the item arrives wrong
Origin placement Per item type, see below
Bounding size Sanity check against the tile size
Faces Y+ Manual, compared with the arrow drawn in the viewport
Colour zones Legal zone colours only, four maximum
UV map Present, and UV2 reported when it is there
Texture coordinates A Mapping node is baked into the exported UVs, since an FBX cannot carry one
Triangle count Against your own budget, with a decimate button
N-gons They triangulate on export and can shade badly
Texture naming Every image classified into a role the game knows
Texture size Nothing in the game is above 2K
Seat height For items a Para sits on, against the heights the game's own furniture uses
Which way round The backrest on the side the game's 43 chairs put theirs, with a half turn to fix it
Target mod folder Exists, ends in .mod, and is not inside the game

Origin rules, from the wiki:

Item type Rule
Floor item Centred in X and Y, base at Z=0
Wall item Centred in X and Z, back at Y=0
Window or door Centred on all three axes

Colour zones, three ways in

Paralives reads up to four recolourable zones from vertex colours, plus yellow for decals. Painting them by hand is slow, and an asset that arrives from a marketplace or a generator has none at all.

  1. Select faces, click a zone. The obvious one, with coloured buttons.
  2. One material per zone. Most imported assets already split that way, so the slots map straight onto zones.
  3. Pick a colour off the model, then grow it by tolerance. This is the one that rescues an asset whose only texture is a single bake. The picker samples the texture through a raycast rather than reading screen pixels, so viewport lighting never shifts the result, and the tolerance slider is live in the redo panel.

The legal colours are exact, and nothing else is read by the game:

Zone Colour
0 white 1, 1, 1
1 red 1, 0, 0
2 green 0, 1, 0
3 blue 0, 0, 1
Decal yellow 1, 1, 0, never recolourable

Textures, and the naming that configures them

The suffix on the file name is what makes the game assign the right import settings. Getting it right removes a manual configuration pass per texture, which is the single largest saving in the whole pipeline.

Suffix Content Note
GrayMask Recolourable base, 50 percent gray is the neutral tone sRGB
Detail Free colour, not recolourable one per item
NormalOcclusion Normal map in RGB, occlusion in the alpha data
Smoothness White is glossy, black is matte data
ColorZone Zone map, for meshes that cannot carry vertex colours data
Master Walls and floors: R GrayMask, G variant, B hue shift sRGB

ParaForge also writes the .meta sidecar next to each asset, carrying its GUID and its import flags, which makes the import deterministic instead of depending on the game parsing the file name.

A downloaded GLB, converted on its own

A model taken off the web never arrives in the right shape. glTF gives you roughness where the game wants smoothness, packs occlusion into the red channel of an ORM texture, and gltfpack strips image names so Blender calls them Image_0, Image_1, Image_2.

ParaForge identifies each image from three sources of evidence, most reliable first:

  1. the shader graph, meaning what the image is actually wired to. That is a fact rather than a guess, and it is the only clue left when the names are gone;
  2. the file name: _Diffuse, -ORM, _Normal, _BaseColor;
  3. the pixels: a normal map is recognisable, and a texture with no colour is never an albedo.

Then it rebuilds:

baseColor            -> Detail (exact copy) or GrayMask (desaturated, recentred on 50%)
normal + occlusion   -> NormalOcclusion (RGB + alpha)
roughness            -> Smoothness (1 - roughness)
metallic             -> no channel, folded into smoothness
emissive             -> no channel, folded back into the colour

Nothing is invented. An image that nothing identifies stays marked unknown and is not written, rather than putting the wrong map into someone's mod.

Assets with several materials

Paralives gives one surface per mesh. An asset split across five materials therefore does not import as it is. Merge into one surface repacks the UVs of the whole selection into an atlas, bakes every channel, and replaces the materials with one. The look is preserved, the UVs are not.

Writing the item without the Control Panel

A Paralives item is three pieces of plain text, all inside your own mod:

<Mod>/<Name>.prefab                 the object tree, its size, its mesh
<Mod>/Settings/Items.setting        the catalogue entry and its tag
<Mod>/Settings/Translations.setting the label the player reads

A mod carries only what it adds and the game merges everything: the game's own French translation mod contains nothing but a Translations.setting. That is why no game file is ever modified.

Merging is done on the text rather than by rebuilding the file from a parsed model. An Items.setting written by the game may hold fields this add-on has never heard of, and re-serialising it would drop them in silence.

Two handles, and they are not the same thing

A placed item can carry either of the game's two resize widgets, both, or neither. The game keeps them apart itself, in CancelResizeOrScaleItem, and 133 of its prefabs declare both. An item a Para sits on declares neither: see A chair carries no resize handle.

Scaling multiplies the whole item at once. It needs IsScalable on the root, and the bounds are factors:

ItemObjectRoot:
 IsScalable:True
  ScalableAxes:bool3(True, True, True)
  HasMinScale:True
  MinScale:0.1
  HasMaxScale:True
  MaxScale:10

Stretching pulls the item along chosen axes to real dimensions, so a shelf can be made wider without becoming taller. It takes two statements, because the mesh has to be told which of its own axes follow the item's cube:

ItemObjectRoot:
 IsResizable:True
  ResizableAxes:bool3(True, False, True)
  MinSizes:(0.2000, 0.1000, 0.0500)
  HasMaxSize:True
  MaxSizes:(20.0000, 10.0000, 5.0000)
ItemMeshReference:
 IsResizable:bool3(True, False, True)

The sizes are metres in the same order as Size, so ParaForge derives them from the item's own measurements rather than a number out of the air.

HasMaxScale and HasMaxSize are what the clamp actually reads, and there is no HasMinSize anywhere in the assembly: the floor always applies, the ceiling only when declared. 139 shipped prefabs write MaxSizes while only 47 declare HasMaxSize, so 92 of them carry a ceiling the game never applies.

The ranges ParaForge writes are wider than the game's own. Across the 185 prefabs that set them, MinScale runs 0.25 to 1.75 and MaxScale 0.5 to 10, because each is authored for a purpose. A mod item is handed to someone who wants it tiny on a shelf and huge in the garden, so the ceiling is the game's own maximum and the floor goes below anything it ships.

Things can be put on it, except on a chair

ItemCanBeStackedOn is what lets a Para set something down on an item. Counted over the game's items by family:

Family Items Declaring it
Couches 13 100%
Benches 11 100%
Toilets 6 100%
Tables 26 100%
Counters 79 100%
Chairs 48 0%

So it is not about having a flat top, since a chair has one. It is about whether that top is meant to be used. Without it the item looks right and refuses everything a Para tries to put down.

And height is not stretchable

In the order the game writes bool3, width, height and depth, the middle flag is height and it is false almost everywhere: all 13 couches and all 11 benches are True,False,False, and 12 of 26 tables are True,False,True. Stretching an item vertically moves everything anchored to it, which on anything a Para uses means moving the place they stand or sit.

A chair carries no resize handle, but a couch does

Joining the game's whole catalogue to its prefabs by GUID and grouping every item by the template its tag or its own override gives it:

Template Items Resizable
ChairSlotAndLocator 18 1
ShorterChairSlotAndLocator 9 0
LongChairSlotAndLocator 8 0
ArmchairSlotAndLocator 9 0
ShorterArmchairSlotAndLocators 2 0
LowerArmchairSlotAndLocators 2 0
ToiletSlotAndLocators and two variants 6 0
Chairs, armchairs and toilets 54 1
CouchesSlotAndLocators and five variants 13 13
BenchSlotsAndLocators 10 10
CounterSlotsAndLocators and two variants 79 78
TableSlots 22 14

So it is not sitting that the handle breaks. A couch and a bench carry a row of seats and survive being stretched; a chair carries one, and moving it puts it where the Para cannot path to. The item appears in the catalogue, renders correctly, accepts the sit command, and the Para walks to a different chair. Nothing is logged, because as far as the game is concerned nothing failed.

Confirmed on two custom items whose catalogue entries are identical down to the tag and the slot GUID, differing only in the prefab: the one declaring no widget is sat on, the one declaring IsResizable is walked past.

ParaForge drops both handles from chairs, armchairs and toilets, whatever is ticked in the panel, and greys them out with the reason. Couches, benches, beds, tables and counters keep theirs.

Where the seat is

Nothing in a mesh tells the game where to sit, and nothing fixes the height either. Every slot template carries VaryBasedOnHeight:True on its seat locator, with Min and Max children bounding the travel, so the game moves the Para to suit the item rather than demanding a height of it. That is why a stool and a dining chair both work through the same ChairSlotAndLocator.

What the game cannot do is invent a surface. A mesh whose only flat top is at 1.2 m still seats a Para, who then floats, and nothing is logged because nothing failed.

So the seat is measured: the largest single horizontal surface the item has, found by binning upward facing triangles by height and taking the heaviest bin by area. Not a fraction of the item, and not an average, which would mix a chair's seat with its armrests and its backrest top. Run over the game's own furniture:

Family Meshes Seat height Share of the item's height
Chairs 28 0.448 m 49%
Office chairs 5 0.449 m 43%
Benches 12 0.450 m 86%
Ottomans 7 0.449 m 100%
Stools 9 0.649 m 99%

Two heights, then: 0.45 for anything you sit on with your feet down, 0.65 for a stool. The ratios are why a single band cannot exist, since a dining chair's seat is halfway up it and an ottoman's seat is its lid. Couches and beds are assembled from several meshes, cushions apart from frames, so one file measured alone means nothing and they are left out.

ParaForge measures your mesh the same way, names which of the two it matches, and draws both heights in the viewport. It only warns outside 0.20 to 0.75, where no shipped item sits, and never blocks.

And a Para sits facing the arrow, with the backrest behind

Measured rather than reasoned, because reasoning it out of the template's locator coordinates got the answer backwards once. The game's own chairs, imported into Blender, asked where the geometry in the top third of each one sits against the middle of the item:

Chairs and armchairs
Backrest on the Y- side 43
Too symmetrical to say 4
Backrest on the Y+ side 1

So the knees go at the arrow and the backrest behind it. The viewport draws a second arrow at seat height to say so, and the checklist measures your own mesh the same way, warning with a half turn on a button when the back is on the wrong side. Nothing in the game will tell you: the item lands in the catalogue, a Para walks over, sits down, and faces their own backrest.

The floor arrow is a separate thing and is unchanged. It is the item's front, and it is what the catalogue thumbnail and the placement rotation use.

What the wiki does not say, and the game does

Paralives ships its own content as a mod: Main.mod, in the installation folder, is an ordinary folder of FBX, PNG and text. Every number below was measured there, not assumed. This section is the part of the project most likely to be useful to other tools.

The mesh has to be in centimetres

The game multiplies the raw vertex coordinates of an FBX by 0.01. It ignores both the file's unit declaration and any scaling on the node, so a mesh authored in metres arrives a hundred times too small: present, correctly placed, with the right footprint, and far too small to see.

Read out of the .import files, which are what the game made of each FBX:

Mesh Size in its prefab Coordinates in the import
CityGravelPile 4.4642 m around 2.24
Cereal box around 0.3 m around 0.15
An early ParaForge export 1.9086 m around 0.0088

No Blender export option produces this, because Blender puts the factor on the node. ParaForge scales the geometry on a throwaway copy instead.

A Para is 1.702 m, not 1.80

The game's own body meshes carry their world position, so assembling them answers it outright:

Mesh From To
HumanFoot0Left 0.000 0.119
HumanBottom 0.390 0.962
HumanTop 0.918 1.454
HumanHead 1.427 1.702

Feet on the floor, top of the skull at 1.702. Anything built against a 1.80 reference comes out a little large next to the furniture the game ships, which is why ParaForge can draw a Para sized outline beside the item, proportioned on those four numbers.

And it leaves turned around

An asymmetric item exported without a half turn arrives with its front where its back should be. On a chair it shows twice over, and both symptoms are the same rotation: the catalogue thumbnail is shot from behind, and a Para sits down facing their own backrest. Symmetrical items never show it at all.

ParaForge applies a half turn around Z before the Y up rotation, so what you build facing the green viewport arrow is what the game calls the front.

This one was settled in the game rather than in the files. Three attempts to read it out of the shipped meshes gave three different answers, because the FBX importer's axis conversion is not the inverse of the export's and every reading needed a sign that could not be checked. It agrees with the slot template independently: ChairSlotAndLocator puts the front feet at Z +0.42, and Blender +Y leaves on the game's +Z, so the knees land where the arrow points.

And it has to be Y up, in the geometry

Same cause. Blender writes its axis conversion as a rotation on the node, and the game ignores the node, so the mesh arrives Z up in a Y up world, lying on its back. Importing with the conversion switched off shows the files as they are:

CityGravelPile.fbx   base sits on Y=0    node rotation 180 deg about Z
Barbecue.fbx         base sits on Y=0    node rotation 180 deg about Z
an early export      base sits on Z=0    node rotation  90 deg about X

ParaForge bakes the rotation into the geometry too, and then tells the exporter to convert nothing.

Adding to a list, without wiping it

This one is worth knowing for anyone writing .setting files by hand.

A collection can be written three ways, and the marker decides what the game does with the base game's own entries:

Marker Meaning
i<index> Positional. Authoring a collection from scratch. Used by a mod on a list the base game also fills, it drops the base collection and keeps only what the mod wrote
@<GUID> Add a new member to a list the base game already fills. What a content mod wants
g<GUID> Merge fields onto a member that already exists

The symptom of getting this wrong is spectacular and misleading: a Translations.setting with one entry wipes the entire translation table, and every menu label in the game turns into a raw key such as UIBuildModeCatalog_XCancel.

Verified in the game's own data: French.mod extends Translations.Items using g<GUID> markers with no s<N> size line, and never uses i<index>. Credit to paralives-modgen for documenting the @<GUID> form first.

Surfaces are shared, and the item's texture goes in DetailMap

The game does not give each item its own surface. Surfaces are a shared material library: 397 of the 2434 shipped prefabs point at GenericGrayMask, and 370 of those lay their own texture over it through DetailMap. Of the 486 DetailMap textures, 344 are used by exactly one prefab and live next to their mesh, so it really is the per item slot.

That is one of the two shapes ParaForge writes, copied from CityGravelPile.prefab:

ItemMeshReference:
 Surfaces:
  Surface:
   GUID:4303346223996877069        identity of this list entry
   Value:6533686579680309849       GenericGrayMask
 DetailMap:4868737352193020236     the item's own texture

The other shape gives the item a surface of its own, which is the only place a normal map and a smoothness value can live. No prefab field anywhere mentions smoothness, metallic or occlusion, checked across 300 prefabs, so an item borrowing a shared surface has no relief at all. The entry is modelled on TextileQuiltedSquares, one of the 75 shipped surfaces carrying a real normal map:

#Setting.Surfaces
 =AllSurfaces
  @2693213273477870343
   =DisplayName:Stool
   =Texture:4272001606441780869          the game's neutral gray base
   =NormalAndAmbientOcclusionMap:...     the item's relief
   =AmbientOcclusionStrength:1
   =SmoothnessValue:0.42
   =DefaultSwatchGroup:0
   =DefaultSwatch:0

Note that the item's colour is not in there. A surface's Texture is the base the shader tints, and across the game's 925 references it is a GrayMask 634 times, a Master 100 times, and a Detail 133 times, the last almost always under a vegetation or special shader. The colour of an ordinary item arrives through DetailMap on the prefab, over that base. Putting the colour in Texture and dropping DetailMap renders the item white.

Note the @ marker and the absent size line. Writing this positionally is what made the game throw NullReferenceException in SurfaceThumbnailManager.Start() at every launch: the mod was not adding a surface, it was replacing all 950 of them with one.

There is no slot for a smoothness texture anywhere, only a single SmoothnessValue per surface, used by 329 of the shipped ones. A Smoothness map is therefore averaged into that number on export.

Vertex colours are not free

The game reads the presence of a colour attribute, not its contents. Any attribute at all makes the mesh ZoneDefinition:VertexZones and demands a recolourable shader that a plain surface cannot provide:

Material builder got given parameters that don't match any shaders -
ShaderType:Simple ZoneDefinition:VertexZones ...

The item then loads, takes its footprint, and draws nothing. Exporting a single white zone is therefore not neutral, it is what makes the item invisible. The game's own meshes confirm it: CityGravelPile.fbx and ClutterKitchenIngredientCereal.fbx carry no colour attribute at all. ParaForge only exports them when the item really is recolourable.

Measured numbers

Triangle budget. 159 meshes taken at random from Environments/Items and imported into Blender: median 294 triangles, 90th percentile 1 380, maximum 4 060. That maximum is ParaForge's default budget. A downloaded asset at 560 000 triangles is a hundred times the largest object in the game.

Texture resolution. Across 1 446 item textures: 512 px leads, then 256, then 1 024. Nothing above 2 048. A 4K map is four times the largest in the game.

Map usage. Detail 524, GrayMask 474, ColorZone 52, NormalOcclusion 41, Smoothness 22, Master 8. A normal map appears on one item in twenty.

Catalogue tags. The 298 Build Mode tags are extracted from the game by tools/extract_catalog.py into paraforge/catalog.py, with their GUID and their hierarchy. Run it again after a game update.

A door is 2.112 m tall, a single leaf 1.04 m wide. Useful for judging the scale of an imported model by eye.

Everything the game imposes is gathered in paraforge/spec.py, so a game update should never require touching another file.

Clearing out a mod folder

Items in this mod lists every item the mod puts in the catalogue, with the picture the game itself rendered for it, and a button to take one out again.

Removing an item is not removing a file. An item is a prefab, a mesh, its textures, a sidecar for each of those, an entry in Items.setting, an entry in Translations.setting and a thumbnail the game cached somewhere else entirely. Leave any of them and the mod carries a name with nothing behind it, or a catalogue entry pointing at a prefab that is gone.

So the tree is walked from the item's own GUID outwards, by GUID and never by name, because names collide and prefixes lie: Chaise1 is a prefix of Chaise10. A mesh or a texture another item in the same mod still points at is listed as shared and kept. Everything goes through the journal, so Undo the last write puts a removal back.

The pictures are the game's own: it renders one for every item it loads and caches it as a PNG named after the item's GUID, under _GeneratedThumbnails/Items in one of the mod folders. An item the game has never loaded has none, and the panel says how many rather than showing blanks.

Undo, and safety

Every generation is journalled in _paraforge/journal.json inside the mod, with a copy of any file it changed. Undo the last write removes what was created, restores what was changed, and cleans up folders left empty. Pressing it twice walks back two generations. Regenerating an unchanged item does nothing and does not add a step.

Export refuses any folder inside the Paralives installation. Assets belong in a .mod under AppData\LocalLow, or a game update wipes them and they cannot be shared.

Calibration

Three values the developers have never published live in the Calibration panel, so a game update can be absorbed without a new release:

  • Tile size, the size of one Build Mode grid tile in metres
  • Triangle budget, your own ceiling, used for a warning only
  • FBX units per metre, measured at 100

Limits

  • Script mods are out of scope. The developers provide no tools for them and they are not allowed on the Steam Workshop. ParaForge only ever produces mods that can be uploaded.
  • A smoothness map becomes one number. The game has no slot for a smoothness texture, only a value per surface.
  • Recolourable items are only partly tested. The GrayMask base and the colour zones are written, but swatch groups have had far less use than the plain path.
  • The format is not a published contract. Paralives is in early access. Every finding above is recorded with the game build it was measured on.

Build from source

python build.py

Or let Blender validate the manifest while it packages:

python build.py --blender "C:/Program Files/Blender Foundation/Blender 5.2/blender.exe"

The zip lands in dist/. Releases are built the same way by the release workflow.

Tests

python tests/test_i18n.py
blender --background --factory-startup --python tests/test_headless.py
blender --background --factory-startup --python tests/test_ui_contract.py

The first checks that no string was left out of the French catalogue. The second covers geometry, zones, texture detection, baking, units, axes, the .setting merge and the export. The third draws every panel and validates every icon, property and operator against the live API, which catches the errors that only show up on screen.

All three run on every push, against Blender 4.2 and 5.2, in the CI workflow.

Project layout

paraforge/
  spec.py        everything Paralives imposes, in one place
  validate.py    the checklist
  fixes.py       one button per failed check
  geo.py         measurements shared by the validator, overlay and fixers
  zones.py       colour zone authoring, including the texture picker
  textures.py    role detection, naming, export
  imaging.py     channel rebuilding for glTF and ORM sources
  bake.py        merging several materials into one surface
  exporter.py    FBX and texture export into a .mod
  item.py        the prefab and the catalogue entry
  setting.py     reading and extending a .setting file
  sidecar.py     .meta files and GUID derivation
  journal.py     undo history for everything written into a mod
  catalog.py     Build Mode tags, generated from the game
  overlay.py     viewport guides and heads up checklist
  ui.py          the sidebar panel
  i18n.py        French and English strings
tools/
  extract_catalog.py   regenerate catalog.py from an installed game
  mod_diff.py          snapshot and diff a mod folder, without Blender
tests/

Credits and licence

Created by infinition.

Thanks to the Paralives team for shipping a moddable game with a transparent, plain text data format, and to paralives-modgen for documenting the collection merge syntax.

Licensed under the GNU General Public License v3.0 or later. See LICENSE for the full text. Blender add-ons link against bpy and are distributed under the GPL for that reason.

Releases

Sponsor this project

Packages

Contributors

Languages