Closed Captions / Subtitles for Vintage Story.
0
fork

Configure Feed

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

Refactor ModSystem, rewrite yaw calculations.

Move almost all processing out of CaptionsModSystem and into CaptionList.

+131 -104
+1 -4
ClosedCaptions/CaptionsDialog.cs
··· 22 22 public override double DrawOrder => 0.1999; 23 23 public override bool Focusable => false; 24 24 25 - public override bool ShouldReceiveMouseEvents() 26 - { 27 - return false; 28 - } 25 + public override bool ShouldReceiveMouseEvents() => false; 29 26 30 27 public override bool ShouldReceiveKeyboardEvents() 31 28 {
+128 -59
ClosedCaptions/CaptionsList.cs
··· 2 2 using Cairo; 3 3 using Vintagestory.API.MathTools; 4 4 using System; 5 + using Vintagestory.API.Common; 6 + using Vintagestory.API.Config; 5 7 6 8 namespace ClosedCaptions; 7 9 ··· 13 15 14 16 private static readonly int MAX_SOUNDS = 10; 15 17 private static readonly int MAX_AGE_SECONDS = 4; 18 + private static readonly double RANGE_THRESHOLD = 0.8; 19 + 16 20 public class Sound { 17 21 public double age = -1; 18 22 public string name; 19 23 public double textWidth; 20 - public double yaw; 24 + public Vec3f position; 21 25 public double volume; 22 26 23 27 public bool active ··· 26 30 set => age = (value) ? 0 : -1; 27 31 } 28 32 } 29 - public Sound[] soundList = new Sound[MAX_SOUNDS]; 33 + public Sound[] sounds = new Sound[MAX_SOUNDS]; 30 34 31 35 public CaptionsList(ICoreClientAPI capi, ElementBounds bounds) : base(capi, bounds) { 32 36 textTexture = new LoadedTexture(capi); 33 37 font = CairoFont.WhiteMediumText().WithFont("Lora").WithFontSize(28); 34 38 textUtil = new TextDrawUtil(); 35 - for (int i = 0; i < MAX_SOUNDS; i++) { soundList[i] = new Sound(); } 39 + for (int i = 0; i < MAX_SOUNDS; i++) { sounds[i] = new Sound(); } 36 40 } 37 41 38 42 public override void Dispose() { ··· 54 58 public void Recompose() { 55 59 ImageSurface imageSurface = new ImageSurface(Format.Argb32, 300, 320); 56 60 Context context = genContext(imageSurface); 57 - DrawText(context); 61 + DrawCaptions(context); 58 62 generateTexture(imageSurface, ref textTexture); 59 63 context.Dispose(); 60 64 imageSurface.Dispose(); ··· 64 68 for (var i = 0; i < MAX_SOUNDS; i++) 65 69 { 66 70 // Early out if we hit the end of the active sounds. 67 - if (!soundList[i].active) break; 71 + if (!sounds[i].active) break; 68 72 69 - soundList[i].age += deltaTime; 73 + sounds[i].age += deltaTime; 70 74 71 75 // Early out if this sound is still young. 72 - if (soundList[i].age < MAX_AGE_SECONDS) continue; 76 + if (sounds[i].age < MAX_AGE_SECONDS) continue; 73 77 74 78 RemoveSound(i); 75 79 } ··· 78 82 private void RemoveSound(int index) 79 83 { 80 84 for (var j = index; j < MAX_SOUNDS - 1; j++) 81 - soundList[j] = soundList[j + 1]; 82 - soundList[MAX_SOUNDS - 1] = new Sound(); 85 + sounds[j] = sounds[j + 1]; 86 + sounds[MAX_SOUNDS - 1] = new Sound(); 83 87 } 84 88 85 - private void DrawText(Context ctx) 89 + private void DrawCaptions(Context ctx) 86 90 { 87 91 font.SetupContext(ctx); 88 92 //ctx.SetSourceRGBA(0, .1, .5, 0.75); 89 - //ctx.Rectangle(0, 0, 300, 450); 93 + //ctx.Rectangle(0, 0, 300, 320); 90 94 //ctx.Fill(); 91 95 92 96 double y = 32 * MAX_SOUNDS; 93 - foreach (var sound in soundList) 97 + var playerPos = api.World.Player.Entity.Pos; 98 + 99 + foreach (var sound in sounds) 94 100 { 95 - y -= 32; 96 101 if (!sound.active) continue; 102 + y -= 32; 97 103 98 104 var brightness = ((1 - (sound.age / MAX_AGE_SECONDS)) * Math.Max(1, sound.volume) / 2 + 0.5); 99 105 ··· 116 122 ctx.SetSourceRGB(brightness, brightness, brightness); 117 123 } 118 124 textUtil.DrawTextLine(ctx, font, soundName, 150 - sound.textWidth / 2, y+6); 119 - 120 - if (Double.IsNaN(sound.yaw)) continue; 125 + 126 + if (sound.position == null || sound.position.IsZero) continue; 127 + 128 + var dist = sound.position.DistanceTo(playerPos.XYZFloat); 129 + var yaw = Math.Atan2(sound.position.Z - playerPos.Z, sound.position.X - playerPos.X); 130 + 131 + if (dist < 2) continue; 121 132 122 - var direction = GameMath.Mod((sound.yaw + api.World.Player.CameraYaw) / GameMath.TWOPI * 12, 12); 123 - if (direction > 2 && direction < 4) 133 + // 0 is directly in front of the player 134 + // ±4 is directly left/right of the player, respectively 135 + // ±8 is directly behind the player 136 + var direction = GameMath.Mod((yaw + api.World.Player.CameraYaw) / GameMath.TWOPI * 16 + 4, 16) - 8; 137 + 138 + api.Logger.Debug("[Captions] Sound: " + sound.name + " direction: " + direction); 139 + 140 + // BEHIND YOU 141 + if (Math.Abs(direction) > 6) 124 142 { 143 + // d=r*4*(sqrt(2)-1)/3 125 144 ctx.NewPath(); 126 - ctx.MoveTo(292, y+15); 127 - ctx.LineTo(277, y+15 - 13); 128 - ctx.LineTo(277, y+15 + 13); 129 - ctx.LineTo(292, y+15); 130 - ctx.MoveTo(280, y+15); 131 - ctx.LineTo(265, y+15 - 13); 132 - ctx.LineTo(265, y+15 + 13); 133 - ctx.LineTo(280, y+15); 134 - ctx.ClosePath(); 145 + ctx.Arc(26, y+16, 4, 0, GameMath.TWOPI); 135 146 ctx.Fill(); 147 + ctx.NewPath(); 148 + ctx.Arc(274, y+16, 4, 0, GameMath.TWOPI); 149 + ctx.Fill(); 150 + } 151 + // >> 152 + else if (direction > 3) 153 + { 154 + DrawTriangle(ctx, 292, y + 16, 15, 26); 155 + DrawTriangle(ctx, 280, y + 16, 15, 26); 136 156 //textUtil.DrawTextLine(ctx, font, ">>", 270, y+4); 137 157 } 138 - else if (direction > 1 && direction < 5) 158 + // > 159 + else if (direction > 1) 139 160 { 140 - ctx.NewPath(); 141 - ctx.MoveTo(280, y+15); 142 - ctx.LineTo(265, y+15 - 13); 143 - ctx.LineTo(265, y+15 + 13); 144 - ctx.LineTo(280, y+15); 145 - ctx.Fill(); 161 + DrawTriangle(ctx, 280, y + 16, 15, 26); 146 162 //textUtil.DrawTextLine(ctx, font, ">", 270, y+4); 147 163 } 148 - else if (direction > 8 && direction < 10) 164 + // << 165 + else if (direction < -3) 149 166 { 150 - ctx.NewPath(); 151 - ctx.MoveTo(13, y+15); 152 - ctx.LineTo(28, y+15 - 13); 153 - ctx.LineTo(28, y+15 + 13); 154 - ctx.LineTo(13, y+15); 155 - ctx.MoveTo(25, y+15); 156 - ctx.LineTo(40, y+15 - 13); 157 - ctx.LineTo(40, y+15 + 13); 158 - ctx.LineTo(25, y+15); 159 - ctx.ClosePath(); 160 - ctx.Fill(); 167 + DrawTriangle(ctx, 25, y + 16, -15, 26); 168 + DrawTriangle(ctx, 13, y + 16, -15, 26); 161 169 //textUtil.DrawTextLine(ctx, font, "<<", 30, y+4); 162 170 } 163 - else if (direction > 7 && direction < 11) 171 + // < 172 + else if (direction < -1) 164 173 { 165 - ctx.NewPath(); 166 - ctx.MoveTo(25, y+15); 167 - ctx.LineTo(40, y+15 - 13); 168 - ctx.LineTo(40, y+15 + 13); 169 - ctx.LineTo(25, y+15); 170 - ctx.Fill(); 174 + DrawTriangle(ctx, 25, y + 16, -15, 26); 171 175 //textUtil.DrawTextLine(ctx, font, "<", 30, y+4); 172 176 } 173 177 } 174 178 } 179 + 180 + public void DrawTriangle(Context ctx, double x, double y, double w, double h) 181 + { 182 + ctx.NewPath(); 183 + ctx.MoveTo(x, y); 184 + ctx.LineTo(x - w, y - h/2); 185 + ctx.LineTo(x - w, y + h/2); 186 + ctx.LineTo(x, y); 187 + ctx.Fill(); 188 + } 189 + 190 + public void ProcessSound(SoundParams sound) 191 + { 192 + api.Logger.Debug("[Captions] Sound: " + sound.Location.Path); 193 + // Unknown condition yoinked without understanding from SubtitlesMod 194 + var player = api.World.Player; 195 + if (player == null) return; 196 + 197 + // Ignore music 198 + if (sound.SoundType == EnumSoundType.Music) return; 199 + 200 + // Calculate sound ID 201 + var path = sound.Location.Path; 202 + var id = path.StartsWith("sounds/") && path.EndsWith(".ogg") ? path.Substring(7, path.Length - 7 - 4) : path; 203 + // Strip trailing digit 204 + var lastChar = id.ToCharArray(id.Length - 1, 1)[0]; 205 + if (lastChar >= '0' && lastChar <= '9') 206 + id = id.Substring(0, id.Length - 1); 207 + 208 + var name = Lang.GetIfExists("captions:" + id); 209 + // Ignore specified sounds 210 + if (name == "") return; 211 + // Unnamed sounds 212 + if (name == null) name = id; 213 + 214 + api.Logger.Debug("[Captions] Sound name: " + name); 215 + 216 + var position = sound.Position; 217 + if (position == null || position.IsZero) 218 + { 219 + AddSound(name, null, sound.Volume); 220 + return; 221 + } 222 + 223 + var playerPos = player.Entity.Pos.AsBlockPos; 224 + var dx = sound.Position.X - playerPos.X; 225 + var dy = sound.Position.Y - playerPos.Y; 226 + var dz = sound.Position.Z - playerPos.Z; 227 + var dist = Math.Sqrt(dx * dx + dy * dy + dz * dz); 228 + 229 + // Ignore sounds that are out of range. 230 + if (dist > sound.Range * RANGE_THRESHOLD) return; 231 + 232 + api.Logger.Debug("[Captions] Blessed: " + name); 233 + 234 + AddSound(name, sound.Position, sound.Volume); 235 + foreach (var s in sounds) 236 + { 237 + if (!s.active) continue; 238 + api.Logger.Debug($"[Captions] Active: {s.name} age={s.age:0.00}s"); 239 + } 240 + } 175 241 176 - public void AddSound(string name, double yaw, double volume) 242 + public void AddSound(string name, Vec3f position, double volume) 177 243 { 178 244 // Refresh existing slot if it's already present. 179 - foreach (var sound in soundList) 245 + foreach (var sound in sounds) 180 246 { 181 247 if (sound.active && sound.name == name) 182 248 { 249 + api.Logger.Debug("[Captions] Refreshed: " + name); 183 250 sound.age = 0; 184 - sound.yaw = yaw; 251 + sound.position = position; 185 252 sound.volume = volume; 186 253 return; 187 254 } 188 255 } 189 256 190 257 // Fill an empty slot. 191 - foreach (var sound in soundList) 258 + foreach (var sound in sounds) 192 259 { 193 260 if (sound.active) continue; 261 + api.Logger.Debug("[Captions] Added: " + name); 194 262 sound.age = 0; 195 263 sound.name = name; 196 264 font.SetupContext(CairoFont.FontMeasuringContext); 197 265 sound.textWidth = CairoFont.FontMeasuringContext.TextExtents(sound.name).Width; 198 - sound.yaw = yaw; 266 + sound.position = position; 199 267 sound.volume = volume; 200 268 return; 201 269 } ··· 204 272 int oldestSound = 0; 205 273 for (var i = 1; i < MAX_SOUNDS; i++) 206 274 { 207 - if (soundList[i].age > soundList[oldestSound].age) 275 + if (sounds[i].age > sounds[oldestSound].age) 208 276 { 209 277 oldestSound = i; 210 278 } 211 279 } 280 + api.Logger.Debug("[Captions] Recycled: " + sounds[oldestSound].name + " -> " + name); 212 281 RemoveSound(oldestSound); 213 - AddSound(name, yaw, volume); 282 + AddSound(name, position, volume); 214 283 } 215 284 }
+2 -41
ClosedCaptions/CaptionsModSystem.cs
··· 18 18 { 19 19 base.StartClientSide(capi); 20 20 21 + Patch_ClientMain_StartPlaying.capi = capi; 21 22 harmony = new Harmony(Mod.Info.ModID); 22 23 harmony.PatchAll(Assembly.GetExecutingAssembly()); 23 - Patch_ClientMain_StartPlaying.capi = capi; 24 24 25 25 capi.Event.IsPlayerReady += (ref EnumHandling handling) => 26 26 { ··· 42 42 43 43 public static void Prefix(ILoadedSound loadedSound, AssetLocation location) 44 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?.AddSound(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?.AddSound(name, yaw, sound.Volume); 45 + captions?.captionsList?.ProcessSound(loadedSound.Params); 85 46 } 86 47 }