Skip to content

Commit 859e0bf

Browse files
committed
feat(labrinth): rework v3 side types to a single environment field
This field is meant to be able to represent the existing v2 side type information and beyond, in a way that may also be slightly easier to comprehend.
1 parent 4e4a7be commit 859e0bf

15 files changed

+239
-197
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
DO LANGUAGE plpgsql $$
2+
DECLARE
3+
VAR_env_field_id INT;
4+
VAR_env_field_enum_id INT := 4; -- Known available ID for a new enum type
5+
BEGIN
6+
7+
-- Define a new loader field for environment
8+
INSERT INTO loader_field_enums (id, enum_name, ordering, hidable)
9+
VALUES (VAR_env_field_enum_id, 'environment', NULL, TRUE);
10+
11+
INSERT INTO loader_field_enum_values (enum_id, value, ordering, created, metadata)
12+
VALUES
13+
-- Must be installed on both client and (integrated) server
14+
(VAR_env_field_enum_id, 'client_and_server', NULL, NOW(), NULL),
15+
-- Must be installed only on the client
16+
(VAR_env_field_enum_id, 'client_only', NULL, NOW(), NULL),
17+
-- Must be installed on the client, may be installed on a (integrated) server. To be displayed as a
18+
-- client mod
19+
(VAR_env_field_enum_id, 'client_only_server_optional', NULL, NOW(), NULL),
20+
-- Must be installed only on the integrated singleplayer server. To be displayed as a server mod for
21+
-- singleplayer exclusively
22+
(VAR_env_field_enum_id, 'singleplayer_only', NULL, NOW(), NULL),
23+
-- Must be installed only on a (integrated) server
24+
(VAR_env_field_enum_id, 'server_only', NULL, NOW(), NULL),
25+
-- Must be installed on the server, may be installed on the client. To be displayed as a
26+
-- singleplayer-compatible server mod
27+
(VAR_env_field_enum_id, 'server_only_client_optional', NULL, NOW(), NULL),
28+
-- Must be installed only on a dedicated multiplayer server (not the integrated singleplayer server).
29+
-- To be displayed as an server mod for multiplayer exclusively
30+
(VAR_env_field_enum_id, 'dedicated_server_only', NULL, NOW(), NULL),
31+
-- Can be installed on both client and server, with no strong preference for either. To be displayed
32+
-- as both a client and server mod
33+
(VAR_env_field_enum_id, 'client_or_server', NULL, NOW(), NULL),
34+
-- Can be installed on both client and server, with a preference for being installed on both. To be
35+
-- displayed as a client and server mod
36+
(VAR_env_field_enum_id, 'client_or_server_prefers_both', NULL, NOW(), NULL),
37+
(VAR_env_field_enum_id, 'unknown', NULL, NOW(), NULL);
38+
39+
INSERT INTO loader_fields (field, field_type, enum_type, optional)
40+
VALUES ('environment', 'enum', VAR_env_field_enum_id, FALSE)
41+
RETURNING id INTO VAR_env_field_id;
42+
43+
-- Update version_fields to have the new environment field, initializing it from the
44+
-- values of the previous fields
45+
INSERT INTO version_fields (version_id, field_id, enum_value)
46+
SELECT vf.version_id, VAR_env_field_id, (
47+
SELECT id
48+
FROM loader_field_enum_values
49+
WHERE enum_id = VAR_env_field_enum_id
50+
AND value = (
51+
CASE jsonb_object_agg(lf.field, vf.int_value)
52+
WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_only'
53+
WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'client_and_server'
54+
WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_only_server_optional'
55+
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'singleplayer_only'
56+
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_only'
57+
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'client_and_server'
58+
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_only_server_optional'
59+
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'server_only'
60+
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_or_server'
61+
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'server_only_client_optional'
62+
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_or_server_prefers_both'
63+
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'server_only'
64+
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_or_server'
65+
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'server_only_client_optional'
66+
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_or_server_prefers_both'
67+
ELSE 'unknown'
68+
END
69+
)
70+
)
71+
FROM version_fields vf
72+
JOIN loader_fields lf ON vf.field_id = lf.id
73+
WHERE lf.field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only')
74+
GROUP BY vf.version_id
75+
HAVING COUNT(DISTINCT lf.field) = VAR_env_field_enum_id;
76+
77+
-- Clean up old fields from the project versions
78+
DELETE FROM version_fields
79+
WHERE field_id IN (
80+
SELECT id
81+
FROM loader_fields
82+
WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only')
83+
);
84+
85+
-- Switch loader fields definitions on the available loaders to use the new environment field
86+
ALTER TABLE loader_fields_loaders DROP CONSTRAINT unique_loader_field;
87+
ALTER TABLE loader_fields_loaders DROP CONSTRAINT loader_fields_loaders_pkey;
88+
89+
UPDATE loader_fields_loaders
90+
SET loader_field_id = VAR_env_field_id
91+
WHERE loader_field_id IN (
92+
SELECT id
93+
FROM loader_fields
94+
WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only')
95+
);
96+
97+
-- Remove duplicate (loader_id, loader_field_id) pairs that may have been created due to several
98+
-- old fields being converted to a single new field
99+
DELETE FROM loader_fields_loaders
100+
WHERE ctid NOT IN (
101+
SELECT MIN(ctid)
102+
FROM loader_fields_loaders
103+
GROUP BY loader_id, loader_field_id
104+
);
105+
106+
-- Having both a PK and UNIQUE constraint for the same columns is redundant, so only restore the PK
107+
ALTER TABLE loader_fields_loaders ADD PRIMARY KEY (loader_id, loader_field_id);
108+
109+
-- Finally, remove the old loader fields
110+
DELETE FROM loader_fields
111+
WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only');
112+
113+
END;
114+
$$

apps/labrinth/src/models/v2/projects.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,19 @@ impl LegacyProject {
127127
.collect();
128128

129129
if let Some(versions_item) = versions_item {
130-
// Extract side types from remaining fields (singleplayer, client_only, etc)
130+
// Extract side types from remaining fields
131131
let fields = versions_item
132132
.version_fields
133133
.iter()
134134
.map(|f| {
135135
(f.field_name.clone(), f.value.clone().serialize_internal())
136136
})
137137
.collect::<HashMap<_, _>>();
138-
(client_side, server_side) = v2_reroute::convert_side_types_v2(
139-
&fields,
140-
Some(&*og_project_type),
141-
);
138+
(client_side, server_side) =
139+
v2_reroute::convert_v3_side_types_to_v2_side_types(
140+
&fields,
141+
Some(&*og_project_type),
142+
);
142143

143144
// - if loader is mrpack, this is a modpack
144145
// the loaders are whatever the corresponding loader fields are

apps/labrinth/src/models/v2/search.rs

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,28 +102,20 @@ impl LegacyResultSearchProject {
102102

103103
let project_loader_fields =
104104
result_search_project.project_loader_fields.clone();
105-
let get_one_bool_loader_field = |key: &str| {
105+
let get_one_string_loader_field = |key: &str| {
106106
project_loader_fields
107107
.get(key)
108-
.cloned()
109-
.unwrap_or_default()
108+
.map_or(&[][..], |values| values.as_slice())
110109
.first()
111-
.and_then(|s| s.as_bool())
110+
.and_then(|s| s.as_str())
112111
};
113112

114-
let singleplayer = get_one_bool_loader_field("singleplayer");
115-
let client_only =
116-
get_one_bool_loader_field("client_only").unwrap_or(false);
117-
let server_only =
118-
get_one_bool_loader_field("server_only").unwrap_or(false);
119-
let client_and_server = get_one_bool_loader_field("client_and_server");
113+
let environment =
114+
get_one_string_loader_field("environment").unwrap_or("unknown");
120115

121116
let (client_side, server_side) =
122-
v2_reroute::convert_side_types_v2_bools(
123-
singleplayer,
124-
client_only,
125-
server_only,
126-
client_and_server,
117+
v2_reroute::convert_v3_environment_to_v2_side_types(
118+
environment,
127119
Some(&*og_project_type),
128120
);
129121
let client_side = client_side.to_string();

apps/labrinth/src/queue/moderation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ impl AutomatedModerationQueue {
242242
version_specific: HashMap::new(),
243243
};
244244

245-
if project.project_types.iter().any(|x| ["mod", "modpack"].contains(&&**x)) && !project.aggregate_version_fields.iter().any(|x| ["server_only", "client_only", "client_and_server", "singleplayer"].contains(&&*x.field_name)) {
245+
if project.project_types.iter().any(|x| ["mod", "modpack"].contains(&&**x)) && !project.aggregate_version_fields.iter().any(|x| x.field_name == "environment") {
246246
mod_messages.messages.push(ModerationMessage::NoSideTypes);
247247
}
248248

apps/labrinth/src/routes/v2/project_creation.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,12 @@ pub async fn project_create(
158158
.into_iter()
159159
.map(|v| {
160160
let mut fields = HashMap::new();
161-
fields.extend(v2_reroute::convert_side_types_v3(
162-
client_side,
163-
server_side,
164-
));
161+
fields.extend(
162+
v2_reroute::convert_v2_side_types_to_v3_side_types(
163+
client_side,
164+
server_side,
165+
),
166+
);
165167
fields.insert(
166168
"game_versions".to_string(),
167169
json!(v.game_versions),

apps/labrinth/src/routes/v2/projects.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,10 +547,12 @@ pub async fn project_edit(
547547
let version = Version::from(version);
548548
let mut fields = version.fields;
549549
let (current_client_side, current_server_side) =
550-
v2_reroute::convert_side_types_v2(&fields, None);
550+
v2_reroute::convert_v3_side_types_to_v2_side_types(
551+
&fields, None,
552+
);
551553
let client_side = client_side.unwrap_or(current_client_side);
552554
let server_side = server_side.unwrap_or(current_server_side);
553-
fields.extend(v2_reroute::convert_side_types_v3(
555+
fields.extend(v2_reroute::convert_v2_side_types_to_v3_side_types(
554556
client_side,
555557
server_side,
556558
));

apps/labrinth/src/routes/v2/version_creation.rs

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub async fn version_create(
105105
json!(legacy_create.game_versions),
106106
);
107107

108-
// Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc.
108+
// Get all possible side-types for loaders given- we will use these to check if we need to convert/apply side types
109109
let loaders =
110110
match v3::tags::loader_list(client.clone(), redis.clone())
111111
.await
@@ -136,53 +136,32 @@ pub async fn version_create(
136136
.collect::<Vec<_>>();
137137

138138
// Copies side types of another version of the project.
139-
// If no version exists, defaults to all false.
139+
// If no version exists, defaults to an unknown side type.
140140
// This is inherently lossy, but not much can be done about it, as side types are no longer associated with projects,
141-
// so the 'missing' ones can't be easily accessed, and versions do need to have these fields explicitly set.
142-
let side_type_loader_field_names = [
143-
"singleplayer",
144-
"client_and_server",
145-
"client_only",
146-
"server_only",
147-
];
141+
// so the 'missing' ones can't be easily accessed, and versions do need to have that field explicitly set.
148142

149-
// Check if loader_fields_aggregate contains any of these side types
143+
// Check if loader_fields_aggregate contains the side types
150144
// We assume these four fields are linked together.
151145
if loader_fields_aggregate
152146
.iter()
153-
.any(|f| side_type_loader_field_names.contains(&f.as_str()))
147+
.any(|field| field == "environment")
154148
{
155-
// If so, we get the fields of the example version of the project, and set the side types to match.
156-
fields.extend(
157-
side_type_loader_field_names
158-
.iter()
159-
.map(|f| (f.to_string(), json!(false))),
160-
);
161-
if let Some(example_version_fields) =
149+
// If so, we get the field of an example version of the project, and set the side types to match.
150+
fields.insert(
151+
"environment".into(),
162152
get_example_version_fields(
163153
legacy_create.project_id,
164154
client,
165155
&redis,
166156
)
167157
.await?
168-
{
169-
fields.extend(
170-
example_version_fields.into_iter().filter_map(
171-
|f| {
172-
if side_type_loader_field_names
173-
.contains(&f.field_name.as_str())
174-
{
175-
Some((
176-
f.field_name,
177-
f.value.serialize_internal(),
178-
))
179-
} else {
180-
None
181-
}
182-
},
183-
),
184-
);
185-
}
158+
.into_iter()
159+
.flatten()
160+
.find(|f| f.field_name == "environment")
161+
.map_or(json!("unknown"), |f| {
162+
f.value.serialize_internal()
163+
}),
164+
);
186165
}
187166
// Handle project type via file extension prediction
188167
let mut project_type = None;

0 commit comments

Comments
 (0)