@@ -28,8 +28,8 @@ namespace GeneralUpdate.Core.Download;
2828/// if the current client version is below the minimum, the package is skipped.</description></item>
2929/// </list>
3030/// <para>
31- /// Note: This builder does not distinguish between cross-version and in-order updates;
32- /// each package carries its own <c>IsCrossVersion</c> metadata for downstream processing .
31+ /// All packages are treated uniformly; the builder evaluates chain vs full packages
32+ /// based on total download size .
3333/// </para>
3434/// </remarks>
3535public static class DownloadPlanBuilder
@@ -136,41 +136,90 @@ public static DownloadPlan Build(
136136
137137 if ( candidates . Count == 0 ) return DownloadPlan . Empty ;
138138
139- // ── CVP-first selection ──
140- // If a matching cross-version package (CVP) exists whose FromVersion
141- // equals the client's current version, prefer it over chain packages.
142- // This gives the client a single-package shortcut from old → latest.
143- // Prefer the CVP with the highest target version when multiple CVPs match.
144- var matchingCvp = candidates
145- . Where ( a => a . IsCrossVersion )
146- . Where ( a =>
139+ // Separate chain vs full packages
140+ var chainCandidates = candidates
141+ . Where ( a => a . PackageType == ( int ) Configuration . PackageType . Chain )
142+ . ToList ( ) ;
143+
144+ var fullCandidates = candidates
145+ . Where ( a => a . PackageType == ( int ) Configuration . PackageType . Full )
146+ . ToList ( ) ;
147+
148+ // ── Chain vs Full size-based decision ──
149+ // If a full replacement package is available and the total chain download
150+ // size approaches or exceeds the full package size, skip chain and use full.
151+ if ( chainCandidates . Count > 0 && fullCandidates . Count > 0 )
152+ {
153+ // Pick the latest full package (highest version) across all AppTypes
154+ var bestFull = fullCandidates
155+ . OrderByDescending ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } )
156+ . First ( ) ;
157+
158+ long chainTotal = chainCandidates . Sum ( a => a . Size ) ;
159+ var threshold = ( long ) ( bestFull . Size * 0.8 ) ;
160+
161+ if ( chainTotal >= threshold )
147162 {
148- if ( ! Semver . TryParse ( a . FromVersion , out var fromVer ) ) return false ;
149- var localVersion = ( a . AppType == ( int ) AppType . Upgrade )
150- ? uv
151- : cv ;
152- return fromVer == localVersion ;
153- } )
154- . OrderByDescending ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } )
155- . FirstOrDefault ( ) ;
163+ // Chain is too expensive — use full package instead.
164+ // Supplement with chain packages for other AppTypes not covered by full.
165+ GeneralTracer . Info ( $ "DownloadPlanBuilder: chain total { chainTotal } >= 80% of full size { bestFull . Size } , switching to full package { bestFull . Name } ") ;
166+ var planAssets = new List < DownloadAsset > { bestFull } ;
167+ planAssets . AddRange ( chainCandidates
168+ . Where ( a => a . AppType != bestFull . AppType
169+ || ( Semver . TryParse ( a . Version , out var av )
170+ && Semver . TryParse ( bestFull . Version , out var fv )
171+ && av > fv ) )
172+ . OrderBy ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } ) ) ;
173+ return new DownloadPlan ( planAssets , isForcibly ) ;
174+ }
175+ }
156176
157- if ( matchingCvp != null )
177+ // ── Chain plan with fallback fulls ──
178+ // Use chain packages normally. Attach FallbackFull* info to each chain entry
179+ // so that if a chain patch fails, AbstractStrategy can fall back to full.
180+ if ( fullCandidates . Count > 0 )
158181 {
159- // CVP covers one AppType in a single hop. Still need chain packages
160- // for other AppTypes, and for the same AppType beyond the CVP's target.
161- var cvpAppType = matchingCvp . AppType ;
162- Semver . TryParse ( matchingCvp . Version , out var cvpVersion ) ;
163- var planAssets = new List < DownloadAsset > { matchingCvp } ;
164- planAssets . AddRange ( candidates
165- . Where ( a => ! a . IsCrossVersion )
166- . Where ( a => a . AppType != cvpAppType
167- || ( Semver . TryParse ( a . Version , out var av ) && av > cvpVersion ) )
168- . OrderBy ( a => { Semver . TryParse ( a . Version , out var sv ) ; return sv ; } ) ) ;
169- return new DownloadPlan ( planAssets , isForcibly ) ;
182+ var fallbackFulls = new List < DownloadAsset > ( ) ;
183+
184+ var chainWithFallback = chainCandidates
185+ . Select ( chain =>
186+ {
187+ // Find a matching full: same AppType + same Version (or closest)
188+ var match = fullCandidates
189+ . Where ( f => f . AppType == chain . AppType )
190+ . OrderBy ( f => { Semver . TryParse ( f . Version , out var sv ) ; return sv ; } )
191+ . FirstOrDefault ( f =>
192+ {
193+ if ( ! Semver . TryParse ( f . Version , out var fv ) ) return false ;
194+ if ( ! Semver . TryParse ( chain . Version , out var cv ) ) return false ;
195+ return fv >= cv ;
196+ } ) ;
197+
198+ if ( match != null )
199+ {
200+ // Add matching full to the fallback list once
201+ if ( ! fallbackFulls . Any ( f => f . Url == match . Url ) )
202+ fallbackFulls . Add ( match ) ;
203+
204+ return chain with
205+ {
206+ FallbackFullName = match . Name ,
207+ FallbackFullUrl = match . Url ,
208+ FallbackFullHash = match . SHA256
209+ } ;
210+ }
211+ return chain ;
212+ } )
213+ . ToList ( ) ;
214+
215+ return new DownloadPlan ( chainWithFallback , isForcibly )
216+ {
217+ FallbackFulls = fallbackFulls
218+ } ;
170219 }
171220
172- // No matching CVP: return all chain packages sorted by version (ascending)
173- return new DownloadPlan ( candidates , isForcibly ) ;
221+ // No full packages at all: return chain packages as-is
222+ return new DownloadPlan ( chainCandidates , isForcibly ) ;
174223 }
175224
176225 /// <summary>
0 commit comments