Closed Captions / Subtitles for Vintage Story.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Import files from vsmod-Subtitles.

+345 -29
+13
.idea/.idea.ClosedCaptions/.idea/.gitignore
··· 1 + # Default ignored files 2 + /shelf/ 3 + /workspace.xml 4 + # Rider ignored files 5 + /projectSettingsUpdater.xml 6 + /.idea.ClosedCaptions.iml 7 + /contentModel.xml 8 + /modules.xml 9 + # Editor-based HTTP Client requests 10 + /httpRequests/ 11 + # Datasource local storage ignored files 12 + /dataSources/ 13 + /dataSources.local.xml
+1
.idea/.idea.ClosedCaptions/.idea/.name
··· 1 + ClosedCaptions
+6
.idea/.idea.ClosedCaptions/.idea/copilot.data.migration.agent.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="AgentMigrationStateService"> 4 + <option name="migrationStatus" value="COMPLETED" /> 5 + </component> 6 + </project>
+6
.idea/.idea.ClosedCaptions/.idea/copilot.data.migration.ask.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="AskMigrationStateService"> 4 + <option name="migrationStatus" value="COMPLETED" /> 5 + </component> 6 + </project>
+6
.idea/.idea.ClosedCaptions/.idea/copilot.data.migration.ask2agent.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="Ask2AgentMigrationStateService"> 4 + <option name="migrationStatus" value="COMPLETED" /> 5 + </component> 6 + </project>
+6
.idea/.idea.ClosedCaptions/.idea/copilot.data.migration.edit.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="EditMigrationStateService"> 4 + <option name="migrationStatus" value="COMPLETED" /> 5 + </component> 6 + </project>
+4
.idea/.idea.ClosedCaptions/.idea/encodings.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> 4 + </project>
+6
.idea/.idea.ClosedCaptions/.idea/vcs.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="VcsDirectoryMappings"> 4 + <mapping directory="" vcs="Git" /> 5 + </component> 6 + </project>
+29
ClosedCaptions/CaptionsDialog.cs
··· 1 + using Vintagestory.API.Client; 2 + 3 + namespace ClosedCaptions; 4 + 5 + public class CaptionsDialog : HudElement 6 + { 7 + private CaptionsModSystem captionsMod; 8 + public CaptionsList captionsList; 9 + 10 + public CaptionsDialog(ICoreClientAPI capi, CaptionsModSystem captionsMod) : base(capi) { 11 + this.captionsMod = captionsMod; 12 + 13 + ElementBounds dialogBounds = ElementBounds.FixedSize(300, 450).WithAlignment(EnumDialogArea.RightBottom).WithFixedPadding(50); 14 + SingleComposer = capi.Gui.CreateCompo("captions", dialogBounds); 15 + 16 + ElementBounds listBounds = ElementBounds.FixedSize(300, 450); 17 + dialogBounds.WithChild(listBounds); 18 + captionsList = new CaptionsList(SingleComposer.Api, listBounds); 19 + SingleComposer.AddInteractiveElement(captionsList, "captionsList"); 20 + 21 + SingleComposer.Compose(false); 22 + } 23 + 24 + public override string ToggleKeyCombinationCode => null; 25 + 26 + public override bool OnEscapePressed() { 27 + return base.OnEscapePressed(); 28 + } 29 + }
+167
ClosedCaptions/CaptionsList.cs
··· 1 + using Vintagestory.API.Client; 2 + using Cairo; 3 + using Vintagestory.API.MathTools; 4 + using System; 5 + 6 + namespace ClosedCaptions; 7 + 8 + public class CaptionsList : GuiElement 9 + { 10 + private LoadedTexture textTexture; 11 + private TextDrawUtil textUtil; 12 + public CairoFont font; 13 + private ICoreClientAPI capi; 14 + 15 + private static readonly int MAX_SOUNDS = 15; 16 + private static readonly int MAX_AGE_SECONDS = 4; 17 + public class Sound { 18 + public bool active = false; 19 + public double age; 20 + public string name; 21 + public double textWidth; 22 + public double yaw; 23 + public double volume; 24 + } 25 + public Sound[] soundList = new Sound[MAX_SOUNDS]; 26 + 27 + public CaptionsList(ICoreClientAPI capi, ElementBounds bounds) : base(capi, bounds) { 28 + textTexture = new LoadedTexture(capi); 29 + font = CairoFont.WhiteMediumText(); 30 + textUtil = new TextDrawUtil(); 31 + this.capi = capi; 32 + for (int i = 0; i < MAX_SOUNDS; i++) { soundList[i] = new Sound(); } 33 + } 34 + 35 + public override void Dispose() { 36 + textTexture?.Dispose(); 37 + } 38 + 39 + public override void ComposeElements(Context ctx, ImageSurface surface) { 40 + font.SetupContext(ctx); 41 + Bounds.CalcWorldBounds(); 42 + Recompose(); 43 + } 44 + 45 + public override void RenderInteractiveElements(float deltaTime) { 46 + Update(deltaTime); 47 + Recompose(); 48 + api.Render.Render2DTexturePremultipliedAlpha(textTexture.TextureId, (int)Bounds.renderX, (int)Bounds.renderY, (int)Bounds.InnerWidth, (int)Bounds.InnerHeight); 49 + } 50 + 51 + public void Recompose() { 52 + ImageSurface imageSurface = new ImageSurface(Format.Argb32, 300, 450); 53 + Context context = genContext(imageSurface); 54 + DrawText(context); 55 + generateTexture(imageSurface, ref textTexture); 56 + context.Dispose(); 57 + imageSurface.Dispose(); 58 + } 59 + 60 + private void Update(float deltaTime) { 61 + foreach (var sound in soundList) { 62 + if (sound.active) { 63 + sound.age += deltaTime; 64 + if (sound.age > MAX_AGE_SECONDS) { 65 + capi.Logger.Debug("[CAPTIONS] Ended: " + sound.name); 66 + sound.active = false; 67 + } 68 + } 69 + } 70 + } 71 + 72 + private void DrawText(Context ctx) 73 + { 74 + font.SetupContext(ctx); 75 + 76 + ctx.SetSourceRGBA(.5, .1, .5, 0.75); 77 + ctx.Rectangle(0, 0, 300, 450); 78 + ctx.Fill(); 79 + 80 + double y = 30 * MAX_SOUNDS; 81 + foreach (var sound in soundList) 82 + { 83 + y -= 30; 84 + if (!sound.active) continue; 85 + 86 + // TODO: Just calculate this when it's loaded. 87 + if (sound.textWidth == -1) 88 + { 89 + sound.textWidth = ctx.TextExtents(sound.name).Width; 90 + } 91 + 92 + var brightness = ((1 - (sound.age / MAX_AGE_SECONDS)) * Math.Max(1, sound.volume) / 2 + 0.5); 93 + 94 + ctx.SetSourceRGBA(0, 0, 0, 0.5 + (brightness * 0.5)); 95 + ctx.Rectangle(0, y, 300, 30); 96 + ctx.Fill(); 97 + 98 + var soundName = sound.name; 99 + if (soundName.StartsWith("!")) 100 + { 101 + soundName = soundName.Substring(1); 102 + ctx.SetSourceRGB(brightness, brightness * 0.25, brightness * .125); 103 + } 104 + else 105 + { 106 + ctx.SetSourceRGB(brightness, brightness, brightness); 107 + } 108 + textUtil.DrawTextLine(ctx, font, soundName, 150 - sound.textWidth / 2, y+2); 109 + 110 + if (Double.IsNaN(sound.yaw)) continue; 111 + 112 + var direction = GameMath.Mod((sound.yaw + api.World.Player.CameraYaw) / GameMath.TWOPI * 12, 12); 113 + if (direction > 2 && direction < 4) 114 + { 115 + textUtil.DrawTextLine(ctx, font, ">>", 270, y); 116 + } 117 + else if (direction > 1 && direction < 5) 118 + { 119 + textUtil.DrawTextLine(ctx, font, ">", 270, y); 120 + } 121 + else if (direction > 8 && direction < 10) 122 + { 123 + textUtil.DrawTextLine(ctx, font, "<<", 30, y); 124 + } 125 + else if (direction > 7 && direction < 11) 126 + { 127 + textUtil.DrawTextLine(ctx, font, "<", 30, y); 128 + } 129 + } 130 + } 131 + 132 + public void Add(string name, double yaw, double volume) 133 + { 134 + capi.Logger.Debug("[CAPTIONS] Started: " + name); 135 + Sound targetSound = null; 136 + foreach (var sound in soundList) 137 + { 138 + if (sound.active && sound.name == name) 139 + { 140 + targetSound = sound; 141 + break; 142 + } 143 + } 144 + if (targetSound == null) 145 + { 146 + targetSound = soundList[0]; 147 + foreach (var sound in soundList) 148 + { 149 + if (!sound.active) 150 + { 151 + targetSound = sound; 152 + break; 153 + } 154 + if (sound.age > targetSound.age) 155 + { 156 + targetSound = sound; 157 + } 158 + } 159 + } 160 + targetSound.active = true; 161 + targetSound.name = name; 162 + targetSound.age = 0; 163 + targetSound.yaw = yaw; 164 + targetSound.volume = volume; 165 + targetSound.textWidth = -1; 166 + } 167 + }
+86
ClosedCaptions/CaptionsModSystem.cs
··· 1 + using System; 2 + using System.Reflection; 3 + using Vintagestory.API.Client; 4 + using Vintagestory.API.Common; 5 + using Vintagestory.API.Config; 6 + using Vintagestory.Client.NoObf; 7 + using HarmonyLib; 8 + 9 + [assembly: ModInfo("Captions")] 10 + 11 + namespace ClosedCaptions; 12 + 13 + public class CaptionsModSystem : ModSystem 14 + { 15 + private Harmony harmony; 16 + 17 + public override void StartClientSide(ICoreClientAPI capi) 18 + { 19 + base.StartClientSide(capi); 20 + 21 + harmony = new Harmony(Mod.Info.ModID); 22 + harmony.PatchAll(Assembly.GetExecutingAssembly()); 23 + Patch_ClientMain_StartPlaying.capi = capi; 24 + 25 + capi.Event.IsPlayerReady += (ref EnumHandling handling) => 26 + { 27 + Patch_ClientMain_StartPlaying.captions = new CaptionsDialog(capi, this); 28 + Patch_ClientMain_StartPlaying.captions.TryOpen(); 29 + return true; 30 + }; 31 + } 32 + } 33 + 34 + [HarmonyPatch(typeof(ClientMain))] 35 + [HarmonyPatch("StartPlaying")] 36 + [HarmonyPatch(new Type[] { typeof(ILoadedSound), typeof(AssetLocation) })] 37 + public class Patch_ClientMain_StartPlaying 38 + { 39 + public static ICoreClientAPI capi; 40 + public static CaptionsDialog captions; 41 + public static readonly double RANGE_THRESHOLD = 0.8; 42 + 43 + public static void Prefix(ILoadedSound loadedSound, AssetLocation location) 44 + { 45 + // Unknown condition yoinked without understanding from SubtitlesMod 46 + var player = capi.World.Player; 47 + if (player == null) return; 48 + 49 + var sound = loadedSound.Params; 50 + 51 + // Ignore music 52 + if (sound.SoundType == EnumSoundType.Music) return; 53 + 54 + var path = location.Path; 55 + var id = path.StartsWith("sounds/") && path.EndsWith(".ogg") ? path.Substring(7, path.Length - 7 - 4) : path; 56 + var lastChar = id.ToCharArray(id.Length - 1, 1)[0]; 57 + if (lastChar >= '0' && lastChar <= '9') 58 + id = id.Substring(0, id.Length - 1); 59 + 60 + var name = Lang.GetIfExists("captions:" + id); 61 + // Ignore specified sounds 62 + if (name == "") return; 63 + // Unnamed sounds 64 + if (name == null) name = id; 65 + 66 + if (sound.Position.IsZero) 67 + { 68 + captions?.captionsList?.Add(name, 0, sound.Volume); 69 + return; 70 + } 71 + 72 + var playerPos = player.Entity.Pos.AsBlockPos; 73 + var dx = sound.Position.X - playerPos.X; 74 + var dy = sound.Position.Y - playerPos.Y; 75 + var dz = sound.Position.Z - playerPos.Z; 76 + var dist = Math.Sqrt(dx * dx + dy * dy + dz * dz); 77 + 78 + // Ignore sounds that are out of range. 79 + if (dist > sound.Range * RANGE_THRESHOLD) return; 80 + 81 + // Make close sounds directionless. 82 + var yaw = (dist < 1.5) ? Math.Atan2(dz, dx) : Double.NaN; 83 + 84 + captions?.captionsList?.Add(name, yaw, sound.Volume); 85 + } 86 + }
+13 -1
ClosedCaptions/ClosedCaptions.csproj
··· 1 1 <Project Sdk="Microsoft.NET.Sdk"> 2 2 3 3 <PropertyGroup> 4 - <TargetFramework>net7.0</TargetFramework> 4 + <TargetFramework>net8.0</TargetFramework> 5 5 <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> 6 6 <OutputPath>bin\$(Configuration)\Mods\mod</OutputPath> 7 7 </PropertyGroup> 8 8 9 9 <ItemGroup> 10 + <Reference Include="VintagestoryLib"> 11 + <HintPath>$(VINTAGE_STORY)/VintagestoryLib.dll</HintPath> 12 + <Private>false</Private> 13 + </Reference> 10 14 <Reference Include="VintagestoryAPI"> 11 15 <HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath> 12 16 <Private>false</Private> ··· 25 29 </Reference> 26 30 <Reference Include="Newtonsoft.Json"> 27 31 <HintPath>$(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll</HintPath> 32 + <Private>False</Private> 33 + </Reference> 34 + <Reference Include="harmony"> 35 + <HintPath>$(VINTAGE_STORY)/Lib/0Harmony.dll</HintPath> 36 + <Private>False</Private> 37 + </Reference> 38 + <Reference Include="cairo"> 39 + <HintPath>$(VINTAGE_STORY)/Lib/cairo-sharp.dll</HintPath> 28 40 <Private>False</Private> 29 41 </Reference> 30 42 </ItemGroup>
-26
ClosedCaptions/ModTemplateModSystem.cs
··· 1 - using Vintagestory.API.Client; 2 - using Vintagestory.API.Common; 3 - using Vintagestory.API.Config; 4 - using Vintagestory.API.Server; 5 - 6 - namespace ModTemplate 7 - { 8 - public class ModTemplateModSystem : ModSystem 9 - { 10 - // Called on server and client 11 - public override void Start(ICoreAPI api) 12 - { 13 - Mod.Logger.Notification("Hello from template mod: " + Lang.Get("mymodid:hello")); 14 - } 15 - 16 - public override void StartServerSide(ICoreServerAPI api) 17 - { 18 - Mod.Logger.Notification("Hello from template mod server side"); 19 - } 20 - 21 - public override void StartClientSide(ICoreClientAPI api) 22 - { 23 - Mod.Logger.Notification("Hello from template mod client side"); 24 - } 25 - } 26 - }
+2 -2
ClosedCaptions/Properties/launchSettings.json
··· 2 2 "profiles": { 3 3 "Client": { 4 4 "commandName": "Executable", 5 - "executablePath": "$(VINTAGE_STORY)/Vintagestory.exe", 5 + "executablePath": "$(VINTAGE_STORY)/Vintagestory", 6 6 "commandLineArgs": "--tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"", 7 7 "workingDirectory": "$(VINTAGE_STORY)" 8 8 }, 9 9 "Server": { 10 10 "commandName": "Executable", 11 - "executablePath": "$(VINTAGE_STORY)/VintagestoryServer.exe", 11 + "executablePath": "$(VINTAGE_STORY)/VintagestoryServer", 12 12 "commandLineArgs": "--tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"", 13 13 "workingDirectory": "$(VINTAGE_STORY)" 14 14 }