├── .gitattributes ├── daemon ├── Makefile ├── pkg │ ├── stardewtypes │ │ └── sdate.go │ ├── logging │ │ ├── fields.go │ │ └── logger.go │ ├── refutil │ │ └── refutil.go │ ├── functional │ │ └── slice.go │ └── envutil │ │ └── envutil.go ├── internal │ ├── util │ │ └── util.go │ ├── backup │ │ ├── backup_controller.go │ │ └── backup_service.go │ ├── zips │ │ └── zip.go │ └── startup │ │ ├── startup_controller.go │ │ └── startup_service.go ├── buf.gen.yaml ├── go.mod ├── cmd │ └── daemon │ │ └── daemon.go └── gen │ └── proto │ └── go │ └── junimohost │ ├── customers │ └── v1 │ │ └── customers_grpc.pb.go │ ├── admin │ └── v1 │ │ ├── admin_grpc.pb.go │ │ └── admin.pb.go │ ├── gamemanager │ └── v1 │ │ ├── gamemanager_grpc.pb.go │ │ └── gamemanager.pb.go │ ├── billing │ └── v1 │ │ └── billing_grpc.pb.go │ └── stardewdaemon │ └── v1 │ └── stardewdaemon_grpc.pb.go ├── docker ├── config.user.json ├── docker-entrypoint.sh └── Dockerfile ├── mod ├── README.md ├── JunimoServer │ ├── Services │ │ ├── GameLoader │ │ │ ├── GameLoaderSaveData.cs │ │ │ └── GameLoaderService.cs │ │ ├── HostAutomation │ │ │ ├── AutomationUtil.cs │ │ │ ├── Activities │ │ │ │ ├── FestivalStartActivity.cs │ │ │ │ ├── HideHostActivity.cs │ │ │ │ └── MatchFarmhouseToOwnerCabinLevelActivity.cs │ │ │ ├── ActivityList.cs │ │ │ ├── HostBot.cs │ │ │ └── Activity.cs │ │ ├── ChatCommands │ │ │ ├── ChatCommandAPI.cs │ │ │ ├── ChatCommand.cs │ │ │ ├── ReceivedMessage.cs │ │ │ ├── ChatWatcher.cs │ │ │ └── ChatCommands.cs │ │ ├── PersistentOption │ │ │ ├── PersistentOptionsSaveData.cs │ │ │ └── PersistentOptions.cs │ │ ├── GameTweaks │ │ │ └── GameTweaker.cs │ │ ├── CropSaver │ │ │ ├── TerrainFeatureExtensions.cs │ │ │ ├── CropSaverOverrides.cs │ │ │ ├── FarmerUtil.cs │ │ │ ├── CropSaverDataLoader.cs │ │ │ ├── CropWatcher.cs │ │ │ ├── CropSaverData.cs │ │ │ └── CropSaver.cs │ │ ├── ServerOptim │ │ │ ├── NullDisplayDevice.cs │ │ │ ├── ServerOptimizerOverrides.cs │ │ │ └── ServerOptimizer.cs │ │ ├── GalaxyAuth │ │ │ ├── GalaxyAuthService.cs │ │ │ └── GalaxyAuthOverrides.cs │ │ ├── GameCreator │ │ │ ├── NewGameConfig.cs │ │ │ └── GameCreatorService.cs │ │ ├── DebrisOptim │ │ │ ├── DebrisOptimizer.cs │ │ │ └── DebrisOptimizerOverrides.cs │ │ ├── Commands │ │ │ ├── ListAdminsCommand.cs │ │ │ ├── ListBansCommand.cs │ │ │ ├── ChangeWalletCommand.cs │ │ │ ├── ConsoleCommand.cs │ │ │ ├── JojaCommand.cs │ │ │ ├── BanCommand.cs │ │ │ ├── KickCommand.cs │ │ │ ├── UnbanCommand.cs │ │ │ ├── RoleCommands.cs │ │ │ └── CabinCommand.cs │ │ ├── AlwaysOnServer │ │ │ └── AlwaysOnConfig.cs │ │ ├── Backup │ │ │ ├── BackupService.cs │ │ │ └── BackupScheduler.cs │ │ ├── NetworkTweaks │ │ │ ├── NetworkTweaker.cs │ │ │ └── DesyncKicker.cs │ │ ├── Daemon │ │ │ └── DaemonService.cs │ │ ├── Roles │ │ │ └── RoleService.cs │ │ └── CabinManager │ │ │ └── CabinManagerService.cs │ ├── Makefile │ ├── manifest.json │ ├── buf.gen.yaml │ ├── Util │ │ ├── SDateExtensions.cs │ │ ├── ModHelperExtensions.cs │ │ └── SDateHelper.cs │ └── JunimoServer.csproj ├── JunimoMods.sln.DotSettings ├── JunimoMods.sln ├── .gitattributes └── .gitignore ├── .gitignore ├── docker-compose.yml ├── LICENSE ├── README.md ├── Makefile └── .github └── workflows └── build.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /daemon/Makefile: -------------------------------------------------------------------------------- 1 | generate_protos: 2 | buf generate https://github.com/JunimoHost/junimo-api.git 3 | -------------------------------------------------------------------------------- /docker/config.user.json: -------------------------------------------------------------------------------- 1 | { 2 | "CheckForUpdates": false, 3 | "ListenForConsoleInput": false 4 | } -------------------------------------------------------------------------------- /mod/README.md: -------------------------------------------------------------------------------- 1 | # Junimo Mods 2 | 3 | Stardew mods for JunimoHost 4 | 5 | ## Junimo Server 6 | 7 | GRPC API & Game Manager 8 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GameLoader/GameLoaderSaveData.cs: -------------------------------------------------------------------------------- 1 | namespace JunimoServer.Services.GameLoader 2 | { 3 | class GameLoaderSaveData 4 | { 5 | public string SaveNameToLoad { get; set; } = null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | node_modules 3 | gcp-key.json 4 | stardew-sa.json 5 | testmods 6 | bin 7 | .env 8 | .env.production 9 | cmd/daemon/__debug_bin.exe 10 | .idea 11 | docker/game-daemon 12 | docker/mods 13 | .DS_Store 14 | build -------------------------------------------------------------------------------- /mod/JunimoServer/Makefile: -------------------------------------------------------------------------------- 1 | mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) 2 | current_dir := $(dir $(mkfile_path)) 3 | 4 | generate_protos: 5 | buf generate https://github.com/JunimoHost/junimo-api.git --path junimohost/stardewsteamauth --path junimohost/stardewgame -------------------------------------------------------------------------------- /mod/JunimoServer/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "JunimoServer", 3 | "Author": "JunimoHost", 4 | "Version": "0.15.0", 5 | "Description": "JunimoHost's remote game manager", 6 | "UniqueID": "JunimoHost.Server", 7 | "EntryDll": "JunimoServer.dll", 8 | "MinimumApiVersion": "3.0.0", 9 | "UpdateKeys": [] 10 | } -------------------------------------------------------------------------------- /mod/JunimoServer/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | plugins: 5 | # remote urls are links to list of versions 6 | - remote: buf.build/protocolbuffers/plugins/csharp:v21.5.0-1 7 | out: gen/proto/csharp 8 | - remote: buf.build/grpc/plugins/csharp:v1.48.1-1 9 | out: gen/proto/csharp 10 | -------------------------------------------------------------------------------- /daemon/pkg/stardewtypes/sdate.go: -------------------------------------------------------------------------------- 1 | package stardewtypes 2 | 3 | type Season int 4 | 5 | const ( 6 | SPRING Season = 0 7 | SUMMER Season = 1 8 | FALL Season = 2 9 | WINTER Season = 3 10 | ) 11 | 12 | type SDate struct { 13 | Day int `json:"Day"` 14 | Season Season `json:"Season"` 15 | Year int `json:"Year"` 16 | } 17 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/AutomationUtil.cs: -------------------------------------------------------------------------------- 1 | using StardewValley; 2 | 3 | namespace JunimoServer.Services.HostAutomation 4 | { 5 | public static class AutomationUtil 6 | { 7 | public static void WarpToHidingSpot() 8 | { 9 | Game1.warpFarmer("Farm", 64, 15, false); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ChatCommands/ChatCommandAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JunimoServer.Services.ChatCommands 4 | { 5 | public interface IChatCommandApi 6 | { 7 | public void RegisterCommand(string name, string description, Action action); 8 | public void RegisterCommand(ChatCommand command); 9 | } 10 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/PersistentOption/PersistentOptionsSaveData.cs: -------------------------------------------------------------------------------- 1 | using JunimoServer.Services.CabinManager; 2 | 3 | namespace JunimoServer.Services.PersistentOption 4 | { 5 | public class PersistentOptionsSaveData 6 | { 7 | public int MaxPlayers { get; set; } = 6; 8 | 9 | public CabinStrategy CabinStrategy { get; set; } = CabinStrategy.CabinStack; 10 | } 11 | } -------------------------------------------------------------------------------- /daemon/pkg/logging/fields.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import "go.uber.org/zap" 4 | 5 | func ServerID(serverID string) zap.Field { 6 | return zap.String("serverID", serverID) 7 | } 8 | 9 | func SubscriptionID(subscriptionID string) zap.Field { 10 | return zap.String("subscriptionID", subscriptionID) 11 | } 12 | 13 | func WorkflowID(workflowID string) zap.Field { 14 | return zap.String("workflowID", workflowID) 15 | } 16 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export HOME=/root 3 | 4 | /opt/game-daemon & 5 | 6 | # wait until game-daemon says its ok to start stardew 7 | while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080/startup)" != "200" ]]; do sleep 5; done 8 | 9 | tail -F /config/xdg/config/StardewValley/ErrorLogs/SMAPI-latest.txt & 10 | 11 | export XAUTHORITY=~/.Xauthority 12 | HOME=/root pulseaudio -vvv --daemonize 13 | 14 | bash -c "/data/Stardew/Stardew\ Valley/StardewValley" 15 | -------------------------------------------------------------------------------- /daemon/internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func GetBucketAndObjectFromGcsPath(path string) (string, string) { 8 | pathWoGS := strings.Split(path, "//")[1] // junimo-backups/078338ba-6465-43a4-a045-4a6f7df674fc/1648081818 9 | pathParts := strings.Split(pathWoGS, "/") // [junimo-backups, 078338ba-6465-43a4-a045-4a6f7df674fc, 1648081818] 10 | bucketName := pathParts[0] 11 | objectName := strings.Join(pathParts[1:], "/") 12 | return bucketName, objectName 13 | } 14 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/Activities/FestivalStartActivity.cs: -------------------------------------------------------------------------------- 1 | namespace JunimoServer.Services.HostAutomation.Activities 2 | { 3 | public class FestivalStartActivity: Activity 4 | { 5 | 6 | protected override void OnTick(int tickNum) 7 | { 8 | // Utility.isFestivalDay(1,"Fall"); 9 | // 10 | // var data = Game1.temporaryContent.Load("Data\\Festivals"); 11 | // Utility.isFestivalDay(1,"Fall"); 12 | 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /daemon/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | go_package_prefix: 5 | default: github.com/junimohost/game-daemon/gen/proto/go 6 | except: 7 | - buf.build/googleapis/googleapis 8 | plugins: 9 | # remote urls are links to list of versions 10 | - remote: buf.build/protocolbuffers/plugins/go:v1.28.1-1 11 | out: gen/proto/go 12 | opt: 13 | - paths=source_relative 14 | - remote: buf.build/grpc/plugins/go:v1.2.0-1 15 | out: gen/proto/go 16 | opt: 17 | - paths=source_relative 18 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ChatCommands/ChatCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JunimoServer.Services.ChatCommands 4 | { 5 | public class ChatCommand 6 | { 7 | public string Name; 8 | public string Description; 9 | public Action Action; 10 | 11 | public ChatCommand(string name, string description, Action action) 12 | { 13 | Name = name; 14 | Description = description; 15 | Action = action; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Util/SDateExtensions.cs: -------------------------------------------------------------------------------- 1 | using StardewModdingAPI.Utilities; 2 | 3 | namespace JunimoServer.Util 4 | { 5 | public static class SDateExtensions 6 | { 7 | public static bool EqualsIgnoreYear(this SDate date, SDate otherDate) 8 | { 9 | return date.Day == otherDate.Day && date.Season == otherDate.Season; 10 | } 11 | 12 | public static bool IsDayZero(this SDate date) 13 | { 14 | return date.Day == 0 && date.Season == "spring" && date.Year == 1; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GameTweaks/GameTweaker.cs: -------------------------------------------------------------------------------- 1 | using StardewModdingAPI; 2 | using StardewModdingAPI.Events; 3 | using StardewValley; 4 | 5 | namespace JunimoServer.Services.GameTweaks 6 | { 7 | public class GameTweaker 8 | { 9 | public GameTweaker(IModHelper helper) 10 | { 11 | helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; 12 | } 13 | 14 | private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) 15 | { 16 | Game1.options.setMoveBuildingPermissions("on"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ChatCommands/ReceivedMessage.cs: -------------------------------------------------------------------------------- 1 | using StardewValley; 2 | 3 | namespace JunimoServer.Services.ChatCommands 4 | { 5 | public class ReceivedMessage 6 | { 7 | public enum ChatKinds 8 | { 9 | ChatMessage, 10 | ErrorMessage, 11 | UserNotification, 12 | PrivateMessage 13 | } 14 | 15 | public long SourceFarmer { get; set; } 16 | public ChatKinds ChatKind { get; set; } 17 | public LocalizedContentManager.LanguageCode Language { get; set; } 18 | public string Message { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/Activities/HideHostActivity.cs: -------------------------------------------------------------------------------- 1 | using StardewValley; 2 | 3 | namespace JunimoServer.Services.HostAutomation.Activities 4 | { 5 | public class HideHostActivity : Activity 6 | { 7 | protected override void OnDayStart() 8 | { 9 | AutomationUtil.WarpToHidingSpot(); 10 | } 11 | 12 | protected override void OnTick(int tickNum) 13 | { 14 | Game1.displayFarmer = false; 15 | } 16 | 17 | protected override void OnDisabled() 18 | { 19 | Game1.displayFarmer = true; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /daemon/pkg/refutil/refutil.go: -------------------------------------------------------------------------------- 1 | package refutil 2 | 3 | // From https://stackoverflow.com/a/55321839 4 | 5 | import "time" 6 | 7 | func Bool(i bool) *bool { 8 | return &i 9 | } 10 | 11 | func Int(i int) *int { 12 | return &i 13 | } 14 | 15 | func Int32(i int32) *int32 { 16 | return &i 17 | } 18 | 19 | func Int64(i int64) *int64 { 20 | return &i 21 | } 22 | 23 | func String(i string) *string { 24 | return &i 25 | } 26 | 27 | func Duration(i time.Duration) *time.Duration { 28 | return &i 29 | } 30 | 31 | func Strings(ss []string) []*string { 32 | r := make([]*string, len(ss)) 33 | for i := range ss { 34 | r[i] = &ss[i] 35 | } 36 | return r 37 | } 38 | -------------------------------------------------------------------------------- /mod/JunimoMods.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True 4 | True 5 | True -------------------------------------------------------------------------------- /daemon/pkg/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | // CreateLogger - Create a new logger instance 12 | func CreateLogger() *zap.SugaredLogger { 13 | var config zap.Config 14 | // Setup Logging 15 | if os.Getenv("PRODUCTION") != "" { 16 | config = zap.NewProductionConfig() 17 | } else { 18 | config = zap.NewDevelopmentConfig() 19 | } 20 | 21 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 22 | loggerMgr, err := config.Build() 23 | if err != nil { 24 | log.Fatalf("Couldn't start zap logger: %v", err) 25 | } 26 | 27 | logger := loggerMgr.Sugar() 28 | 29 | return logger 30 | } 31 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/TerrainFeatureExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using StardewValley; 3 | using StardewValley.TerrainFeatures; 4 | 5 | namespace JunimoServer.Services.CropSaver 6 | { 7 | public static class TerrainFeatureExtensions 8 | { 9 | public static bool ContainsCrop(this TerrainFeature feature) 10 | { 11 | return feature is HoeDirt dirt && dirt.crop != null; 12 | } 13 | 14 | public static Crop? TryGetCrop(this TerrainFeature feature) 15 | { 16 | if (feature is HoeDirt dirt && dirt.crop != null) 17 | { 18 | return dirt.crop; 19 | } 20 | else 21 | { 22 | return null; 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /daemon/pkg/functional/slice.go: -------------------------------------------------------------------------------- 1 | package functional 2 | 3 | func MapSlice[T any, M any](target []T, f func(T) M) []M { 4 | n := make([]M, len(target)) 5 | for i, val := range target { 6 | n[i] = f(val) 7 | } 8 | return n 9 | } 10 | 11 | func FlatMapSlice[T any, M any](target []T, f func(T) []M) []M { 12 | var n []M 13 | for _, val := range target { 14 | for _, item := range f(val) { 15 | n = append(n, item) 16 | } 17 | } 18 | return n 19 | } 20 | 21 | func Filter[T any](slice []T, f func(T) bool) []T { 22 | var n []T 23 | for _, e := range slice { 24 | if f(e) { 25 | n = append(n, e) 26 | } 27 | } 28 | return n 29 | } 30 | 31 | func Find[T any](slice []T, f func(T) bool) T { 32 | var result T 33 | for _, e := range slice { 34 | if f(e) { 35 | return e 36 | } 37 | } 38 | return result 39 | } 40 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/Activities/MatchFarmhouseToOwnerCabinLevelActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using StardewValley; 3 | using StardewValley.Locations; 4 | 5 | namespace JunimoServer.Services.HostAutomation.Activities 6 | { 7 | public class MatchFarmhouseToOwnerCabinLevelActivity : Activity 8 | { 9 | public MatchFarmhouseToOwnerCabinLevelActivity() : base(60) 10 | { 11 | } 12 | 13 | protected override void OnTick(int tickNum) 14 | { 15 | var owner = ((Cabin)Game1.getFarm().buildings.First(building => building.isCabin).indoors.Value).owner; 16 | if (owner.HouseUpgradeLevel != Game1.player.HouseUpgradeLevel) 17 | { 18 | Game1.player.HouseUpgradeLevel = owner.HouseUpgradeLevel; 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ServerOptim/NullDisplayDevice.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Graphics; 2 | using xTile.Dimensions; 3 | using xTile.Display; 4 | using xTile.Tiles; 5 | 6 | namespace JunimoServer.Services.ServerOptim 7 | { 8 | public class NullDisplayDevice : IDisplayDevice 9 | { 10 | 11 | public void LoadTileSheet(TileSheet tileSheet) 12 | { 13 | } 14 | public void DisposeTileSheet(TileSheet tileSheet) 15 | { 16 | } 17 | public void BeginScene(SpriteBatch b) 18 | { 19 | } 20 | public void SetClippingRegion(Rectangle clippingRegion) 21 | { 22 | } 23 | public void DrawTile(Tile tile, Location location, float layerDepth) 24 | { 25 | } 26 | public void EndScene() 27 | { 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | stardew: 5 | build: docker 6 | container_name: stardew 7 | environment: 8 | - "DISPLAY_HEIGHT=800" 9 | - "DISPLAY_WIDTH=1280" 10 | - "SERVER_ID=docker-test" 11 | - "BACKUP_GCS_BUCKET=junimo-test-backups" 12 | - "GOOGLE_APPLICATION_CREDENTIALS=/etc/gcp/stardew-sa.json" 13 | - "NO_BACKEND=true" 14 | - "FORCE_NEW_DEBUG_GAME=true" 15 | 16 | # cpus: 0.4 17 | # mem_limit: 1Gi 18 | 19 | ports: 20 | # # VNC 21 | # - 5902:5900 22 | # NOVNC WEB 23 | - 8090:5800 24 | # Game 25 | - 24643:24642/udp 26 | - 8082:8081 27 | - 8083:8080 28 | 29 | volumes: 30 | # - ./data/mods:/data/Stardew/Stardew Valley/Mods 31 | - ./data:/config/xdg/config/StardewValley 32 | - ./stardew-sa.json:/etc/gcp/stardew-sa.json 33 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GalaxyAuth/GalaxyAuthService.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Junimohost.Stardewsteamauth.V1; 3 | using StardewModdingAPI; 4 | using StardewValley.SDKs; 5 | 6 | namespace JunimoServer.Services.GalaxyAuth 7 | { 8 | public class GalaxyAuthService 9 | { 10 | public GalaxyAuthService(IMonitor monitor, IModHelper helper, Harmony harmony, StardewSteamAuthService.StardewSteamAuthServiceClient steamAuthClient) 11 | { 12 | GalaxyAuthOverrides.Initialize(monitor, helper, steamAuthClient); 13 | harmony.Patch( 14 | original: AccessTools.Method(typeof(SteamHelper), nameof(SteamHelper.Initialize)), 15 | prefix: new HarmonyMethod(typeof(GalaxyAuthOverrides), 16 | nameof(GalaxyAuthOverrides.SteamHelperInitialize_Prefix) 17 | )); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/PersistentOption/PersistentOptions.cs: -------------------------------------------------------------------------------- 1 | using StardewModdingAPI; 2 | 3 | namespace JunimoServer.Services.PersistentOption 4 | { 5 | public class PersistentOptions 6 | { 7 | private const string SaveKey = "JunimoHost.PersistentOptions"; 8 | 9 | private readonly IModHelper _helper; 10 | public PersistentOptionsSaveData Data { get; private set; } 11 | 12 | public PersistentOptions(IModHelper helper) 13 | { 14 | _helper = helper; 15 | Data = helper.Data.ReadGlobalData(SaveKey) ?? new PersistentOptionsSaveData(); 16 | } 17 | 18 | public void SetPersistentOptions(PersistentOptionsSaveData optionsSaveData) 19 | { 20 | _helper.Data.WriteGlobalData(SaveKey, optionsSaveData); 21 | Data = optionsSaveData; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ChatCommands/ChatWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StardewValley; 3 | 4 | namespace JunimoServer.Services.ChatCommands 5 | { 6 | public static class ChatWatcher 7 | { 8 | private static Action _onChatMessage; 9 | 10 | public static void Initialize(Action onChatMessage) 11 | 12 | { 13 | _onChatMessage = onChatMessage; 14 | } 15 | 16 | public static void receiveChatMessage_Postfix(long sourceFarmer, int chatKind, 17 | LocalizedContentManager.LanguageCode language, string message) 18 | { 19 | var msg = new ReceivedMessage 20 | { 21 | SourceFarmer = sourceFarmer, 22 | ChatKind = (ReceivedMessage.ChatKinds) chatKind, 23 | Language = language, 24 | Message = message 25 | }; 26 | _onChatMessage(msg); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/ActivityList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace JunimoServer.Services.HostAutomation 4 | { 5 | public class ActivityList : List 6 | { 7 | public void EnableAll() 8 | { 9 | foreach (var activity in this) 10 | { 11 | activity.Enable(); 12 | } 13 | } 14 | 15 | public void DisableAll() 16 | { 17 | foreach (var activity in this) 18 | { 19 | activity.Disable(); 20 | } 21 | } 22 | 23 | public void TickAll() 24 | { 25 | foreach (var activity in this) 26 | { 27 | activity.HandleTick(); 28 | } 29 | } 30 | 31 | public void DayStartAll() 32 | { 33 | foreach (var activity in this) 34 | { 35 | activity.HandleDayStart(); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GameCreator/NewGameConfig.cs: -------------------------------------------------------------------------------- 1 | using JunimoServer.Services.CabinManager; 2 | 3 | namespace JunimoServer.Services.GameCreator 4 | { 5 | public class NewGameConfig 6 | { 7 | public int WhichFarm { get; set; } = 0; 8 | public bool UseSeparateWallets { get; set; } = true; 9 | public int StartingCabins { get; set; } = 1; 10 | public bool CatPerson { get; set; } = false; 11 | public string FarmName { get; set; } = "Junimo"; 12 | 13 | public int MaxPlayers { get; set; } = 4; 14 | 15 | public int CabinStrategy { get; set; } = (int)JunimoServer.Services.CabinManager.CabinStrategy.CabinStack; 16 | 17 | public override string ToString() 18 | { 19 | return 20 | $"{nameof(WhichFarm)}: {WhichFarm}, {nameof(UseSeparateWallets)}: {UseSeparateWallets}, {nameof(StartingCabins)}: {StartingCabins}, {nameof(CatPerson)}: {CatPerson}, {nameof(FarmName)}: {FarmName}, {nameof(MaxPlayers)}: {MaxPlayers}"; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 junimohost 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /daemon/pkg/envutil/envutil.go: -------------------------------------------------------------------------------- 1 | package envutil 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func GetInt64EnvOrFail(envKey string) int64 { 10 | value := GetEnvOrFail(envKey) 11 | intValue, err := strconv.Atoi(value) 12 | if err != nil { 13 | zap.S().Fatalf(envKey + " env variable not an int!") 14 | } 15 | return int64(intValue) 16 | } 17 | 18 | func GetIntEnvOrFail(envKey string) int { 19 | value := GetEnvOrFail(envKey) 20 | intValue, err := strconv.Atoi(value) 21 | if err != nil { 22 | zap.S().Fatalf(envKey + " env variable not an int!") 23 | } 24 | return intValue 25 | } 26 | 27 | func GetEnvOrFail(envKey string) string { 28 | 29 | value := os.Getenv(envKey) 30 | if value == "" { 31 | zap.S().Fatalf(envKey + " env variable not set!") 32 | } 33 | 34 | return value 35 | } 36 | 37 | func GetEnvOrDefault(envKey, defaultVal string) string { 38 | 39 | value := os.Getenv(envKey) 40 | if value == "" { 41 | zap.S().Infof("%v env variable not set defaulting to %v", envKey, defaultVal) 42 | return defaultVal 43 | } 44 | 45 | return value 46 | } 47 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/CropSaverOverrides.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using StardewModdingAPI; 3 | using StardewValley; 4 | 5 | namespace JunimoServer.Services.CropSaver 6 | { 7 | public class CropSaverOverrides 8 | { 9 | private static IModHelper _helper; 10 | private static IMonitor _monitor; 11 | private static CropSaverDataLoader _cropSaverDataLoader; 12 | 13 | public static void Initialize(IModHelper helper, IMonitor monitor, CropSaverDataLoader cropSaverDataLoader) 14 | { 15 | _helper = helper; 16 | _monitor = monitor; 17 | _cropSaverDataLoader = cropSaverDataLoader; 18 | } 19 | 20 | public static bool KillCrop_Prefix(ref Crop __instance) 21 | { 22 | var cropLocation = _helper.Reflection.GetField(__instance, "tilePosition").GetValue(); 23 | 24 | var townieCrop = _cropSaverDataLoader.GetSaverCrop("Farm", cropLocation); 25 | 26 | if (townieCrop != null) return false; 27 | 28 | return true; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/FarmerUtil.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using StardewValley; 3 | 4 | namespace JunimoServer.Services.CropSaver 5 | { 6 | public static class FarmerUtil 7 | { 8 | public static Farmer GetClosestFarmer(GameLocation location, Vector2 tileLocation) 9 | { 10 | var farmers = Game1.getOnlineFarmers(); 11 | var closestFarmer = Game1.player; // assign ownership of crop to host as fallback (should only be the case if crop planting was automated) 12 | var closestDistance = float.MaxValue; 13 | foreach (var farmer in farmers) 14 | { 15 | if (!farmer.currentLocation.Equals(location)) continue; 16 | var farmerDistance = Vector2.Distance(farmer.getTileLocation(), tileLocation); 17 | if (farmerDistance < closestDistance) 18 | { 19 | closestFarmer = farmer; 20 | closestDistance = farmerDistance; 21 | } 22 | } 23 | 24 | return closestFarmer; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /mod/JunimoServer/JunimoServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | JunimoServer 4 | JunimoServer 5 | 1.0.0 6 | net5.0 7 | true 8 | 9 | All 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | $(GamePath)\GalaxyCSharp.dll 22 | 23 | 24 | $(GamePath)\Steamworks.NET.dll 25 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JunimoHost.com Stardew Dedicated Server 2 | 3 | This repository contains everything needed to build a docker container running headless Stardew Valley. 4 | 5 | ## Usage 6 | 7 | Game config is pulled from the backend to the daemon before the game starts. The game mod retrieves its config from the daemon. There are default options provided in the case the backend service is not available. Use docker-compose to run the container. 8 | 9 | ## Compilation 10 | 11 | 12 | ### Requirements 13 | All 14 | - make 15 | - docker 16 | 17 | Daemon 18 | - go 1.19 19 | 20 | Mod 21 | - .NET 5+ sdk 22 | - Installed SMAPI & Stardew 23 | 24 | ### Command 25 | run ```make build``` 26 | 27 | ## Context 28 | The headless server contains three main components: 29 | - Docker 30 | - Daemon 31 | - JunimoServer SMAPI Mod 32 | 33 | ### Docker 34 | Docker files are used to build a container that headlessly runs Stardew. 35 | 36 | ### Daemon 37 | The daemon is used to manage the system the game is running on. It's responsible for creating backups, getting configs from the backend, and eventually managing the game process completely. 38 | 39 | 40 | ### JunimoServer 41 | JunimoServer is a SMAPI mod used to automate the host and add in features condusive to server play. -------------------------------------------------------------------------------- /mod/JunimoServer/Services/DebrisOptim/DebrisOptimizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using HarmonyLib; 4 | using Microsoft.VisualBasic; 5 | using StardewModdingAPI; 6 | using StardewModdingAPI.Events; 7 | using StardewValley; 8 | using StardewValley.SDKs; 9 | 10 | namespace JunimoServer.Services.DebrisOptim 11 | { 12 | public class DebrisOptimizer 13 | { 14 | private readonly IMonitor _monitor; 15 | 16 | public DebrisOptimizer( 17 | Harmony harmony, 18 | IMonitor monitor, 19 | IModHelper helper 20 | ) 21 | { 22 | _monitor = monitor; 23 | DebrisOptimizerOverrides.Initialize(monitor, helper); 24 | harmony.Patch( 25 | original: AccessTools.Method(typeof(Debris), nameof(Debris.updateChunks)), 26 | prefix: new HarmonyMethod(typeof(DebrisOptimizerOverrides), 27 | nameof(DebrisOptimizerOverrides.updateChunks_Prefix)) 28 | ); 29 | 30 | helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; 31 | } 32 | 33 | private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) 34 | { 35 | DebrisOptimizerOverrides.Update(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /mod/JunimoMods.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31424.327 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JunimoServer", "JunimoServer\JunimoServer.csproj", "{73F95CD6-9989-441A-95A2-1F4F8BD246F3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {73F95CD6-9989-441A-95A2-1F4F8BD246F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {73F95CD6-9989-441A-95A2-1F4F8BD246F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {73F95CD6-9989-441A-95A2-1F4F8BD246F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {73F95CD6-9989-441A-95A2-1F4F8BD246F3}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {73F95CD6-9989-441A-95A2-1F4F8BD246F3}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 19 | EndGlobalSection 20 | GlobalSection(SolutionProperties) = preSolution 21 | HideSolutionNode = FALSE 22 | EndGlobalSection 23 | GlobalSection(ExtensibilityGlobals) = postSolution 24 | SolutionGuid = {4C481B93-8466-4C03-A28F-D29CB3F5BF62} 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/HostBot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using JunimoServer.Services.HostAutomation.Activities; 3 | using StardewModdingAPI; 4 | using StardewModdingAPI.Events; 5 | 6 | namespace JunimoServer.Services.HostAutomation 7 | { 8 | public class HostBot 9 | { 10 | 11 | private readonly ActivityList _activities; 12 | public HostBot(IModHelper helper, IMonitor monitor) 13 | { 14 | 15 | _activities = new ActivityList 16 | { 17 | new HideHostActivity(), 18 | new MatchFarmhouseToOwnerCabinLevelActivity(), 19 | }; 20 | 21 | helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; 22 | helper.Events.GameLoop.UpdateTicked += OnTick; 23 | helper.Events.GameLoop.DayStarted += OnDayStarted; 24 | } 25 | private void OnDayStarted(object sender, DayStartedEventArgs e) 26 | { 27 | _activities.DayStartAll(); 28 | } 29 | private void OnTick(object sender, UpdateTickedEventArgs e) 30 | { 31 | _activities.TickAll(); 32 | } 33 | 34 | private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) 35 | { 36 | _activities.EnableAll(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/ListAdminsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public class ListAdminsCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("listadmins", "list bans", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | 22 | helper.SendPrivateMessage(msg.SourceFarmer, "Admins:"); 23 | 24 | foreach (var farmerId in roleService.GetAdmins()) 25 | 26 | { 27 | var farmerName = helper.GetFarmerNameById(farmerId); 28 | var userName = helper.GetFarmerUserNameById(farmerId); 29 | helper.SendPrivateMessage(msg.SourceFarmer, $"{farmerName} | {userName}"); 30 | } 31 | }); 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=v0.20.7 2 | 3 | build: docker/mods/JunimoServer $(shell find docker -type f) 4 | docker build --platform=amd64 -t gcr.io/junimo-host/stardew-base:$(VERSION) -f docker/Dockerfile . 5 | 6 | clean: 7 | rm -rf ./docker/mods/JunimoServer 8 | rm -rf ./mod/build 9 | 10 | docker/mods/JunimoServer: $(shell find mod/JunimoServer/**/*.cs -type f) ./mod/JunimoServer/JunimoServer.csproj 11 | ifeq ($(CI), true) 12 | cd mod && dotnet build -o ./build --configuration Release "/p:EnableModZip=false;EnableModDeploy=false;GamePath=/home/runner/actions-runner/_work/junimohost-stardew-server/junimohost-stardew-server/Stardew Valley" 13 | else 14 | cd mod && dotnet build -o ./build --configuration Release 15 | endif 16 | mkdir -p ./docker/mods/JunimoServer 17 | ls ./mod/build 18 | cp ./mod/build/JunimoServer.dll ./mod/build/Microsoft.Extensions.Logging.Abstractions.dll ./mod/build/Google.Protobuf.dll ./mod/build/Grpc.Core.Api.dll ./mod/build/Grpc.Net.Client.dll ./mod/build/Grpc.Net.Common.dll ./mod/JunimoServer/manifest.json ./docker/mods/JunimoServer 19 | 20 | game-daemon: $(shell find daemon -type f) 21 | GOOS=linux GOARCH=amd64 go build -o game-daemon ./cmd/daemon/daemon.go 22 | 23 | push: build 24 | docker push gcr.io/junimo-host/stardew-base:$(VERSION) 25 | 26 | daemon_windows: 27 | cd daemon && set GOOS=linux && go build -o game-daemon ./cmd/daemon/daemon.go 28 | 29 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/ListBansCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public class ListBansCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("listbans", "list bans", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | 22 | if (Game1.bannedUsers.Count == 0) 23 | { 24 | helper.SendPrivateMessage(msg.SourceFarmer, "There are 0 banned users."); 25 | return; 26 | } 27 | 28 | helper.SendPrivateMessage(msg.SourceFarmer, "Banned users:"); 29 | 30 | foreach (var (k, v) in Game1.bannedUsers) 31 | { 32 | helper.SendPrivateMessage(msg.SourceFarmer, $"{k} | {v} "); 33 | } 34 | }); 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/ChangeWalletCommand.cs: -------------------------------------------------------------------------------- 1 | using JunimoServer.Services.ChatCommands; 2 | using JunimoServer.Services.Roles; 3 | using JunimoServer.Util; 4 | using StardewModdingAPI; 5 | using StardewValley; 6 | using StardewValley.Locations; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public class ChangeWalletCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("changewallet", "Immediate toggle between shared or split money.", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | 22 | var isSeparateWallets = Game1.player.team.useSeparateWallets.Value; 23 | if (isSeparateWallets) 24 | { 25 | ManorHouse.MergeWallets(); 26 | helper.SendPublicMessage("Now using shared money."); 27 | } 28 | else 29 | { 30 | ManorHouse.SeparateWallets(); 31 | helper.SendPublicMessage("Now using separate money."); 32 | } 33 | }); 34 | } 35 | 36 | } 37 | } -------------------------------------------------------------------------------- /daemon/internal/backup/backup_controller.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/junimohost/game-daemon/pkg/stardewtypes" 7 | "go.uber.org/zap" 8 | "io/ioutil" 9 | "net/http" 10 | ) 11 | 12 | type CreateBackupRequest struct { 13 | Date stardewtypes.SDate `json:"date"` 14 | } 15 | 16 | type Controller struct { 17 | backupService *Service 18 | } 19 | 20 | func NewController(backupService *Service) *Controller { 21 | return &Controller{backupService: backupService} 22 | } 23 | 24 | func (c *Controller) AddRoutes(gin *gin.Engine) { 25 | gin.POST("/backup", c.onCreateBackup) 26 | } 27 | 28 | func (c *Controller) onCreateBackup(ctx *gin.Context) { 29 | body, err := ioutil.ReadAll(ctx.Request.Body) 30 | 31 | if err != nil { 32 | zap.S().Errorf("Error reading request body: %v", err) 33 | ctx.Status(http.StatusBadRequest) 34 | return 35 | } 36 | 37 | var createBackupRequest CreateBackupRequest 38 | err = json.Unmarshal(body, &createBackupRequest) 39 | if err != nil { 40 | zap.S().Errorf("Error parsing request body: %v", err) 41 | ctx.Status(http.StatusBadRequest) 42 | return 43 | } 44 | 45 | err = c.backupService.CreateBackup(ctx, createBackupRequest.Date) 46 | if err != nil { 47 | zap.S().Errorf("Error creating backup: %v", err) 48 | ctx.Status(http.StatusInternalServerError) 49 | return 50 | } 51 | 52 | zap.S().Infof("Created Backup") 53 | ctx.Status(http.StatusOK) 54 | } 55 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/AlwaysOnServer/AlwaysOnConfig.cs: -------------------------------------------------------------------------------- 1 | using StardewModdingAPI; 2 | 3 | namespace JunimoServer.Services.AlwaysOnServer 4 | { 5 | public class AlwaysOnConfig 6 | { 7 | public SButton ServerHotKey { get; set; } = SButton.F9; 8 | 9 | public string PetName { get; set; } = "Apples"; 10 | public bool FarmCaveChoiceIsMushrooms { get; set; } = true; 11 | public bool IsCommunityCenterRun { get; set; } = true; 12 | 13 | public bool LockPlayerChests { get; set; } = true; 14 | 15 | public int EggHuntCountDownConfig { get; set; } = 300; 16 | public int FlowerDanceCountDownConfig { get; set; } = 300; 17 | public int LuauSoupCountDownConfig { get; set; } = 300; 18 | public int JellyDanceCountDownConfig { get; set; } = 300; 19 | public int GrangeDisplayCountDownConfig { get; set; } = 300; 20 | public int IceFishingCountDownConfig { get; set; } = 300; 21 | 22 | public int EndOfDayTimeOut { get; set; } = 120000; 23 | public int FairTimeOut { get; set; } = 120000; 24 | public int SpiritsEveTimeOut { get; set; } = 120000; 25 | public int WinterStarTimeOut { get; set; } = 120000; 26 | 27 | public int EggFestivalTimeOut { get; set; } = 120000; 28 | public int FlowerDanceTimeOut { get; set; } = 120000; 29 | public int LuauTimeOut { get; set; } = 120000; 30 | public int DanceOfJelliesTimeOut { get; set; } = 120000; 31 | public int FestivalOfIceTimeOut { get; set; } = 120000; 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/ConsoleCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.AlwaysOnServer; 3 | using JunimoServer.Services.ChatCommands; 4 | using JunimoServer.Services.Roles; 5 | using JunimoServer.Util; 6 | using StardewModdingAPI; 7 | using StardewValley; 8 | 9 | namespace JunimoServer.Services.Commands 10 | { 11 | public static class ConsoleCommand 12 | { 13 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 14 | { 15 | chatCommandApi.RegisterCommand("console", 16 | "forwards the proceeding command to be run on the server.", 17 | (args, msg) => 18 | { 19 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 20 | { 21 | helper.SendPrivateMessage(msg.SourceFarmer, "Only admins can run console commands."); 22 | return; 23 | } 24 | 25 | if (args.Length < 1) 26 | { 27 | helper.SendPrivateMessage(msg.SourceFarmer, 28 | "Invalid use of command. You must provide a console command to run."); 29 | return; 30 | } 31 | 32 | var remainingArgs = args.Skip(1).ToArray(); 33 | helper.ConsoleCommands.Trigger(args[0], remainingArgs); 34 | helper.SendPublicMessage("Joja run permanently enabled!"); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/JojaCommand.cs: -------------------------------------------------------------------------------- 1 | using JunimoServer.Services.AlwaysOnServer; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public static class JojaCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService, 13 | AlwaysOnConfig alwaysOnConfig) 14 | { 15 | chatCommandApi.RegisterCommand("joja", 16 | "Type \"!joja IRREVERSIBLY_ENABLE_JOJA_RUN\" to enable joja and disable the standard community center forever.", 17 | (args, msg) => 18 | { 19 | if (!roleService.IsPlayerOwner(msg.SourceFarmer)) 20 | { 21 | helper.SendPrivateMessage(msg.SourceFarmer, "Only the owner of the server can enable Joja."); 22 | return; 23 | } 24 | 25 | if (args.Length != 1 || (args.Length == 1 && args[0] != "IRREVERSIBLY_ENABLE_JOJA_RUN")) 26 | { 27 | helper.SendPrivateMessage(msg.SourceFarmer, 28 | "Invalid use of command. You must type \"!joja IRREVERSIBLY_ENABLE_JOJA_RUN\" exactly."); 29 | return; 30 | } 31 | 32 | alwaysOnConfig.IsCommunityCenterRun = false; 33 | helper.SendPublicMessage("Joja run permanently enabled!"); 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | env: 4 | PROJECT_ID: ${{ secrets.GKE_PROJECT }} 5 | 6 | jobs: 7 | setup-build-publish: 8 | name: Setup, Build, Publish 9 | runs-on: buildjet-4vcpu-ubuntu-2204 10 | 11 | steps: 12 | - name: Download Code 13 | uses: actions/checkout@v3 14 | 15 | - name: Download Stardew 16 | uses: wei/wget@v1 17 | with: 18 | args: -O stardew.tar.gz https://storage.googleapis.com/junimo-public/SV156.tar.gz 19 | 20 | - name: Decompress Stardew 21 | run: |- 22 | tar -zxf stardew.tar.gz 23 | 24 | - name: Download SMAPI 3.18.6 25 | uses: wei/wget@v1 26 | with: 27 | args: -O smapi.zip https://github.com/Pathoschild/SMAPI/releases/download/3.18.6/SMAPI-3.18.6-installer.zip 28 | 29 | - name: Install SMAPI 30 | run: |- 31 | unzip -qq smapi.zip -d ./smapi 32 | echo -e "2\n\n" | ./smapi/SMAPI\ 3.18.6\ installer/internal/linux/SMAPI.Installer --install --game-path "/home/runner/actions-runner/_work/junimohost-stardew-server/junimohost-stardew-server/Stardew Valley" 33 | continue-on-error: true # runs installer twice for some reason, fails on second time, but succeeds on first 34 | 35 | - uses: actions/setup-dotnet@v2 36 | with: 37 | dotnet-version: "6.0.302" 38 | 39 | - uses: google-github-actions/setup-gcloud@v0 40 | with: 41 | service_account_key: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} 42 | project_id: ${{ secrets.GKE_PROJECT }} 43 | 44 | - run: |- 45 | gcloud --quiet auth configure-docker 46 | 47 | - name: Build and Publish 48 | run: |- 49 | make push 50 | -------------------------------------------------------------------------------- /daemon/internal/zips/zip.go: -------------------------------------------------------------------------------- 1 | package zips 2 | 3 | import ( 4 | "archive/zip" 5 | "go.uber.org/zap" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | const bufferSize = 5120 12 | 13 | func ExtractZipToPath(targetPath string, zipReader *zip.Reader) []string { 14 | topLevelDirectories := make([]string, 0) 15 | buffer := make([]byte, bufferSize) 16 | targetTopDir := filepath.Base(targetPath) 17 | for _, f := range zipReader.File { 18 | // if this is a directory don't unzip it 19 | if f.FileInfo().IsDir() { 20 | pathList := filepath.SplitList(f.Name) 21 | // is this a top level directory? 22 | if len(pathList) >= 2 && pathList[len(pathList)-2] == targetTopDir { 23 | topLevelDirectories = append(topLevelDirectories, f.Name) 24 | } 25 | continue 26 | } 27 | targetFileName := filepath.Join(targetPath, f.Name) 28 | zap.S().Infof("Unzipping: %v to %v", f.Name, targetFileName) 29 | 30 | targetFile, err := createFileWithDirs(targetFileName) 31 | if err != nil { 32 | zap.S().Errorf("Failed to create: %v, reason: %v", targetFileName, err) 33 | continue 34 | } 35 | 36 | fileContents, err := f.Open() 37 | if err != nil { 38 | zap.S().Errorf("Failed to open zip file: %v, reason: %v", f.Name, err) 39 | continue 40 | } 41 | 42 | _, err = io.CopyBuffer(targetFile, fileContents, buffer) 43 | if err != nil { 44 | zap.S().Errorf("Failed to copy zip file contents to disk: %v, reason: %v", f.Name, err) 45 | fileContents.Close() 46 | continue 47 | } 48 | 49 | fileContents.Close() 50 | } 51 | return topLevelDirectories 52 | } 53 | 54 | func createFileWithDirs(p string) (*os.File, error) { 55 | if err := os.MkdirAll(filepath.Dir(p), 0666); err != nil { 56 | return nil, err 57 | } 58 | return os.Create(p) 59 | } 60 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Backup/BackupService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using StardewModdingAPI; 6 | using StardewModdingAPI.Utilities; 7 | 8 | namespace JunimoServer.Services.Backup 9 | { 10 | public class CreateBackupRequest 11 | { 12 | public BackupDate Date { get; set; } 13 | } 14 | 15 | public class BackupDate 16 | { 17 | public int Day { get; set; } 18 | public int Season { get; set; } 19 | public int Year { get; set; } 20 | } 21 | 22 | public class BackupService 23 | { 24 | private readonly HttpClient _httpClient; 25 | private readonly IMonitor _monitor; 26 | 27 | public BackupService(HttpClient httpClient, IMonitor monitor) 28 | { 29 | _httpClient = httpClient; 30 | _monitor = monitor; 31 | } 32 | 33 | public bool CreateBackupForCurrentDaySync() 34 | { 35 | var dateNow = SDate.Now(); 36 | var request = new CreateBackupRequest 37 | { 38 | Date = new BackupDate 39 | { 40 | Day = dateNow.Day, 41 | Season = dateNow.SeasonIndex, 42 | Year = dateNow.Year 43 | } 44 | }; 45 | 46 | 47 | try 48 | { 49 | var response = _httpClient.PostAsJsonAsync("/backup", request); 50 | response.Wait(); 51 | return response.Result.StatusCode == HttpStatusCode.OK; 52 | } 53 | catch (Exception e) 54 | { 55 | _monitor.Log(e.ToString(), LogLevel.Error); 56 | return false; 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/NetworkTweaks/NetworkTweaker.cs: -------------------------------------------------------------------------------- 1 | using JunimoServer.Services.PersistentOption; 2 | using StardewModdingAPI; 3 | using StardewModdingAPI.Events; 4 | using StardewValley; 5 | 6 | namespace JunimoServer.Services.NetworkTweaks 7 | { 8 | public class NetworkTweaker 9 | { 10 | private readonly PersistentOptions _options; 11 | private readonly IModHelper _helper; 12 | private Multiplayer _multiplayer; 13 | 14 | public NetworkTweaker(IModHelper helper, PersistentOptions options) 15 | { 16 | _options = options; 17 | _helper = helper; 18 | helper.Events.GameLoop.UpdateTicked += OnTick; 19 | } 20 | 21 | private void OnTick(object sender, UpdateTickedEventArgs e) 22 | { 23 | if (Game1.netWorldState == null || !Game1.hasLoadedGame) return; 24 | 25 | _multiplayer ??= _helper.Reflection.GetField(typeof(Game1), "multiplayer").GetValue(); 26 | 27 | _multiplayer.defaultInterpolationTicks = 7; 28 | _multiplayer.farmerDeltaBroadcastPeriod = 1; 29 | _multiplayer.locationDeltaBroadcastPeriod = 1; 30 | _multiplayer.worldStateDeltaBroadcastPeriod = 1; 31 | 32 | var maxPlayers = _options.Data.MaxPlayers; 33 | _multiplayer.playerLimit = maxPlayers; 34 | 35 | if (Game1.netWorldState.Value.CurrentPlayerLimit.Value != maxPlayers) 36 | { 37 | Game1.netWorldState.Value.CurrentPlayerLimit.Set(maxPlayers); 38 | } 39 | 40 | if (Game1.netWorldState.Value.HighestPlayerLimit.Value != maxPlayers) 41 | { 42 | Game1.netWorldState.Value.HighestPlayerLimit.Set(maxPlayers); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Backup/BackupScheduler.cs: -------------------------------------------------------------------------------- 1 | using JunimoServer.Util; 2 | using StardewModdingAPI; 3 | using StardewModdingAPI.Events; 4 | 5 | namespace JunimoServer.Services.Backup 6 | { 7 | public class BackupScheduler 8 | { 9 | private readonly BackupService _backupService; 10 | private readonly IModHelper _modHelper; 11 | private readonly IMonitor _monitor; 12 | 13 | private const string BackupCreating = "Creating Backup..."; 14 | private const string BackupSuccess = "Backup Successful"; 15 | private const string BackupFail = "Backup Failed"; 16 | 17 | 18 | public BackupScheduler(IModHelper modHelper, BackupService backupService, IMonitor monitor) 19 | { 20 | _backupService = backupService; 21 | _monitor = monitor; 22 | _modHelper = modHelper; 23 | 24 | modHelper.Events.GameLoop.SaveCreated += OnSaveCreated; // day 0 25 | modHelper.Events.GameLoop.Saved += OnSaveCreated; // every other day 26 | } 27 | 28 | private void OnSaveCreated(object sender, SavedEventArgs e) 29 | { 30 | CreateBackup(); 31 | } 32 | 33 | private void OnSaveCreated(object sender, SaveCreatedEventArgs e) 34 | { 35 | CreateBackup(); 36 | } 37 | 38 | private void CreateBackup() 39 | { 40 | _monitor.Log(BackupCreating, LogLevel.Info); 41 | _modHelper.SendPublicMessage(BackupCreating); 42 | var success = _backupService.CreateBackupForCurrentDaySync(); 43 | 44 | var backupStatus = success ? BackupSuccess : BackupFail; 45 | _modHelper.SendPublicMessage(backupStatus); 46 | _monitor.Log(backupStatus, LogLevel.Info); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/HostAutomation/Activity.cs: -------------------------------------------------------------------------------- 1 | namespace JunimoServer.Services.HostAutomation 2 | { 3 | public abstract class Activity 4 | { 5 | private bool _enabled = false; 6 | private readonly int _everyXTicks; 7 | private int _tickNum = 0; 8 | private int _ticksToWaitRemaining = 0; 9 | 10 | public Activity(int everyXTicks = 1) 11 | { 12 | _everyXTicks = everyXTicks; 13 | } 14 | 15 | protected virtual void OnTick(int tickNum) 16 | { 17 | } 18 | 19 | protected virtual void OnEnabled() 20 | { 21 | } 22 | 23 | protected virtual void OnDisabled() 24 | { 25 | } 26 | 27 | protected virtual void OnDayStart(){} 28 | 29 | public void HandleDayStart() 30 | { 31 | if (!_enabled) return; 32 | OnDayStart(); 33 | } 34 | 35 | public void HandleTick() 36 | { 37 | if (!_enabled) return; 38 | 39 | _ticksToWaitRemaining--; 40 | 41 | if (_ticksToWaitRemaining <= 0) 42 | { 43 | OnTick(_tickNum); 44 | _ticksToWaitRemaining += _everyXTicks; 45 | } 46 | _tickNum++; 47 | _tickNum %= 1000; 48 | } 49 | 50 | public void Enable() 51 | { 52 | if (_enabled) return; 53 | _enabled = true; 54 | OnEnabled(); 55 | } 56 | 57 | public void Disable() 58 | { 59 | if (!_enabled) return; 60 | _enabled = false; 61 | OnDisabled(); 62 | } 63 | 64 | public void PauseForNumTicks(int numTicks) 65 | { 66 | _ticksToWaitRemaining = numTicks; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ServerOptim/ServerOptimizerOverrides.cs: -------------------------------------------------------------------------------- 1 | using StardewModdingAPI; 2 | using StardewValley; 3 | using StardewValley.SDKs; 4 | using xTile.Display; 5 | 6 | namespace JunimoServer.Services.ServerOptim 7 | { 8 | public class ServerOptimizerOverrides 9 | { 10 | private static IMonitor _monitor; 11 | private static bool _shouldDrawFrame = true; 12 | 13 | public static void Initialize(IMonitor monitor) 14 | { 15 | _monitor = monitor; 16 | } 17 | 18 | 19 | public static bool AssignNullDisplay_Prefix() 20 | { 21 | Game1.mapDisplayDevice = new NullDisplayDevice(); 22 | return false; 23 | } 24 | 25 | public static bool ReturnNullDisplay_Prefix(IDisplayDevice __result) 26 | { 27 | __result = new NullDisplayDevice(); 28 | return false; 29 | } 30 | 31 | public static void CreateLobby_Prefix(ref ServerPrivacy privacy, ref uint memberLimit) 32 | { 33 | privacy = ServerPrivacy.Public; 34 | memberLimit = 150; 35 | } 36 | 37 | public static bool Disable_Prefix() 38 | { 39 | return false; 40 | } 41 | 42 | // ReSharper disable once InconsistentNaming 43 | public static bool Draw_Prefix(GameRunner __instance) 44 | 45 | { 46 | if (!_shouldDrawFrame) 47 | { 48 | __instance.SuppressDraw(); 49 | } 50 | 51 | return _shouldDrawFrame; 52 | } 53 | 54 | public static void DisableDrawing() 55 | { 56 | _shouldDrawFrame = false; 57 | } 58 | 59 | public static void EnableDrawing() 60 | { 61 | _shouldDrawFrame = true; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /daemon/internal/startup/startup_controller.go: -------------------------------------------------------------------------------- 1 | package startup 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type StatusUpdate struct { 10 | BackupRestoreSuccessful int `json:"BackupRestoreSuccessful,omitempty"` 11 | ServerConnectable int `json:"ServerConnectable,omitempty"` 12 | } 13 | 14 | type Controller struct { 15 | startupService *Service 16 | } 17 | 18 | func NewController(startupService *Service) *Controller { 19 | return &Controller{startupService: startupService} 20 | } 21 | 22 | func (c *Controller) AddRoutes(gin *gin.Engine) { 23 | gin.GET("/startup", c.OnGetStartup) 24 | gin.GET("/config", c.OnGetConfig) 25 | gin.POST("/status", c.OnUpdateStatus) 26 | } 27 | 28 | func (c *Controller) OnGetStartup(ctx *gin.Context) { 29 | if c.startupService.IsStartupScriptFinished() { 30 | ctx.Status(http.StatusOK) 31 | } else { 32 | ctx.Status(http.StatusAccepted) 33 | } 34 | } 35 | 36 | func (c *Controller) OnGetConfig(ctx *gin.Context) { 37 | ctx.JSON(200, NewGameConfig{ 38 | WhichFarm: int(c.startupService.config.WhichFarm), 39 | UseSeparateWallets: c.startupService.config.UseSeparateWallets, 40 | StartingCabins: int(c.startupService.config.StartingCabins), 41 | CatPerson: c.startupService.config.CatPerson, 42 | FarmName: c.startupService.config.FarmName, 43 | MaxPlayers: int(c.startupService.config.MaxPlayers), 44 | CabinStrategy: int(c.startupService.config.CabinStrategy), 45 | }) 46 | } 47 | 48 | func (c *Controller) OnUpdateStatus(ctx *gin.Context) { 49 | var payload StatusUpdate 50 | err := ctx.ShouldBindJSON(&payload) 51 | if err != nil { 52 | ctx.Status(http.StatusBadRequest) 53 | return 54 | } 55 | 56 | err = c.startupService.UpdateStatus(payload) 57 | 58 | if err != nil { 59 | ctx.Status(http.StatusBadRequest) 60 | return 61 | } 62 | 63 | ctx.Status(http.StatusOK) 64 | } 65 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/CropSaverDataLoader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.Xna.Framework; 4 | using StardewModdingAPI; 5 | 6 | namespace JunimoServer.Services.CropSaver 7 | { 8 | public class CropSaverDataLoader 9 | { 10 | private const string CropSaverDataKey = "JunimoHost.CropSaver.data"; 11 | private readonly IModHelper _helper; 12 | 13 | private CropSaverData _data = new CropSaverData(); 14 | 15 | public CropSaverDataLoader(IModHelper helper) 16 | { 17 | _helper = helper; 18 | } 19 | 20 | 21 | public void AddCrop(SaverCrop crop) 22 | { 23 | _data.Crops.Add(crop); 24 | } 25 | 26 | public void RemoveCrop(SaverCrop crop) 27 | { 28 | _data.Crops.Remove(crop); 29 | } 30 | 31 | public void RemoveCrop(string locationName, Vector2 tileLocation) 32 | { 33 | var crop = GetSaverCrop(locationName, tileLocation); 34 | if (crop != null) 35 | { 36 | RemoveCrop(crop); 37 | } 38 | } 39 | 40 | public SaverCrop GetSaverCrop(string locationName, Vector2 tileLocation) 41 | { 42 | var i = _data.Crops.FindIndex((crop) => crop.IsLocatedAt(locationName, tileLocation)); 43 | return i != -1 ? _data.Crops.ElementAt(i) : null; 44 | } 45 | 46 | public List GetSaverCrops() 47 | { 48 | return _data.Crops; 49 | } 50 | 51 | public void LoadDataFromDisk() 52 | { 53 | _data = _helper.Data.ReadSaveData(CropSaverDataKey) ?? new CropSaverData(); 54 | } 55 | 56 | public void SaveDataToDisk() 57 | { 58 | _helper.Data.WriteSaveData(CropSaverDataKey, _data); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/BanCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public class BanCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("ban", "\"farmerName|userName\" to ban the player.", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | if (args.Length != 1 || (args.Length == 1 && args[0] == "")) 22 | { 23 | helper.SendPrivateMessage(msg.SourceFarmer, "Invalid use of command. Correct format is !ban name"); 24 | return; 25 | } 26 | 27 | var nameFromCommand = args[0]; 28 | var targetFarmer = helper.FindPlayerIdByFarmerNameOrUserName(nameFromCommand); 29 | 30 | if (targetFarmer == null) 31 | { 32 | helper.SendPrivateMessage(msg.SourceFarmer, "Player not found: " + nameFromCommand); 33 | return; 34 | } 35 | 36 | if (roleService.IsPlayerOwner(targetFarmer.UniqueMultiplayerID)) 37 | { 38 | helper.SendPrivateMessage(msg.SourceFarmer, "You can't ban the owner of the server."); 39 | return; 40 | } 41 | 42 | Game1.server.ban(targetFarmer.UniqueMultiplayerID); 43 | helper.SendPrivateMessage(msg.SourceFarmer, "Banned: " + targetFarmer.Name); 44 | }); 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/KickCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public class KickCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("kick", "\"farmerName|userName\" to kick the player.", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | if (args.Length != 1 || (args.Length == 1 && args[0] == "")) 22 | { 23 | helper.SendPrivateMessage(msg.SourceFarmer, "Invalid use of command. Correct format is !kick name"); 24 | return; 25 | } 26 | 27 | var nameFromCommand = args[0]; 28 | var targetFarmer = helper.FindPlayerIdByFarmerNameOrUserName(nameFromCommand); 29 | 30 | if (targetFarmer == null) 31 | { 32 | helper.SendPrivateMessage(msg.SourceFarmer, "Player not found: " + nameFromCommand); 33 | return; 34 | } 35 | 36 | if (targetFarmer.UniqueMultiplayerID == helper.GetOwnerPlayerId()) 37 | { 38 | helper.SendPrivateMessage(msg.SourceFarmer, "You can't kick the owner of the server."); 39 | return; 40 | } 41 | 42 | Game1.server.kick(targetFarmer.UniqueMultiplayerID); 43 | helper.SendPrivateMessage(msg.SourceFarmer, "Kicked: " + targetFarmer.Name); 44 | }); 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/UnbanCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public class UnbanCommand 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("unban", "\"id|userName\" to unban the player. Use !listban to find ID.", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | if (args.Length != 1 || (args.Length == 1 && args[0] == "")) 22 | { 23 | helper.SendPrivateMessage(msg.SourceFarmer, "Invalid use of command. Correct format is !unban name"); 24 | return; 25 | } 26 | 27 | var target = args[0]; 28 | 29 | var unbanKey = ""; 30 | var unbanVal = ""; 31 | foreach (var (k, v) in Game1.bannedUsers) 32 | { 33 | if (k == target || v == target) 34 | { 35 | unbanKey = k; 36 | unbanVal = v; 37 | break; 38 | } 39 | } 40 | 41 | if (unbanKey == "") 42 | { 43 | helper.SendPrivateMessage(msg.SourceFarmer, "Player not found: " + target); 44 | return; 45 | } 46 | Game1.bannedUsers.Remove(unbanKey); 47 | helper.SendPrivateMessage(msg.SourceFarmer, $"Unbanned: {unbanKey} | {unbanVal}"); 48 | }); 49 | } 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Daemon/DaemonService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Json; 3 | using System.Threading.Tasks; 4 | using JunimoServer.Services.GameCreator; 5 | using StardewModdingAPI; 6 | 7 | namespace JunimoServer.Services.Daemon 8 | { 9 | /// 10 | /// StatusUpdateBody 11 | /// 0 is unchanged 12 | /// 1 is false 13 | /// 2 is true 14 | /// 15 | public class StatusUpdateBody 16 | { 17 | public int BackupRestoreSuccessful { get; set; } 18 | public int ServerConnectable { get; set; } 19 | } 20 | 21 | public class DaemonService 22 | { 23 | private readonly HttpClient _httpClient; 24 | private readonly IMonitor _monitor; 25 | 26 | 27 | public async Task GetConfig() 28 | { 29 | _monitor.Log("Fetching Config from Daemon", LogLevel.Info); 30 | 31 | var response = await _httpClient.GetAsync("/config"); 32 | var config = await response.Content.ReadFromJsonAsync(); 33 | 34 | _monitor.Log($"Received config: {config}", LogLevel.Info); 35 | return config; 36 | } 37 | 38 | private async Task UpdateStatus(StatusUpdateBody update) 39 | { 40 | await _httpClient.PostAsJsonAsync("/status", update); 41 | } 42 | 43 | public async Task UpdateConnectableStatus() 44 | { 45 | await UpdateStatus(new StatusUpdateBody 46 | { 47 | BackupRestoreSuccessful = 0, 48 | ServerConnectable = 2 49 | }); 50 | } 51 | 52 | public async Task UpdateNotConnectableStatus() 53 | { 54 | await UpdateStatus(new StatusUpdateBody 55 | { 56 | BackupRestoreSuccessful = 0, 57 | ServerConnectable = 1 58 | }); 59 | } 60 | 61 | 62 | public DaemonService(HttpClient httpClient, IMonitor monitor) 63 | { 64 | _monitor = monitor; 65 | _httpClient = httpClient; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /daemon/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junimohost/game-daemon 2 | 3 | go 1.19 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.10.0 7 | github.com/gin-gonic/gin v1.7.7 8 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.8.0 9 | go.uber.org/zap v1.21.0 10 | google.golang.org/grpc v1.45.0 11 | google.golang.org/protobuf v1.27.1 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go v0.65.0 // indirect 16 | github.com/gin-contrib/sse v0.1.0 // indirect 17 | github.com/go-playground/locales v0.13.0 // indirect 18 | github.com/go-playground/universal-translator v0.17.0 // indirect 19 | github.com/go-playground/validator/v10 v10.4.1 // indirect 20 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 21 | github.com/golang/protobuf v1.5.2 // indirect 22 | github.com/googleapis/gax-go/v2 v2.0.5 // indirect 23 | github.com/json-iterator/go v1.1.9 // indirect 24 | github.com/jstemmer/go-junit-report v0.9.1 // indirect 25 | github.com/leodido/go-urn v1.2.0 // indirect 26 | github.com/mattn/go-isatty v0.0.12 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 29 | github.com/ugorji/go/codec v1.1.7 // indirect 30 | go.opencensus.io v0.22.4 // indirect 31 | go.uber.org/atomic v1.7.0 // indirect 32 | go.uber.org/multierr v1.6.0 // indirect 33 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 34 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect 35 | golang.org/x/mod v0.4.2 // indirect 36 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 37 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 38 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 39 | golang.org/x/text v0.3.7 // indirect 40 | golang.org/x/tools v0.1.5 // indirect 41 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 42 | google.golang.org/api v0.30.0 // indirect 43 | google.golang.org/appengine v1.6.6 // indirect 44 | google.golang.org/genproto v0.0.0-20220302033224-9aa15565e42a // indirect 45 | gopkg.in/yaml.v2 v2.4.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Roles/RoleService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using JunimoServer.Util; 4 | using StardewModdingAPI; 5 | using StardewModdingAPI.Events; 6 | 7 | namespace JunimoServer.Services.Roles 8 | { 9 | public enum Role 10 | { 11 | Admin, 12 | Unassigned, 13 | } 14 | 15 | public class RoleData 16 | { 17 | public Dictionary Roles = new Dictionary(); 18 | } 19 | 20 | public class RoleService 21 | { 22 | private RoleData _data = new RoleData(); 23 | private const string RoleDataKey = "JunimoHost.Roles.data"; 24 | 25 | 26 | private readonly IModHelper _helper; 27 | 28 | public RoleService(IModHelper helper) 29 | { 30 | _helper = helper; 31 | helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; 32 | } 33 | 34 | private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) 35 | { 36 | var saveData = _helper.Data.ReadSaveData(RoleDataKey); 37 | 38 | if (saveData != null) 39 | { 40 | _data = saveData; 41 | return; 42 | } 43 | 44 | _data.Roles[_helper.GetOwnerPlayerId()] = Role.Admin; 45 | SaveData(); 46 | } 47 | 48 | private void SaveData() 49 | { 50 | _helper.Data.WriteSaveData(RoleDataKey, _data); 51 | } 52 | 53 | public void AssignAdmin(long playerId) 54 | { 55 | _data.Roles[playerId] = Role.Admin; 56 | SaveData(); 57 | } 58 | 59 | public void UnassignAdmin(long playerId) 60 | { 61 | if (playerId == _helper.GetOwnerPlayerId()) return; 62 | 63 | _data.Roles[playerId] = Role.Unassigned; 64 | SaveData(); 65 | } 66 | 67 | public bool IsPlayerAdmin(long playerId) 68 | { 69 | return _data.Roles.ContainsKey(playerId) && _data.Roles[playerId] == Role.Admin; 70 | } 71 | 72 | public bool IsPlayerOwner(long playerId) 73 | { 74 | return _helper.GetOwnerPlayerId() == playerId; 75 | } 76 | 77 | public long[] GetAdmins() 78 | { 79 | return _data.Roles.Keys.Where(IsPlayerAdmin).ToArray(); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/CropWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Xna.Framework; 4 | using StardewModdingAPI; 5 | using StardewModdingAPI.Events; 6 | using StardewValley; 7 | using StardewValley.TerrainFeatures; 8 | 9 | namespace JunimoServer.Services.CropSaver 10 | { 11 | public class CropWatcher 12 | { 13 | private readonly Dictionary _previousHasCrop = new Dictionary(); 14 | 15 | private readonly Action _onCropAdded; 16 | private readonly Action _onCropRemoved; 17 | 18 | private const int UpdateEveryTicks = 5; 19 | 20 | public CropWatcher(IModHelper helper, Action onCropAdded, Action onCropRemoved) 21 | { 22 | helper.Events.GameLoop.UpdateTicked += GameLoopOnUpdateTicked; 23 | _onCropAdded = onCropAdded; 24 | _onCropRemoved = onCropRemoved; 25 | } 26 | 27 | private int _timer = 0; 28 | private void GameLoopOnUpdateTicked(object sender, UpdateTickedEventArgs e) 29 | { 30 | if (_timer > 1) 31 | { 32 | _timer--; 33 | return; 34 | } 35 | 36 | _timer = UpdateEveryTicks; 37 | var farmLocation = Game1.getLocationFromName("Farm"); 38 | if (farmLocation == null) return; 39 | 40 | var farmTerrainFeatures = farmLocation.terrainFeatures.Values; 41 | 42 | foreach (var feature in farmTerrainFeatures) 43 | { 44 | var tileLoc = feature.currentTileLocation; 45 | var hasCrop = feature.ContainsCrop(); 46 | if (!_previousHasCrop.ContainsKey(tileLoc)) 47 | { 48 | _previousHasCrop.Add(tileLoc, hasCrop); 49 | continue; 50 | } 51 | 52 | if (hasCrop && !_previousHasCrop[tileLoc]) 53 | { 54 | _onCropAdded(feature); 55 | } 56 | 57 | if (!hasCrop && _previousHasCrop[tileLoc]) 58 | { 59 | _onCropRemoved(feature); 60 | } 61 | 62 | _previousHasCrop[tileLoc] = hasCrop; 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Util/ModHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using StardewModdingAPI; 4 | using StardewValley; 5 | using StardewValley.Locations; 6 | using StardewValley.SDKs; 7 | 8 | namespace JunimoServer.Util 9 | { 10 | public static class ModHelperExtensions 11 | { 12 | public static Multiplayer GetMultiplayer(this IModHelper helper) 13 | { 14 | return helper.Reflection.GetField(typeof(Game1), "multiplayer").GetValue(); 15 | } 16 | 17 | public static Farmer FindPlayerIdByFarmerNameOrUserName(this IModHelper helper, string name) 18 | { 19 | return Game1 20 | .getAllFarmers() 21 | .FirstOrDefault(farmer => farmer.Name == name 22 | || helper.GetFarmerUserNameById(farmer.UniqueMultiplayerID) == name); 23 | 24 | 25 | } 26 | 27 | public static string GetFarmerUserNameById(this IModHelper helper, long id) 28 | { 29 | return helper.GetMultiplayer().getUserName(id); 30 | } 31 | 32 | public static string GetFarmerNameById(this IModHelper helper, long id) 33 | { 34 | return Game1 35 | .getAllFarmers() 36 | .FirstOrDefault(farmer => farmer.UniqueMultiplayerID == id)?.Name; 37 | } 38 | 39 | public static void SendPublicMessage(this IModHelper helper, string msg) 40 | { 41 | helper.GetMultiplayer() 42 | .sendChatMessage(LocalizedContentManager.CurrentLanguageCode, msg, Multiplayer.AllPlayers); 43 | } 44 | 45 | public static void SendPrivateMessage(this IModHelper helper, long uniqueMultiplayerId, string msg) 46 | { 47 | helper.GetMultiplayer() 48 | .sendChatMessage(LocalizedContentManager.CurrentLanguageCode, msg, uniqueMultiplayerId); 49 | } 50 | 51 | public static int GetCurrentNumCabins(this IModHelper helper) 52 | { 53 | return helper.Reflection.GetMethod(Game1.server, "cabins") 54 | .Invoke>().ToList().Count; 55 | } 56 | 57 | public static bool IsNetworkingReady(this IModHelper helper) 58 | { 59 | var sdk = helper.Reflection.GetField(typeof(Program), "_sdk").GetValue(); 60 | return sdk != null && sdk.Networking != null; 61 | } 62 | 63 | public static long GetOwnerPlayerId(this IModHelper helper) 64 | { 65 | return ((Cabin)Game1.getFarm().buildings.First(building => building.isCabin).indoors.Value).owner.UniqueMultiplayerID; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /mod/.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ChatCommands/ChatCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using HarmonyLib; 6 | using JunimoServer.Util; 7 | using StardewModdingAPI; 8 | using StardewValley.Menus; 9 | 10 | namespace JunimoServer.Services.ChatCommands 11 | { 12 | public class ChatCommands : IChatCommandApi 13 | { 14 | private readonly IMonitor _monitor; 15 | private readonly IModHelper _helper; 16 | 17 | private readonly List _registeredCommands = new List(); 18 | 19 | public ChatCommands(IMonitor monitor, Harmony harmony, IModHelper helper) 20 | { 21 | _monitor = monitor; 22 | _helper = helper; 23 | 24 | ChatWatcher.Initialize(OnChatMessage); 25 | 26 | harmony.Patch( 27 | original: AccessTools.Method(typeof(ChatBox), nameof(ChatBox.receiveChatMessage)), 28 | postfix: new HarmonyMethod(typeof(ChatWatcher), nameof(ChatWatcher.receiveChatMessage_Postfix)) 29 | ); 30 | 31 | RegisterCommand(new ChatCommand("help", "Displays available commands.", HelpCommand)); 32 | } 33 | 34 | private void HelpCommand(string[] args, ReceivedMessage msg) 35 | { 36 | 37 | foreach (var command in _registeredCommands) 38 | { 39 | _helper.SendPrivateMessage(msg.SourceFarmer, $"!{command.Name}: {command.Description}"); 40 | } 41 | 42 | } 43 | 44 | 45 | private void OnChatMessage(ReceivedMessage obj) 46 | { 47 | _monitor.Log($"{obj.Message}"); 48 | 49 | var msg = obj.Message; 50 | if (String.IsNullOrEmpty(msg) || msg[0] != '!' || msg.Length < 2) 51 | { 52 | return; 53 | } 54 | 55 | var commandStringWithArgs = msg[1..]; 56 | var commandParts = commandStringWithArgs.Split(" "); 57 | var commandName = commandParts[0]; 58 | var commandArgs = Array.Empty(); 59 | if (commandParts.Length > 1) 60 | { 61 | commandArgs = commandParts[1..]; 62 | } 63 | 64 | foreach (var command in _registeredCommands.Where(command => command.Name.Equals(commandName))) 65 | { 66 | command.Action(commandArgs, obj); 67 | } 68 | } 69 | 70 | public void RegisterCommand(string name, string description, Action action) 71 | { 72 | _registeredCommands.Add(new ChatCommand(name, description, action)); 73 | } 74 | 75 | public void RegisterCommand(ChatCommand command) 76 | { 77 | _registeredCommands.Add(command); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/CropSaverData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Xna.Framework; 4 | using StardewModdingAPI.Utilities; 5 | using StardewValley; 6 | using StardewValley.TerrainFeatures; 7 | 8 | namespace JunimoServer.Services.CropSaver 9 | { 10 | public class CropSaverData 11 | { 12 | public List Crops { get; set; } = new List(); 13 | } 14 | 15 | public class SaverCrop 16 | { 17 | public string cropLocationName; 18 | public Vector2 cropLocationTile; 19 | public long ownerId; 20 | public SDate datePlanted; 21 | 22 | public int extraDays; 23 | 24 | 25 | public SaverCrop(string cropLocationName, Vector2 cropLocationTile, long ownerId, SDate datePlanted, 26 | int extraDays = 0) 27 | { 28 | this.cropLocationName = cropLocationName; 29 | this.cropLocationTile = cropLocationTile; 30 | this.ownerId = ownerId; 31 | this.datePlanted = datePlanted; 32 | this.extraDays = extraDays; 33 | } 34 | 35 | 36 | public void IncrementExtraDays() 37 | { 38 | extraDays++; 39 | } 40 | 41 | public bool IsLocatedAt(string cropLocation, Vector2 cropPosition) 42 | { 43 | return cropLocation.Equals(cropLocationName) && cropLocationTile.Equals(cropPosition); 44 | } 45 | 46 | public HoeDirt TryGetCoorespondingDirt() 47 | { 48 | var location = Game1.getLocationFromName(cropLocationName); 49 | if (location.terrainFeatures.TryGetValue(cropLocationTile, out TerrainFeature terrainFeature)) 50 | { 51 | if (terrainFeature is HoeDirt dirt) 52 | { 53 | return dirt; 54 | } 55 | } 56 | 57 | return null; 58 | } 59 | 60 | public Crop TryGetCoorespondingCrop() 61 | { 62 | var dirt = TryGetCoorespondingDirt(); 63 | return dirt is {crop: { }} ? dirt.crop : null; 64 | } 65 | 66 | protected bool Equals(SaverCrop other) 67 | { 68 | return cropLocationName == other.cropLocationName && cropLocationTile.Equals(other.cropLocationTile) && 69 | ownerId == other.ownerId && Equals(datePlanted, other.datePlanted); 70 | } 71 | 72 | public override bool Equals(object obj) 73 | { 74 | if (ReferenceEquals(null, obj)) return false; 75 | if (ReferenceEquals(this, obj)) return true; 76 | if (obj.GetType() != this.GetType()) return false; 77 | return Equals((SaverCrop) obj); 78 | } 79 | 80 | public override int GetHashCode() 81 | { 82 | return HashCode.Combine(cropLocationName, cropLocationTile, ownerId, datePlanted); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Util/SDateHelper.cs: -------------------------------------------------------------------------------- 1 | using StardewModdingAPI.Utilities; 2 | 3 | namespace JunimoServer.Util 4 | { 5 | public class SDateHelper 6 | { 7 | private static readonly SDate EggFestival = new SDate(13, "spring"); 8 | private static readonly SDate FlowerDance = new SDate(24, "spring"); 9 | private static readonly SDate Luau = new SDate(11, "summer"); 10 | private static readonly SDate DanceOfJellies = new SDate(28, "summer"); 11 | private static readonly SDate StardewValleyFair = new SDate(16, "fall"); 12 | private static readonly SDate SpiritsEve = new SDate(27, "fall"); 13 | private static readonly SDate FestivalOfIce = new SDate(8, "winter"); 14 | private static readonly SDate FeastOfWinterStar = new SDate(25, "winter"); 15 | 16 | private static readonly SDate GrandpasGhost = new SDate(1, "spring", 3); 17 | 18 | private static readonly SDate[] Festivals = 19 | { 20 | EggFestival, FlowerDance, Luau, DanceOfJellies, StardewValleyFair, SpiritsEve, FestivalOfIce, 21 | FeastOfWinterStar 22 | }; 23 | 24 | 25 | public static bool IsFestivalToday() 26 | { 27 | var currentDate = SDate.Now(); 28 | 29 | // grandpas ghost only happens on year 3 30 | if (currentDate == GrandpasGhost) 31 | { 32 | return true; 33 | } 34 | 35 | foreach (var festival in Festivals) 36 | { 37 | if (currentDate.EqualsIgnoreYear(festival)) 38 | { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | public static bool IsEggFestivalToday() 47 | { 48 | return SDate.Now().EqualsIgnoreYear(EggFestival); 49 | } 50 | 51 | public static bool IsFlowerDanceToday() 52 | { 53 | return SDate.Now().EqualsIgnoreYear(FlowerDance); 54 | } 55 | 56 | public static bool IsLuauToday() 57 | { 58 | return SDate.Now().EqualsIgnoreYear(Luau); 59 | } 60 | 61 | public static bool IsDanceOfJelliesToday() 62 | { 63 | return SDate.Now().EqualsIgnoreYear(DanceOfJellies); 64 | } 65 | 66 | public static bool IsStardewValleyFairToday() 67 | { 68 | return SDate.Now().EqualsIgnoreYear(StardewValleyFair); 69 | } 70 | 71 | public static bool IsSpiritsEveToday() 72 | { 73 | return SDate.Now().EqualsIgnoreYear(SpiritsEve); 74 | } 75 | 76 | public static bool IsFestivalOfIceToday() 77 | { 78 | return SDate.Now().EqualsIgnoreYear(FestivalOfIce); 79 | } 80 | 81 | public static bool IsFeastOfWinterStarToday() 82 | { 83 | return SDate.Now().EqualsIgnoreYear(FeastOfWinterStar); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # docker context is from the root folder, not ./docker 2 | # --- Build Stage --- 3 | FROM jlesage/baseimage-gui:debian-11 as build-stage 4 | 5 | WORKDIR /app 6 | 7 | # Install Go 1.21 8 | RUN apt-get update && \ 9 | apt-get install -y wget && \ 10 | wget https://golang.org/dl/go1.21.5.linux-amd64.tar.gz && \ 11 | tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz && \ 12 | rm go1.21.5.linux-amd64.tar.gz 13 | 14 | # Add Go to PATH 15 | ENV PATH="/usr/local/go/bin:${PATH}" 16 | 17 | # Verify Go installation 18 | RUN go version 19 | 20 | # Copy go mod and sum files 21 | COPY daemon/go.mod daemon/go.sum ./ 22 | 23 | # Download all dependencies. 24 | # Dependencies will be cached if the go.mod and go.sum files are not changed 25 | RUN go mod download 26 | 27 | COPY daemon . 28 | 29 | # Build the Go app 30 | RUN go build -o game-daemon ./cmd/daemon/daemon.go 31 | 32 | # --- Final Stage --- 33 | FROM jlesage/baseimage-gui:debian-11 34 | 35 | ENV APP_NAME="StardewValley" 36 | # set user to root 37 | ENV USER_ID=0 38 | ENV GROUP_ID=0 39 | ENV TZ=America/New_York 40 | ENV HOME=/root 41 | 42 | RUN apt-get update \ 43 | && apt-get install -y \ 44 | wget unzip tar curl pulseaudio 45 | 46 | ARG STARDEW_DOWNLOAD_URL=https://storage.googleapis.com/junimo-public/SV156.tar.gz 47 | RUN mkdir -p /data/Stardew && \ 48 | mkdir -p /data/nexus && \ 49 | wget $STARDEW_DOWNLOAD_URL -qO /data/latest.tar.gz && \ 50 | tar xf /data/latest.tar.gz -C /data/Stardew && \ 51 | rm /data/latest.tar.gz 52 | 53 | RUN wget https://github.com/Pathoschild/SMAPI/releases/download/3.18.6/SMAPI-3.18.6-installer.zip -qO /data/nexus.zip && \ 54 | unzip /data/nexus.zip -d /data/nexus/ && \ 55 | /bin/bash -c "echo -e \"2\n\n\" | /data/nexus/SMAPI\ 3.18.6\ installer/internal/linux/SMAPI.Installer --install --game-path \"/data/Stardew/Stardew Valley\"" || : && \ 56 | rm -rf "/data/Stardew/Stardew Valley/Mods/SaveBackup" 57 | 58 | RUN rm "/data/Stardew/Stardew Valley/steam_api64.dll" && \ 59 | wget https://storage.googleapis.com/junimo-public/steam/steam_api64.dll -O /data/Stardew/Stardew\ Valley/steam_api64.dll && \ 60 | rm "/data/Stardew/Stardew Valley/libsteam_api.so" && \ 61 | wget https://storage.googleapis.com/junimo-public/steam/libsteam_api.so -O /data/Stardew/Stardew\ Valley/libsteam_api.so && \ 62 | mkdir -p /root/.steam/sdk64 && \ 63 | wget https://storage.googleapis.com/junimo-public/steam/steamclient.so -O /root/.steam/sdk64/steamclient.so 64 | 65 | COPY ["docker/config.user.json", "/data/Stardew/Stardew Valley/smapi-internal/"] 66 | 67 | RUN chmod +x /data/Stardew/Stardew\ Valley/StardewValley && \ 68 | chmod -R 777 /data/Stardew/ && \ 69 | chown -R 1000:1000 /data/Stardew 70 | 71 | RUN rm "/data/Stardew/Stardew Valley/Content/XACT/Wave Bank.xwb" "/data/Stardew/Stardew Valley/Content/XACT/Wave Bank(1.4).xwb" 72 | 73 | COPY ["docker/mods", "/data/Stardew/Stardew Valley/Mods/"] 74 | RUN chmod -R 777 /data/Stardew/Stardew\ Valley/Mods 75 | 76 | COPY --from=build-stage /app/game-daemon /opt/game-daemon 77 | RUN chmod +x /opt/game-daemon 78 | 79 | COPY docker/docker-entrypoint.sh /startapp.sh -------------------------------------------------------------------------------- /daemon/cmd/daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "cloud.google.com/go/storage" 10 | "github.com/gin-gonic/gin" 11 | pbsm "github.com/junimohost/game-daemon/gen/proto/go/junimohost/servermanager/v1" 12 | pbsd "github.com/junimohost/game-daemon/gen/proto/go/junimohost/stardewdaemon/v1" 13 | "github.com/junimohost/game-daemon/internal/backup" 14 | "github.com/junimohost/game-daemon/internal/startup" 15 | "github.com/junimohost/game-daemon/pkg/envutil" 16 | "github.com/junimohost/game-daemon/pkg/logging" 17 | "go.uber.org/zap" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/credentials/insecure" 20 | ) 21 | 22 | func main() { 23 | logger := logging.CreateLogger() 24 | defer logger.Sync() 25 | 26 | undo := zap.ReplaceGlobals(logger.Desugar()) 27 | defer undo() 28 | 29 | ctx := context.Background() 30 | ginServer := gin.Default() 31 | 32 | serverID := envutil.GetEnvOrFail("SERVER_ID") 33 | backupBucketName := envutil.GetEnvOrFail("BACKUP_GCS_BUCKET") 34 | 35 | noBackendEnv := envutil.GetEnvOrDefault("NO_BACKEND", "") 36 | backendAvailable := noBackendEnv != "true" 37 | 38 | var backendServerAddress string 39 | if backendAvailable { 40 | backendServerAddress = envutil.GetEnvOrFail("BACKEND_HOSTPORT") 41 | } else { 42 | backendServerAddress = "" 43 | } 44 | 45 | storageClient, err := storage.NewClient(ctx) 46 | if err != nil { 47 | logger.Fatalf("Failed to start GCS client: %v", err) 48 | } 49 | 50 | var stardewDaemonServiceClient pbsd.StardewDaemonServiceClient 51 | var serverManagerServiceClient pbsm.ServerManagerServiceClient 52 | if backendAvailable { 53 | conn, err := grpc.Dial(backendServerAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) 54 | if err != nil { 55 | logger.Fatalf("Failed to connect to backend: %v", err) 56 | } 57 | defer conn.Close() 58 | stardewDaemonServiceClient = pbsd.NewStardewDaemonServiceClient(conn) 59 | serverManagerServiceClient = pbsm.NewServerManagerServiceClient(conn) 60 | } 61 | 62 | backupService := backup.NewService(storageClient, stardewDaemonServiceClient, backupBucketName, serverID, backendAvailable) 63 | backupController := backup.NewController(backupService) 64 | 65 | backupController.AddRoutes(ginServer) 66 | 67 | startupService := startup.NewService(storageClient, backupService, stardewDaemonServiceClient, serverManagerServiceClient, serverID, backendAvailable) 68 | startupController := startup.NewController(startupService) 69 | 70 | startupController.AddRoutes(ginServer) 71 | 72 | httpPort := envutil.GetEnvOrDefault("DAEMON_HTTP_PORT", "8080") 73 | go func() { 74 | if err := ginServer.Run("0.0.0.0:" + httpPort); err != nil { 75 | logger.Fatalf("Failed to serve gin server: %v", err) 76 | } 77 | }() 78 | 79 | if backendAvailable { 80 | logger.Debugf("Running Startup Script!") 81 | err := startupService.RunStartupScript(ctx) 82 | if err != nil { 83 | logger.Errorf("Failed to complete startup script: %v", err) 84 | } 85 | } 86 | 87 | logger.Infof("game-daemon started on: %v", httpPort) 88 | 89 | c := make(chan os.Signal, 1) 90 | signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 91 | <-c 92 | } 93 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GameLoader/GameLoaderService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using StardewModdingAPI; 4 | using StardewValley; 5 | using StardewValley.Menus; 6 | 7 | namespace JunimoServer.Services.GameLoader 8 | { 9 | class GameLoaderService 10 | { 11 | private const string SaveKey = "JunimoHost.GameLoader"; 12 | 13 | private readonly IModHelper _helper; 14 | private readonly IMonitor _monitor; 15 | private readonly GameLoaderSaveData _saveData; 16 | public GameLoaderService(IModHelper helper, IMonitor monitor) 17 | { 18 | _helper = helper; 19 | _monitor = monitor; 20 | _saveData = helper.Data.ReadGlobalData(SaveKey) ?? new GameLoaderSaveData(); 21 | } 22 | 23 | public bool HasLoadableSave() 24 | { 25 | var saveName = _saveData.SaveNameToLoad; 26 | 27 | if (saveName == null) 28 | { 29 | return false; 30 | } 31 | 32 | var savePath = GetSavePath(saveName); 33 | var saveExists = Directory.Exists(savePath); 34 | 35 | return saveExists; 36 | 37 | } 38 | 39 | public bool LoadSave() 40 | { 41 | var saveName = _saveData.SaveNameToLoad; 42 | var savePath = GetSavePath(saveName); 43 | var saveExists = Directory.Exists(savePath); 44 | if (!saveExists) 45 | { 46 | _monitor.Log($"{savePath} does not exist. Aborting load.", LogLevel.Warn); 47 | return false; 48 | } 49 | 50 | Game1.multiplayerMode = 2; 51 | 52 | try 53 | { 54 | _monitor.Log($"Loading Save: {saveName}", LogLevel.Info); 55 | SaveGame.Load(saveName); 56 | if (Game1.activeClickableMenu is IClickableMenu menu && menu != null) 57 | { 58 | menu.exitThisMenu(false); 59 | } 60 | } 61 | catch (Exception ex) 62 | { 63 | _monitor.Log($"Load Failed: {ex.Message} ", LogLevel.Error); 64 | return false; 65 | } 66 | 67 | return true; 68 | 69 | } 70 | 71 | public void SetSaveNameToLoad(string saveName) 72 | { 73 | _saveData.SaveNameToLoad = saveName; 74 | _helper.Data.WriteGlobalData(SaveKey, _saveData); 75 | _monitor.Log($"Save set to load: {saveName}", LogLevel.Info); 76 | } 77 | 78 | public void SetCurrentGameAsSaveToLoad(string FarmName) 79 | { 80 | // Save name goes from nothing -> _uuid -> farmName_uuid (once loaded in completely) 81 | var saveName = Constants.SaveFolderName; 82 | if (Constants.SaveFolderName.Substring(0, 1) == "_") // savename is in midpoint 83 | { 84 | saveName = SaveGame.FilterFileName(FarmName) + saveName; 85 | } 86 | 87 | SetSaveNameToLoad(saveName); 88 | } 89 | 90 | private string GetSavePath(string saveName) 91 | { 92 | return Path.Combine(Constants.SavesPath, saveName); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/RoleCommands.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JunimoServer.Services.ChatCommands; 3 | using JunimoServer.Services.Roles; 4 | using JunimoServer.Util; 5 | using StardewModdingAPI; 6 | using StardewValley; 7 | 8 | namespace JunimoServer.Services.Commands 9 | { 10 | public static class RoleCommands 11 | { 12 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, RoleService roleService) 13 | { 14 | chatCommandApi.RegisterCommand("admin", "\"farmerName|userName\" to assign admin status to the player.", (args, msg) => 15 | { 16 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 17 | { 18 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 19 | return; 20 | } 21 | if (args.Length != 1 || (args.Length == 1 && args[0] == "")) 22 | { 23 | helper.SendPrivateMessage(msg.SourceFarmer, "Invalid use of command. Correct format is !admin name"); 24 | return; 25 | } 26 | 27 | var nameFromCommand = args[0]; 28 | var farmerToAdmin = helper.FindPlayerIdByFarmerNameOrUserName(nameFromCommand); 29 | 30 | if (farmerToAdmin == null) 31 | { 32 | helper.SendPrivateMessage(msg.SourceFarmer, "Player not found: " + nameFromCommand); 33 | return; 34 | } 35 | 36 | roleService.AssignAdmin(farmerToAdmin.UniqueMultiplayerID); 37 | helper.SendPrivateMessage(msg.SourceFarmer, "Assigned Admin to: " + farmerToAdmin.Name); 38 | }); 39 | chatCommandApi.RegisterCommand("unadmin", "\"farmerName|userName\" to take away admin status from the player.", (args, msg) => 40 | { 41 | if (!roleService.IsPlayerAdmin(msg.SourceFarmer)) 42 | { 43 | helper.SendPrivateMessage(msg.SourceFarmer, "You are not an admin."); 44 | return; 45 | } 46 | 47 | if (args.Length != 1 || (args.Length == 1 && args[0] == "")) 48 | { 49 | helper.SendPrivateMessage(msg.SourceFarmer, "Invalid use of command. Correct format is !unadmin name"); 50 | return; 51 | } 52 | 53 | var nameFromCommand = args[0]; 54 | var farmerToUnadmin = helper.FindPlayerIdByFarmerNameOrUserName(nameFromCommand); 55 | 56 | if (farmerToUnadmin == null) 57 | { 58 | helper.SendPrivateMessage(msg.SourceFarmer, "Player not found: " + nameFromCommand); 59 | return; 60 | } 61 | 62 | if (farmerToUnadmin.UniqueMultiplayerID == helper.GetOwnerPlayerId()) 63 | { 64 | helper.SendPrivateMessage(msg.SourceFarmer, "You can't unadmin the owner of the server."); 65 | return; 66 | } 67 | 68 | roleService.UnassignAdmin(farmerToUnadmin.UniqueMultiplayerID); 69 | helper.SendPrivateMessage(msg.SourceFarmer, "Took away admin from: " + farmerToUnadmin.Name); 70 | }); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/Commands/CabinCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using JunimoServer.Services.CabinManager; 4 | using JunimoServer.Services.ChatCommands; 5 | using JunimoServer.Services.PersistentOption; 6 | using JunimoServer.Util; 7 | using Microsoft.Xna.Framework; 8 | using StardewModdingAPI; 9 | using StardewValley; 10 | using StardewValley.Locations; 11 | 12 | namespace JunimoServer.Services.Commands 13 | { 14 | public static class CabinCommand 15 | { 16 | public static void Register(IModHelper helper, IChatCommandApi chatCommandApi, 17 | PersistentOptions options, IMonitor monitor) 18 | { 19 | chatCommandApi.RegisterCommand("cabin", 20 | "Moves your cabin to the right of your player.\nThis will clear basic debris to make space.", 21 | (args, msg) => 22 | { 23 | if (options.Data.CabinStrategy == CabinStrategy.FarmhouseStack) 24 | { 25 | helper.SendPrivateMessage(msg.SourceFarmer, "Can't move cabin. The host has chosen to keep all cabins in the farmhouse."); 26 | return; 27 | } 28 | var fromPlayer = Game1.getOnlineFarmers() 29 | .First(farmer => farmer.UniqueMultiplayerID == msg.SourceFarmer); 30 | 31 | if (fromPlayer.currentLocation.Name != "Farm") 32 | { 33 | helper.SendPrivateMessage(msg.SourceFarmer, "Must be on Farm to move your cabin."); 34 | return; 35 | } 36 | 37 | var isOwnersCabin = helper.GetOwnerPlayerId() == fromPlayer.UniqueMultiplayerID; 38 | if (isOwnersCabin) 39 | { 40 | helper.SendPrivateMessage(msg.SourceFarmer, "Can't move cabin as primary admin. (Your cabin is the farmhouse)"); 41 | return; 42 | } 43 | 44 | var cabinBlueprint = new BluePrint("Log Cabin"); 45 | var cabinLocation = new Vector2(fromPlayer.getTileX() + 1, fromPlayer.getTileY()); 46 | 47 | for (var x = 0; x < cabinBlueprint.tilesWidth; x++) 48 | { 49 | for (var y = 0; y < cabinBlueprint.tilesHeight; y++) 50 | { 51 | var currentTileLocation = new Vector2(cabinLocation.X + x, cabinLocation.Y + y); 52 | 53 | fromPlayer.currentLocation.terrainFeatures.Remove(currentTileLocation); 54 | fromPlayer.currentLocation.objects.Remove(currentTileLocation); 55 | 56 | } 57 | } 58 | 59 | var farmersCabin = Game1.getFarm().buildings.First(building => building.isCabin && ((Cabin)(building.indoors.Value)).owner.UniqueMultiplayerID == msg.SourceFarmer); 60 | farmersCabin.tileX.Value = (int)cabinLocation.X; 61 | farmersCabin.tileY.Value = (int)cabinLocation.Y; 62 | 63 | var indoor = (Cabin)farmersCabin.indoors.Value; 64 | foreach (var warp in indoor.warps.Where(warp => warp.TargetName == "Farm")) 65 | { 66 | warp.TargetX = (int)cabinLocation.X + 2; 67 | warp.TargetY = (int)cabinLocation.Y + 2; 68 | } 69 | } 70 | ); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GalaxyAuth/GalaxyAuthOverrides.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Galaxy.Api; 3 | using Junimohost.Stardewsteamauth.V1; 4 | using StardewModdingAPI; 5 | using StardewValley.SDKs; 6 | using Steamworks; 7 | 8 | namespace JunimoServer.Services.GalaxyAuth 9 | { 10 | public class GalaxyAuthOverrides 11 | { 12 | private static IMonitor _monitor; 13 | private static IModHelper _helper; 14 | private static StardewSteamAuthService.StardewSteamAuthServiceClient _steamAuthClient; 15 | 16 | private static GalaxyHelper.OperationalStateChangeListener _stateChangeListener; 17 | private static GalaxyHelper.AuthListener _authListener; 18 | 19 | 20 | public static void Initialize(IMonitor monitor, IModHelper helper, StardewSteamAuthService.StardewSteamAuthServiceClient steamAuthClient) 21 | { 22 | _monitor = monitor; 23 | _helper = helper; 24 | _steamAuthClient = steamAuthClient; 25 | } 26 | 27 | 28 | public static void Relog() 29 | { 30 | _monitor.Log("Galaxy auth lost Relogging", LogLevel.Info); 31 | try 32 | { 33 | _monitor.Log("Testing Sign Out", LogLevel.Info); 34 | GalaxyInstance.User().SignOut(); 35 | } 36 | catch (Exception e) 37 | { 38 | _monitor.Log(e.ToString(), LogLevel.Error); 39 | } 40 | _monitor.Log("Signing In", LogLevel.Info); 41 | var ticket = _steamAuthClient.GetSteamTicket(new GetSteamTicketRequest()); 42 | GalaxyInstance.User().SignInSteam(ticket.Ticket.ToByteArray(), Convert.ToUInt32(ticket.TicketLength), ticket.Name); 43 | 44 | } 45 | 46 | public static bool SteamHelperInitialize_Prefix(SteamHelper __instance) 47 | { 48 | var onGalaxyAuthSuccess = 49 | new Action(() => _helper.Reflection.GetMethod(__instance, "onGalaxyAuthSuccess").Invoke()); 50 | var onGalaxyAuthFailure = 51 | new Action((reason) => 52 | _helper.Reflection.GetMethod(__instance, "onGalaxyAuthFailure").Invoke(reason)); 53 | 54 | var onGalaxyStateChange = 55 | new Action((num) => _helper.Reflection.GetMethod(__instance, "onGalaxyStateChange").Invoke(num)); 56 | 57 | try 58 | { 59 | __instance.active = SteamAPI.Init(); 60 | Console.WriteLine("Steam logged on: " + SteamUser.BLoggedOn().ToString()); 61 | if (__instance.active) 62 | { 63 | _helper.Reflection.GetField(__instance, "_runningOnSteamDeck").SetValue(false); 64 | Console.WriteLine("Initializing GalaxySDK"); 65 | GalaxyInstance.Init(new InitParams("48767653913349277", 66 | "58be5c2e55d7f535cf8c4b6bbc09d185de90b152c8c42703cc13502465f0d04a", ".")); 67 | 68 | _authListener = 69 | new GalaxyHelper.AuthListener(onGalaxyAuthSuccess, onGalaxyAuthFailure, Relog); 70 | _stateChangeListener = new GalaxyHelper.OperationalStateChangeListener(onGalaxyStateChange); 71 | 72 | Console.WriteLine("Requesting Steam app ticket"); 73 | 74 | var ticket = _steamAuthClient.GetSteamTicket(new GetSteamTicketRequest()); 75 | GalaxyInstance.User().SignInSteam(ticket.Ticket.ToByteArray(), Convert.ToUInt32(ticket.TicketLength), ticket.Name); 76 | 77 | 78 | _helper.Reflection.GetProperty(__instance, "ConnectionProgress") 79 | .SetValue(__instance.ConnectionProgress + 1); 80 | } 81 | } 82 | catch (Exception value) 83 | { 84 | Console.WriteLine(value); 85 | __instance.active = false; 86 | _helper.Reflection.GetProperty(__instance, "ConnectionFinished").SetValue(true); 87 | } 88 | 89 | return false; 90 | } 91 | 92 | } 93 | } -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/customers/v1/customers_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc (unknown) 5 | // source: junimohost/customers/v1/customers.proto 6 | 7 | package customersv1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // CustomersServiceClient is the client API for CustomersService service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type CustomersServiceClient interface { 25 | GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) 26 | } 27 | 28 | type customersServiceClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewCustomersServiceClient(cc grpc.ClientConnInterface) CustomersServiceClient { 33 | return &customersServiceClient{cc} 34 | } 35 | 36 | func (c *customersServiceClient) GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) { 37 | out := new(GetMeResponse) 38 | err := c.cc.Invoke(ctx, "/junimohost.customers.v1.CustomersService/GetMe", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | // CustomersServiceServer is the server API for CustomersService service. 46 | // All implementations must embed UnimplementedCustomersServiceServer 47 | // for forward compatibility 48 | type CustomersServiceServer interface { 49 | GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) 50 | mustEmbedUnimplementedCustomersServiceServer() 51 | } 52 | 53 | // UnimplementedCustomersServiceServer must be embedded to have forward compatible implementations. 54 | type UnimplementedCustomersServiceServer struct { 55 | } 56 | 57 | func (UnimplementedCustomersServiceServer) GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) { 58 | return nil, status.Errorf(codes.Unimplemented, "method GetMe not implemented") 59 | } 60 | func (UnimplementedCustomersServiceServer) mustEmbedUnimplementedCustomersServiceServer() {} 61 | 62 | // UnsafeCustomersServiceServer may be embedded to opt out of forward compatibility for this service. 63 | // Use of this interface is not recommended, as added methods to CustomersServiceServer will 64 | // result in compilation errors. 65 | type UnsafeCustomersServiceServer interface { 66 | mustEmbedUnimplementedCustomersServiceServer() 67 | } 68 | 69 | func RegisterCustomersServiceServer(s grpc.ServiceRegistrar, srv CustomersServiceServer) { 70 | s.RegisterService(&CustomersService_ServiceDesc, srv) 71 | } 72 | 73 | func _CustomersService_GetMe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 74 | in := new(GetMeRequest) 75 | if err := dec(in); err != nil { 76 | return nil, err 77 | } 78 | if interceptor == nil { 79 | return srv.(CustomersServiceServer).GetMe(ctx, in) 80 | } 81 | info := &grpc.UnaryServerInfo{ 82 | Server: srv, 83 | FullMethod: "/junimohost.customers.v1.CustomersService/GetMe", 84 | } 85 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 86 | return srv.(CustomersServiceServer).GetMe(ctx, req.(*GetMeRequest)) 87 | } 88 | return interceptor(ctx, in, info, handler) 89 | } 90 | 91 | // CustomersService_ServiceDesc is the grpc.ServiceDesc for CustomersService service. 92 | // It's only intended for direct use with grpc.RegisterService, 93 | // and not to be introspected or modified (even as a copy) 94 | var CustomersService_ServiceDesc = grpc.ServiceDesc{ 95 | ServiceName: "junimohost.customers.v1.CustomersService", 96 | HandlerType: (*CustomersServiceServer)(nil), 97 | Methods: []grpc.MethodDesc{ 98 | { 99 | MethodName: "GetMe", 100 | Handler: _CustomersService_GetMe_Handler, 101 | }, 102 | }, 103 | Streams: []grpc.StreamDesc{}, 104 | Metadata: "junimohost/customers/v1/customers.proto", 105 | } 106 | -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/admin/v1/admin_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc (unknown) 5 | // source: junimohost/admin/v1/admin.proto 6 | 7 | package adminv1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // AdminServiceClient is the client API for AdminService service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type AdminServiceClient interface { 25 | UpgradeDeployment(ctx context.Context, in *UpgradeDeploymentRequest, opts ...grpc.CallOption) (*UpgradeDeploymentResponse, error) 26 | } 27 | 28 | type adminServiceClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewAdminServiceClient(cc grpc.ClientConnInterface) AdminServiceClient { 33 | return &adminServiceClient{cc} 34 | } 35 | 36 | func (c *adminServiceClient) UpgradeDeployment(ctx context.Context, in *UpgradeDeploymentRequest, opts ...grpc.CallOption) (*UpgradeDeploymentResponse, error) { 37 | out := new(UpgradeDeploymentResponse) 38 | err := c.cc.Invoke(ctx, "/junimohost.admin.v1.AdminService/UpgradeDeployment", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | // AdminServiceServer is the server API for AdminService service. 46 | // All implementations must embed UnimplementedAdminServiceServer 47 | // for forward compatibility 48 | type AdminServiceServer interface { 49 | UpgradeDeployment(context.Context, *UpgradeDeploymentRequest) (*UpgradeDeploymentResponse, error) 50 | mustEmbedUnimplementedAdminServiceServer() 51 | } 52 | 53 | // UnimplementedAdminServiceServer must be embedded to have forward compatible implementations. 54 | type UnimplementedAdminServiceServer struct { 55 | } 56 | 57 | func (UnimplementedAdminServiceServer) UpgradeDeployment(context.Context, *UpgradeDeploymentRequest) (*UpgradeDeploymentResponse, error) { 58 | return nil, status.Errorf(codes.Unimplemented, "method UpgradeDeployment not implemented") 59 | } 60 | func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {} 61 | 62 | // UnsafeAdminServiceServer may be embedded to opt out of forward compatibility for this service. 63 | // Use of this interface is not recommended, as added methods to AdminServiceServer will 64 | // result in compilation errors. 65 | type UnsafeAdminServiceServer interface { 66 | mustEmbedUnimplementedAdminServiceServer() 67 | } 68 | 69 | func RegisterAdminServiceServer(s grpc.ServiceRegistrar, srv AdminServiceServer) { 70 | s.RegisterService(&AdminService_ServiceDesc, srv) 71 | } 72 | 73 | func _AdminService_UpgradeDeployment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 74 | in := new(UpgradeDeploymentRequest) 75 | if err := dec(in); err != nil { 76 | return nil, err 77 | } 78 | if interceptor == nil { 79 | return srv.(AdminServiceServer).UpgradeDeployment(ctx, in) 80 | } 81 | info := &grpc.UnaryServerInfo{ 82 | Server: srv, 83 | FullMethod: "/junimohost.admin.v1.AdminService/UpgradeDeployment", 84 | } 85 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 86 | return srv.(AdminServiceServer).UpgradeDeployment(ctx, req.(*UpgradeDeploymentRequest)) 87 | } 88 | return interceptor(ctx, in, info, handler) 89 | } 90 | 91 | // AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service. 92 | // It's only intended for direct use with grpc.RegisterService, 93 | // and not to be introspected or modified (even as a copy) 94 | var AdminService_ServiceDesc = grpc.ServiceDesc{ 95 | ServiceName: "junimohost.admin.v1.AdminService", 96 | HandlerType: (*AdminServiceServer)(nil), 97 | Methods: []grpc.MethodDesc{ 98 | { 99 | MethodName: "UpgradeDeployment", 100 | Handler: _AdminService_UpgradeDeployment_Handler, 101 | }, 102 | }, 103 | Streams: []grpc.StreamDesc{}, 104 | Metadata: "junimohost/admin/v1/admin.proto", 105 | } 106 | -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/gamemanager/v1/gamemanager_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc (unknown) 5 | // source: junimohost/gamemanager/v1/gamemanager.proto 6 | 7 | package gamemanagerv1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | // GameManagerServiceClient is the client API for GameManagerService service. 23 | // 24 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 25 | type GameManagerServiceClient interface { 26 | // maybe need to return long running operation? idk how long this will take 27 | CreateGame(ctx context.Context, in *CreateGameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 28 | } 29 | 30 | type gameManagerServiceClient struct { 31 | cc grpc.ClientConnInterface 32 | } 33 | 34 | func NewGameManagerServiceClient(cc grpc.ClientConnInterface) GameManagerServiceClient { 35 | return &gameManagerServiceClient{cc} 36 | } 37 | 38 | func (c *gameManagerServiceClient) CreateGame(ctx context.Context, in *CreateGameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 39 | out := new(emptypb.Empty) 40 | err := c.cc.Invoke(ctx, "/junimohost.gamemanager.v1.GameManagerService/CreateGame", in, out, opts...) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return out, nil 45 | } 46 | 47 | // GameManagerServiceServer is the server API for GameManagerService service. 48 | // All implementations must embed UnimplementedGameManagerServiceServer 49 | // for forward compatibility 50 | type GameManagerServiceServer interface { 51 | // maybe need to return long running operation? idk how long this will take 52 | CreateGame(context.Context, *CreateGameRequest) (*emptypb.Empty, error) 53 | mustEmbedUnimplementedGameManagerServiceServer() 54 | } 55 | 56 | // UnimplementedGameManagerServiceServer must be embedded to have forward compatible implementations. 57 | type UnimplementedGameManagerServiceServer struct { 58 | } 59 | 60 | func (UnimplementedGameManagerServiceServer) CreateGame(context.Context, *CreateGameRequest) (*emptypb.Empty, error) { 61 | return nil, status.Errorf(codes.Unimplemented, "method CreateGame not implemented") 62 | } 63 | func (UnimplementedGameManagerServiceServer) mustEmbedUnimplementedGameManagerServiceServer() {} 64 | 65 | // UnsafeGameManagerServiceServer may be embedded to opt out of forward compatibility for this service. 66 | // Use of this interface is not recommended, as added methods to GameManagerServiceServer will 67 | // result in compilation errors. 68 | type UnsafeGameManagerServiceServer interface { 69 | mustEmbedUnimplementedGameManagerServiceServer() 70 | } 71 | 72 | func RegisterGameManagerServiceServer(s grpc.ServiceRegistrar, srv GameManagerServiceServer) { 73 | s.RegisterService(&GameManagerService_ServiceDesc, srv) 74 | } 75 | 76 | func _GameManagerService_CreateGame_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 77 | in := new(CreateGameRequest) 78 | if err := dec(in); err != nil { 79 | return nil, err 80 | } 81 | if interceptor == nil { 82 | return srv.(GameManagerServiceServer).CreateGame(ctx, in) 83 | } 84 | info := &grpc.UnaryServerInfo{ 85 | Server: srv, 86 | FullMethod: "/junimohost.gamemanager.v1.GameManagerService/CreateGame", 87 | } 88 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 89 | return srv.(GameManagerServiceServer).CreateGame(ctx, req.(*CreateGameRequest)) 90 | } 91 | return interceptor(ctx, in, info, handler) 92 | } 93 | 94 | // GameManagerService_ServiceDesc is the grpc.ServiceDesc for GameManagerService service. 95 | // It's only intended for direct use with grpc.RegisterService, 96 | // and not to be introspected or modified (even as a copy) 97 | var GameManagerService_ServiceDesc = grpc.ServiceDesc{ 98 | ServiceName: "junimohost.gamemanager.v1.GameManagerService", 99 | HandlerType: (*GameManagerServiceServer)(nil), 100 | Methods: []grpc.MethodDesc{ 101 | { 102 | MethodName: "CreateGame", 103 | Handler: _GameManagerService_CreateGame_Handler, 104 | }, 105 | }, 106 | Streams: []grpc.StreamDesc{}, 107 | Metadata: "junimohost/gamemanager/v1/gamemanager.proto", 108 | } 109 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/GameCreator/GameCreatorService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using JunimoServer.Services.CabinManager; 5 | using JunimoServer.Services.Daemon; 6 | using JunimoServer.Services.GameLoader; 7 | using JunimoServer.Services.PersistentOption; 8 | using Microsoft.Xna.Framework; 9 | using StardewModdingAPI; 10 | using StardewValley; 11 | 12 | namespace JunimoServer.Services.GameCreator 13 | { 14 | class GameCreatorService 15 | { 16 | private readonly DaemonService _daemonService; 17 | private readonly CabinManagerService _cabinManagerService; 18 | private readonly GameLoaderService _gameLoader; 19 | private static readonly Mutex CreateGameMutex = new Mutex(); 20 | private readonly PersistentOptions _options; 21 | private readonly IMonitor _monitor; 22 | private readonly IModHelper _helper; 23 | 24 | public bool GameIsCreating { get; private set; } 25 | 26 | public GameCreatorService(GameLoaderService gameLoader, PersistentOptions options, IMonitor monitor, 27 | DaemonService daemonService, CabinManagerService cabinManagerService, IModHelper helper) 28 | { 29 | _daemonService = daemonService; 30 | _options = options; 31 | _gameLoader = gameLoader; 32 | _monitor = monitor; 33 | _cabinManagerService = cabinManagerService; 34 | _helper = helper; 35 | } 36 | 37 | public bool CreateNewGameFromDaemonConfig() 38 | { 39 | try 40 | { 41 | var configTask = _daemonService.GetConfig(); 42 | configTask.Wait(); 43 | var config = configTask.Result; 44 | 45 | CreateNewGame(config); 46 | return true; 47 | } 48 | catch (Exception e) 49 | { 50 | _monitor.Log(e.ToString(), LogLevel.Error); 51 | return false; 52 | } 53 | } 54 | 55 | public void CreateNewGame(NewGameConfig config) 56 | { 57 | CreateGameMutex.WaitOne(); //prevent trying to start new game while in the middle of creating a game 58 | GameIsCreating = true; 59 | 60 | _options.SetPersistentOptions(new PersistentOptionsSaveData 61 | { 62 | MaxPlayers = config.MaxPlayers, 63 | CabinStrategy = (CabinStrategy)config.CabinStrategy 64 | }); 65 | 66 | 67 | Game1.player.team.useSeparateWallets.Value = config.UseSeparateWallets; 68 | Game1.startingCabins = 1; 69 | 70 | var isUltimateFarmModLoaded = _helper.ModRegistry.GetAll().Any(mod => mod.Manifest.Name == "Ultimate Farm CP"); 71 | if (isUltimateFarmModLoaded) 72 | { 73 | Game1.whichFarm = 1; // Ultimate Farm CP expects riverland == 1 74 | } 75 | else 76 | { 77 | Game1.whichFarm = config.WhichFarm; 78 | } 79 | 80 | var isWildernessFarm = config.WhichFarm == 4; 81 | Game1.spawnMonstersAtNight = isWildernessFarm; 82 | 83 | Game1.player.catPerson = config.CatPerson; 84 | 85 | Game1.player.Name = "junimohost"; 86 | Game1.player.displayName = Game1.player.Name; 87 | Game1.player.favoriteThing.Value = "Junimos"; 88 | Game1.player.farmName.Value = config.FarmName; 89 | 90 | Game1.player.isCustomized.Value = true; 91 | Game1.player.ConvertClothingOverrideToClothesItems(); 92 | 93 | Game1.multiplayerMode = 2; // multiplayer enabled 94 | 95 | // From TitleMenu.createdNewCharacter 96 | Game1.loadForNewGame(false); 97 | Game1.saveOnNewDay = true; 98 | Game1.player.eventsSeen.Add(60367); 99 | Game1.player.currentLocation = Utility.getHomeOfFarmer(Game1.player); 100 | Game1.player.Position = new Vector2(9f, 9f) * 64f; 101 | Game1.player.isInBed.Value = true; 102 | Game1.NewDay(0f); 103 | Game1.exitActiveMenu(); 104 | Game1.setGameMode(3); 105 | 106 | _gameLoader.SetCurrentGameAsSaveToLoad(config.FarmName); 107 | 108 | 109 | 110 | var initialCabin = Game1.getFarm().buildings.First(building => building.isCabin); 111 | var cabinLocation = new Vector2(initialCabin.tileX.Value, initialCabin.tileY.Value); 112 | 113 | _cabinManagerService.SetDefaultCabinLocation(cabinLocation); 114 | _cabinManagerService.MoveCabinToHiddenStack(initialCabin); 115 | 116 | 117 | GameIsCreating = false; 118 | CreateGameMutex.ReleaseMutex(); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/DebrisOptim/DebrisOptimizerOverrides.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using StardewModdingAPI; 3 | using StardewValley; 4 | using System.Collections.Generic; 5 | using Netcode; 6 | 7 | namespace JunimoServer.Services.DebrisOptim 8 | { 9 | public class DebrisOptimizerOverrides 10 | { 11 | private static IMonitor _monitor; 12 | private static IModHelper _helper; 13 | private static Queue _debrisQueue = new Queue(); 14 | private static HashSet _debrisSet = new HashSet(); 15 | private static int _maxUpdatesPerTick = 1; 16 | private static int _updatesThisTick = 0; 17 | private static Multiplayer _multiplayer; 18 | 19 | public class DebrisLocation 20 | { 21 | public Debris Debris { get; set; } 22 | public GameLocation Location { get; set; } 23 | 24 | public DebrisLocation(Debris debris, GameLocation location) 25 | { 26 | Debris = debris; 27 | Location = location; 28 | } 29 | } 30 | 31 | 32 | public static void Initialize(IMonitor monitor, IModHelper helper) 33 | { 34 | _monitor = monitor; 35 | _helper = helper; 36 | } 37 | 38 | // ReSharper disable twice InconsistentNaming 39 | public static bool updateChunks_Prefix(Debris __instance, GameTime time, GameLocation location, 40 | ref bool __result) 41 | { 42 | if (__instance.debrisType.Value != Debris.DebrisType.RESOURCE) 43 | { 44 | return true; 45 | } 46 | 47 | if (_updatesThisTick >= _maxUpdatesPerTick) 48 | { 49 | __result = false; 50 | return false; 51 | } 52 | 53 | 54 | Vector2 debrisPos = new Vector2(); 55 | foreach (Chunk chunk in __instance.Chunks) 56 | { 57 | debrisPos += chunk.position.Value; 58 | } 59 | 60 | debrisPos /= __instance.Chunks.Count; 61 | 62 | Farmer closestFarmer = null; 63 | var closestDistance = float.MaxValue; 64 | foreach (Farmer farmer in location.farmers) 65 | { 66 | var distance = (farmer.Position - debrisPos).LengthSquared(); 67 | if (distance < closestDistance) 68 | { 69 | closestFarmer = farmer; 70 | closestDistance = distance; 71 | } 72 | } 73 | 74 | if (closestFarmer == null) 75 | { 76 | __result = false; 77 | return false; 78 | } 79 | 80 | var added = false; 81 | _multiplayer ??= _helper.Reflection.GetField(typeof(Game1), "multiplayer").GetValue(); 82 | 83 | for (int index = __instance.Chunks.Count - 1; index >= 0; --index) 84 | { 85 | var chunk = __instance.Chunks[index]; 86 | 87 | int itemId = 88 | __instance.debrisType.Value is Debris.DebrisType.ARCHAEOLOGY or Debris.DebrisType.OBJECT 89 | ? chunk.debrisType 90 | : chunk.debrisType - chunk.debrisType % 2; 91 | 92 | Item item; 93 | if (chunk.debrisType != 93 && chunk.debrisType != 94) 94 | 95 | item = new Object(Vector2.Zero, itemId, 1) 96 | { 97 | Quality = __instance.itemQuality, 98 | }; 99 | else 100 | item = new Torch(Vector2.Zero, 1, itemId); 101 | 102 | var farmerCanAccept = closestFarmer.couldInventoryAcceptThisItem(item); 103 | if (farmerCanAccept) 104 | { 105 | closestFarmer.addItemToInventory(item); 106 | __instance.Chunks.RemoveAt(index); 107 | added = true; 108 | //TODO: keep track of whos inventory was changed and batch it so farmer delta broadcasting is done in Update() 109 | } 110 | } 111 | 112 | _updatesThisTick++; 113 | __result = __instance.Chunks.Count == 0; 114 | 115 | if (added) 116 | { 117 | var farmerRoot = _multiplayer.farmerRoot(closestFarmer.UniqueMultiplayerID); 118 | var delta = _multiplayer.writeObjectDeltaBytes(farmerRoot); 119 | foreach (var otherFarmer in Game1.otherFarmers) 120 | { 121 | if (otherFarmer.Value.UniqueMultiplayerID != Game1.player.UniqueMultiplayerID) 122 | otherFarmer.Value.queueMessage(0, closestFarmer, 123 | closestFarmer.UniqueMultiplayerID, delta); 124 | } 125 | } 126 | 127 | return false; 128 | } 129 | 130 | public static void Update() 131 | { 132 | _updatesThisTick = 0; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/NetworkTweaks/DesyncKicker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using HarmonyLib; 7 | using JunimoServer.Util; 8 | using StardewModdingAPI; 9 | using StardewModdingAPI.Events; 10 | using StardewModdingAPI.Utilities; 11 | using StardewValley; 12 | 13 | namespace JunimoServer.Services.NetworkTweaks 14 | { 15 | public class DesyncKicker 16 | { 17 | private const int barrierDesyncMaxTime = 20; 18 | private const int endOfDayDesyncMaxTime = 60; 19 | 20 | private readonly IModHelper _helper; 21 | private readonly IMonitor _monitor; 22 | 23 | private CancellationTokenSource _currentEndOfDayCancelToken; 24 | private CancellationTokenSource _currentNewDayBarrierCancelToken; 25 | 26 | public DesyncKicker(IModHelper helper, IMonitor monitor) 27 | { 28 | _helper = helper; 29 | _monitor = monitor; 30 | _helper.Events.GameLoop.DayEnding += OnDayEnding; 31 | _helper.Events.GameLoop.Saving += OnSaving; 32 | _helper.Events.GameLoop.Saved += OnSaved; 33 | 34 | } 35 | 36 | private void OnSaved(object sender, SavedEventArgs e) 37 | { 38 | 39 | _currentEndOfDayCancelToken.Cancel(); 40 | } 41 | 42 | private void OnSaving(object sender, SavingEventArgs e) 43 | { 44 | _currentNewDayBarrierCancelToken.Cancel(); 45 | _monitor.Log("Saving"); 46 | 47 | _currentEndOfDayCancelToken = new CancellationTokenSource(); 48 | var token = _currentEndOfDayCancelToken.Token; 49 | 50 | Task.Run(async () => 51 | { 52 | _monitor.Log($"waiting {endOfDayDesyncMaxTime} sec to kick non-ready players"); 53 | 54 | await Task.Delay(endOfDayDesyncMaxTime * 1000); 55 | if (token.IsCancellationRequested) 56 | { 57 | _monitor.Log($"waited {endOfDayDesyncMaxTime} sec to kick non-ready players. Was Canceled."); 58 | return; 59 | } 60 | 61 | _monitor.Log($"waited {endOfDayDesyncMaxTime} sec to kick non-ready players"); 62 | KickDesyncedPlayers(); 63 | }); 64 | } 65 | 66 | private void LogChecks() 67 | { 68 | _monitor.Log("-----------------------------------------------------"); 69 | 70 | var checks = new string[] 71 | { 72 | "ready_for_save", "wakeup" 73 | }; 74 | 75 | foreach (var farmer in Game1.getOnlineFarmers()) 76 | { 77 | foreach (var check in checks) 78 | { 79 | var passedCheck = Game1.player.team.IsOtherFarmerReady(check, farmer); 80 | _monitor.Log($"{farmer.Name}:{farmer.UniqueMultiplayerID} {check}:{passedCheck}"); 81 | } 82 | 83 | _monitor.Log( 84 | $"{farmer.Name}:{farmer.UniqueMultiplayerID} endOfNightStatus: {Game1.player.team.endOfNightStatus.GetStatusText(farmer.UniqueMultiplayerID)}"); 85 | } 86 | } 87 | 88 | 89 | private void OnDayEnding(object sender, DayEndingEventArgs e) 90 | { 91 | if (SDate.Now().IsDayZero()) return; 92 | _monitor.Log("DayEnding"); 93 | 94 | _currentNewDayBarrierCancelToken = new CancellationTokenSource(); 95 | var token = _currentNewDayBarrierCancelToken.Token; 96 | 97 | Task.Run(async () => 98 | { 99 | _monitor.Log($"waiting {barrierDesyncMaxTime} sec to kick barrier"); 100 | 101 | await Task.Delay(barrierDesyncMaxTime * 1000); 102 | if (token.IsCancellationRequested) 103 | { 104 | _monitor.Log($"waited {barrierDesyncMaxTime} sec to kick barrier. Was Canceled"); 105 | return; 106 | } 107 | _monitor.Log($"waited {barrierDesyncMaxTime} sec to kick barrier"); 108 | 109 | 110 | _monitor.Log("still stuck in barrier, going to try kicking"); 111 | 112 | var readyPlayers = _helper.Reflection.GetMethod(Game1.newDaySync, "barrierPlayers") 113 | .Invoke>("sleep"); 114 | foreach (var key in (IEnumerable)Game1.otherFarmers.Keys) 115 | { 116 | if (!readyPlayers.Contains(key)) 117 | { 118 | Game1.server.kick(key); 119 | _monitor.Log("kicking due to not making past barrier: " + key); 120 | } 121 | } 122 | 123 | }); 124 | } 125 | 126 | private void KickDesyncedPlayers() 127 | { 128 | foreach (var farmer in Game1.otherFarmers.Values.ToArray() 129 | .Where(farmer => Game1.player.team.endOfNightStatus.GetStatusText(farmer.UniqueMultiplayerID) != "ready")) 130 | { 131 | _monitor.Log($"Kicking {farmer.Name} because they aren't ready"); 132 | Game1.server.kick(farmer.UniqueMultiplayerID); 133 | } 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/billing/v1/billing_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc (unknown) 5 | // source: junimohost/billing/v1/billing.proto 6 | 7 | package billingv1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // BillingServiceClient is the client API for BillingService service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type BillingServiceClient interface { 25 | CreateCheckoutSession(ctx context.Context, in *CreateCheckoutSessionRequest, opts ...grpc.CallOption) (*CreateCheckoutSessionResponse, error) 26 | GetCustomerPortal(ctx context.Context, in *GetCustomerPortalRequest, opts ...grpc.CallOption) (*GetCustomerPortalResponse, error) 27 | } 28 | 29 | type billingServiceClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewBillingServiceClient(cc grpc.ClientConnInterface) BillingServiceClient { 34 | return &billingServiceClient{cc} 35 | } 36 | 37 | func (c *billingServiceClient) CreateCheckoutSession(ctx context.Context, in *CreateCheckoutSessionRequest, opts ...grpc.CallOption) (*CreateCheckoutSessionResponse, error) { 38 | out := new(CreateCheckoutSessionResponse) 39 | err := c.cc.Invoke(ctx, "/junimohost.billing.v1.BillingService/CreateCheckoutSession", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *billingServiceClient) GetCustomerPortal(ctx context.Context, in *GetCustomerPortalRequest, opts ...grpc.CallOption) (*GetCustomerPortalResponse, error) { 47 | out := new(GetCustomerPortalResponse) 48 | err := c.cc.Invoke(ctx, "/junimohost.billing.v1.BillingService/GetCustomerPortal", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | // BillingServiceServer is the server API for BillingService service. 56 | // All implementations must embed UnimplementedBillingServiceServer 57 | // for forward compatibility 58 | type BillingServiceServer interface { 59 | CreateCheckoutSession(context.Context, *CreateCheckoutSessionRequest) (*CreateCheckoutSessionResponse, error) 60 | GetCustomerPortal(context.Context, *GetCustomerPortalRequest) (*GetCustomerPortalResponse, error) 61 | mustEmbedUnimplementedBillingServiceServer() 62 | } 63 | 64 | // UnimplementedBillingServiceServer must be embedded to have forward compatible implementations. 65 | type UnimplementedBillingServiceServer struct { 66 | } 67 | 68 | func (UnimplementedBillingServiceServer) CreateCheckoutSession(context.Context, *CreateCheckoutSessionRequest) (*CreateCheckoutSessionResponse, error) { 69 | return nil, status.Errorf(codes.Unimplemented, "method CreateCheckoutSession not implemented") 70 | } 71 | func (UnimplementedBillingServiceServer) GetCustomerPortal(context.Context, *GetCustomerPortalRequest) (*GetCustomerPortalResponse, error) { 72 | return nil, status.Errorf(codes.Unimplemented, "method GetCustomerPortal not implemented") 73 | } 74 | func (UnimplementedBillingServiceServer) mustEmbedUnimplementedBillingServiceServer() {} 75 | 76 | // UnsafeBillingServiceServer may be embedded to opt out of forward compatibility for this service. 77 | // Use of this interface is not recommended, as added methods to BillingServiceServer will 78 | // result in compilation errors. 79 | type UnsafeBillingServiceServer interface { 80 | mustEmbedUnimplementedBillingServiceServer() 81 | } 82 | 83 | func RegisterBillingServiceServer(s grpc.ServiceRegistrar, srv BillingServiceServer) { 84 | s.RegisterService(&BillingService_ServiceDesc, srv) 85 | } 86 | 87 | func _BillingService_CreateCheckoutSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 88 | in := new(CreateCheckoutSessionRequest) 89 | if err := dec(in); err != nil { 90 | return nil, err 91 | } 92 | if interceptor == nil { 93 | return srv.(BillingServiceServer).CreateCheckoutSession(ctx, in) 94 | } 95 | info := &grpc.UnaryServerInfo{ 96 | Server: srv, 97 | FullMethod: "/junimohost.billing.v1.BillingService/CreateCheckoutSession", 98 | } 99 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 100 | return srv.(BillingServiceServer).CreateCheckoutSession(ctx, req.(*CreateCheckoutSessionRequest)) 101 | } 102 | return interceptor(ctx, in, info, handler) 103 | } 104 | 105 | func _BillingService_GetCustomerPortal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(GetCustomerPortalRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(BillingServiceServer).GetCustomerPortal(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: "/junimohost.billing.v1.BillingService/GetCustomerPortal", 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(BillingServiceServer).GetCustomerPortal(ctx, req.(*GetCustomerPortalRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | // BillingService_ServiceDesc is the grpc.ServiceDesc for BillingService service. 124 | // It's only intended for direct use with grpc.RegisterService, 125 | // and not to be introspected or modified (even as a copy) 126 | var BillingService_ServiceDesc = grpc.ServiceDesc{ 127 | ServiceName: "junimohost.billing.v1.BillingService", 128 | HandlerType: (*BillingServiceServer)(nil), 129 | Methods: []grpc.MethodDesc{ 130 | { 131 | MethodName: "CreateCheckoutSession", 132 | Handler: _BillingService_CreateCheckoutSession_Handler, 133 | }, 134 | { 135 | MethodName: "GetCustomerPortal", 136 | Handler: _BillingService_GetCustomerPortal_Handler, 137 | }, 138 | }, 139 | Streams: []grpc.StreamDesc{}, 140 | Metadata: "junimohost/billing/v1/billing.proto", 141 | } 142 | -------------------------------------------------------------------------------- /daemon/internal/backup/backup_service.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "io" 9 | "io/fs" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | "time" 15 | 16 | "cloud.google.com/go/storage" 17 | pbsd "github.com/junimohost/game-daemon/gen/proto/go/junimohost/stardewdaemon/v1" 18 | "github.com/junimohost/game-daemon/internal/util" 19 | "github.com/junimohost/game-daemon/internal/zips" 20 | "github.com/junimohost/game-daemon/pkg/stardewtypes" 21 | "go.uber.org/zap" 22 | ) 23 | 24 | const ( 25 | GameDataPath = "/config/xdg/config/StardewValley" 26 | StartupPreferencesPath = GameDataPath + "/startup_preferences" 27 | SavesPath = GameDataPath + "/Saves" 28 | SmapiDataPath = GameDataPath + "/.smapi" 29 | GameLoaderJsonPath = SmapiDataPath + "/mod-data/junimohost.server/junimohost.gameloader.json" 30 | 31 | OldSaveGameInfo = "SaveGameInfo_old" 32 | ) 33 | 34 | type Service struct { 35 | storageClient *storage.Client 36 | stardewDaemonClient pbsd.StardewDaemonServiceClient 37 | backupBucketName string 38 | serverID string 39 | backendAvailable bool 40 | } 41 | 42 | func NewService(storageClient *storage.Client, stardewDaemonClient pbsd.StardewDaemonServiceClient, backupBucketName string, serverID string, backendAvailable bool) *Service { 43 | return &Service{storageClient: storageClient, stardewDaemonClient: stardewDaemonClient, backupBucketName: backupBucketName, serverID: serverID, backendAvailable: backendAvailable} 44 | } 45 | 46 | func (s *Service) CreateBackup(ctx context.Context, date stardewtypes.SDate) error { 47 | objectName, err := s.uploadBackupZipToGCS(ctx) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if s.backendAvailable { 53 | _, err = s.stardewDaemonClient.IndexBackup(ctx, &pbsd.IndexBackupRequest{ 54 | ServerId: s.serverID, 55 | GcsPath: "gs://" + s.backupBucketName + "/" + objectName, 56 | StardewYear: int64(date.Year), 57 | StardewSeason: pbsd.StardewSeasons(date.Season), 58 | StardewDay: int64(date.Day), 59 | }) 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | 65 | return nil 66 | 67 | } 68 | 69 | func (s *Service) RestoreBackup(ctx context.Context, backupPath string) error { 70 | 71 | bucketName, objectName := util.GetBucketAndObjectFromGcsPath(backupPath) 72 | gcsR, err := s.storageClient.Bucket(bucketName).Object(objectName).NewReader(ctx) 73 | if err != nil { 74 | zap.S().Errorf("Failed to find backup: %v", err) 75 | return err 76 | } 77 | defer gcsR.Close() 78 | 79 | backupFileBuffer, err := ioutil.ReadAll(gcsR) //backup size is not going to be larger than 20-30MB (with mods) 80 | if err != nil { 81 | zap.S().Errorf("Failed to download backup: %v", err) 82 | return err 83 | } 84 | 85 | // remove localfiles 86 | err = s.deleteCurrentGameFiles(err) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | // unzip 92 | backupFileReader := bytes.NewReader(backupFileBuffer) 93 | zipReader, err := zip.NewReader(backupFileReader, int64(len(backupFileBuffer))) 94 | if err != nil { 95 | zap.S().Errorf("Failed to open zip: %v", err) 96 | return err 97 | } 98 | 99 | zips.ExtractZipToPath(GameDataPath, zipReader) 100 | return nil 101 | } 102 | 103 | func (s *Service) deleteCurrentGameFiles(err error) error { 104 | // just get direct children and let RemoveAll handle sub dirs 105 | filesToDelete, err := filepath.Glob(GameDataPath + "/*") 106 | if err != nil { 107 | zap.S().Errorf("Failed to Glob files: %v", err) 108 | return err 109 | } 110 | 111 | for _, file := range filesToDelete { 112 | if err := os.RemoveAll(file); err != nil { 113 | zap.S().Errorf("Failed to clear game files: %v", err) 114 | return err 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | func (s *Service) uploadBackupZipToGCS(ctx context.Context) (string, error) { 121 | objectName := s.serverID + "/" + strconv.Itoa(int(time.Now().Unix())) 122 | gcsW := s.storageClient.Bucket(s.backupBucketName).Object(objectName).NewWriter(ctx) 123 | defer gcsW.Close() 124 | 125 | err := s.createBackupZip(gcsW) 126 | if err != nil { 127 | return "", err 128 | } 129 | 130 | return objectName, nil 131 | } 132 | 133 | func (s *Service) createBackupZip(writer io.Writer) error { 134 | zipW := zip.NewWriter(writer) 135 | defer zipW.Close() 136 | 137 | gameName, err := getCurrentGameName() 138 | if err != nil { 139 | zap.S().Errorf("Failed to create backup: %v", err) 140 | return err 141 | } 142 | oldGameName := gameName + "_old" 143 | 144 | err = filepath.Walk(GameDataPath, func(path string, info fs.FileInfo, err error) error { 145 | if err != nil { 146 | return err 147 | } 148 | 149 | // skip ErrorLogs folder 150 | if info.IsDir() && info.Name() == "ErrorLogs" { 151 | return filepath.SkipDir 152 | } 153 | 154 | // skip "_old" files 155 | if info.Name() == oldGameName || info.Name() == OldSaveGameInfo { 156 | return nil 157 | } 158 | 159 | // skip dirs 160 | if info.IsDir() { 161 | return nil 162 | } 163 | 164 | header, err := zip.FileInfoHeader(info) 165 | if err != nil { 166 | return err 167 | } 168 | header.Method = zip.Deflate 169 | header.Name, err = filepath.Rel(GameDataPath, path) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | headerWriter, err := zipW.CreateHeader(header) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | f, err := os.Open(path) 180 | if err != nil { 181 | return err 182 | } 183 | defer f.Close() 184 | 185 | zap.S().Debugf("Adding To Archive: %v", header.Name) 186 | _, err = io.Copy(headerWriter, f) 187 | return err 188 | 189 | }) 190 | 191 | if err != nil { 192 | zap.S().Errorf("Failed walking Stardew data folder: %v", err) 193 | return err 194 | } 195 | 196 | return nil 197 | } 198 | 199 | type GameLoaderJsonData struct { 200 | SaveNameToLoad string `json:"SaveNameToLoad"` 201 | } 202 | 203 | func getCurrentGameName() (string, error) { 204 | loaderJson, err := ioutil.ReadFile(GameLoaderJsonPath) 205 | if err != nil { 206 | zap.S().Errorf("Couldn't find GameLoader json file: %v", err) 207 | return "", err 208 | } 209 | 210 | var loaderData GameLoaderJsonData 211 | err = json.Unmarshal(loaderJson, &loaderData) 212 | 213 | if err != nil { 214 | zap.S().Errorf("Couldn't parse GameLoader json: %v", err) 215 | return "", err 216 | } 217 | 218 | return loaderData.SaveNameToLoad, nil 219 | } 220 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/ServerOptim/ServerOptimizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using HarmonyLib; 4 | using Microsoft.VisualBasic; 5 | using Microsoft.VisualBasic.CompilerServices; 6 | using StardewModdingAPI; 7 | using StardewModdingAPI.Events; 8 | using StardewValley; 9 | using StardewValley.SDKs; 10 | 11 | namespace JunimoServer.Services.ServerOptim 12 | { 13 | public class ServerOptimizer 14 | { 15 | private readonly bool _disableRendering; 16 | private readonly IMonitor _monitor; 17 | 18 | public ServerOptimizer( 19 | Harmony harmony, 20 | IMonitor monitor, 21 | IModHelper helper, 22 | bool disableRendering, 23 | bool enableModIncompatibleOptimizations 24 | ) 25 | { 26 | _monitor = monitor; 27 | _disableRendering = disableRendering; 28 | ServerOptimizerOverrides.Initialize(monitor); 29 | harmony.Patch( 30 | original: AccessTools.Method(typeof(GameRunner), "BeginDraw"), 31 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 32 | nameof(ServerOptimizerOverrides.Draw_Prefix)) 33 | ); 34 | 35 | 36 | 37 | 38 | harmony.Patch( 39 | original: AccessTools.Method("StardewValley.Game1:updateMusic"), 40 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 41 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 42 | 43 | harmony.Patch( 44 | original: AccessTools.Method("StardewValley.BellsAndWhistles.Butterfly:update"), 45 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 46 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 47 | 48 | harmony.Patch( 49 | original: AccessTools.Method("StardewValley.BellsAndWhistles.AmbientLocationSounds:update"), 50 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 51 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 52 | 53 | harmony.Patch( 54 | original: AccessTools.Method(typeof(GalaxySocket), "CreateLobby"), 55 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 56 | nameof(ServerOptimizerOverrides.CreateLobby_Prefix))); 57 | 58 | 59 | if (disableRendering) 60 | { 61 | harmony.Patch( 62 | original: AccessTools.Method("StardewModdingAPI.Framework.SCore:OnInstanceContentLoaded"), 63 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 64 | nameof(ServerOptimizerOverrides.AssignNullDisplay_Prefix))); 65 | 66 | harmony.Patch( 67 | original: AccessTools.Method("StardewModdingAPI.Framework.SCore:GetMapDisplayDevice"), 68 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 69 | nameof(ServerOptimizerOverrides.ReturnNullDisplay_Prefix))); 70 | 71 | harmony.Patch( 72 | original: AccessTools.Method("Microsoft.Xna.Framework.Input.Keyboard:PlatformGetState"), 73 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 74 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 75 | 76 | harmony.Patch( 77 | original: AccessTools.Method("Microsoft.Xna.Framework.Input.Mouse:PlatformGetState"), 78 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 79 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 80 | 81 | harmony.Patch( 82 | original: AccessTools.Method("StardewValley.Game1:UpdateControlInput"), 83 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 84 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 85 | } 86 | 87 | 88 | 89 | if (enableModIncompatibleOptimizations) 90 | { 91 | harmony.Patch( 92 | original: AccessTools.Method("StardewModdingAPI.Framework.StateTracking.Snapshots.PlayerSnapshot:Update"), 93 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 94 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 95 | 96 | 97 | harmony.Patch( 98 | original: AccessTools.Method("StardewModdingAPI.Framework.StateTracking.Snapshots.WorldLocationsSnapshot:Update"), 99 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 100 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 101 | 102 | harmony.Patch( 103 | original: AccessTools.Method("StardewModdingAPI.Framework.StateTracking.PlayerTracker:Update"), 104 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 105 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 106 | 107 | harmony.Patch( 108 | original: AccessTools.Method("StardewModdingAPI.Framework.StateTracking.WorldLocationsTracker:Update"), 109 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 110 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 111 | 112 | harmony.Patch( 113 | original: AccessTools.Method("StardewModdingAPI.Framework.StateTracking.WorldLocationsTracker:Reset"), 114 | prefix: new HarmonyMethod(typeof(ServerOptimizerOverrides), 115 | nameof(ServerOptimizerOverrides.Disable_Prefix))); 116 | } 117 | 118 | helper.Events.GameLoop.DayStarted += OnDayStarted; 119 | helper.Events.GameLoop.Saving += OnSaving; 120 | helper.Events.GameLoop.DayEnding += OnDayEnding; 121 | 122 | } 123 | 124 | private void OnDayEnding(object sender, DayEndingEventArgs e) 125 | { 126 | var before = checked ((long) Math.Round(Process.GetCurrentProcess().PrivateMemorySize64 / 1024.0 / 1024.0)); 127 | _monitor.Log($"Running GC", LogLevel.Info); 128 | GC.Collect(generation: 0, GCCollectionMode.Forced, blocking: true); 129 | GC.Collect(generation: 1, GCCollectionMode.Forced, blocking: true); 130 | GC.Collect(generation: 2, GCCollectionMode.Forced, blocking: true); 131 | var after = checked ((long) Math.Round(Process.GetCurrentProcess().PrivateMemorySize64 / 1024.0 / 1024.0)); 132 | var beforeFormatted = Strings.Format(before / 1024.0, "0.00") + " GB"; 133 | var afterFormatted = Strings.Format(after / 1024.0, "0.00") + " GB"; 134 | _monitor.Log($"Ran GC {beforeFormatted} -> {afterFormatted}", LogLevel.Info); 135 | } 136 | 137 | private void OnDayStarted(object sender, DayStartedEventArgs e) 138 | { 139 | if (_disableRendering) 140 | { 141 | ServerOptimizerOverrides.DisableDrawing(); 142 | } 143 | } 144 | 145 | private void OnSaving(object sender, SavingEventArgs e) 146 | { 147 | if (_disableRendering) 148 | { 149 | ServerOptimizerOverrides.EnableDrawing(); 150 | } 151 | } 152 | 153 | } 154 | } -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CabinManager/CabinManagerService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using HarmonyLib; 4 | using JunimoServer.Services.PersistentOption; 5 | using Microsoft.Xna.Framework; 6 | using StardewModdingAPI; 7 | using StardewModdingAPI.Events; 8 | using StardewValley; 9 | using StardewValley.Buildings; 10 | using StardewValley.Locations; 11 | using StardewValley.Network; 12 | 13 | namespace JunimoServer.Services.CabinManager 14 | { 15 | public enum CabinStrategy 16 | { 17 | CabinStack, 18 | FarmhouseStack 19 | } 20 | 21 | public class CabinManagerData 22 | { 23 | public Vector2? DefaultCabinLocation = null; 24 | public HashSet AllPlayerIdsEverJoined = new HashSet(); 25 | } 26 | 27 | 28 | public class CabinManagerService 29 | { 30 | private readonly IModHelper _helper; 31 | private readonly IMonitor _monitor; 32 | private CabinManagerData _data; 33 | 34 | private CabinManagerData Data 35 | { 36 | get => _data; 37 | set 38 | { 39 | _data = value; 40 | CabinManagerOverrides.SetCabinManagerData(_data); 41 | } 42 | } 43 | 44 | private const string cabinManagerDataKey = "JunimoHost.CabinManager.data"; 45 | private const int minEmptyCabins = 4; 46 | public const int HiddenCabinX = -20; 47 | public const int HiddenCabinY = -20; 48 | 49 | private readonly PersistentOptions _options; 50 | private readonly HashSet farmersInFarmhouse = new HashSet(); 51 | 52 | public CabinManagerService(IModHelper helper, IMonitor monitor, Harmony harmony, PersistentOptions options, 53 | bool debug = false) 54 | { 55 | _helper = helper; 56 | _monitor = monitor; 57 | _options = options; 58 | Data = new CabinManagerData(); 59 | CabinManagerOverrides.Initialize(helper, monitor, options, OnServerJoined); 60 | 61 | _helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; 62 | _helper.Events.GameLoop.UpdateTicked += OnTicked; 63 | if (debug) 64 | { 65 | harmony.Patch( 66 | original: AccessTools.Method(typeof(Multiplayer), nameof(Multiplayer.processIncomingMessage)), 67 | postfix: new HarmonyMethod(typeof(CabinManagerOverrides), 68 | nameof(CabinManagerOverrides.processIncomingMessage_Postfix)) 69 | ); 70 | } 71 | 72 | 73 | harmony.Patch( 74 | original: AccessTools.Method(typeof(GameServer), "sendMessage", 75 | new[] { typeof(long), typeof(OutgoingMessage) }), 76 | prefix: new HarmonyMethod(typeof(CabinManagerOverrides), 77 | nameof(CabinManagerOverrides.sendMessage_Prefix)) 78 | ); 79 | 80 | harmony.Patch( 81 | original: AccessTools.Method(typeof(GameServer), nameof(GameServer.sendServerIntroduction)), 82 | postfix: new HarmonyMethod(typeof(CabinManagerOverrides), 83 | nameof(CabinManagerOverrides.sendServerIntroduction_Postfix)) 84 | ); 85 | } 86 | 87 | private void OnServerJoined(long peerId) 88 | { 89 | Data.AllPlayerIdsEverJoined.Add(peerId); 90 | _helper.Data.WriteSaveData(cabinManagerDataKey, Data); 91 | EnsureAtLeastXCabins(); 92 | } 93 | 94 | 95 | private void OnTicked(object sender, UpdateTickedEventArgs e) 96 | { 97 | MonitorFarmhouse(); 98 | } 99 | 100 | private void MonitorFarmhouse() 101 | { 102 | if (!Game1.hasLoadedGame) return; 103 | 104 | var idsInFarmHouse = new HashSet(); 105 | 106 | foreach (var farmer in Game1.getLocationFromName("Farmhouse").farmers) 107 | { 108 | idsInFarmHouse.Add(farmer.UniqueMultiplayerID); 109 | } 110 | 111 | foreach (var farmer in Game1.getLocationFromName("Farmhouse").farmers) 112 | { 113 | var id = farmer.UniqueMultiplayerID; 114 | if (!farmersInFarmhouse.Contains(id)) 115 | { 116 | farmersInFarmhouse.Add(id); 117 | OnFarmerEnteredFarmhouse(farmer); 118 | } 119 | } 120 | 121 | farmersInFarmhouse.RemoveWhere(farmerId => !idsInFarmHouse.Contains(farmerId)); 122 | } 123 | 124 | private void OnFarmerEnteredFarmhouse(Farmer farmer) 125 | { 126 | if (farmer.UniqueMultiplayerID == Game1.player.UniqueMultiplayerID) return; 127 | 128 | 129 | Building farmersCabin; 130 | if (_options.Data.CabinStrategy == CabinStrategy.FarmhouseStack) 131 | { 132 | farmersCabin = Game1.getFarm().buildings.First(building => 133 | building.isCabin && ((Cabin)building.indoors.Value).owner.UniqueMultiplayerID == 134 | farmer.UniqueMultiplayerID); 135 | } 136 | else 137 | { 138 | farmersCabin = Game1.getFarm().buildings.First(building => building.isCabin); 139 | } 140 | 141 | 142 | var farmersCabinUniqueLocation = farmersCabin.nameOfIndoors; 143 | var farmersCabinEntrypoint = ((Cabin)farmersCabin.indoors.Value).getEntryLocation(); 144 | 145 | // Pass out request 146 | Game1.server.sendMessage(farmer.UniqueMultiplayerID, 29, Game1.player, new object[] 147 | { 148 | farmersCabinUniqueLocation, farmersCabinEntrypoint.X, farmersCabinEntrypoint.Y, true 149 | }); 150 | } 151 | 152 | 153 | private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) 154 | { 155 | Data = _helper.Data.ReadSaveData(cabinManagerDataKey) ?? new CabinManagerData(); 156 | } 157 | 158 | public void SetDefaultCabinLocation(Vector2 location) 159 | { 160 | Data.DefaultCabinLocation = location; 161 | _helper.Data.WriteSaveData(cabinManagerDataKey, Data); 162 | } 163 | 164 | public void MoveCabinToHiddenStack(Building cabin) 165 | { 166 | cabin.tileX.Value = HiddenCabinX; 167 | cabin.tileY.Value = HiddenCabinY; 168 | } 169 | 170 | public void EnsureAtLeastXCabins() 171 | { 172 | var cabinBlueprint = new BluePrint("Log Cabin"); 173 | var outOfBoundsLocation = new Vector2(HiddenCabinX, HiddenCabinY); 174 | 175 | 176 | var numEmptyCabins = Game1.getFarm().buildings.Where(building => building.isCabin) 177 | .Count(cabin => 178 | !Data.AllPlayerIdsEverJoined.Contains( 179 | ((Cabin)cabin.indoors.Value).farmhand.Value.UniqueMultiplayerID) 180 | ); 181 | 182 | 183 | var cabinsToBuild = minEmptyCabins - numEmptyCabins; 184 | if (cabinsToBuild <= 0) return; 185 | 186 | for (var i = 0; i < cabinsToBuild; i++) 187 | { 188 | if (Game1.getFarm().buildStructure(cabinBlueprint, outOfBoundsLocation, Game1.player, 189 | skipSafetyChecks: true)) 190 | { 191 | var cabin = Game1.getFarm().buildings.Last(); 192 | cabin.daysOfConstructionLeft.Value = 0; 193 | } 194 | } 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /mod/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Ww][Ii][Nn]32/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Oo]ut/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | .DS_Store 368 | -------------------------------------------------------------------------------- /mod/JunimoServer/Services/CropSaver/CropSaver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using HarmonyLib; 3 | using Netcode; 4 | using StardewModdingAPI; 5 | using StardewModdingAPI.Events; 6 | using StardewModdingAPI.Utilities; 7 | using StardewValley; 8 | using StardewValley.TerrainFeatures; 9 | 10 | namespace JunimoServer.Services.CropSaver 11 | { 12 | public class CropSaver 13 | { 14 | 15 | private readonly CropWatcher _cropWatcher; 16 | private readonly CropSaverDataLoader _cropSaverDataLoader; 17 | 18 | private readonly IMonitor _monitor; 19 | private readonly IModHelper _helper; 20 | 21 | public CropSaver(IModHelper helper, Harmony harmony, IMonitor monitor) 22 | { 23 | _monitor = monitor; 24 | _helper = helper; 25 | _cropWatcher = new CropWatcher(helper, OnCropAdded, OnCropRemoved); 26 | _cropSaverDataLoader = new CropSaverDataLoader(helper); 27 | CropSaverOverrides.Initialize(helper, _monitor, _cropSaverDataLoader); 28 | harmony.Patch( 29 | original: AccessTools.Method(typeof(Crop), nameof(Crop.Kill)), 30 | prefix: new HarmonyMethod(typeof(CropSaverOverrides), nameof(CropSaverOverrides.KillCrop_Prefix)) 31 | ); 32 | 33 | helper.Events.GameLoop.Saving += OnSaving; 34 | helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; 35 | helper.Events.GameLoop.DayEnding += OnDayEnd; 36 | } 37 | 38 | 39 | private void OnDayEnd(object sender, DayEndingEventArgs e) 40 | { 41 | //prolong crops 42 | var onlineIds = new HashSet(); 43 | foreach (var farmer in Game1.getOnlineFarmers()) 44 | { 45 | onlineIds.Add(farmer.UniqueMultiplayerID); 46 | } 47 | 48 | 49 | _cropSaverDataLoader.GetSaverCrops().ForEach(saverCrop => 50 | { 51 | var dirt = saverCrop.TryGetCoorespondingDirt(); 52 | if (dirt != null) 53 | { 54 | if (!onlineIds.Contains(saverCrop.ownerId) && dirt.state.Value != HoeDirt.watered) 55 | { 56 | saverCrop.IncrementExtraDays(); 57 | } 58 | } 59 | }); 60 | 61 | //remove crops 62 | for (var i = _cropSaverDataLoader.GetSaverCrops().Count - 1; i >= 0; i--) 63 | { 64 | var saverCrop = _cropSaverDataLoader.GetSaverCrops()[i]; 65 | var crop = saverCrop.TryGetCoorespondingCrop(); 66 | if (crop == null) 67 | { 68 | _monitor.Log( 69 | $"Crop at {saverCrop.cropLocationTile.X}, {saverCrop.cropLocationTile.Y} was still " + 70 | $"being managed by CropSaver after death." + 71 | $"\nRemoving from managed crops...", LogLevel.Warn); 72 | _cropSaverDataLoader.RemoveCrop(saverCrop.cropLocationName, saverCrop.cropLocationTile); 73 | continue; 74 | } 75 | 76 | 77 | var nightOfDeath = CalculateDateOfDeath(crop, saverCrop); 78 | var fullyGrown = CalculateFullyGrown(crop); 79 | var earliestFullyGrownDate = CalculateEarliestPossibleFullyGrownDate(crop, saverCrop); 80 | var now = SDate.Now(); 81 | var isAfterDateOfDeath = now >= nightOfDeath; 82 | 83 | if (!fullyGrown && now.Day == 28 && nightOfDeath < earliestFullyGrownDate) 84 | { 85 | KillCrop(saverCrop, crop); 86 | } 87 | else if (isAfterDateOfDeath && !(fullyGrown && onlineIds.Contains(saverCrop.ownerId))) 88 | { 89 | KillCrop(saverCrop, crop); 90 | } 91 | } 92 | } 93 | private void KillCrop(SaverCrop saverCrop, Crop crop) 94 | { 95 | 96 | _cropSaverDataLoader.RemoveCrop(saverCrop.cropLocationName, saverCrop.cropLocationTile); 97 | var dead = _helper.Reflection.GetField(crop, "dead").GetValue(); 98 | var raisedSeeds = _helper.Reflection.GetField(crop, "raisedSeeds").GetValue(); 99 | 100 | dead.Value = true; 101 | raisedSeeds.Value = false; 102 | 103 | _monitor.Log($"Killing crop owned by {saverCrop.ownerId}"); 104 | } 105 | 106 | private bool CalculateFullyGrown(Crop crop) 107 | { 108 | var currentPhase = _helper.Reflection.GetField(crop, "currentPhase").GetValue().Value; 109 | var phaseDays = _helper.Reflection.GetField(crop, "phaseDays").GetValue(); 110 | 111 | 112 | var fullyGrown = (currentPhase >= phaseDays.Count - 1); 113 | return fullyGrown; 114 | } 115 | 116 | 117 | private SDate CalculateEarliestPossibleFullyGrownDate(Crop crop, SaverCrop saverCrop) 118 | { 119 | if (CalculateFullyGrown(crop)) return SDate.Now(); 120 | 121 | var dirt = saverCrop.TryGetCoorespondingDirt(); 122 | if (dirt == null) return SDate.Now(); 123 | 124 | var extraDayForUnwatered = 1; 125 | if (dirt.state.Value == HoeDirt.watered) 126 | { 127 | extraDayForUnwatered = 0; 128 | } 129 | 130 | var phaseDays = _helper.Reflection.GetField(crop, "phaseDays").GetValue(); 131 | var currentPhase = _helper.Reflection.GetField(crop, "currentPhase").GetValue().Value; 132 | 133 | var daysOfCurrentPhase = _helper.Reflection.GetField(crop, "dayOfCurrentPhase").GetValue().Value; 134 | 135 | var daysLeftOfCurrentPhase = phaseDays[currentPhase] - daysOfCurrentPhase; 136 | var daysLeftOfPhasesUntilGrown = 0; 137 | 138 | for (int i = currentPhase + 1; i < phaseDays.Count - 1; i++) 139 | { 140 | daysLeftOfPhasesUntilGrown += phaseDays[i]; 141 | } 142 | 143 | return SDate.Now().AddDays(daysLeftOfCurrentPhase + daysLeftOfPhasesUntilGrown + extraDayForUnwatered); 144 | } 145 | private static SDate CalculateDateOfDeath(Crop crop, SaverCrop saverCrop) 146 | { 147 | var numSeasons = crop.seasonsToGrowIn.Count - 148 | (crop.seasonsToGrowIn.IndexOf(saverCrop.datePlanted.Season)); 149 | var numDaysToLive = saverCrop.extraDays + (28 * numSeasons) - saverCrop.datePlanted.Day; 150 | var dateOfDeath = saverCrop.datePlanted.AddDays(numDaysToLive); 151 | 152 | return dateOfDeath; 153 | } 154 | 155 | private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) 156 | { 157 | _cropSaverDataLoader.LoadDataFromDisk(); 158 | } 159 | 160 | private void OnSaving(object sender, SavingEventArgs e) 161 | { 162 | _cropSaverDataLoader.SaveDataToDisk(); 163 | } 164 | 165 | 166 | private void OnCropAdded(TerrainFeature feature) 167 | { 168 | var closestFarmer = FarmerUtil.GetClosestFarmer(feature.currentLocation, feature.currentTileLocation); 169 | _cropSaverDataLoader.AddCrop(new SaverCrop( 170 | feature.currentLocation.Name, 171 | feature.currentTileLocation, 172 | closestFarmer.UniqueMultiplayerID, 173 | SDate.Now() 174 | ) 175 | ); 176 | 177 | // _monitor.Log( 178 | // $"Added crop planted at: {feature.currentLocation} on: {feature.currentTileLocation} by: {closestFarmer.Name}"); 179 | } 180 | 181 | private void OnCropRemoved(TerrainFeature feature) 182 | { 183 | _cropSaverDataLoader.RemoveCrop(feature.currentLocation.Name, feature.currentTileLocation); 184 | // _monitor.Log( 185 | // $"Removed crop at: {feature.currentLocation} on: {feature.currentTileLocation}"); 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/stardewdaemon/v1/stardewdaemon_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc (unknown) 5 | // source: junimohost/stardewdaemon/v1/stardewdaemon.proto 6 | 7 | package stardewdaemonv1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // StardewDaemonServiceClient is the client API for StardewDaemonService service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type StardewDaemonServiceClient interface { 25 | IndexBackup(ctx context.Context, in *IndexBackupRequest, opts ...grpc.CallOption) (*IndexBackupResponse, error) 26 | GetStartupConfig(ctx context.Context, in *GetStartupConfigRequest, opts ...grpc.CallOption) (*GetStartupConfigResponse, error) 27 | UpdateStatus(ctx context.Context, in *UpdateStatusRequest, opts ...grpc.CallOption) (*UpdateStatusResponse, error) 28 | } 29 | 30 | type stardewDaemonServiceClient struct { 31 | cc grpc.ClientConnInterface 32 | } 33 | 34 | func NewStardewDaemonServiceClient(cc grpc.ClientConnInterface) StardewDaemonServiceClient { 35 | return &stardewDaemonServiceClient{cc} 36 | } 37 | 38 | func (c *stardewDaemonServiceClient) IndexBackup(ctx context.Context, in *IndexBackupRequest, opts ...grpc.CallOption) (*IndexBackupResponse, error) { 39 | out := new(IndexBackupResponse) 40 | err := c.cc.Invoke(ctx, "/junimohost.stardewdaemon.v1.StardewDaemonService/IndexBackup", in, out, opts...) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return out, nil 45 | } 46 | 47 | func (c *stardewDaemonServiceClient) GetStartupConfig(ctx context.Context, in *GetStartupConfigRequest, opts ...grpc.CallOption) (*GetStartupConfigResponse, error) { 48 | out := new(GetStartupConfigResponse) 49 | err := c.cc.Invoke(ctx, "/junimohost.stardewdaemon.v1.StardewDaemonService/GetStartupConfig", in, out, opts...) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return out, nil 54 | } 55 | 56 | func (c *stardewDaemonServiceClient) UpdateStatus(ctx context.Context, in *UpdateStatusRequest, opts ...grpc.CallOption) (*UpdateStatusResponse, error) { 57 | out := new(UpdateStatusResponse) 58 | err := c.cc.Invoke(ctx, "/junimohost.stardewdaemon.v1.StardewDaemonService/UpdateStatus", in, out, opts...) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return out, nil 63 | } 64 | 65 | // StardewDaemonServiceServer is the server API for StardewDaemonService service. 66 | // All implementations must embed UnimplementedStardewDaemonServiceServer 67 | // for forward compatibility 68 | type StardewDaemonServiceServer interface { 69 | IndexBackup(context.Context, *IndexBackupRequest) (*IndexBackupResponse, error) 70 | GetStartupConfig(context.Context, *GetStartupConfigRequest) (*GetStartupConfigResponse, error) 71 | UpdateStatus(context.Context, *UpdateStatusRequest) (*UpdateStatusResponse, error) 72 | mustEmbedUnimplementedStardewDaemonServiceServer() 73 | } 74 | 75 | // UnimplementedStardewDaemonServiceServer must be embedded to have forward compatible implementations. 76 | type UnimplementedStardewDaemonServiceServer struct { 77 | } 78 | 79 | func (UnimplementedStardewDaemonServiceServer) IndexBackup(context.Context, *IndexBackupRequest) (*IndexBackupResponse, error) { 80 | return nil, status.Errorf(codes.Unimplemented, "method IndexBackup not implemented") 81 | } 82 | func (UnimplementedStardewDaemonServiceServer) GetStartupConfig(context.Context, *GetStartupConfigRequest) (*GetStartupConfigResponse, error) { 83 | return nil, status.Errorf(codes.Unimplemented, "method GetStartupConfig not implemented") 84 | } 85 | func (UnimplementedStardewDaemonServiceServer) UpdateStatus(context.Context, *UpdateStatusRequest) (*UpdateStatusResponse, error) { 86 | return nil, status.Errorf(codes.Unimplemented, "method UpdateStatus not implemented") 87 | } 88 | func (UnimplementedStardewDaemonServiceServer) mustEmbedUnimplementedStardewDaemonServiceServer() {} 89 | 90 | // UnsafeStardewDaemonServiceServer may be embedded to opt out of forward compatibility for this service. 91 | // Use of this interface is not recommended, as added methods to StardewDaemonServiceServer will 92 | // result in compilation errors. 93 | type UnsafeStardewDaemonServiceServer interface { 94 | mustEmbedUnimplementedStardewDaemonServiceServer() 95 | } 96 | 97 | func RegisterStardewDaemonServiceServer(s grpc.ServiceRegistrar, srv StardewDaemonServiceServer) { 98 | s.RegisterService(&StardewDaemonService_ServiceDesc, srv) 99 | } 100 | 101 | func _StardewDaemonService_IndexBackup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 102 | in := new(IndexBackupRequest) 103 | if err := dec(in); err != nil { 104 | return nil, err 105 | } 106 | if interceptor == nil { 107 | return srv.(StardewDaemonServiceServer).IndexBackup(ctx, in) 108 | } 109 | info := &grpc.UnaryServerInfo{ 110 | Server: srv, 111 | FullMethod: "/junimohost.stardewdaemon.v1.StardewDaemonService/IndexBackup", 112 | } 113 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 114 | return srv.(StardewDaemonServiceServer).IndexBackup(ctx, req.(*IndexBackupRequest)) 115 | } 116 | return interceptor(ctx, in, info, handler) 117 | } 118 | 119 | func _StardewDaemonService_GetStartupConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 120 | in := new(GetStartupConfigRequest) 121 | if err := dec(in); err != nil { 122 | return nil, err 123 | } 124 | if interceptor == nil { 125 | return srv.(StardewDaemonServiceServer).GetStartupConfig(ctx, in) 126 | } 127 | info := &grpc.UnaryServerInfo{ 128 | Server: srv, 129 | FullMethod: "/junimohost.stardewdaemon.v1.StardewDaemonService/GetStartupConfig", 130 | } 131 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 132 | return srv.(StardewDaemonServiceServer).GetStartupConfig(ctx, req.(*GetStartupConfigRequest)) 133 | } 134 | return interceptor(ctx, in, info, handler) 135 | } 136 | 137 | func _StardewDaemonService_UpdateStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 138 | in := new(UpdateStatusRequest) 139 | if err := dec(in); err != nil { 140 | return nil, err 141 | } 142 | if interceptor == nil { 143 | return srv.(StardewDaemonServiceServer).UpdateStatus(ctx, in) 144 | } 145 | info := &grpc.UnaryServerInfo{ 146 | Server: srv, 147 | FullMethod: "/junimohost.stardewdaemon.v1.StardewDaemonService/UpdateStatus", 148 | } 149 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 150 | return srv.(StardewDaemonServiceServer).UpdateStatus(ctx, req.(*UpdateStatusRequest)) 151 | } 152 | return interceptor(ctx, in, info, handler) 153 | } 154 | 155 | // StardewDaemonService_ServiceDesc is the grpc.ServiceDesc for StardewDaemonService service. 156 | // It's only intended for direct use with grpc.RegisterService, 157 | // and not to be introspected or modified (even as a copy) 158 | var StardewDaemonService_ServiceDesc = grpc.ServiceDesc{ 159 | ServiceName: "junimohost.stardewdaemon.v1.StardewDaemonService", 160 | HandlerType: (*StardewDaemonServiceServer)(nil), 161 | Methods: []grpc.MethodDesc{ 162 | { 163 | MethodName: "IndexBackup", 164 | Handler: _StardewDaemonService_IndexBackup_Handler, 165 | }, 166 | { 167 | MethodName: "GetStartupConfig", 168 | Handler: _StardewDaemonService_GetStartupConfig_Handler, 169 | }, 170 | { 171 | MethodName: "UpdateStatus", 172 | Handler: _StardewDaemonService_UpdateStatus_Handler, 173 | }, 174 | }, 175 | Streams: []grpc.StreamDesc{}, 176 | Metadata: "junimohost/stardewdaemon/v1/stardewdaemon.proto", 177 | } 178 | -------------------------------------------------------------------------------- /daemon/internal/startup/startup_service.go: -------------------------------------------------------------------------------- 1 | package startup 2 | 3 | import ( 4 | "archive/zip" 5 | "bufio" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "os" 11 | "sync" 12 | 13 | "cloud.google.com/go/storage" 14 | pbsm "github.com/junimohost/game-daemon/gen/proto/go/junimohost/servermanager/v1" 15 | pbsd "github.com/junimohost/game-daemon/gen/proto/go/junimohost/stardewdaemon/v1" 16 | "github.com/junimohost/game-daemon/internal/backup" 17 | "github.com/junimohost/game-daemon/internal/util" 18 | "github.com/junimohost/game-daemon/internal/zips" 19 | "github.com/junimohost/game-daemon/pkg/functional" 20 | "go.uber.org/zap" 21 | ) 22 | 23 | const ( 24 | bufferSize = 5120 25 | ModFolder = "/data/Stardew/Stardew Valley/Mods" 26 | ModsStateFile = ModFolder + "/state.json" 27 | ) 28 | 29 | type NewGameConfig struct { 30 | WhichFarm int `json:"WhichFarm"` 31 | UseSeparateWallets bool `json:"UseSeparateWallets"` 32 | StartingCabins int `json:"StartingCabins"` 33 | CatPerson bool `json:"CatPerson"` 34 | FarmName string `json:"FarmName"` 35 | MaxPlayers int `json:"MaxPlayers"` 36 | CabinStrategy int `json:"CabinStrategy"` 37 | } 38 | 39 | type loadedModSavedPaths struct { 40 | VersionId string `json:"VersionId,omitempty"` 41 | Paths []string `json:"Paths,omitempty"` 42 | } 43 | 44 | type modVersionPathDestination struct { 45 | VersionId string 46 | Path string 47 | } 48 | 49 | type Service struct { 50 | backendAvailable bool 51 | startupScriptFinished bool 52 | config pbsm.GameConfig 53 | 54 | storageClient *storage.Client 55 | backupService *backup.Service 56 | stardewDaemonClient pbsd.StardewDaemonServiceClient 57 | serverManagerClient pbsm.ServerManagerServiceClient 58 | serverId string 59 | } 60 | 61 | func NewService( 62 | storageClient *storage.Client, 63 | backupService *backup.Service, 64 | stardewDaemonClient pbsd.StardewDaemonServiceClient, 65 | serverManagerClient pbsm.ServerManagerServiceClient, 66 | serverId string, 67 | backendAvailable bool) *Service { 68 | return &Service{ 69 | backendAvailable: backendAvailable, 70 | storageClient: storageClient, 71 | backupService: backupService, 72 | stardewDaemonClient: stardewDaemonClient, 73 | serverManagerClient: serverManagerClient, 74 | serverId: serverId, 75 | startupScriptFinished: false, 76 | //default config for when backend is unavailable 77 | config: pbsm.GameConfig{ 78 | WhichFarm: 0, 79 | UseSeparateWallets: false, 80 | StartingCabins: 1, 81 | CatPerson: false, 82 | FarmName: "Test", 83 | ModVersionIds: nil, 84 | MaxPlayers: 10, 85 | CabinStrategy: 0, 86 | }, 87 | } 88 | } 89 | 90 | func (s *Service) RunStartupScript(ctx context.Context) error { 91 | defer func() { 92 | zap.S().Info("Finished Startup Script") 93 | s.startupScriptFinished = true 94 | }() 95 | 96 | startupConfig, err := s.stardewDaemonClient.GetStartupConfig(ctx, &pbsd.GetStartupConfigRequest{ 97 | ServerId: s.serverId, 98 | }) 99 | 100 | if err != nil { 101 | zap.S().Errorf("Failed to fetch startup config: %v", err) 102 | return err 103 | } 104 | 105 | if startupConfig.BackupPath != "" { 106 | err = s.backupService.RestoreBackup(ctx, startupConfig.BackupPath) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | _, err := s.stardewDaemonClient.UpdateStatus(ctx, &pbsd.UpdateStatusRequest{ 112 | ServerId: s.serverId, 113 | BackupRestoreSuccessful: true, 114 | }) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | 120 | config := startupConfig.Config 121 | 122 | if config == nil { 123 | return nil 124 | } 125 | 126 | s.config = *config 127 | s.setupModsForServer(ctx, config.ModVersionIds) 128 | 129 | return nil 130 | } 131 | 132 | func (s *Service) setupModsForServer(ctx context.Context, modVersionIdsToDownload []string) { 133 | zap.S().Infof("Downloading mods: %v", modVersionIdsToDownload) 134 | 135 | response, err := s.serverManagerClient.GetAvailableMods(ctx, &pbsm.GetAvailableModsRequest{}) 136 | if err != nil { 137 | return 138 | } 139 | 140 | allMods := response.Mods 141 | allVersions := functional.FlatMapSlice(allMods, func(m *pbsm.Mod) []*pbsm.ModVersion { 142 | if m != nil { 143 | return m.Versions 144 | } 145 | var temp []*pbsm.ModVersion 146 | return temp 147 | }) 148 | 149 | idsToDownloadPaths := map[string]string{} 150 | 151 | for _, versionId := range modVersionIdsToDownload { 152 | matched := functional.Find(allVersions, func(m *pbsm.ModVersion) bool { return m.ModVersionId == versionId }) 153 | if matched.GcsPath == "" { 154 | zap.S().Errorf("Could not find GCS Path for Version ID: %v", modVersionIdsToDownload) 155 | continue 156 | } 157 | idsToDownloadPaths[versionId] = matched.GcsPath 158 | } 159 | 160 | var wg sync.WaitGroup 161 | wg.Add(len(idsToDownloadPaths)) 162 | 163 | outputFolderNameChan := make(chan modVersionPathDestination) 164 | errorChan := make(chan error) 165 | 166 | for versionId, gcsPath := range idsToDownloadPaths { 167 | go s.extractModZipFromGcsToLocal(ctx, versionId, gcsPath, outputFolderNameChan, errorChan, &wg) 168 | } 169 | 170 | wg.Wait() 171 | close(outputFolderNameChan) 172 | close(errorChan) 173 | 174 | writeStateFileFromOutputChannel(outputFolderNameChan) 175 | 176 | for err := range errorChan { 177 | zap.S().Errorf("Received error from mod goroutine: %v", err) 178 | } 179 | } 180 | 181 | func writeStateFileFromOutputChannel(outputFolderNameChan chan modVersionPathDestination) { 182 | var modStateMap map[string]*loadedModSavedPaths 183 | 184 | for output := range outputFolderNameChan { 185 | if val, ok := modStateMap[output.VersionId]; ok { 186 | val.Paths = append(val.Paths, output.Path) 187 | } else { 188 | modStateMap[output.VersionId] = &loadedModSavedPaths{ 189 | VersionId: output.VersionId, 190 | Paths: []string{output.Path}, 191 | } 192 | } 193 | } 194 | 195 | var modPaths []loadedModSavedPaths 196 | for _, val := range modStateMap { 197 | modPaths = append(modPaths, *val) 198 | } 199 | 200 | marshal, err := json.Marshal(modPaths) 201 | if err != nil { 202 | return 203 | } 204 | 205 | _ = os.WriteFile(ModsStateFile, marshal, 0644) 206 | } 207 | 208 | func (s *Service) extractModZipFromGcsToLocal( 209 | ctx context.Context, 210 | versionId, 211 | gcsPath string, 212 | outputFolderNameChan chan modVersionPathDestination, 213 | errorChan chan error, 214 | wg *sync.WaitGroup, 215 | ) { 216 | defer wg.Done() 217 | bucketName, objectName := util.GetBucketAndObjectFromGcsPath(gcsPath) 218 | gcsR, err := s.storageClient.Bucket(bucketName).Object(objectName).NewReader(ctx) 219 | if err != nil { 220 | errorChan <- err 221 | return 222 | } 223 | defer gcsR.Close() 224 | // TODO: delete if folder exists 225 | fileName := fmt.Sprintf("%v/%v.zip", os.TempDir(), versionId) 226 | 227 | err = bufferedWriteFromGcsToFile(gcsR, fileName) 228 | if err != nil { 229 | errorChan <- err 230 | return 231 | } 232 | 233 | zipReader, err := zip.OpenReader(fileName) 234 | defer zipReader.Close() 235 | if err != nil { 236 | errorChan <- err 237 | return 238 | } 239 | 240 | outputDirs := zips.ExtractZipToPath(ModFolder, &zipReader.Reader) 241 | 242 | for _, dir := range outputDirs { 243 | outputFolderNameChan <- modVersionPathDestination{ 244 | VersionId: versionId, 245 | Path: dir, 246 | } 247 | } 248 | 249 | err = os.Remove(fileName) 250 | if err != nil { 251 | zap.S().Errorf("Failed to delete zip file %v", err) 252 | } 253 | } 254 | 255 | func bufferedWriteFromGcsToFile(gcsReader *storage.Reader, fileName string) error { 256 | buffer := make([]byte, bufferSize) 257 | f, err := os.Create(fileName) 258 | defer f.Close() 259 | w := bufio.NewWriter(f) 260 | _, err = io.CopyBuffer(w, gcsReader, buffer) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | err = w.Flush() 266 | if err != nil { 267 | return err 268 | } 269 | return nil 270 | } 271 | 272 | func (s *Service) IsStartupScriptFinished() bool { 273 | if !s.backendAvailable { 274 | return true 275 | } 276 | return s.startupScriptFinished 277 | } 278 | 279 | func (s *Service) UpdateStatus(update StatusUpdate) error { 280 | _, err := s.stardewDaemonClient.UpdateStatus(context.Background(), &pbsd.UpdateStatusRequest{ 281 | ServerId: s.serverId, 282 | BackupRestoreSuccessfulV2: pbsd.Status(update.BackupRestoreSuccessful), 283 | ServerConnectable: pbsd.Status(update.ServerConnectable), 284 | }) 285 | 286 | if err != nil { 287 | return err 288 | } 289 | 290 | return nil 291 | } 292 | -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/gamemanager/v1/gamemanager.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc (unknown) 5 | // source: junimohost/gamemanager/v1/gamemanager.proto 6 | 7 | package gamemanagerv1 8 | 9 | import ( 10 | v1 "github.com/junimohost/game-daemon/gen/proto/go/junimohost/servermanager/v1" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | emptypb "google.golang.org/protobuf/types/known/emptypb" 14 | reflect "reflect" 15 | sync "sync" 16 | ) 17 | 18 | const ( 19 | // Verify that this generated code is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 21 | // Verify that runtime/protoimpl is sufficiently up-to-date. 22 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 23 | ) 24 | 25 | type CreateGameRequest struct { 26 | state protoimpl.MessageState 27 | sizeCache protoimpl.SizeCache 28 | unknownFields protoimpl.UnknownFields 29 | 30 | Config *v1.GameConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` 31 | } 32 | 33 | func (x *CreateGameRequest) Reset() { 34 | *x = CreateGameRequest{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_junimohost_gamemanager_v1_gamemanager_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *CreateGameRequest) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*CreateGameRequest) ProtoMessage() {} 47 | 48 | func (x *CreateGameRequest) ProtoReflect() protoreflect.Message { 49 | mi := &file_junimohost_gamemanager_v1_gamemanager_proto_msgTypes[0] 50 | if protoimpl.UnsafeEnabled && x != nil { 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | if ms.LoadMessageInfo() == nil { 53 | ms.StoreMessageInfo(mi) 54 | } 55 | return ms 56 | } 57 | return mi.MessageOf(x) 58 | } 59 | 60 | // Deprecated: Use CreateGameRequest.ProtoReflect.Descriptor instead. 61 | func (*CreateGameRequest) Descriptor() ([]byte, []int) { 62 | return file_junimohost_gamemanager_v1_gamemanager_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *CreateGameRequest) GetConfig() *v1.GameConfig { 66 | if x != nil { 67 | return x.Config 68 | } 69 | return nil 70 | } 71 | 72 | var File_junimohost_gamemanager_v1_gamemanager_proto protoreflect.FileDescriptor 73 | 74 | var file_junimohost_gamemanager_v1_gamemanager_proto_rawDesc = []byte{ 75 | 0x0a, 0x2b, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x67, 0x61, 0x6d, 76 | 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x61, 0x6d, 0x65, 77 | 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x6a, 78 | 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 79 | 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 80 | 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x6d, 0x61, 0x6e, 0x61, 0x67, 81 | 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x6d, 0x61, 0x6e, 0x61, 82 | 0x67, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 83 | 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 84 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x54, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 85 | 0x47, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x06, 0x63, 86 | 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6a, 0x75, 87 | 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x6d, 88 | 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x6d, 0x65, 0x43, 0x6f, 89 | 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0x68, 0x0a, 0x12, 90 | 0x47, 0x61, 0x6d, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 91 | 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x6d, 0x65, 92 | 0x12, 0x2c, 0x2e, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x67, 0x61, 93 | 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 94 | 0x61, 0x74, 0x65, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 95 | 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 96 | 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x8f, 0x02, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x6a, 97 | 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 98 | 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x10, 0x47, 0x61, 0x6d, 0x65, 0x6d, 0x61, 99 | 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x56, 0x67, 0x69, 100 | 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 101 | 0x6f, 0x73, 0x74, 0x2f, 0x67, 0x61, 0x6d, 0x65, 0x2d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 102 | 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x6a, 0x75, 0x6e, 103 | 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 104 | 0x67, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 105 | 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4a, 0x47, 0x58, 0xaa, 0x02, 0x19, 0x4a, 0x75, 0x6e, 106 | 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x47, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 107 | 0x67, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x19, 0x4a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 108 | 0x6f, 0x73, 0x74, 0x5c, 0x47, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5c, 109 | 0x56, 0x31, 0xe2, 0x02, 0x25, 0x4a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x5c, 110 | 0x47, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 111 | 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x4a, 0x75, 0x6e, 112 | 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x3a, 0x47, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 113 | 0x61, 0x67, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 114 | } 115 | 116 | var ( 117 | file_junimohost_gamemanager_v1_gamemanager_proto_rawDescOnce sync.Once 118 | file_junimohost_gamemanager_v1_gamemanager_proto_rawDescData = file_junimohost_gamemanager_v1_gamemanager_proto_rawDesc 119 | ) 120 | 121 | func file_junimohost_gamemanager_v1_gamemanager_proto_rawDescGZIP() []byte { 122 | file_junimohost_gamemanager_v1_gamemanager_proto_rawDescOnce.Do(func() { 123 | file_junimohost_gamemanager_v1_gamemanager_proto_rawDescData = protoimpl.X.CompressGZIP(file_junimohost_gamemanager_v1_gamemanager_proto_rawDescData) 124 | }) 125 | return file_junimohost_gamemanager_v1_gamemanager_proto_rawDescData 126 | } 127 | 128 | var file_junimohost_gamemanager_v1_gamemanager_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 129 | var file_junimohost_gamemanager_v1_gamemanager_proto_goTypes = []interface{}{ 130 | (*CreateGameRequest)(nil), // 0: junimohost.gamemanager.v1.CreateGameRequest 131 | (*v1.GameConfig)(nil), // 1: junimohost.servermanager.v1.GameConfig 132 | (*emptypb.Empty)(nil), // 2: google.protobuf.Empty 133 | } 134 | var file_junimohost_gamemanager_v1_gamemanager_proto_depIdxs = []int32{ 135 | 1, // 0: junimohost.gamemanager.v1.CreateGameRequest.config:type_name -> junimohost.servermanager.v1.GameConfig 136 | 0, // 1: junimohost.gamemanager.v1.GameManagerService.CreateGame:input_type -> junimohost.gamemanager.v1.CreateGameRequest 137 | 2, // 2: junimohost.gamemanager.v1.GameManagerService.CreateGame:output_type -> google.protobuf.Empty 138 | 2, // [2:3] is the sub-list for method output_type 139 | 1, // [1:2] is the sub-list for method input_type 140 | 1, // [1:1] is the sub-list for extension type_name 141 | 1, // [1:1] is the sub-list for extension extendee 142 | 0, // [0:1] is the sub-list for field type_name 143 | } 144 | 145 | func init() { file_junimohost_gamemanager_v1_gamemanager_proto_init() } 146 | func file_junimohost_gamemanager_v1_gamemanager_proto_init() { 147 | if File_junimohost_gamemanager_v1_gamemanager_proto != nil { 148 | return 149 | } 150 | if !protoimpl.UnsafeEnabled { 151 | file_junimohost_gamemanager_v1_gamemanager_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 152 | switch v := v.(*CreateGameRequest); i { 153 | case 0: 154 | return &v.state 155 | case 1: 156 | return &v.sizeCache 157 | case 2: 158 | return &v.unknownFields 159 | default: 160 | return nil 161 | } 162 | } 163 | } 164 | type x struct{} 165 | out := protoimpl.TypeBuilder{ 166 | File: protoimpl.DescBuilder{ 167 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 168 | RawDescriptor: file_junimohost_gamemanager_v1_gamemanager_proto_rawDesc, 169 | NumEnums: 0, 170 | NumMessages: 1, 171 | NumExtensions: 0, 172 | NumServices: 1, 173 | }, 174 | GoTypes: file_junimohost_gamemanager_v1_gamemanager_proto_goTypes, 175 | DependencyIndexes: file_junimohost_gamemanager_v1_gamemanager_proto_depIdxs, 176 | MessageInfos: file_junimohost_gamemanager_v1_gamemanager_proto_msgTypes, 177 | }.Build() 178 | File_junimohost_gamemanager_v1_gamemanager_proto = out.File 179 | file_junimohost_gamemanager_v1_gamemanager_proto_rawDesc = nil 180 | file_junimohost_gamemanager_v1_gamemanager_proto_goTypes = nil 181 | file_junimohost_gamemanager_v1_gamemanager_proto_depIdxs = nil 182 | } 183 | -------------------------------------------------------------------------------- /daemon/gen/proto/go/junimohost/admin/v1/admin.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc (unknown) 5 | // source: junimohost/admin/v1/admin.proto 6 | 7 | package adminv1 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type UpgradeDeploymentRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | ServerId string `protobuf:"bytes,1,opt,name=server_id,json=serverId,proto3" json:"server_id,omitempty"` 29 | } 30 | 31 | func (x *UpgradeDeploymentRequest) Reset() { 32 | *x = UpgradeDeploymentRequest{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_junimohost_admin_v1_admin_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *UpgradeDeploymentRequest) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*UpgradeDeploymentRequest) ProtoMessage() {} 45 | 46 | func (x *UpgradeDeploymentRequest) ProtoReflect() protoreflect.Message { 47 | mi := &file_junimohost_admin_v1_admin_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use UpgradeDeploymentRequest.ProtoReflect.Descriptor instead. 59 | func (*UpgradeDeploymentRequest) Descriptor() ([]byte, []int) { 60 | return file_junimohost_admin_v1_admin_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *UpgradeDeploymentRequest) GetServerId() string { 64 | if x != nil { 65 | return x.ServerId 66 | } 67 | return "" 68 | } 69 | 70 | type UpgradeDeploymentResponse struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | } 75 | 76 | func (x *UpgradeDeploymentResponse) Reset() { 77 | *x = UpgradeDeploymentResponse{} 78 | if protoimpl.UnsafeEnabled { 79 | mi := &file_junimohost_admin_v1_admin_proto_msgTypes[1] 80 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 81 | ms.StoreMessageInfo(mi) 82 | } 83 | } 84 | 85 | func (x *UpgradeDeploymentResponse) String() string { 86 | return protoimpl.X.MessageStringOf(x) 87 | } 88 | 89 | func (*UpgradeDeploymentResponse) ProtoMessage() {} 90 | 91 | func (x *UpgradeDeploymentResponse) ProtoReflect() protoreflect.Message { 92 | mi := &file_junimohost_admin_v1_admin_proto_msgTypes[1] 93 | if protoimpl.UnsafeEnabled && x != nil { 94 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 95 | if ms.LoadMessageInfo() == nil { 96 | ms.StoreMessageInfo(mi) 97 | } 98 | return ms 99 | } 100 | return mi.MessageOf(x) 101 | } 102 | 103 | // Deprecated: Use UpgradeDeploymentResponse.ProtoReflect.Descriptor instead. 104 | func (*UpgradeDeploymentResponse) Descriptor() ([]byte, []int) { 105 | return file_junimohost_admin_v1_admin_proto_rawDescGZIP(), []int{1} 106 | } 107 | 108 | var File_junimohost_admin_v1_admin_proto protoreflect.FileDescriptor 109 | 110 | var file_junimohost_admin_v1_admin_proto_rawDesc = []byte{ 111 | 0x0a, 0x1f, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x61, 0x64, 0x6d, 112 | 0x69, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 113 | 0x6f, 0x12, 0x13, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x61, 0x64, 114 | 0x6d, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x22, 0x37, 0x0a, 0x18, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 115 | 0x65, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 116 | 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 117 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x22, 118 | 0x1b, 0x0a, 0x19, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 119 | 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x82, 0x01, 0x0a, 120 | 0x0c, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x72, 0x0a, 121 | 0x11, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 122 | 0x6e, 0x74, 0x12, 0x2d, 0x2e, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 123 | 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 124 | 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 125 | 0x74, 0x1a, 0x2e, 0x2e, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x61, 126 | 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x44, 127 | 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 128 | 0x65, 0x42, 0xdf, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 129 | 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x42, 0x0a, 0x41, 130 | 0x64, 0x6d, 0x69, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4a, 0x67, 0x69, 0x74, 131 | 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 132 | 0x73, 0x74, 0x2f, 0x67, 0x61, 0x6d, 0x65, 0x2d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x67, 133 | 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x6a, 0x75, 0x6e, 0x69, 134 | 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x76, 0x31, 0x3b, 135 | 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4a, 0x41, 0x58, 0xaa, 0x02, 0x13, 136 | 0x4a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 137 | 0x2e, 0x56, 0x31, 0xca, 0x02, 0x13, 0x4a, 0x75, 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 138 | 0x5c, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1f, 0x4a, 0x75, 0x6e, 0x69, 139 | 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x5c, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x5c, 0x56, 0x31, 0x5c, 140 | 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x15, 0x4a, 0x75, 141 | 0x6e, 0x69, 0x6d, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x3a, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x3a, 142 | 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 143 | } 144 | 145 | var ( 146 | file_junimohost_admin_v1_admin_proto_rawDescOnce sync.Once 147 | file_junimohost_admin_v1_admin_proto_rawDescData = file_junimohost_admin_v1_admin_proto_rawDesc 148 | ) 149 | 150 | func file_junimohost_admin_v1_admin_proto_rawDescGZIP() []byte { 151 | file_junimohost_admin_v1_admin_proto_rawDescOnce.Do(func() { 152 | file_junimohost_admin_v1_admin_proto_rawDescData = protoimpl.X.CompressGZIP(file_junimohost_admin_v1_admin_proto_rawDescData) 153 | }) 154 | return file_junimohost_admin_v1_admin_proto_rawDescData 155 | } 156 | 157 | var file_junimohost_admin_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 158 | var file_junimohost_admin_v1_admin_proto_goTypes = []interface{}{ 159 | (*UpgradeDeploymentRequest)(nil), // 0: junimohost.admin.v1.UpgradeDeploymentRequest 160 | (*UpgradeDeploymentResponse)(nil), // 1: junimohost.admin.v1.UpgradeDeploymentResponse 161 | } 162 | var file_junimohost_admin_v1_admin_proto_depIdxs = []int32{ 163 | 0, // 0: junimohost.admin.v1.AdminService.UpgradeDeployment:input_type -> junimohost.admin.v1.UpgradeDeploymentRequest 164 | 1, // 1: junimohost.admin.v1.AdminService.UpgradeDeployment:output_type -> junimohost.admin.v1.UpgradeDeploymentResponse 165 | 1, // [1:2] is the sub-list for method output_type 166 | 0, // [0:1] is the sub-list for method input_type 167 | 0, // [0:0] is the sub-list for extension type_name 168 | 0, // [0:0] is the sub-list for extension extendee 169 | 0, // [0:0] is the sub-list for field type_name 170 | } 171 | 172 | func init() { file_junimohost_admin_v1_admin_proto_init() } 173 | func file_junimohost_admin_v1_admin_proto_init() { 174 | if File_junimohost_admin_v1_admin_proto != nil { 175 | return 176 | } 177 | if !protoimpl.UnsafeEnabled { 178 | file_junimohost_admin_v1_admin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 179 | switch v := v.(*UpgradeDeploymentRequest); i { 180 | case 0: 181 | return &v.state 182 | case 1: 183 | return &v.sizeCache 184 | case 2: 185 | return &v.unknownFields 186 | default: 187 | return nil 188 | } 189 | } 190 | file_junimohost_admin_v1_admin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 191 | switch v := v.(*UpgradeDeploymentResponse); i { 192 | case 0: 193 | return &v.state 194 | case 1: 195 | return &v.sizeCache 196 | case 2: 197 | return &v.unknownFields 198 | default: 199 | return nil 200 | } 201 | } 202 | } 203 | type x struct{} 204 | out := protoimpl.TypeBuilder{ 205 | File: protoimpl.DescBuilder{ 206 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 207 | RawDescriptor: file_junimohost_admin_v1_admin_proto_rawDesc, 208 | NumEnums: 0, 209 | NumMessages: 2, 210 | NumExtensions: 0, 211 | NumServices: 1, 212 | }, 213 | GoTypes: file_junimohost_admin_v1_admin_proto_goTypes, 214 | DependencyIndexes: file_junimohost_admin_v1_admin_proto_depIdxs, 215 | MessageInfos: file_junimohost_admin_v1_admin_proto_msgTypes, 216 | }.Build() 217 | File_junimohost_admin_v1_admin_proto = out.File 218 | file_junimohost_admin_v1_admin_proto_rawDesc = nil 219 | file_junimohost_admin_v1_admin_proto_goTypes = nil 220 | file_junimohost_admin_v1_admin_proto_depIdxs = nil 221 | } 222 | --------------------------------------------------------------------------------