Merge commit '74cf3f076eff43755bb4bef62f1c1bb3fc0e6c2a' into feature-clean
2
packages/app-lib/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# SQLite database file location
|
||||
DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||
12
packages/app-lib/.sqlx/query-1a979fbc58be7dde562b6f7c8fdcf9a4fc5a9817f4831c66bd56f2f4d0a00f82.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM attached_world_data\n WHERE profile_path = $1 and world_type = $2 and world_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1a979fbc58be7dde562b6f7c8fdcf9a4fc5a9817f4831c66bd56f2f4d0a00f82"
|
||||
}
|
||||
12
packages/app-lib/.sqlx/query-27283e20fc86c941c7d6d09259d8f4ec2e248f751f98140f77bea4f9d5971ef1.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11,\n $12, $13, $14,\n $15, $16,\n $17, jsonb($18), jsonb($19),\n $20, $21, $22, $23,\n $24, $25, $26,\n $27, $28\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n\n created = $12,\n modified = $13,\n last_played = $14,\n\n submitted_time_played = $15,\n recent_time_played = $16,\n\n override_java_path = $17,\n override_extra_launch_args = jsonb($18),\n override_custom_env_vars = jsonb($19),\n override_mc_memory_max = $20,\n override_mc_force_fullscreen = $21,\n override_mc_game_resolution_x = $22,\n override_mc_game_resolution_y = $23,\n\n override_hook_pre_launch = $24,\n override_hook_wrapper = $25,\n override_hook_post_exit = $26,\n\n protocol_version = $27,\n launcher_feature_version = $28\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 28
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "27283e20fc86c941c7d6d09259d8f4ec2e248f751f98140f77bea4f9d5971ef1"
|
||||
}
|
||||
38
packages/app-lib/.sqlx/query-54a8629d3d660bfeed582b08aee4a8f1543f6b962e54ecab491a006d28c9a18c.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT profile_path, host, port, join_time\n FROM join_log\n WHERE profile_path = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "profile_path",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "host",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "join_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "54a8629d3d660bfeed582b08aee4a8f1543f6b962e54ecab491a006d28c9a18c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
|
||||
"query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -14,123 +14,133 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"name": "launcher_feature_version",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "icon_path",
|
||||
"name": "name",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "game_version",
|
||||
"name": "icon_path",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mod_loader",
|
||||
"name": "game_version",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mod_loader_version",
|
||||
"name": "protocol_version",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "mod_loader",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "groups!: serde_json::Value",
|
||||
"ordinal": 7,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "linked_project_id",
|
||||
"name": "mod_loader_version",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "linked_version_id",
|
||||
"name": "groups!: serde_json::Value",
|
||||
"ordinal": 9,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "linked_project_id",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "linked_version_id",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "locked",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"ordinal": 11,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "modified",
|
||||
"ordinal": 12,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "last_played",
|
||||
"name": "created",
|
||||
"ordinal": 13,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "submitted_time_played",
|
||||
"name": "modified",
|
||||
"ordinal": 14,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recent_time_played",
|
||||
"name": "last_played",
|
||||
"ordinal": 15,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"name": "submitted_time_played",
|
||||
"ordinal": 16,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recent_time_played",
|
||||
"ordinal": 17,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_extra_launch_args!: serde_json::Value",
|
||||
"ordinal": 17,
|
||||
"ordinal": 19,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_custom_env_vars!: serde_json::Value",
|
||||
"ordinal": 18,
|
||||
"ordinal": 20,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_memory_max",
|
||||
"ordinal": 19,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"ordinal": 20,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"ordinal": 21,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"ordinal": 22,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_pre_launch",
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"ordinal": 23,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"ordinal": 24,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_pre_launch",
|
||||
"ordinal": 25,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_wrapper",
|
||||
"ordinal": 24,
|
||||
"ordinal": 26,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_post_exit",
|
||||
"ordinal": 25,
|
||||
"ordinal": 27,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -141,8 +151,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
@@ -166,5 +178,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5265d5ad85da898855d628f6b45e39026908fc950aad3c7797be37b5d0b74094"
|
||||
"hash": "6a434cc55635b6e325e9e5f06d21b787af281db4402c8ff45fe6a77b5be6c929"
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
{
|
||||
"name": "display_claims!: serde_json::Value",
|
||||
"ordinal": 7,
|
||||
"type_info": "Null"
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
20
packages/app-lib/.sqlx/query-a2184fc5d62570aec0a15c0a8d628a597e90c2bf7ce5dc1b39edb6977e2f6da6.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT display_status\n FROM attached_world_data\n WHERE profile_path = $1 and world_type = $2 and world_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "display_status",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a2184fc5d62570aec0a15c0a8d628a597e90c2bf7ce5dc1b39edb6977e2f6da6"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
|
||||
"query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -14,123 +14,133 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"name": "launcher_feature_version",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "icon_path",
|
||||
"name": "name",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "game_version",
|
||||
"name": "icon_path",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mod_loader",
|
||||
"name": "game_version",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mod_loader_version",
|
||||
"name": "protocol_version",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "mod_loader",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "groups!: serde_json::Value",
|
||||
"ordinal": 7,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "linked_project_id",
|
||||
"name": "mod_loader_version",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "linked_version_id",
|
||||
"name": "groups!: serde_json::Value",
|
||||
"ordinal": 9,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "linked_project_id",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "linked_version_id",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "locked",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"ordinal": 11,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "modified",
|
||||
"ordinal": 12,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "last_played",
|
||||
"name": "created",
|
||||
"ordinal": 13,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "submitted_time_played",
|
||||
"name": "modified",
|
||||
"ordinal": 14,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recent_time_played",
|
||||
"name": "last_played",
|
||||
"ordinal": 15,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"name": "submitted_time_played",
|
||||
"ordinal": 16,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recent_time_played",
|
||||
"ordinal": 17,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_extra_launch_args!: serde_json::Value",
|
||||
"ordinal": 17,
|
||||
"ordinal": 19,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_custom_env_vars!: serde_json::Value",
|
||||
"ordinal": 18,
|
||||
"ordinal": 20,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_memory_max",
|
||||
"ordinal": 19,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"ordinal": 20,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"ordinal": 21,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"ordinal": 22,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_pre_launch",
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"ordinal": 23,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"ordinal": 24,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_pre_launch",
|
||||
"ordinal": 25,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_wrapper",
|
||||
"ordinal": 24,
|
||||
"ordinal": 26,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_post_exit",
|
||||
"ordinal": 25,
|
||||
"ordinal": 27,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -141,8 +151,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
@@ -166,5 +178,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4acd47f6bad3d2d4df5e5d43b3441fa2714cb8ad978adc108acc67f042380df1"
|
||||
"hash": "c108849d77c7627d6a11d5be34984938c41283e6091a1301fc3ca0b355ffcfa9"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11,\n $12, $13, $14,\n $15, $16,\n $17, jsonb($18), jsonb($19),\n $20, $21, $22, $23,\n $24, $25, $26\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n\n created = $12,\n modified = $13,\n last_played = $14,\n\n submitted_time_played = $15,\n recent_time_played = $16,\n\n override_java_path = $17,\n override_extra_launch_args = jsonb($18),\n override_custom_env_vars = jsonb($19),\n override_mc_memory_max = $20,\n override_mc_force_fullscreen = $21,\n override_mc_game_resolution_x = $22,\n override_mc_game_resolution_y = $23,\n\n override_hook_pre_launch = $24,\n override_hook_wrapper = $25,\n override_hook_post_exit = $26\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 26
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "db1f94b9c17c790c029a7691620d6bbdcbdfcce4b069b8ed46dc3abd2f5f4e58"
|
||||
}
|
||||
12
packages/app-lib/.sqlx/query-df600f2615979ab61bfe235a04add18a4900021ee6ccfc165c9a6dad41046cba.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO attached_world_data (profile_path, world_type, world_id, display_status)\nVALUES ($1, $2, $3, $4)\nON CONFLICT (profile_path, world_type, world_id) DO UPDATE\n SET display_status = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "df600f2615979ab61bfe235a04add18a4900021ee6ccfc165c9a6dad41046cba"
|
||||
}
|
||||
12
packages/app-lib/.sqlx/query-e9f3d57ac9055575366d5ebd40b64f627ae744a20acf5affaa729e68e3eb6641.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO join_log (profile_path, host, port, join_time)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (profile_path, host, port) DO UPDATE SET\n join_time = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e9f3d57ac9055575366d5ebd40b64f627ae744a20acf5affaa729e68e3eb6641"
|
||||
}
|
||||
32
packages/app-lib/.sqlx/query-fd834e256e142820f25305ccffaf07f736c5772045b973dcc10573b399111344.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT world_type, world_id, display_status\n FROM attached_world_data\n WHERE profile_path = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "world_type",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "world_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "display_status",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fd834e256e142820f25305ccffaf07f736c5772045b973dcc10573b399111344"
|
||||
}
|
||||
@@ -1,70 +1,76 @@
|
||||
[package]
|
||||
name = "theseus"
|
||||
version = "0.9.3"
|
||||
version = "0.9.5"
|
||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_ini = "0.2.0"
|
||||
sha1_smol = { version = "1.0.0", features = ["std"] }
|
||||
sha2 = "0.10.8"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1", features = ["serde", "v4"] }
|
||||
zip = "0.6.5"
|
||||
async_zip = { version = "0.0.17", features = ["chrono", "tokio-fs", "deflate", "bzip2", "zstd", "deflate64"] }
|
||||
flate2 = "1.0.28"
|
||||
tempfile = "3.5.0"
|
||||
dashmap = { version = "6.0.1", features = ["serde"] }
|
||||
bytes.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_ini.workspace = true
|
||||
sha1_smol.workspace = true
|
||||
sha2.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
zip.workspace = true
|
||||
async_zip = { workspace = true, features = ["chrono", "tokio-fs", "deflate", "bzip2", "zstd", "deflate64"] }
|
||||
flate2.workspace = true
|
||||
tempfile.workspace = true
|
||||
dashmap = { workspace = true, features = ["serde"] }
|
||||
quick-xml = { workspace = true, features = ["async-tokio"] }
|
||||
enumset.workspace = true
|
||||
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
daedalus = { path = "../../packages/daedalus" }
|
||||
dirs = "5.0.1"
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
daedalus.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
regex = "1.5"
|
||||
sys-info = "0.9.0"
|
||||
sysinfo = "0.30.8"
|
||||
thiserror = "1.0"
|
||||
either = "1.13"
|
||||
regex.workspace = true
|
||||
sysinfo = { workspace = true, features = ["system", "disk"] }
|
||||
thiserror.workspace = true
|
||||
either.workspace = true
|
||||
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["chrono", "env-filter"] }
|
||||
tracing-error.workspace = true
|
||||
|
||||
paste = { version = "1.0" }
|
||||
paste.workspace = true
|
||||
|
||||
tauri = { version = "2.0.0-rc", optional = true }
|
||||
indicatif = { version = "0.17.3", optional = true }
|
||||
tauri = { workspace = true, optional = true }
|
||||
indicatif = { workspace = true, optional = true }
|
||||
|
||||
async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] }
|
||||
futures = "0.3"
|
||||
reqwest = { version = "0.12.3", features = ["json", "stream", "deflate", "gzip", "brotli", "rustls-tls", "charset", "http2", "macos-system-configuration"], default-features = false }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
async-recursion = "1.0.4"
|
||||
async-tungstenite = { workspace = true, features = ["tokio-runtime", "tokio-rustls-webpki-roots"] }
|
||||
futures = { workspace = true, features = ["async-await", "alloc"] }
|
||||
reqwest = { workspace = true, features = ["json", "stream", "deflate", "gzip", "brotli", "rustls-tls-webpki-roots", "charset", "http2", "macos-system-configuration"] }
|
||||
tokio = { workspace = true, features = ["time", "io-util", "net", "sync", "fs", "macros", "process"] }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
async-recursion.workspace = true
|
||||
fs4 = { workspace = true, features = ["tokio"] }
|
||||
async-walkdir.workspace = true
|
||||
async-compression = { workspace = true, features = ["tokio", "gzip"] }
|
||||
|
||||
notify = { version = "6.1.1", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||
notify.workspace = true
|
||||
notify-debouncer-mini.workspace = true
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
dunce = "1.0.3"
|
||||
dunce.workspace = true
|
||||
|
||||
whoami = "1.4.0"
|
||||
whoami.workspace = true
|
||||
|
||||
discord-rich-presence = "0.2.4"
|
||||
discord-rich-presence.workspace = true
|
||||
|
||||
p256 = { version = "0.13.2", features = ["ecdsa"] }
|
||||
rand = "0.8"
|
||||
byteorder = "1.5.0"
|
||||
base64 = "0.22.0"
|
||||
p256 = { workspace = true, features = ["ecdsa"] }
|
||||
rand.workspace = true
|
||||
base64.workspace = true
|
||||
|
||||
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "sqlite", "macros" ] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "macros", "migrate", "json"] }
|
||||
|
||||
ariadne = { path = "../ariadne" }
|
||||
quartz_nbt = { workspace = true, features = ["serde"] }
|
||||
hickory-resolver.workspace = true
|
||||
|
||||
ariadne.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52.0"
|
||||
winreg.workspace = true
|
||||
|
||||
[features]
|
||||
tauri = ["dep:tauri"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE profiles ADD COLUMN protocol_version INTEGER NULL
|
||||
10
packages/app-lib/migrations/20250408181656_add-join-log.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE join_log (
|
||||
profile_path TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
join_time INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (profile_path, host, port),
|
||||
FOREIGN KEY (profile_path) REFERENCES profiles(path) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX join_log_profile_path ON join_log(profile_path);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE profiles ADD COLUMN launcher_feature_version TEXT NOT NULL DEFAULT 'none'
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE attached_world_data (
|
||||
profile_path TEXT NOT NULL,
|
||||
world_type TEXT CHECK ( world_type in ('singleplayer', 'server') ) NOT NULL,
|
||||
world_id TEXT NOT NULL,
|
||||
display_status TEXT NOT NULL DEFAULT 'normal',
|
||||
|
||||
PRIMARY KEY (profile_path, world_type, world_id),
|
||||
FOREIGN KEY (profile_path) REFERENCES profiles(path) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX attached_world_data_profile_path ON attached_world_data(profile_path);
|
||||
@@ -2,8 +2,8 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_command, emit_warning},
|
||||
CommandPayload,
|
||||
emit::{emit_command, emit_warning},
|
||||
},
|
||||
util::io,
|
||||
};
|
||||
|
||||
@@ -6,12 +6,13 @@ use dashmap::DashMap;
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use sysinfo::{MemoryRefreshKind, RefreshKind};
|
||||
|
||||
use crate::util::io;
|
||||
use crate::util::jre::extract_java_majorminor_version;
|
||||
use crate::{
|
||||
util::jre::{self},
|
||||
LoadingBarType, State,
|
||||
util::jre::{self},
|
||||
};
|
||||
|
||||
pub async fn get_java_versions() -> crate::Result<DashMap<u32, JavaVersion>> {
|
||||
@@ -175,11 +176,10 @@ pub async fn test_jre(
|
||||
|
||||
// Gets maximum memory in KiB.
|
||||
pub async fn get_max_memory() -> crate::Result<u64> {
|
||||
Ok(sys_info::mem_info()
|
||||
.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::LauncherError(
|
||||
"Unable to get computer memory".to_string(),
|
||||
))
|
||||
})?
|
||||
.total)
|
||||
Ok(sysinfo::System::new_with_specifics(
|
||||
RefreshKind::nothing()
|
||||
.with_memory(MemoryRefreshKind::nothing().with_ram()),
|
||||
)
|
||||
.total_memory()
|
||||
/ 1024)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ use tokio::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
prelude::Credentials,
|
||||
util::io::{self, IOError},
|
||||
State,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -42,8 +42,8 @@ impl CensoredString {
|
||||
pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self {
|
||||
let username = whoami::username();
|
||||
s = s
|
||||
.replace(&format!("/{}/", username), "/{COMPUTER_USERNAME}/")
|
||||
.replace(&format!("\\{}\\", username), "\\{COMPUTER_USERNAME}\\");
|
||||
.replace(&format!("/{username}/"), "/{COMPUTER_USERNAME}/")
|
||||
.replace(&format!("\\{username}\\"), "\\{COMPUTER_USERNAME}\\");
|
||||
for credentials in credentials_set {
|
||||
s = s
|
||||
.replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}")
|
||||
@@ -298,7 +298,7 @@ pub async fn get_latest_log_cursor(
|
||||
profile_path: &str,
|
||||
cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
|
||||
get_generic_live_log_cursor(profile_path, "launcher_log.txt", cursor).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::state::CachedEntry;
|
||||
use crate::State;
|
||||
use crate::state::CachedEntry;
|
||||
pub use daedalus::minecraft::VersionManifest;
|
||||
pub use daedalus::modded::Manifest;
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn get_loader_versions(loader: &str) -> crate::Result<Manifest> {
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor(format!("{} loader versions", loader))
|
||||
crate::ErrorKind::NoValueFor(format!("{loader} loader versions"))
|
||||
})?;
|
||||
|
||||
Ok(loaders.manifest)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Authentication flow interface
|
||||
|
||||
use crate::state::{Credentials, MinecraftLoginFlow};
|
||||
use crate::State;
|
||||
use crate::state::{Credentials, MinecraftLoginFlow};
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
|
||||
|
||||
@@ -12,7 +12,8 @@ pub mod process;
|
||||
pub mod profile;
|
||||
pub mod settings;
|
||||
pub mod tags;
|
||||
pub mod download;
|
||||
pub mod download; // AstralRinth
|
||||
pub mod worlds;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::state::{
|
||||
@@ -27,12 +28,12 @@ pub mod data {
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
State,
|
||||
data::*,
|
||||
event::CommandPayload,
|
||||
jre, metadata, minecraft_auth, mr_auth, pack, process,
|
||||
profile::{self, create, Profile},
|
||||
profile::{self, Profile, create},
|
||||
settings,
|
||||
util::io::{canonicalize, IOError},
|
||||
State,
|
||||
util::io::{IOError, canonicalize},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
pack::{
|
||||
self,
|
||||
import::{self, copy_dotminecraft},
|
||||
@@ -11,7 +12,6 @@ use crate::{
|
||||
prelude::ModLoader,
|
||||
state::{LinkedData, ProfileInstallStage},
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -162,7 +162,7 @@ pub async fn import_atlauncher(
|
||||
profile_path: profile_path.to_string(),
|
||||
};
|
||||
|
||||
let backup_name = format!("ATLauncher-{}", instance_folder);
|
||||
let backup_name = format!("ATLauncher-{instance_folder}");
|
||||
let minecraft_folder = atlauncher_instance_path;
|
||||
|
||||
import_atlauncher_unmanaged(
|
||||
@@ -190,8 +190,7 @@ async fn import_atlauncher_unmanaged(
|
||||
let mod_loader: ModLoader = serde_json::from_str::<ModLoader>(&mod_loader)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not parse mod loader type: {}",
|
||||
mod_loader
|
||||
"Could not parse mod loader type: {mod_loader}"
|
||||
))
|
||||
})?;
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
prelude::ModLoader,
|
||||
state::ProfileInstallStage,
|
||||
util::{
|
||||
fetch::{fetch, write_cached_icon},
|
||||
io,
|
||||
},
|
||||
State,
|
||||
};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{prelude::ModLoader, state::ProfileInstallStage, util::io, State};
|
||||
use crate::{State, prelude::ModLoader, state::ProfileInstallStage, util::io};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
pack::{
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::{self, CreatePackDescription, PackDependency},
|
||||
},
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
|
||||
// instance.cfg
|
||||
|
||||
@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_loading, init_or_edit_loading},
|
||||
LoadingBarId,
|
||||
emit::{emit_loading, init_or_edit_loading},
|
||||
},
|
||||
util::{
|
||||
fetch::{self, IoSemaphore},
|
||||
@@ -71,7 +71,7 @@ pub async fn get_importable_instances(
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Launcher type Unknown".to_string(),
|
||||
)
|
||||
.into())
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -187,11 +187,7 @@ pub fn get_default_launcher_path(
|
||||
ImportLauncherType::Unknown => None,
|
||||
};
|
||||
let path = path?;
|
||||
if path.exists() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if path.exists() { Some(path) } else { None }
|
||||
}
|
||||
|
||||
/// Checks if this PathBuf is a valid instance for the given launcher type
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::State;
|
||||
use crate::data::ModLoader;
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::state::{CachedEntry, LinkedData, ProfileInstallStage, SideType};
|
||||
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
|
||||
use crate::util::io;
|
||||
use crate::State;
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
||||
};
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::pack::install_from::{
|
||||
set_profile_information, EnvType, PackFile, PackFileHash,
|
||||
EnvType, PackFile, PackFileHash, set_profile_information,
|
||||
};
|
||||
use crate::state::{
|
||||
cache_file_hash, CacheBehaviour, CachedEntry, ProfileInstallStage, SideType,
|
||||
CacheBehaviour, CachedEntry, ProfileInstallStage, SideType, cache_file_hash,
|
||||
};
|
||||
use crate::util::fetch::{fetch_mirrors, write};
|
||||
use crate::util::io;
|
||||
use crate::{profile, State};
|
||||
use crate::{State, profile};
|
||||
use async_zip::base::read::seek::ZipFileReader;
|
||||
|
||||
use super::install_from::{
|
||||
generate_pack_from_file, generate_pack_from_version_id, CreatePack,
|
||||
CreatePackLocation, PackFormat,
|
||||
CreatePack, CreatePackLocation, PackFormat, generate_pack_from_file,
|
||||
generate_pack_from_version_id,
|
||||
};
|
||||
use crate::data::ProjectType;
|
||||
use std::io::Cursor;
|
||||
@@ -266,10 +266,7 @@ pub async fn install_zipped_mrpack_files(
|
||||
emit_loading(
|
||||
&loading_bar,
|
||||
30.0 / total_len as f64,
|
||||
Some(&format!(
|
||||
"Extracting override {}/{}",
|
||||
index, total_len
|
||||
)),
|
||||
Some(&format!("Extracting override {index}/{total_len}")),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use crate::state::ProcessMetadata;
|
||||
pub use crate::{
|
||||
state::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
State,
|
||||
state::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::launcher::get_loader_version_from_profile;
|
||||
use crate::settings::Hooks;
|
||||
use crate::state::{LinkedData, ProfileInstallStage};
|
||||
use crate::state::{LauncherFeatureVersion, LinkedData, ProfileInstallStage};
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{ErrorKind, pack, profile};
|
||||
pub use crate::{State, state::Profile};
|
||||
use crate::{
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
event::{ProfilePayloadType, emit::emit_profile},
|
||||
prelude::ModLoader,
|
||||
};
|
||||
use crate::{pack, profile, ErrorKind};
|
||||
pub use crate::{state::Profile, State};
|
||||
use chrono::Utc;
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, trace};
|
||||
@@ -74,9 +74,11 @@ pub async fn profile_create(
|
||||
let mut profile = Profile {
|
||||
path: path.clone(),
|
||||
install_stage: ProfileInstallStage::NotInstalled,
|
||||
launcher_feature_version: LauncherFeatureVersion::MOST_RECENT,
|
||||
name,
|
||||
icon_path: None,
|
||||
game_version,
|
||||
protocol_version: None,
|
||||
loader: modloader,
|
||||
loader_version: loader.map(|x| x.id),
|
||||
groups: Vec::new(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_loading, loading_try_for_each_concurrent,
|
||||
};
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
};
|
||||
@@ -12,10 +12,10 @@ use crate::state::{
|
||||
ProfileFile, ProfileInstallStage, ProjectType, SideType,
|
||||
};
|
||||
|
||||
use crate::event::{emit::emit_profile, ProfilePayloadType};
|
||||
use crate::event::{ProfilePayloadType, emit::emit_profile};
|
||||
use crate::util::fetch;
|
||||
use crate::util::io::{self, IOError};
|
||||
pub use crate::{state::Profile, State};
|
||||
pub use crate::{State, state::Profile};
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use serde_json::json;
|
||||
@@ -36,6 +36,13 @@ use tokio::{fs::File, process::Command, sync::RwLock};
|
||||
pub mod create;
|
||||
pub mod update;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum QuickPlayType {
|
||||
None,
|
||||
Singleplayer(String),
|
||||
Server(String),
|
||||
}
|
||||
|
||||
/// Remove a profile
|
||||
#[tracing::instrument]
|
||||
pub async fn remove(path: &str) -> crate::Result<()> {
|
||||
@@ -463,8 +470,7 @@ pub async fn export_mrpack(
|
||||
state.io_semaphore.0.acquire().await?;
|
||||
let profile = get(profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to export a nonexistent or unloaded profile at path {}!",
|
||||
profile_path
|
||||
"Tried to export a nonexistent or unloaded profile at path {profile_path}!"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -610,8 +616,7 @@ fn pack_get_relative_path(
|
||||
.strip_prefix(profile_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile",
|
||||
path = path
|
||||
"Path {path:?} does not correspond to a profile"
|
||||
))
|
||||
})?
|
||||
.components()
|
||||
@@ -623,14 +628,17 @@ fn pack_get_relative_path(
|
||||
/// Run Minecraft using a profile and the default credentials, logged in credentials,
|
||||
/// failing with an error if no credentials are available
|
||||
#[tracing::instrument]
|
||||
pub async fn run(path: &str) -> crate::Result<ProcessMetadata> {
|
||||
pub async fn run(
|
||||
path: &str,
|
||||
quick_play_type: &QuickPlayType,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let default_account = Credentials::get_default_credential(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?;
|
||||
|
||||
run_credentials(path, &default_account).await
|
||||
run_credentials(path, &default_account, quick_play_type).await
|
||||
}
|
||||
|
||||
/// Run Minecraft using a profile, and credentials for authentication
|
||||
@@ -640,13 +648,13 @@ pub async fn run(path: &str) -> crate::Result<ProcessMetadata> {
|
||||
pub async fn run_credentials(
|
||||
path: &str,
|
||||
credentials: &Credentials,
|
||||
quick_play_type: &QuickPlayType,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
let state = State::get().await?;
|
||||
let settings = Settings::get(&state.pool).await?;
|
||||
let profile = get(path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to run a nonexistent or unloaded profile at path {}!",
|
||||
path
|
||||
"Tried to run a nonexistent or unloaded profile at path {path}!"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -719,6 +727,7 @@ pub async fn run_credentials(
|
||||
credentials,
|
||||
post_exit_hook,
|
||||
&profile,
|
||||
quick_play_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -741,8 +750,7 @@ pub async fn try_update_playtime(path: &str) -> crate::Result<()> {
|
||||
|
||||
let profile = get(path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to update playtime for a nonexistent or unloaded profile at path {}!",
|
||||
path
|
||||
"Tried to update playtime for a nonexistent or unloaded profile at path {path}!"
|
||||
))
|
||||
})?;
|
||||
let updated_recent_playtime = profile.recent_time_played;
|
||||
@@ -823,7 +831,7 @@ pub async fn create_mrpack_json(
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Loader version mismatch".to_string(),
|
||||
)
|
||||
.into())
|
||||
.into());
|
||||
}
|
||||
};
|
||||
dependencies
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::state::CacheBehaviour;
|
||||
use crate::{
|
||||
LoadingBarType,
|
||||
event::{
|
||||
emit::{emit_profile, init_loading},
|
||||
ProfilePayloadType,
|
||||
emit::{emit_profile, init_loading},
|
||||
},
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
profile::get,
|
||||
state::ProfileInstallStage,
|
||||
LoadingBarType,
|
||||
};
|
||||
use futures::try_join;
|
||||
|
||||
@@ -24,9 +24,9 @@ pub async fn update_managed_modrinth_version(
|
||||
})?;
|
||||
|
||||
let unmanaged_err = || {
|
||||
crate::ErrorKind::InputError(
|
||||
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
|
||||
)
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Profile at {profile_path} is not a managed modrinth pack, or has been disconnected."
|
||||
))
|
||||
};
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
@@ -58,9 +58,9 @@ pub async fn repair_managed_modrinth(profile_path: &str) -> crate::Result<()> {
|
||||
})?;
|
||||
|
||||
let unmanaged_err = || {
|
||||
crate::ErrorKind::InputError(
|
||||
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
|
||||
)
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Profile at {profile_path} is not a managed modrinth pack, or has been disconnected."
|
||||
))
|
||||
};
|
||||
|
||||
// For repairing specifically, first we remove all installed projects (to ensure we do remove ones that aren't in the pack)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
pub use crate::{
|
||||
state::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
State,
|
||||
state::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
};
|
||||
|
||||
/// Gets entire settings
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Theseus tag management interface
|
||||
use crate::state::CachedEntry;
|
||||
pub use crate::{
|
||||
state::{Category, DonationPlatform, GameVersion, Loader},
|
||||
State,
|
||||
state::{Category, DonationPlatform, GameVersion, Loader},
|
||||
};
|
||||
|
||||
/// Get category tags
|
||||
|
||||
967
packages/app-lib/src/api/worlds.rs
Normal file
@@ -0,0 +1,967 @@
|
||||
use crate::data::ModLoader;
|
||||
use crate::launcher::get_loader_version_from_profile;
|
||||
use crate::profile::get_full_path;
|
||||
use crate::state::attached_world_data::AttachedWorldData;
|
||||
use crate::state::{
|
||||
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
||||
};
|
||||
pub use crate::util::server_ping::{
|
||||
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
||||
};
|
||||
use crate::util::{io, server_ping};
|
||||
use crate::{Error, ErrorKind, Result, State, launcher};
|
||||
use async_walkdir::WalkDir;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
use either::Either;
|
||||
use enumset::{EnumSet, EnumSetType};
|
||||
use fs4::tokio::AsyncFileExt;
|
||||
use futures::StreamExt;
|
||||
use quartz_nbt::{NbtCompound, NbtTag};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Reverse;
|
||||
use std::io::Cursor;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct WorldWithProfile {
|
||||
pub profile: String,
|
||||
#[serde(flatten)]
|
||||
pub world: World,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct World {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_played: Option<DateTime<Utc>>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "either::serde_untagged_optional"
|
||||
)]
|
||||
pub icon: Option<Either<PathBuf, Url>>,
|
||||
pub display_status: DisplayStatus,
|
||||
#[serde(flatten)]
|
||||
pub details: WorldDetails,
|
||||
}
|
||||
|
||||
impl World {
|
||||
pub fn world_type(&self) -> WorldType {
|
||||
match self.details {
|
||||
WorldDetails::Singleplayer { .. } => WorldType::Singleplayer,
|
||||
WorldDetails::Server { .. } => WorldType::Server,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn world_id(&self) -> &str {
|
||||
match &self.details {
|
||||
WorldDetails::Singleplayer { path, .. } => path,
|
||||
WorldDetails::Server { address, .. } => address,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorldType {
|
||||
#[default]
|
||||
Singleplayer,
|
||||
Server,
|
||||
}
|
||||
|
||||
impl WorldType {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Singleplayer => "singleplayer",
|
||||
Self::Server => "server",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> Self {
|
||||
match string {
|
||||
"singleplayer" => Self::Singleplayer,
|
||||
"server" => Self::Server,
|
||||
_ => Self::Singleplayer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, EnumSetType, Debug, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[enumset(serialize_repr = "list")]
|
||||
pub enum DisplayStatus {
|
||||
#[default]
|
||||
Normal,
|
||||
Hidden,
|
||||
Favorite,
|
||||
}
|
||||
|
||||
impl DisplayStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => "normal",
|
||||
Self::Hidden => "hidden",
|
||||
Self::Favorite => "favorite",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> Self {
|
||||
match string {
|
||||
"normal" => Self::Normal,
|
||||
"hidden" => Self::Hidden,
|
||||
"favorite" => Self::Favorite,
|
||||
_ => Self::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum WorldDetails {
|
||||
Singleplayer {
|
||||
path: String,
|
||||
game_mode: SingleplayerGameMode,
|
||||
hardcore: bool,
|
||||
locked: bool,
|
||||
},
|
||||
Server {
|
||||
index: usize,
|
||||
address: String,
|
||||
pack_status: ServerPackStatus,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Copy, Clone, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SingleplayerGameMode {
|
||||
#[default]
|
||||
Survival,
|
||||
Creative,
|
||||
Adventure,
|
||||
Spectator,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Copy, Clone, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ServerPackStatus {
|
||||
Enabled,
|
||||
Disabled,
|
||||
#[default]
|
||||
Prompt,
|
||||
}
|
||||
|
||||
impl From<Option<bool>> for ServerPackStatus {
|
||||
fn from(value: Option<bool>) -> Self {
|
||||
match value {
|
||||
Some(true) => ServerPackStatus::Enabled,
|
||||
Some(false) => ServerPackStatus::Disabled,
|
||||
None => ServerPackStatus::Prompt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerPackStatus> for Option<bool> {
|
||||
fn from(val: ServerPackStatus) -> Self {
|
||||
match val {
|
||||
ServerPackStatus::Enabled => Some(true),
|
||||
ServerPackStatus::Disabled => Some(false),
|
||||
ServerPackStatus::Prompt => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_recent_worlds(
|
||||
limit: usize,
|
||||
display_statuses: EnumSet<DisplayStatus>,
|
||||
) -> Result<Vec<WorldWithProfile>> {
|
||||
let state = State::get().await?;
|
||||
let profiles_dir = state.directories.profiles_dir();
|
||||
|
||||
let mut profiles = Profile::get_all(&state.pool).await?;
|
||||
profiles.sort_by_key(|x| Reverse(x.last_played));
|
||||
|
||||
let mut result = Vec::with_capacity(limit);
|
||||
|
||||
let mut least_recent_time = None;
|
||||
for profile in profiles {
|
||||
if result.len() >= limit && profile.last_played < least_recent_time {
|
||||
break;
|
||||
}
|
||||
let profile_path = &profile.path;
|
||||
let profile_dir = profiles_dir.join(profile_path);
|
||||
let profile_worlds =
|
||||
get_all_worlds_in_profile(profile_path, &profile_dir).await;
|
||||
if let Err(e) = profile_worlds {
|
||||
tracing::error!(
|
||||
"Failed to get worlds for profile {}: {}",
|
||||
profile_path,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for world in profile_worlds? {
|
||||
let is_older = least_recent_time.is_none()
|
||||
|| world.last_played < least_recent_time;
|
||||
if result.len() >= limit && is_older {
|
||||
continue;
|
||||
}
|
||||
if !display_statuses.contains(world.display_status) {
|
||||
continue;
|
||||
}
|
||||
if is_older {
|
||||
least_recent_time = world.last_played;
|
||||
}
|
||||
result.push(WorldWithProfile {
|
||||
profile: profile_path.clone(),
|
||||
world,
|
||||
});
|
||||
}
|
||||
if result.len() > limit {
|
||||
result.sort_by_key(|x| Reverse(x.world.last_played));
|
||||
result.truncate(limit);
|
||||
}
|
||||
}
|
||||
|
||||
if result.len() <= limit {
|
||||
result.sort_by_key(|x| Reverse(x.world.last_played));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_profile_worlds(profile_path: &str) -> Result<Vec<World>> {
|
||||
get_all_worlds_in_profile(profile_path, &get_full_path(profile_path).await?)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_worlds_in_profile(
|
||||
profile_path: &str,
|
||||
profile_dir: &Path,
|
||||
) -> Result<Vec<World>> {
|
||||
let mut worlds = vec![];
|
||||
get_singleplayer_worlds_in_profile(profile_dir, &mut worlds).await?;
|
||||
get_server_worlds_in_profile(profile_path, profile_dir, &mut worlds)
|
||||
.await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let attached_data =
|
||||
AttachedWorldData::get_all_for_instance(profile_path, &state.pool)
|
||||
.await?;
|
||||
if !attached_data.is_empty() {
|
||||
for world in worlds.iter_mut() {
|
||||
if let Some(data) = attached_data
|
||||
.get(&(world.world_type(), world.world_id().to_owned()))
|
||||
{
|
||||
attach_world_data_to_world(world, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(worlds)
|
||||
}
|
||||
|
||||
async fn get_singleplayer_worlds_in_profile(
|
||||
instance_dir: &Path,
|
||||
worlds: &mut Vec<World>,
|
||||
) -> Result<()> {
|
||||
let saves_dir = instance_dir.join("saves");
|
||||
if !saves_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut saves_dir = io::read_dir(saves_dir).await?;
|
||||
while let Some(world_dir) = saves_dir.next_entry().await? {
|
||||
let world_path = world_dir.path();
|
||||
let level_dat_path = world_path.join("level.dat");
|
||||
if !level_dat_path.exists() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(world) = read_singleplayer_world(world_path).await {
|
||||
worlds.push(world);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_singleplayer_world(
|
||||
instance: &str,
|
||||
world: &str,
|
||||
) -> Result<World> {
|
||||
let state = State::get().await?;
|
||||
let profile_path = state.directories.profiles_dir().join(instance);
|
||||
let mut world =
|
||||
read_singleplayer_world(get_world_dir(&profile_path, world)).await?;
|
||||
|
||||
if let Some(data) = AttachedWorldData::get_for_world(
|
||||
instance,
|
||||
world.world_type(),
|
||||
world.world_id(),
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
attach_world_data_to_world(&mut world, &data);
|
||||
}
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
async fn read_singleplayer_world(world_path: PathBuf) -> Result<World> {
|
||||
if let Some(_lock) = try_get_world_session_lock(&world_path).await? {
|
||||
read_singleplayer_world_maybe_locked(world_path, false).await
|
||||
} else {
|
||||
read_singleplayer_world_maybe_locked(world_path, true).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_singleplayer_world_maybe_locked(
|
||||
world_path: PathBuf,
|
||||
locked: bool,
|
||||
) -> Result<World> {
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct LevelDataRoot {
|
||||
data: LevelData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct LevelData {
|
||||
#[serde(default)]
|
||||
level_name: String,
|
||||
#[serde(default)]
|
||||
last_played: i64,
|
||||
#[serde(default)]
|
||||
game_type: i32,
|
||||
#[serde(default, rename = "hardcore")]
|
||||
hardcore: bool,
|
||||
}
|
||||
|
||||
let level_data = io::read(world_path.join("level.dat")).await?;
|
||||
let level_data: LevelDataRoot = quartz_nbt::serde::deserialize(
|
||||
&level_data,
|
||||
quartz_nbt::io::Flavor::GzCompressed,
|
||||
)?
|
||||
.0;
|
||||
let level_data = level_data.data;
|
||||
|
||||
let icon = Some(world_path.join("icon.png")).filter(|i| i.exists());
|
||||
|
||||
let game_mode = match level_data.game_type {
|
||||
0 => SingleplayerGameMode::Survival,
|
||||
1 => SingleplayerGameMode::Creative,
|
||||
2 => SingleplayerGameMode::Adventure,
|
||||
3 => SingleplayerGameMode::Spectator,
|
||||
_ => SingleplayerGameMode::Survival,
|
||||
};
|
||||
|
||||
Ok(World {
|
||||
name: level_data.level_name,
|
||||
last_played: Utc.timestamp_millis_opt(level_data.last_played).single(),
|
||||
icon: icon.map(Either::Left),
|
||||
display_status: DisplayStatus::Normal,
|
||||
details: WorldDetails::Singleplayer {
|
||||
path: world_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
game_mode,
|
||||
hardcore: level_data.hardcore,
|
||||
locked,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_server_worlds_in_profile(
|
||||
profile_path: &str,
|
||||
instance_dir: &Path,
|
||||
worlds: &mut Vec<World>,
|
||||
) -> Result<()> {
|
||||
let servers = servers_data::read(instance_dir).await?;
|
||||
if servers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let join_log = server_join_log::get_joins(profile_path, &state.pool)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
for (index, server) in servers.into_iter().enumerate() {
|
||||
if server.hidden {
|
||||
// TODO: Figure out whether we want to hide or show direct connect servers
|
||||
continue;
|
||||
}
|
||||
let icon = server.icon.and_then(|icon| {
|
||||
Url::parse(&format!("data:image/png;base64,{icon}")).ok()
|
||||
});
|
||||
let last_played = join_log
|
||||
.as_ref()
|
||||
.and_then(|log| {
|
||||
let address = parse_server_address(&server.ip).ok()?;
|
||||
log.get(&(address.0.to_owned(), address.1))
|
||||
})
|
||||
.copied();
|
||||
let world = World {
|
||||
name: server.name,
|
||||
last_played,
|
||||
icon: icon.map(Either::Right),
|
||||
display_status: DisplayStatus::Normal,
|
||||
details: WorldDetails::Server {
|
||||
index,
|
||||
address: server.ip,
|
||||
pack_status: server.accept_textures.into(),
|
||||
},
|
||||
};
|
||||
worlds.push(world);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attach_world_data_to_world(world: &mut World, data: &AttachedWorldData) {
|
||||
world.display_status = data.display_status;
|
||||
}
|
||||
|
||||
pub async fn set_world_display_status(
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
display_status: DisplayStatus,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
attached_world_data::set_display_status(
|
||||
instance,
|
||||
world_type,
|
||||
world_id,
|
||||
display_status,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rename_world(
|
||||
instance: &Path,
|
||||
world: &str,
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
let world = get_world_dir(instance, world);
|
||||
let level_dat_path = world.join("level.dat");
|
||||
if !level_dat_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let _lock = get_world_session_lock(&world).await?;
|
||||
|
||||
let level_data = io::read(&level_dat_path).await?;
|
||||
let (mut root_data, _) = quartz_nbt::io::read_nbt(
|
||||
&mut Cursor::new(level_data),
|
||||
quartz_nbt::io::Flavor::GzCompressed,
|
||||
)?;
|
||||
let data = root_data.get_mut::<_, &mut NbtCompound>("Data")?;
|
||||
|
||||
data.insert(
|
||||
"LevelName",
|
||||
NbtTag::String(new_name.trim_ascii().to_string()),
|
||||
);
|
||||
|
||||
let mut level_data = vec![];
|
||||
quartz_nbt::io::write_nbt(
|
||||
&mut level_data,
|
||||
None,
|
||||
&root_data,
|
||||
quartz_nbt::io::Flavor::GzCompressed,
|
||||
)?;
|
||||
io::write(level_dat_path, level_data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_world_icon(instance: &Path, world: &str) -> Result<()> {
|
||||
let world = get_world_dir(instance, world);
|
||||
let icon = world.join("icon.png");
|
||||
if let Some(_lock) = try_get_world_session_lock(&world).await? {
|
||||
let _ = io::remove_file(icon).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn backup_world(instance: &Path, world: &str) -> Result<u64> {
|
||||
let world_dir = get_world_dir(instance, world);
|
||||
let _lock = get_world_session_lock(&world_dir).await?;
|
||||
let backups_dir = instance.join("backups");
|
||||
|
||||
io::create_dir_all(&backups_dir).await?;
|
||||
|
||||
let name_base = {
|
||||
let now = Local::now();
|
||||
let formatted_time = now.format("%Y-%m-%d_%H-%M-%S");
|
||||
format!("{formatted_time}_{world}")
|
||||
};
|
||||
let output_path =
|
||||
backups_dir.join(find_available_name(&backups_dir, &name_base, ".zip"));
|
||||
|
||||
let writer = tokio::fs::File::create(&output_path).await?;
|
||||
let mut writer = async_zip::tokio::write::ZipFileWriter::with_tokio(writer);
|
||||
|
||||
let mut walker = WalkDir::new(&world_dir);
|
||||
while let Some(entry) = walker.next().await {
|
||||
let entry = entry.map_err(|e| io::IOError::IOPathError {
|
||||
path: e.path().unwrap().to_string_lossy().to_string(),
|
||||
source: e.into_io().unwrap(),
|
||||
})?;
|
||||
if !entry.file_type().await?.is_file() {
|
||||
continue;
|
||||
}
|
||||
if entry.file_name() == "session.lock" {
|
||||
continue;
|
||||
}
|
||||
let zip_filename = format!(
|
||||
"{world}/{}",
|
||||
entry
|
||||
.path()
|
||||
.strip_prefix(&world_dir)?
|
||||
.display()
|
||||
.to_string()
|
||||
.replace('\\', "/")
|
||||
);
|
||||
let mut stream = writer
|
||||
.write_entry_stream(
|
||||
ZipEntryBuilder::new(zip_filename.into(), Compression::Deflate)
|
||||
.build(),
|
||||
)
|
||||
.await?
|
||||
.compat_write();
|
||||
let mut source = tokio::fs::File::open(entry.path()).await?;
|
||||
tokio::io::copy(&mut source, &mut stream).await?;
|
||||
stream.into_inner().close().await?;
|
||||
}
|
||||
|
||||
writer.close().await?;
|
||||
Ok(io::metadata(output_path).await?.len())
|
||||
}
|
||||
|
||||
fn find_available_name(dir: &Path, file_name: &str, extension: &str) -> String {
|
||||
static RESERVED_WINDOWS_FILENAMES: LazyLock<Regex> = LazyLock::new(|| {
|
||||
RegexBuilder::new(r#"^.*\.|(?:COM|CLOCK\$|CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$"#)
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
static COPY_COUNTER_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
|
||||
RegexBuilder::new(r#"^(?<name>.*) \((?<count>\d*)\)$"#)
|
||||
.case_insensitive(true)
|
||||
.unicode(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let mut file_name = file_name.replace(
|
||||
[
|
||||
'/', '\n', '\r', '\t', '\0', '\x0c', '`', '?', '*', '\\', '<', '>',
|
||||
'|', '"', ':', '.', '/', '"',
|
||||
],
|
||||
"_",
|
||||
);
|
||||
if RESERVED_WINDOWS_FILENAMES.is_match(&file_name) {
|
||||
file_name.insert(0, '_');
|
||||
file_name.push('_');
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
if let Some(find) = COPY_COUNTER_PATTERN.captures(&file_name) {
|
||||
count = find
|
||||
.name("count")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse::<i32>()
|
||||
.unwrap_or(0);
|
||||
let end = find.name("name").unwrap().end();
|
||||
drop(find);
|
||||
file_name.truncate(end);
|
||||
}
|
||||
|
||||
if file_name.len() > 255 - extension.len() {
|
||||
file_name.truncate(255 - extension.len());
|
||||
}
|
||||
|
||||
let mut current_attempt = file_name.clone();
|
||||
loop {
|
||||
if count != 0 {
|
||||
let with_count = format!(" ({count})");
|
||||
if file_name.len() > 255 - with_count.len() {
|
||||
current_attempt.truncate(255 - with_count.len());
|
||||
}
|
||||
current_attempt.push_str(&with_count);
|
||||
}
|
||||
|
||||
current_attempt.push_str(extension);
|
||||
|
||||
let result = dir.join(¤t_attempt);
|
||||
if !result.exists() {
|
||||
return current_attempt;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
current_attempt.replace_range(..current_attempt.len(), &file_name);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_world(instance: &Path, world: &str) -> Result<()> {
|
||||
let world = get_world_dir(instance, world);
|
||||
let lock = get_world_session_lock(&world).await?;
|
||||
let lock_path = world.join("session.lock");
|
||||
|
||||
let mut dir = io::read_dir(&world).await?;
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if entry.file_type().await?.is_dir() {
|
||||
io::remove_dir_all(path).await?;
|
||||
continue;
|
||||
}
|
||||
if path != lock_path {
|
||||
io::remove_file(path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
drop(lock);
|
||||
io::remove_file(lock_path).await?;
|
||||
io::remove_dir(world).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_world_dir(instance: &Path, world: &str) -> PathBuf {
|
||||
instance.join("saves").join(world)
|
||||
}
|
||||
|
||||
async fn get_world_session_lock(world: &Path) -> Result<tokio::fs::File> {
|
||||
let lock_path = world.join("session.lock");
|
||||
let mut file = tokio::fs::File::options()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.await?;
|
||||
file.write_all("☃".as_bytes()).await?;
|
||||
file.sync_all().await?;
|
||||
let locked = file.try_lock_exclusive()?;
|
||||
locked.then_some(file).ok_or_else(|| {
|
||||
io::IOError::IOPathError {
|
||||
source: std::io::Error::new(
|
||||
std::io::ErrorKind::ResourceBusy,
|
||||
"already locked by Minecraft",
|
||||
),
|
||||
path: lock_path.to_string_lossy().into_owned(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
async fn try_get_world_session_lock(
|
||||
world: &Path,
|
||||
) -> Result<Option<tokio::fs::File>> {
|
||||
let file = tokio::fs::File::options()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.open(world.join("session.lock"))
|
||||
.await?;
|
||||
file.sync_all().await?;
|
||||
let locked = file.try_lock_exclusive()?;
|
||||
Ok(locked.then_some(file))
|
||||
}
|
||||
|
||||
pub async fn add_server_to_profile(
|
||||
profile_path: &Path,
|
||||
name: String,
|
||||
address: String,
|
||||
pack_status: ServerPackStatus,
|
||||
) -> Result<usize> {
|
||||
let mut servers = servers_data::read(profile_path).await?;
|
||||
let insert_index = servers
|
||||
.iter()
|
||||
.position(|x| x.hidden)
|
||||
.unwrap_or(servers.len());
|
||||
servers.insert(
|
||||
insert_index,
|
||||
servers_data::ServerData {
|
||||
name,
|
||||
ip: address,
|
||||
accept_textures: pack_status.into(),
|
||||
hidden: false,
|
||||
icon: None,
|
||||
},
|
||||
);
|
||||
servers_data::write(profile_path, &servers).await?;
|
||||
Ok(insert_index)
|
||||
}
|
||||
|
||||
pub async fn edit_server_in_profile(
|
||||
profile_path: &Path,
|
||||
index: usize,
|
||||
name: String,
|
||||
address: String,
|
||||
pack_status: ServerPackStatus,
|
||||
) -> Result<()> {
|
||||
let mut servers = servers_data::read(profile_path).await?;
|
||||
let server =
|
||||
servers
|
||||
.get_mut(index)
|
||||
.filter(|x| !x.hidden)
|
||||
.ok_or_else(|| {
|
||||
ErrorKind::InputError(format!(
|
||||
"No editable server at index {index}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
server.name = name;
|
||||
server.ip = address;
|
||||
server.accept_textures = pack_status.into();
|
||||
servers_data::write(profile_path, &servers).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_server_from_profile(
|
||||
profile_path: &Path,
|
||||
index: usize,
|
||||
) -> Result<()> {
|
||||
let mut servers = servers_data::read(profile_path).await?;
|
||||
if servers.get(index).filter(|x| !x.hidden).is_none() {
|
||||
return Err(ErrorKind::InputError(format!(
|
||||
"No removable server at index {index}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
servers.remove(index);
|
||||
servers_data::write(profile_path, &servers).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod servers_data {
|
||||
use crate::Result;
|
||||
use crate::util::io;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerData {
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ip: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accept_textures: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn read(instance_dir: &Path) -> Result<Vec<ServerData>> {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ServersData {
|
||||
#[serde(default)]
|
||||
servers: Vec<ServerData>,
|
||||
}
|
||||
|
||||
let servers_dat_path = instance_dir.join("servers.dat");
|
||||
if !servers_dat_path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let servers_data = io::read(servers_dat_path).await?;
|
||||
let servers_data: ServersData = quartz_nbt::serde::deserialize(
|
||||
&servers_data,
|
||||
quartz_nbt::io::Flavor::Uncompressed,
|
||||
)?
|
||||
.0;
|
||||
Ok(servers_data.servers)
|
||||
}
|
||||
|
||||
pub async fn write(
|
||||
instance_dir: &Path,
|
||||
servers: &[ServerData],
|
||||
) -> Result<()> {
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ServersData<'a> {
|
||||
servers: &'a [ServerData],
|
||||
}
|
||||
|
||||
let servers_dat_path = instance_dir.join("servers.dat");
|
||||
let data = quartz_nbt::serde::serialize(
|
||||
&ServersData { servers },
|
||||
None,
|
||||
quartz_nbt::io::Flavor::Uncompressed,
|
||||
)?;
|
||||
io::write(servers_dat_path, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_profile_protocol_version(
|
||||
profile: &str,
|
||||
) -> Result<Option<i32>> {
|
||||
let mut profile = super::profile::get(profile).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(format!(
|
||||
"Could not find profile {profile}"
|
||||
))
|
||||
})?;
|
||||
if profile.install_stage != ProfileInstallStage::Installed {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(protocol_version) = profile.protocol_version {
|
||||
return Ok(Some(protocol_version));
|
||||
}
|
||||
|
||||
let minecraft = crate::api::metadata::get_minecraft_versions().await?;
|
||||
let version_index = minecraft
|
||||
.versions
|
||||
.iter()
|
||||
.position(|it| it.id == profile.game_version)
|
||||
.ok_or(crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid game version: {}",
|
||||
profile.game_version
|
||||
)))?;
|
||||
let version = &minecraft.versions[version_index];
|
||||
|
||||
let loader_version = get_loader_version_from_profile(
|
||||
&profile.game_version,
|
||||
profile.loader,
|
||||
profile.loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
if profile.loader != ModLoader::Vanilla && loader_version.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let version_jar =
|
||||
loader_version.as_ref().map_or(version.id.clone(), |it| {
|
||||
format!("{}-{}", version.id.clone(), it.id.clone())
|
||||
});
|
||||
|
||||
let state = State::get().await?;
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
if !client_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let version = launcher::read_protocol_version_from_jar(client_path).await?;
|
||||
if version.is_some() {
|
||||
profile.protocol_version = version;
|
||||
profile.upsert(&state.pool).await?;
|
||||
}
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
pub async fn get_server_status(
|
||||
address: &str,
|
||||
protocol_version: Option<i32>,
|
||||
) -> Result<ServerStatus> {
|
||||
let (original_host, original_port) = parse_server_address(address)?;
|
||||
let (host, port) =
|
||||
resolve_server_address(original_host, original_port).await?;
|
||||
server_ping::get_server_status(
|
||||
&(&host as &str, port),
|
||||
(original_host, original_port),
|
||||
protocol_version,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn parse_server_address(address: &str) -> Result<(&str, u16)> {
|
||||
parse_server_address_inner(address)
|
||||
.map_err(|e| Error::from(ErrorKind::InputError(e)))
|
||||
}
|
||||
|
||||
// Reimplementation of Guava's HostAndPort#fromString with a default port of 25565
|
||||
fn parse_server_address_inner(
|
||||
address: &str,
|
||||
) -> std::result::Result<(&str, u16), String> {
|
||||
let (host, port_str) = if address.starts_with("[") {
|
||||
let colon_index = address.find(':');
|
||||
let close_bracket_index = address.rfind(']');
|
||||
if colon_index.is_none() || close_bracket_index.is_none() {
|
||||
return Err(format!("Invalid bracketed host/port: {address}"));
|
||||
}
|
||||
let close_bracket_index = close_bracket_index.unwrap();
|
||||
|
||||
let host = &address[1..close_bracket_index];
|
||||
if close_bracket_index + 1 == address.len() {
|
||||
(host, "")
|
||||
} else {
|
||||
if address.as_bytes().get(close_bracket_index).copied()
|
||||
!= Some(b':')
|
||||
{
|
||||
return Err(format!(
|
||||
"Only a colon may follow a close bracket: {address}"
|
||||
));
|
||||
}
|
||||
let port_str = &address[close_bracket_index + 2..];
|
||||
for c in port_str.chars() {
|
||||
if !c.is_ascii_digit() {
|
||||
return Err(format!("Port must be numeric: {address}"));
|
||||
}
|
||||
}
|
||||
(host, port_str)
|
||||
}
|
||||
} else {
|
||||
let colon_pos = address.find(':');
|
||||
if let Some(colon_pos) = colon_pos {
|
||||
(&address[..colon_pos], &address[colon_pos + 1..])
|
||||
} else {
|
||||
(address, "")
|
||||
}
|
||||
};
|
||||
|
||||
let mut port = None;
|
||||
if !port_str.is_empty() {
|
||||
if port_str.starts_with('+') {
|
||||
return Err(format!("Unparseable port number: {port_str}"));
|
||||
}
|
||||
port = port_str.parse::<u16>().ok();
|
||||
if port.is_none() {
|
||||
return Err(format!("Unparseable port number: {port_str}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((host, port.unwrap_or(25565)))
|
||||
}
|
||||
|
||||
async fn resolve_server_address(
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<(String, u16)> {
|
||||
if host.parse::<Ipv4Addr>().is_ok() || host.parse::<Ipv6Addr>().is_ok() {
|
||||
return Ok((host.to_owned(), port));
|
||||
}
|
||||
let resolver = hickory_resolver::TokioResolver::builder_tokio()?.build();
|
||||
Ok(
|
||||
match resolver.srv_lookup(format!("_minecraft._tcp.{host}")).await {
|
||||
Err(e)
|
||||
if e.proto()
|
||||
.filter(|x| x.kind().is_no_records_found())
|
||||
.is_some() =>
|
||||
{
|
||||
None
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
Ok(lookup) => lookup
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|r| (r.target().to_string(), r.port())),
|
||||
}
|
||||
.unwrap_or_else(|| (host.to_owned(), port)),
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,12 @@ pub enum ErrorKind {
|
||||
#[error("Serialization error (JSON): {0}")]
|
||||
JSONError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Serialization error (NBT): {0}")]
|
||||
NBTError(#[from] quartz_nbt::io::NbtIoError),
|
||||
|
||||
#[error("NBT data structure error: {0}")]
|
||||
NBTReprError(#[from] quartz_nbt::NbtReprError),
|
||||
|
||||
#[error("Serialization error (websocket): {0}")]
|
||||
WebsocketSerializationError(
|
||||
#[from] ariadne::networking::serialization::SerializationError,
|
||||
@@ -116,6 +122,9 @@ pub enum ErrorKind {
|
||||
|
||||
#[error("Move directory error: {0}")]
|
||||
DirectoryMoveError(String),
|
||||
|
||||
#[error("Error resolving DNS: {0}")]
|
||||
DNSError(#[from] hickory_resolver::ResolveError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn emit_loading(
|
||||
let display_frac = loading_bar.current / loading_bar.total;
|
||||
let opt_display_frac = if display_frac >= 1.0 {
|
||||
None // by convention, when its done, we submit None
|
||||
// any further updates will be ignored (also sending None)
|
||||
// any further updates will be ignored (also sending None)
|
||||
} else {
|
||||
Some(display_frac)
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Theseus state management system
|
||||
use ariadne::users::{UserId, UserStatus};
|
||||
use ariadne::ids::UserId;
|
||||
use ariadne::users::UserStatus;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
@@ -234,13 +236,23 @@ pub enum ProcessPayloadType {
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ProfilePayload {
|
||||
pub profile_path_id: String,
|
||||
#[serde(flatten)]
|
||||
pub event: ProfilePayloadType,
|
||||
}
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "event", rename_all = "snake_case")]
|
||||
pub enum ProfilePayloadType {
|
||||
Created,
|
||||
Synced,
|
||||
ServersUpdated,
|
||||
WorldUpdated {
|
||||
world: String,
|
||||
},
|
||||
ServerJoined {
|
||||
host: String,
|
||||
port: u16,
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
Edited,
|
||||
Removed,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Minecraft CLI argument logic
|
||||
use crate::launcher::parse_rules;
|
||||
use crate::profile::QuickPlayType;
|
||||
use crate::state::Credentials;
|
||||
use crate::{
|
||||
state::{MemorySettings, WindowSize},
|
||||
@@ -31,7 +32,12 @@ pub fn get_class_paths(
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !parse_rules(rules, java_arch, minecraft_updated) {
|
||||
if !parse_rules(
|
||||
rules,
|
||||
java_arch,
|
||||
&QuickPlayType::None,
|
||||
minecraft_updated,
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -111,6 +117,7 @@ pub fn get_jvm_arguments(
|
||||
memory: MemorySettings,
|
||||
custom_args: Vec<String>,
|
||||
java_arch: &str,
|
||||
quick_play_type: &QuickPlayType,
|
||||
log_config: Option<&LoggingConfiguration>,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
@@ -130,6 +137,7 @@ pub fn get_jvm_arguments(
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
quick_play_type,
|
||||
)?;
|
||||
} else {
|
||||
parsed_arguments.push(format!(
|
||||
@@ -214,6 +222,7 @@ pub fn get_minecraft_arguments(
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
java_arch: &str,
|
||||
quick_play_type: &QuickPlayType,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
if let Some(arguments) = arguments {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
@@ -233,9 +242,11 @@ pub fn get_minecraft_arguments(
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
quick_play_type,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
quick_play_type,
|
||||
)?;
|
||||
|
||||
Ok(parsed_arguments)
|
||||
@@ -253,6 +264,7 @@ pub fn get_minecraft_arguments(
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
quick_play_type,
|
||||
)?);
|
||||
}
|
||||
Ok(parsed_arguments)
|
||||
@@ -273,6 +285,7 @@ fn parse_minecraft_argument(
|
||||
assets_directory: &Path,
|
||||
version_type: &VersionType,
|
||||
resolution: WindowSize,
|
||||
quick_play_type: &QuickPlayType,
|
||||
) -> crate::Result<String> {
|
||||
Ok(argument
|
||||
.replace("${accessToken}", access_token)
|
||||
@@ -326,7 +339,21 @@ fn parse_minecraft_argument(
|
||||
)
|
||||
.replace("${version_type}", version_type.as_str())
|
||||
.replace("${resolution_width}", &resolution.0.to_string())
|
||||
.replace("${resolution_height}", &resolution.1.to_string()))
|
||||
.replace("${resolution_height}", &resolution.1.to_string())
|
||||
.replace(
|
||||
"${quickPlaySingleplayer}",
|
||||
match quick_play_type {
|
||||
QuickPlayType::Singleplayer(world) => world,
|
||||
_ => "",
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
"${quickPlayMultiplayer}",
|
||||
match quick_play_type {
|
||||
QuickPlayType::Server(address) => address,
|
||||
_ => "",
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_arguments<F>(
|
||||
@@ -334,6 +361,7 @@ fn parse_arguments<F>(
|
||||
parsed_arguments: &mut Vec<String>,
|
||||
parse_function: F,
|
||||
java_arch: &str,
|
||||
quick_play_type: &QuickPlayType,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(&str) -> crate::Result<String>,
|
||||
@@ -348,7 +376,7 @@ where
|
||||
}
|
||||
}
|
||||
Argument::Ruled { rules, value } => {
|
||||
if parse_rules(rules, java_arch, true) {
|
||||
if parse_rules(rules, java_arch, quick_play_type, true) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed_arguments.push(parse_function(
|
||||
@@ -410,16 +438,14 @@ pub async fn get_processor_main_class(
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor at {}",
|
||||
path
|
||||
"Cannot read processor at {path}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor manifest at {}",
|
||||
path
|
||||
"Cannot read processor manifest at {path}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Downloader for Minecraft data
|
||||
|
||||
use crate::launcher::parse_rules;
|
||||
use crate::profile::QuickPlayType;
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_loading, loading_try_for_each_concurrent},
|
||||
LoadingBarId,
|
||||
emit::{emit_loading, loading_try_for_each_concurrent},
|
||||
},
|
||||
state::State,
|
||||
util::{fetch::*, io, platform::OsExt},
|
||||
@@ -295,7 +296,7 @@ pub async fn download_libraries(
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
|
||||
if let Some(rules) = &library.rules {
|
||||
if !parse_rules(rules, java_arch, minecraft_updated) {
|
||||
if !parse_rules(rules, java_arch, &QuickPlayType::None, minecraft_updated) {
|
||||
tracing::trace!("Skipped library {}", &library.name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -4,18 +4,21 @@ use crate::event::emit::{emit_loading, init_or_edit_loading};
|
||||
use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::launcher::download::download_log_config;
|
||||
use crate::launcher::io::IOError;
|
||||
use crate::profile::QuickPlayType;
|
||||
use crate::state::{
|
||||
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
||||
};
|
||||
use crate::util::io;
|
||||
use crate::{process, state as st, State};
|
||||
use crate::{State, process, state as st};
|
||||
use chrono::Utc;
|
||||
use daedalus as d;
|
||||
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::seq::SliceRandom; // AstralRinth
|
||||
use serde::Deserialize;
|
||||
use st::Profile;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
|
||||
mod args;
|
||||
@@ -31,11 +34,14 @@ use crate::state::ACTIVE_STATE;
|
||||
pub fn parse_rules(
|
||||
rules: &[d::minecraft::Rule],
|
||||
java_version: &str,
|
||||
quick_play_type: &QuickPlayType,
|
||||
minecraft_updated: bool,
|
||||
) -> bool {
|
||||
let mut x = rules
|
||||
.iter()
|
||||
.map(|x| parse_rule(x, java_version, minecraft_updated))
|
||||
.map(|x| {
|
||||
parse_rule(x, java_version, quick_play_type, minecraft_updated)
|
||||
})
|
||||
.collect::<Vec<Option<bool>>>();
|
||||
|
||||
if rules
|
||||
@@ -56,26 +62,30 @@ pub fn parse_rules(
|
||||
pub fn parse_rule(
|
||||
rule: &d::minecraft::Rule,
|
||||
java_version: &str,
|
||||
quick_play_type: &QuickPlayType,
|
||||
minecraft_updated: bool,
|
||||
) -> Option<bool> {
|
||||
use d::minecraft::{Rule, RuleAction};
|
||||
|
||||
let res = match rule {
|
||||
Rule {
|
||||
os: Some(ref os), ..
|
||||
} => {
|
||||
Rule { os: Some(os), .. } => {
|
||||
crate::util::platform::os_rule(os, java_version, minecraft_updated)
|
||||
}
|
||||
Rule {
|
||||
features: Some(ref features),
|
||||
features: Some(features),
|
||||
..
|
||||
} => {
|
||||
!features.is_demo_user.unwrap_or(true)
|
||||
|| features.has_custom_resolution.unwrap_or(false)
|
||||
|| !features.has_quick_plays_support.unwrap_or(true)
|
||||
|| !features.is_quick_play_multiplayer.unwrap_or(true)
|
||||
|| (features.is_quick_play_singleplayer.unwrap_or(false)
|
||||
&& matches!(
|
||||
quick_play_type,
|
||||
QuickPlayType::Singleplayer(_)
|
||||
))
|
||||
|| (features.is_quick_play_multiplayer.unwrap_or(false)
|
||||
&& matches!(quick_play_type, QuickPlayType::Server(..)))
|
||||
|| !features.is_quick_play_realms.unwrap_or(true)
|
||||
|| !features.is_quick_play_singleplayer.unwrap_or(true)
|
||||
}
|
||||
_ => return Some(true),
|
||||
};
|
||||
@@ -288,8 +298,7 @@ pub async fn install_minecraft(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Java path invalid or non-functional: {:?}",
|
||||
java_version
|
||||
"Java path invalid or non-functional: {java_version:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -308,12 +317,11 @@ pub async fn install_minecraft(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.join(format!("{version_jar}.jar"));
|
||||
if let Some(processors) = &version_info.processors {
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
let libraries_dir = state.directories.libraries_dir();
|
||||
|
||||
if let Some(ref mut data) = version_info.data {
|
||||
@@ -398,16 +406,18 @@ pub async fn install_minecraft(
|
||||
&loading_bar,
|
||||
30.0 / total_length as f64,
|
||||
Some(&format!(
|
||||
"Running forge processor {}/{}",
|
||||
index, total_length
|
||||
"Running forge processor {index}/{total_length}"
|
||||
)),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let protocol_version = read_protocol_version_from_jar(client_path).await?;
|
||||
|
||||
crate::api::profile::edit(&profile.path, |prof| {
|
||||
prof.install_stage = ProfileInstallStage::Installed;
|
||||
prof.protocol_version = protocol_version;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
@@ -417,6 +427,34 @@ pub async fn install_minecraft(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_protocol_version_from_jar(
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Option<i32>> {
|
||||
let zip = async_zip::tokio::read::fs::ZipFileReader::new(path).await?;
|
||||
let Some(entry_index) = zip
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|x| matches!(x.filename().as_str(), Ok("version.json")))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct VersionData {
|
||||
protocol_version: Option<i32>,
|
||||
}
|
||||
|
||||
let mut data = vec![];
|
||||
zip.reader_with_entry(entry_index)
|
||||
.await?
|
||||
.read_to_end_checked(&mut data)
|
||||
.await?;
|
||||
let data: VersionData = serde_json::from_slice(&data)?;
|
||||
|
||||
Ok(data.protocol_version)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn launch_minecraft(
|
||||
@@ -429,6 +467,7 @@ pub async fn launch_minecraft(
|
||||
credentials: &Credentials,
|
||||
post_exit_hook: Option<String>,
|
||||
profile: &Profile,
|
||||
quick_play_type: &QuickPlayType,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
if profile.install_stage == ProfileInstallStage::PackInstalling
|
||||
|| profile.install_stage == ProfileInstallStage::MinecraftInstalling
|
||||
@@ -584,6 +623,7 @@ pub async fn launch_minecraft(
|
||||
*memory,
|
||||
Vec::from(java_args),
|
||||
&java_version.architecture,
|
||||
quick_play_type,
|
||||
version_info
|
||||
.logging
|
||||
.as_ref()
|
||||
@@ -606,6 +646,7 @@ pub async fn launch_minecraft(
|
||||
&version.type_,
|
||||
*resolution,
|
||||
&java_version.architecture,
|
||||
quick_play_type,
|
||||
)?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -637,10 +678,10 @@ pub async fn launch_minecraft(
|
||||
// check if the regex exists in the file
|
||||
if !re.is_match(&options_string) {
|
||||
// The key was not found in the file, so append it
|
||||
options_string.push_str(&format!("\n{}:{}", key, value));
|
||||
options_string.push_str(&format!("\n{key}:{value}"));
|
||||
} else {
|
||||
let replaced_string = re
|
||||
.replace_all(&options_string, &format!("{}:{}", key, value))
|
||||
.replace_all(&options_string, &format!("{key}:{value}"))
|
||||
.to_string();
|
||||
options_string = replaced_string;
|
||||
}
|
||||
@@ -658,12 +699,10 @@ pub async fn launch_minecraft(
|
||||
|
||||
let mut censor_strings = HashMap::new();
|
||||
let username = whoami::username();
|
||||
censor_strings
|
||||
.insert(format!("/{username}/"), "/{COMPUTER_USERNAME}/".to_string());
|
||||
censor_strings.insert(
|
||||
format!("/{}/", username),
|
||||
"/{COMPUTER_USERNAME}/".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
format!("\\{}\\", username),
|
||||
format!("\\{username}\\"),
|
||||
"\\{COMPUTER_USERNAME}\\".to_string(),
|
||||
);
|
||||
censor_strings.insert(
|
||||
@@ -712,6 +751,12 @@ pub async fn launch_minecraft(
|
||||
// This also spawns the process and prepares the subsequent processes
|
||||
state
|
||||
.process_manager
|
||||
.insert_new_process(&profile.path, command, post_exit_hook)
|
||||
.insert_new_process(
|
||||
&profile.path,
|
||||
command,
|
||||
post_exit_hook,
|
||||
state.directories.profile_logs_dir(&profile.path),
|
||||
version_info.logging.is_some(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ mod state;
|
||||
pub use api::*;
|
||||
pub use error::*;
|
||||
pub use event::{
|
||||
emit::emit_loading, emit::init_loading, EventState, LoadingBar,
|
||||
LoadingBarType,
|
||||
EventState, LoadingBar, LoadingBarType, emit::emit_loading,
|
||||
emit::init_loading,
|
||||
};
|
||||
pub use logger::start_logger;
|
||||
pub use state::State;
|
||||
|
||||
122
packages/app-lib/src/state/attached_world_data.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use crate::worlds::{DisplayStatus, WorldType};
|
||||
use paste::paste;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AttachedWorldData {
|
||||
pub display_status: DisplayStatus,
|
||||
}
|
||||
|
||||
impl AttachedWorldData {
|
||||
pub async fn get_for_world(
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<Option<Self>> {
|
||||
let world_type = world_type.as_str();
|
||||
|
||||
let attached_data = sqlx::query!(
|
||||
"
|
||||
SELECT display_status
|
||||
FROM attached_world_data
|
||||
WHERE profile_path = $1 and world_type = $2 and world_id = $3
|
||||
",
|
||||
instance,
|
||||
world_type,
|
||||
world_id
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(attached_data.map(|x| AttachedWorldData {
|
||||
display_status: DisplayStatus::from_string(&x.display_status),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_all_for_instance(
|
||||
instance: &str,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<HashMap<(WorldType, String), Self>> {
|
||||
let attached_data = sqlx::query!(
|
||||
"
|
||||
SELECT world_type, world_id, display_status
|
||||
FROM attached_world_data
|
||||
WHERE profile_path = $1
|
||||
",
|
||||
instance
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(attached_data
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let world_type = WorldType::from_string(&x.world_type);
|
||||
let display_status =
|
||||
DisplayStatus::from_string(&x.display_status);
|
||||
(
|
||||
(world_type, x.world_id),
|
||||
AttachedWorldData { display_status },
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn remove_for_world(
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let world_type = world_type.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM attached_world_data
|
||||
WHERE profile_path = $1 and world_type = $2 and world_id = $3
|
||||
",
|
||||
instance,
|
||||
world_type,
|
||||
world_id
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! attached_data_setter {
|
||||
($parameter:ident: $parameter_type:ty, $column:expr $(=> $adapter:expr)?) => {
|
||||
paste! {
|
||||
pub async fn [<set_ $parameter>](
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
$parameter: $parameter_type,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let world_type = world_type.as_str();
|
||||
$(let $parameter = $adapter;)?
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO attached_world_data (profile_path, world_type, world_id, " + $column + ")\n" +
|
||||
"VALUES ($1, $2, $3, $4)\n" +
|
||||
"ON CONFLICT (profile_path, world_type, world_id) DO UPDATE\n" +
|
||||
" SET " + $column + " = $4",
|
||||
instance,
|
||||
world_type,
|
||||
world_id,
|
||||
$parameter
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attached_data_setter!(display_status: DisplayStatus, "display_status" => display_status.as_str());
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::{META_URL, MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
||||
use crate::state::ProjectType;
|
||||
use crate::util::fetch::{fetch_json, sha1_async, FetchSemaphore};
|
||||
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashSet;
|
||||
use reqwest::Method;
|
||||
@@ -843,7 +843,7 @@ impl CachedEntry {
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<Vec<T>> {
|
||||
const MAX_REQUEST_SIZE: usize = 1000;
|
||||
const MAX_REQUEST_SIZE: usize = 800;
|
||||
|
||||
let urls = keys
|
||||
.iter()
|
||||
@@ -1072,7 +1072,7 @@ impl CachedEntry {
|
||||
CacheValueType::File => {
|
||||
let mut versions = fetch_json::<HashMap<String, Version>>(
|
||||
Method::POST,
|
||||
&format!("{}version_files", MODRINTH_API_URL),
|
||||
&format!("{MODRINTH_API_URL}version_files"),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"algorithm": "sha1",
|
||||
@@ -1285,7 +1285,7 @@ impl CachedEntry {
|
||||
|
||||
if let Some(values) =
|
||||
filtered_keys.iter_mut().find(|x| {
|
||||
x.0 .0 == loaders_key && x.0 .1 == game_version
|
||||
x.0.0 == loaders_key && x.0.1 == game_version
|
||||
})
|
||||
{
|
||||
values.1.push(hash.to_string());
|
||||
@@ -1307,7 +1307,7 @@ impl CachedEntry {
|
||||
});
|
||||
|
||||
let version_update_url =
|
||||
format!("{}version_files/update", MODRINTH_API_URL);
|
||||
format!("{MODRINTH_API_URL}version_files/update");
|
||||
let variations =
|
||||
futures::future::try_join_all(filtered_keys.iter().map(
|
||||
|((loaders_key, game_version), hashes)| {
|
||||
@@ -1481,7 +1481,7 @@ pub async fn cache_file_hash(
|
||||
|
||||
CachedEntry::upsert_many(
|
||||
&[CacheValue::FileHash(CachedFileHash {
|
||||
path: format!("{}/{}", profile_path, path),
|
||||
path: format!("{profile_path}/{path}"),
|
||||
size: size as u64,
|
||||
hash,
|
||||
project_type,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Theseus directory information
|
||||
use crate::LoadingBarType;
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::state::{JavaVersion, Profile, Settings};
|
||||
use crate::util::fetch::IoSemaphore;
|
||||
use crate::LoadingBarType;
|
||||
use dashmap::DashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
time::{SystemTime, UNIX_EPOCH}, // AstralRinth
|
||||
};
|
||||
|
||||
use discord_rich_presence::{
|
||||
activity::{Activity, Assets, Timestamps},
|
||||
activity::{Activity, Assets, Timestamps}, // AstralRinth
|
||||
DiscordIpc, DiscordIpcClient,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::seq::SliceRandom; // AstralRinth
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// use crate::state::Profile;
|
||||
use crate::util::utils;
|
||||
use crate::util::utils; // AstralRinth
|
||||
use crate::State;
|
||||
|
||||
pub struct DiscordGuard {
|
||||
@@ -43,8 +42,7 @@ impl DiscordGuard {
|
||||
let dipc =
|
||||
DiscordIpcClient::new("1190718475832918136").map_err(|e| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not create Discord client {}",
|
||||
e,
|
||||
"Could not create Discord client {e}",
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -133,8 +131,7 @@ impl DiscordGuard {
|
||||
let res = client.set_activity(activity.clone());
|
||||
let could_not_set_err = |e: Box<dyn serde::ser::StdError>| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not update Discord activity {}",
|
||||
e,
|
||||
"Could not update Discord activity {e}",
|
||||
))
|
||||
};
|
||||
|
||||
@@ -142,8 +139,7 @@ impl DiscordGuard {
|
||||
if let Err(_e) = res {
|
||||
client.reconnect().map_err(|e| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not reconnect to Discord IPC {}",
|
||||
e,
|
||||
"Could not reconnect to Discord IPC {e}",
|
||||
))
|
||||
})?;
|
||||
return Ok(client
|
||||
@@ -174,8 +170,7 @@ impl DiscordGuard {
|
||||
|
||||
let could_not_clear_err = |e: Box<dyn serde::ser::StdError>| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not clear Discord activity {}",
|
||||
e,
|
||||
"Could not clear Discord activity {e}",
|
||||
))
|
||||
};
|
||||
|
||||
@@ -183,8 +178,7 @@ impl DiscordGuard {
|
||||
if res.is_err() {
|
||||
client.reconnect().map_err(|e| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not reconnect to Discord IPC {}",
|
||||
e,
|
||||
"Could not reconnect to Discord IPC {e}",
|
||||
))
|
||||
})?;
|
||||
return Ok(client
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
use crate::config::{MODRINTH_API_URL_V3, MODRINTH_SOCKET_URL};
|
||||
use crate::data::ModrinthCredentials;
|
||||
use crate::event::emit::emit_friend;
|
||||
use crate::event::FriendPayload;
|
||||
use crate::event::emit::emit_friend;
|
||||
use crate::state::tunnel::InternalTunnelSocket;
|
||||
use crate::state::{ProcessManager, Profile, TunnelSocket};
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json, FetchSemaphore};
|
||||
use crate::util::fetch::{FetchSemaphore, fetch_advanced, fetch_json};
|
||||
use ariadne::ids::UserId;
|
||||
use ariadne::networking::message::{
|
||||
ClientToServerMessage, ServerToClientMessage,
|
||||
};
|
||||
use ariadne::users::{UserId, UserStatus};
|
||||
use async_tungstenite::tokio::{connect_async, ConnectStream};
|
||||
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use async_tungstenite::tungstenite::Message;
|
||||
use ariadne::users::UserStatus;
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use async_tungstenite::tokio::{ConnectStream, connect_async};
|
||||
use async_tungstenite::tungstenite::Message;
|
||||
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use either::Either;
|
||||
use futures::stream::SplitSink;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::Method;
|
||||
use reqwest::header::HeaderValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::tcp::OwnedReadHalf;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::tcp::OwnedReadHalf;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -204,7 +206,10 @@ impl FriendsSocket {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error handling message from websocket server: {:?}", e);
|
||||
tracing::error!(
|
||||
"Error handling message from websocket server: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +263,7 @@ impl FriendsSocket {
|
||||
last_ping = Utc::now();
|
||||
let mut write = state.friends_socket.write.write().await;
|
||||
if let Some(write) = write.as_mut() {
|
||||
let _ = write.send(Message::Ping(Vec::new())).await;
|
||||
let _ = write.send(Message::Ping(Bytes::new())).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
use crate::event::emit::{emit_profile, emit_warning};
|
||||
use crate::State;
|
||||
use crate::event::ProfilePayloadType;
|
||||
use crate::state::{DirectoryInfo, ProfileInstallStage, ProjectType};
|
||||
use futures::{channel::mpsc::channel, SinkExt, StreamExt};
|
||||
use crate::event::emit::{emit_profile, emit_warning};
|
||||
use crate::state::{
|
||||
DirectoryInfo, ProfileInstallStage, ProjectType, attached_world_data,
|
||||
};
|
||||
use crate::worlds::WorldType;
|
||||
use notify::{RecommendedWatcher, RecursiveMode};
|
||||
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
|
||||
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{RwLock, mpsc::channel};
|
||||
|
||||
pub type FileWatcher = RwLock<Debouncer<RecommendedWatcher>>;
|
||||
|
||||
pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
let (mut tx, mut rx) = channel(1);
|
||||
let (tx, mut rx) = channel(1);
|
||||
|
||||
let file_watcher = new_debouncer(
|
||||
Duration::from_secs_f32(1.0),
|
||||
move |res: DebounceEventResult| {
|
||||
futures::executor::block_on(async {
|
||||
tx.send(res).await.unwrap();
|
||||
})
|
||||
tx.blocking_send(res).ok();
|
||||
},
|
||||
)?;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(tracing::Level::INFO, "init_watcher");
|
||||
tracing::info!(parent: &span, "Initting watcher");
|
||||
while let Some(res) = rx.next().await {
|
||||
while let Some(res) = rx.recv().await {
|
||||
let _span = span.enter();
|
||||
|
||||
match res {
|
||||
@@ -37,9 +38,7 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
let mut found = false;
|
||||
for component in e.path.components() {
|
||||
if found {
|
||||
profile_path = Some(
|
||||
component.as_os_str().to_string_lossy(),
|
||||
);
|
||||
profile_path = Some(component.as_os_str());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -51,26 +50,87 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
}
|
||||
|
||||
if let Some(profile_path) = profile_path {
|
||||
if e.path
|
||||
let profile_path_str =
|
||||
profile_path.to_string_lossy().to_string();
|
||||
let first_file_name = e
|
||||
.path
|
||||
.components()
|
||||
.any(|x| x.as_os_str() == "crash-reports")
|
||||
.skip_while(|x| x.as_os_str() != profile_path)
|
||||
.nth(1)
|
||||
.map(|x| x.as_os_str());
|
||||
if first_file_name
|
||||
.filter(|x| *x == "crash-reports")
|
||||
.is_some()
|
||||
&& e.path
|
||||
.extension()
|
||||
.map(|x| x == "txt")
|
||||
.unwrap_or(false)
|
||||
.filter(|x| *x == "txt")
|
||||
.is_some()
|
||||
{
|
||||
crash_task(profile_path.to_string());
|
||||
crash_task(profile_path_str);
|
||||
} else if !visited_profiles.contains(&profile_path)
|
||||
{
|
||||
let path = profile_path.to_string();
|
||||
tokio::spawn(async move {
|
||||
let _ = emit_profile(
|
||||
&path,
|
||||
ProfilePayloadType::Synced,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
visited_profiles.push(profile_path);
|
||||
let event = if first_file_name
|
||||
.filter(|x| *x == "servers.dat")
|
||||
.is_some()
|
||||
{
|
||||
Some(ProfilePayloadType::ServersUpdated)
|
||||
} else if first_file_name
|
||||
.filter(|x| {
|
||||
*x == "saves"
|
||||
&& e.path
|
||||
.file_name()
|
||||
.filter(|x| *x == "level.dat")
|
||||
.is_some()
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
tracing::info!(
|
||||
"World updated: {}",
|
||||
e.path.display()
|
||||
);
|
||||
let world = e
|
||||
.path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if !e.path.is_file() {
|
||||
let profile_path_str = profile_path_str.clone();
|
||||
let world = world.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(state) = State::get().await {
|
||||
if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
||||
&profile_path_str,
|
||||
WorldType::Singleplayer,
|
||||
&world,
|
||||
&state.pool
|
||||
).await {
|
||||
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(ProfilePayloadType::WorldUpdated { world })
|
||||
} else if first_file_name
|
||||
.filter(|x| *x == "saves")
|
||||
.is_none()
|
||||
{
|
||||
Some(ProfilePayloadType::Synced)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(event) = event {
|
||||
tokio::spawn(async move {
|
||||
let _ = emit_profile(
|
||||
&profile_path_str,
|
||||
event,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
visited_profiles.push(profile_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -111,27 +171,47 @@ pub(crate) async fn watch_profile(
|
||||
let profile_path = dirs.profiles_dir().join(profile_path);
|
||||
|
||||
if profile_path.exists() && profile_path.is_dir() {
|
||||
for folder in ProjectType::iterator()
|
||||
.map(|x| x.get_folder())
|
||||
.chain(["crash-reports"])
|
||||
{
|
||||
let path = profile_path.join(folder);
|
||||
for sub_path in ProjectType::iterator().map(|x| x.get_folder()).chain([
|
||||
"crash-reports",
|
||||
"saves",
|
||||
"servers.dat",
|
||||
]) {
|
||||
let full_path = profile_path.join(sub_path);
|
||||
|
||||
if !path.exists() && !path.is_symlink() {
|
||||
if let Err(e) = crate::util::io::create_dir_all(&path).await {
|
||||
tracing::error!(
|
||||
"Failed to create directory for watcher {path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
if !full_path.exists() && !full_path.is_symlink() {
|
||||
if !sub_path.contains(".") {
|
||||
if let Err(e) =
|
||||
crate::util::io::create_dir_all(&full_path).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to create directory for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if sub_path == "servers.dat" {
|
||||
const EMPTY_NBT: &[u8] = &[
|
||||
10, // Compound tag
|
||||
0, 0, // Empty name
|
||||
0, // End of compound tag
|
||||
];
|
||||
if let Err(e) =
|
||||
crate::util::io::write(&full_path, EMPTY_NBT).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to create file for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut watcher = watcher.write().await;
|
||||
if let Err(e) =
|
||||
watcher.watcher().watch(&path, RecursiveMode::Recursive)
|
||||
if let Err(e) = watcher
|
||||
.watcher()
|
||||
.watch(&full_path, RecursiveMode::Recursive)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to watch directory for watcher {path:?}: {e}"
|
||||
"Failed to watch directory for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ use crate::state;
|
||||
use crate::state::{
|
||||
CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate,
|
||||
Credentials, DefaultPage, DependencyType, DeviceToken, DeviceTokenKey,
|
||||
DeviceTokenPair, FileType, Hooks, LinkedData, MemorySettings,
|
||||
ModrinthCredentials, Profile, ProfileInstallStage, TeamMember, Theme,
|
||||
VersionFile, WindowSize,
|
||||
DeviceTokenPair, FileType, Hooks, LauncherFeatureVersion, LinkedData,
|
||||
MemorySettings, ModrinthCredentials, Profile, ProfileInstallStage,
|
||||
TeamMember, Theme, VersionFile, WindowSize,
|
||||
};
|
||||
use crate::util::fetch::{read_json, IoSemaphore};
|
||||
use crate::util::fetch::{IoSemaphore, read_json};
|
||||
use chrono::{DateTime, Utc};
|
||||
use p256::ecdsa::SigningKey;
|
||||
use p256::pkcs8::DecodePrivateKey;
|
||||
@@ -250,9 +250,11 @@ where
|
||||
.metadata
|
||||
.game_version
|
||||
.clone(),
|
||||
loaders: vec![mod_loader
|
||||
.as_str()
|
||||
.to_string()],
|
||||
loaders: vec![
|
||||
mod_loader
|
||||
.as_str()
|
||||
.to_string(),
|
||||
],
|
||||
update_version_id:
|
||||
update_version.id.clone(),
|
||||
},
|
||||
@@ -317,9 +319,11 @@ where
|
||||
ProfileInstallStage::NotInstalled
|
||||
}
|
||||
},
|
||||
launcher_feature_version: LauncherFeatureVersion::None,
|
||||
name: profile.metadata.name,
|
||||
icon_path: profile.metadata.icon,
|
||||
game_version: profile.metadata.game_version,
|
||||
protocol_version: None,
|
||||
loader: profile.metadata.loader.into(),
|
||||
loader_version: profile
|
||||
.metadata
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use crate::util::fetch::REQWEST_CLIENT;
|
||||
use crate::ErrorKind;
|
||||
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
|
||||
use crate::util::fetch::REQWEST_CLIENT;
|
||||
use base64::Engine;
|
||||
use byteorder::BigEndian;
|
||||
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use p256::ecdsa::signature::Signer;
|
||||
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
|
||||
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
use reqwest::header::HeaderMap;
|
||||
use rand::rngs::OsRng;
|
||||
use reqwest::Response;
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -62,12 +61,6 @@ pub enum MinecraftAuthenticationError {
|
||||
#[source]
|
||||
source: reqwest::Error,
|
||||
},
|
||||
#[error("Error creating signed request buffer {step:?}: {source}")]
|
||||
ConstructingSignedRequest {
|
||||
step: MinecraftAuthStep,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("Error reading XBOX Session ID header")]
|
||||
NoSessionId,
|
||||
#[error("Error reading user hash")]
|
||||
@@ -1110,56 +1103,25 @@ async fn send_signed_request<T: DeserializeOwned>(
|
||||
let time: u128 =
|
||||
{ ((current_date.timestamp() as u128) + 11644473600) * 10000000 };
|
||||
|
||||
use byteorder::WriteBytesExt;
|
||||
let mut buffer = Vec::new();
|
||||
buffer.write_u32::<BigEndian>(1).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer
|
||||
.write_u64::<BigEndian>(time as u64)
|
||||
.map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest {
|
||||
source,
|
||||
step,
|
||||
}
|
||||
})?;
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.extend_from_slice(&1_u32.to_be_bytes()[..]);
|
||||
buffer.push(0_u8);
|
||||
buffer.extend_from_slice(&(time as u64).to_be_bytes()[..]);
|
||||
buffer.push(0_u8);
|
||||
buffer.extend_from_slice("POST".as_bytes());
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.push(0_u8);
|
||||
buffer.extend_from_slice(url_path.as_bytes());
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.push(0_u8);
|
||||
buffer.extend_from_slice(&auth);
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.push(0_u8);
|
||||
buffer.extend_from_slice(&body);
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.push(0_u8);
|
||||
|
||||
let ecdsa_sig: Signature = key.key.sign(&buffer);
|
||||
|
||||
let mut sig_buffer = Vec::new();
|
||||
sig_buffer.write_i32::<BigEndian>(1).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
sig_buffer
|
||||
.write_u64::<BigEndian>(time as u64)
|
||||
.map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest {
|
||||
source,
|
||||
step,
|
||||
}
|
||||
})?;
|
||||
sig_buffer.extend_from_slice(&1_i32.to_be_bytes()[..]);
|
||||
sig_buffer.extend_from_slice(&(time as u64).to_be_bytes()[..]);
|
||||
sig_buffer.extend_from_slice(&ecdsa_sig.r().to_bytes());
|
||||
sig_buffer.extend_from_slice(&ecdsa_sig.s().to_bytes());
|
||||
|
||||
@@ -1224,6 +1186,6 @@ fn get_date_header(headers: &HeaderMap) -> DateTime<Utc> {
|
||||
fn generate_oauth_challenge() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let bytes: Vec<u8> = (0..64).map(|_| rng.gen::<u8>()).collect();
|
||||
bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
|
||||
let bytes: Vec<u8> = (0..64).map(|_| rng.r#gen::<u8>()).collect();
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ pub use self::mr_auth::*;
|
||||
|
||||
mod legacy_converter;
|
||||
|
||||
pub mod attached_world_data;
|
||||
pub mod server_join_log;
|
||||
|
||||
// Global state
|
||||
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
@@ -108,7 +111,9 @@ impl State {
|
||||
/// Get the current launcher state, waiting for initialization
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
if !LAUNCHER_STATE.initialized() {
|
||||
tracing::error!("Attempted to get state before it is initialized - this should never happen!");
|
||||
tracing::error!(
|
||||
"Attempted to get state before it is initialized - this should never happen!"
|
||||
);
|
||||
while !LAUNCHER_STATE.initialized() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::{MODRINTH_API_URL, MODRINTH_URL};
|
||||
use crate::state::{CacheBehaviour, CachedEntry};
|
||||
use crate::util::fetch::{fetch_advanced, FetchSemaphore};
|
||||
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use crate::event::emit::emit_process;
|
||||
use crate::event::ProcessPayloadType;
|
||||
use crate::event::emit::{emit_process, emit_profile};
|
||||
use crate::event::{ProcessPayloadType, ProfilePayloadType};
|
||||
use crate::profile;
|
||||
use crate::util::io::IOError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use quick_xml::Reader;
|
||||
use quick_xml::events::Event;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use uuid::Uuid;
|
||||
|
||||
const LAUNCHER_LOG_PATH: &str = "launcher_log.txt";
|
||||
|
||||
pub struct ProcessManager {
|
||||
processes: DashMap<Uuid, Process>,
|
||||
}
|
||||
@@ -32,8 +40,16 @@ impl ProcessManager {
|
||||
profile_path: &str,
|
||||
mut mc_command: Command,
|
||||
post_exit_command: Option<String>,
|
||||
logs_folder: PathBuf,
|
||||
xml_logging: bool,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
let mc_proc = mc_command.spawn().map_err(IOError::from)?;
|
||||
mc_command.stdout(std::process::Stdio::piped());
|
||||
mc_command.stderr(std::process::Stdio::piped());
|
||||
|
||||
let mut mc_proc = mc_command.spawn().map_err(IOError::from)?;
|
||||
|
||||
let stdout = mc_proc.stdout.take();
|
||||
let stderr = mc_proc.stderr.take();
|
||||
|
||||
let process = Process {
|
||||
metadata: ProcessMetadata {
|
||||
@@ -46,6 +62,65 @@ impl ProcessManager {
|
||||
|
||||
let metadata = process.metadata.clone();
|
||||
|
||||
if !logs_folder.exists() {
|
||||
tokio::fs::create_dir_all(&logs_folder)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
}
|
||||
|
||||
let log_path = logs_folder.join(LAUNCHER_LOG_PATH);
|
||||
|
||||
{
|
||||
let mut log_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&log_path)
|
||||
.map_err(|e| IOError::with_path(e, &log_path))?;
|
||||
|
||||
// Initialize with timestamp header
|
||||
let now = chrono::Local::now();
|
||||
writeln!(
|
||||
log_file,
|
||||
"# Minecraft launcher log started at {}",
|
||||
now.format("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
.map_err(|e| IOError::with_path(e, &log_path))?;
|
||||
writeln!(log_file, "# Profile: {profile_path} \n")
|
||||
.map_err(|e| IOError::with_path(e, &log_path))?;
|
||||
writeln!(log_file).map_err(|e| IOError::with_path(e, &log_path))?;
|
||||
}
|
||||
|
||||
if let Some(stdout) = stdout {
|
||||
let log_path_clone = log_path.clone();
|
||||
|
||||
let profile_path = metadata.profile_path.clone();
|
||||
tokio::spawn(async move {
|
||||
Process::process_output(
|
||||
&profile_path,
|
||||
stdout,
|
||||
log_path_clone,
|
||||
xml_logging,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = stderr {
|
||||
let log_path_clone = log_path.clone();
|
||||
|
||||
let profile_path = metadata.profile_path.clone();
|
||||
tokio::spawn(async move {
|
||||
Process::process_output(
|
||||
&profile_path,
|
||||
stderr,
|
||||
log_path_clone,
|
||||
xml_logging,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
tokio::spawn(Process::sequential_process_manager(
|
||||
profile_path.to_string(),
|
||||
post_exit_command,
|
||||
@@ -120,7 +195,384 @@ struct Process {
|
||||
child: Child,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Log4jEvent {
|
||||
timestamp: Option<String>,
|
||||
logger: Option<String>,
|
||||
level: Option<String>,
|
||||
thread: Option<String>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
impl Process {
|
||||
async fn process_output<R>(
|
||||
profile_path: &str,
|
||||
reader: R,
|
||||
log_path: impl AsRef<Path>,
|
||||
xml_logging: bool,
|
||||
) where
|
||||
R: tokio::io::AsyncRead + Unpin,
|
||||
{
|
||||
let mut buf_reader = BufReader::new(reader);
|
||||
|
||||
if xml_logging {
|
||||
let mut reader = Reader::from_reader(buf_reader);
|
||||
reader.config_mut().enable_all_checks(false);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut current_event = Log4jEvent::default();
|
||||
let mut in_event = false;
|
||||
let mut in_message = false;
|
||||
let mut in_throwable = false;
|
||||
let mut current_content = String::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into_async(&mut buf).await {
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
e
|
||||
);
|
||||
break;
|
||||
}
|
||||
// exits the loop when reaching end of file
|
||||
Ok(Event::Eof) => break,
|
||||
|
||||
Ok(Event::Start(e)) => {
|
||||
match e.name().as_ref() {
|
||||
b"log4j:Event" => {
|
||||
// Reset for new event
|
||||
current_event = Log4jEvent::default();
|
||||
in_event = true;
|
||||
|
||||
// Extract attributes
|
||||
for attr in e.attributes().flatten() {
|
||||
let key = String::from_utf8_lossy(
|
||||
attr.key.into_inner(),
|
||||
)
|
||||
.to_string();
|
||||
let value =
|
||||
String::from_utf8_lossy(&attr.value)
|
||||
.to_string();
|
||||
|
||||
match key.as_str() {
|
||||
"logger" => {
|
||||
current_event.logger = Some(value)
|
||||
}
|
||||
"level" => {
|
||||
current_event.level = Some(value)
|
||||
}
|
||||
"thread" => {
|
||||
current_event.thread = Some(value)
|
||||
}
|
||||
"timestamp" => {
|
||||
current_event.timestamp =
|
||||
Some(value)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
b"log4j:Message" => {
|
||||
in_message = true;
|
||||
current_content = String::new();
|
||||
}
|
||||
b"log4j:Throwable" => {
|
||||
in_throwable = true;
|
||||
current_content = String::new();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Event::End(e)) => {
|
||||
match e.name().as_ref() {
|
||||
b"log4j:Message" => {
|
||||
in_message = false;
|
||||
current_event.message =
|
||||
Some(current_content.clone());
|
||||
}
|
||||
b"log4j:Throwable" => {
|
||||
in_throwable = false;
|
||||
// Process and write the log entry
|
||||
let thread = current_event
|
||||
.thread
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
let level = current_event
|
||||
.level
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
let logger = current_event
|
||||
.logger
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
|
||||
if let Some(message) = ¤t_event.message {
|
||||
let formatted_time =
|
||||
Process::format_timestamp(
|
||||
current_event.timestamp.as_deref(),
|
||||
);
|
||||
let formatted_log = format!(
|
||||
"{} [{}] [{}{}]: {}\n",
|
||||
formatted_time,
|
||||
thread,
|
||||
if !logger.is_empty() {
|
||||
format!("{logger}/")
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
level,
|
||||
message.trim()
|
||||
);
|
||||
|
||||
// Write the log message
|
||||
if let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&formatted_log,
|
||||
) {
|
||||
tracing::error!(
|
||||
"Failed to write to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Write the throwable if present
|
||||
if !current_content.is_empty() {
|
||||
if let Err(e) =
|
||||
Process::append_to_log_file(
|
||||
&log_path,
|
||||
¤t_content,
|
||||
)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to write throwable to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b"log4j:Event" => {
|
||||
in_event = false;
|
||||
// If no throwable was present, write the log entry at the end of the event
|
||||
if current_event.message.is_some()
|
||||
&& !in_throwable
|
||||
{
|
||||
let thread = current_event
|
||||
.thread
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
let level = current_event
|
||||
.level
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
let logger = current_event
|
||||
.logger
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
let message = current_event
|
||||
.message
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
|
||||
let formatted_time =
|
||||
Process::format_timestamp(
|
||||
current_event.timestamp.as_deref(),
|
||||
);
|
||||
let formatted_log = format!(
|
||||
"{} [{}] [{}{}]: {}\n",
|
||||
formatted_time,
|
||||
thread,
|
||||
if !logger.is_empty() {
|
||||
format!("{logger}/")
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
level,
|
||||
message
|
||||
);
|
||||
|
||||
// Write the log message
|
||||
if let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&formatted_log,
|
||||
) {
|
||||
tracing::error!(
|
||||
"Failed to write to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(timestamp) =
|
||||
current_event.timestamp.as_deref()
|
||||
{
|
||||
if let Err(e) = Self::maybe_handle_server_join_logging(
|
||||
profile_path,
|
||||
timestamp,
|
||||
message
|
||||
).await {
|
||||
tracing::error!("Failed to handle server join logging: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Event::Text(mut e)) => {
|
||||
if in_message || in_throwable {
|
||||
if let Ok(text) = e.unescape() {
|
||||
current_content.push_str(&text);
|
||||
}
|
||||
} else if !in_event
|
||||
&& !e.inplace_trim_end()
|
||||
&& !e.inplace_trim_start()
|
||||
{
|
||||
if let Ok(text) = e.unescape() {
|
||||
if let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&format!("{text}\n"),
|
||||
) {
|
||||
tracing::error!(
|
||||
"Failed to write to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::CData(e)) => {
|
||||
if in_message || in_throwable {
|
||||
if let Ok(text) = e
|
||||
.escape()
|
||||
.map_err(|x| x.into())
|
||||
.and_then(|x| x.unescape())
|
||||
{
|
||||
current_content.push_str(&text);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
}
|
||||
} else {
|
||||
let mut line = String::new();
|
||||
|
||||
while let Ok(bytes_read) = buf_reader.read_line(&mut line).await {
|
||||
if bytes_read == 0 {
|
||||
break; // End of stream
|
||||
}
|
||||
|
||||
if !line.is_empty() {
|
||||
if let Err(e) = Self::append_to_log_file(&log_path, &line) {
|
||||
tracing::warn!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_timestamp(timestamp: Option<&str>) -> String {
|
||||
if let Some(timestamp_str) = timestamp {
|
||||
if let Ok(timestamp_val) = timestamp_str.parse::<i64>() {
|
||||
let datetime_utc = if timestamp_val > i32::MAX as i64 {
|
||||
let secs = timestamp_val / 1000;
|
||||
let nsecs = ((timestamp_val % 1000) * 1_000_000) as u32;
|
||||
|
||||
chrono::DateTime::<Utc>::from_timestamp(secs, nsecs)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
chrono::DateTime::<Utc>::from_timestamp(timestamp_val, 0)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let datetime_local = datetime_utc.with_timezone(&chrono::Local);
|
||||
format!("[{}]", datetime_local.format("%H:%M:%S"))
|
||||
} else {
|
||||
"[??:??:??]".to_string()
|
||||
}
|
||||
} else {
|
||||
"[??:??:??]".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn append_to_log_file(
|
||||
path: impl AsRef<Path>,
|
||||
line: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let mut file =
|
||||
OpenOptions::new().append(true).create(true).open(path)?;
|
||||
|
||||
file.write_all(line.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_handle_server_join_logging(
|
||||
profile_path: &str,
|
||||
timestamp: &str,
|
||||
message: &str,
|
||||
) -> crate::Result<()> {
|
||||
let Some(host_port_string) = message.strip_prefix("Connecting to ")
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some((host, port_string)) = host_port_string.rsplit_once(", ")
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(port) = port_string.parse::<u16>().ok() else {
|
||||
return Ok(());
|
||||
};
|
||||
let timestamp = timestamp
|
||||
.parse::<i64>()
|
||||
.map(|x| x / 1000)
|
||||
.map_err(|x| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Failed to parse timestamp: {x}"
|
||||
))
|
||||
})
|
||||
.and_then(|x| {
|
||||
Utc.timestamp_opt(x, 0).single().ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(
|
||||
"Failed to convert timestamp to DateTime".to_string(),
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
let state = crate::State::get().await?;
|
||||
crate::state::server_join_log::JoinLogEntry {
|
||||
profile_path: profile_path.to_owned(),
|
||||
host: host.to_string(),
|
||||
port,
|
||||
join_time: timestamp,
|
||||
}
|
||||
.upsert(&state.pool)
|
||||
.await?;
|
||||
{
|
||||
let profile_path = profile_path.to_owned();
|
||||
let host = host.to_owned();
|
||||
tokio::spawn(async move {
|
||||
let _ = emit_profile(
|
||||
&profile_path,
|
||||
ProfilePayloadType::ServerJoined {
|
||||
host,
|
||||
port,
|
||||
timestamp,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Spawns a new child process and inserts it into the hashmap
|
||||
// Also, as the process ends, it spawns the follow-up process if it exists
|
||||
// By convention, ExitStatus is last command's exit status, and we exit on the first non-zero exit status
|
||||
@@ -204,6 +656,21 @@ impl Process {
|
||||
}
|
||||
});
|
||||
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path);
|
||||
let log_path = logs_folder.join(LAUNCHER_LOG_PATH);
|
||||
|
||||
if log_path.exists() {
|
||||
if let Err(e) = Process::append_to_log_file(
|
||||
&log_path,
|
||||
&format!("\n# Process exited with status: {mc_exit_status}\n"),
|
||||
) {
|
||||
tracing::warn!(
|
||||
"Failed to write exit status to log file: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = state.discord_rpc.clear_to_default(true).await;
|
||||
|
||||
let _ = state.friends_socket.update_status(None).await;
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
use super::settings::{Hooks, MemorySettings, WindowSize};
|
||||
use crate::profile::get_full_path;
|
||||
use crate::state::server_join_log::JoinLogEntry;
|
||||
use crate::state::{
|
||||
cache_file_hash, CacheBehaviour, CachedEntry, CachedFileHash,
|
||||
CacheBehaviour, CachedEntry, CachedFileHash, cache_file_hash,
|
||||
};
|
||||
use crate::util;
|
||||
use crate::util::fetch::{write_cached_icon, FetchSemaphore, IoSemaphore};
|
||||
use crate::util::fetch::{FetchSemaphore, IoSemaphore, write_cached_icon};
|
||||
use crate::util::io::{self};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use chrono::{DateTime, TimeDelta, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
use tokio::fs::DirEntry;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
// Represent a Minecraft instance.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
pub path: String,
|
||||
pub install_stage: ProfileInstallStage,
|
||||
pub launcher_feature_version: LauncherFeatureVersion,
|
||||
|
||||
pub name: String,
|
||||
pub icon_path: Option<String>,
|
||||
|
||||
pub game_version: String,
|
||||
pub protocol_version: Option<i32>,
|
||||
pub loader: ModLoader,
|
||||
pub loader_version: Option<String>,
|
||||
|
||||
@@ -86,6 +96,38 @@ impl ProfileInstallStage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LauncherFeatureVersion {
|
||||
None,
|
||||
MigratedServerLastPlayTime,
|
||||
}
|
||||
|
||||
impl LauncherFeatureVersion {
|
||||
pub const MOST_RECENT: Self = Self::MigratedServerLastPlayTime;
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
Self::None => "none",
|
||||
Self::MigratedServerLastPlayTime => {
|
||||
"migrated_server_last_play_time"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(val: &str) -> Self {
|
||||
match val {
|
||||
"none" => Self::None,
|
||||
"migrated_server_last_play_time" => {
|
||||
Self::MigratedServerLastPlayTime
|
||||
}
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LinkedData {
|
||||
pub project_id: String,
|
||||
@@ -261,6 +303,8 @@ struct ProfileQueryResult {
|
||||
override_hook_pre_launch: Option<String>,
|
||||
override_hook_wrapper: Option<String>,
|
||||
override_hook_post_exit: Option<String>,
|
||||
protocol_version: Option<i64>,
|
||||
launcher_feature_version: String,
|
||||
}
|
||||
|
||||
impl TryFrom<ProfileQueryResult> for Profile {
|
||||
@@ -270,9 +314,13 @@ impl TryFrom<ProfileQueryResult> for Profile {
|
||||
Ok(Profile {
|
||||
path: x.path,
|
||||
install_stage: ProfileInstallStage::from_str(&x.install_stage),
|
||||
launcher_feature_version: LauncherFeatureVersion::from_str(
|
||||
&x.launcher_feature_version,
|
||||
),
|
||||
name: x.name,
|
||||
icon_path: x.icon_path,
|
||||
game_version: x.game_version,
|
||||
protocol_version: x.protocol_version.map(|x| x as i32),
|
||||
loader: ModLoader::from_string(&x.mod_loader),
|
||||
loader_version: x.mod_loader_version,
|
||||
groups: serde_json::from_value(x.groups).unwrap_or_default(),
|
||||
@@ -336,8 +384,8 @@ macro_rules! select_profiles_with_predicate {
|
||||
ProfileQueryResult,
|
||||
r#"
|
||||
SELECT
|
||||
path, install_stage, name, icon_path,
|
||||
game_version, mod_loader, mod_loader_version,
|
||||
path, install_stage, launcher_feature_version, name, icon_path,
|
||||
game_version, protocol_version, mod_loader, mod_loader_version,
|
||||
json(groups) as "groups!: serde_json::Value",
|
||||
linked_project_id, linked_version_id, locked,
|
||||
created, modified, last_played,
|
||||
@@ -399,6 +447,8 @@ impl Profile {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let install_stage = self.install_stage.as_str();
|
||||
let launcher_feature_version = self.launcher_feature_version.as_str();
|
||||
|
||||
let mod_loader = self.loader.as_str();
|
||||
|
||||
let groups = serde_json::to_string(&self.groups)?;
|
||||
@@ -435,7 +485,8 @@ impl Profile {
|
||||
submitted_time_played, recent_time_played,
|
||||
override_java_path, override_extra_launch_args, override_custom_env_vars,
|
||||
override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,
|
||||
override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit
|
||||
override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,
|
||||
protocol_version, launcher_feature_version
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4,
|
||||
@@ -446,7 +497,8 @@ impl Profile {
|
||||
$15, $16,
|
||||
$17, jsonb($18), jsonb($19),
|
||||
$20, $21, $22, $23,
|
||||
$24, $25, $26
|
||||
$24, $25, $26,
|
||||
$27, $28
|
||||
)
|
||||
ON CONFLICT (path) DO UPDATE SET
|
||||
install_stage = $2,
|
||||
@@ -480,7 +532,10 @@ impl Profile {
|
||||
|
||||
override_hook_pre_launch = $24,
|
||||
override_hook_wrapper = $25,
|
||||
override_hook_post_exit = $26
|
||||
override_hook_post_exit = $26,
|
||||
|
||||
protocol_version = $27,
|
||||
launcher_feature_version = $28
|
||||
",
|
||||
self.path,
|
||||
install_stage,
|
||||
@@ -508,6 +563,8 @@ impl Profile {
|
||||
self.hooks.pre_launch,
|
||||
self.hooks.wrapper,
|
||||
self.hooks.post_exit,
|
||||
self.protocol_version,
|
||||
launcher_feature_version
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -557,10 +614,10 @@ impl Profile {
|
||||
let mut all = Self::get_all(&state.pool).await?;
|
||||
|
||||
let mut keys = vec![];
|
||||
let mut migrations = JoinSet::new();
|
||||
|
||||
for profile in &mut all {
|
||||
let path =
|
||||
crate::api::profile::get_full_path(&profile.path).await?;
|
||||
let path = get_full_path(&profile.path).await?;
|
||||
|
||||
for project_type in ProjectType::iterator() {
|
||||
let folder = project_type.get_folder();
|
||||
@@ -602,7 +659,42 @@ impl Profile {
|
||||
profile.install_stage = ProfileInstallStage::NotInstalled;
|
||||
profile.upsert(&state.pool).await?;
|
||||
}
|
||||
|
||||
if profile.launcher_feature_version
|
||||
< LauncherFeatureVersion::MOST_RECENT
|
||||
{
|
||||
let state = state.clone();
|
||||
let profile_path = profile.path.clone();
|
||||
migrations.spawn(async move {
|
||||
let Ok(Some(mut profile)) = Self::get(&profile_path, &state.pool).await else {
|
||||
tracing::error!("Failed to find instance '{}' for migration", profile_path);
|
||||
return;
|
||||
};
|
||||
drop(profile_path);
|
||||
|
||||
tracing::info!(
|
||||
"Migrating profile '{}' from launcher feature version {:?} to {:?}",
|
||||
profile.path, profile.launcher_feature_version, LauncherFeatureVersion::MOST_RECENT
|
||||
);
|
||||
loop {
|
||||
let result = profile.perform_launcher_feature_migration(&state).await;
|
||||
if result.is_err() || profile.launcher_feature_version == LauncherFeatureVersion::MOST_RECENT {
|
||||
if let Err(err) = result {
|
||||
tracing::error!("Failed to migrate instance '{}': {}", profile.path, err);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = profile.upsert(&state.pool).await {
|
||||
tracing::error!("Failed to update instance '{}' migration state: {}", profile.path, err);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
tracing::info!("Finished migration for profile '{}'", profile.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
migrations.join_all().await;
|
||||
|
||||
let file_hashes = CachedEntry::get_file_hash_many(
|
||||
&keys.iter().map(|s| &**s).collect::<Vec<_>>(),
|
||||
@@ -643,6 +735,144 @@ impl Profile {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn perform_launcher_feature_migration(
|
||||
&mut self,
|
||||
state: &crate::State,
|
||||
) -> crate::Result<()> {
|
||||
match self.launcher_feature_version {
|
||||
LauncherFeatureVersion::None => {
|
||||
if self.last_played.is_none() {
|
||||
self.launcher_feature_version =
|
||||
LauncherFeatureVersion::MigratedServerLastPlayTime;
|
||||
return Ok(());
|
||||
}
|
||||
let mut join_log_entry = JoinLogEntry {
|
||||
profile_path: self.path.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let logs_path = state.directories.profile_logs_dir(&self.path);
|
||||
let Ok(mut directory) = io::read_dir(&logs_path).await else {
|
||||
self.launcher_feature_version =
|
||||
LauncherFeatureVersion::MigratedServerLastPlayTime;
|
||||
return Ok(());
|
||||
};
|
||||
let existing_joins_map =
|
||||
super::server_join_log::get_joins(&self.path, &state.pool)
|
||||
.await?;
|
||||
let existing_joins = existing_joins_map
|
||||
.keys()
|
||||
.map(|x| (&x.0 as &str, x.1))
|
||||
.collect::<HashSet<_>>();
|
||||
while let Some(log_file) = directory.next_entry().await? {
|
||||
if let Err(err) = Self::parse_log_file(
|
||||
&log_file,
|
||||
|host, port| existing_joins.contains(&(host, port)),
|
||||
state,
|
||||
&mut join_log_entry,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to parse log file '{}': {}",
|
||||
log_file.path().display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
self.launcher_feature_version =
|
||||
LauncherFeatureVersion::MigratedServerLastPlayTime;
|
||||
}
|
||||
LauncherFeatureVersion::MOST_RECENT => unreachable!(
|
||||
"LauncherFeatureVersion::MOST_RECENT was not updated"
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Parses a log file on a best-effort basis, using the log's creation time, rather than the
|
||||
// actual times mentioned in the log file, which are missing date information.
|
||||
async fn parse_log_file(
|
||||
log_file: &DirEntry,
|
||||
should_skip: impl Fn(&str, u16) -> bool,
|
||||
state: &crate::State,
|
||||
join_entry: &mut JoinLogEntry,
|
||||
) -> crate::Result<()> {
|
||||
let file_name = log_file.file_name();
|
||||
let Some(file_name) = file_name.to_str() else {
|
||||
return Ok(());
|
||||
};
|
||||
let log_time = io::metadata(&log_file.path()).await?.created()?.into();
|
||||
if file_name == "latest.log" {
|
||||
let file = io::open_file(&log_file.path()).await?;
|
||||
Self::parse_open_log_file(
|
||||
file,
|
||||
should_skip,
|
||||
log_time,
|
||||
state,
|
||||
join_entry,
|
||||
)
|
||||
.await
|
||||
} else if file_name.ends_with(".log.gz") {
|
||||
let file = io::open_file(&log_file.path()).await?;
|
||||
let file = tokio::io::BufReader::new(file);
|
||||
let file =
|
||||
async_compression::tokio::bufread::GzipDecoder::new(file);
|
||||
Self::parse_open_log_file(
|
||||
file,
|
||||
should_skip,
|
||||
log_time,
|
||||
state,
|
||||
join_entry,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn parse_open_log_file(
|
||||
reader: impl AsyncRead + Unpin,
|
||||
should_skip: impl Fn(&str, u16) -> bool,
|
||||
mut log_time: DateTime<Utc>,
|
||||
state: &crate::State,
|
||||
join_entry: &mut JoinLogEntry,
|
||||
) -> crate::Result<()> {
|
||||
static LOG_LINE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^\[[0-9]{2}(?::[0-9]{2}){2}] \[.+?/[A-Z]+?]: Connecting to (.+?), ([1-9][0-9]{0,4})$").unwrap()
|
||||
});
|
||||
let reader = tokio::io::BufReader::new(reader);
|
||||
let mut lines = reader.lines();
|
||||
while let Some(log_line) = lines.next_line().await? {
|
||||
let Some(log_line) = LOG_LINE_REGEX.captures(&log_line) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(host) = log_line.get(1) else {
|
||||
continue;
|
||||
};
|
||||
let host = host.as_str();
|
||||
|
||||
let Some(port) = log_line.get(2) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(port) = port.as_str().parse::<u16>() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if should_skip(host, port) {
|
||||
continue;
|
||||
}
|
||||
|
||||
join_entry.host = host.to_string();
|
||||
join_entry.port = port;
|
||||
join_entry.join_time = log_time;
|
||||
join_entry.upsert(&state.pool).await?;
|
||||
|
||||
log_time += TimeDelta::seconds(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_projects(
|
||||
&self,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
|
||||
65
packages/app-lib/src/state/server_join_log.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JoinLogEntry {
|
||||
pub profile_path: String,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub join_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl JoinLogEntry {
|
||||
pub async fn upsert(
|
||||
&self,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let join_time = self.join_time.timestamp();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO join_log (profile_path, host, port, join_time)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (profile_path, host, port) DO UPDATE SET
|
||||
join_time = $4
|
||||
",
|
||||
self.profile_path,
|
||||
self.host,
|
||||
self.port,
|
||||
join_time
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_joins(
|
||||
instance: &str,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<HashMap<(String, u16), DateTime<Utc>>> {
|
||||
let joins = sqlx::query!(
|
||||
"
|
||||
SELECT profile_path, host, port, join_time
|
||||
FROM join_log
|
||||
WHERE profile_path = $1
|
||||
",
|
||||
instance
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(joins
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
(
|
||||
(x.host, x.port as u16),
|
||||
Utc.timestamp_opt(x.join_time, 0)
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -44,6 +44,8 @@ pub struct Settings {
|
||||
pub enum FeatureFlag {
|
||||
PagePath,
|
||||
ProjectBackground,
|
||||
WorldsTab,
|
||||
WorldsInHome,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::state::friends::{TunnelSockets, WriteSocket};
|
||||
use crate::state::FriendsSocket;
|
||||
use crate::state::friends::{TunnelSockets, WriteSocket};
|
||||
use ariadne::networking::message::ClientToServerMessage;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Functions for fetching infromation from the Internet
|
||||
//! Functions for fetching information from the Internet
|
||||
use super::io::{self, IOError};
|
||||
use crate::config::{MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
||||
use crate::event::emit::emit_loading;
|
||||
use crate::event::LoadingBarId;
|
||||
use crate::event::emit::emit_loading;
|
||||
use bytes::Bytes;
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::Method;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use std::time::{self};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
@@ -18,22 +18,20 @@ pub struct IoSemaphore(pub Semaphore);
|
||||
#[derive(Debug)]
|
||||
pub struct FetchSemaphore(pub Semaphore);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REQWEST_CLIENT: reqwest::Client = {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let header = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"modrinth/theseus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.unwrap();
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.expect("Reqwest Client Building Failed")
|
||||
};
|
||||
}
|
||||
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let header = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"modrinth/theseus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.unwrap();
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.expect("Reqwest Client Building Failed")
|
||||
});
|
||||
const FETCH_ATTEMPTS: usize = 3;
|
||||
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
|
||||
@@ -255,3 +255,42 @@ pub async fn remove_file(
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// open file
|
||||
pub async fn open_file(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<tokio::fs::File, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::File::open(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// remove dir
|
||||
pub async fn remove_dir(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::remove_dir(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// metadata
|
||||
pub async fn metadata(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<std::fs::Metadata, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::metadata(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use tokio::task::JoinError;
|
||||
use crate::State;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::{
|
||||
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY},
|
||||
RegKey,
|
||||
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY},
|
||||
};
|
||||
|
||||
// Entrypoint function (Windows)
|
||||
@@ -276,11 +276,10 @@ pub async fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
|
||||
};
|
||||
|
||||
let bytes = include_bytes!("../../library/JavaInfo.class");
|
||||
let tempdir: PathBuf = tempfile::tempdir().ok()?.into_path();
|
||||
if !tempdir.exists() {
|
||||
let Ok(tempdir) = tempfile::tempdir() else {
|
||||
return None;
|
||||
}
|
||||
let file_path = tempdir.join("JavaInfo.class");
|
||||
};
|
||||
let file_path = tempdir.path().join("JavaInfo.class");
|
||||
io::write(&file_path, bytes).await.ok()?;
|
||||
|
||||
let output = Command::new(&java)
|
||||
|
||||
@@ -3,7 +3,8 @@ pub mod fetch;
|
||||
pub mod io;
|
||||
pub mod jre;
|
||||
pub mod platform;
|
||||
pub mod utils;
|
||||
pub mod utils; // AstralRinth
|
||||
pub mod server_ping;
|
||||
|
||||
/// Wrap a builder which uses a mut reference into one which outputs an owned value
|
||||
macro_rules! wrap_ref_builder {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Platform-related code
|
||||
use daedalus::minecraft::{Os, OsRule};
|
||||
use regex::Regex;
|
||||
|
||||
// OS detection
|
||||
pub trait OsExt {
|
||||
@@ -92,12 +91,16 @@ pub fn os_rule(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &=
|
||||
regex.is_match(&sys_info::os_release().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
// `rule.version` is ignored because it's not usually seen on real recent
|
||||
// Minecraft version manifests, its alleged regex syntax is undefined and is
|
||||
// likely to not match `Regex`'s, and the way to get the value to match it
|
||||
// against is allegedly calling `System.getProperty("os.version")`, which
|
||||
// on Windows the OpenJDK implements by fetching the kernel32.dll version,
|
||||
// an approach that no public Rust library implements. Moreover, launchers
|
||||
// such as PrismLauncher also ignore this field. Code references:
|
||||
// - https://github.com/openjdk/jdk/blob/948ade8e7003a41683600428c8e3155c7ed798db/src/java.base/windows/native/libjava/java_props_md.c#L556
|
||||
// - https://github.com/PrismLauncher/PrismLauncher/blob/1c20faccf88999474af70db098a4c10e7a03af33/launcher/minecraft/Rule.h#L77
|
||||
// - https://github.com/FillZpp/sys-info-rs/blob/60ecf1470a5b7c90242f429934a3bacb6023ec4d/c/windows.c#L23-L38
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
||||
223
packages/app-lib/src/util/server_ping.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use crate::ErrorKind;
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::time::Duration;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
use tokio::select;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerStatus {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<Box<RawValue>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub players: Option<ServerPlayers>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<ServerVersion>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub favicon: Option<Url>,
|
||||
#[serde(default)]
|
||||
pub enforces_secure_chat: bool,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ping: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerPlayers {
|
||||
pub max: i32,
|
||||
pub online: i32,
|
||||
#[serde(default)]
|
||||
pub sample: Vec<ServerGameProfile>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerGameProfile {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerVersion {
|
||||
pub name: String,
|
||||
pub protocol: i32,
|
||||
}
|
||||
|
||||
pub async fn get_server_status(
|
||||
address: &impl ToSocketAddrs,
|
||||
original_address: (&str, u16),
|
||||
protocol_version: Option<i32>,
|
||||
) -> Result<ServerStatus> {
|
||||
select! {
|
||||
res = modern::status(address, original_address, protocol_version) => res,
|
||||
_ = tokio::time::sleep(Duration::from_secs(30)) => Err(ErrorKind::OtherError(
|
||||
format!("Ping of {}:{} timed out", original_address.0, original_address.1)
|
||||
).into())
|
||||
}
|
||||
}
|
||||
|
||||
mod modern {
|
||||
use super::ServerStatus;
|
||||
use crate::ErrorKind;
|
||||
use chrono::Utc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
|
||||
pub async fn status(
|
||||
address: &impl ToSocketAddrs,
|
||||
original_address: (&str, u16),
|
||||
protocol_version: Option<i32>,
|
||||
) -> crate::Result<ServerStatus> {
|
||||
let mut stream = TcpStream::connect(address).await?;
|
||||
handshake(&mut stream, original_address, protocol_version).await?;
|
||||
let mut result = status_body(&mut stream).await?;
|
||||
result.ping = ping(&mut stream).await.ok();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn handshake(
|
||||
stream: &mut TcpStream,
|
||||
original_address: (&str, u16),
|
||||
protocol_version: Option<i32>,
|
||||
) -> crate::Result<()> {
|
||||
let (host, port) = original_address;
|
||||
let protocol_version = protocol_version.unwrap_or(-1);
|
||||
|
||||
const PACKET_ID: i32 = 0;
|
||||
const NEXT_STATE: i32 = 1;
|
||||
|
||||
let packet_size = varint::get_byte_size(PACKET_ID)
|
||||
+ varint::get_byte_size(protocol_version)
|
||||
+ varint::get_byte_size(host.len() as i32)
|
||||
+ host.len()
|
||||
+ size_of::<u16>()
|
||||
+ varint::get_byte_size(NEXT_STATE);
|
||||
|
||||
let mut packet_buffer = Vec::with_capacity(
|
||||
varint::get_byte_size(packet_size as i32) + packet_size,
|
||||
);
|
||||
|
||||
varint::write(&mut packet_buffer, packet_size as i32);
|
||||
varint::write(&mut packet_buffer, PACKET_ID);
|
||||
varint::write(&mut packet_buffer, protocol_version);
|
||||
varint::write(&mut packet_buffer, host.len() as i32);
|
||||
packet_buffer.extend_from_slice(host.as_bytes());
|
||||
packet_buffer.extend_from_slice(&port.to_be_bytes());
|
||||
varint::write(&mut packet_buffer, NEXT_STATE);
|
||||
|
||||
stream.write_all(&packet_buffer).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status_body(
|
||||
stream: &mut TcpStream,
|
||||
) -> crate::Result<ServerStatus> {
|
||||
stream.write_all(&[0x01, 0x00]).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
let packet_length = varint::read(stream).await?;
|
||||
if packet_length < 0 {
|
||||
return Err(ErrorKind::InputError(
|
||||
"Invalid status response packet length".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut packet_stream = stream.take(packet_length as u64);
|
||||
let packet_id = varint::read(&mut packet_stream).await?;
|
||||
if packet_id != 0x00 {
|
||||
return Err(ErrorKind::InputError(
|
||||
"Unexpected status response".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let response_length = varint::read(&mut packet_stream).await?;
|
||||
let mut json_response = vec![0_u8; response_length as usize];
|
||||
packet_stream.read_exact(&mut json_response).await?;
|
||||
|
||||
if packet_stream.limit() > 0 {
|
||||
tokio::io::copy(&mut packet_stream, &mut tokio::io::sink()).await?;
|
||||
}
|
||||
|
||||
Ok(serde_json::from_slice(&json_response)?)
|
||||
}
|
||||
|
||||
async fn ping(stream: &mut TcpStream) -> crate::Result<i64> {
|
||||
let start_time = Utc::now();
|
||||
let ping_magic = start_time.timestamp_millis();
|
||||
|
||||
stream.write_all(&[0x09, 0x01]).await?;
|
||||
stream.write_i64(ping_magic).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
let mut response_prefix = [0_u8; 2];
|
||||
stream.read_exact(&mut response_prefix).await?;
|
||||
let response_magic = stream.read_i64().await?;
|
||||
if response_prefix != [0x09, 0x01] || response_magic != ping_magic {
|
||||
return Err(ErrorKind::InputError(
|
||||
"Unexpected ping response".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let response_time = Utc::now();
|
||||
Ok((response_time - start_time).num_milliseconds())
|
||||
}
|
||||
|
||||
mod varint {
|
||||
use std::io;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
const MAX_VARINT_SIZE: usize = 5;
|
||||
const DATA_BITS_MASK: u32 = 0x7f;
|
||||
const CONT_BIT_MASK_U8: u8 = 0x80;
|
||||
const CONT_BIT_MASK_U32: u32 = CONT_BIT_MASK_U8 as u32;
|
||||
const DATA_BITS_PER_BYTE: usize = 7;
|
||||
|
||||
pub fn get_byte_size(x: i32) -> usize {
|
||||
let x = x as u32;
|
||||
for size in 1..MAX_VARINT_SIZE {
|
||||
if (x & (u32::MAX << (size * DATA_BITS_PER_BYTE))) == 0 {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
MAX_VARINT_SIZE
|
||||
}
|
||||
|
||||
pub fn write(out: &mut Vec<u8>, value: i32) {
|
||||
let mut value = value as u32;
|
||||
while value >= CONT_BIT_MASK_U32 {
|
||||
out.push(((value & DATA_BITS_MASK) | CONT_BIT_MASK_U32) as u8);
|
||||
value >>= DATA_BITS_PER_BYTE;
|
||||
}
|
||||
out.push(value as u8);
|
||||
}
|
||||
|
||||
pub async fn read<R: AsyncRead + Unpin>(
|
||||
reader: &mut R,
|
||||
) -> io::Result<i32> {
|
||||
let mut result = 0;
|
||||
let mut shift = 0;
|
||||
|
||||
loop {
|
||||
let b = reader.read_u8().await?;
|
||||
result |=
|
||||
(b as u32 & DATA_BITS_MASK) << (shift * DATA_BITS_PER_BYTE);
|
||||
shift += 1;
|
||||
if shift > MAX_VARINT_SIZE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VarInt too big",
|
||||
));
|
||||
}
|
||||
if b & CONT_BIT_MASK_U8 == 0 {
|
||||
return Ok(result as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io;
|
||||
|
||||
/*
|
||||
AstralRinth Utils
|
||||
*/
|
||||
const PACKAGE_JSON_CONTENT: &str =
|
||||
// include_str!("../../../../apps/app-frontend/package.json");
|
||||
include_str!("../../../../apps/app/tauri.conf.json");
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
[package]
|
||||
name = "ariadne"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
uuid = { version = "1.2.2", features = ["v4", "fast-rng", "serde"] }
|
||||
serde_bytes = "0.11"
|
||||
rand = "0.8.5"
|
||||
either = "1.13"
|
||||
chrono = { version = "0.4.26", features = ["serde"] }
|
||||
serde_cbor = "0.11"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng", "serde"] }
|
||||
serde_bytes.workspace = true
|
||||
rand.workspace = true
|
||||
either.workspace = true
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
serde_cbor.workspace = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub use super::users::UserId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
@@ -33,7 +32,7 @@ pub fn random_base62_rng_range<R: rand::RngCore>(
|
||||
) -> u64 {
|
||||
use rand::Rng;
|
||||
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
|
||||
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
// random_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
// which is n characters long when encoded
|
||||
rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max])
|
||||
}
|
||||
@@ -72,24 +71,7 @@ pub enum DecodingError {
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! from_base62id {
|
||||
($($struct:ty, $con:expr;)+) => {
|
||||
$(
|
||||
impl From<Base62Id> for $struct {
|
||||
fn from(id: Base62Id) -> $struct {
|
||||
$con(id.0)
|
||||
}
|
||||
}
|
||||
impl From<$struct> for Base62Id {
|
||||
fn from(id: $struct) -> Base62Id {
|
||||
Base62Id(id.0)
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! impl_base62_display {
|
||||
($struct:ty) => {
|
||||
impl std::fmt::Display for $struct {
|
||||
@@ -102,15 +84,42 @@ macro_rules! impl_base62_display {
|
||||
impl_base62_display!(Base62Id);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base62_id_impl {
|
||||
($struct:ty, $cons:expr) => {
|
||||
$crate::ids::from_base62id!($struct, $cons;);
|
||||
$crate::ids::impl_base62_display!($struct);
|
||||
}
|
||||
}
|
||||
base62_id_impl!(UserId, UserId);
|
||||
macro_rules! base62_id {
|
||||
($struct:ident) => {
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
Debug,
|
||||
Hash,
|
||||
)]
|
||||
#[serde(from = "ariadne::ids::Base62Id")]
|
||||
#[serde(into = "ariadne::ids::Base62Id")]
|
||||
pub struct $struct(pub u64);
|
||||
|
||||
pub use {base62_id_impl, from_base62id, impl_base62_display};
|
||||
$crate::ids::impl_base62_display!($struct);
|
||||
|
||||
impl From<$crate::ids::Base62Id> for $struct {
|
||||
fn from(id: $crate::ids::Base62Id) -> Self {
|
||||
Self(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$struct> for $crate::ids::Base62Id {
|
||||
fn from(id: $struct) -> Self {
|
||||
Self(id.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
use crate as ariadne; // Hack because serde(from) and serde(into) don't work with $crate
|
||||
base62_id!(UserId);
|
||||
|
||||
pub use {base62_id, impl_base62_display};
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod ids;
|
||||
pub mod networking;
|
||||
pub mod users;
|
||||
pub mod versions;
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
use super::ids::Base62Id;
|
||||
use super::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct UserId(pub u64);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserStatus {
|
||||
pub user_id: UserId,
|
||||
|
||||
45
packages/ariadne/src/versions.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::{collections::HashMap, sync::LazyLock};
|
||||
|
||||
static SPECIAL_PARENTS: LazyLock<HashMap<&'static str, &'static str>> =
|
||||
LazyLock::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("15w14a", "1.8.3");
|
||||
m.insert("1.RV-Pre1", "1.9.2");
|
||||
m.insert("3D Shareware v1.34", "19w13b");
|
||||
m.insert("20w14infinite", "20w13b");
|
||||
m.insert("22w13oneblockatatime", "1.18.2");
|
||||
m.insert("23w13a_or_b", "23w13a");
|
||||
m.insert("24w14potato", "24w12a");
|
||||
m
|
||||
});
|
||||
|
||||
pub fn is_feature_supported_in(
|
||||
version: &str,
|
||||
first_release: &str,
|
||||
first_snapshot: &str,
|
||||
) -> bool {
|
||||
let version = SPECIAL_PARENTS.get(version).copied().unwrap_or(version);
|
||||
if version.contains('w') && version.len() == 6 {
|
||||
return version >= first_snapshot;
|
||||
}
|
||||
if version == first_release {
|
||||
return true;
|
||||
}
|
||||
let parts_version = version.split('.');
|
||||
let parts_first_release = first_release.split('.');
|
||||
for (part_version, part_first_release) in
|
||||
parts_version.zip(parts_first_release)
|
||||
{
|
||||
if part_version == part_first_release {
|
||||
continue;
|
||||
}
|
||||
if let Ok(part_version) = part_version.parse::<u32>() {
|
||||
if let Ok(part_first_release) = part_first_release.parse::<u32>() {
|
||||
if part_version > part_first_release {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
BIN
packages/assets/branding/rinthbot/angry.webp
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
packages/assets/branding/rinthbot/annoyed.webp
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
packages/assets/branding/rinthbot/confused.webp
Normal file
|
After Width: | Height: | Size: 347 KiB |
BIN
packages/assets/branding/rinthbot/excited.webp
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
packages/assets/branding/rinthbot/laughing.webp
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
packages/assets/branding/rinthbot/sad.webp
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
packages/assets/branding/rinthbot/sleeping.webp
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
packages/assets/branding/rinthbot/sobbing.webp
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
packages/assets/branding/rinthbot/thinking.webp
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
packages/assets/branding/rinthbot/waving.webp
Normal file
|
After Width: | Height: | Size: 241 KiB |
1
packages/assets/external/curseforge.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>CurseForge</title><path fill="currentColor" d="M18.326 9.2145S23.2261 8.4418 24 6.1882h-7.5066V4.4H0l2.0318 2.3576V9.173s5.1267-.2665 7.1098 1.2372c2.7146 2.516-3.053 5.917-3.053 5.917L5.0995 19.6c1.5465-1.4726 4.494-3.3775 9.8983-3.2857-2.0565.65-4.1245 1.6651-5.7344 3.2857h10.9248l-1.0288-3.2726s-7.918-4.6688-.8336-7.1127z"/></svg>
|
||||
|
After Width: | Height: | Size: 414 B |
1
packages/assets/external/pyro.svg
vendored
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" fill="none" width="24" height="24"><path fill="currentColor" d="M9.1 23v-1.1c.3-1 1.1-1.7 2.2-1.9h2.5c1.1.2 2 1 2.2 1.9v2c0 .8 0 1.2-.2 1.5 0 .2-.3.3-.5.4-.3.2-.7.2-1.6.2h-2.3c-.8 0-1.3 0-1.6-.2-.2 0-.4-.2-.5-.4-.2-.3-.2-.7-.2-1.4v-1Z"></path><path fill="currentColor" d="M4 26h2.8v-.5c-1.5-10.4 9-7.2 9.8-13.9C17 7 14 7 15.2.6V0l-.7.2C9.5 3 6 8.2 7.3 11.5c.2.4.3.7.2.8-.2 0-.4 0-.8-.3-.7-.4-1.5-1-2.1-2l-.4-.4-.4.4c-6.2 6.7-3.7 13.5-.1 16H4Z"></path><path fill="currentColor" d="M19.3 10.2c.3 3-2 6.2-4.5 7.6-.4.2-.6.3-.6.5l.8.3c3.4 1 4.4 4.3 3.3 7v.3l.5.1H21c8-4.6 5.2-12.4-1-16.2l-.7-.3c-.2.1-.2.3-.1.7Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 681 B |
5
packages/assets/icons.d.ts
vendored
@@ -4,3 +4,8 @@ declare module '*.svg?component' {
|
||||
const src: FunctionalComponent<SVGAttributes>
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.webp' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
1
packages/assets/icons/blocks.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>
|
||||
|
After Width: | Height: | Size: 367 B |
1
packages/assets/icons/file-archive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-archive-icon lucide-file-archive"><path d="M10 12v-1"/><path d="M10 18v-2"/><path d="M10 7V6"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 .274 1.01"/><circle cx="10" cy="20" r="2"/></svg>
|
||||
|
After Width: | Height: | Size: 458 B |
12
packages/assets/icons/no-signal.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M2 20h.01"/>
|
||||
<path d="M7,20v-4" />
|
||||
<path d="M12,20v-8" />
|
||||
<path d="M17,20v-12" />
|
||||
<path d="M22,4v16" />
|
||||
<g stroke="var(--color-red)">
|
||||
<line x1="2" y1="4" x2="7.1" y2="9.1" />
|
||||
<line x1="2" y1="9.1" x2="7.1" y2="4" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 459 B |
1
packages/assets/icons/pickaxe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pickaxe"><path d="M14.531 12.469 6.619 20.38a1 1 0 1 1-3-3l7.912-7.912"/><path d="M15.686 4.314A12.5 12.5 0 0 0 5.461 2.958 1 1 0 0 0 5.58 4.71a22 22 0 0 1 6.318 3.393"/><path d="M17.7 3.7a1 1 0 0 0-1.4 0l-4.6 4.6a1 1 0 0 0 0 1.4l2.6 2.6a1 1 0 0 0 1.4 0l4.6-4.6a1 1 0 0 0 0-1.4z"/><path d="M19.686 8.314a12.501 12.501 0 0 1 1.356 10.225 1 1 0 0 1-1.751-.119 22 22 0 0 0-3.393-6.319"/></svg>
|
||||
|
After Width: | Height: | Size: 592 B |
1
packages/assets/icons/signal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signal"><path stroke="var(--_signal-1, var(--_signal-2, var(--_signal-3, var(--_signal-4, var(--_signal-5, currentColor)))))" d="M2 20h.01"/><path stroke="var(--_signal-2, var(--_signal-3, var(--_signal-4, var(--_signal-5, currentColor))))" d="M7 20v-4"/><path stroke="var(--_signal-3, var(--_signal-4, var(--_signal-5, currentColor)))" d="M12 20v-8"/><path stroke="var(--_signal-4, var(--_signal-5, currentColor))" d="M17 20V8"/><path stroke="var(--_signal-5, currentColor)" d="M22 4v16"/></svg>
|
||||
|
After Width: | Height: | Size: 699 B |
1
packages/assets/icons/skull.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-skull"><path d="m12.5 17-.5-1-.5 1h1z"/><path d="M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="12" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 414 B |
1
packages/assets/icons/world.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-earth-icon lucide-earth"><path d="M21.54 15H17a2 2 0 0 0-2 2v4.54"/><path d="M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17"/><path d="M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"/><circle cx="12" cy="12" r="10"/></svg>
|
||||
|
After Width: | Height: | Size: 479 B |
@@ -4,6 +4,16 @@
|
||||
import _ModrinthIcon from './branding/logo.svg?component'
|
||||
import _FourOhFourNotFound from './branding/404.svg?component'
|
||||
import _ModrinthPlusIcon from './branding/modrinth-plus.svg?component'
|
||||
import _AngryRinthbot from './branding/rinthbot/angry.webp'
|
||||
import _AnnoyedRinthbot from './branding/rinthbot/annoyed.webp'
|
||||
import _ConfusedRinthbot from './branding/rinthbot/confused.webp'
|
||||
import _ExcitedRinthbot from './branding/rinthbot/excited.webp'
|
||||
import _LaughingRinthbot from './branding/rinthbot/laughing.webp'
|
||||
import _SadRinthbot from './branding/rinthbot/sad.webp'
|
||||
import _SleepingRinthbot from './branding/rinthbot/sleeping.webp'
|
||||
import _SobbingRinthbot from './branding/rinthbot/sobbing.webp'
|
||||
import _ThinkingRinthbot from './branding/rinthbot/thinking.webp'
|
||||
import _WavingRinthbot from './branding/rinthbot/waving.webp'
|
||||
|
||||
// External Icons
|
||||
import _SSODiscordIcon from './external/sso/discord.svg?component'
|
||||
@@ -15,6 +25,7 @@ import _SSOSteamIcon from './external/sso/steam.svg?component'
|
||||
import _AppleIcon from './external/apple.svg?component'
|
||||
import _BlueskyIcon from './external/bluesky.svg?component'
|
||||
import _BuyMeACoffeeIcon from './external/bmac.svg?component'
|
||||
import _CurseForgeIcon from './external/curseforge.svg?component'
|
||||
import _DiscordIcon from './external/discord.svg?component'
|
||||
import _GithubIcon from './external/github.svg?component'
|
||||
import _KoFiIcon from './external/kofi.svg?component'
|
||||
@@ -27,7 +38,6 @@ import _TumblrIcon from './external/tumblr.svg?component'
|
||||
import _TwitterIcon from './external/twitter.svg?component'
|
||||
import _WindowsIcon from './external/windows.svg?component'
|
||||
import _YouTubeIcon from './external/youtube.svg?component'
|
||||
import _PyroIcon from './external/pyro.svg?component'
|
||||
|
||||
// Icons
|
||||
import _AlignLeftIcon from './icons/align-left.svg?component'
|
||||
@@ -37,6 +47,7 @@ import _AsteriskIcon from './icons/asterisk.svg?component'
|
||||
import _BanIcon from './icons/ban.svg?component'
|
||||
import _BellIcon from './icons/bell.svg?component'
|
||||
import _BellRingIcon from './icons/bell-ring.svg?component'
|
||||
import _BlocksIcon from './icons/blocks.svg?component'
|
||||
import _BookIcon from './icons/book.svg?component'
|
||||
import _BookTextIcon from './icons/book-text.svg?component'
|
||||
import _BookmarkIcon from './icons/bookmark.svg?component'
|
||||
@@ -74,6 +85,7 @@ import _ExternalIcon from './icons/external.svg?component'
|
||||
import _EyeIcon from './icons/eye.svg?component'
|
||||
import _EyeOffIcon from './icons/eye-off.svg?component'
|
||||
import _FileIcon from './icons/file.svg?component'
|
||||
import _FileArchiveIcon from './icons/file-archive.svg?component'
|
||||
import _FileTextIcon from './icons/file-text.svg?component'
|
||||
import _FilterIcon from './icons/filter.svg?component'
|
||||
import _FilterXIcon from './icons/filter-x.svg?component'
|
||||
@@ -123,12 +135,14 @@ import _MoonIcon from './icons/moon.svg?component'
|
||||
import _MoreHorizontalIcon from './icons/more-horizontal.svg?component'
|
||||
import _MoreVerticalIcon from './icons/more-vertical.svg?component'
|
||||
import _NewspaperIcon from './icons/newspaper.svg?component'
|
||||
import _NoSignalIcon from './icons/no-signal.svg?component'
|
||||
import _OmorphiaIcon from './icons/omorphia.svg?component'
|
||||
import _OrganizationIcon from './icons/organization.svg?component'
|
||||
import _PackageIcon from './icons/package.svg?component'
|
||||
import _PackageOpenIcon from './icons/package-open.svg?component'
|
||||
import _PackageClosedIcon from './icons/package-closed.svg?component'
|
||||
import _PaintBrushIcon from './icons/paintbrush.svg?component'
|
||||
import _PickaxeIcon from './icons/pickaxe.svg?component'
|
||||
import _PlayIcon from './icons/play.svg?component'
|
||||
import _PlugIcon from './icons/plug.svg?component'
|
||||
import _PlusIcon from './icons/plus.svg?component'
|
||||
@@ -150,6 +164,8 @@ import _ServerIcon from './icons/server.svg?component'
|
||||
import _SettingsIcon from './icons/settings.svg?component'
|
||||
import _ShareIcon from './icons/share.svg?component'
|
||||
import _ShieldIcon from './icons/shield.svg?component'
|
||||
import _SignalIcon from './icons/signal.svg?component'
|
||||
import _SkullIcon from './icons/skull.svg?component'
|
||||
import _SlashIcon from './icons/slash.svg?component'
|
||||
import _SortAscendingIcon from './icons/sort-asc.svg?component'
|
||||
import _SortDescendingIcon from './icons/sort-desc.svg?component'
|
||||
@@ -179,6 +195,7 @@ import _UsersIcon from './icons/users.svg?component'
|
||||
import _VersionIcon from './icons/version.svg?component'
|
||||
import _WikiIcon from './icons/wiki.svg?component'
|
||||
import _WindowIcon from './icons/window.svg?component'
|
||||
import _WorldIcon from './icons/world.svg?component'
|
||||
import _WrenchIcon from './icons/wrench.svg?component'
|
||||
import _XIcon from './icons/x.svg?component'
|
||||
import _XCircleIcon from './icons/x-circle.svg?component'
|
||||
@@ -226,6 +243,16 @@ export const AstralRinthLogo = _AstralRinthLogo
|
||||
export const ModrinthIcon = _ModrinthIcon
|
||||
export const FourOhFourNotFound = _FourOhFourNotFound
|
||||
export const ModrinthPlusIcon = _ModrinthPlusIcon
|
||||
export const AngryRinthbot = _AngryRinthbot
|
||||
export const AnnoyedRinthbot = _AnnoyedRinthbot
|
||||
export const ConfusedRinthbot = _ConfusedRinthbot
|
||||
export const ExcitedRinthbot = _ExcitedRinthbot
|
||||
export const LaughingRinthbot = _LaughingRinthbot
|
||||
export const SadRinthbot = _SadRinthbot
|
||||
export const SleepingRinthbot = _SleepingRinthbot
|
||||
export const SobbingRinthbot = _SobbingRinthbot
|
||||
export const ThinkingRinthbot = _ThinkingRinthbot
|
||||
export const WavingRinthbot = _WavingRinthbot
|
||||
export const SSODiscordIcon = _SSODiscordIcon
|
||||
export const SSOGitHubIcon = _SSOGitHubIcon
|
||||
export const SSOGitLabIcon = _SSOGitLabIcon
|
||||
@@ -236,13 +263,13 @@ export const AppleIcon = _AppleIcon
|
||||
export const BlueskyIcon = _BlueskyIcon
|
||||
export const BuyMeACoffeeIcon = _BuyMeACoffeeIcon
|
||||
export const GithubIcon = _GithubIcon
|
||||
export const CurseForgeIcon = _CurseForgeIcon
|
||||
export const DiscordIcon = _DiscordIcon
|
||||
export const KoFiIcon = _KoFiIcon
|
||||
export const MastodonIcon = _MastodonIcon
|
||||
export const OpenCollectiveIcon = _OpenCollectiveIcon
|
||||
export const PatreonIcon = _PatreonIcon
|
||||
export const PayPalIcon = _PayPalIcon
|
||||
export const PyroIcon = _PyroIcon
|
||||
export const RedditIcon = _RedditIcon
|
||||
export const TumblrIcon = _TumblrIcon
|
||||
export const TwitterIcon = _TwitterIcon
|
||||
@@ -255,6 +282,7 @@ export const AsteriskIcon = _AsteriskIcon
|
||||
export const BanIcon = _BanIcon
|
||||
export const BellIcon = _BellIcon
|
||||
export const BellRingIcon = _BellRingIcon
|
||||
export const BlocksIcon = _BlocksIcon
|
||||
export const BookIcon = _BookIcon
|
||||
export const BookTextIcon = _BookTextIcon
|
||||
export const BookmarkIcon = _BookmarkIcon
|
||||
@@ -292,6 +320,7 @@ export const ExternalIcon = _ExternalIcon
|
||||
export const EyeIcon = _EyeIcon
|
||||
export const EyeOffIcon = _EyeOffIcon
|
||||
export const FileIcon = _FileIcon
|
||||
export const FileArchiveIcon = _FileArchiveIcon
|
||||
export const FileTextIcon = _FileTextIcon
|
||||
export const FilterIcon = _FilterIcon
|
||||
export const FilterXIcon = _FilterXIcon
|
||||
@@ -341,12 +370,14 @@ export const MoonIcon = _MoonIcon
|
||||
export const MoreHorizontalIcon = _MoreHorizontalIcon
|
||||
export const MoreVerticalIcon = _MoreVerticalIcon
|
||||
export const NewspaperIcon = _NewspaperIcon
|
||||
export const NoSignalIcon = _NoSignalIcon
|
||||
export const OmorphiaIcon = _OmorphiaIcon
|
||||
export const OrganizationIcon = _OrganizationIcon
|
||||
export const PackageIcon = _PackageIcon
|
||||
export const PackageOpenIcon = _PackageOpenIcon
|
||||
export const PackageClosedIcon = _PackageClosedIcon
|
||||
export const PaintBrushIcon = _PaintBrushIcon
|
||||
export const PickaxeIcon = _PickaxeIcon
|
||||
export const PlayIcon = _PlayIcon
|
||||
export const PlugIcon = _PlugIcon
|
||||
export const PlusIcon = _PlusIcon
|
||||
@@ -368,6 +399,8 @@ export const ServerIcon = _ServerIcon
|
||||
export const SettingsIcon = _SettingsIcon
|
||||
export const ShareIcon = _ShareIcon
|
||||
export const ShieldIcon = _ShieldIcon
|
||||
export const SignalIcon = _SignalIcon
|
||||
export const SkullIcon = _SkullIcon
|
||||
export const SlashIcon = _SlashIcon
|
||||
export const SortAscendingIcon = _SortAscendingIcon
|
||||
export const SortDescendingIcon = _SortDescendingIcon
|
||||
@@ -397,6 +430,7 @@ export const UsersIcon = _UsersIcon
|
||||
export const VersionIcon = _VersionIcon
|
||||
export const WikiIcon = _WikiIcon
|
||||
export const WindowIcon = _WindowIcon
|
||||
export const WorldIcon = _WorldIcon
|
||||
export const WrenchIcon = _WrenchIcon
|
||||
export const XIcon = _XIcon
|
||||
export const XCircleIcon = _XCircleIcon
|
||||
|
||||
@@ -59,7 +59,7 @@ textarea,
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 40px;
|
||||
min-height: 36px;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
@@ -159,7 +159,7 @@ input[type='number'] {
|
||||
@extend .transparent, .icon-only;
|
||||
|
||||
position: absolute;
|
||||
right: 0.25rem;
|
||||
right: 0.125rem;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
--color-platform-velocity: #4b98b0;
|
||||
--color-platform-waterfall: #5f83cb;
|
||||
--color-platform-sponge: #c49528;
|
||||
|
||||
--hover-brightness: 0.9;
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -196,6 +198,8 @@ html {
|
||||
--color-platform-velocity: #83d5ef;
|
||||
--color-platform-waterfall: #78a4fb;
|
||||
--color-platform-sponge: #f9e580;
|
||||
|
||||
--hover-brightness: 1.25;
|
||||
}
|
||||
|
||||
.oled-mode {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "daedalus"
|
||||
version = "0.2.3"
|
||||
authors = ["Jai A <jai@modrinth.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Utilities for querying and parsing Minecraft metadata"
|
||||
repository = "https://github.com/modrinth/daedalus/"
|
||||
@@ -14,7 +14,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
thiserror = "1.0"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './src/components/index'
|
||||
export { commonMessages, commonSettingsMessages } from './src/utils/common-messages'
|
||||
export * from './src/utils/search'
|
||||
export * from './src/components'
|
||||
export * from './src/utils'
|
||||
export * from './src/composables'
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.4.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "4.3.0",
|
||||
"typescript": "^5.4.5"
|
||||
"vue-router": "4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.3.2",
|
||||
@@ -29,6 +29,7 @@
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@vintl/how-ago": "^3.0.1",
|
||||
"apexcharts": "^3.44.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed } from 'vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||
size?: 'standard' | 'large'
|
||||
size?: 'standard' | 'large' | 'small'
|
||||
circular?: boolean
|
||||
type?: 'standard' | 'outlined' | 'transparent' | 'highlight' | 'highlight-colored-text'
|
||||
colorFill?: 'auto' | 'background' | 'text' | 'none'
|
||||
@@ -67,6 +67,8 @@ const colorVar = computed(() => {
|
||||
const height = computed(() => {
|
||||
if (props.size === 'large') {
|
||||
return '3rem'
|
||||
} else if (props.size === 'small') {
|
||||
return '1.5rem'
|
||||
}
|
||||
return '2.25rem'
|
||||
})
|
||||
@@ -74,6 +76,8 @@ const height = computed(() => {
|
||||
const width = computed(() => {
|
||||
if (props.size === 'large') {
|
||||
return props.circular ? '3rem' : 'auto'
|
||||
} else if (props.size === 'small') {
|
||||
return props.circular ? '1.5rem' : 'auto'
|
||||
}
|
||||
return props.circular ? '2.25rem' : 'auto'
|
||||
})
|
||||
@@ -82,6 +86,8 @@ const paddingX = computed(() => {
|
||||
let padding = props.circular ? '0.5rem' : '0.75rem'
|
||||
if (props.size === 'large') {
|
||||
padding = props.circular ? '0.75rem' : '1rem'
|
||||
} else if (props.size === 'small') {
|
||||
padding = props.circular ? '0.125rem' : '0.5rem'
|
||||
}
|
||||
return `calc(${padding} - 0.125rem)`
|
||||
})
|
||||
@@ -96,6 +102,8 @@ const paddingY = computed(() => {
|
||||
const gap = computed(() => {
|
||||
if (props.size === 'large') {
|
||||
return '0.5rem'
|
||||
} else if (props.size === 'small') {
|
||||
return '0.25rem'
|
||||
}
|
||||
return '0.375rem'
|
||||
})
|
||||
@@ -114,6 +122,8 @@ const radius = computed(() => {
|
||||
|
||||
if (props.size === 'large') {
|
||||
return '1rem'
|
||||
} else if (props.size === 'small') {
|
||||
return '0.5rem'
|
||||
}
|
||||
return '0.75rem'
|
||||
})
|
||||
@@ -121,6 +131,8 @@ const radius = computed(() => {
|
||||
const iconSize = computed(() => {
|
||||
if (props.size === 'large') {
|
||||
return '1.5rem'
|
||||
} else if (props.size === 'small') {
|
||||
return '1rem'
|
||||
}
|
||||
return '1.25rem'
|
||||
})
|
||||
@@ -192,12 +204,19 @@ const colorVariables = computed(() => {
|
||||
|
||||
return `--_bg: ${colors.bg}; --_text: ${colors.text}; --_hover-bg: ${hoverColors.bg}; --_hover-text: ${hoverColors.text};`
|
||||
})
|
||||
|
||||
const fontSize = computed(() => {
|
||||
if (props.size === 'small') {
|
||||
return 'text-sm'
|
||||
}
|
||||
return 'text-base'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="btn-wrapper"
|
||||
:class="{ outline: type === 'outlined' }"
|
||||
:class="[{ outline: type === 'outlined' }, fontSize]"
|
||||
:style="`${colorVariables}--_height:${height};--_width:${width};--_radius: ${radius};--_padding-x:${paddingX};--_padding-y:${paddingY};--_gap:${gap};--_font-weight:${fontWeight};--_icon-size:${iconSize};`"
|
||||
>
|
||||
<slot />
|
||||
@@ -245,7 +264,7 @@ const colorVariables = computed(() => {
|
||||
}
|
||||
|
||||
&:not([disabled]):not([disabled='true']):not(.disabled) {
|
||||
@apply active:scale-95 hover:brightness-125 focus-visible:brightness-125 hover:bg-[--_hover-bg] hover:text-[--_hover-text] focus-visible:bg-[--_hover-bg] focus-visible:text-[--_hover-text];
|
||||
@apply active:scale-95 hover:brightness-[--hover-brightness] focus-visible:brightness-[--hover-brightness] hover:bg-[--_hover-bg] hover:text-[--_hover-text] focus-visible:bg-[--_hover-bg] focus-visible:text-[--_hover-text];
|
||||
|
||||
&:hover svg:first-child,
|
||||
&:focus-visible svg:first-child {
|
||||
|
||||
56
packages/ui/src/components/base/FilterBar.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="(showAllOptions && options.length > 0) || options.length > 1"
|
||||
class="flex flex-wrap gap-1 items-center"
|
||||
>
|
||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||
<button
|
||||
v-for="filter in options"
|
||||
:key="`filter-${filter.id}`"
|
||||
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||
@click="toggleFilter(filter.id)"
|
||||
>
|
||||
{{ formatMessage(filter.message) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FilterIcon } from '@modrinth/assets'
|
||||
import { watch } from 'vue'
|
||||
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
export type FilterBarOption = {
|
||||
id: string
|
||||
message: MessageDescriptor
|
||||
}
|
||||
|
||||
const selectedFilters = defineModel<string[]>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
options: FilterBarOption[]
|
||||
showAllOptions?: boolean
|
||||
}>()
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
() => {
|
||||
for (let i = 0; i < selectedFilters.value.length; i++) {
|
||||
const option = selectedFilters.value[i]
|
||||
if (!props.options.some((x) => x.id === option)) {
|
||||
selectedFilters.value.splice(i, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function toggleFilter(option: string) {
|
||||
if (selectedFilters.value.includes(option)) {
|
||||
selectedFilters.value.splice(selectedFilters.value.indexOf(option), 1)
|
||||
} else {
|
||||
selectedFilters.value.push(option)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
20
packages/ui/src/components/base/HeadingLink.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<AutoLink
|
||||
:to="to"
|
||||
class="flex mb-3 leading-none items-center gap-1 text-primary text-lg font-bold hover:underline group w-fit"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRightIcon
|
||||
class="h-5 w-5 stroke-[3px] group-hover:translate-x-1 transition-transform group-hover:text-brand"
|
||||
/>
|
||||
</AutoLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AutoLink from './AutoLink.vue'
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
defineProps<{
|
||||
to: unknown
|
||||
}>()
|
||||
</script>
|
||||