Having accumulated a box of empty 12 gram CO₂ capsules and having already done Too Many bomb fins:

The capsule is obviously the wrong shape, too short, and only 19 mm diameter, but it’s the thought that counts.
Apply the contour gauge to a genuine slightly battered 20×102mm cartridge:

Scan the sketch, import into Inkscape, rotate the image to correct the case taper angle vs. the page, lay lines & curves around the perimeter, align half of it at the page origin to work with OpenSCAD, export as SVG:

Import into OpenSCAD, let rotate_extrude do the heavy lifting, and remove some pieces:

The little disk represents a fired primer you’d print separately in a different color and glue into the pocket shown in this cutaway view:

The interior void could hold sand for additional heft, as the whole thing is obviously nose-heavy; that’s certainly in the nature of fine tuning. Obviously, we are not dealing with anything that could go bang.
It builds just like you’d expect:

Dab some adhesive on the capsule tip, ditto for the primer, stick them in place, and it’s all good.
I like the gray PETG-CF version:

Maybe not such a good idea in this day & age. Print responsibly, as they say.
Update
Print a sabot to fit a CO₂ capsule into a genuine steel cartridge.
The solid model:

The OpenSCAD making it happen:
module Sabot() {
tube(SabotOA[LENGTH],id=SabotOA[ID],od=SabotOA[OD],anchor=BOTTOM)
position(BOTTOM)
tube(SabotOA[LENGTH]/2,id=SabotOA[ID],od=CartridgeOA[ID],anchor=BOTTOM);
}
The result:

The OpenSCAD source code (minus the sabot) and outline as a GitHub Gist:
| // 20x102mm cartridge | |
| // Ed Nisley – KE4ZNU | |
| // 2025-05-18 | |
| include <BOSL2/std.scad> | |
| Layout = "Show"; // [Show,Build] | |
| Powder = true; // build internal void | |
| /* [Hidden] */ | |
| ID = 0; | |
| OD = 1; | |
| LENGTH = 2; | |
| HoleWindage = 0.2; | |
| Protrusion = 0.1; | |
| NumSides = 3*3*4; | |
| $fn = NumSides; | |
| CartridgeOA = [21.0,29.5,101.4]; // must match SVG pretty closely | |
| PrimerOA = [2.0,8.0,2.0]; | |
| CapsuleTip = [7.5,7.5,5.0]; | |
| Capsule = [7.5,18.8 + HoleWindage,83]; | |
| SeatingDepth = 25.0; | |
| Void = [CartridgeOA[ID]- 4.0,CartridgeOA[OD]- 4.0,CartridgeOA[LENGTH] – SeatingDepth – 4*PrimerOA[LENGTH]]; | |
| //———- | |
| // Define shapes | |
| module Cartridge() { | |
| difference() { | |
| rotate_extrude() | |
| import("Cartridge – 20x102mm outline.svg",layer="Cartridge Aligned Half"); | |
| up(PrimerOA[LENGTH]) | |
| cyl(PrimerOA[LENGTH] + Protrusion,d=PrimerOA[OD],anchor=TOP); | |
| up(CartridgeOA[LENGTH] + CapsuleTip[LENGTH]) | |
| cyl(SeatingDepth,d=Capsule[OD],anchor=TOP); | |
| up(CartridgeOA[LENGTH] – SeatingDepth) | |
| cyl(Void[LENGTH],d=CapsuleTip[OD],anchor=BOTTOM); | |
| if (Powder) { | |
| up(Void[LENGTH]/2) | |
| cyl(Void[LENGTH],d=CapsuleTip[OD],anchor=BOTTOM); | |
| up(2*PrimerOA[LENGTH]) | |
| cyl(Void[LENGTH],d=Void[OD],rounding=Void[OD]/2,anchor=BOTTOM); | |
| down(Protrusion) | |
| cyl(Void[LENGTH],d=PrimerOA[ID],anchor=BOTTOM); | |
| } | |
| } | |
| } | |
| module Primer() { | |
| difference() { | |
| cyl(PrimerOA[LENGTH] – Protrusion,d=PrimerOA[OD] – HoleWindage,anchor=BOTTOM); | |
| up(PrimerOA[LENGTH]) | |
| spheroid(d=PrimerOA[ID]); | |
| } | |
| } | |
| //———- | |
| // Build things | |
| if (Layout == "Show") | |
| //render() | |
| difference() { | |
| Cartridge(); | |
| cuboid(3*CartridgeOA[LENGTH],anchor=LEFT+BACK); | |
| } | |
| if (Layout == "Build") { | |
| Cartridge(); | |
| right(CartridgeOA[OD]) | |
| Primer(); | |
| } |