Skip to content

feat(labrinth): rework v3 side types to a single environment field #3701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
DO LANGUAGE plpgsql $$
DECLARE
VAR_env_field_id INT;
VAR_env_field_enum_id INT := 4; -- Known available ID for a new enum type
BEGIN

-- Define a new loader field for environment
INSERT INTO loader_field_enums (id, enum_name, ordering, hidable)
VALUES (VAR_env_field_enum_id, 'environment', NULL, TRUE);

INSERT INTO loader_field_enum_values (enum_id, value, ordering, created, metadata)
VALUES
-- Must be installed on both client and (integrated) server
(VAR_env_field_enum_id, 'client_and_server', NULL, NOW(), NULL),
-- Must be installed only on the client
(VAR_env_field_enum_id, 'client_only', NULL, NOW(), NULL),
-- Must be installed on the client, may be installed on a (integrated) server. To be displayed as a
-- client mod
(VAR_env_field_enum_id, 'client_only_server_optional', NULL, NOW(), NULL),
-- Must be installed only on the integrated singleplayer server. To be displayed as a server mod for
-- singleplayer exclusively
(VAR_env_field_enum_id, 'singleplayer_only', NULL, NOW(), NULL),
-- Must be installed only on a (integrated) server
(VAR_env_field_enum_id, 'server_only', NULL, NOW(), NULL),
-- Must be installed on the server, may be installed on the client. To be displayed as a
-- singleplayer-compatible server mod
(VAR_env_field_enum_id, 'server_only_client_optional', NULL, NOW(), NULL),
-- Must be installed only on a dedicated multiplayer server (not the integrated singleplayer server).
-- To be displayed as an server mod for multiplayer exclusively
(VAR_env_field_enum_id, 'dedicated_server_only', NULL, NOW(), NULL),
-- Can be installed on both client and server, with no strong preference for either. To be displayed
-- as both a client and server mod
(VAR_env_field_enum_id, 'client_or_server', NULL, NOW(), NULL),
-- Can be installed on both client and server, with a preference for being installed on both. To be
-- displayed as a client and server mod
(VAR_env_field_enum_id, 'client_or_server_prefers_both', NULL, NOW(), NULL),
(VAR_env_field_enum_id, 'unknown', NULL, NOW(), NULL);

INSERT INTO loader_fields (field, field_type, enum_type, optional)
VALUES ('environment', 'enum', VAR_env_field_enum_id, FALSE)
RETURNING id INTO VAR_env_field_id;

-- Update version_fields to have the new environment field, initializing it from the
-- values of the previous fields
INSERT INTO version_fields (version_id, field_id, enum_value)
SELECT vf.version_id, VAR_env_field_id, (
SELECT id
FROM loader_field_enum_values
WHERE enum_id = VAR_env_field_enum_id
AND value = (
CASE jsonb_object_agg(lf.field, vf.int_value)
WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_only'
WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'client_and_server'
WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_only_server_optional'
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'singleplayer_only'
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_only'
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'client_and_server'
WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_only_server_optional'
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'server_only'
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_or_server'
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'server_only_client_optional'
WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_or_server_prefers_both'
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'server_only'
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_or_server'
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'server_only_client_optional'
WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_or_server_prefers_both'
ELSE 'unknown'
END
)
)
FROM version_fields vf
JOIN loader_fields lf ON vf.field_id = lf.id
WHERE lf.field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only')
GROUP BY vf.version_id
HAVING COUNT(DISTINCT lf.field) = 4;

-- Clean up old fields from the project versions
DELETE FROM version_fields
WHERE field_id IN (
SELECT id
FROM loader_fields
WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only')
);

-- Switch loader fields definitions on the available loaders to use the new environment field
ALTER TABLE loader_fields_loaders DROP CONSTRAINT unique_loader_field;
ALTER TABLE loader_fields_loaders DROP CONSTRAINT loader_fields_loaders_pkey;

UPDATE loader_fields_loaders
SET loader_field_id = VAR_env_field_id
WHERE loader_field_id IN (
SELECT id
FROM loader_fields
WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only')
);

-- Remove duplicate (loader_id, loader_field_id) pairs that may have been created due to several
-- old fields being converted to a single new field
DELETE FROM loader_fields_loaders
WHERE ctid NOT IN (
SELECT MIN(ctid)
FROM loader_fields_loaders
GROUP BY loader_id, loader_field_id
);

-- Having both a PK and UNIQUE constraint for the same columns is redundant, so only restore the PK
ALTER TABLE loader_fields_loaders ADD PRIMARY KEY (loader_id, loader_field_id);

-- Finally, remove the old loader fields
DELETE FROM loader_fields
WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only');

END;
$$
11 changes: 6 additions & 5 deletions apps/labrinth/src/models/v2/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,19 @@ impl LegacyProject {
.collect();

if let Some(versions_item) = versions_item {
// Extract side types from remaining fields (singleplayer, client_only, etc)
// Extract side types from remaining fields
let fields = versions_item
.version_fields
.iter()
.map(|f| {
(f.field_name.clone(), f.value.clone().serialize_internal())
})
.collect::<HashMap<_, _>>();
(client_side, server_side) = v2_reroute::convert_side_types_v2(
&fields,
Some(&*og_project_type),
);
(client_side, server_side) =
v2_reroute::convert_v3_side_types_to_v2_side_types(
&fields,
Some(&*og_project_type),
);

// - if loader is mrpack, this is a modpack
// the loaders are whatever the corresponding loader fields are
Expand Down
22 changes: 7 additions & 15 deletions apps/labrinth/src/models/v2/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,20 @@ impl LegacyResultSearchProject {

let project_loader_fields =
result_search_project.project_loader_fields.clone();
let get_one_bool_loader_field = |key: &str| {
let get_one_string_loader_field = |key: &str| {
project_loader_fields
.get(key)
.cloned()
.unwrap_or_default()
.map_or(&[][..], |values| values.as_slice())
.first()
.and_then(|s| s.as_bool())
.and_then(|s| s.as_str())
};

let singleplayer = get_one_bool_loader_field("singleplayer");
let client_only =
get_one_bool_loader_field("client_only").unwrap_or(false);
let server_only =
get_one_bool_loader_field("server_only").unwrap_or(false);
let client_and_server = get_one_bool_loader_field("client_and_server");
let environment =
get_one_string_loader_field("environment").unwrap_or("unknown");

let (client_side, server_side) =
v2_reroute::convert_side_types_v2_bools(
singleplayer,
client_only,
server_only,
client_and_server,
v2_reroute::convert_v3_environment_to_v2_side_types(
environment,
Some(&*og_project_type),
);
let client_side = client_side.to_string();
Expand Down
2 changes: 1 addition & 1 deletion apps/labrinth/src/queue/moderation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ impl AutomatedModerationQueue {
version_specific: HashMap::new(),
};

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)) {
if project.project_types.iter().any(|x| ["mod", "modpack"].contains(&&**x)) && !project.aggregate_version_fields.iter().any(|x| x.field_name == "environment") {
mod_messages.messages.push(ModerationMessage::NoSideTypes);
}

Expand Down
10 changes: 6 additions & 4 deletions apps/labrinth/src/routes/v2/project_creation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,12 @@ pub async fn project_create(
.into_iter()
.map(|v| {
let mut fields = HashMap::new();
fields.extend(v2_reroute::convert_side_types_v3(
client_side,
server_side,
));
fields.extend(
v2_reroute::convert_v2_side_types_to_v3_side_types(
client_side,
server_side,
),
);
fields.insert(
"game_versions".to_string(),
json!(v.game_versions),
Expand Down
6 changes: 4 additions & 2 deletions apps/labrinth/src/routes/v2/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,10 +547,12 @@ pub async fn project_edit(
let version = Version::from(version);
let mut fields = version.fields;
let (current_client_side, current_server_side) =
v2_reroute::convert_side_types_v2(&fields, None);
v2_reroute::convert_v3_side_types_to_v2_side_types(
&fields, None,
);
let client_side = client_side.unwrap_or(current_client_side);
let server_side = server_side.unwrap_or(current_server_side);
fields.extend(v2_reroute::convert_side_types_v3(
fields.extend(v2_reroute::convert_v2_side_types_to_v3_side_types(
client_side,
server_side,
));
Expand Down
51 changes: 15 additions & 36 deletions apps/labrinth/src/routes/v2/version_creation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub async fn version_create(
json!(legacy_create.game_versions),
);

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

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

// Check if loader_fields_aggregate contains any of these side types
// Check if loader_fields_aggregate contains the side types
// We assume these four fields are linked together.
if loader_fields_aggregate
.iter()
.any(|f| side_type_loader_field_names.contains(&f.as_str()))
.any(|field| field == "environment")
{
// If so, we get the fields of the example version of the project, and set the side types to match.
fields.extend(
side_type_loader_field_names
.iter()
.map(|f| (f.to_string(), json!(false))),
);
if let Some(example_version_fields) =
// If so, we get the field of an example version of the project, and set the side types to match.
fields.insert(
"environment".into(),
get_example_version_fields(
legacy_create.project_id,
client,
&redis,
)
.await?
{
fields.extend(
example_version_fields.into_iter().filter_map(
|f| {
if side_type_loader_field_names
.contains(&f.field_name.as_str())
{
Some((
f.field_name,
f.value.serialize_internal(),
))
} else {
None
}
},
),
);
}
.into_iter()
.flatten()
.find(|f| f.field_name == "environment")
.map_or(json!("unknown"), |f| {
f.value.serialize_internal()
}),
);
}
// Handle project type via file extension prediction
let mut project_type = None;
Expand Down
Loading
Loading