Skip to content

Commit 6d6ae09

Browse files
committed
2026-06-29T0929Z
1 parent 6003d88 commit 6d6ae09

2 files changed

Lines changed: 107 additions & 95 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 0.66.5
2+
3+
- fix: continual improvements to _CSGPHS8_
4+
- fix(wine): send file paths to Popen which are resolved by `winepath -w`
5+
- fix(wine): erroneous parsing of `AppSettings.xml` when launching `RCCService.exe` via Wine
6+
- build: integrate certain `pip` packages directly into `./Source/vendored`
7+
18
# 0.66.4
29

310
- feat: players can be held in loading screen until RCC first calls `/rfd/data-transfer`
@@ -12,13 +19,13 @@
1219

1320
- feat(beta): support for modern _CSGPHS8_ physics meshes
1421
- fix: default remote resource `rbxmtl-studs.dds` not loading
15-
- fix: properly repaired `--clear_temp_cache` command flag
16-
- build: `zstandard` and `dracopy` now come installed in this and future releases
22+
- fix: properly repair `--clear_temp_cache` command flag
23+
- build: `zstandard` and `dracopy` to come installed in this and future releases
1724
- test: add `--clear_temp_cache` by default to most VSCode debug options
1825

1926
# 0.66.{0,1}
2027

21-
- **[fix](https://github.com/Windows81/Roblox-Freedom-Distribution/issues/138): repaired v347 by replacing player's `ClientAppSettings.json`**
28+
- **[fix](https://github.com/Windows81/Roblox-Freedom-Distribution/issues/138): repair v347 by replacing Player's `ClientAppSettings.json`**
2229
- feat!(config): changed method signatures of `server_core.check_user_allowed` and `server_core.retrieve_default_user_code`
2330
- feat(config): add GameConfig option `server_core.retrieve_membership_type(id_num, user_code)`
2431
- feat(webserver): add `/Game/Badge/HasBadge.ashx` and `/assets/award-badge` endpoints
@@ -29,7 +36,7 @@
2936
- fix(webserver): persistence `target` not existing on route `/persistence/set`
3037
- fix(webserver): `/marketplace/productinfo` to support all-lowercase `assetid` query param
3138
- fix(webserver): route `/Login/Negotiate.ashx` to already-existing `/login/negotiate.ashx`
32-
- fix(launcher): repaired `--clear_temp_cache` command flag
39+
- fix(launcher): repair `--clear_temp_cache` command flag
3340
- build: update link to `sqlite-worker` dependency
3441
- test: add unit test for CSGMDL2's hashing algorithm
3542
- test: add unit test to convert CSGMDL5 to CSGMDL2 format
@@ -52,13 +59,13 @@
5259

5360
# 0.65.0
5461

55-
- [feat!](https://github.com/Windows81/Roblox-Freedom-Distribution/discussions/139): moved `./Roblox/v348` to `./Roblox/v347`
56-
- [feat!](https://github.com/Windows81/Roblox-Freedom-Distribution/discussions/130): to address DataStore2 behaviour, ordered data stores no longer collide with unordered data stores of the same name
62+
- [feat!](https://github.com/Windows81/Roblox-Freedom-Distribution/discussions/139): move `./Roblox/v348` to `./Roblox/v347`
63+
- [feat!](https://github.com/Windows81/Roblox-Freedom-Distribution/discussions/130): to address DataStore2 behaviour, ordered data stores to no longer collide with unordered data stores of the same name
5764
- feat(launcher): assign `-p` alias for both web and RCC connection
5865
- feat(launcher): support for multiple RFD instances across consecutive TCP/UDP ports
5966
- build: update `pyinstaller` to 6.18.0
6067

6168
# 0.64.4
6269

6370
- docs(guides): additional background information for `PatchTLSVerification` guide
64-
- feat!(launcher): changed flag name `--clear_cache` to `--clear_temp_cache`
71+
- feat!(launcher): change flag name `--clear_cache` to `--clear_temp_cache`

Source/assets/serialisers/csg/csgphs8.py

Lines changed: 93 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
# Works Cited:
2+
3+
# krakow10. (2025). rbx_mesh/src/union_physics/v8/edgebreaker.rs at master · krakow10/rbx_mesh. GitHub.
4+
# https://github.com/krakow10/rbx_mesh/blob/master/src/union_physics/v8/edgebreaker.rs
5+
6+
# Rossignac, J., Safonova, A., & Szymczak, A. (2001). Rossignac, Safonova, Szymczak:3D Compression Made Simple 3D Compression Made Simple: Edgebreaker on a Corner-Table.
7+
# https://faculty.cc.gatech.edu/~jarek/papers/CornerTableSMI.pdf
8+
19
import enum
10+
import math
211
from . import util
312

413
from collections.abc import Iterator
@@ -35,24 +44,23 @@ def read_bits(clers_bytes: bytes, total_bits: int) -> Iterator[int]:
3544
https://github.com/krakow10/rbx_mesh/blob/master/src/union_physics/v8/roblox_bit_reader.rs
3645
'''
3746
# Bytes are clustered into groups of 4 (or fewer, if at the end) from first to last.
38-
num_clusters = total_bits // CLUSTER_SIZE // 8 + 1
47+
num_clusters = math.ceil(total_bits / (8 * CLUSTER_SIZE))
3948
for cluster_num in range(num_clusters):
40-
cluster_base = CLUSTER_SIZE * cluster_num
4149

4250
# Clusters are least-significant-bit aligned.
43-
# The final cluster is smaller than 4 bytes.
44-
if cluster_num == num_clusters - 1:
45-
cluster_size = total_bits % CLUSTER_SIZE
46-
else:
47-
cluster_size = CLUSTER_SIZE
48-
49-
# Each cluster has its bytes read from last to first.
50-
for byte_num in range(cluster_size):
51-
byte_idx = cluster_base + cluster_size - 1 - byte_num
52-
53-
# Each byte has its bits read from last to first.
54-
for bit_shift in range(7, -1, -1):
55-
yield (clers_bytes[byte_idx] >> bit_shift) & 0b0000_0001
51+
# The final cluster can be smaller than 4 bytes.
52+
cluster = clers_bytes[
53+
CLUSTER_SIZE * (cluster_num + 0):
54+
CLUSTER_SIZE * (cluster_num + 1)
55+
]
56+
chunk_as_int = int.from_bytes(cluster, 'little')
57+
58+
# Each chunk has its bits read from most to least significant.
59+
cluster_bit_count = min(total_bits, 8 * CLUSTER_SIZE)
60+
total_bits -= cluster_bit_count
61+
for i in range(cluster_bit_count):
62+
yield (chunk_as_int >> (cluster_bit_count - 1 - i)) % 2
63+
5664
return
5765

5866

@@ -68,50 +76,23 @@ def get_prev_edge(c: int) -> int:
6876
return c - 1
6977

7078

71-
def zip_boundary(cursor_edge: int, adjacency_list: list[int], index_list: list[int]) -> int:
72-
current_edge = cursor_edge
73-
74-
# Loops while an edge set to SENTINEL_PROCESSING still needs to be paired.
75-
# Infinitely loops if bad format.
76-
while adjacency_list[current_edge] == SENTINEL_PROCESSING:
77-
candidate_edge = get_next_edge(current_edge)
78-
79-
# Walks the fan via twin, then next until we reach a boundary edge.
80-
# Infinitely loops if bad format.
81-
while adjacency_list[candidate_edge] >= 0:
82-
opposite_edge = adjacency_list[candidate_edge]
83-
candidate_edge = get_next_edge(opposite_edge)
84-
85-
if adjacency_list[candidate_edge] != SENTINEL_BOUNDARY:
86-
break
87-
88-
# Links the two boundary edges as twins (zips them shut).
89-
adjacency_list[current_edge] = candidate_edge
90-
adjacency_list[candidate_edge] = current_edge
91-
92-
prev_edge = current_edge
93-
cand_prev_edge = get_prev_edge(candidate_edge)
94-
95-
# Rewrites the merged corner with the surviving (donor) vertex iden.
96-
index_list[get_prev_edge(current_edge)] = index_list[cand_prev_edge]
97-
98-
# Propagates that vertex iden around the rest of the merged fan.
99-
connected_edge = adjacency_list[current_edge]
100-
101-
# Infinitely loops if bad format.
102-
while connected_edge >= 0 and candidate_edge != prev_edge:
103-
prev_edge = get_prev_edge(connected_edge)
104-
prev_of_prev = get_prev_edge(prev_edge)
105-
index_list[prev_of_prev] = index_list[cand_prev_edge]
106-
connected_edge = adjacency_list[prev_edge]
107-
108-
# Hops along the connected fan to the next still-unzipped edge.
109-
# Infinitely loops if bad format.
110-
while adjacency_list[current_edge] >= 0 and current_edge != candidate_edge:
111-
next_link = adjacency_list[current_edge]
112-
current_edge = get_prev_edge(next_link)
79+
def test_lists(
80+
adjacency_list: list[int],
81+
index_list: list[int],
82+
) -> bool:
83+
if len(index_list) != len(adjacency_list):
84+
return False
11385

114-
return current_edge
86+
for i in range(len(index_list)):
87+
if adjacency_list[i] == SENTINEL_BOUNDARY:
88+
continue
89+
if adjacency_list[i] == SENTINEL_PROCESSING:
90+
continue
91+
if adjacency_list[i] == SENTINEL_UNINIT:
92+
continue
93+
if i != adjacency_list[adjacency_list[i]]:
94+
return False
95+
return True
11596

11697

11798
class CLERS(enum.Enum):
@@ -130,13 +111,8 @@ def decode_clers_symbols(bitreader: Iterator[int]) -> Iterator[CLERS]:
130111
yield CLERS.C
131112
continue
132113

133-
b2 = next(bitreader, None)
134-
if b2 is None:
135-
return
136-
137-
b3 = next(bitreader, None)
138-
if b3 is None:
139-
return
114+
b2 = next(bitreader)
115+
b3 = next(bitreader)
140116

141117
op = (
142118
(b1 * 0b100) +
@@ -161,6 +137,35 @@ def decode_clers_symbols(bitreader: Iterator[int]) -> Iterator[CLERS]:
161137
continue
162138

163139

140+
def zip_boundary(c: int, adjacency_list: list[int], index_list: list[int]):
141+
while True:
142+
b = get_next_edge(c)
143+
144+
while adjacency_list[b] >= 0:
145+
b = get_next_edge(adjacency_list[b])
146+
147+
if adjacency_list[b] != SENTINEL_BOUNDARY:
148+
return
149+
150+
adjacency_list[c] = b
151+
adjacency_list[b] = c
152+
153+
a = get_prev_edge(c)
154+
index_list[get_prev_edge(a)] = index_list[get_prev_edge(b)]
155+
156+
while adjacency_list[a] >= 0 and b != a:
157+
a = get_prev_edge(adjacency_list[a])
158+
index_list[get_prev_edge(
159+
a)] = index_list[get_prev_edge(b)]
160+
161+
c = get_prev_edge(c)
162+
while adjacency_list[c] >= 0 and c != b:
163+
c = get_prev_edge(adjacency_list[c])
164+
165+
if adjacency_list[c] != SENTINEL_PROCESSING:
166+
return
167+
168+
164169
def _decode_triangles(
165170
clers_iter: Iterator[CLERS],
166171
adjacency_list: list[int],
@@ -176,60 +181,58 @@ def _decode_triangles(
176181

177182
# Emits a new triangle and glue its edge 0 to cursor_edge as twins;
178183
# Edges 1 and 2 inherit the corner vertices from the gate edge.
184+
current_triangle += 1
179185
tri_base_edge = 3 * current_triangle
180186

181-
adjacency_list[tri_base_edge + 0] = temp_cursor_edge
182-
adjacency_list[tri_base_edge + 1] = SENTINEL_UNINIT
183-
adjacency_list[tri_base_edge + 2] = SENTINEL_UNINIT
184-
187+
adjacency_list[tri_base_edge] = temp_cursor_edge
185188
adjacency_list[temp_cursor_edge] = tri_base_edge
186189

187-
index_list[tri_base_edge + 1] = (
188-
index_list[get_prev_edge(temp_cursor_edge)]
189-
)
190-
index_list[tri_base_edge + 2] = (
191-
index_list[get_next_edge(temp_cursor_edge)]
190+
(
191+
index_list[get_next_edge(tri_base_edge)],
192+
index_list[get_prev_edge(tri_base_edge)],
193+
) = (
194+
index_list[get_prev_edge(temp_cursor_edge)],
195+
index_list[get_next_edge(temp_cursor_edge)],
192196
)
193-
cursor_stack[-1] = tri_base_edge
194197

195-
current_triangle += 1
198+
cursor_stack[-1] = get_next_edge(tri_base_edge)
196199

197200
op = next(clers_iter, None)
198201
if op is None:
199202
break
200203

201204
if op == CLERS.C: # C: introduce new vertex
205+
vertex_counter += 1
202206
index_list[tri_base_edge] = vertex_counter
203207
next_edge = get_next_edge(cursor_stack[-1])
204208
adjacency_list[next_edge] = SENTINEL_BOUNDARY
205-
vertex_counter += 1
206209
continue
207210

208211
if op == CLERS.L: # L: turn left
209-
adjacency_list[cursor_stack[-1]] = SENTINEL_PROCESSING
210-
cursor_stack[-1] = get_next_edge(cursor_stack[-1])
211-
continue
212-
213-
if op == CLERS.E: # E: end
214-
adjacency_list[cursor_stack[-1]] = SENTINEL_PROCESSING
215212
next_edge = get_next_edge(cursor_stack[-1])
216213
adjacency_list[next_edge] = SENTINEL_PROCESSING
217214
zip_boundary(
218-
cursor_edge=next_edge,
215+
c=next_edge,
219216
adjacency_list=adjacency_list,
220217
index_list=index_list,
221218
)
222-
cursor_stack.pop()
223219
continue
224220

225-
if op == CLERS.R: # R: turn right
221+
if op == CLERS.E: # E: end
222+
adjacency_list[cursor_stack[-1]] = SENTINEL_PROCESSING
226223
next_edge = get_next_edge(cursor_stack[-1])
227224
adjacency_list[next_edge] = SENTINEL_PROCESSING
228225
zip_boundary(
229-
cursor_edge=next_edge,
226+
c=next_edge,
230227
adjacency_list=adjacency_list,
231228
index_list=index_list,
232229
)
230+
cursor_stack.pop()
231+
continue
232+
233+
if op == CLERS.R: # R: turn right
234+
cursor_stack[-1] = get_next_edge(cursor_stack[-1])
235+
adjacency_list[cursor_stack[-1]] = SENTINEL_PROCESSING
233236
continue
234237

235238
if op == CLERS.S: # S: split
@@ -258,8 +261,8 @@ def _edgebreaker_decode(
258261
clers_data = list(clers_reader)
259262
clers_iter = iter(clers_data)
260263
current_triangle = 0
261-
262264
vertex_counter = 2
265+
263266
for _h in range(hull_count):
264267
# Middle edge (1) left as SENTINEL_UNINIT so the decoder starts walking from it.
265268
adjacency_list = [
@@ -285,6 +288,8 @@ def _edgebreaker_decode(
285288
vertex_counter=vertex_counter,
286289
)
287290

291+
assert (test_lists(adjacency_list, index_list))
292+
288293
hull_verts = []
289294
hull_tris = []
290295
max_local_idx = 0

0 commit comments

Comments
 (0)