Làm thế nào là các trò chơi xác định có thể đối mặt với chủ nghĩa không xác định dấu phẩy động?


52

Để làm cho một trò chơi giống như một RTS được nối mạng, tôi đã thấy một số câu trả lời ở đây gợi ý để làm cho trò chơi hoàn toàn xác định; sau đó, bạn chỉ phải chuyển các hành động của người dùng sang nhau và làm chậm những gì hiển thị một chút để "khóa" đầu vào của mọi người trước khi khung tiếp theo được hiển thị. Sau đó, những thứ như vị trí, sức khỏe, v.v. không cần phải được cập nhật liên tục qua mạng, bởi vì mô phỏng của mọi người chơi sẽ giống hệt nhau. Tôi cũng đã nghe điều tương tự được đề xuất để thực hiện replay.

Tuy nhiên, vì các phép tính dấu phẩy động là không xác định giữa các máy hoặc thậm chí giữa các phần tổng hợp khác nhau của cùng một chương trình trên cùng một máy, điều này có thực sự khả thi không? Làm thế nào để chúng tôi ngăn chặn thực tế đó gây ra sự khác biệt nhỏ giữa những người chơi (hoặc phát lại) mà gợn trong suốt trò chơi ?

Tôi đã nghe một số người đề nghị tránh hoàn toàn các số có dấu phẩy động và sử dụng intđể biểu thị thương số của một phân số, nhưng điều đó nghe có vẻ không thực tế đối với tôi - ví dụ, nếu tôi cần lấy cosin của một góc thì sao? Tôi có thực sự cần phải viết lại toàn bộ thư viện toán học không?

Lưu ý rằng tôi chủ yếu quan tâm đến C #, theo như tôi có thể nói, có chính xác các vấn đề tương tự như C ++ về vấn đề này.


5
Đây không phải là câu trả lời cho câu hỏi của bạn, nhưng để giải quyết vấn đề kết nối mạng RTS, tôi thỉnh thoảng khuyên bạn nên đồng bộ hóa các vị trí đơn vị và sức khỏe để sửa lỗi cho bất kỳ sự trôi dạt nào. Chính xác những thông tin cần được đồng bộ hóa sẽ phụ thuộc vào trò chơi; bạn có thể tối ưu hóa việc sử dụng băng thông bằng cách không bận tâm đồng bộ hóa các thứ như hiệu ứng trạng thái.
jhocking

@jhocking Tuy nhiên, nó có lẽ là câu trả lời tốt nhất.
Jonathan Connell

Starcraft II infacti chỉ đồng bộ chính xác mỗi lần, tôi đã xem lại trò chơi phát lại với bạn bè của mình mỗi máy tính cạnh nhau và ở đó có sự khác biệt (cũng có ý nghĩa) đặc biệt là độ trễ cao (1 giây). Tôi đã có một số đơn vị mà 6/7 hình vuông bản đồ đi xa trên màn hình của tôi đối với chính anh ấy
Nhà phát triển GameD

Câu trả lời:


15

Là dấu phẩy động xác định?

Tôi đã đọc rất nhiều về vấn đề này vài năm trước khi tôi muốn viết một RTS bằng cách sử dụng cùng một kiến ​​trúc khóa bước bạn làm.

Kết luận của tôi về các điểm nổi phần cứng là:

  • Mã lắp ráp gốc giống nhau rất có thể là xác định miễn là bạn cẩn thận với các cờ điểm và cài đặt trình biên dịch.
  • Có một dự án RTS mã nguồn mở tuyên bố rằng họ có các trình biên dịch C / C ++ xác định trên các trình biên dịch khác nhau bằng thư viện trình bao bọc. Tôi đã không xác minh yêu cầu đó. (Nếu tôi nhớ chính xác thì đó là về STREFLOPthư viện)
  • JIT .net được cho phép khá nhiều thời gian. Đặc biệt, nó được phép sử dụng độ chính xác cao hơn yêu cầu. Ngoài ra, nó sử dụng các bộ hướng dẫn khác nhau trên x86 và AMD64 (Tôi nghĩ trên x86, nó sử dụng x87, AMD64, nó sử dụng một số lệnh SSE có hành vi khác nhau đối với các biến thể).
  • Các hướng dẫn phức tạp (bao gồm hàm lượng giác, hàm mũ, logarit) đặc biệt có vấn đề.

Tôi đã kết luận rằng không thể sử dụng các loại dấu phẩy động được xây dựng trong .net một cách xác định.

Cách giải quyết có thể

Vì vậy, tôi cần cách giải quyết. Tôi đã xem xét:

  1. Thực hiện FixedPoint32trong C #. Mặc dù điều này không quá khó (tôi đã thực hiện xong một nửa) nhưng phạm vi giá trị rất nhỏ khiến nó khó chịu khi sử dụng. Bạn phải cẩn thận mọi lúc để bạn không bị tràn, cũng không mất quá nhiều độ chính xác. Cuối cùng, tôi thấy điều này không dễ hơn là sử dụng số nguyên trực tiếp.
  2. Thực hiện FixedPoint64trong C #. Tôi thấy điều này khá khó để làm. Đối với một số hoạt động, số nguyên trung gian 128 bit sẽ hữu ích. Nhưng .net không cung cấp loại như vậy.
  3. Sử dụng mã riêng cho các phép toán có tính xác định trên một nền tảng. Phát sinh chi phí của một cuộc gọi đại biểu cho mọi hoạt động toán học. Mất khả năng chạy đa nền tảng.
  4. Sử dụng Decimal. Nhưng nó chậm, chiếm nhiều bộ nhớ và dễ dàng đưa ra các ngoại lệ (chia cho 0, tràn). Nó rất tốt cho sử dụng tài chính, nhưng không phù hợp cho các trò chơi.
  5. Thực hiện một dấu phẩy động 32 bit tùy chỉnh. Nghe có vẻ khá khó khăn lúc đầu. Việc thiếu một BitScanReverse nội tại gây ra một vài phiền toái khi thực hiện điều này.

SoftFloat của tôi

Lấy cảm hứng từ bài đăng của bạn trên StackOverflow , tôi mới bắt đầu triển khai loại dấu phẩy động 32 bit trong phần mềm và kết quả rất hứa hẹn.

  • Biểu diễn bộ nhớ tương thích nhị phân với các float của IEEE, vì vậy tôi có thể diễn giải lại việc truyền khi xuất chúng thành mã đồ họa.
  • Nó hỗ trợ SubNorms, infinity và NaNs.
  • Các kết quả chính xác không giống với kết quả của IEEE, nhưng điều đó thường không quan trọng đối với các trò chơi. Trong loại mã này, điều quan trọng là kết quả là giống nhau cho tất cả người dùng, không phải là nó chính xác đến chữ số cuối cùng.
  • Hiệu suất là khá. Một thử nghiệm tầm thường cho thấy nó có thể thực hiện khoảng 75MFLOPS so với 220-260MFLOPS với floatphép cộng / nhân ( Chuỗi đơn trên i3 2,66GHz). Nếu bất cứ ai có điểm chuẩn điểm nổi tốt cho .net, vui lòng gửi chúng cho tôi, vì bài kiểm tra hiện tại của tôi rất thô sơ.
  • Làm tròn có thể được cải thiện. Hiện tại nó cắt ngắn, gần tương ứng với làm tròn về không.
  • Nó vẫn chưa hoàn thiện. Hiện tại phân chia, phôi và các hoạt động toán học phức tạp đang thiếu.

Nếu bất cứ ai muốn đóng góp kiểm tra hoặc cải thiện mã, chỉ cần liên hệ với tôi hoặc đưa ra yêu cầu kéo trên github. https://github.com/CodesInChaos/SoftFloat

Các nguồn không xác định khác

Ngoài ra còn có các nguồn không xác định khác trong .net.

  • lặp qua a Dictionary<TKey,TValue>hoặc HashSet<T>trả về các phần tử theo thứ tự không xác định.
  • object.GetHashCode() khác với chạy để chạy.
  • Việc thực hiện các lớp được xây dựng Randomlà không xác định, sử dụng của riêng bạn.
  • Đa luồng với khóa ngây thơ dẫn đến kết quả sắp xếp lại và khác nhau. Hãy rất cẩn thận để sử dụng chủ đề chính xác.
  • Khi WeakReferences mất mục tiêu của họ là không xác định bởi vì GC có thể chạy bất cứ lúc nào.

23

Câu trả lời cho câu hỏi này là từ liên kết bạn đã đăng. Cụ thể bạn nên đọc trích dẫn từ Gas Powered Games:

Tôi làm việc tại Gas Powered Games và tôi có thể nói với bạn rằng toán học dấu phẩy động có tính quyết định. Bạn chỉ cần cùng một bộ hướng dẫn và trình biên dịch và tất nhiên bộ xử lý của người dùng tuân thủ tiêu chuẩn IEEE754, bao gồm tất cả các khách hàng PC và 360 của chúng tôi. Động cơ chạy DemiGod, Chỉ huy tối cao 1 và 2 dựa trên tiêu chuẩn IEEE754. Chưa kể có lẽ tất cả các game RTS ngang hàng khác trên thị trường.

Và sau đó bên dưới là cái này:

Nếu bạn lưu trữ phát lại dưới dạng đầu vào của bộ điều khiển, chúng không thể được phát lại trên các máy có kiến ​​trúc CPU, trình biên dịch hoặc cài đặt tối ưu hóa khác nhau. Trong MotoGP, điều này có nghĩa là chúng tôi không thể chia sẻ các replay đã lưu giữa Xbox và PC.

Một trò chơi xác định sẽ chỉ mang tính quyết định khi sử dụng các tệp được biên dịch giống hệt nhau và chạy trên các hệ thống tuân thủ các tiêu chuẩn của IEEE. Mô phỏng mạng đồng bộ hóa đa nền tảng hoặc phát lại sẽ không thể.


2
Như tôi đã đề cập, tôi quan tâm chủ yếu đến C #; do JIT thực hiện biên dịch thực tế, tôi không thể đảm bảo rằng nó được "biên dịch với cùng các cài đặt tối ưu hóa" ngay cả khi chạy cùng một tệp thực thi trên cùng một máy!
BlueRaja - Daniel Pflughoeft

2
Đôi khi chế độ làm tròn được đặt khác nhau trong fpu, trên một số kiến ​​trúc, fpu có một thanh ghi bên trong lớn hơn độ chính xác để tính toán ... trong khi đó, IEEE754 không mất thời gian liên quan đến cách hoạt động của FP, nhưng nó không hoạt động ' t cụ thể làm thế nào bất kỳ chức năng toán học cụ thể nên được thực hiện, có thể phơi bày sự khác biệt kiến ​​trúc.
Stephen

4
@ 3nixios Với kiểu thiết lập đó, trò chơi không mang tính quyết định. Chỉ các trò chơi có dấu thời gian cố định mới có thể xác định. Bạn đang tranh luận một cái gì đó đã không có tính quyết định khi chạy lại một mô phỏng trên một máy duy nhất.
Tấn

4
@ 3nixios, "Tôi đã nói rằng ngay cả với dấu thời gian cố định, không có gì đảm bảo rằng dấu thời gian sẽ luôn cố định." Bạn hoàn toàn sai. Điểm của dấu thời gian cố định là luôn có cùng thời gian delta cho mỗi lần đánh dấu khi cập nhật
AttackHobo

3
@ 3nixios Sử dụng dấu thời gian cố định đảm bảo dấu thời gian sẽ được sửa vì nhà phát triển của chúng tôi không cho phép khác. Bạn đang hiểu sai về độ trễ nên được bù khi sử dụng bước thời gian cố định. Mỗi bản cập nhật trò chơi phải sử dụng cùng một thời gian cập nhật; do đó, khi kết nối của người dùng bị chậm, nó vẫn phải tính toán từng cập nhật riêng lẻ mà nó đã bỏ lỡ.
Keeblebrox

3

Sử dụng mỹ phẩm điểm cố định. Hoặc chọn một máy chủ có thẩm quyền và thỉnh thoảng đồng bộ hóa trạng thái trò chơi - đó là những gì MMORTS làm. (Ít nhất, Element of War hoạt động như thế này. Nó cũng được viết bằng C #.) Theo cách này, lỗi không có cơ hội tích lũy.


Vâng, tôi có thể biến nó thành một máy chủ-máy khách và giải quyết những vấn đề đau đầu về dự đoán và ngoại suy phía khách hàng. Câu hỏi này là về kiến ​​trúc ngang hàng, được cho là dễ dàng hơn ...
BlueRaja - Danny Pflughoeft

Chà, bạn không cần phải dự đoán theo kịch bản "tất cả khách hàng chạy cùng một mã". Vai trò của máy chủ là chỉ có thẩm quyền đồng bộ hóa.
Nevermind

Máy chủ / máy khách chỉ là một phương pháp để tạo trò chơi nối mạng. Một phương pháp phổ biến (đặc biệt là cho các trò chơi ví dụ. RTS, như C & C hoặc Starcraft) là peer-to-peer, trong đó có không có máy chủ có thẩm quyền. Để có thể làm được điều đó, tất cả các tính toán phải hoàn toàn xác định và nhất quán trên tất cả các khách hàng - do đó là câu hỏi của tôi.
BlueRaja - Daniel Pflughoeft

Chà, không giống như bạn hoàn toàn CÓ THỂ làm cho trò chơi trở nên nghiêm túc p2p mà không có máy chủ / máy khách nâng cao. Nếu bạn hoàn toàn phải, mặc dù - sau đó sử dụng điểm cố định.
Nevermind

3

Chỉnh sửa: Liên kết đến một lớp điểm cố định (Người mua hãy cẩn thận! - Tôi chưa sử dụng nó ...)

Bạn luôn có thể rơi vào số học điểm cố định . Ai đó (thực hiện một rts, không kém) đã thực hiện công việc chân trên stackoverflow .

Bạn sẽ phải trả tiền phạt hiệu suất, nhưng điều này có thể hoặc không phải là vấn đề, vì .net sẽ không đặc biệt hiệu quả ở đây vì nó sẽ không sử dụng hướng dẫn simd. Điểm chuẩn!

NB Rõ ràng ai đó ở intel dường như có một giải pháp cho phép bạn sử dụng thư viện nguyên thủy hiệu năng intel từ c #. Điều này có thể giúp véc tơ mã điểm cố định để bù cho hiệu suất chậm hơn.


decimalcũng sẽ làm việc tốt cho điều đó; nhưng, điều này vẫn có những vấn đề tương tự như khi sử dụng int- làm thế nào để tôi thực hiện trig với nó?
BlueRaja - Daniel Pflughoeft

1
Cách dễ nhất là xây dựng bảng tra cứu và gửi nó cùng với ứng dụng. Bạn có thể nội suy giữa các giá trị nếu độ chính xác là một vấn đề.
LukeN

Ngoài ra, bạn có thể thay thế các cuộc gọi trig bằng số ảo hoặc bậc bốn (nếu 3D). Đây là một lời giải thích tuyệt vời
LukeN

Điều này cũng có thể giúp đỡ.
LukeN

Tại công ty tôi từng làm việc, chúng tôi đã chế tạo một máy siêu âm sử dụng toán học điểm cố định, vì vậy nó chắc chắn sẽ làm việc cho bạn.
LukeN

3

Tôi đã làm việc trên một số danh hiệu lớn.

Câu trả lời ngắn: Có thể nếu bạn cực kỳ nghiêm khắc, nhưng có lẽ không đáng. Trừ khi bạn đang ở trên một kiến ​​trúc cố định (đọc: bảng điều khiển), nó rất khó, dễ vỡ và đi kèm với một loạt các vấn đề thứ yếu như tham gia muộn.

Nếu bạn đọc một số bài viết được đề cập, bạn sẽ lưu ý rằng trong khi bạn có thể đặt chế độ CPU, có những trường hợp kỳ lạ như khi trình điều khiển in chuyển chế độ CPU vì nó ở cùng một không gian địa chỉ. Tôi đã gặp trường hợp một ứng dụng bị khóa khung với thiết bị bên ngoài, nhưng một thành phần bị lỗi đã khiến CPU bị giảm tốc do nhiệt và báo cáo khác nhau vào buổi sáng và buổi chiều, khiến nó có hiệu lực của một máy khác sau bữa trưa.

Những khác biệt về nền tảng này nằm ở silicon chứ không phải phần mềm, vì vậy để trả lời câu hỏi của bạn, C # cũng bị ảnh hưởng. Các hướng dẫn như hợp nhất nhiều lần thêm (FMA) so với ADD + MUL thay đổi kết quả vì nó chỉ làm tròn nội bộ một lần thay vì hai lần. C cung cấp cho bạn nhiều quyền kiểm soát hơn để buộc CPU thực hiện những gì bạn muốn về cơ bản bằng cách loại trừ các hoạt động như FMA để làm cho mọi thứ trở thành "tiêu chuẩn" - nhưng với chi phí tốc độ. Nội tại dường như là dễ bị khác biệt nhất. Trong một dự án, tôi đã phải tìm ra một cuốn sách acos 150 năm tuổi để lấy các giá trị để so sánh để xác định CPU nào là "đúng". Nhiều CPU sử dụng xấp xỉ đa thức cho các hàm trig nhưng không phải lúc nào cũng có cùng hệ số.

Đề nghị của tôi:

Bất kể bạn đi theo bước số nguyên hay đồng bộ hóa, hãy xử lý các cơ chế trò chơi cốt lõi tách biệt với bản trình bày. Làm cho trò chơi chơi chính xác, nhưng đừng lo lắng về độ chính xác trong lớp trình bày. Cũng cần nhớ rằng bạn không cần phải gửi tất cả dữ liệu thế giới được nối mạng ở cùng tốc độ khung hình. Bạn có thể ưu tiên tin nhắn của bạn. Nếu mô phỏng của bạn trùng khớp 99,999%, bạn sẽ không cần truyền tải thường xuyên để được ghép nối. (Ngăn chặn gian lận sang một bên.)

Có một bài viết hay về Công cụ nguồn giải thích một cách để đồng bộ hóa: https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking

Hãy nhớ rằng, nếu bạn quan tâm đến việc tham gia muộn, dù sao bạn cũng sẽ phải cắn viên đạn và đồng bộ hóa.


1

Lưu ý rằng tôi chủ yếu quan tâm đến C #, theo như tôi có thể nói, có chính xác các vấn đề tương tự như C ++ về vấn đề này.

Có, C # có cùng các vấn đề với C ++. Nhưng nó cũng có nhiều hơn nữa.

Ví dụ: lấy tuyên bố này từ Shawn Hawgraves:

Nếu bạn lưu trữ phát lại dưới dạng đầu vào của bộ điều khiển, chúng không thể được phát lại trên các máy có kiến ​​trúc CPU, trình biên dịch hoặc cài đặt tối ưu hóa khác nhau.

Đó là "đủ dễ dàng" để đảm bảo rằng điều này xảy ra trong C ++. Tuy nhiên, trong C # , điều đó sẽ khó giải quyết hơn rất nhiều. Điều này là nhờ JIT.

Bạn cho rằng điều gì sẽ xảy ra nếu trình thông dịch chạy mã của bạn được thông dịch một lần, nhưng sau đó JIT lại là lần thứ hai? Hoặc có thể nó diễn giải nó hai lần trên máy của người khác, nhưng sau đó thì JIT?

JIT không mang tính quyết định, bởi vì bạn có rất ít quyền kiểm soát nó. Đây là một trong những điều bạn từ bỏ để sử dụng CLR.

Và Chúa sẽ giúp bạn nếu một người đang sử dụng .NET 4.0 để chạy trò chơi của bạn, trong khi một người khác đang sử dụng Mono CLR (tất nhiên là sử dụng các thư viện .NET). Ngay cả .NET 4.0 so với .NET 5.0 cũng có thể khác. Bạn chỉ cần kiểm soát nhiều hơn các chi tiết cấp thấp của một nền tảng để đảm bảo loại điều này.

Bạn nên có thể thoát khỏi môn toán điểm cố định. Nhưng đó là về nó.


Ngoài các môn toán, còn điều gì khác có thể khác biệt? Có lẽ, các hành động "đầu vào" đang được gửi dưới dạng các hành động logic trong một rts. Ví dụ: Di chuyển đơn vị "a" đến vị trí "b". Tôi có thể thấy một vấn đề với việc tạo số ngẫu nhiên, nhưng điều đó rõ ràng cũng cần phải được gửi dưới dạng đầu vào mô phỏng.
LukeN

@LukeN: Vấn đề là vị trí của Shawn cho thấy mạnh mẽ rằng sự khác biệt của trình biên dịch có thể có tác động thực sự đối với toán học dấu phẩy động. Anh ấy sẽ biết nhiều hơn tôi; Tôi chỉ đơn giản là ngoại suy cảnh báo của anh ấy ra ngoài lãnh vực biên dịch / giải thích JIT và C #.
Nicol Bolas

C # có quá tải toán tử và cũng có loại tích hợp cho toán học điểm cố định. Tuy nhiên, điều này có cùng các vấn đề như sử dụng một int- làm thế nào để tôi thực hiện trig với nó?
BlueRaja - Daniel Pflughoeft

1
> Bạn cho rằng điều gì sẽ xảy ra nếu trình thông dịch chạy mã của bạn> được giải thích một lần, nhưng sau đó JIT'd lần thứ hai? Hoặc có thể nó> diễn giải nó hai lần trên máy của người khác, nhưng sau đó thì JIT? 1. Mã CLR luôn được ghi lại trước khi thực hiện. 2. .net sử dụng IEEE 754 cho phao tất nhiên. > JIT không mang tính quyết định, bởi vì bạn có rất ít quyền kiểm soát nó. Bạn kết luận là nguyên nhân khá yếu của tuyên bố sai. > Và Chúa sẽ giúp bạn nếu một người đang sử dụng .NET 4.0 để chạy trò chơi của bạn,> trong khi một người khác đang sử dụng Mono CLR (sử dụng các thư viện .NET của> khóa học). Ngay cả .NET
Romanenkov Andrey

@Romanenkov: "Bạn kết luận là nguyên nhân khá yếu của tuyên bố sai." Xin vui lòng, cho tôi biết những tuyên bố là sai và tại sao, để chúng có thể được sửa chữa.
Nicol Bolas

1

Tôi nhận ra sau khi viết câu trả lời này rằng nó không thực sự trả lời câu hỏi, mà cụ thể là về thuyết không phổ biến điểm nổi. Nhưng có lẽ điều này hữu ích với anh ta nếu anh ta sẽ tạo ra một trò chơi nối mạng theo cách này.

Cùng với việc chia sẻ đầu vào mà bạn đang phát cho tất cả người chơi, việc tạo và phát một tổng kiểm trạng thái trò chơi quan trọng, chẳng hạn như vị trí người chơi, sức khỏe, v.v. Khi xử lý đầu vào, hãy khẳng định rằng trạng thái trò chơi sẽ kiểm tra tất cả người chơi từ xa đang đồng bộ. Bạn được bảo đảm không có lỗi đồng bộ hóa (OOS) để khắc phục và điều này sẽ giúp bạn dễ dàng hơn - bạn sẽ có một thông báo trước đó rằng có lỗi xảy ra (sẽ giúp bạn tìm ra các bước tái tạo) và bạn có thể thêm nhiều hơn nữa trạng thái trò chơi đăng nhập vào mã nghi ngờ để cho phép bạn đóng khung bất cứ điều gì gây ra OOS.


0

Tôi nghĩ rằng ý tưởng từ blog được liên kết đến vẫn cần đồng bộ hóa định kỳ là khả thi - tôi đã thấy đủ lỗi trong các trò chơi RTS được nối mạng không áp dụng phương pháp đó.

Mạng bị mất, chậm, có độ trễ và thậm chí có thể làm ô nhiễm dữ liệu của bạn. "Chủ nghĩa xác định dấu phẩy động", (nghe có vẻ khó hiểu đến mức khiến tôi hoài nghi) là điều ít lo lắng nhất của bạn trong thực tế ... đặc biệt nếu bạn sử dụng bước thời gian cố định. với các bước thời gian thay đổi, bạn sẽ cần phải nội suy giữa các bước thời gian cố định để tránh các vấn đề xác định. Tôi nghĩ rằng đây thường là những gì có nghĩa là hành vi "dấu phẩy động" không xác định - chỉ các bước thời gian biến đổi đó gây ra sự tích hợp để phân kỳ - không liên quan gì đến các thư viện toán học hoặc các hàm cấp thấp.

Đồng bộ hóa là chìa khóa mặc dù.


-2

Bạn làm cho họ xác định. Để có một ví dụ tuyệt vời, hãy xem Bản trình bày GDC của Dungeon Siege về cách họ làm cho các địa điểm trên thế giới có thể kết nối mạng.

Cũng nên nhớ rằng tính xác định cũng áp dụng cho các sự kiện 'ngẫu nhiên'!


1
Đây là những gì OP muốn biết làm thế nào, với điểm nổi.
Vịt Cộng sản
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.